/* ==========================================================================
   Landing page — "title block on a sheet." Shares tokens (--ink, --bg,
   --line, --side, --font) from reference-layout.css, loaded first.

   Mobile-portrait-first: everything below with no media query IS the
   phone-in-hand layout (majority of visitors, per Liam). Width-based
   from there up, not orientation-based — three min-width tiers (768/
   1024/1280px, see "Wide footer" further down) progressively widen the
   footer from phone through tablet to desktop, per Liam's
   phone->tablet->desktop overhaul. This replaced an earlier
   orientation-based version (@media (orientation: landscape)) that
   read a rotated phone as "enhance to desktop" but had no way to
   distinguish a portrait tablet — genuinely wide, just not
   landscape — from a portrait phone, so it fell through to the same
   compact mobile layout as a 375px screen. Old version tagged at
   footer-mobile-only-backup if anything here needs comparing back.
   ========================================================================== */

/* Three-size type scale for the whole title block, per Liam: logo
   large, titles medium, everything else small. Named tokens so the
   hierarchy stays a deliberate 3-step system rather than one-off rem
   values creeping back in as the page evolves.

   "Titles" = the section labels (Selected Work, Journal, Scale
   1:100, Contact) at the size/weight/caps they already had — Liam
   confirmed he likes that as the medium tier as-is. "Text" = the
   actual content under each title (project names, journal entry
   names, contact details) — smaller than the label above it, not
   bigger, which is the inverse of a typical heading/body relationship
   but matches the reference sheet's quiet, caption-driven feel. */
:root {
  --tb-size-lg: 1.375rem;
  --tb-size-md: 0.625rem;
  --tb-size-sm: 0.5625rem;
}

.sheet-body {
  /* Still needed here for .sheet__canvas's own landscape padding
     (further down this file) — that element is a SIBLING of
     .titleblock, not a descendant, so it can't inherit the footer's
     own --side re-declaration (see .titleblock below). Redundant with
     that re-declaration wherever both apply (same value, harmless);
     necessary here specifically because the canvas doesn't. */
  --side: 0.875rem;
  /* position:fixed, not just overflow:hidden — this page fits the
     viewport exactly and must NEVER pan, but mobile Safari famously
     ignores overflow:hidden on <body> for page panning, which let the
     whole sheet scroll up past the footer and reveal blank space below
     it on Liam's phone (desktop never reproduced it: overflow:hidden
     is honored there). A fixed-position body is the standard
     scroll-lock that actually holds on iOS, and overscroll-behavior
     stops rubber-band bounce from chaining.

     height is the real story, through two rounds: first dropped
     entirely in favour of bare inset:0 (reasoning: inset:0 alone pins
     all four edges exactly, no viewport-unit math needed) — but Liam
     could still drag/rubber-band the page and watch the footer's
     hairline visibly travel down to the TRUE bottom before springing
     back once released. That's the signature of a known iOS Safari
     bug: position:fixed's own containing block resolves against the
     LAYOUT viewport, not the VISUAL viewport (what's actually on
     screen) — inset:0 alone can't fix that, because the bug isn't in
     the height math, it's in which viewport iOS measures fixed
     positioning against in the first place. --vvh (set by
     viewport-height-fix.js via the window.visualViewport API) is a
     live, accurate visual-viewport height, and fixed THIS symptom —
     but a real diagnostic overlay on Liam's actual phone (turned out
     to be an installed home-screen PWA, not a Safari tab — the
     "Safari toolbar" theory above was chasing the wrong culprit)
     showed innerHeight/visualViewport.height/--vvh/body's own
     rendered bottom all agreeing exactly (894 = 894 = 894 = 894): the
     CSS was doing precisely what it was told, and what it was told
     was still short of the true screen. In standalone PWA display
     mode specifically, visualViewport.height EXCLUDES the bottom
     safe-area strip (env(safe-area-inset-bottom), measured 34px)
     rather than including it — theorized that .titleblock__row-main
     already reserves that same 34px as padding to keep its own TEXT
     clear of the home indicator, so adding it back here too should
     close the gap. REVERTED — on Liam's actual phone this added a
     dead 34px strip at the very bottom without closing the original
     gap at all, AND broke the hamburger menu (its expanded Journal/
     Portal/Copyright row got clipped, text cut off) — so the extra
     34px wasn't landing where the flex math above assumes it should,
     for a reason not yet understood. Back to plain var(--vvh, 100dvh)
     until there's a real fix rather than another guess; --vvh alone
     is at least a known-neutral state (fixed the drag/rubber-band
     symptom, broke nothing else). */
  position: fixed;
  inset: 0;
  height: var(--vvh, 100dvh);
  overflow: hidden;
  overscroll-behavior: none;
}
/* ---- Drawing title strip ----
   Sits between the canvas and the footer, where a sheet carries its drawing
   name and scale. flex:0 0 auto so it takes its height out of the canvas,
   which is the only flexible track - the page fits the viewport exactly and
   nothing here may grow the column.

   Left inset is --side, the same value the footer re-declares for itself, so
   the line starts on the wordmark's own margin rather than the canvas frame's
   wider inset. */
.sheet__caption {
  /* Fills the canvas's bottom inset exactly - the band between the frame's
     bottom hairline and the footer - and centres the line in it. Two things
     follow, both of which the previous flow-item version got wrong:

     the band goes back to var(--canvas-top), so it matches the top band by
     construction again on any device, notch or none; and the gap above the
     line equals the gap below it for free, because the text is centred in a
     box whose edges ARE those two references. No offset to hand-tune, and
     nothing to re-tune when the type size changes. */
  position: absolute;
  left: var(--side);
  right: var(--side);
  bottom: 0;
  height: var(--canvas-top);
  display: flex;
  align-items: center;
  margin: 0;
  /* Centred in the band, then lifted, per Liam. The padding shortens the
     box from the bottom, so centring resolves half of it higher up - it
     rides toward the drawing rather than sitting dead centre, which is
     where a sheet's title usually sits relative to its margin.

     Kept as a bottom padding rather than a negative offset so the box still
     ENDS on the footer's edge: that edge stays the reference the line is
     measured from, and the lift is the only tuned number here. */
  padding: 0 0 0.55rem;
}

/* Back to one line, per Liam. The band is fixed at var(--canvas-top), so a
   second line never really fit it anyway - it only worked by tightening the
   leading, which is a sign the band was being asked for something it does
   not have. The type itself is .dwg in reference-layout.css, shared with
   every framed page's own strip. */

.sheet {
  display: flex;
  flex-direction: column;
  /* 100%, not 100dvh — inherits .sheet-body's own exact, inset:0-
     derived height instead of computing its own independent (and
     possibly slightly different) dvh value. See .sheet-body's own
     comment above for why that gap matters here specifically. */
  height: 100%;
}

/* ---- Theme toggle ----
   The one control on this page: flips the whole palette black-on-white
   vs white-on-black (see reference-layout.css :root vs [data-theme]).
   Lives inline in the Scale & Theme column now (was fixed top-right of
   the whole viewport) — overrides reference-layout.css's base
   .theme-toggle, which assumes an absolutely-positioned home inside
   .ref-nav. A second toggle (adjustable element scale, for
   readability) is a deliberately deferred idea — not built yet. */
.theme-toggle--inline {
  position: static;
  /* reference-layout.css's base .theme-toggle sets transform:
     translateY(-50%) as half of a position:absolute vertical-
     centering trick (top:50% + this) for its original nav-bar home.
     position:static above cancels the "top:50%" half but NOT this —
     transform doesn't get reset just because position changed — so it
     was still shifting the switch up by half its own height (~6.7px)
     for no reason in this static/grid context. Root cause of the
     scale-bar-line-vs-toggle vertical misalignment two commits ago;
     fixing the row-track sizing didn't touch this, which is why that
     fix had zero effect on the measured offset. */
  transform: none;
  margin: 0;
  padding: 0;
}
/* --ink-soft (the muted "text"/caption tone used everywhere nearby —
   Menu: Large, Theme: Light, etc.) rather than the bolder var(--ink)
   the scale-bar line itself uses — per Liam, var(--ink) still read too
   dark/heavy against the surrounding soft-toned UI. Track never fills
   solid on check (overriding reference-layout.css's checked-state
   track-background swap) so the outline+circle look stays consistent
   in both toggle states. */
.theme-toggle--inline .theme-toggle__track {
  border-color: var(--ink-soft);
  /* Same weight as the scale bar and the north point's circle - see
     --hairline in reference-layout.css. */
  border-width: var(--hairline);
  background: transparent;
}
.theme-toggle--inline .theme-toggle__thumb {
  background: var(--ink-soft);
}
.theme-toggle--inline[aria-checked="true"] .theme-toggle__track {
  background: transparent;
}
.theme-toggle--inline[aria-checked="true"] .theme-toggle__thumb {
  background: var(--ink-soft);
}

/* ---- Canvas: the "drawing" ----
   Most of the viewport, quiet like a real sheet — object-fit:contain
   (not cover) so a drawing never gets cropped or distorted.

   Insets are var(--side) on every edge (was an ad-hoc 1.5rem/1rem), per
   Liam's site-wide frame: the same gap the footer keeps from the screen
   edges, on all four sides of every page's content. This page never
   scrolls, so plain padding already behaves like the project pages'
   persistent margin-frame (.project-scroll in reference-layout.css). */
.sheet__canvas {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  background: var(--bg);
  /* Top band doubled to match the project pages' --frame-y, per Liam
     ("double as thick top and bottom") — the canvas's bottom edge is
     mid-viewport (the footer occupies the rest), so only its TOP inset
     is a real frame band; the visual bottom band above the footer's
     hairline comes from the canvas padding-bottom, also doubled.
     env(safe-area-inset-top) added on top of that base band, per Liam
     ("the frame/inner border of the page... sits lower without
     anything being cut off by iphones stuff") — viewport-fit=cover
     extends the page into the status bar/notch/Dynamic Island area
     (needed for the footer's own bottom safe-area lift), so without
     this the frame's top hairline sat right up against — sometimes
     under — that hardware on a real phone. 0px on everything else
     (desktop, Android, older iPhones with no inset to report), so
     this only ever adds space where a device actually needs it.
     Base band bumped from *2 to *3, per Liam looking at this on a
     real iPhone ("move that top hairline & frame down so it clears
     the battery and time easily") — *2 technically cleared the status
     bar per env()'s own reported inset, but "clears" isn't the same
     bar as "clears easily"; the extra --side is breathing room on top
     of the hardware-measured minimum, not a correction to it. */
  --canvas-top: calc(var(--side) * 3 + env(safe-area-inset-top, 0px));
  /* Bottom was calc(var(--side) * 2) — a plain --side*2 band with no
     matching top counterpart, since --canvas-top carries the extra
     safe-area clearance real iPhone hardware needed up top. Per Liam
     ("bring up the frame/bottom hairline... so the distance of the
     top hairline to the top of the page matches the distance of the
     top of the footer to the bottom of the hairline"), reusing
     --canvas-top verbatim for the bottom makes the two gaps equal by
     construction — on ANY device, notch or none, since it's the exact
     same computed value on both sides, not a separately-tuned match
     that could drift apart again later. */
  /* Bottom band is the project pages' --frame-y (--side * 2 = 28px), not
     --canvas-top (--side * 3 + notch = 42px). The two were made equal on
     purpose once, so the gap above the hairline matched the gap below it
     - but --canvas-top has to carry the iPhone's safe area up top, and
     matching that at the foot simply made the foot 14px deeper than every
     project page. Liam: "the home page has heaps of space between the
     footer and the bottom hairline frame thing. i want it to be the same
     as the project pages." The top keeps its clearance; the foot matches
     the rest of the site. */
  padding: var(--canvas-top) var(--side) calc(var(--side) * 2);
}
/* Hairline on the frame's INSIDE edge, matching .project-scroll's
   border on the project pages (reference-layout.css) — drawn as an
   overlay pseudo-element rather than a border on the canvas itself,
   since the canvas box spans the full viewport width (the frame here
   is its padding, so a real border would land at the screen edge, not
   the frame line). */
.sheet__canvas::after {
  content: '';
  position: absolute;
  inset: var(--canvas-top) var(--side) var(--canvas-top);
  border: 1px solid var(--line);
  pointer-events: none;
}
.sheet__canvas-media {
  position: absolute;
  /* Extra 0.75rem on every side beyond the frame's own --side inset —
     per Liam ("make the video and pic a bit smaller... so theres a
     bit of a gap between it and the edges"): object-fit:contain was
     otherwise letting the drawing/video touch the frame's own hairline
     border flush on whichever axis its aspect ratio filled exactly
     (horizontally, for this doodle). */
  inset: calc(var(--canvas-top) + 0.75rem) calc(var(--side) + 0.75rem) calc(var(--canvas-top) + 0.75rem);
  width: calc(100% - var(--side) * 2 - 1.5rem);
  height: calc(100% - var(--canvas-top) * 2 - 1.5rem);
  object-fit: contain;
  opacity: 0;
  transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.sheet__canvas-media.is-active {
  opacity: 0.92;
}

/* Liam: "the loading animation on desktop is way too big make it half the
   size it is now."

   Desktop only - on a phone the doodle is already modest and shrinking it
   would leave it swimming in an empty sheet. 60rem is where this page's
   other two-column behaviour begins, so it is the same line.

   transform rather than a smaller box: the drawing is object-fit:contain
   inside its frame, so halving the FRAME would only halve it on the axis
   that happens to be binding, and the doodle is wider than it is tall.
   Scaling the element halves both, exactly, about its own centre - and
   costs no layout, so the split hand-off still takes over the same frame
   it always did.

   The split videos are deliberately NOT scaled. They are the content this
   animation is covering the wait for; the doodle is the distraction. */
@media (min-width: 60rem) {
  .sheet__canvas-media { transform: scale(0.5); }
}
/* Split hand-off, 5s after the signature reveal (see video-signature-
   reveal.js's SPLIT_DELAY_MS) — same box as .sheet__canvas-media
   (identical inset), so it takes over the exact same frame rather than
   a new one. Starts invisible/inert (pointer-events:none) so it can't
   be clicked or read by a screen reader before .is-split-active flips
   it on; specificity alone (three classes vs .sheet__canvas-media's
   two) is enough to win over .is-active's 0.92 without !important. */
.sheet__canvas-split {
  position: absolute;
  inset: var(--canvas-top) var(--side) var(--canvas-top);
  display: flex;
  flex-direction: column;
  gap: 2px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 1s cubic-bezier(0.16, 1, 0.3, 1);
}
.sheet__canvas.is-split-active .sheet__canvas-split {
  opacity: 1;
  pointer-events: auto;
}
.sheet__canvas.is-split-active .sheet__canvas-media,
.sheet__canvas.is-split-active .hero-loading {
  opacity: 0;
  pointer-events: none;
}
/* ---- Hero loading indicator ----
   A thin grey bar that fills up while the sketch video draws —
   replaces the old cursive "Palme Hus" signature entirely, per Liam;
   first built as a spinner + typed "Loading..." ellipsis, then
   simplified per his follow-up ("lets nmot do the 'loading...' or the
   spinner, let's instead do a thin grey bar, and have it slowly fill
   up"). hero-loading.js positions `top` (same ink-bottom measurement
   the old signature block used) and sets the fill's
   transition-duration so "full" lands exactly at "the drawing's
   finished + the 2s hold is over," same moment the split videos take
   over. */
.hero-loading {
  position: absolute;
  /* Fallback only — hero-loading.js sets the real value once it can
     measure the doodle's own ink. Roughly where that ink bottom tends
     to sit (the SVG's drawn paths span ~y286-743 of a 1024-tall
     viewBox, letterboxed by object-fit:contain), so a positioning
     failure never strands this at the canvas's very top. */
  top: 62%;
  left: 50%;
  transform: translateX(-50%);
  transition: opacity 0.6s ease;
}
.hero-loading.is-hidden {
  opacity: 0;
  pointer-events: none;
}
/* Track + fill, same hairline language as the footer's scale bar/
   north point — outline now the same var(--ink-soft) as the north
   point's own stroke, per Liam. */
.hero-loading__bar {
  box-sizing: border-box;
  width: 5rem;
  height: 4px;
  background: var(--line);
  border: 1px solid var(--ink-soft);
  border-radius: 999px;
  overflow: hidden;
}
.hero-loading__bar-fill {
  width: 0%;
  height: 100%;
  background: var(--ink);
  /* Duration set in JS (hero-loading.js), once the video's real
     length is known — transition-timing-function stays linear here
     since a steady, predictable fill rate is what reads as "loading",
     not an eased one. */
  transition: width linear;
}
@media (prefers-reduced-motion: reduce) {
  /* No transition at all (not even an instant jump to 100% — that
     would misrepresent "loading" as "done" for the whole wait), just
     a static, unfilled bar until hero-loading.js's timeout fires and
     hides it. */
  .hero-loading__bar-fill { transition: none; }
  .hero-loading { transition: none; }
}
.sheet__canvas-split-half {
  flex: 1 1 50%;
  min-height: 0;
  overflow: hidden;
  /* An anchor now (two-step project links, split-preview.js), so it needs
     the box behaviour a div had. */
  display: block;
  position: relative;
  text-decoration: none;
  color: var(--ink);
}
.sheet__canvas-split-half video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Scaled slightly under the blur. A blur samples past the element's own
     edges, which would otherwise fade the footage out to transparent along
     all four sides and leave a soft halo inside the crisp frame; the extra
     6% pushes those sampled edges outside the clip. overflow:hidden above
     keeps the growth invisible. */
  transition: filter 0.45s cubic-bezier(0.16, 1, 0.3, 1),
              transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}
.sheet__canvas-split-half.is-armed video {
  filter: blur(7px);
  transform: scale(1.06);
}

/* The label. Hidden from sight AND from the pointer until armed, but never
   from assistive tech - it is the link's accessible name, and a screen
   reader should hear "View Skjul Hus Project" rather than a bare URL. That
   rules out display:none and visibility:hidden, which is why this fades on
   opacity alone. */
.split-half__cta {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 var(--side);
  /* Title case, near-normal tracking, per Liam - the caps were shouting and
     the 0.14em was pulling the words apart. Tracking that wide is a fix for
     all-caps at small sizes; on mixed case it just loosens the line. Size
     goes up a step to compensate, since lowercase at the same measure reads
     smaller than caps did. Weight drops to 500 to match the menu's own
     strapline rather than sitting a full bold above everything near it. */
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.005em;
  color: var(--ink);
  /* A scrim in the page's own paper, not black: the footage sits on a bone
     sheet and a dark veil would read as a video player's chrome. Mixed
     rather than a fixed rgba so it follows the theme. */
  background: color-mix(in srgb, var(--bg) 58%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
}
.sheet__canvas-split-half.is-armed .split-half__cta {
  opacity: 1;
}
/* Keyboard users skip the two-step entirely, so the label has to be visible
   the moment the link takes focus - otherwise the focus ring lands on
   footage with no indication of where Enter would go. */
.sheet__canvas-split-half:focus-visible .split-half__cta {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .sheet__canvas-split-half video,
  .split-half__cta {
    transition: none;
  }
}
.sheet__canvas-dots {
  position: absolute;
  left: 0; right: 0; bottom: 1rem;
  display: flex;
  justify-content: center;
  gap: 0.5rem;
}
.sheet__canvas-dot {
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  padding: 0;
  background: var(--line);
  transition: background-color 0.3s ease, transform 0.3s ease;
}
.sheet__canvas-dot:hover {
  background: var(--ink-soft);
}
.sheet__canvas-dot.is-active {
  background: var(--ink);
  transform: scale(1.25);
}

/* ---- Title block ----
   Rebuilt after Liam shared a Pompidou exhibition sheet as reference
   (SPECTRA 17): a few lines, one large confident text element (there,
   the show title; here, LYKKE), everything else broken up by TEXT
   ALIGNMENT and whitespace rather than boxes. So: one hairline at the
   very top of the block (.titleblock's own border-top) and nothing
   else drawn — no per-column border grid. Structure now comes from
   generous gaps and the last column (Contact) right-aligning against
   the others' left alignment, the same asymmetric push the reference
   uses between its title block and its right-aligned venue/date field.

   Portrait/base (phone upright), rebuilt sleeker per Liam: just two
   VISIBLE rows now — the logo row, and one row holding Selected
   Works / Contact / Scale & Theme side by side, in that order.
   Journal moved into a hidden slide-down menu opened by the hamburger
   in the logo row (see .titleblock__menu below) — "only have the logo
   row and the row below it," everything else tucked away. */
.titleblock {
  /* Halves --side (1.75rem site-wide) for the footer specifically, per
     Liam — was scoped to .sheet-body (the landing page's own <body>
     class), which worked while the footer only ever lived on that one
     page. Now that footer-include.js shares this same markup across
     pages with different body-level needs (skjul-hus.html's own
     content reads --side too, for hero insets/nav padding, and must
     NOT be halved), the footer has to carry its own required value
     regardless of host page — a reusable component shouldn't depend on
     its host setting up ancestor variables just for it. */
  --side: 0.875rem;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--line);
  /* No shadow above the hairline anymore — it existed to mark where the
     footer starts back when scrolling content ran right down to the
     hairline, but the frame (see .project-scroll in reference-layout.css)
     now keeps a permanent bg-colored gap above the footer, which does
     that job structurally. Removed per Liam, right after the frame
     landed. */
  background: var(--bg);
}
.titleblock__col {
  padding: 0.625rem var(--side);
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: flex-start;
  gap: 0.3125rem;
}
/* The 3-across row: Selected Work, Contact, Scale & Theme — equal
   thirds, no vertical dividers (still the SPECTRA-reference "whitespace
   not boxes" language), just column-gap. Internal padding tightened
   too (not just the gap) — three columns each carrying the full --side
   padding on both sides was eating most of the row's width, pinching
   Scale & Theme's label onto two lines. Outer page-edge inset (first/
   last column) stays at --side to match the rest of the page. */
.titleblock__row-main {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* Tightened together with the per-column padding below, per Liam —
     Contact needs to left-align with "Architectural"/"Builder's" in
     the credentials block above it (measured at x=125), 8px left of
     where the old 0.375rem gap + 0.375rem padding combo put it
     (x=133). Split 4px/4px between the two rather than taking it all
     from one, since padding alone can only give back 6px max (can't
     go negative) — gap had to cover the rest. */
  column-gap: 0.125rem;
  border-bottom: 1px solid var(--line);
  /* Bottom safe-area lift, per Liam ("iphone users cant access the
     bottom rounded corners... a margin at the bottom of the screen
     that pushes everything up slightly... above the bottom
     hairlines"): padding on the CONTAINER, above its own
     border-bottom, so the content rises off the home-indicator zone
     while the hairline itself still closes the composition at the
     screen's true bottom edge. env() needs viewport-fit=cover in the
     pages' meta (added) and reads 0 everywhere else — the 0.625rem
     floor is the universal "slightly" so non-iPhones lift a touch
     too. This padding only exists while row-main is a real box; in
     the Large Menu it goes display:contents and the bottom row's own
     cells carry the lift instead (see .is-scale-large below). */
  padding-bottom: max(0.625rem, env(safe-area-inset-bottom, 0px));
}
.titleblock__row-main .titleblock__col {
  padding-left: 0.125rem;
  padding-right: 0.125rem;
}
.titleblock__row-main .titleblock__col:first-child {
  padding-left: var(--side);
}
.titleblock__row-main .titleblock__col:last-child {
  padding-right: var(--side);
}
.titleblock__col--scale,
.titleblock__col--contact {
  gap: 0.25rem;
}
/* REVERTED — briefly removed this (see git history), reasoning that
   .brand-credentials' own calc() fix made this nudge's original job
   obsolete. Wrong on a real device: Liam's own screenshot showed the
   Selected-Work-to-Contact gap get WORSE (bigger), not better, once
   this was gone — the opposite of every desktop-Chromium measurement
   taken at the time, which showed Contact->Scale as the bigger gap
   either way. That's now TWO separate bugs (this one, and the
   footer's bottom-hairline gap) where real-device rendering
   contradicts desktop testing in ways not yet understood — pixel
   nudges tuned/verified only against a desktop browser cannot be
   trusted for this page's footer until there's a way to inspect the
   real device directly (Safari Web Inspector, cable-connected). Back
   to the working -0.1875rem rather than compounding the guess. */
.titleblock__col--contact {
  margin-left: -0.1875rem;
}
/* Sized off REAL text edges, measured via Range.getBoundingClientRect
   on the text nodes themselves — NOT element boxes. Two earlier
   attempts each failed on a different stretched-box artifact: (1)
   matching column boxes ignored that Contact's text is far narrower
   than its 1fr box (gap read ~34px vs ~14.6px), and (2) the first
   "content-based" fix measured Projects' index-number gap against the
   index SPAN's box edge (stretched to 121.7px) instead of the index's
   real text end (110.03px), so it equalized against a phantom 3px
   when the real Projects-Contact text gap is 14.63px — Liam caught
   that one visually ("can you see the gap isn't the same?").
   Target: Contact's real text end (215.86px) + 14.63px = 230.49px;
   Scale's content sits at 253.33px unmargined; difference = -22.84px
   = -1.4275rem at 375px — REPLACED with calc(), per Liam ("I think you
   have used fixed pixel gaps," and he was right — same fix as
   .brand-credentials above, same file).

   vw, NOT a percentage — this is the one margin on this page where a
   percentage is actively dangerous, and it single-handedly sank two
   earlier attempts at this fix. A percentage margin on a GRID ITEM
   feeds back into the grid's own track resolution: the big negative
   value shrank this item's contribution, the 1fr tracks re-solved
   unequal (measured 112/92/244px instead of three 123.66px thirds),
   which moved Contact, which corrupted every "baseline" measurement
   taken on the same page — including the readings the second attempt's
   constants were fit from, making the bug look like a pre-existing
   overflow ("min-content-driven columns") when it was the fix itself
   both times. Confirmed by checking document.styleSheets' parsed rule
   text against computed positions in the same tab: fresh CSS, warped
   tracks. vw resolves against the viewport alone, so it CANNOT feed
   back into track sizing — and this page's body is fixed/overflow:
   hidden in portrait, so there's no scrollbar to make 100vw diverge
   from the layout width.

   Derivation (all from the committed, unwarped geometry): row-main's
   track t = (100vw - 4px)/3 (two 2px gaps); Scale's natural content
   left = 2t + gaps + its 0.125rem padding = 2(100vw-4)/3 + 6px; the
   approved target (Contact's text end + 14.63px, per the measurements
   above) = t + 106.83px. Margin = target - natural =
   -(100vw-4)/3 + 100.83px = -33.3333vw + 102.163px = 6.3852rem.
   Sanity-checked against the approved state: reproduces -22.84px at
   exactly 375px (the value Liam signed off), -41.17px at 430px where
   the fixed version visibly drifted. */
.titleblock__col--scale {
  margin-left: calc(-33.3333vw + 6.3852rem);
}
/* No border-bottom, per Liam - "remove its bottom hairline, we no longer
   need it". It used to separate the logo row from the row below it, but in
   the mini footer (which is now the footer everywhere) row-main is hidden,
   so the line had nothing left to separate and simply closed off the bottom
   of the screen. */

/* align-items:flex-start above matters specifically for the logo — a
   replaced element with a fixed height and a real aspect ratio
   (558:124) — so it renders at its natural width instead of being
   stretched to fill the column (flex's default align-items:stretch has
   no intrinsic-ratio exception, so a height-only <img> gets its width
   forced to 100%, distorting it). */
.titleblock__logo {
  /* 3% smaller than the --tb-size-lg token, per Liam — brings the
     logo's own right edge in to meet "SELECTED WORK" label's right
     edge below it (measured ~3.4% apart beforehand: logo 99px wide,
     label ending 3.4px short of the logo's own right edge). Scaled via
     height (width stays auto, aspect-ratio-locked) rather than a
     fixed width, so it shrinks proportionally instead of distorting. */
  height: calc(var(--tb-size-lg) * 0.97);
  width: auto;
}
/* Real black/white logo files (not a CSS filter/invert), swapped per
   theme — same pattern as .ref-nav__logo in reference-layout.css. */
.titleblock__logo-light { display: none; }
[data-theme="light"] .titleblock__logo-dark { display: none; }
[data-theme="light"] .titleblock__logo-light { display: block; }

/* ---- Brand tap-to-reveal definition ----
   Tap the mark, it reveals a dictionary-style gloss of "Lykke" below
   itself. Collapse/expand uses the CSS grid-template-rows 0fr->1fr
   trick (animates to true content height, no JS measuring): the outer
   .brand-definition is the grid container with a single 0fr row by
   default, .brand-definition__inner clips via overflow:hidden while
   collapsed. Toggled purely by the button's aria-expanded state
   (brand-definition.js just flips the attribute) — no separate class.

   Why the bottom hairline (.titleblock__col--brand's own border-
   bottom) stays visually anchored while the mark moves up: that
   border lives on the whole cell, and this cell is the FIRST row
   inside .titleblock, which itself sits at the bottom of a flex
   column (.sheet) with the canvas absorbing the remaining space above
   it. When this cell's height grows (the definition adding height
   below the mark), the titleblock's total height grows by the same
   amount, so its top edge rises by exactly that amount — and since
   this cell's OWN bottom edge = old top + this cell's new height, the
   two effects cancel out and the bottom edge doesn't move. No
   position/transform tricks needed; it falls out of ordinary flex
   layout math for free. */
.brand-row {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  width: 100%;
  /* Reserves the hamburger's own footprint (see .hamburger's absolute
     width below, plus a little breathing room) so credentials/
     instruments never lay content underneath it. */
  padding-right: 1.5rem;
}
/* Absolutely positioned against .brand-row's own right edge — which
   already sits at the page's real --side inset, the same gap LYKKE
   keeps on the left (per Liam: "hamburger should be right aligned to
   the frame of the video above it, so it has the same gap from the
   edge that LYKKE does"). flex margin-left:auto USED to place this,
   which put it at the mercy of everything else in the row: once the
   mini footer's credentials + instruments got wide enough to overflow
   .brand-row's own box (overflow:visible let the excess spill out
   silently), the hamburger rode that overflow right past the frame's
   edge instead of stopping at it. Absolute positioning can't overflow
   — its right:0 is always exactly the row's own edge, regardless of
   how tight or wide everything else gets. top:50%/translateY(-50%)
   reproduces the old align-self:center vertical position without
   needing the row's own flex alignment. */
.hamburger {
  position: absolute;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Was touch-action:none, so footer-drag.js could claim vertical touch
     moves before the browser turned them into a page scroll. That script is
     deleted and the footer no longer drags, but `none` also blocks a scroll
     that merely STARTS on this button - a dead gesture on the one control
     sitting in the corner a thumb reaches for. Inherits the root's
     manipulation now. */
  /* 25% smaller than the original 1.125rem/0.1875rem, per Liam. */
  gap: 0.140625rem;
  width: 0.84375rem;
  height: 0.84375rem;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}
.hamburger span {
  display: block;
  height: 1px;
  /* Same tone as the theme toggle next to it, per Liam — was var(--ink),
     which read heavier/darker than the rest of this quiet caption row. */
  background: var(--ink-soft);
}
/* Three lines, one colour, per Liam.

   The opacity map that used to live here made the icon a live diagram of
   the footer - one line per footer row, full colour if that row was on
   screen and halftone if it was hidden. That was meaningful while the
   footer had three states to be in. It no longer does: the footer is always
   the mini bar and the hamburger opens the full-screen menu, so every line
   was describing a row that either always exists or never appears, and the
   varying opacity just read as an unexplained gradient. */
.brand-toggle {
  display: inline-flex;
  padding: 0;
  margin: 0;
  border: none;
  background: none;
  cursor: pointer;
}
/* ---- Journal menu (hidden until the hamburger opens it) ----
   Same grid-template-rows 0fr->1fr reveal as .brand-definition —
   .titleblock__menu is the grid container (0fr by default, 1fr when
   .is-open), .titleblock__menu-inner clips via overflow:hidden while
   collapsed. Toggled by menu-toggle.js. */
.titleblock__menu {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.4s ease;
}
.titleblock__menu.is-open {
  grid-template-rows: 1fr;
}
/* Flex row, not the row-main grid — Journal and Client Portal each
   hold at a fixed width matching one of row-main's three 1fr tracks
   (see below), rather than splitting the row evenly between just the
   two of them. No border-top on this row or its columns, per Liam
   ("only one continuous hairline above it") — row-main's own
   border-bottom, directly above this whenever the menu opens, is
   already a single line spanning the full row width (it lives on the
   container, not per-column, so it doesn't have the segmented-with-a-
   gap problem journal/portal's individual border-tops used to have).
   Adding another one here would just stack a second line right under
   it. In landscape, row-main's border-bottom disappears too (that
   whole element becomes display:contents there, see below) — correct,
   since there's no "row below" to separate from in a single-row
   layout. */
.titleblock__menu-inner {
  overflow: hidden;
  min-height: 0;
  display: flex;
  /* Matches row-main's own column-gap (0.125rem) — Journal/Portal need
     to match Selected Work/Contact's LEFT positions, not just their
     widths, so this has to track that value, not just each column's
     own flex-basis. */
  gap: 0.125rem;
}
/* Same box width as Selected Work's column, per Liam — Journal isn't
   one of .titleblock__row-main's three 1fr tracks (it lives in the
   hidden menu instead, full-width by default), so its width has to be
   worked out by hand to match: row-main splits (100% - 2 gaps of
   0.125rem) into three equal tracks. flex:0 0 (not grow/shrink) pins
   it there exactly. Reset in the landscape media query, where Journal
   really is one of row-main's siblings again (via display:contents)
   and already sizes correctly off its own 1.2fr track. */
.titleblock__menu .titleblock__col--journal {
  flex: 0 0 calc((100% - 0.25rem) / 3);
}
/* Client Portal: second section on the hidden-menu row, per Liam —
   same fixed width as Contact's column above (same formula as
   Journal, since row-main's tracks are equal thirds). */
.titleblock__menu .titleblock__col--portal {
  flex: 0 0 calc((100% - 0.25rem) / 3);
  min-width: 0;
}
/* Copyright grows to the row's end instead of taking a fixed third —
   mirroring how Scale & Theme's column box above actually behaves:
   its negative margin-left pulls its left edge in while the grid
   track's right edge stays pinned at the page edge, so its content
   area is ~1.4rem WIDER than a plain third. flex-grow reproduces that
   here without copying the magic number: this is the last item, so
   growing to the row's end + the same page-edge padding-right lands
   the wrap width at the same content span as Scale's, per Liam
   ("same width as the scale & theme column above"). */
.titleblock__menu .titleblock__col--copyright {
  flex: 1 1 calc((100% - 0.25rem) / 3);
  min-width: 0;
}
/* Top-aligned with the logo (.brand-row's align-items:flex-start) —
   the block ends up shorter than the logo purely because two lines of
   small text at a tight line-height is less tall than the logo's own
   height, so its bottom lands slightly above the logo's bottom without
   any explicit offset needed, per Liam's spec. Two-column grid (label,
   then the actual rego number right-aligned) — column widths size to
   their own content automatically, so "13302" and the longer builder
   number both land flush against the same right edge regardless of
   how many digits either one is. */
.brand-credentials {
  display: grid;
  grid-template-columns: auto auto;
  column-gap: 0.375rem;
  opacity: 1;
  transition: opacity 0.4s ease;
  /* Tracks Contact's left edge via calc(), not a fixed nudge, per Liam
     ("Architect Builder does not align with Contact below... I think
     you have used fixed pixel gaps" — correct: the old 0.168rem was
     measured once at exactly 375px and never held at any other real
     phone width).

     First attempt at this used a hand-derived constant that measured
     WRONG in testing (looked like row-main's columns were min-content-
     driven and already overflowing) — that turned out to be a false
     reading caused by testing while a second, also-wrong calc() on
     .titleblock__col--scale was simultaneously dragging that column
     across the row with a large bad margin, corrupting every
     downstream measurement taken at the same time. Reverted both,
     re-measured cleanly one at a time (row-main genuinely IS equal
     1fr thirds — confirmed 123.66/123.66/123.66px at 375px width,
     142/142/142px at 430px, zero overflow at either), then fit this
     constant directly from real getBoundingClientRect readings at
     375px and 430px (a Range on the actual text node, not the element
     box) rather than re-deriving it symbolically a third time. */
  margin-left: calc(33.3523% - 7.06528rem);
}
/* Same :has() pattern as .brand-definition's own reveal — credentials
   fade out while the definition is open, fade back in once it's
   closed, per Liam. */
.brand-row:has([data-brand-toggle][aria-expanded="true"]) .brand-credentials {
  opacity: 0;
}
.brand-credentials span {
  color: var(--ink-soft);
  font-family: var(--font);
  font-size: 0.5rem;
  line-height: 1.1;
}
/* Left-aligned (was right-aligned) and pushed out to start at the same
   x as "SCALE & THEME" below, per Liam — a fixed margin-left rather
   than restructuring the grid's own column widths, since the natural
   post-label starting point is already the same for both rows
   regardless of which label is longer (a shared grid track, not sized
   per-row), so a uniform push from there reaches the same target
   either way. Tuned for a ~375px phone, same as Contact/Scale's own
   nudges above — none of this is grid-relative, so exact alignment
   only holds at one specific viewport width. Value follows Scale's own
   updated target (.titleblock__col--scale above) — natural position
   206.234px + 1.516rem lands it at the same 230.49px Scale's content
   now starts at. */
.brand-credentials span:nth-child(even) {
  text-align: left;
  /* Reduced from 1.516rem by the same 2.69px .brand-credentials' own
     new margin-left adds, so the numbers stay pinned to Scale & Theme's
     x (250.33px) instead of drifting right along with the labels. */
  margin-left: 1.348rem;
}
.brand-definition {
  display: grid;
  grid-template-rows: 0fr;
  width: 100%;
  transition: grid-template-rows 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
/* :has() rather than the old ~ sibling combinator — the toggle button
   is now nested inside .brand-row (next to the new credentials text),
   no longer a direct sibling of .brand-definition, so a plain sibling
   selector stopped matching when that wrapper was introduced. */
.brand-row:has([data-brand-toggle][aria-expanded="true"]) + .brand-definition {
  grid-template-rows: 1fr;
}
.brand-definition__inner {
  overflow: hidden;
  min-height: 0;
}
/* Same title-tier weight/size/family as .titleblock__label, but NOT
   its uppercase transform — this line is deliberately mixed case now
   ("Danish" title case, "(noun)" lowercase, per Liam), set directly
   in the HTML rather than relying on CSS to case it. */
.brand-definition__pos {
  margin: 0.75rem 0 0;
  color: var(--ink-soft);
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--tb-size-md);
  letter-spacing: 0.06em;
}
/* Font stays Georgia (kept, per Liam) — size dropped to the text tier
   and color now follows the same light/dark pattern as the rest of
   the page's "text" content (.titleblock__link's default var(--ink),
   swapped to --ink-soft in dark mode below) instead of being
   permanently --ink-soft regardless of theme. */
.brand-definition__def {
  margin: 0.125rem 0 0;
  color: var(--ink);
  font-family: Georgia, "Times New Roman", serif;
  font-size: var(--tb-size-sm);
  line-height: 1.4;
}
/* Own line, own class — "normal font" (var(--font)/Helvetica, same as
   the rest of the page's text), not Georgia like the sentence below
   it. Only the quoted "luuh-kah" itself is italic (the <em> in the
   markup), "Pronounced" stays upright. */
.brand-definition__pronunciation {
  margin: 0.125rem 0 0;
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--tb-size-sm);
  line-height: 1.4;
}
.brand-definition__pronunciation em {
  font-style: italic;
}
/* Medium tier ("titles"): the eyebrow caption above every column's
   real content (Selected Work, Journal, Scale 1:100, Contact) —
   never a link itself, matching Projects' pattern across all four
   columns. */
.titleblock__label {
  margin: 0;
  color: var(--ink-soft);
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--tb-size-md);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* These are eyebrow captions, always meant to read as one short line
     — without this, a flex column with align-items:flex-start can
     settle on a width narrower than the label's own max-content
     (shrink-to-fit allows wrapping to reduce the box below that), so
     a label can silently wrap even when its column technically has
     room. nowrap forces the box to reserve its true full width instead. */
  white-space: nowrap;
}
/* Small tier ("text"): the actual content — project names, journal
   entry headlines, the Contact email-as-link. Deliberately smaller
   than the label above it, not bigger. */
.titleblock__link {
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--tb-size-sm);
  line-height: 1.25;
  transition: opacity 0.3s ease;
}
.titleblock__link:hover {
  opacity: 0.6;
}
/* The footer link pointing at the page you're currently viewing, per
   Liam ("if a link is selected, it should be in bold... when we are
   viewing that page") — is-current is set at runtime by
   current-page.js (one shared footer partial, no per-page markup to
   hand-mark). 700 matches the weight already used for section labels
   elsewhere in this footer (.titleblock__label), so a bold link reads
   as consistent with the rest of the type system rather than a one-off. */
.titleblock__link.is-current {
  font-weight: 700;
}
.titleblock__link-index {
  color: var(--ink-soft);
  font-size: var(--tb-size-sm);
}
/* Same display:contents-on-the-<a> pattern as .contact-grid — see
   that comment for the full reasoning. Here it puts every project's
   index number in one aligned column regardless of name length. */
/* Column 1 fixed (not auto) — the names in it cycle Danish/English
   at different lengths ("Skjul Hus" vs "Shelter House" vs "Courtyard
   House," the longest), and an auto column recalculates its width
   from whatever's currently rendered, which would shift the index-
   number column every 10s as names changed length. Fixed width sized
   to comfortably fit the longest variant keeps the numbers pinned
   regardless of which language/project is showing. Tightened from 6rem
   to 4.5rem per Liam ("reduce space between names and numbers") — the
   widest variant, "Courtyard House," only measures ~4.25rem
   (getBoundingClientRect at --tb-size-sm), so 6rem was leaving nearly
   a full rem of dead air before the shortest names' index numbers.
   column-gap tightened alongside it for the same reason. */
.projects-grid {
  display: grid;
  grid-template-columns: 4.5rem auto;
  column-gap: 0.25rem;
  row-gap: 0.0625rem;
  width: 100%;
}
.projects-grid a {
  display: contents;
}
/* Catch-all "Other Projects" row — smallest text tier (same as
   .contact-cta/scale-bar numbers) and its own margin-top to read as a
   separate line set apart from the three named works above it, per
   Liam. Applied to the spans themselves (not the <a>, which has no box
   of its own via display:contents above) so both cells in the row
   shift down together without disturbing the row-gap between the
   named rows. */
/* margin-top 0 (was 0.1875rem) — per Liam, this row needs to align
   with Book a meeting/Menu: Large/Theme: Light next to it, which
   measurement showed sitting 3px higher; the projects-grid row-gap
   above (0.0625rem) still keeps a sliver of separation from Gårdhus. */
.projects-cta span {
  margin-top: 0;
  font-size: 0.5rem;
  color: var(--ink-soft);
}
/* Journal's own catch-all ("Other Entries", per Liam — "in the same
   text size as Book a meeting") — same smallest tier and soft ink as
   .projects-cta/.contact-cta, its own margin-top since journal-list's
   entry gap is deliberately tighter than a section break. */
.journal-cta {
  margin-top: 0.1875rem;
  font-size: 0.5rem;
  color: var(--ink-soft);
}
/* Journal entries: one line each, truncated with an ellipsis, rather
   than hand-trimming the real title's wording to fit (per Liam). Two
   things have to be true for that truncation to actually clip instead
   of pushing the layout wider: align-self:stretch (the column's
   default align-items:flex-start lets a flex child shrink to its
   content width, which would make text-overflow:ellipsis never
   trigger) AND min-width:0 on the column itself (a CSS grid item's
   min-width defaults to auto = its content's max-content width as a
   floor, so a long nowrap line can blow out a 1fr track's actual width
   — without this the whole Journal column, and everything next to it,
   was growing to fit the unclipped text instead of holding at 1fr). */
/* Left/right padding tightened to match row-main's own columns
   (0.125rem, var(--side) only on the true page edges) — .titleblock__col's
   base rule gives every column var(--side) (14px) on BOTH sides, which
   is correct for Journal's left edge (it's genuinely first, same as
   Selected Work) but left dead air on its right side compared to
   Projects' own tightened padding. Single-class selectors (not
   compound, e.g. not ".titleblock__menu .titleblock__col--journal")
   so these carry the same specificity as the landscape media query's
   own overrides below and lose to them there on source order, rather
   than winning by specificity regardless of media query — per Liam
   ("still not left aligned... as Contact row above"), caught after
   Journal happened to look right already (first-column padding-left
   is var(--side) either way) while Portal's was visibly 8px off. */
.titleblock__col--journal {
  min-width: 0;
  padding-left: var(--side);
  padding-right: 0.125rem;
}
.titleblock__col--portal {
  padding-left: 0.125rem;
  /* Mid-row now that Copyright sits to its right (was var(--side)
     back when Portal was the row's last column). */
  padding-right: 0.125rem;
  /* Follows Contact's own nudge (.titleblock__col--contact,
     margin-left: -0.1875rem above) — Portal is built to match
     Contact's width AND position exactly, so it has to move with it
     rather than staying anchored to its own unshifted flex-basis. */
  margin-left: -0.1875rem;
}
/* Copyright column (row 2, column 3) — mirrors Scale & Theme above it
   the same way Portal mirrors Contact: same content left edge, so the
   footer reads as a real 3-over-3 grid. Both columns are the third
   (equal-thirds) track of their own row, same column-gap, same
   0.125rem padding-left — so the exact same margin-left that pulls
   Scale's content to its target x pulls Copyright's there too, by
   construction, at every viewport width. Was its own fixed -1.24rem,
   solved once at ~375px and never re-derived — the same fixed-pixel
   bug Scale's own margin already got caught and fixed for (see that
   rule's comment), just not carried over here. Per Liam ("align
   copyright with scale and theme"): drifted apart on any phone other
   than the one this was eyeballed on. Same single-class-selector
   specificity reasoning as Journal/Portal above. */
.titleblock__col--copyright {
  min-width: 0;
  padding-left: 0.125rem;
  padding-right: var(--side);
  margin-left: calc(-33.3333vw + 6.3852rem);
}
/* Tighter than .titleblock__col's own gap (which still governs the
   label-to-first-entry spacing, unchanged) — per Liam, reduce space
   between entries specifically, not above them. */
.journal-list {
  display: flex;
  flex-direction: column;
  gap: 0.0625rem;
  width: 100%;
  min-width: 0;
}
.titleblock__col--journal .titleblock__link {
  /* Fixed 6rem (was align-self:stretch to the column box), per Liam
     ("respect the column width of Selected Work above") — the ellipsis
     now cuts at the same x the index numbers' text ends above (~110px
     at 375: 14px content left + 96px), instead of running ~12px wider
     to the column box edge. Reset to auto in landscape, where the
     column is its own track. */
  width: 6rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- Client Portal (UI-only mockup, no real backend) ----
   Each field's placeholder IS its visible label, per Liam ("typed into
   and replace the text") — the native <input placeholder> behaviour
   already does exactly that with no JS needed. A real <label> still
   exists per field for screen readers (.visually-hidden below), just
   not shown, since the placeholder disappears the moment there's a
   real value and an AT user would otherwise lose the field's name. */
/* Gap/line-height/padding all tuned to reproduce .journal-list's exact
   row rhythm (0.0625rem row-gap, --tb-size-sm text at line-height:1.25)
   so each of the three fields lines up with its corresponding Journal
   entry next to it, per Liam — a native <input> carries its own
   UA line-height/padding baggage that a plain <a> text row doesn't, so
   this needs to be set explicitly rather than just copying the gap
   value alone. */
.portal-form {
  display: flex;
  flex-direction: column;
  gap: 0.0625rem;
  width: 100%;
}
.portal-field {
  width: 100%;
}
/* No box — same "whitespace and alignment, not boxes" language as the
   rest of the title block (see the SPECTRA-reference comment above
   .titleblock) — just a hairline to mark the field, same --line color
   as every other rule on this page. Brightens to --ink on focus as the
   only feedback a boxless input has that it's actually active. */
/* 16px is the floor on anything typed into, because iOS zooms the page when
   a focused field is under it - and since these pages now refuse the pinch
   (touch-action on .project-body), a reader who got auto-zoomed would have
   no easy way back out. max() rather than a flat override so a breakpoint
   that already sets something larger keeps it. */
.portal-input {
  display: block;
  /* Fixed, not 100% — the underlines used to run the full column box
     (which reaches well past Contact's visible text above), so the
     column read wider than Contact despite identical box widths.
     5.7rem terminates each line at the same x "studiolykke.au" ends
     (~216px at 375), per Liam ("terminate a bit earlier... nice grid
     formation"). Reset to 100% in landscape, where the column is its
     own track with a different budget. */
  width: 5.7rem;
  margin: 0;
  padding: 0;
  border: none;
  border-bottom: 1px solid var(--line);
  background: none;
  color: var(--ink);
  font-family: var(--font);
  font-size: max(1rem, var(--tb-size-sm));
  /* -1px to offset border-bottom's own height, which a plain <a> text
     row (.titleblock__link, line-height:1.25 with no border) doesn't
     have to account for — without this every row here lands 1px
     taller than Journal's, drifting the two columns further out of
     alignment with each successive line. */
  line-height: calc(1.25em - 1px);
  outline: none;
  transition: border-color 0.3s ease;
}
.portal-input::placeholder {
  /* The typed value has to be 16px or iOS zooms the page on focus, but the
     placeholder is descriptive text, not something being typed - and iOS
     measures the INPUT's font-size, not the placeholder's. So the field
     keeps its 16px floor and the prompt goes back to the size the design
     wants. Liam: the describing words should be small, the typed ones must
     be what they must be. */
  font-size: var(--tb-size-sm);
  color: var(--ink-soft);
}
.portal-input:focus {
  border-bottom-color: var(--ink);
}
[data-theme="dark"] .portal-input {
  color: var(--ink-soft);
}

/* ---- Copyright (row 2, column 3) ----
   Liam's real notice, one sentence wrapping naturally to the column
   width (was three hand-broken nowrap lines while the copy was still
   placeholder). Quiet tier (0.5rem/--ink-soft, same as Book a
   meeting) — legal boilerplate shouldn't compete with the nav
   content. line-height matches Journal/Portal's row rhythm so the
   wrapped lines sit on roughly the same beats as the rows beside
   them. */
.copyright-list {
  display: flex;
  flex-direction: column;
  gap: 0.0625rem;
}
.copyright-list p {
  margin: 0;
  color: var(--ink-soft);
  font-family: var(--font);
  font-size: 0.5rem;
  line-height: 1.25;
}
/* Standard clip-to-1px pattern — keeps each field's real <label> in
   the accessibility tree without showing it, since the placeholder is
   the visible label here per Liam's spec. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Small tier ("text"): secondary meta under a title (architect
   registration, studiolykke.au) — same size as the content above it,
   not the label. */
.titleblock__meta {
  margin: 0;
  line-height: 1.25;
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--tb-size-sm);
}

/* Dark mode only: swap which tier gets the brighter color. Everywhere
   else (light mode, the base rules above) titles are the dim eyebrow
   caption and content is full-ink; in dark mode Liam wants that
   inverted — titles reading lighter/brighter than the content beneath
   them. theme-toggle.js always sets data-theme explicitly ("dark" or
   "light"), so this can target the attribute directly rather than
   guess at a no-attribute default. */
[data-theme="dark"] .titleblock__label {
  color: var(--ink);
}
[data-theme="dark"] .titleblock__link,
[data-theme="dark"] .titleblock__meta,
[data-theme="dark"] .brand-definition__def,
[data-theme="dark"] .brand-definition__pronunciation {
  color: var(--ink-soft);
}

/* ---- Contact rows ----
   Each row is one <a href> with display:contents — its two <span>
   children (left label, right value) become direct items of this
   grid, so all three rows share one 2-column alignment instead of
   each row sizing its own columns independently. row-gap keeps this
   tighter than the reference image Liam shared (which had generous
   vertical space) per "less gap." */
.contact-grid {
  display: grid;
  grid-template-columns: auto 1fr;
  /* Tightened per Liam ("remove 1 or 2 spaces from the gap") — was
     0.75rem. */
  column-gap: 0.375rem;
  row-gap: 0.0625rem;
  width: 100%;
}
.contact-row {
  display: contents;
}
/* The value half of each row (studiolykke.au) reads as secondary —
   --ink-soft, same muted tone as the labels/captions elsewhere — while
   hello@/www./insta stay full --ink, per Liam. nth-child(even) counts
   .contact-row's two <span> children once display:contents promotes
   them to direct .contact-grid items (2nd, 4th, 6th of the three
   rows); .contact-cta is a real <a>, not a span, so it's unaffected
   either way. No dark-mode override needed — the existing dark-mode
   rule already flattens all .titleblock__link content to --ink-soft
   uniformly there, so this is already a no-op/redundant match in dark
   mode rather than a conflict. */
.contact-grid span:nth-child(even) {
  color: var(--ink-soft);
}
/* One phrase, not a label/value pair — a real grid item (not
   display:contents like the rows above it) spanning both columns. A
   step more subtle than the hello@/www./insta rows above it — smaller,
   and pinned to --ink-soft regardless of theme (.titleblock__link.
   compound selector so it wins over the dark-mode swap rule, which
   would otherwise brighten it in dark mode along with real content). */
.titleblock__link.contact-cta {
  grid-column: 1 / -1;
  margin-top: 0.0625rem;
  font-size: 0.5rem;
  color: var(--ink-soft);
}

/* ---- Scale bar ----
   The most deliberately "gimmicky"/decorative element on the page, per
   Liam — a real graphic scale bar (alternating filled/open divisions +
   numbered distances), not just plain ruler ticks. Reads at full width
   in portrait, compresses into its own column in landscape. Sized small
   — a flourish next to the real nav content, not competing with it for
   vertical space (portrait title-block height is being reworked toward
   ~1/3 of screen height; this is the easy part of that, done now). */
.scale-row {
  display: grid;
  grid-auto-flow: column;
  /* Rows 1 & 2 pinned to a known 0.75rem rather than "auto" — an
     SVG's intrinsic/replaced-element sizing quirks in a grid track
     didn't match a <button>'s the same way plain block content would,
     so letting both auto-size left row 2 taller than expected and the
     two cells centered within different effective heights (~7px off
     between the line and the switch, verified via
     getBoundingClientRect, despite both nominally being ~12-13px
     tall). Fixed tracks remove the ambiguity entirely. */
  grid-template-rows: 0.75rem 0.75rem auto;
  align-items: center;
  justify-content: start;
  column-gap: 0.625rem;
  /* 0, not a uniform value — row-gap applies between EVERY row
     equally, but Liam only wants the numbers-to-lines gap (row 1->2)
     collapsed to near-nothing, not the lines-to-captions gap too.
     Removed here, restored on row 3's items directly via margin-top
     below (.scale-row__caption) so that gap stays as it was. */
  row-gap: 0;
  width: 100%;
  margin-top: 0.25rem;
}
/* Empty row-1 placeholder in the toggle's column — the toggle only
   needs to align with the LINE row (row 2), not the numbers above it,
   so this holds row 1's second cell instead of leaving grid-auto-flow
   to place the toggle there by default. Height matches
   .scale-bar__labels so row 1's height is identical either column. */
.scale-row__spacer {
  height: 0.75rem;
}
/* Centers the switch horizontally over "Theme: Light" below it — the
   column's own width is set by whichever cell is wider (usually the
   caption text), so the toggle needs its own justify-self rather than
   relying on the column's default stretch/start. */
.theme-toggle--inline {
  justify-self: center;
}
/* North point (drawing-sheet compass), right of the theme toggle, per
   Liam — see the markup comment in footer-partial.html. Explicitly
   placed: the circle spans grid rows 1-2 (the numbers row + line row,
   1.5rem together) for a readable diameter — the fixed 0.75rem line
   row alone would squash it to half the toggle's size — and "North"
   sits in the caption row below, centered under it like the toggle
   over its own caption. Soft ink, same quiet tier as the captions. */
.north-point {
  grid-column: 3;
  grid-row: 1 / 3;
  align-self: center;
  justify-self: center;
  display: block;
  /* Square viewBox again (needle fully inside the circle now), a
     step smaller per Liam ("a bit smaller"). */
  width: 1.125rem;
  height: 1.125rem;
  color: var(--ink-soft);
}
.north-point__caption {
  grid-column: 3;
  grid-row: 3;
  justify-self: center;
}
/* "North" reads as a label, not a control — the shared caption class
   assumes button-ness (both its other users toggle something). */
.scale-row__caption--static {
  cursor: default;
}
/* ---- Mini footer (landing page first-load state) ----
   Per Liam: only the brand row ("top tab") visible at first, but
   carrying the scale bar's linework + numbers and the north point —
   no captions, no toggle. mini-footer.js moves the real nodes into
   [data-brand-instruments] and flips .is-mini; the hamburger's first
   click reverses it all. The slot is display:none outside mini so
   the (empty) div never affects the normal brand-row layout. */
.brand-row__instruments {
  display: none;
}
.titleblock.is-mini .titleblock__row-main {
  display: none;
}
.titleblock.is-mini .brand-row {
  /* Liam: "the footer needs to spread out the 6 elements as the screen
     gets wider. and also the hamburger always needs to be in the right
     position. and the gaps between all the elements should calculate
     based on the width of the screen."

     Then, seeing it spread evenly: "it doesnt look good. lets have the
     north point the scale bar and the hamburger stay right side and
     the reg numbers stay left side and the space grows between them."

     So two clusters. LYKKE and the credentials sit left, the north
     point, scale and hamburger sit right, each cluster with the one
     gap below between its pieces - a gap that still follows the
     screen, gently - and the north point's margin-left:auto is what
     makes all the free width open up between the two clusters. The
     hamburger is a flex item, last in the row, so it is on the frame's
     right line by construction. */
  justify-content: flex-start;
  /* Liam: "a little more space (near double) between the icons if / when
     we can afford it." Was 1.5vw capped at 1.75rem - 22px at 1440. Now
     3vw capped at 3.25rem: 43px at 1440, 23px on a tablet, and still the
     0.5rem floor on a phone, where there is nothing to afford it with. */
  gap: clamp(0.5rem, 3vw, 3.25rem);
  padding-right: 0;
  /* Bottom-align credentials with the LYKKE logo's own bottom edge,
     per Liam ("bottom align builders license with bottom of LYKKE
     logo, and bring Architectural down with it") — align-items
     shifts the whole credentials block as one flex item, so both
     lines move down together rather than needing a hand-tuned
     margin-top on just the bottom line. Base rule's flex-start
     (landing-page.css above) is what was leaving credentials
     top-anchored while the taller logo wordmark hung below it.
     .brand-row__instruments keeps its own align-self:center below,
     unaffected. */
  align-items: flex-end;
}
.titleblock.is-mini .brand-row__instruments {
  /* Same two-row stack the scale-row grid gives these pieces at home:
     numbers over linework, north point spanning both rows beside
     them. margin-left:auto walks it toward the row's right edge — it
     stops at .brand-row's own padding-right (reserved for the
     hamburger, which is absolutely positioned now and no longer
     competes for flex space at all). pointer-events:none — up here
     these are drawing-sheet furniture, not controls; it also keeps
     the moved scale bar's own click-to-cycle listener (which rides
     along with the node) from opening the Large Menu out of a state
     it was never designed to open from.
     margin-right carves out real breathing room before the hamburger
     — with content this close to the row's full width, margin-left:
     auto was landing the block flush against the padding-right
     reserve every time (a fixed ~10px gap no matter what upstream
     spacing did), so widening that gap has to come from this side,
     not from freeing space earlier in the row. */
  display: grid;
  grid-template-rows: 0.75rem 0.75rem;
  grid-template-columns: 1fr;
  align-items: center;
  /* Just the scale now - numbers over line work. The north point left
     this block to be spaced by the row like everything else, and the
     margins that used to walk the block toward the hamburger are gone
     for the same reason: the row's space-between does the walking. */
  margin-left: 0;
  margin-right: 0;
  align-self: center;
  pointer-events: none;
}
.titleblock.is-mini .brand-row > .north-point {
  /* First of the right-hand cluster: this margin is the growing space. */
  margin-left: auto;
  /* Centred like the scale block beside it, then dropped by half the
     height difference (the block is two 0.75rem rows, the point is
     1.125rem) so its bottom lands on the scale bar's bottom - the same
     alignment it had inside the block, per Liam. */
  align-self: center;
  transform: translateY(0.1875rem);
  pointer-events: none;
}
.titleblock.is-mini .hamburger {
  position: static;
  transform: none;
  align-self: center;
}
/* From 768px up .titleblock is a wrapping flex row and the brand column
   keeps its content width, so with the other columns hidden in mini the
   row sat 434px wide inside a 1440px frame and the hamburger with it
   (Liam: "the hamburger always needs to be in the right position").
   The brand column is the whole row now; space-between above does the
   rest. */
.titleblock.is-mini .titleblock__col--brand {
  flex: 1 1 100%;
  width: 100%;
  max-width: none;
  /* The wide-screen column rule gives this column 0.375rem on the right
     - its inner edge when it was the first of several. As the whole
     row it needs the page inset on both sides, or the hamburger lands
     8px past the frame line (measured 1434 against a frame at 1426). */
  padding-right: var(--side);
}
/* North point first (left), scale bar second (right) — swapped per
   Liam ("just switch the place of the scale bar and nth point in the
   top tab"). */
.titleblock.is-mini .brand-row__instruments .scale-bar__labels {
  grid-column: 1;
  grid-row: 1;
}
.titleblock.is-mini .brand-row__instruments .scale-bar {
  grid-column: 1;
  grid-row: 2;
}
.titleblock.is-mini .brand-row__instruments .north-point {
  grid-column: 1;
  grid-row: 1 / 3;
  /* Was center (the grid's own align-items) — per Liam ("bring the
     nth point down so its bottom is at the bottom of scale bar"),
     bottom-align within the spanned two rows instead, so its own
     bottom edge lands flush with the scale bar's (row 2's bottom)
     rather than centered 3px above it. */
  align-self: end;
}
/* The instruments squeeze the brand row; without this the credential
   labels wrap mid-phrase ("Architectural Reg. / No."). Nowrap keeps
   each label one line — the block is narrow again in mini (natural
   0.375rem column gap, no fraction alignment), so it fits. */
.titleblock.is-mini .brand-credentials span {
  white-space: nowrap;
}
/* Pull the whole credentials block (labels AND numbers — the numbers
   ride 0.375rem behind the labels in mini) in close behind the logo,
   per Liam ("move arch builder more left, and the numbers too") —
   the base rule's margin-left calc() targets the expanded footer's
   30% Contact line, which reads as a stranded island in the mini
   tab's tighter composition. */
.titleblock.is-mini .brand-credentials {
  /* Was 0.5rem, which made the first of the row's four gaps 8px wider
     than the other three at 375 (measured 16 / 8 / 8 / 8). The row's
     own gap and space-between now set every gap, so nothing here. */
  margin-left: 0;
  /* align-items:flex-end above matches BOX bottoms, not ink — the
     LYKKE PNG has a few px of transparent padding baked in below the
     mark, and the credentials text has its own line-height gap below
     the glyphs, so box-bottom parity doesn't guarantee the visible
     ink lines up. The two gaps are close enough to cancel out in some
     renderers but not others (per Liam, still visibly off on his
     phone) — nudging down a touch further closes it without
     overcorrecting the cases where it already looked right — bumped
     from 1px to 2px per Liam's follow-up ("still needs to come down a
     tiny bit"). */
  transform: translateY(2px);
}
/* The numbers' 1.348rem margin-left (base rule, above) exists only to
   line "13302"/"496664C" up with the expanded footer's Scale & Theme
   row underneath — there's nothing below to align to in mini, so it
   was just pushing the numbers needlessly close to the scale bar and
   north point. Per Liam ("move numbers a bit more left giving more
   room to the scale bar and nth point"). */
.titleblock.is-mini .brand-credentials span:nth-child(even) {
  margin-left: 0;
}
/* Fixed-content pieces never shrink in the mini row — whatever
   squeeze happens at narrow widths comes out of the flexible space
   between credentials and instruments, not out of the furniture. */
.titleblock.is-mini .brand-row__instruments {
  flex-shrink: 0;
}
/* Same quiet tier as .contact-cta ("Book a meeting") — regular
   weight, small, always --ink-soft. Buttons (were <p>), since both
   captions are themselves toggles — reset the browser chrome so they
   still read as plain text. */
/* margin-top empirically tuned (measured via getBoundingClientRect)
   so this row's baseline lines up with Contact's "Book a meeting" row
   next to it — the two columns have different content shapes above
   this point (Contact: label + 4 contact-grid rows; Scale: label +
   3-row scale-row), so they don't reach the same Y naturally without
   this nudge. Re-check if either column's content above changes. */
.scale-row__caption {
  margin: 0.625rem 0 0;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
  color: var(--ink-soft);
  font-family: var(--font);
  font-size: 0.5rem;
}
/* 4.75rem explicit width (walked back from 5.25rem — "slightly less
   wide," per Liam; originally width:100% + max-width:4.5rem). Explicit,
   not max-width-capped: the scale-row's auto grid track sizes to its
   content's max-content, and a replaced element's max-content width is
   its aspect-ratio width (height 0.75rem x the 200:32 viewBox = 75px)
   — so with width:100% the svg could never exceed the very track it
   was itself sizing, and the max-width cap was unreachable. A fixed
   width becomes the track's own size instead. Verified via
   bounding-rect sweep, no viewport overflow. */
/* Stroke weights live here rather than on the SVG elements so the scale bar,
   the north point and the theme switch cannot drift apart - they were three
   separate literals cross-referenced by comment before. */
.scale-bar polyline {
  stroke-width: var(--hairline);
}
.north-point circle {
  stroke-width: var(--hairline);
}
/* Double, per Liam - the needle is the one piece of this linework meant to
   read as heavier than the rest. */
.north-point line {
  stroke-width: calc(var(--hairline) * 2);
}

.scale-bar {
  display: block;
  width: 4.75rem;
  height: 0.75rem;
  /* Was var(--ink) — matched to the north point's var(--ink-soft) per
     Liam ("scale bar line colour needs to be the same colour as the
     nth point"), so the two pieces of drawing-sheet linework read as
     one consistent tone instead of the bar reading heavier. */
  color: var(--ink-soft);
  /* The bar itself toggles the scale state now (scale-toggle.js), not
     just the caption below it — per Liam ("when I click on the scale
     bar the numbers should change"). */
  cursor: pointer;
}
/* Absolute, not flex space-between: each label sits at the EXACT
   percentage matching the polyline's tick x-coordinates (0/50/100/150/
   200 of a 200-wide viewBox = 0/25/50/75/100%), anchored by its own
   edge/center rather than by its box in a flex row — see the comment
   above the markup in landing-page.html for why that distinction
   matters here. Numbers now sit above the line graphic (their own
   grid row) rather than below it, per Liam. */
.scale-bar__labels {
  position: relative;
  width: 4.75rem;
  height: 0.75rem;
  cursor: pointer;
  color: var(--ink-soft);
  font-family: var(--font);
  /* Same size as .contact-cta ("Book a meeting"), not the text tier —
     per Liam, makes the whole scale bar read slightly smaller/quieter. */
  font-size: 0.5rem;
}
.scale-bar__labels span {
  position: absolute;
  top: 0;
}
/* 4 labels for 5 tick positions — 1.5's prong deliberately has no
   number over it, per Liam. "2" right-anchors at the bar's flush end. */
.scale-bar__labels span:nth-child(1) { left: 0%; }
.scale-bar__labels span:nth-child(2) { left: 25%; transform: translateX(-50%); }
.scale-bar__labels span:nth-child(3) { left: 50%; transform: translateX(-50%); }
.scale-bar__labels span:nth-child(4) { left: 100%; transform: translateX(-100%); }
/* ---- Wide footer: tablet & desktop, single row ----
   Replaces the old @media(orientation:landscape) system entirely, per
   Liam: "the transition from phone to tablet and then to desktop...
   all of the text on the footer, in a single row. no large menu, no
   hamburger icon, etc... as the resolution gets wider, more
   categories, like journal, should start appearing, but obviously not
   twice, in the event of someone using the hamburger menu."

   Width-based now, not orientation-based — orientation was the wrong
   signal for "how much room does the footer actually have": a
   landscape phone at 667px and a portrait iPad at 834px are both
   meaningfully wide, but only the phone used to get anything beyond
   the compact, hamburger-driven mobile layout. A portrait tablet fell
   through the gap entirely and got the phone treatment regardless of
   its real width. This backup tag captures the pre-overhaul state if
   anything here needs unwinding: `git show
   footer-mobile-only-backup:landing-page.css`.

   Three tiers, each min-width and CUMULATIVE (mobile-first — a wider
   tier layers its rules on top of the narrower ones below it, it
   doesn't replace them): Journal graduates out of the hamburger menu
   first (768px), then Client Portal (1024px), then Copyright (1280px,
   where the hamburger itself disappears — nothing left to toggle).
   Each graduation is a single `display: block` override two lines
   long (see Tiers 2 and 3 below) — everything a graduated column
   needs (width, padding, order) is already defined once, here in
   Tier 1, and just keeps applying unchanged as more columns join it.

   Flexbox, not the old CSS Grid `repeat(7, max-content)` — a fixed
   7-track grid still reserves 2 empty "ghost" tracks whenever fewer
   than 7 columns are actually visible (true for Tiers 1 and 2 here),
   and `justify-content: space-between` distributes gaps by TRACK
   COUNT, not by how many are occupied — the ghost tracks would still
   claim their own gap-slots, opening exactly the "super large gaps"
   Liam ruled out. Flexbox's space-between only ever distributes space
   among items actually being rendered, so a column that's
   `display:none` simply isn't counted at all.

   Both row wrappers stay `display: contents` throughout (same
   technique the old landscape block and the Large Menu already used):
   Selected Work/Contact/Scale and Journal/Client Portal/Copyright all
   become direct flex children of .titleblock, with `order` fixing
   their final sequence regardless of which subset is currently
   visible. A column that hasn't graduated yet is plain
   `display: none` (removed from the flex flow entirely — no ghost
   gap) rather than height-collapsed via the old grid-template-rows
   accordion trick: that trick needs an actual box to animate, and a
   `display: contents` parent no longer offers one. menu-toggle.js's
   click handler and its aria-expanded/.is-open bookkeeping are
   completely unchanged — only the CSS meaning of "open" changes here,
   from "slide this box's height open" to "display:block whichever
   columns haven't graduated yet." `.titleblock__menu.is-open
   .titleblock__col--portal` still matches correctly even though
   `.titleblock__menu` itself is `display:contents` — that only
   removes ITS OWN box, not its place in the DOM the selector engine
   reads, so descendant selectors through it work exactly as normal.
   This is also the direct answer to "not twice... using the hamburger
   menu": a column is EITHER visible because it's graduated (a plain,
   unconditional `display:block` at its own tier) OR visible because
   the still-open hamburger says so — never both mechanisms fighting
   over the same column, and resizing across a tier boundary just
   changes which of the two is in force, with no DOM move and nothing
   to desync.

   `max-width` + centred margin stops the row itself from stretching
   edge-to-edge past 90rem: every flexible column's own width already
   self-caps via its `min(REM, calc(vw...))` ceiling (unchanged from
   the old landscape formulas below — verified those ceilings resolve
   by ~1230-1400px viewport width regardless of tier), so without also
   capping the ROW's width, `space-between` would keep inflating the
   GAPS between columns forever on an ultra-wide monitor once every
   column has already hit its own ceiling — exactly the "at no point
   should there be super large gaps" Liam asked to avoid.

   Resize-safety: .is-mini (mini-footer.js) and footer-drag.js's
   inline margin-bottom now both actively reset themselves once
   window.innerWidth crosses 768px (see those files) — neither has any
   media-query gating of its own, so without an explicit reset either
   one could persist across a resize/rotation and corrupt this layout
   (.is-mini's `display:none` on .titleblock__row-main would blank out
   Selected Work/Contact/Scale entirely; a stale negative margin-bottom
   would squeeze the whole row). footer-gap-equalizer.js's guard was
   also hardened (see that file) to bail out on ANY mode where
   .titleblock__row-main is display:contents, rather than naming
   specific classes to avoid — it now catches this new system, the old
   Large Menu, and anything added here in the future, all by the same
   one check. */
@media (min-width: 768px) {
  .titleblock {
    display: flex;
    /* Base rule (mobile) sets column — resetting explicitly here since
       display:flex alone doesn't reset flex-direction back to its
       initial row; left unset this whole tier silently stacked every
       column vertically instead of laying them out in a single row
       (caught via getBoundingClientRect: every column reported the
       same width and increasing top offsets). */
    flex-direction: row;
    flex-wrap: nowrap;
  }
  /* Tapping the hamburger at the narrow end of this tier (768px) can
     reveal Client Portal AND Copyright at once — together with the
     five already-visible columns, that's genuinely more content than
     768px holds even at everyone's minimum width (confirmed via
     getBoundingClientRect: Copyright's right edge landed 288px past
     the row's own 768px box with nowrap). Rather than let it overflow
     horizontally, allow wrapping ONLY while the menu is actually open
     — :has() lets this live on .titleblock itself despite is-open
     being set on the descendant .titleblock__menu (which stays
     display:contents and so isn't a real box .titleblock could
     otherwise select against). Harmless at wider tiers/viewports
     where everything already fits on one line with room to spare —
     flex-wrap:wrap only visibly wraps when it has to. */
  .titleblock:has(.titleblock__menu.is-open) {
    flex-wrap: wrap;
    row-gap: 0.5rem;
  }
  .titleblock {
    /* Journal/Scale's flex-grow (below) consumes all leftover space
       up to their own max-width — space-between only ever has
       anything left to distribute once BOTH of them are already
       maxed out (a genuinely wide row), at which point it spreads the
       remainder evenly rather than leaving it all as a single dead
       gap after the last column (flex's default flex-start would). */
    justify-content: space-between;
    gap: 1rem;
    /* Liam: "make the title block span the FULL WIDTH of the desktop
       absolutely no stopping short."

       It was capped at 90rem and centred, so on anything wider than
       1440px the block stopped and sat in the middle of the screen with
       dead space either side - which on a title block, of all things,
       reads as a drawing sheet that missed its own paper. A title block
       runs the width of the sheet. No cap. */
    max-width: none;
    margin-inline: 0;
    /* .titleblock is itself a flex item of .sheet (column-direction),
       and relying on stretch alone left it wider than .sheet — 838px
       measured inside a 768px .sheet, i.e. its own children's combined
       preferred width was winning over whatever cross-size .sheet was
       actually offering. min-width:0 (the usual fix for a nested flex
       container refusing to shrink below its content's automatic
       minimum) made no measurable difference on its own; explicit
       width:100% is what actually pinned it to .sheet's real width
       (confirmed via getBoundingClientRect immediately before/after:
       838px -> 768px) — kept both since min-width:0 is still correct
       practice for a nested flex container and costs nothing. */
    min-width: 0;
    width: 100%;
  }
  .titleblock__row-main,
  .titleblock__menu,
  .titleblock__menu-inner {
    display: contents;
  }
  .titleblock__col--brand { order: 1; }
  .titleblock__col--projects { order: 2; }
  .titleblock__col--contact { order: 3; }
  .titleblock__col--journal { order: 4; }
  .titleblock__col--portal { order: 5; }
  .titleblock__col--scale { order: 6; }
  .titleblock__col--copyright { order: 7; }

  /* Brand/Selected Work/Contact/Portal/Copyright keep their own
     content-driven width and never shrink — Journal and Scale are the
     two that flex (see below), so they're what absorbs the difference
     between a cramped tablet and a roomy desktop. flex-shrink:0 alone
     (not an explicit width) lets each one size to its natural content,
     same as it already does in the compact 3-column row-main. */
  .titleblock__col--brand,
  .titleblock__col--projects,
  .titleblock__col--contact,
  .titleblock__col--portal,
  .titleblock__col--copyright {
    flex-shrink: 0;
  }

  /* Journal and Scale share whatever space is left over, growing and
     shrinking together (flex:1 1 0 on both — equal weight) between a
     floor that stays legible and a ceiling that stops them reading as
     oversized on an ultra-wide row. This replaces an earlier attempt
     at hand-derived vw-based width formulas (calc((100vw - 668px) *
     0.37), carried over from the old orientation:landscape block) —
     those numbers were tuned for a layout where ALL SIX categories
     were simultaneously competing for space; reused as-is here, they
     put Journal/Scale into negative width the moment a narrower
     viewport had fewer categories (fewer siblings should mean MORE
     room, not a formula that doesn't know they're not there). Flexbox
     grow/shrink solves this natively: the browser computes the actual
     leftover space live, against whichever siblings are ACTUALLY
     visible at the moment — no separate constant to re-derive by hand
     every time Portal or Copyright's visibility changes at a later
     tier. Verified via getBoundingClientRect at 768/1024/1280px: no
     overflow past the row's own width at any of the three tier
     thresholds (see the "Test the new responsive footer" pass this
     shipped with). */
  .titleblock__col--scale {
    flex: 1 1 0%;
    min-width: 6rem;
    max-width: 13.5rem;
  }
  /* Compound selector, matching .titleblock__menu .titleblock__col--
     journal's own specificity (0,2,0) below — a bare .titleblock__col
     --journal here loses to that older, more specific rule regardless
     of source order (CSS specificity beats position in the cascade),
     which is exactly what happened the first time this shipped:
     Journal kept the old rule's flex:0 0 calc((100% - 0.25rem) / 3)
     (a fixed one-third share, sized for a context where it sits next
     to Selected Work/Contact in row-main's own grid — meaningless
     here) instead of this tier's flex:1 1 0%, and would only ever
     render at whatever that stale calc() happened to produce. */
  .titleblock__menu .titleblock__col--journal {
    flex: 1 1 0%;
    min-width: 6rem;
    max-width: 13rem;
  }

  /* Portrait's fixed-viewport-width nudges (Contact's -0.1875rem,
     Scale's -33.33vw calc, Portal's and Copyright's own) exist purely
     to line compact-mode columns up under one another on a ~375px
     phone — meaningless once every column is its own flex item, and
     left in place they'd silently throw off the row's spacing (the
     same reset the old landscape block needed for the same reason). */
  .brand-credentials { margin-left: 0; }
  .brand-credentials span:nth-child(even) {
    text-align: right;
    margin-left: 0;
  }
  .brand-credentials span { white-space: nowrap; }
  .titleblock__col--contact,
  .titleblock__col--scale,
  .titleblock__col--portal,
  .titleblock__col--copyright {
    margin-left: 0;
  }

  .titleblock__col {
    padding-top: 1.5rem;
    /* No bottom hairline row at this width — the lift rides on each
       cell's own padding instead of .titleblock__row-main's (which
       stops existing the moment that wrapper goes display:contents). */
    padding-bottom: calc(1.5rem + max(0.375rem, env(safe-area-inset-bottom, 0px)));
  }
  .titleblock__col--brand {
    padding-left: var(--side);
    padding-right: 0.375rem;
    /* Was the divider between the logo row and the row below it —
       there's no separate "row below" once everything's one row. */
    border-bottom: none;
  }
  .titleblock__row-main .titleblock__col:first-child,
  .titleblock__row-main .titleblock__col:last-child {
    padding-left: 0.375rem;
    padding-right: 0.375rem;
  }
  .titleblock__col--journal,
  .titleblock__col--portal {
    padding-left: 0.375rem;
    padding-right: 0.375rem;
  }
  .titleblock__col--copyright {
    padding-left: 0.375rem;
    padding-right: var(--side);
  }

  /* Journal graduates first — always visible from here on, no longer
     gated by the hamburger. */
  .titleblock__col--journal .titleblock__link {
    width: 100%;
    align-self: stretch;
  }

  /* Client Portal and Copyright haven't graduated yet at this tier —
     plain display:none (see the header comment for why not the old
     accordion trick), shown only while the hamburger's menu is open. */
  .titleblock__col--portal,
  .titleblock__col--copyright {
    display: none;
  }
  .titleblock__menu.is-open .titleblock__col--portal,
  .titleblock__menu.is-open .titleblock__col--copyright {
    display: block;
  }
  /* flex, not width — same specificity-mismatch reason as Journal
     above: .titleblock__menu .titleblock__col--portal/--copyright
     (0,2,0) already set their OWN flex-basis via a `flex: 0 0
     calc((100% - 0.25rem) / 3)` / `flex: 1 1 calc(...)` rule sized for
     row-main's equal-thirds grid, and a plain `width` declaration here
     can't out-rank a flex-basis that's already been set — flex-basis
     wins ties with width on the main axis by spec, so the stale
     one-third share would keep winning regardless. Matching the
     compound selector lets these actually override it. */
  .titleblock__menu .titleblock__col--portal {
    flex: 0 0 7.5rem;
  }
  .portal-input { width: 100%; }
  .titleblock__menu .titleblock__col--copyright {
    flex: 0 0 11rem;
  }

  .scale-bar,
  .scale-bar__labels {
    width: 100%;
    max-width: none;
  }

  .sheet__canvas { padding: calc(var(--side) * 2) var(--side); }
  .sheet__canvas-media { inset: calc(var(--side) * 2 + 0.75rem) calc(var(--side) + 0.75rem); width: calc(100% - var(--side) * 2 - 1.5rem); height: calc(100% - var(--side) * 4 - 1.5rem); }
}

/* ---- Wide footer, tier 2: Client Portal graduates ----
   One override: Portal's display:none (Tier 1, above) is replaced by
   an unconditional display:block, same specificity but later in the
   cascade so it wins — the hamburger-reveal rule
   (.titleblock__menu.is-open .titleblock__col--portal) still matches
   too, it's just redundant now (Portal shows either way), which is
   harmless. Everything else Portal needs — width, padding, order,
   flex-shrink:0 — was already defined once in Tier 1 and keeps
   applying unchanged; Journal/Scale's flex:1 1 0% automatically gives
   up exactly the room Portal now needs, live, with no separate
   recalculation required here. */
@media (min-width: 1024px) {
  .titleblock__col--portal {
    display: block;
  }
}

/* ---- Wide footer, tier 3: Copyright graduates ----
   The hamburger used to be hidden here, on the reasoning that once every
   column was on screen there was nothing left for it to reveal. That
   premise is gone: the columns moved into the full-screen menu and the
   hamburger is now the only way in, at any width. Hiding it at 1280px
   made the menu unreachable on a desktop. */
@media (min-width: 1280px) {
  .titleblock__col--copyright {
    display: block;
  }
}

/* ---- Large Menu (portrait, phone-width only) ----
   Per Liam's overnight list (original build): hitting the scale bar
   cycles the scale state, and "Menu: Large" opens this — every
   category on screen at once, two per row, four rows counting the
   logo row:

     LYKKE + credentials  (spans both columns)
     Selected Work        | Contact
     Journal              | Client Portal
     Scale & Theme        | Copyright

   All text +50% except the logo. scale-toggle.js flips
   .is-scale-large on .titleblock; everything here is gated on that
   class AND portrait AND phone-width, so the wide tiers above and the
   default compact footer are untouched.

   max-width: 767.98px added here as part of the phone->tablet->desktop
   overhaul — Large Menu is a phone-portrait affordance for seeing
   everything at once when there's no room to show it all inline; once
   a portrait viewport is wide enough for the tiers above to apply
   (768px+), they already show MORE than Large Menu ever did (a real
   single row, not a 2-column stack) so Large Menu would just be a
   redundant, confusing extra state on something like a portrait
   iPad. Same display:contents mechanic the wide tiers use to flatten
   the row wrappers into one grid. */
@media (orientation: portrait) and (max-width: 767.98px) {
  .titleblock.is-scale-large {
    display: grid;
    grid-template-columns: 1fr 1fr;
    /* +50% type, via the tokens nearly everything in the footer
       already reads from (--tb-size-lg deliberately untouched — "the
       font size of everything except the logo"). The handful of
       hardcoded 0.5rem captions are overridden individually below. */
    --tb-size-md: 0.9375rem;
    --tb-size-sm: 0.84375rem;
  }
  .titleblock.is-scale-large .titleblock__row-main,
  .titleblock.is-scale-large .titleblock__menu,
  .titleblock.is-scale-large .titleblock__menu-inner {
    display: contents;
  }
  .titleblock.is-scale-large .titleblock__col--brand { grid-column: 1 / -1; }
  /* Pair order — Copyright and Portal swapped per Liam ("make copyright
     the same width as journal and swap its place with client
     portal"): (Selected, Contact) / (Journal, Portal) / (Copyright,
     Scale). Grid auto-placement fills row by row, so odd orders land
     in the left column, even in the right. */
  .titleblock.is-scale-large .titleblock__col--projects { order: 1; }
  .titleblock.is-scale-large .titleblock__col--contact { order: 2; }
  .titleblock.is-scale-large .titleblock__col--journal { order: 3; }
  .titleblock.is-scale-large .titleblock__col--portal { order: 4; }
  .titleblock.is-scale-large .titleblock__col--copyright { order: 5; }
  .titleblock.is-scale-large .titleblock__col--scale { order: 6; }
  /* Hairlines between the category rows, mirroring the compact
     footer's row-main border — drawn per-cell (both cells of rows 1
     and 2) since the row wrappers are display:contents and can't
     paint their own. Row 3 (Copyright, Scale) is the last row, no
     hairline below it. */
  .titleblock.is-scale-large .titleblock__col--projects,
  .titleblock.is-scale-large .titleblock__col--contact,
  .titleblock.is-scale-large .titleblock__col--journal,
  .titleblock.is-scale-large .titleblock__col--portal {
    border-bottom: 1px solid var(--line);
  }
  /* Page-edge padding by column side (left column = odd orders above);
     every portrait alignment nudge (Contact's -3px, Scale's and
     Copyright's shared -33vw calc, Portal's -0.1875rem — all tuned
     against the COMPACT layout's geometry) resets to 0 here, where
     the 2-column grid it was correcting for no longer exists. */
  .titleblock.is-scale-large .titleblock__col--projects,
  .titleblock.is-scale-large .titleblock__col--journal,
  .titleblock.is-scale-large .titleblock__col--copyright {
    padding-left: var(--side);
    padding-right: 0.125rem;
  }
  .titleblock.is-scale-large .titleblock__col--contact,
  .titleblock.is-scale-large .titleblock__col--portal,
  .titleblock.is-scale-large .titleblock__col--scale {
    padding-left: 0.125rem;
    padding-right: var(--side);
  }
  .titleblock.is-scale-large .titleblock__col--contact,
  .titleblock.is-scale-large .titleblock__col--portal,
  .titleblock.is-scale-large .titleblock__col--scale,
  .titleblock.is-scale-large .titleblock__col--copyright {
    margin-left: 0;
  }
  /* The hamburger stays, but as a single line rather than the 3-line
     "row map" used elsewhere — every row is already on screen here,
     so that map doesn't apply. Tapping it minimises the whole Large
     Menu back to the compact footer, per Liam ("single line hamburger
     menu that just minimises the whole thing"); the actual collapse
     runs through scale-toggle.js's shared state so the Scale & Theme
     caption/numbers stay in sync (see menu-toggle.js). */
  .titleblock.is-scale-large .hamburger span:nth-child(2),
  .titleblock.is-scale-large .hamburger span:nth-child(3) {
    display: none;
  }
  /* Journal entries and Copyright, per Liam ("make the journal entries
     longer so their ellipses align with the project numbers' end
     above" / "same with copyright") — 8.875rem, measured (Range on the
     actual index-number text, not its stretched grid-cell box — see
     the "measure text, not boxes" note elsewhere in this codebase) as
     the distance from Selected Work's left edge to where the indices
     actually stop rendering, not the wider column/box edge those
     numbers' cells stretch to fill. Copyright's paragraph wraps to
     fewer lines at this wider width than its previous 6.75rem. */
  .titleblock.is-scale-large .titleblock__col--journal .titleblock__link {
    width: 8.875rem;
  }
  .titleblock.is-scale-large .titleblock__col--copyright .copyright-list {
    width: 8.875rem;
  }
  /* Portal's underlines, per Liam ("make the client portal lines
     extend so its the same width as contact above it") — Portal now
     sits directly under Contact (both order-even, right column, same
     cell padding), and Contact's own grid is already width:100% of
     that column, so matching it here is just width:100% rather than
     a re-measured fixed value. */
  .titleblock.is-scale-large .portal-input { width: 100%; }
  /* Selected Work reverted to its pre-stretch fixed name track — same
     6.75rem as above (the compact 4.5rem ×1.5), so the index numbers
     sit right after the names again instead of pushed to the column's
     right edge. Scale keeps its full-width bar/numbers (not part of
     Liam's "too wide" complaint, and a graphic bar reads fine full-
     width where a text column doesn't). */
  .titleblock.is-scale-large .projects-grid { grid-template-columns: 6.75rem auto; }
  .titleblock.is-scale-large .scale-bar,
  .titleblock.is-scale-large .scale-bar__labels {
    width: 100%;
    max-width: none;
  }
  /* Bottom safe-area lift for the Large Menu's own last row — the
     compact layout's lift lives on .titleblock__row-main's padding
     (see its base rule), which stops existing here the moment that
     wrapper goes display:contents, so Copyright & Scale's cells carry
     it instead (new last row per the Copyright/Portal swap above), on
     top of their normal 0.625rem cell padding. Same env()+floor as
     everywhere: rises off the home indicator on iPhones, lifts
     "slightly" elsewhere. */
  .titleblock.is-scale-large .titleblock__col--copyright,
  .titleblock.is-scale-large .titleblock__col--scale {
    padding-bottom: calc(0.625rem + max(0.625rem, env(safe-area-inset-bottom, 0px)));
  }
  /* The hardcoded 0.5rem caption tier, +50% -> 0.75rem: credential
     lines, the two CTAs, Journal's Other Entries, scale numbers and
     captions. */
  .titleblock.is-scale-large .brand-credentials span,
  .titleblock.is-scale-large .projects-cta span,
  .titleblock.is-scale-large .contact-cta,
  .titleblock.is-scale-large .journal-cta,
  .titleblock.is-scale-large .scale-bar__labels,
  .titleblock.is-scale-large .scale-row__caption {
    font-size: 0.75rem;
  }
  /* North point rides the same +50% as the captions around it. */
  .titleblock.is-scale-large .north-point {
    width: 1.6875rem;
    height: 1.6875rem;
  }
}

/* ==========================================================================
   FULL-SCREEN MENU (.fsmenu)
   Opened by the brand row's hamburger. The footer now rests in its mini
   state and everything it used to hold lives here, per Liam.

   Phone-first: the rules below are the phone layout and the two min-width
   blocks at the end only widen it. Composition follows the reference Liam
   set - the mark with a rule running past it, the definition in Danish
   beside the English, navigation indented from the margin, and a right-hand
   buffer nothing crosses except the top-right control and the return arrow.

   No monospace anywhere, deliberately: per Liam it reads as machine-made and
   is not to appear on this site.
   ========================================================================== */
.fsmenu {
  position: fixed;
  inset: 0;
  z-index: 200;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* Edge to edge, per Liam - the inset left grey showing around the sheet.
     The sheet is the background now, so the overlay behind it never shows. */
  background: var(--bg);
  padding: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.4s ease, visibility 0s linear 0.4s;
  /* Nav and the foot sit in from the margin; the buffer is the clear gutter
     on the right. Both widen with the viewport. */
  --fsmenu-indent: 18%;
  /* Was a 12% right gutter. Removed per Liam; the bar and the return arrow
     that used to reach into it now simply sit at the margin. */
  --fsmenu-buffer: 0px;
  /* The sheet's own inline padding, named so a section rule can cancel it and
     bleed to the screen edge while its text stays on the margin. Overridden
     at the wider tiers alongside .fsmenu__sheet's padding - keep them in step. */
  --fsmenu-pad-x: 1.15rem;
}
.fsmenu.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.4s ease;
}
/* Liam: "the bottom of the viewable area should have a band that has
   that sort of 50% opacity and gets less visible toward the bottom like
   a gradient, this is to create language that people can scroll to
   reveal more."

   A fixed band across the foot of the open menu: the page's own ground
   at half strength where it meets the list, fading out downward, so the
   last row is read as continuing rather than ending. Fixed rather than
   absolute so it stays at the bottom of what is on screen while the
   sheet scrolls under it, and pointer-events:none so it never eats a tap
   on the row beneath.

   It goes away at the end of the scroll - .fsmenu.is-at-end, set by
   menu-scroll-hint.js - because a fade over the real last item is a lie
   about there being more. */
.fsmenu::after {
  content: "";
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: clamp(3.5rem, 9vh, 6.5rem);
  pointer-events: none;
  z-index: 1;
  opacity: 1;
  transition: opacity 0.35s ease;
  background: linear-gradient(to top,
              color-mix(in srgb, var(--bg) 50%, transparent) 0%,
              color-mix(in srgb, var(--bg) 30%, transparent) 45%,
              transparent 100%);
}
.fsmenu.is-at-end::after { opacity: 0; }

/* The same band at the top, per Liam, with one difference that matters:
   at the bottom the band is a hint that there is more below, so it fades
   the whole way. At the top it also has a job - the theme switch, Contact
   and Close sit on it - so it is OPAQUE across them and only begins to
   lose opacity below: "opaque on and above them and only start losing
   opacity a bit below them".

   It hangs off the bar rather than off .fsmenu, which is not a detail.
   .fsmenu__sheet carries a transform, so it is its own stacking context;
   a band painted on .fsmenu sits ABOVE the whole sheet and hid the very
   controls it was meant to sit behind. Inside the bar, at z-index -1, it
   paints behind the controls and over the rows scrolling past.

   The bar is sticky at the sheet's own top padding, so it stays exactly
   where it appears at the top of the menu - Liam: "i dont want them to
   move from where they appear now" - and the rows pass underneath the
   solid part instead of through it. --fsmenu-top-pad is that padding,
   named because each tier sets a different one and all three have to
   agree with .fsmenu__sheet's padding above. */
.fsmenu__sheet { --fsmenu-top-pad: calc(2.4rem + env(safe-area-inset-top)); }
.fsmenu__bar {
  position: sticky;
  top: var(--fsmenu-top-pad);
  z-index: 2;
}
.fsmenu__bar::before {
  content: "";
  position: absolute;
  z-index: -1;
  pointer-events: none;
  /* Up to the viewport's top edge, and out to both screen edges past the
     sheet's own inline padding. */
  top: calc(-1 * var(--fsmenu-top-pad));
  left: calc(-1 * (var(--fsmenu-pad-x) + env(safe-area-inset-left)));
  right: calc(-1 * (var(--fsmenu-pad-x) + env(safe-area-inset-right)));
  /* Solid to just past the controls, then the foot band's gradient,
     mirrored. */
  --fsmenu-bar-clear: calc(var(--fsmenu-top-pad) + 1.5rem);
  height: calc(var(--fsmenu-bar-clear) + clamp(2.5rem, 6vh, 4.5rem));
  background: linear-gradient(to bottom,
              var(--bg) 0%,
              var(--bg) var(--fsmenu-bar-clear),
              color-mix(in srgb, var(--bg) 50%, transparent) calc(var(--fsmenu-bar-clear) + 1.2rem),
              transparent 100%);
}

/* The wordmark is a link home, per Liam: "clicking lykke logo in the menu
   takes you to home page". A block link so the whole mark is the target,
   and the rule under the word stays on the heading rather than moving to
   the anchor - it belongs to the composition, not to the link. */
.fsmenu__home {
  display: block;
  line-height: 0;
}

.fsmenu__sheet {
  min-height: 100%;
  background: var(--bg);
  border-radius: 0;
  /* Clears the notch and the dynamic island; Close sat in the no-go zone
     without it. Needs viewport-fit=cover in the page's viewport meta, or
     env() resolves to 0 and this silently does nothing.

     The base was 1.15rem, which relied on env() carrying the rest. In Safari
     it does (about 59px on an iPhone 15), but in an installed PWA - which is
     how Liam actually runs this - iOS lays the content out BELOW the status
     bar already and reports safe-area-inset-top as 0. The bar then had only
     18px above it and sat right up against the island. 2.4rem is the floor
     that makes the unnotched and PWA cases breathe on their own; env() still
     adds on top wherever the browser genuinely hands us the inset. */
  padding: calc(2.4rem + env(safe-area-inset-top))
           calc(var(--fsmenu-pad-x) + env(safe-area-inset-right)) 0
           calc(var(--fsmenu-pad-x) + env(safe-area-inset-left));
  transform: translateY(12px);
  transition: transform 0.5s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.fsmenu.is-open .fsmenu__sheet {
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .fsmenu,
  .fsmenu__sheet {
    transition: none;
  }
}

/* The bar and the return arrow are the only things allowed into the buffer. */
.fsmenu__bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  /* Centre, not baseline: two of the three items are now inline SVGs, whose
     baseline is their bottom edge - on a baseline grid they hung visibly low
     against Contact's cap height. */
  align-items: center;
  margin-right: calc(-1 * var(--fsmenu-buffer));
}

/* Both bar icons. Sized just over the bar's cap height so they read as
   peers of Contact rather than as decoration, and stroked at 1 unit in a
   16-unit box so the line lands at the same hairline weight as the rules
   below once scaled. */
.fsmenu__icon {
  display: block;
  width: 0.95rem;
  height: 0.95rem;
  overflow: visible;
}
.fsmenu__bar a,
.fsmenu__bar button {
  /* Was pointed at a --font-body token, which is only ever defined in
     styles.css - a file this page does not load. A bare var() on an
     undefined property is invalid at computed-value time, so the
     declaration did not fall back, it became `inherit`. It inherited the
     same face from body, so nothing ever looked wrong, which is precisely
     why it sat here dead. */
  font-family: var(--font);
  font-size: 0.58rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
/* Qualified with .fsmenu__bar so it outranks `.fsmenu__bar a` above, which
   sets var(--ink). As a bare class it lost the cascade and Contact rendered
   in ink rather than the accent. */
/* Qualified with .fsmenu__bar, like .fsmenu__contact below it. The switch
   also carries .theme-toggle--inline, which sets justify-self:center to
   centre it over its caption in the footer - identical specificity to a bare
   .fsmenu__theme, so which one won came down to source order alone. It
   happened to land the right way; this makes it mean it. */
.fsmenu__bar .fsmenu__theme {
  grid-column: 1;
  justify-self: start;
}
.fsmenu__bar .fsmenu__contact {
  color: var(--fsmenu-accent);
  grid-column: 2;
  /* Liam: "Contact in the top of the menu needs to be in bold." It is the
     one thing in that bar you are being invited to press - the toggle is a
     setting and the cross is a way out - so it carries the weight. */
  font-weight: 700;
}
.fsmenu__close {
  grid-column: 3;
  justify-self: end;
}

/* Hero: the mark, then a rule that carries on past it. That overhang is most
   of what makes the composition read as composed rather than centred. */
.fsmenu__hero {
  /* The measuring basis for the mark and the strapline below, so 1cqw is
     exactly 1% of the hero's own content width. Percentages could not do
     this job: .fsmenu__mark's % resolves against .fsmenu__word, which is
     itself 92% here and 78% on desktop, and a font-size has no percentage
     basis in width at all. Containment is inline-size only - the hero's
     width still comes from its parent, so nothing about the layout moves. */
  container-type: inline-size;
  /* The reference holds a large, deliberate void between the bar and the
     display word - roughly three times the word's own cap height - and that
     emptiness is most of why the page reads as composed rather than merely
     top-aligned. Ours was 5vh, which just cleared the bar.

     Measured against the mark rather than guessed: LYKKE stands about 43px
     at this width, so ~3.3x is ~140px, which is what 16vh gives on a
     393x852 iPhone 15. The clamp keeps it sane on a short landscape phone
     and stops it running away on a tall desktop. */
  margin-top: clamp(3.5rem, 16vh, 8rem);
}
.fsmenu__word {
  margin: 0;
  line-height: 0;
  border-bottom: 1px solid var(--line-strong);
  width: 92%;
  padding-bottom: 0.55rem;
}
.fsmenu__mark {
  display: block;
  /* 50% of the sheet, per Liam. The first value is the fallback for anything
     without container queries - 54.3% of the 92%-wide rule is the same 50%,
     but only at this tier, which is exactly the fragility cqw removes. The
     rule itself still runs to 92%, so an overhang past the mark remains. */
  width: 54.3%;
  width: 50cqw;
  height: auto;
}
.fsmenu__mark--dark {
  display: block;
}
.fsmenu__mark--light {
  display: none;
}
[data-theme="light"] .fsmenu__mark--dark {
  display: none;
}
[data-theme="light"] .fsmenu__mark--light {
  display: block;
}

.fsmenu__sub {
  /* Set to the width of the mark above it, per Liam, rather than to a size
     that happens to look close.

     Derived, not guessed. Type scales linearly, so the rendered width of
     this string is a constant multiple of its font-size; measured on the
     glyphs with a Range (the <p> is full-width and its box says nothing),
     "Architects & Builders" in Helvetica 500 with this tracking runs
     9.4363x the font-size. To occupy 50cqw the size must therefore be
     50 / 9.4363 = 5.299cqw, and because both sides of that are container
     units the two stay locked at every width - no per-breakpoint retuning.

     The em-based letter-spacing below is already inside the 9.4363: it was
     measured on the rendered text, and em tracking scales with the size.
     Change the string, the weight or the tracking and the multiple has to
     be re-measured. */
  font-size: 1.18rem;
  font-size: 5.299cqw;
  margin: 0.6rem 0 0;
  font-weight: 500;
  /* Title case, per Liam - the words carry their own capitals from the
     markup. Uppercasing them made the line shout over the wordmark it sits
     under; the reference keeps everything below the display word quiet.
     Letter-spacing drops with it: tracking is a fix for all-caps, and on
     mixed case at this size it just loosens the words. */
  letter-spacing: 0.01em;
}
.fsmenu__meta em,
.fsmenu__da em {
  font-style: italic;
}
.fsmenu__meta {
  /* Up from 0.62rem, per Liam. */
  font-size: 0.7rem;
  line-height: 1.85;
  color: var(--ink-soft);
  margin: 0.7rem 0 0;
}

/* Danish beside English, side by side even on a phone - the pairing is the
   reason this composition suits the practice, so it does not collapse. */
.fsmenu__pair {
  display: grid;
  /* Even halves, not `auto 1fr`: with the fuller Danish entry the auto
     column took nearly all the width and wrapped the English to one word
     a line. minmax(0,...) so a long word cannot blow the track out. */
  /* Uneven, per Liam - the English takes the wider track. The Danish holds
     up because it simply wraps to shorter lines rather than being squeezed. */
  grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
  gap: 1.1rem;
  /* A real void between the English gloss of lykke and this pair, per Liam -
     the reference holds roughly a fifth of the screen empty there, and that
     pause is what stops the hero and the definition reading as one block.
     Was 6vh, which only separated them. */
  margin-top: clamp(2.6rem, 13vh, 6.5rem);
  /* Bottoms level, per Liam - the taller column grows UPWARD from a shared
     bottom edge, which is how the reference sets its own two-language pair.

     `end` rather than `last baseline`: the Danish is emptied and refilled by
     menu-typewriter.js, and an empty element contributes no baseline, so a
     baseline-aligned row would shift as the first character lands. Geometric
     alignment holds still through the animation. The two columns carry
     slightly different sizes, so their half-leading differs by well under a
     pixel - see the measurement in the commit. */
  align-items: end;
}
.fsmenu__da {
  /* Up from 0.56rem, per Liam. This also lands it on the same 0.62rem as
     .fsmenu__en beside it, so the pair now shares a size - which makes the
     bottom alignment exact rather than 0.11px out, since identical
     line-height and size means identical half-leading below the last
     baseline. */
  font-size: 0.62rem;
  line-height: 1.95;
  color: var(--ink-soft);
  margin: 0;
}

.fsmenu__en {
  margin: 0;
  font-size: 0.62rem;
  line-height: 1.95;
  text-align: right;
}

.fsmenu__groups {
  display: flex;
  /* No rule above the nav groups, per Liam. The reference does not draw one
     there either - the arrows and the gap already separate the group from
     the definition, and a rule made it read as a table header. The rules
     that remain open the Project Index and Journal sections, where they are
     doing real work. padding-top stays: it was the rule's breathing room and
     is now the whole separation. */
  padding-top: 1.1rem;
  gap: 0.9rem;
  margin-top: clamp(1.8rem, 6vh, 3rem);
  margin-left: var(--fsmenu-indent);
  flex-wrap: wrap;
  /* Ranged right, per Liam, as the reference does - the group sits against
     the right margin with the empty space opening to its left, rather than
     starting at the indent and trailing off. The indent above stays as a
     floor for narrow screens; justify-content only has space to distribute
     once the groups are narrower than the track.

     Only the ROW is pushed right. The words inside each group stay ranged
     left under their arrow, which is also what the reference does. */
  justify-content: flex-end;
}
.fsmenu__group {
  display: flex;
  gap: 0.3rem;
  text-decoration: none;
  color: var(--ink);
}
/* Drawn, not typed. U+2198 and U+21B0 both carry emoji presentations that
   iOS picks by default, which rendered them as coloured emoji. An inline
   SVG takes the ink colour and cannot be substituted by a font. */
.fsmenu__arw {
  flex: 0 0 auto;
  width: 0.62rem;
  height: 0.62rem;
  margin-top: 0.24em;
  color: var(--ink-soft);
}
.fsmenu__arw svg {
  display: block;
  width: 100%;
  height: 100%;
}
.fsmenu__totop svg {
  display: block;
  width: 0.85rem;
  height: 0.85rem;
}
.fsmenu__words {
  font-size: 0.95rem;
  line-height: 1.16;
  letter-spacing: -0.015em;
}
.fsmenu__n {
  color: var(--fsmenu-accent);
}
.fsmenu__group:hover .fsmenu__words {
  text-decoration: underline;
  text-underline-offset: 0.16em;
  text-decoration-thickness: 1px;
}

/* The rule sits BELOW the heading, per Liam, not above it.

   It is not a new rule - .fsmenu__rows already carried a border-top for
   exactly this, and it has been invisible since it was written because it
   was drawn against a --color-line token defined nowhere in the
   stylesheets these pages load. The fallback was bone white on a bone
   background, the same failure that hid every --color-line-strong rule.
   Repointed to --line (whose light value is the same 0.14 alpha the
   fallback was reaching for), which also brings back the hairline under
   each project row - the reference sets its lists as hairline tables, and
   that is what these rules were for.

   So this heading gives up its own border-top rather than gaining a second
   rule; a hairline above AND below would box the heading in, which the
   reference never does. */
.fsmenu__sec {
  font-size: 1.12rem;
  font-weight: 400;
  letter-spacing: -0.02em;
  /* Full bleed, per Liam. Negative margins cancel the sheet's inline padding
     so the rule reaches the screen edge; matching padding puts the words back
     on the margin. The reference does exactly this - its section rules cross
     the whole sheet while the text stays inset.

     2px against the 1px between projects: "double weight compared to
     hairlines between projects". Same --line tone, so it reads as heavier
     rather than darker. */
  border-bottom: 2px solid var(--line);
  margin: clamp(1.8rem, 6vh, 3rem)
          calc(-1 * (var(--fsmenu-pad-x) + env(safe-area-inset-right)))
          0.8rem
          calc(-1 * (var(--fsmenu-pad-x) + env(safe-area-inset-left)));
  padding: 0 calc(var(--fsmenu-pad-x) + env(safe-area-inset-right)) 0.5rem
             calc(var(--fsmenu-pad-x) + env(safe-area-inset-left));
}
.fsmenu__row {
  display: grid;
  grid-template-columns: 2.7rem 1fr;
  gap: 0.1rem 0.8rem;
  align-items: baseline;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: var(--ink);
}
/* Project and journal indices, per Liam: a darker blue and slightly bolder.

   The colour is now just --fsmenu-accent: Liam asked for one blue across the
   menu, so the deeper step became the accent rather than a second token.

   The weight is NOT font-weight. This site is set in Helvetica/Arial, which
   ships Regular and Bold and nothing between - measured, 400 and 500 render
   identically (93.23px for the same string) and 600 and 700 are both the
   same Bold (93.72px). So font-weight offers one step here, 400 to 700,
   which at 8.5px with this tracking is a shout rather than the "slightly"
   asked for.

   A hairline stroke in the text's own colour thickens the letterforms
   without touching their advance widths - so the indices stay a hair under
   a pixel wider than nothing, and the index column keeps the exact left
   alignment it had. Unsupported browsers simply render plain 400. */
.fsmenu__idx {
  font-size: 0.53rem;
  letter-spacing: 0.07em;
  color: var(--fsmenu-accent);
  -webkit-text-stroke: 0.25px currentColor;
}
.fsmenu__name {
  font-size: 0.82rem;
}
.fsmenu__alt {
  font-size: 0.62rem;
  color: var(--ink-soft);
  grid-column: 2;
}
.fsmenu__row:hover .fsmenu__name {
  text-decoration: underline;
  text-underline-offset: 0.16em;
  text-decoration-thickness: 1px;
}

.fsmenu__totop {
  display: block;
  margin: clamp(1.5rem, 5vh, 2.4rem) calc(-1 * var(--fsmenu-buffer)) 0 auto;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--ink);
  font-size: 1.15rem;
  line-height: 1;
}
/* Aligned to the index, not to the indented nav: A, E and the Instagram
   mark sit in the same column as the project and journal indices, per
   Liam. */
.fsmenu__foot {
  display: grid;
  /* Three tracks, per Liam: "right aligned is correct but about 2 tab spaces
     from the letters only".

     The contact block was ranged right against a 1fr track, which stretched
     it to the full sheet width and flung the text to the far edge. The middle
     track is max-content instead, so it is exactly as wide as its widest line
     and sits just past the labels - the block still ranges right, but to its
     own edge rather than the page's.

     It has to be a shared TRACK rather than a width on each dd: sized
     individually every entry would end at a different x, and the whole point
     is that they share a right edge. The empty 1fr third track exists so the
     copyright, which spans 1 / -1, still runs the full width. */
  grid-template-columns: 2.7rem max-content 1fr;
  gap: 0.55rem 0.8rem;
  margin: 1.4rem 0 0;
  padding: 1.3rem 0 calc(1.8rem + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line-strong);
  align-items: start;
}

/* Explicit columns, because auto-placement across three tracks would put
   the second row's dt in the third column and unravel the whole list. */
.fsmenu__foot dt {
  grid-column: 1;
  font-size: 0.78rem;
  color: var(--ink-soft);
}

/* Ranged right, per Liam. Two things fall out of it: the address gets the
   whole column instead of being capped at wherever the email happens to end,
   so it fits on two lines again; and every right edge is flush, so no line
   can overhang the one below it. It also makes the earlier hidden-prefix
   indent redundant - "hello@studiolykke.au" and "@studiolykke.au" share a
   suffix, and two right-flush strings align on a shared suffix for free.
   The A / E / Instagram labels stay left in their own column. */
.fsmenu__foot dd {
  grid-column: 2;
  margin: 0;
  /* The same size as a journal title, per Liam - so the address, the email
     and the Instagram handle read as entries in the menu rather than as a
     tier of their own. They track .fsmenu__name at every breakpoint, which
     is why the 1000px step that used to push these to 1.12rem is gone: the
     titles do not grow there either. */
  font-size: 0.82rem;
  line-height: 1.45;
  text-align: right;
}
.fsmenu__foot a {
  color: var(--fsmenu-accent);
  text-decoration: none;
}
.fsmenu__rights {
  grid-column: 1 / -1;
  /* Smaller, per Liam. 0.74rem made the copyright notice LARGER than the
     definitions above it (0.7rem) and the Danish pair (0.62rem) - the
     quietest text on the sheet was the loudest thing in the small tier.
     0.56rem is an existing step in this menu rather than a new one, and
     puts it below everything it should sit under. */
  font-size: 0.56rem;
  color: var(--ink-soft);
  margin: 1rem 0 0;
}

@media (min-width: 600px) {
  .fsmenu {
    --fsmenu-indent: 26%;
  }
  .fsmenu__sheet {
    --fsmenu-pad-x: 2rem;
    --fsmenu-top-pad: 1.6rem;
    padding: 1.6rem calc(2rem + var(--fsmenu-buffer)) 0 2rem;
  }
  .fsmenu__bar a,
  .fsmenu__bar button {
    font-size: 0.62rem;
  }
  .fsmenu__word {
    width: 88%;
    padding-bottom: 0.8rem;
  }
  /* .fsmenu__mark / .fsmenu__sub are container-unit sized and hold their
     40cqw relationship through this tier's wider padding on their own. */
  .fsmenu__meta,
  .fsmenu__da,
  .fsmenu__en {
    font-size: 0.95rem;
  }
  .fsmenu__words {
    font-size: 1.5rem;
  }
  .fsmenu__groups {
    gap: 1.6rem;
  }
  .fsmenu__sec {
    font-size: 1.5rem;
  }
  .fsmenu__row {
    grid-template-columns: 3.4rem 1fr 1fr;
  }
  .fsmenu__alt {
    grid-column: 3;
    text-align: right;
  }
  .fsmenu__name,
  .fsmenu__alt {
    font-size: 0.95rem;
  }
  .fsmenu__foot dd {
    font-size: 0.95rem;
  }
}
@media (min-width: 1000px) {
  .fsmenu {
    --fsmenu-indent: 22%;
  }
  .fsmenu__sheet {
    --fsmenu-pad-x: 3rem;
    --fsmenu-top-pad: 2.2rem;
    padding: 2.2rem calc(3rem + var(--fsmenu-buffer)) 0 3rem;
  }
  .fsmenu__word {
    width: 78%;
    padding-bottom: 1.1rem;
  }
  /* No .fsmenu__mark / .fsmenu__sub overrides here any more: both are in
     container units now and track the hero's width on their own, including
     through this tier's wider padding. The old pair (50% of a 78% rule, and
     1.15rem) worked out to almost exactly the same 39% of the hero, so this
     is the proportion that was already here - now stated once. */
  .fsmenu__pair {
    grid-template-columns: 1fr 1fr;
  }
  .fsmenu__words {
    font-size: 2rem;
  }
  .fsmenu__groups {
    gap: clamp(2rem, 6vw, 5rem);
  }
  .fsmenu__sec {
    font-size: 1.9rem;
  }
  .fsmenu__foot {
    grid-template-columns: 1.8rem max-content 1fr;
  }
}

/* ---- Sticky footer on included-footer pages ----
   The landing page fits the viewport exactly (.sheet-body is
   position:fixed, overflow:hidden), so its footer is always on screen
   by construction. Every other page scrolls normally and injects the
   same markup into <div data-footer-include> as body's last child, so
   the footer sat at the very bottom of the document and only appeared
   after scrolling the whole page - per Liam, "footer is there but
   should be sticky".

   Sticky rather than fixed, deliberately: sticky keeps the element in
   normal flow, so it still reserves its own height at the end of the
   document and nothing has to be padded to clear it. A fixed footer
   would need every page to carry a matching padding-bottom that has
   to be kept in step with the footer's real height by hand - and the
   footer's height is not constant (the mini tab, the expanded rows,
   and the safe-area inset all change it).

   Targets .titleblock, not the [data-footer-include] placeholder:
   footer-include.js assigns to slot.outerHTML, which REPLACES the
   placeholder rather than filling it, so that attribute is gone from
   the DOM by the time any of this matters. The <footer> it leaves
   behind is a direct child of <body>, which is what sticky needs -
   an element can never move outside its own parent's box, so a tall
   parent is the whole requirement.

   The background is required, not decorative - the footer is
   otherwise transparent and page content would scroll visibly
   through it. */
/* ---- Status-bar fill on scrolling pages ----
   viewport-fit=cover lets the page extend under the iPhone's status bar,
   which the footer's safe-area handling needs. On the landing page that is
   fine: .sheet-body is fixed and its own top band keeps content clear. On a
   page that SCROLLS, content slides up under the clock and the signal icons
   and reads as a mess - Liam, on Gardhus: "words sit behind the iphone clock
   and wifi symbols".

   A fixed strip exactly the height of the top inset, in the page's own
   paper, painted over whatever scrolls beneath it. env() resolves to 0
   anywhere there is no inset, so on a desktop this is a zero-height box
   that costs nothing. pointer-events:none so it never intercepts a tap on
   the one row of content that can sit under it before scrolling. */
body:not(.sheet-body)::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-top, 0px);
  background: var(--bg);
  z-index: 60;
  pointer-events: none;
}

body:not(.sheet-body) > .titleblock {
  position: sticky;
  bottom: 0;
  z-index: 40;
  background: var(--bg);
  /* Clears the home indicator. Liam, on a phone: the Gardhus footer is
     clipped where the landing page's is not - and the reason the landing
     page escapes it is that .sheet-body is position:fixed at var(--vvh),
     the VISUAL viewport height, which already excludes that strip.

     A sticky footer on a normally-scrolling page sits at the true viewport
     bottom instead, under the indicator. The footer's own safe-area padding
     lives on .titleblock__row-main, and row-main is display:none in the
     mini state that every page now uses - so nothing was reserving it. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* ---- Phone in landscape: a thin, centred footer ----
   Per Liam: "if i rotate to landscape. footer should be nice and thin.
   dont worry about the curves. just move the elements toward the center and
   space them out neatly."

   A rotated phone (852x393) falls into the min-width:768px tier above, which
   was built for tablets and desktops: 1.5rem of padding top and bottom made
   the footer 84px tall, and the brand row kept its portrait layout - logo
   hard left, instruments pushed right by margin-left:auto, hamburger
   absolute at the row's right edge - which on an 852px row meant everything
   huddled in the first 320px with 530px of nothing after it.

   Keyed on HEIGHT, not just orientation: max-height:500px is true of every
   phone on its side and false of every tablet and desktop, which is the
   distinction the old orientation-only query could not make. Lateral safe
   areas (the notch, the rounded corners) are deliberately not handled -
   "dont worry about the curves".

   Same selectors as the mini rules they override, so they win on source
   order alone rather than needing more specificity. */
@media (orientation: landscape) and (max-height: 500px) {
  /* Half the height again, per Liam: "you can make it half of what it is
     now (only in landscape mode) because the curves of the phone screen
     arent close enough to cut anything off." So no safe-area inset at all
     here - not on this cell, and not the one the sticky rule adds below.
     The first pass measured 43px, and on the phone the sticky rule's
     env(safe-area-inset-bottom) sat under that too, so it read taller
     still. Target is ~21px. */
  .titleblock.is-mini .titleblock__col--brand {
    flex: 1 1 100%;
    padding-top: 0.1rem;
    padding-bottom: 0.1rem;
    padding-right: var(--side);
  }
  body:not(.sheet-body) > .titleblock {
    padding-bottom: 0;
  }
  /* Padding alone cannot halve it - the content was 24-27px tall. Every
     piece scales down together so the row keeps its proportions:
     instrument stack 24 -> 16px, north point 27 -> 16px, logo 21 -> 14px,
     credentials 18 -> 14px, hamburger 13.5 -> 11px. */
  .titleblock.is-mini .titleblock__logo {
    height: 0.875rem;
  }
  .titleblock.is-mini .brand-row__instruments {
    grid-template-rows: 0.5rem 0.5rem;
  }
  .titleblock.is-mini .brand-row__instruments .scale-bar,
  .titleblock.is-mini .brand-row__instruments .scale-bar__labels {
    height: 0.5rem;
  }
  .titleblock.is-mini .brand-row__instruments .scale-bar__labels {
    font-size: 0.42rem;
  }
  .titleblock.is-mini .brand-row__instruments .north-point {
    width: 1rem;
    height: 1rem;
  }
  .titleblock.is-mini .brand-credentials span {
    font-size: 0.42rem;
    line-height: 1.05;
  }
  .titleblock.is-mini .hamburger {
    width: 0.7rem;
    height: 0.7rem;
    gap: 0.11rem;
  }
  /* Centred and evenly spaced. The reserved right padding existed only to
     keep content clear of the absolutely-positioned hamburger, which now
     sits in the flow with everything else. */
  .titleblock.is-mini .brand-row {
    justify-content: flex-start;
    align-items: center;
    padding-right: 0;
  }
  /* Instruments stop being pushed to the right edge, and their internal
     gap matches the row's. Without that the measured spacing read
     44 / 44 / 20 / 44: north point and scale bar still clustered as a
     pair (the grid's own 1.25rem column-gap) while everything else sat
     2.75rem apart. "Space them out neatly" means one gap, not two. */
  .titleblock.is-mini .brand-row__instruments {
    margin-left: 0;
    margin-right: 0;
  }
  /* ...credentials stop being offset from the logo... */
  .titleblock.is-mini .brand-credentials {
    margin-left: 0;
    transform: none;
  }
  /* ...and the hamburger takes its place in the row rather than floating at
     the edge, so the centring includes it. */
  .titleblock.is-mini .hamburger {
    position: static;
    transform: none;
  }
}

/* ---- Photographs are not for saving ----
   Liam: "across the board make saving photos hard". The part CSS can do:
   no drag-out, no long-press sheet on an iPhone, no selection halo when a
   swipe overshoots. photo-guard.js takes the right-click menu and the
   video download control; see it for the rest of the reasoning. */
img, picture, video {
  -webkit-user-drag: none;
  user-drag: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* Icons at 2x on desktop - the menu's share of the block that lives at
   the end of reference-layout.css. Here because this file loads AFTER
   that one, so these selectors would otherwise beat it on source order
   and the menu would keep its phone-sized glyphs. See that block for the
   reasoning; the multiplier is the same. */
@media (min-width: 60rem) {
  /* Liam: "make the logo and texts a bit bigger too please."

     The whole title block is driven by three tokens, so raising them
     here lifts the logo, the labels and the values together and keeps
     their relationship to each other exactly as drawn. About a quarter
     up, which matches the step the icons took without shouting. The logo
     follows automatically - its height is --tb-size-lg. */
  .titleblock {
    /* 1.75rem, then "make the logo bigger again in the footer": 2.25rem.
       The logo's height IS this token, so nothing else needs touching. */
    --tb-size-lg: 2.25rem;
    --tb-size-md: 0.8rem;
    --tb-size-sm: 0.72rem;
  }

  /* Liam: "thicker line weights too". The icons grew, but the strokes
     grew only in proportion, so they were still hairlines - just longer
     ones. These are set as presentation ATTRIBUTES on the paths
     (stroke-width="1"), which any stylesheet outranks, so the weight can
     be lifted here without touching the markup. */
  .fsmenu__icon path,
  .fsmenu__totop svg path { stroke-width: 1.8; }
  /* Drawn as bars rather than a stroke, so its "line weight" is a
     height. */
  .hamburger span { height: 1.5px; }

  .fsmenu__icon { width: 1.9rem; height: 1.9rem; }
  .fsmenu__arw { width: 1.24rem; }
  .fsmenu__totop svg { width: 1.7rem; height: 1.7rem; }
  /* Drawn as bars rather than an svg, so the gap scales with the box or
     the three lines close into a block. */
  /* "hamburger needs to be reduced in size" - it went to 2x with every
     other icon and was the one that did not want it: three bars at that
     size read as a heading rule rather than a control. */
  .hamburger {
    width: 1.125rem;
    height: 1.125rem;
    gap: 0.1875rem;
  }
}


/* ==================================================================
   Footer, desktop only (2026-09-11)
   ------------------------------------------------------------------
   Liam: "make the logo bigger again in the footer. make the arch reg and
   builders lic 30% bigger too. same for the north point and scale bar.
   hamburger needs to be reduced in size. lets put a blue Book a meeting,
   left of the north point too. all of this is desktop only." And a
   "Follow us" sign-up to the right of the credentials.

   Every rule is qualified .titleblock.is-mini, because that is the
   specificity of the rules it replaces - an unqualified override here
   would lose to them and do nothing, silently.

   The 30% is applied to the real numbers, stated rather than calculated:
   credentials 0.5rem -> 0.65rem (a literal that the size tokens raised
   earlier never reached, so this is its first real change); north point
   1.125rem -> 1.4625rem; scale bar 4.75 x 0.75rem -> 6.175 x 0.975rem.
   ================================================================== */
.footer-book,
.footer-follow { display: none; }

@media (min-width: 60rem) {
  .titleblock.is-mini .brand-credentials span { font-size: 0.65rem; }

  /* The two scale rows grow with the bar, and the north point's drop -
     which lands its bottom on the scale bar's - is half the new height
     difference: (2 x 0.975 - 1.4625) / 2. */
  .titleblock.is-mini .brand-row__instruments {
    grid-template-rows: 0.975rem 0.975rem;
  }
  .titleblock.is-mini .brand-row__instruments .scale-bar,
  .titleblock.is-mini .brand-row__instruments .scale-bar__labels {
    width: 6.175rem;
    height: 0.975rem;
  }
  .titleblock.is-mini .brand-row__instruments .scale-bar__labels { font-size: 0.65rem; }
  .titleblock.is-mini .brand-row > .north-point {
    width: 1.4625rem;
    height: 1.4625rem;
    transform: translateY(0.24375rem);
    /* Book a meeting now takes the growing space on its left, so the pair
       sits together at the right of the row. */
    margin-left: 0;
  }

  /* Liam, second pass: "all that text needs to be same size as the arch
     and builder stuff, and fit onto the same lines as them. follow us is
     aligned to arch reg no. and email is aligned to builders license.
     book a meeting is on bottom line, not bold, keep the blue."

     So everything here is set exactly as the credentials are - 0.65rem,
     line-height 1.1, bottom-aligned in the row with the same 2px drop -
     and the two hairlines are drawn as background gradients, which take
     no layout height. A real border would push each line down a pixel
     and the rows would stop matching. */
  /* Liam, third pass: "Book a meeting can actually get raised up so its
     vertical center is aligned to the centre of the north point circle".
     Given the north point's exact box - same height, same bottom
     alignment - and its text centred in that box. Sharing the north
     point's 0.24375rem drop left the text 5.6px low at 1440 (measured with
     a Range on the text against the circle), because the font's line box
     sits below its glyphs' optical centre; the drop here is that
     measurement taken off: 0.24375rem - 0.35rem. */
  .titleblock.is-mini .footer-book {
    display: inline-flex;
    align-items: center;
    height: 1.4625rem;
    margin-left: auto;
    align-self: flex-end;
    transform: translateY(-0.10625rem);
    font-family: var(--font);
    font-size: 0.65rem;
    line-height: 1.1;
    font-weight: 400;
    color: var(--fsmenu-accent, #232fb0);
    text-decoration: none;
    white-space: nowrap;
  }
  .titleblock.is-mini .footer-book:hover,
  .titleblock.is-mini .footer-book:focus-visible {
    text-decoration: underline;
    text-underline-offset: 0.2em;
  }

  .titleblock.is-mini .footer-follow {
    position: relative;
    display: grid;
    grid-template-rows: auto auto;
    align-self: flex-end;
    transform: translateY(2px);
    width: 9rem;
    margin: 0;
  }
  .footer-follow__label,
  .footer-follow__input {
    font-family: var(--font);
    font-size: 0.65rem;
    line-height: 1.1;
    font-weight: 400;
    color: var(--ink-soft);
  }
  .footer-follow__label {
    margin: 0;
    background: linear-gradient(var(--line-strong, currentColor), var(--line-strong, currentColor)) left bottom / 100% 1px no-repeat;
  }
  .footer-follow__row {
    display: flex;
    align-items: center;
    background: linear-gradient(var(--line, rgba(128, 128, 128, 0.45)), var(--line, rgba(128, 128, 128, 0.45))) left bottom / 100% 1px no-repeat;
  }
  .footer-follow__input {
    flex: 1 1 auto;
    min-width: 0;
    height: 1.1em;
    padding: 0;
    border: 0;
    background: transparent;
    outline: none;
  }
  .footer-follow__input::placeholder { color: var(--ink-soft); opacity: 0.55; }
  .footer-follow__submit {
    flex: 0 0 auto;
    display: inline-flex;
    padding: 0 0 0 0.3rem;
    border: 0;
    background: transparent;
    color: var(--ink-soft);
    cursor: pointer;
  }
  .footer-follow__submit svg { width: 0.65rem; height: 0.65rem; display: block; }
  .footer-follow__submit:hover,
  .footer-follow__submit:focus-visible { color: var(--ink); }
  .footer-follow__row:focus-within {
    background-image: linear-gradient(var(--ink), var(--ink));
  }
  /* The status sits under the form rather than in it, so a "Thanks" or
     an error never pushes the two lines out of step with the credentials. */
  .footer-follow__status {
    position: absolute;
    top: 100%;
    left: 0;
    margin: 0.15rem 0 0;
    font-family: var(--font);
    font-size: 0.65rem;
    line-height: 1.1;
    color: var(--ink-soft);
    white-space: nowrap;
  }
  .footer-follow__status:empty { display: none; }
  .footer-follow__status.is-error { color: var(--city, #a4442f); }
  /* Sent: the thanks takes the email line's place, so the block still
     reads as two lines. */
  .footer-follow.is-sent .footer-follow__row { display: none; }
  .footer-follow.is-sent .footer-follow__status { position: static; margin: 0; }
  /* The honeypot: off screen, never display:none - bots skip hidden
     fields but fill positioned-away ones. */
  .footer-follow .follow-form__trap {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
  }
}

/* Desktop homepage: the Palme Hus film on its own - Liam: "on desktop just
   play the palme hus video on landing page. keep both videos for smaller
   resolutions". The Skjul half goes, and the Palme half, the split's only
   remaining child, fills the whole canvas (flex 1 1 50% in a column of
   one). Its video ships preload="none" and hero-loading.js only loads the
   halves on screen, so the hidden one costs no download here. */
@media (min-width: 60rem) {
  .sheet__canvas-split-half[data-split-half="skjul"] { display: none; }
}
