/* ==========================================================================
   Layout reference clone — structure/spacing only.
   Inverted palette (dark bg, light ink) — same structure as the reference
   image, colors flipped. Generic placeholder text, blank image blocks.
   Deliberately not wired into the Studio Lykke design system (fonts,
   content) per "forget everything for now", but this file is becoming
   the shared project-page template as it's refined.
   ========================================================================== */

* { box-sizing: border-box; }
/* ---- No double-tap zoom, anywhere ----
   Per Liam. touch-action:manipulation tells the browser this element will
   never need the double-tap gesture, so it stops reserving ~300ms after
   every tap to find out whether a second one is coming - taps land
   immediately as well as never zooming.

   Deliberately NOT user-scalable=no in the viewport meta, which is the
   other way to stop double-tap zoom: that kills PINCH zoom too, which is
   how someone with poor eyesight reads a page, and iOS has ignored it since
   iOS 10 regardless. manipulation leaves pinch and scrolling untouched.

   On the root, because the browser resolves a gesture against the touched
   element AND its ancestors - one declaration here covers every descendant
   without a universal selector. */
html {
  touch-action: manipulation;
}

/* overflow-x: clip, not hidden — hidden on only one axis forces the
   OTHER axis's computed overflow to auto too (a real CSS spec quirk:
   "if one of overflow-x/overflow-y is visible and the other isn't, the
   visible one computes to auto"), which breaks position:sticky for any
   descendant whose containing block resolves through html/body — this
   silently broke .scroll-video__stage's scroll-linked pin (see Section
   2). clip has the same horizontal-scrollbar-prevention effect without
   that side effect. */
html, body { margin: 0; padding: 0; overflow-x: clip; }
a { color: inherit; text-decoration: none; }
button { font: inherit; background: none; border: none; cursor: pointer; color: inherit; }
table { border-collapse: collapse; width: 100%; }

:root {
  /* Dark palette matched to Claude's own dark theme, per Liam ("a
     lighter shade, like what the claude browser uses... not stark
     white text either") — pulled directly from claude.ai's computed
     styles: --cds-page-bg #151515, --cds-text-primary #f0efec,
     --cds-text-secondary #c3c2b7. Was #0c0c0b/#f3f2ee/#a8a6a0 (near-
     black, slightly duller secondary) before this pass. */
  --ink: #f0efec;
  --ink-soft: #c3c2b7;
  --line: rgba(240, 239, 236, 0.16);
  /* The heavier rule the full-screen menu and project pages draw under a
     title and above each section - the reference's hairlines, which sit a
     step stronger than --line's faint field divisions.

     It exists because those rules were written against --color-line-strong,
     which is only ever DEFINED in styles.css. Neither the landing page nor
     the project pages load that file, so every one of them fell through to
     the hardcoded fallback rgba(243, 242, 238, 0.28) - bone white, i.e. the
     light theme's own background painted onto the light theme's background.
     The hairlines were never missing, just invisible, which is exactly the
     failure mode a token defined in only one theme produces. */
  --line-strong: rgba(240, 239, 236, 0.30);
  --bg: #151515;
  /* Dark theme takes a saturated tint rather than the light theme's hex:
     #3a45f0 sits too close to a #151515 ground to read as bright. */
  /* One blue for the whole menu, per Liam. Set to the deeper step he asked
     for on the indices rather than keeping two. On the dark ground "deeper"
     is a more saturated tint rather than a darker one, which would lose it
     against the background. */
  --fsmenu-accent: #7a8bff;
  --blank: #232322;
  /* The drawing-sheet hairline: the scale bar, the north point's circle and
     the theme switch's track are all drawn at this one weight, per Liam
     ("all one hairline width, same width as the toggle in the menu"). The
     north needle is the single exception and takes double.

     Not theme-dependent, so it is declared once here rather than in both
     palettes. Real CSS pixels everywhere it is used - the two SVGs carry
     vector-effect="non-scaling-stroke", which draws the stroke in screen
     space, so this value is not distorted by their viewBox scaling. */
  --hairline: 0.6px;
  --side: 1.75rem;
  --max-width: 1400px;
  --font: Helvetica, Arial, sans-serif;

  /* Project-page type scale, per Liam ("smaller fonts for the titles...
     more thematic with the footer's texts and hierarchies"). Applies
     site-wide (any page built from this template — skjul-hus.html,
     palme-hus.html, future ones), not scoped to the new footer-shell
     pages specifically, since "everything on project pages" reads as a
     general design-system statement, not tied to whether a given page
     currently has the shared footer included yet.

     Named and sized to echo landing-page.css's own --tb-size-lg/md/sm
     tokens (1.375rem/0.625rem/0.5625rem) without literally reusing
     those variable names — this stylesheet isn't the footer, it drives
     a real long-scrolling article with actual reading copy, so it
     needs its own tier for paragraph text (--proj-size-sm) that stays
     legible at a real reading length rather than matching the footer's
     tiny 9px caption size, which is fine for a one-line caption but not
     multi-sentence body copy. --proj-size-md, by contrast, DOES match
     the footer's --tb-size-md exactly (0.625rem) — section labels here
     ("1 Introduction") are structurally the same eyebrow-caption role
     as the footer's own labels ("Selected Work"), so there's no reason
     for them to differ. */
  --proj-size-lg: 1.75rem;     /* project title ("Skjul Hus") — was clamp(2.5rem, 6vw, 4.5rem) */
  --proj-size-index: 0.875rem; /* project index — half of --proj-size-lg, same ratio as before */
  --proj-size-md: 0.625rem;    /* section labels, table headers, captions — matches the footer's own label tier */
  --proj-size-sm: 0.6875rem;   /* body copy, table values, pagination — was a uniform 0.75rem throughout */
}

/* Light mode: a true inversion of the dark values above (--ink and --bg
   literally swap), set via [data-theme-toggle] in theme-toggle.js.
   Everything downstream is built on these variables, so this one block
   is the whole theme swap — see theme-toggle.js for persistence /
   system-preference defaulting. */
:root[data-theme="light"] {
  --ink: #0c0c0b;
  --ink-soft: #5c5b57;
  --line: rgba(12, 12, 11, 0.14);
  --line-strong: rgba(12, 12, 11, 0.28);
  --bg: #f3f2ee;
  /* Electric, not navy. #1f31b5 read as dark ink rather than an accent -
     per Liam it was "still not popping". The reference spends its whole
     colour budget on one vivid blue, so this has to carry that alone. */
  --fsmenu-accent: #232fb0;
  --blank: #e4e3df;
}

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 12px;
  line-height: 1.5;
}

.ref-blank { background: var(--blank); }

/* Same mobile-Safari scroll lock as landing-page.css's .sheet-body —
   Safari ignores overflow:hidden on <body> for page panning, so a
   swipe that starts ON THE FOOTER (which lives outside .project-scroll,
   so the touch falls through to the body) panned the whole page up and
   revealed dead space below it on Liam's phone. A fixed-position body
   is the lock that actually holds there; desktop never reproduced
   this because it honors the overflow.

   height, through two rounds: first dropped entirely in favour of
   bare inset:0 (inset:0 alone pins all four edges exactly, no
   viewport-unit math needed, in theory) — 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 window.visualViewport) 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). */
.project-body {
  position: fixed;
  inset: 0;
  height: var(--vvh, 100dvh);
  overflow: hidden;
  overscroll-behavior: none;
  /* No pinch-zoom on the project pages, per Liam - zooming on his iPhone
     pulled the footer and the frame out of place.

     The cause is the same viewport split the comment above is about: a
     pinch shrinks the VISUAL viewport while position:fixed stays anchored
     to the LAYOUT one, so this shell and everything fixed inside it - the
     frame, the back link, the footer - stop agreeing with the content
     being looked at.

     touch-action rather than `user-scalable=no` in the viewport meta,
     because iOS Safari has deliberately ignored that since iOS 10 and
     would have changed nothing on the device the problem was reported on.
     touch-action IS honoured. pan-y, not none, so the sheet still scrolls
     under the finger.

     The cost is real: this also removes pinch-zoom as a way to read small
     text. It is survivable here because nothing on these pages depends on
     zooming to be usable, and iOS's system-level Accessibility > Zoom
     still works over the top of it - but it is the reason not to copy this
     onto a text-heavy page. */
  touch-action: pan-y;
}
/* .project-scroll is the element the finger actually drags, and it inherits
   nothing here - touch-action is not inherited - so the pinch has to be
   refused on it too or the gesture simply lands one level down. */
.project-scroll {
  touch-action: pan-y;
}

/* The journal articles share this shell but are long-form reading, which is
   exactly where pinch-zoom earns its keep, so they opt back out. Explicit
   class rather than :has() so the rule cannot fail open on an older
   browser and silently take zoom away from a page of body copy. */
.project-body--readable,
.project-body--readable .project-scroll {
  touch-action: auto;
}

/* "Back to [Page Name]" directory, per Liam — lives INSIDE the top
   frame band (the doubled-height strip above the hairline, see
   .project-shell's --frame-y), not in the page content, so it stays
   put while the sheet scrolls behind the frame and never collides
   with anything. Fixed positioning is safe here because the body
   itself is fixed (.project-body) — there's no page scroll for this
   to drift against. Typography matches the footer's label tier
   (--proj-size-md, uppercase, soft ink), same hierarchy family as
   "Selected Work". */
.frame-back {
  position: fixed;
  top: 0;
  left: calc(var(--frame, 0.875rem) + 1px); /* flush with the hairline's inner edge */
  /* Matches .project-scroll's own top margin exactly (below) — this
     band is what that margin is measured against, so if the two ever
     drift apart, the link ends up mispositioned relative to where
     content actually starts. Both grow with env(safe-area-inset-top),
     and both got the same 2x -> 3x --frame bump per Liam's "starting
     right at where the curve finishes" follow-up. */
  height: calc(var(--frame-y, 1.75rem) + var(--frame, 0.875rem) + env(safe-area-inset-top, 0px));
  display: flex;
  /* Was align-items:center — centering across a band that STARTS at
     the true screen edge puts the text at roughly half the status
     bar's own height, i.e. still on top of it on a real device (per
     Liam, looking at his actual phone: "back to links... currently on
     the iphone time"). flex-end plus a little breathing room instead
     sits it at the BOTTOM of the band — same fix in spirit as
     .sheet__canvas's own top clearance in landing-page.css: below the
     hardware-measured minimum, with room past it, not centered across
     it. */
  align-items: flex-end;
  padding-bottom: 0.375rem;
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.frame-back:hover { color: var(--ink); }

/* Liam: "Back to home also doesn't work."

   It worked - it was buried. The fullscreen gallery is a fixed overlay at
   z-index 60 and this link has no z-index at all, so in fullscreen it sat
   underneath: visible through nothing, untappable, and since tap-to-
   advance landed it turned a press on "back to home" into the next photo.

   A lightbox should own the screen, so the link is hidden rather than
   raised - there is a close button, an Escape key and the toggle to get
   out. What must not happen is a control that looks live and is not. */
html.has-gallery-fullscreen .frame-back,
html.has-gallery-fullscreen .frame-title { display: none; }

/* ---- Project-page shell (persistent footer) ----
   Per Liam: the shared footer (footer-partial.html, see footer-include.js)
   sits at the bottom of every page, and this page's own content occupies
   the space above it rather than being covered by it — "the viewport of
   that page starts above the hairline above the LYKKE logo." Same
   flex-column-with-a-scrollable-middle mechanic landing-page.css already
   uses for .sheet/.sheet__canvas, just under different names (.sheet__canvas
   there is a static blank "sheet," never scrollable; this one holds a real
   long-scrolling project page, so it needs overflow-y:auto, which that
   pattern doesn't).

   --side re-declared at 0.875rem here (was inherited at the site-wide
   1.75rem before) — per Liam, the buffer around a project page's content
   should match the footer's own buffer exactly: same distance top,
   left, and right, and at the bottom measured up from the footer's own
   top hairline (see .project-scroll's own padding-bottom below), not
   the page's absolute bottom edge. Every section on this page already
   insets itself with var(--side) (hero margin, title-block/section-nav/
   main padding, row grids...), so re-scoping the variable here tightens
   the WHOLE page's spacing at once rather than hunting down each rule —
   also serves "everything on project pages should be more thematic with
   the footer" for spacing specifically (the type-scale pass for that is
   further down this file, at .ref-title-block/.ref-label). This is a
   local re-declaration, not a change to reference-layout.css's own
   :root value — a page not wrapped in .project-shell would keep the
   original 1.75rem (both project pages have the shell now; this only
   matters for a hypothetical future page built without it).

   No .ref-nav on this page anymore (see the comment above <body> in
   skjul-hus.html) — .project-scroll's own top edge is now
   whatever the first real content element's own var(--side) margin/
   padding provides (.ref-hero's, currently), same mechanism as the
   left/right buffer, no separate top-specific rule needed here.

   Scrollbar hidden (Firefox/IE via property, Chrome/Safari via the
   pseudo-element below) per Liam ("no scroll wheels top or bottom,
   ever") — content still scrolls by touch/wheel/keyboard, just without
   the native scrollbar track/thumb ever being drawn. */
.project-shell {
  /* The frame width, per Liam ("the same size the footer buffer has") —
     scoped here rather than at :root, since the "buffer measured from
     the footer's top hairline" concept only makes sense on a page that
     actually HAS the shared footer included (both project pages do now).

     --side zeroed for everything INSIDE the shell, per Liam ("there
     doesn't have to be another margin inside of that border, everything
     can bleed up to the border line") — the frame itself is the page's
     one and only inset now, so every section's old var(--side) edge
     inset collapses to flush-with-the-hairline. 0rem, not bare 0: some
     rules use it in calc() addition (e.g. calc(var(--side) + 3rem)),
     where a unitless 0 makes the whole expression invalid. The footer
     is unaffected — .titleblock declares its own --side. */
  --frame: 0.875rem;
  /* Top/bottom bands are double the side width, per Liam ("make the
     frame double as thick top and bottom") — the extra headroom up top
     is also what the "Back to ..." directory link sits in. */
  --frame-y: calc(var(--frame) * 2);
  --side: 0rem;
  display: flex;
  flex-direction: column;
  /* 100%, not 100dvh — inherits .project-body's own exact, inset:0-
     derived height instead of computing an independent (and possibly
     slightly different) dvh value. See .project-body's own comment
     above for why that gap matters here specifically. */
  height: 100%;
  overflow: hidden;
}
.project-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Vertical is the ONLY scroll axis, per Liam ("no scrolling across
     the page, the mobile view is just scrolling down") — hidden, not
     clip, so it pairs with overflow-y:auto as a scroll container while
     guaranteeing an accidentally-too-wide element (like the 75vh photo
     sizing bug this frame surfaced) can never drag the sheet sideways
     across the hairline again; it just gets cropped at the frame. */
  overflow-x: hidden;
  /* Reaching the top/bottom of this inner scroll must not chain into
     the page rubber-band on iOS — that bounce is the other way the
     "dead space" below the footer could flash into view. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* The buffer is MARGIN on the scroll container, not padding inside
     it, per Liam ("a frame around the viewport... when you scroll down
     or up there is always a gap top and bottom between content and
     edge of screen or between content and footer"). Padding scrolls
     with the content — mid-scroll, content slid right up to the screen
     edge and the footer hairline, so the gap only existed at the
     extremes. Margin sits OUTSIDE the overflow clip: content shears
     off at the frame's inner edge and the gap holds at every scroll
     position — top against the screen edge, bottom against the
     footer's top hairline.

     All four sides, not just top/bottom — the frame carries its own
     hairline border (Liam's follow-up), and a border on this container
     only lands at the frame line if the container itself is inset from
     the screen edges. Content scrolling past clips at the hairline
     like a sheet sliding behind a window mat, and bleeds right up to
     the border with no inner margin (--side is 0rem inside the shell,
     see .project-shell above). The two old full-bleed sections
     (.scroll-video, .drawing-carousel) had their breakouts removed as
     part of this — nothing may cross the frame line. */
  /* Side margins guard against the notch: the pages now declare
     viewport-fit=cover (needed for the footer's bottom safe-area
     lift, see landing-page.css's .titleblock__row-main), which means
     a landscape iPhone extends the page INTO the sensor housing —
     max() widens the frame band on that side to whatever the device
     reports (~47px) instead of letting the hairline and content run
     behind the notch. Everywhere else env() is 0 and this collapses
     to the plain var(--frame).

     Top gets the same treatment now, additively rather than max()'d,
     per Liam ("the frame/inner border of the page... sits lower
     without anything being cut off by iphones stuff") — in portrait
     the status bar/notch/Dynamic Island sits ABOVE the frame band
     already provides, not overlapping it the way the landscape
     sensor housing overlaps the side margin, so this needs to ADD
     clearance on top of --frame-y rather than take whichever is
     bigger. 0px on everything else, so this only ever grows the top
     band where a device actually reports an inset. Bottom is
     untouched — the footer's own padding already carries that lift.

     Base bumped from --frame-y (2x --frame) to 3x, per Liam looking
     at this on his actual phone ("the hairline is still starting
     right at where the curve finishes") — env(safe-area-inset-top)
     clears the status bar/notch itself correctly, but the hairline
     sitting immediately at that boundary reads as touching the
     screen's own physical curve, not clear of it. Same extra-
     breathing-room move as .sheet__canvas's top clearance in
     landing-page.css (also bumped 2x -> 3x for the same reason).

     Bottom is 0 rather than --frame-y: the band below the hairline is now
     .frame-title, a real element of exactly that height (see below), so the
     gap is the strip's box instead of a margin above it. Same geometry, but
     the drawing title lives IN the sheet margin the way it does on a real
     sheet - as a flow item after this it would have added its height to the
     band and doubled it. Every .project-shell page carries the strip. */
  margin: calc(var(--frame-y) + var(--frame) + env(safe-area-inset-top, 0px))
          max(var(--frame), env(safe-area-inset-right, 0px))
          0
          max(var(--frame), env(safe-area-inset-left, 0px));
  border: 1px solid var(--line);
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.project-scroll::-webkit-scrollbar {
  display: none;
}

/* ---- Drawing title strip -------------------------------------------
   Every page is a sheet in one set, per Liam, so every page names itself
   the way a sheet does: DRAWING: <name>, then the page code, then the
   scale. One line.

   The codes are the ones the menu's project index already uses (_008
   Skjul, _012 Palme, _024 Gardhus, J_01..J_03 for the journal) rather
   than a second invented series - the home page, contact and portal
   take _000.._002 ahead of the projects, which start at 008.

   .dwg is the line itself and carries all the type, so it renders
   identically wherever it is placed; .frame-title (below) and the
   landing page's own .sheet__caption only position it. */
.dwg {
  display: flex;
  align-items: baseline;
  /* The gap between fields, not between words - "DRAWING: HOME PAGE" is
     one field and keeps its ordinary space inside. em, so the gap tracks
     the type if the size ever changes. */
  gap: 1.7em;
  font-family: var(--font);
  font-size: 0.56rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  /* Softened off full ink, per Liam ("slightly too black" on the home
     page): bold caps carry more weight than their size suggests. --ink-soft
     is the fallback where color-mix is not supported, which is the right
     direction to fail in. */
  color: var(--ink-soft);
  color: color-mix(in srgb, var(--ink) 78%, var(--bg));
  white-space: nowrap;
}
/* The sheet margin below the frame, on every .project-shell page - the
   bottom counterpart to .frame-back's band at the top, and the same band
   .sheet__caption fills on the home page. Its height IS the frame's bottom
   gap (see .project-scroll's margin above), so the strip costs no space. */
.frame-title {
  flex: 0 0 auto;
  height: var(--frame-y);
  margin: 0;
  /* +1px on the left so the line starts on the hairline's INNER edge,
     exactly where .frame-back sits in the band at the top - without it the
     two labels are a pixel apart on the same page. */
  padding-left: calc(max(var(--frame), env(safe-area-inset-left, 0px)) + 1px);
  padding-right: max(var(--frame), env(safe-area-inset-right, 0px));
  display: flex;
  align-items: center;
  overflow: hidden;
}

/* ---- Top nav ----
   Fixed and transparent so it sits on every page, always over whatever's
   underneath (hero image or plain dark content further down the page).
   White text throughout since the page background is dark almost
   everywhere, per the "on all pages" requirement. */
.ref-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* Padding here is a second, nested inset — not just var(--side)
     (the hero's own inset from the page edge) — so the nav titles sit
     further in from the hero photo's edge, not flush with it, matching
     the reference layout's double-inset (page border, then a further
     inset for the text on top of the photo). Extra right padding
     reserves room for the theme toggle, which sits absolutely
     positioned in that space (not a flex item) so it can't shift
     Contact or throw off nav-align.js's column matching. Bottom padding
     is deliberately smaller than top: the text position only depends on
     padding-top (align-items:center does nothing here since every child
     is the same single-line height), so shrinking just the bottom pulls
     the box's lower edge — and with it, how much of the video the 50%
     hover/scrolled shade covers — up without shifting the text at all. */
  padding: calc(var(--side) * 2) calc(var(--side) * 2 + 3rem) var(--side) calc(var(--side) * 2);
  background: transparent;
  color: #fff;
  font-weight: 500;
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition: background-color 0.4s cubic-bezier(0.16, 1, 0.3, 1),
              padding 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Crisp white is a dark-mode-only override — light mode keeps the
   themed (dark) ink color, same as before. */
[data-theme="light"] .ref-nav { color: var(--ink); }
/* The opaque shade appears on hover (any time, anywhere on the page)
   AND once scrolled (see header-shade.js) — stays fully transparent
   only when at the very top of the page and not being hovered. Shrinks
   only on scroll, not on hover (hover shouldn't shift layout under the
   cursor). nav-align.js's ResizeObserver picks up the scroll-driven
   height change and recomputes the section-nav's column widths. */
.ref-nav:hover,
.ref-nav.is-scrolled {
  background: rgba(12, 12, 11, 0.5);
}
[data-theme="light"] .ref-nav:hover,
[data-theme="light"] .ref-nav.is-scrolled {
  background: rgba(243, 242, 238, 0.5);
}
.ref-nav.is-scrolled {
  padding: 1.25rem calc(var(--side) + 3rem) 1.25rem var(--side);
}

/* ---- Theme toggle: minimal iOS-style switch, monochrome ---- */
.theme-toggle {
  position: absolute;
  /* Was top:50% of the whole (tall) nav box — but the box's padding-top
     and padding-bottom aren't equal (see .ref-nav's comment), so 50% of
     the box no longer lands on the text's actual vertical center. This
     targets the links' real line-center directly instead (measured:
     padding-top + 8.25px, the header font's half-line-height), and
     mirrors the same math for the scrolled/shrunk state below — so the
     toggle tracks the links exactly and moves with them as the header
     transitions between states, rather than drifting off on its own. */
  top: calc(var(--side) * 2 + 8.25px);
  /* Padding gives a larger tap target without growing the visible track;
     right is nudged in by the same amount so the track's edge still
     lands at --side, same as before. (A previous negative-margin version
     of this trick broke vertical centering — removed.) */
  right: calc(var(--side) - 0.5rem);
  transform: translateY(-50%);
  padding: 0.5rem;
  line-height: 0;
  transition: top 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.ref-nav.is-scrolled .theme-toggle {
  top: calc(1.25rem + 8.25px);
}
.theme-toggle__track {
  display: block;
  width: 1.5rem;
  height: 0.8333rem;
  border-radius: 999px;
  border: 1px solid var(--ink);
  background: transparent;
  position: relative;
  transition: background-color 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.theme-toggle__thumb {
  position: absolute;
  top: 0.125rem;
  left: 0.125rem;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--ink);
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
              background-color 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.theme-toggle[aria-checked="true"] .theme-toggle__track {
  background: var(--ink);
}
.theme-toggle[aria-checked="true"] .theme-toggle__thumb {
  transform: translateX(0.75rem);
  background: var(--bg);
}

@media (prefers-reduced-motion: reduce) {
  .theme-toggle__track, .theme-toggle__thumb { transition: none; }
}

@media (prefers-reduced-motion: reduce) {
  .ref-nav { transition: none; }
}
.ref-nav__logo { display: block; }
.ref-nav__logo img { height: 0.6875rem; width: auto; }
/* Real black/white logo files (not a CSS filter/invert), swapped per
   theme same as everything else — see theme-toggle.js for the attribute. */
.ref-nav__logo-light { display: none; }
[data-theme="light"] .ref-nav__logo-dark { display: none; }
[data-theme="light"] .ref-nav__logo-light { display: block; }

/* Hover: the hovered item grows, its siblings dim — except the logo,
   which never dims (it's the brand mark, not just another nav item) but
   still grows on its own hover like everything else. Subtle scale and a
   slow, gentle ease, deliberately calm. */
.ref-nav > a {
  display: inline-block;
  transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
              transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.ref-nav:has(> a:hover) > a:not(:hover):not(.ref-nav__logo) {
  opacity: 0.5;
}
.ref-nav > a:hover {
  opacity: 1;
  transform: scale(1.08);
}

@media (prefers-reduced-motion: reduce) {
  .ref-nav > a { transition: none; }
  .ref-nav > a:hover { transform: none; }
}

/* ---- Hero ----
   Still a carousel (auto-advances, see hero-selector.js), just without
   the busy filmstrip-of-thumbnails selector — a small dot indicator
   instead, and no title overlaid on the photo. Sized so hero +
   title-block + section-nav together fill roughly one screen on load,
   regardless of the visitor's resolution, rather than a fixed aspect
   ratio that can grow taller than the viewport on some screens. 65px
   approximates the section-nav's height, 8rem the title-block's
   (padding + its largest clamped font size) — see the mobile override
   below, where this isn't the goal and a fixed aspect ratio is used
   instead. var(--footer-height, 0px) (set by footer-include.js via
   ResizeObserver once the shared footer loads) also comes out of the
   budget now that this page has one pinned at the bottom eating into
   "the viewport" — without it, on wide/landscape viewports where this
   rule actually applies, the title block landed partly behind the
   footer on first load (0px fallback for the brief window before the
   footer's real height is known, same "one screen" fit as before, just
   possibly overlapping the footer for a frame — cheaper than blocking
   the hero's first paint on the footer's fetch resolving). */
.ref-hero {
  position: relative;
  /* Top margin dropped — .project-scroll's own margin-top IS the top gap
     now (the persistent frame), so a margin here would double it. */
  margin: 0 var(--side) 0;
  /* Budget subtracts both of .project-scroll's frame margins (the
     doubled top and bottom bands, hence --frame-y * 2) where the old
     version subtracted the hero's own single top margin. Not --side —
     that's 0rem inside the shell (see .project-shell). */
  height: calc(100dvh - 65px - 8rem - (var(--frame-y) * 2) - var(--footer-height, 0px));
  min-height: 26rem;
  overflow: hidden;
}
.ref-hero__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.ref-hero__media.is-active {
  opacity: 1;
}
.ref-hero__dots {
  position: absolute;
  left: 0; right: 0; bottom: 1.25rem;
  display: flex;
  justify-content: center;
  gap: 0.5rem;
}
.ref-hero__dot {
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  padding: 0;
  background: rgba(243, 242, 238, 0.5);
  transition: background-color 0.3s ease, transform 0.3s ease;
}
.ref-hero__dot:hover {
  background: rgba(243, 242, 238, 0.8);
}
.ref-hero__dot.is-active {
  background: #f3f2ee;
  transform: scale(1.25);
}

/* ---- Title block ----
   Plain Helvetica name/number pair in its own row. Sized off
   --proj-size-lg/--proj-size-index now (was clamp(2.5rem, 6vw, 4.5rem)/
   clamp(1.25rem, 3vw, 2.25rem)) — dropped the fluid clamp() scaling
   along with the size cut, per Liam ("more thematic with the footer"):
   the footer's own type scale is fixed, not viewport-fluid, so matching
   its hierarchy means matching that too, not just the pixel values.
   Index stays exactly half the title's size, same ratio as before. */
/* ---- Project hero, shared by every project page ----
   The composition Liam set on Gardhus, now on all three: the Danish name at
   size with a rule that carries past it, the English translation beneath,
   then the small facts, then the description - and only after all of that,
   the video.

   It lives in THIS file because every project page loads it and only one
   loads project-page.css. Gardhus keeps its own .proj__* names: those sit inside
   .proj, which supplies the page inset, while these carry the inset
   themselves so they can stand at the very top of the .ref-* pages, which
   have no such wrapper. */

.proj-hero {
  /* Much less air above the name than the first pass, per Liam - "the gap
     between top and Project name can be reduced a lot. not completely."
     The page frame's own inset already separates the name from the edge, so
     this only has to keep it off the return link. */
  /* Roughly half way back toward the original, per Liam - the first pass
     was 2.2/9vh/4, the second cut it to 0.9/3vh/1.6, and this sits between
     them: enough air under the frame's top hairline for the name to land
     deliberately, without the name floating away from it. */
  padding: clamp(1.5rem, 5.5vh, 2.6rem) var(--side)
           clamp(1.1rem, 4vh, 2rem);
}
.proj-hero__title {
  margin: 0;
  font-size: clamp(2.6rem, 15vw, 8rem);
  line-height: 0.86;
  letter-spacing: -0.045em;
  font-weight: 400;
  /* Stops short of the measure, so the rule reads as carrying past the word
     rather than boxing it in. */
  border-bottom: 1px solid var(--line-strong);
  width: 92%;
  padding-bottom: 0.4rem;
}
.proj-hero__alt {
  font-size: 0.86rem;
  margin: 0.6rem 0 0;
  font-weight: 500;
  /* Sentence case, per Liam. The tracking goes with the caps - 0.07em is a
     fix for uppercase at small sizes and only loosens mixed case. */
  letter-spacing: 0.01em;
}
/* The project number belongs here, beside the English name, and nowhere
   else - it used to also sit in the facts below, which said it twice. */
.proj-hero__index {
  margin-left: 0.85em;
  font-size: 0.62rem;
  font-weight: 400;
  letter-spacing: 0.07em;
  color: var(--fsmenu-accent);
  vertical-align: 0.1em;
}
.proj-hero__meta {
  font-size: 0.62rem;
  line-height: 1.85;
  color: var(--ink-soft);
  margin: 0.7rem 0 0;
}
.proj-hero__body {
  margin: clamp(1.3rem, 5vh, 2.2rem) 0 clamp(1.6rem, 6vh, 2.8rem);
  /* Also used below the video now, where it is a top-level element with no
     wrapper to inset it - so it carries the page inset itself. */
  padding-inline: var(--side);
  /* A measure, not the full column - long lines are the fastest way to make
     a description look unread. */
  max-width: 34rem;
  /* The same --proj-size-sm and line-height as .ref-copy, per Liam: at
     0.82rem this was 13.12px against the page's 11px body copy and table,
     so the description was the largest text on the page below the name.
     Reading off the same token as .ref-copy means it cannot drift again -
     the previous value was a number I picked rather than one the page
     already used. */
  font-size: var(--proj-size-sm);
  line-height: 1.7;
}

.ref-title-block {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  padding: 1.5rem var(--side);
  border-bottom: 1px solid var(--line);
}
.ref-title-block__name {
  margin: 0;
  color: var(--ink);
  font-size: var(--proj-size-lg);
  font-weight: 400;
  line-height: 1.1;
}
/* Dictionary-entry treatment: small, muted, italic — reads as a
   translation gloss under the headword, not a subtitle. Same idea as
   the footer's own "Pronounced 'luuh-kah'" gloss under the Danish
   definition, just for this project's name instead of the brand's. */
.ref-title-block__translation {
  display: block;
  margin-top: 0.25rem;
  color: var(--ink-soft);
  font-family: Georgia, "Times New Roman", serif;
  font-size: var(--proj-size-sm);
  font-style: italic;
  text-transform: capitalize;
  letter-spacing: 0.02em;
}
.ref-title-block__index {
  color: var(--ink-soft);
  font-size: var(--proj-size-index);
  font-weight: 300;
}

/* ---- Main content ----
   No max-width/centering here, deliberately: matches the header's model
   (full viewport width, padded from the true edge by --side) at every
   screen size, including very wide/fullscreen viewports where a centered
   max-width box would drift out of alignment with the header. */
.ref-main {
  padding: 0 var(--side);
}

/* Breathing room above any section that's a jump target (Introduction/
   Site Model/Architectural Drawings) when section-nav scrolls you
   there — scroll-margin-top affects native anchor scrolling with no JS
   needed. Both values cut way down from their old 4rem/7rem, per Liam
   ("more thematic with the footer" — a page with no fixed header to
   clear anymore, and a footer whose own rows sit close together, has
   no reason for this much air). scroll-margin-top now just matches
   --side, the same "one buffer unit" of clearance everything else on
   this page uses, rather than a header-height-shaped number tied to an
   element that no longer exists. Scoped to [id] specifically so the
   tighter sub-rows within a section (pagination, body copy) aren't
   pushed apart by this too. */
.ref-row[id] {
  scroll-margin-top: var(--side);
  padding-top: 3rem;
}

/* A third of that for the section directly under the video, per Liam. The
   3rem above is the rhythm BETWEEN sections, which is right where one body
   of text ends and the next begins - but Introduction opens hard against
   the footage, and there the same gap reads as a hole. Measured 50px from
   the video's edge to the label's own text; 1rem takes it to ~18. Scoped to
   the id rather than :first-of-type so it cannot silently move to whatever
   section happens to come first later. */
#introduction {
  padding-top: 1rem;
}

/* 3-track grid: content (2 "columns") / gap (half a "column") / content
   (2.5 "columns") — matches the reference's proportions, where the intro
   photo below spans the same first-2-column width before a real gap
   track, not just a plain 50/50 split with a small gutter. */
.ref-row {
  display: grid;
  grid-template-columns: 2fr 0.5fr 2.5fr;
  padding-top: 1.5rem;
}
/* Same role as the footer's own .titleblock__label (eyebrow caption
   above the real content), so sized and colored to match it exactly —
   was font-size:0.75rem, color inherited from body (full --ink), which
   read heavier than the footer's own quiet labels. */
.ref-label {
  grid-column: 1 / 2;
  color: var(--ink-soft);
  font-size: var(--proj-size-md);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  margin: 0;
}
/* Reads like body copy (close to .ref-copy's own --proj-size-sm — bold
   caps + letter-spacing was what made it look bigger, and still would)
   rather than a section label. Used for "Architectural Drawings" only,
   per Liam. */
/* A .ref-label used on its own rather than as a .ref-row's first column -
   Gardhus's Drawings heading, which is followed by a full-width carousel
   instead of a two-column row. The class supplies the type; this restores
   the block behaviour and spacing the grid would otherwise have given it.

   Placed AFTER .ref-label deliberately. Both are single classes, so they
   carry identical specificity and source order alone decides - sitting
   above it, .ref-label's own `margin: 0` won and this rule's margins were
   silently zeroed (measured 0px on both sides). */
.ref-label--standalone {
  display: block;
  margin-top: 3rem;      /* the rhythm .ref-row[id] puts between sections */
  margin-bottom: 0.9rem;
}

.ref-label--plain {
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
}
.ref-row__right {
  grid-column: 3 / 4;
  max-width: 22rem;
}

.ref-table th, .ref-table td {
  text-align: left;
  font-weight: 400;
  padding: 0.5rem 0;
  border-top: 1px solid var(--line);
  font-size: var(--proj-size-sm);
}
.ref-table tr:last-child th,
.ref-table tr:last-child td { border-bottom: 1px solid var(--line); }
.ref-table th { color: var(--ink-soft); width: 45%; }

.ref-copy {
  font-size: var(--proj-size-sm);
  line-height: 1.7;
  color: var(--ink);
  margin: 1.5rem 0 0;
  max-width: 34ch;
}
.ref-copy--short { max-width: 100%; }

/* ---- Photos row ---- */
.ref-photos {
  display: grid;
  grid-template-columns: 2fr 0.5fr 2.5fr;
  /* The carousel's height, named once. The side column has to match it
     exactly - "each half the height of the gallery pic" is only true if
     both read the same number - and two hard-coded 75vh values drift the
     first time one of them is tuned. */
  --gallery-h: 75vh;
  /* Matches .ref-row's own (tightened) top spacing, per Liam ("more
     thematic with the footer") — was 2.5rem, one-off from .ref-row's
     old value even though both mark the same "space above this row"
     role. */
  padding-top: 1.5rem;
}
.ref-photos__main-wrap {
  grid-column: 1 / 2;
  position: relative;
  /* A proper 5:7 photo ratio (Liam's spec) — but driven by HEIGHT, not
     width: a fixed width with a 5:7 ratio was still taller than the
     screen on normal viewports, cutting off the pagination controls
     below it. justify-self:start stops the grid item from stretching to
     the column's full width, so the box sizes itself from height +
     aspect-ratio instead — max-width is just a safety cap for narrow
     viewports where 5:7 of this height would overflow the column. */
  justify-self: start;
  height: var(--gallery-h);
  max-width: 100%;
  aspect-ratio: 5 / 7;
}
.ref-photos__main {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.ref-photos__main.is-active {
  opacity: 1;
}
/* Side by side on desktop, per Liam: "palme hus showing the fixed image
   stacked on top of axo. that should not be happening on desktop, change
   it back to what it was, for desktop."

   The stack was asked for from a PHONE and I applied it at every width.
   It is right on a narrow screen - see the 767px block below, which has
   done it that way all along - and wrong here, where there is room for
   the two to sit beside each other as they always did. */
.ref-photos__side {
  grid-column: 3 / 4;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem;
  align-items: start;
}
/* height:auto is load-bearing, not tidiness. An <img> with width/height
   ATTRIBUTES (which these want, so the box is reserved before the file
   arrives) gets them as presentational hints on the width and height
   PROPERTIES. width:100% here beats the width hint, but with no height
   declared the hint stands - and a computed height beats aspect-ratio.
   Caught when the side photo was given its intrinsic size: the box went
   from 343x457 to 343x3200 and pushed the whole row down the page. */
/* height:auto is load-bearing, not tidiness. An <img> with width/height
   ATTRIBUTES gets them as presentational hints on the width and height
   PROPERTIES; width:100% beats the width hint, but with no height declared
   the hint stands, and a computed height beats aspect-ratio. Caught when
   the side photo was given its intrinsic size: 343x457 became 343x3200 and
   pushed the whole row down the page. */
.ref-photos__small { width: 100%; height: auto; aspect-ratio: 3 / 4; object-fit: cover; }
.ref-photos__plan { width: 100%; height: auto; aspect-ratio: 1 / 1; object-fit: cover; }
.ref-photos__plan--placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--blank);
}
.ref-photos__plan--axo {
  display: flex;
  align-items: center;
  justify-content: center;
  /* The side column is align-items:start, so a 1:1 box sits at the top of
     a row set by the 3:4 photo beside it and the drawing reads as if it
     had slipped upward. Centring just this cell puts the axo on the
     photo's midline; the photo keeps defining the row height. */
  align-self: center;
}
.ref-photos__axo {
  width: 100%;
  height: 100%;
  /* contain, not cover: this box is 1:1 and the drawing is landscape, so
     covering would crop the site off both ends. */
  object-fit: contain;
}
.ref-photos__axo--light { display: none; }
.ref-photos__axo--dark { display: block; }
[data-theme="light"] .ref-photos__axo--light { display: block; }
[data-theme="light"] .ref-photos__axo--dark { display: none; }

.ref-photos__plan--placeholder span {
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* ---- Pagination + facts row ---- */
.ref-row--pagination {
  align-items: start;
}
.ref-pagination {
  grid-column: 1 / 2;
  display: flex;
  align-items: center;
  gap: 1rem;
  font-size: var(--proj-size-md);
  padding-top: 0.75rem;
  border-top: 1px solid var(--ink);
}
.ref-pagination button { font-size: 0.9375rem; }
/* Horizontal row, not a stacked list — top edge (text baseline) matches
   .ref-pagination's via the same border-top + padding-top treatment, so
   "Detail one" starts at the same y as "1/6" beside it. */
.ref-facts {
  grid-column: 3 / 4;
  display: flex;
  gap: 1.5rem;
  list-style: none;
  margin: 0;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line);
  font-size: var(--proj-size-md);
}

/* ---- Body copy + credits row ----
   Shares the same 2fr/0.5fr/2.5fr grid as .ref-row above (not its own
   independent ratio), so "Design/Photographer" starts at the same x as
   "Location" does in the Introduction row. */
.ref-copy-block { grid-column: 1 / 2; }
.ref-copy-block .ref-copy { max-width: 46ch; margin-top: 0; }
.ref-copy-block .ref-copy + .ref-copy { margin-top: 1.25rem; }
.ref-table--credits {
  grid-column: 3 / 4;
  max-width: 18rem;
  align-self: start;
}
.ref-table--credits td { border-top: 1px solid var(--line); }
.ref-table--credits tr:last-child td { border-bottom: 1px solid var(--line); }

/* ---- Drawing carousel ----
   True full-bleed (no max-width, no side padding) — deliberately breaks
   from the rest of the page's edge padding for a full-width statement.
   Auto-advances every 10s (carousel.js); click the left/right edge to
   navigate manually. Progress bar width = position/total, not a segment
   per slide (2 of 4 = 50% width). */

/* ---- Scroll-linked video (theme-aware) ----
   Reusable pattern: pins for 4x viewport height, scrubs whichever video
   matches the current theme as you scroll (see scroll-video-themed.js).
   Intended for both Site Model (now) and Drawings (later, same markup
   block, different filenames) — see project memory. */
.scroll-video {
  position: relative;
  height: 400vh;
  margin-top: 2.5rem;
  /* No full-bleed breakout anymore (was negative side margins +
     compensated width) — under the site-wide frame, per Liam ("a frame
     around the viewport on all pages... always a gap"), nothing is
     allowed to touch the screen edges, so this now sits at .ref-main's
     standard var(--side) inset like every other section. Same change
     as .drawing-carousel below. */
  /* Marks the end of Section 2 (Site Model), same convention as the
     filmstrip's border-bottom above the Introduction section. On the
     outer (400vh) element, not .scroll-video__stage, so it renders at a
     fixed point right before Section 3 begins, not wherever the sticky
     inner stage happens to be mid-scroll. */
  border-bottom: 1px solid var(--line);
}
.scroll-video__stage {
  position: sticky;
  top: 0;
  height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--blank);
}
/* Quarter width, not a full-bleed cover — the source is only 1180x776
   (an AI-generated site-axo animation), so stretching it to fill the
   whole 100dvh stage made it visibly pixelated. Sized from its own
   intrinsic aspect ratio instead of object-fit:cover, and the flex
   stage (align-items/justify-content:center) centers it. */
.scroll-video__media {
  width: 25%;
  height: auto;
  display: none;
}
.scroll-video__placeholder-note {
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  text-align: center;
  max-width: 24rem;
  padding: 0 2rem;
}
.scroll-video__progress {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  background: var(--ink);
  transform-origin: left;
  transform: scaleX(0);
}

@media (prefers-reduced-motion: reduce) {
  .scroll-video { height: auto; }
  .scroll-video__stage { position: relative; height: 60vh; }
}

.drawing-carousel {
  margin-top: 2.5rem;
  /* Full-bleed breakout removed — see .scroll-video above; the frame
     means no section reaches the screen edge anymore. */
}

/* Liam: "architectural drawings should take up 3/4 with room for text on
   the left."

   The same row Gardhus's plans use, so the two pages read alike: drawing
   at three quarters, a column beside it for the sentence belonging to
   whichever sheet is up. The column is held whether or not there is
   anything in it - that is the point of asking for it before the words
   exist - and it collapses to a single column below 60rem, where a
   quarter of a phone is not a column at all.

   grid on the SECTION rather than a new wrapper, so no markup on either
   page has to change: the stage is the wide cell, the note the narrow one,
   and order puts the words on the left. */
@media (min-width: 60rem) {
  .drawing-carousel {
    display: grid;
    grid-template-columns: 1fr 3fr;
    gap: 2rem;
    align-items: start;
  }
  .drawing-carousel__stage { order: 2; min-width: 0; }
  .drawing-carousel__note { order: 1; padding-top: 2.25rem; }
}
.drawing-carousel__note-text {
  margin: 0;
  max-width: 42ch;
  font-size: var(--proj-size-sm);
  line-height: 1.55;
  color: var(--ink);
}
.drawing-carousel__note-text:empty { display: none; }

/* ---- Section 4: Video ---- */
/* The real White House video, full-bleed like .drawing-carousel — moved
   here from the hero filmstrip per Liam. Autoplay/muted/loop, no
   controls — no audio track, so nothing for a HUD to control. */
/* Inset the same amount as .ref-hero (var(--side), sharp corners, no
   full-bleed break-out) rather than the .drawing-carousel full-bleed
   treatment it started with — matches the hero shot's framing. */
.video-section {
  position: relative;
  margin: 2.5rem var(--side) 2.5rem;
}
.video-section__media {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--blank);
}
/* Sound toggle: only relevant for videos with an actual audio track
   (see data-sound-toggle in HTML — only added where the source has
   audio). Bottom-right, same minimal monochrome language as the theme
   toggle. The fullscreen toggle (every hero video gets one, per Liam)
   shares the whole visual recipe, so both read as one control set. */
.video-sound-toggle,
.video-fullscreen-toggle,
.video-captions-toggle {
  position: absolute;
  right: 1rem;
  bottom: 1rem;
  width: 2.25rem;
  height: 2.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(12, 12, 11, 0.5);
  color: #f3f2ee;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.video-sound-toggle svg,
.video-fullscreen-toggle svg,
.video-captions-toggle svg {
  width: 1.1rem;
  height: 1.1rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}
/* Where a sound toggle already occupies the corner (Palme Hus),
   fullscreen sits one slot to its left: 1rem edge + 2.25rem button +
   0.5rem gap. */
.video-fullscreen-toggle--beside-sound {
  right: 3.75rem;
}
/* Captions takes the third slot: 3.75rem + 2.25rem button + 0.5rem gap.
   Only Palme Hus has a voice-over, so only it carries the button. */
.video-captions-toggle {
  right: 6.5rem;
}
/* Pressed = the box fills and the letters go dark - the same one-glance
   on/off read as the sound icon's waves appearing. */
.video-captions-toggle[aria-pressed="true"] svg rect { fill: currentColor; }
.video-captions-toggle[aria-pressed="true"] svg .cc-glyph { stroke: #0c0c0b; }
/* The captions themselves. The browser draws them; this sets the type to
   the site's and pins them above the corner buttons so the last line is
   never under a control. Sizes are relative to the video box so a phone
   gets a sensible line and a desktop hero does not shout. */
video::cue {
  font-family: Helvetica, Arial, sans-serif;
  font-size: clamp(0.8rem, 2.2vw, 1.15rem);
  line-height: 1.35;
  color: #f3f2ee;
  background: rgba(12, 12, 11, 0.55);
}
/* Liam: "make the text size for the captions smaller on the video on
   desktop please."

   The clamp above pins to its 1.15rem ceiling on anything wider than
   about 830px, so every desktop was getting the largest size the scale
   allows - sized for a phone held at arm's length, shown on a screen a
   metre away. The ceiling comes down here rather than in the clamp
   itself, so the phone end is untouched.

   A separate rule and not a narrower clamp: ::cue takes a limited set of
   properties and browsers disagree about how they cascade, so one plain
   declaration in a media query is more predictable than arithmetic
   inside a function. */
@media (min-width: 60rem) {
  video::cue { font-size: 0.85rem; }
}
.video-fullscreen-toggle .icon-compress { display: none; }
.video-fullscreen-toggle[aria-pressed="true"] .icon-expand { display: none; }
.video-fullscreen-toggle[aria-pressed="true"] .icon-compress { display: block; }
.video-sound-toggle .icon-muted { display: block; }
.video-sound-toggle .icon-unmuted { display: none; }
.video-sound-toggle[aria-pressed="true"] .icon-muted { display: none; }
.video-sound-toggle[aria-pressed="true"] .icon-unmuted { display: block; }
.drawing-carousel__stage {
  position: relative;
  width: 100%;
  aspect-ratio: 21 / 9;
  background: var(--bg);
  overflow: hidden;
}
/* Same convention as .ref-photos__plan--placeholder — a plain --blank
   box with centered label, until real drawings land for a project. */
.drawing-carousel__stage--placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--blank);
}
.drawing-carousel__stage--placeholder span {
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.drawing-carousel__slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 1rem 2rem;
  box-sizing: border-box;
  opacity: 0;
  transition: opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.drawing-carousel__slide.is-active {
  opacity: 1;
}

/* A slide that carries a dark variant is a <div> holding both images. The
   slide keeps its own position/padding/opacity rules above; these two make
   the images fill it exactly as a bare <img> slide would, and swap on
   theme. Default is the dark variant, overridden for [data-theme="light"],
   matching how the Gardhus drawings do it. */
.drawing-carousel__art {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.drawing-carousel__art--light { display: none; }
.drawing-carousel__art--dark { display: block; }
[data-theme="light"] .drawing-carousel__art--light { display: block; }
[data-theme="light"] .drawing-carousel__art--dark { display: none; }
.drawing-carousel__edge {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 15%;
  cursor: pointer;
  display: flex;
  align-items: center;
  opacity: 0;
  transition: opacity 0.3s ease;
  color: var(--ink);
  font-size: 1.5rem;
}
.drawing-carousel__edge:hover { opacity: 0.5; }
.drawing-carousel__edge--prev { left: 0; justify-content: flex-start; padding-left: 1.5rem; }
.drawing-carousel__edge--next { right: 0; justify-content: flex-end; padding-right: 1.5rem; }
.drawing-carousel__edge--prev::before { content: "\2190"; }
.drawing-carousel__edge--next::before { content: "\2192"; }
.drawing-carousel__caption {
  position: absolute;
  left: var(--side);
  bottom: 1rem;
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
}
.drawing-carousel__progress {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 25%;
  background: var(--ink);
  transition: width 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  .drawing-carousel__slide { transition: none; }
  .drawing-carousel__progress { transition: none; }
}

@media (max-width: 767px) {
  .drawing-carousel__stage { aspect-ratio: 4 / 5; }
  .drawing-carousel__slide { padding: 1.5rem; }
  .drawing-carousel__edge { width: 25%; }
}

@media (max-width: 767px) {
  .ref-nav { grid-template-columns: repeat(2, 1fr); row-gap: 0.75rem; }
  .ref-hero { height: auto; min-height: 0; aspect-ratio: 4 / 5; }
  .ref-title-block { flex-direction: column; align-items: flex-start; gap: 0.5rem; }.ref-row, .ref-photos, .ref-row--body { grid-template-columns: 1fr; }
  .ref-row__right, .ref-facts, .ref-table--credits,
  .ref-photos__main-wrap, .ref-photos__side { grid-column: 1 / 2; max-width: none; justify-self: stretch; }
  /* Width-driven on mobile, not the desktop height-driven 75vh sizing —
     75vh of a portrait phone at a 5:7 ratio is WIDER than the column
     (435px vs ~345px at 375pt), which always overflowed, but the old
     window-level overflow-x:clip quietly hid it. Inside .project-scroll
     (the framed clip context) that same overflow became real horizontal
     scrolling, dragging content across the frame's hairline. */
  .ref-photos__main-wrap { height: auto; width: 100%; }
  .ref-facts { flex-wrap: wrap; gap: 0.75rem 1.5rem; }
  /* Liam's rule for the narrow layout: the fixed portrait goes NEXT TO
     the carousel and the axo stacks under it, each half the carousel's
     height. The side column's width follows from that: two half-height
     squares beside a 5:7 carousel means side = 0.7 x carousel width,
     and with the two sharing the row, side = 0.7/1.7 of what is left
     after the gap. Row heights then split the carousel's height in two
     and the images cover their cell, so the arithmetic never has to be
     exact to the pixel. */
  .ref-photos {
    grid-template-columns: 1fr calc((100% - 0.75rem) * 0.4118);
    column-gap: 0.75rem;
    align-items: stretch;
  }
  /* min-width:0 on both, or the 1fr track refuses to shrink below the
     carousel's min-content (its own images) and the row runs 100px past
     the frame - caught at 375: carousel 302 + gap + side 137 = 451 in a
     345 row, clipped silently by .project-scroll. */
  .ref-photos__main-wrap { grid-column: 1 / 2; min-width: 0; }
  .ref-photos__side {
    grid-column: 2 / 3;
    min-width: 0;
    margin-top: 0;
    grid-template-columns: 1fr;
    grid-template-rows: 1fr 1fr;
    gap: 0.75rem;
    height: 100%;
    min-height: 0;
    align-items: stretch;
  }
  /* The carousel sets the row's height (5:7 of its width); the side
     column follows it. height:0 + min-height:100% is what makes that
     one-way: the side images contribute NO intrinsic height while the
     row is being sized (a percentage min-height of an indefinite height
     is 0), then once the row is definite they fill their cells. Without
     it the portrait's natural 2:3 height set the row and the carousel
     stretched to 1:2.2, cropping every photo to a slot - measured 196x423
     against the 196x274 the ratio wants. */
  .ref-photos__main-wrap { aspect-ratio: 5 / 7; }
  .ref-photos__small,
  .ref-photos__plan {
    aspect-ratio: auto;
    height: 0;
    min-height: 100%;
    object-fit: cover;
  }
  .ref-photos__plan--axo { align-self: stretch; }
}

/* ---- Follow this project ----
   One field and one word, in the same underline language as the booking
   form and the portal inputs. Typed size is 16px for the same reason as
   everywhere else (iOS zooms a focused field under that); the prompt is
   placeholder text and stays at the description size. */
.ref-row--follow { align-items: start; }
/* Liam: "insert a line between follow this project and the descriptive
   text". The same hairline the Introduction's facts table opens with, at
   the same level as the label - so on a phone, where label and text
   stack, the line sits between them, and on a desktop it tops the text
   column beside the label. */
.ref-row--follow .ref-row__right {
  border-top: 1px solid var(--line);
  padding-top: 0.9rem;
}
/* Liam: "follow this project info should be in the right quarter of the
   [follow] section. and have a hairline dividing it from the 3rd quarter."

   The row is normally 2fr / 0.5fr / 2.5fr - a label column and a wide
   text column. Here it becomes four quarters, and the form takes only the
   last of them, with a vertical hairline down its left edge.

   Why it suits this section and not the others: a follow field is one
   input and one word. Given the full text column it stretched into a
   letterbox, and the eye reads a wide box as a lot to fill in. A quarter
   is the size of the thing being asked for.

   The hairline runs down rather than across because the division is now
   between columns rather than between a label and its text - so the
   horizontal rule above it stands down at this width, or the two would
   box the form in on two sides. */
@media (min-width: 60rem) {
  .ref-row--follow {
    grid-template-columns: repeat(4, 1fr);
  }
  .ref-row--follow .ref-label { grid-column: 1 / 2; }
  .ref-row--follow .ref-row__right {
    grid-column: 4 / 5;
    border-top: 0;
    border-left: 1px solid var(--line);
    padding-top: 0;
    padding-left: 1.5rem;
  }
}

.follow__intro {
  font-family: var(--font);
  font-size: var(--proj-size-sm);
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 0 0 1rem;
  max-width: 32ch;
}
.follow-form[hidden] { display: none; }
.follow-form__row {
  display: flex;
  align-items: flex-end;
  gap: 1.5rem;
  max-width: 28rem;
}
.follow-form__input {
  flex: 1 1 0;
  min-width: 0;
  font-family: var(--font);
  font-size: max(1rem, var(--proj-size-sm));
  color: var(--ink);
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  padding: 0.375rem 0;
  transition: border-color 0.3s ease;
}
.follow-form__input::placeholder { font-size: var(--proj-size-sm); color: var(--ink-soft); }
.follow-form__input:focus { outline: none; border-bottom-color: var(--ink); }
.follow-form__submit {
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
  background: none;
  border: none;
  border-bottom: 1px solid var(--ink);
  padding: 0.375rem 0;
  cursor: pointer;
}
.follow-form__submit:disabled { opacity: 0.5; cursor: default; }
.follow-form__trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.follow__status {
  font-family: var(--font);
  font-size: var(--proj-size-sm);
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 1rem 0 0;
  max-width: 32ch;
}
.follow__status.is-error { color: var(--ink); }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ---- Journal article pages ----
   journal-*.html — same shell/frame/footer as the project pages, but
   a reading page: one centred measure of quiet text instead of a hero
   and sections. Type stays on the --proj tokens so the articles sit in
   the same hierarchy family as everything else; the only new size is
   the pull-quote, which borrows --proj-size-index (the project pages'
   project-index tier) to read one confident step above body copy. */
.journal-article {
  max-width: 36rem;
  margin: 0 auto;
  padding: 2.5rem 1.25rem 3rem;
}
/* Liam: "can you do journal entries to be in quarters 2 and 3."

   The middle half of the page: starts a quarter in, ends a quarter from
   the right. It was a 36rem column centred in whatever space there was,
   which on a 1920 screen is a narrow ribbon with a great deal of nothing
   either side.

   50% centred IS quarters two and three - the maths is the same and the
   intent is easier to read back. */
@media (min-width: 60rem) {
  .journal-article {
    max-width: none;
    width: 50%;
    margin-inline: auto;
    padding-inline: 0;
  }
}
.journal-article__label {
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 0 0 1.25rem;
}
.journal-article h1 {
  font-family: var(--font);
  font-weight: 400;
  font-size: var(--proj-size-lg);
  line-height: 1.15;
  margin: 0 0 2rem;
  max-width: 22ch;
}
/* Full-width portrait within the reading column — same treatment as
   the project pages' own photography, no caption/frame clutter. */
.journal-article__photo {
  margin: 0 0 2rem;
}
.journal-article__photo img {
  display: block;
  width: 100%;
  height: auto;
}
/* A headshot is not a photograph of a building. Full column width put a
   face 691px tall at the top of the page (Liam: "headshot is a bit
   large"), so a portrait figure takes just over half the measure and
   sits on the left edge of the text, the way a byline portrait does. On
   a phone the column is already narrow, so it takes a little more. */
.journal-article__photo--portrait {
  max-width: 55%;
}
@media (max-width: 767px) {
  .journal-article__photo--portrait { max-width: 70%; }
}
.journal-article h2 {
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 2.25rem 0 0.75rem;
}
.journal-article p {
  font-family: var(--font);
  font-size: var(--proj-size-sm);
  line-height: 1.7;
  color: var(--ink);
  margin: 0 0 1rem;
}
.journal-article blockquote {
  margin: 2rem 0;
  padding-left: 1.25rem;
  border-left: 1px solid var(--ink);
  font-size: var(--proj-size-index);
  line-height: 1.5;
  color: var(--ink);
}
.journal-article blockquote p {
  font-size: inherit;
  line-height: inherit;
  margin: 0;
}
.journal-article blockquote footer {
  margin-top: 0.75rem;
  font-size: var(--proj-size-md);
  color: var(--ink-soft);
}
/* h3 is the question tier inside the "Common questions" block. It has
   to read as a question a person asked, not as another section label,
   so it drops h2's uppercase/letter-spacing and takes the body colour
   at body size in bold - the same distinction a printed FAQ makes. */
.journal-article h3 {
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-sm);
  line-height: 1.4;
  color: var(--ink);
  margin: 1.75rem 0 0.5rem;
}
/* The closing "read next" line: body size, quieter colour, links
   underlined because there is nothing else marking them as links. */
.journal-article__more {
  margin-top: 2.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
}
.journal-article a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
.journal-article a:hover { color: var(--ink); }

/* Comparison tables reuse .ref-table wholesale — same hairline rows
   and label/value tiers as the project pages' fact tables. */
.journal-article .ref-table {
  margin: 1.5rem 0;
}


/* Journal tables. .ref-table is a two-column fact list - label on the
   left, value on the right - and these are real matrices, so they get
   their own rule rather than being bent into that one. Hairlines only,
   no fills: the site's tables are read as ruled paper, not as boxes.

   The first column is the stub and stays left; every data column is
   equal width and centred, so a row of three recommendations reads as
   one comparison rather than three sentences. Wrapped in a scroller,
   because three columns of text at a phone's width is the one place a
   table can push a page sideways. */
.journal-article .journal-table {
  width: 100%;
  border-collapse: collapse;
  margin: 1.5rem 0;
  font-family: var(--font);
  font-size: var(--proj-size-sm);
  line-height: 1.45;
}
.journal-article .journal-table th,
.journal-article .journal-table td {
  border-top: 1px solid var(--line);
  padding: 0.55rem 0.75rem 0.55rem 0;
  text-align: left;
  vertical-align: top;
}
.journal-article .journal-table tbody tr:last-child th,
.journal-article .journal-table tbody tr:last-child td {
  border-bottom: 1px solid var(--line);
}
.journal-article .journal-table thead th {
  border-top: 0;
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding-bottom: 0.4rem;
}
.journal-article .journal-table tbody th[scope="row"] {
  font-weight: 700;
  color: var(--ink);
  width: 26%;
  padding-right: 1.25rem;
}
/* The matrix reads by column, so its cells are centred and its recommendation
   levels carry weight rather than colour - the page has one accent and it is
   spent on links. "Not recommended" and "Over-servicing" go quiet instead. */
.journal-table--matrix td { text-align: center; }
.journal-table--matrix th[scope="col"] { text-align: center; }
.journal-table--matrix td[data-rec="high"] { font-weight: 700; color: var(--ink); }
.journal-table--matrix td[data-rec="yes"] { color: var(--ink); }
.journal-table--matrix td[data-rec="no"] { color: var(--ink-soft); }
/* The date under the title: the label tier, so it sits with "Journal:
   Practice" above it rather than competing with the heading. */
.journal-article__date {
  font-family: var(--font);
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: -1.25rem 0 2rem;
}
@media (max-width: 767px) {
  .journal-article .journal-table { font-size: calc(var(--proj-size-sm) * 0.94); }
  .journal-article .journal-table th,
  .journal-article .journal-table td { padding-right: 0.5rem; }
  .journal-article .journal-table tbody th[scope="row"] { width: 30%; padding-right: 0.6rem; }
}

/* A photograph made cohesive by blending rather than by cutting. The
   light copy multiplies (white x ground = ground, so the paper goes);
   the dark copy is inverted and screens (black + ground = ground, same
   trick mirrored). Both are the page's own theme swap, qualified with
   .journal-article so they outrank the article's own `img` rules. */
.journal-article__photo--blend { position: relative; }
.journal-article .journal-blend {
  display: block;
  width: 100%;
  height: auto;
}
.journal-article .journal-blend--light { mix-blend-mode: multiply; }
.journal-article .journal-blend--dark { mix-blend-mode: screen; }
.journal-article .journal-blend--light { display: none; }
.journal-article .journal-blend--dark { display: block; }
[data-theme="light"] .journal-article .journal-blend--light { display: block; }
[data-theme="light"] .journal-article .journal-blend--dark { display: none; }

/* A figure caption: italic, quiet, tight under its photograph - the way
   a plate is captioned in a book rather than a line of body copy that
   happens to sit below an image. */
.journal-article__photo figcaption {
  font-family: var(--font);
  font-style: italic;
  font-size: var(--proj-size-sm);
  line-height: 1.5;
  color: var(--ink-soft);
  margin-top: 0.6rem;
}
/* The closing call to action. Same hairline the questions block opens
   with, so the foot of the article reads as one closing pair. */
.journal-cta {
  margin-top: 2.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
.journal-cta h2 { margin-top: 0; }
/* Qualified with .journal-article, not left as a bare class. The
   article's own `a` rule is 0,1,1 and this was 0,1,0, so its underline
   won and the link carried both that and the border - the double
   underline Liam saw. The blue is the site's own accent, the one the
   project index numbers use, so it is not a new colour. */
.journal-article .journal-cta__link {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--proj-size-md);
  color: var(--fsmenu-accent);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 0.15rem;
}
.journal-article .journal-cta__link:hover { color: var(--fsmenu-accent); opacity: 0.75; }

/* The profile page's questions: in the page for the answer engines, out
   of the way for a reader. Closed by default; the summary is a quiet
   line in the label tier, not a heading that competes with the article's
   own. Marker removed and replaced with a plus that becomes a minus, so
   it reads as something to open rather than a browser default. */
/* Liam, twice: "i dont want those questions visible at all. they are
   purely for seo and googling. if you need them written for seo then put
   them in the same colour as the background and embed them or something."

   So they are gone from the page's surface entirely - no summary line, no
   rule, nothing drawn. What is used is the standard visually-hidden clip
   rather than colouring the text to match the background: it is the
   same result for a reader, but it is the technique screen readers and
   search engines are built to expect, where background-coloured text is
   the textbook signature of cloaking and the one thing that gets a page
   penalised for it. The words are still in the DOM, still selectable by
   a crawler, still what the FAQPage markup in the head describes.

   I said this once already and Liam asked again, so it is his call and
   this is the safest way to give him what he asked for. */
.journal-faq {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.journal-faq > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.journal-faq > summary::-webkit-details-marker { display: none; }
.journal-faq > summary::after { content: "+"; margin-left: auto; }
.journal-faq[open] > summary::after { content: "3"; }
.journal-faq > summary:hover { color: var(--ink); }
.journal-faq h3:first-of-type { margin-top: 1.25rem; }
/* Stating the collapse rather than trusting the browser's own. Measured
   closed, every question still had height: the user-agent rule that
   hides a closed <details>'s content was not applying here, and the
   questions sat on the page in full. This is the same behaviour written
   down, so it cannot depend on a browser default - and it is still a
   real <details>, so opening it shows them and crawlers read them. */
.journal-faq:not([open]) > *:not(summary) { display: none; }

/* ---- Book a Meeting ----
   book-a-meeting.html — same reading column as the journal articles
   (.journal-article's own max-width/padding, reused wholesale), a
   real working form instead of prose. Fields mirror studiolykke.au's
   existing Squarespace booking form (Project Scope, Address, Name,
   Brief, Phone, Email, referral source) — simplified where the
   original's full per-field country-code dropdowns were overkill for
   a Sydney-based practice. See booking-form.js for the actual submit
   handling and Formspree wiring. */
.booking__intro {
  font-family: var(--font);
  font-size: var(--proj-size-sm);
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 0 0 2rem;
  max-width: 32ch;
}
/* [hidden] needs restating here — an author rule always beats the
   UA stylesheet's own [hidden] { display: none }, even at equal
   specificity, so .booking-form's own display:flex below would
   otherwise win and leave the "submitted" form visibly on screen
   (caught live: booking-form.js sets .hidden on success, and the
   form stayed put, filled-in values and all). */
.booking-form[hidden] {
  display: none;
}
.booking-form {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
.booking-row {
  display: flex;
  gap: 1.5rem;
}
.booking-row .booking-field {
  flex: 1 1 0;
  min-width: 0;
}
.booking-field {
  display: flex;
  flex-direction: column;
}
.booking-field label {
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 0.5rem;
}
/* Boxless underline fields, matching .portal-input's own quiet
   language elsewhere on the site — brightens to --ink on focus as the
   only feedback an underline-only field needs. */
.booking-field input,
/* 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. */
.booking-field select,
.booking-field textarea {
  display: block;
  width: 100%;
  font-family: var(--font);
  font-size: max(1rem, var(--proj-size-sm));
  color: var(--ink);
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  padding: 0.375rem 0;
  transition: border-color 0.3s ease;
}
.booking-field textarea {
  resize: vertical;
  min-height: 6rem;
  line-height: 1.5;
}
.booking-field input::placeholder,
.booking-field textarea::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(--proj-size-sm);
  color: var(--ink-soft);
}
.booking-field input:focus,
.booking-field select:focus,
.booking-field textarea:focus {
  outline: none;
  border-bottom-color: var(--ink);
}
/* Native <select> has no themeable arrow glyph without an SVG
   background-image hack; left as the browser default rather than
   fighting it, same restraint as the client portal's own plain
   inputs elsewhere on the site. */
.booking-field select {
  color: var(--ink);
}
.booking-field select:invalid {
  color: var(--ink-soft);
}
/* Honeypot — real form fields the browser autofills would defeat a
   simple display:none trap on the input itself (some autofill
   engines skip hidden fields, but not all), so this hides the whole
   labeled wrapper instead. Off-screen, not display:none — some spam
   bots skip display:none fields deliberately. */
.booking-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
/* Matches the client portal's Log in, per Liam: blue text, no fill, no box.
   It was a solid black block, which made it the loudest thing on any page
   here - nothing else on this site is a filled rectangle of colour. Kept
   left-aligned because this form is, where the portal's is centred; that is
   the only difference between them.

   Sized a step above the fields rather than at the portal's 0.86rem: this
   page's type scale runs smaller, and the point of the match is the
   treatment, not the millimetre. */
.booking-submit {
  align-self: flex-start;
  margin-top: 1.25rem;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--font);
  font-weight: 500;
  font-size: 0.78rem;
  letter-spacing: 0.01em;
  color: var(--fsmenu-accent);
  cursor: pointer;
  transition: opacity 0.2s ease;
}
.booking-submit:hover {
  text-decoration: underline;
  text-underline-offset: 0.16em;
  text-decoration-thickness: 1px;
}
/* Disabled while a submission is in flight - opacity still reads now that
   there is no fill behind it. */
.booking-submit:disabled {
  opacity: 0.5;
  cursor: default;
  text-decoration: none;
}
.booking-status {
  font-family: var(--font);
  font-size: var(--proj-size-sm);
  color: var(--ink);
  margin: 1.5rem 0 0;
}
.booking-status.is-error {
  color: #c0554a;
}
/* First/Last, Email/Phone stack on narrow phones — .journal-article's
   36rem column plus its own padding leaves too little room for two
   real text inputs side by side much below ~420px. */
@media (max-width: 26.25em) {
  .booking-row {
    flex-direction: column;
    gap: 1.5rem;
  }
}

/* ---- Site Model: Autodesk Platform Services viewer ------------------
   Replaces the old open-in-new-tab panel. Autodesk hard-blocks iframing
   viewer.autodesk.com (X-Frame-Options: DENY), so this is the real SDK
   embed instead — see aps-viewer.js for how the token and theme work.

   Sits at .ref-main's standard var(--side) inset like every other
   section, not full-bleed — same reasoning as .scroll-video above ("a
   frame around the viewport on all pages... always a gap").

   Fixed aspect rather than a viewport height: the Viewer needs a real
   sized box before it will initialise, and a percentage height inside a
   section with no height of its own collapses to zero. min-height keeps
   it usable on very wide screens where the ratio alone would make it
   short. */
.aps-viewer {
  position: relative;
  /* Its own section now, sibling to .scroll-video, so it needs the same
     top margin the row-based version got from its parent. */
  margin-top: 2.5rem;
  margin-bottom: 2.5rem;
  /* Liam: "skjul hus same issue with the viewer so wide you get stuck in
     it."

     Same trap as Gardhus, and the fix missed these two pages because it
     was hung on .proj__model - a wrapper Skjul and Palme do not have,
     their viewer being a bare <section>. Put on .aps-viewer itself so it
     reaches every page that has one, however the page is built.

     15% off the width, per the number Liam asked for on Gardhus, taken as
     margin so the strip either side is PAGE - the Viewer swallows wheel
     and drag, and what is needed is somewhere to scroll that the canvas
     does not own. Wide screens only: the phone rule further down does the
     same job in rem, and a percentage there would leave nothing to grab. */
  aspect-ratio: 16 / 10;
  min-height: 26rem;
  /* No frame, per Liam. The border and the grey tint both drew a rectangle
     round the model; now that the Viewer paints its own backdrop in the
     page's --bg (aps-viewer.js applyBackground), the model sits directly on
     the sheet and any box edge was the only thing left saying "embedded
     widget". overflow stays - it clips the Viewer's own chrome to the
     stage. */
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Marks the end of Section 2, matching .scroll-video's convention. */
  border-bottom: 1px solid var(--line);
}
/* The Viewer's own container. Autodesk's stylesheet gives it a theme
   background of its own (white in light-theme, near-black in dark), and the
   WebGL canvas inside it can land a pixel short of the container's box, so
   that colour showed as a hairline rectangle around the model - the "frame"
   still visible after the section's real border came off. Any sliver of
   container that shows must be the page's own paper. */
.aps-viewer .adsk-viewing-viewer {
  background: var(--bg) !important;
}

/* ---- The Viewer's own chrome, hidden ----
   Per Liam: no home icon, no toolbar, for now.

   Done in CSS rather than through the API deliberately. This build exposes
   none of setToolbarVisible / displayViewCube / setViewCubeVisibility -
   checked on the live viewer, all undefined - and thirteen extensions load
   themselves regardless of the `extensions: []` we pass at construction,
   among them Autodesk.ViewCubeUi. Unloading them one by one would be a list
   to maintain against a library that adds to it; hiding the three
   containers they render into holds whatever they do.

   !important because Autodesk sets these inline and re-shows them when a
   tool activates.

   To bring them back, delete this block - nothing else references it. The
   model still orbits, pans and zooms by drag and pinch; only the buttons
   are gone. */
.aps-viewer #guiviewer3d-toolbar,
.aps-viewer .viewcubeWrapper,
.aps-viewer .homeViewWrapper {
  display: none !important;
}
/* Except where the tools ARE the subject. The client portal journal piece
   shows a viewer with measure and section on, so that one keeps its
   toolbar - the cube and the home button stay gone, since the page has
   already said which way is north and the model starts framed. */
.aps-viewer--tools #guiviewer3d-toolbar {
  display: flex !important;
}

/* ---- Book a meeting, and the portal sign-in: no scrolling ----
   Liam: "each one taking too much vertical room i dont want to scroll
   down." Both are a single job - fill this in - and a form that runs off
   the bottom of a phone reads as longer than it is before a word has been
   typed. Measured at iPhone 16 (393x852): the booking page was 963 tall
   in a 730 frame.

   The savings are taken from spacing, never from the fields, and the
   biggest single one is keeping first/last and email/phone side by side
   on a phone instead of stacking: four short fields in two rows rather
   than four, which is 156px on its own. Two-up is fine at this width
   because none of the four needs more than half a line. */
.book-tight .journal-article { padding-top: 1.25rem; padding-bottom: 1.5rem; }
.book-tight .journal-article h1 { margin-bottom: 1rem; }
.book-tight .booking__intro { margin-bottom: 0.9rem; }
.book-tight .booking-form { gap: 0.875rem; }
.book-tight .booking-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.875rem; }
.book-tight .booking-field textarea { min-height: 0; }

/* ---- Gallery fullscreen ----
   The toggle at the pager's right, the same icon recipe as the drawings
   and the video toggles: hairline strokes in currentColor, expand swaps
   for compress when pressed. */
.ref-pagination__fullscreen {
  margin-left: auto;
  display: flex;
  align-items: center;
  color: var(--ink);
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.ref-pagination__fullscreen svg { width: 0.95rem; height: 0.95rem; fill: none; stroke: currentColor; stroke-width: 1.5; }
.ref-pagination__fullscreen .icon-compress { display: none; }
.ref-pagination__fullscreen[aria-pressed="true"] .icon-expand { display: none; }
.ref-pagination__fullscreen[aria-pressed="true"] .icon-compress { display: block; }

/* "View Full Screen" beside the icon, in the pager's own type - the row
   already carries the count and the arrows at this size, and a label in
   any other voice would read as a button bolted on rather than part of
   the control. gap sits it off the icon without a margin that would
   also apply when the label is hidden. */
.ref-pagination__fullscreen { gap: 0.45rem; }
.fs-label {
  font-family: var(--font);
  font-size: var(--proj-size-sm, 0.7rem);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}
.fs-label--exit { display: none; }
.ref-pagination__fullscreen[aria-pressed="true"] .fs-label--enter { display: none; }
.ref-pagination__fullscreen[aria-pressed="true"] .fs-label--exit { display: inline; }

/* Liam, of the gallery on his phone: "I can't see button is there one?"

   There was, at 15x15 with no label - a hairline icon and a tap target a
   third the 44px minimum. That was this rule's doing: the words were
   hidden below 30rem on the assumption they would push the arrows off
   the line. Measured on an iPhone 16, they do not. The pager is 363px
   wide and carries about 220px of content with the label in, so it was
   a guess, and a wrong one that cost the control its visibility.

   The label stays at every width. What changes on a phone is the target:
   the button gets a real one, sized for a thumb rather than a cursor. */
@media (max-width: 40rem) {
  .ref-pagination__fullscreen {
    min-height: 44px;
    padding-left: 0.5rem;
    margin-left: auto;
  }
  .ref-pagination__fullscreen svg { width: 1.1rem; height: 1.1rem; }
}

/* The wrap itself becomes the overlay - the same element, so the swipe
   in photo-carousel.js rides along untouched. The page-mode sizing (75vh,
   5:7) is undone here rather than overridden, and the photo goes from
   cover to contain: in the page it fills a portrait box, in fullscreen it
   has to show all of itself on whatever shape the screen is. No rule for
   :fullscreen alone, on purpose: iPhone Safari never has it, and the class
   is what it gets. */
.ref-photos__main-wrap.is-fullscreen {
  position: fixed;
  inset: 0;
  z-index: 60;
  /* Liam: "full screen doesnt work in the gallery for computer."

     It opened 28 pixels wide. The desktop rule gives this element
     justify-self:start so the carousel sizes from its height and aspect
     ratio rather than stretching across its grid column - and a FIXED
     element is still a grid item, so that alignment survived into
     fullscreen and collapsed inset:0 to the content's own minimum width.
     Height was right the whole time, which is why it read as "nothing
     happens" rather than as a layout fault.

     Only ever wrong on desktop: the narrow layout sets justify-self:
     stretch on the same element, which is why the phone was fine. */
  justify-self: stretch;
  align-self: stretch;
  width: auto;
  height: auto;
  max-width: none;
  aspect-ratio: auto;
  margin: 0;
  padding: var(--frame, 0.875rem);
  background: var(--bg);
  overflow: hidden;
  cursor: default;
}
.ref-photos__main-wrap.is-fullscreen .ref-photos__main {
  inset: var(--frame, 0.875rem);
  width: auto;
  height: auto;
  max-width: calc(100% - 2 * var(--frame, 0.875rem));
  max-height: calc(100% - 2 * var(--frame, 0.875rem));
  margin: auto;
  object-fit: contain;
}
html.has-gallery-fullscreen, html.has-gallery-fullscreen body { overflow: hidden; }

/* The overlay's chrome: a count top left, a close top right, and the
   progress bar along the foot - the drawings carousel's bar, one photo's
   width per photo, so a reader knows where they are in the run without a
   row of dots. Hidden until the class arrives. */
.gallery-fs { display: none; }
.ref-photos__main-wrap.is-fullscreen .gallery-fs { display: contents; }
.gallery-fs__count,
.gallery-fs__close {
  position: absolute;
  top: var(--frame, 0.875rem);
  z-index: 2;
  font-family: var(--font);
  font-size: var(--proj-size-md);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--ink);
  line-height: 1;
}
/* On the picture too, and equally bare. Its left and top are set in JS
   alongside the cross. */
.gallery-fs__count {
  padding: 0.35rem 0.5rem;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
}
/* Liam: "Just a cross no box around it or anything, nice and minimal."

   No pill, no ground, no label - just the menu's crossed hairlines,
   sitting ON the picture at its top right. JS measures where that corner
   actually is, since the photo is object-fit:contain and the letterboxing
   changes with every image (see placeOnPhoto in gallery-fullscreen.js).

   White, with a soft dark shadow under it rather than a box. A bare white
   cross on a white ceiling would vanish, which is what the box was for;
   the shadow does the same job without drawing a shape. Same reasoning as
   the hand - Liam: "the swipe animation should always be a white hand". */
.gallery-fs__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: #fff;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
  cursor: pointer;
}
.gallery-fs__x { display: block; width: 1.05rem; height: 1.05rem; overflow: visible; }
.gallery-fs__close:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.gallery-fs__bar {
  position: absolute;
  left: var(--frame, 0.875rem);
  bottom: var(--frame, 0.875rem);
  height: 2px;
  width: 0;
  z-index: 2;
  background: var(--ink);
  transition: width 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
/* A faint rail under the bar, so the bar reads as progress rather than as
   a stray rule at whatever width it happens to be. */
.ref-photos__main-wrap.is-fullscreen::after {
  content: "";
  position: absolute;
  left: var(--frame, 0.875rem);
  right: var(--frame, 0.875rem);
  bottom: var(--frame, 0.875rem);
  height: 2px;
  background: var(--line-strong);
  z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
  .gallery-fs__bar { transition: none; }
}

/* ---- Share this page ----
   The last thing on a client's project page. Set like a caption, not a
   call to action: it is offered to the person who owns the page, and they
   do not need persuading. The note beneath is where "Link copied" appears
   on a desktop, which has no share sheet to open. */
.share-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  margin: clamp(2rem, 6vh, 3.5rem) 0 0;
  padding-top: 1.3rem;
  border-top: 1px solid var(--line-strong);
}
.share-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font);
  font-size: var(--proj-size-md);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink);
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.share-btn__icon {
  width: 1.15em;
  height: 1.15em;
  flex: none;
}
.share-btn:hover span { text-decoration: underline; text-underline-offset: 0.16em; text-decoration-thickness: 1px; }
.share-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.share-row__note {
  margin: 0;
  font-family: var(--font);
  font-size: var(--proj-size-sm);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* ---- The demonstration buttons ----
   Under the four-ways viewer on the client portal journal page. They sit
   directly beneath the viewer with no gap, reading as its own footer rail
   rather than as a separate control panel: the viewer's border continues
   into them.

   A pressed button inverts rather than tinting, because the page is
   already carrying a light blue and a sage and does not need a third
   accent to say "this one". Wrapping is allowed and the row stays left,
   so on a phone they stack into two tidy lines instead of squeezing. */
.viewer-demos {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.6rem 0 1.6rem;
}
.viewer-demos__btn {
  font-family: var(--font);
  font-size: var(--proj-size-sm);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 0;
  padding: 0.42rem 0.8rem;
  cursor: pointer;
  transition: color 0.18s ease, background-color 0.18s ease, border-color 0.18s ease;
}
.viewer-demos__btn:hover {
  color: var(--ink);
  border-color: var(--line-strong);
}
.viewer-demos__btn.is-on {
  color: var(--bg);
  background: var(--ink);
  border-color: var(--ink);
}
.viewer-demos__btn:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .viewer-demos__btn { transition: none; }
}

/* ---- Two models, compared ----
   Liam: "3d models both new and demolition. demo first new after. no
   spinning just side by side comparison of models. or stacked whatever is
   best for the screen resolution."

   So: side by side from the width where two models are still worth
   looking at, stacked below it. auto-fit rather than a breakpoint means
   the switch happens when the models get too narrow to read, not at a
   device size someone guessed. minmax's floor is the narrowest a model
   still reads at; min-width:0 keeps a 1fr track from being widened by the
   viewer canvas inside it. */
.journal-models {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(21rem, 1fr));
  gap: 1.25rem;
  margin: 2rem 0 0.5rem;
}
.journal-models > figure {
  margin: 0;
  min-width: 0;
}
.journal-models .aps-viewer {
  aspect-ratio: 4 / 3;
}
.journal-models figcaption {
  font-family: var(--font);
  font-size: var(--proj-size-sm);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 0.6rem 0 0;
}
.aps-viewer__stage {
  position: absolute;
  inset: 0;
}
/* Same treatment as .scroll-video__placeholder-note so a missing model
   reads identically to missing footage. */
.aps-viewer__note {
  position: relative;
  z-index: 1;
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--proj-size-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  text-align: center;
  max-width: 24rem;
  padding: 0 2rem;
  margin: 0;
}
/* The Viewer paints its own background once a model is in; the border
   stays, the placeholder ground goes. */
@media (min-width: 40rem) {
  .aps-viewer {
    width: 85%;
    margin-inline: auto;
  }
  /* Liam: "bring the widths of the model in on palme hus a bit more."
   *
   * Keyed to the PROJECT rather than to the page, so it follows this
   * model wherever it is shown - the journal piece embeds the same one -
   * and so the next project page does not inherit a number chosen for
   * this house. Palme is a long low building on a wide site, so at 85%
   * it ran further across the page than the others do. */
  .aps-viewer[data-aps-project="palme-hus"] {
    width: 72%;
  }
  /* Gardhus's viewer sits inside .proj__model, which already insets it by
     7.5% a side. Both would compound to 28% and leave the model a
     postage stamp, so the wrapper's own padding stands down where this
     applies. */
  .proj__model { padding-inline: 0; }
}

.aps-viewer.is-loaded {
  background: transparent;
}
@media (max-width: 40rem) {
  /* Landscape on a phone, not portrait, and capped against the screen.

     A 4/5 box came out 454px tall in a 614px frame - 74% of the screen -
     and the Viewer claims vertical drags for orbiting, so that 74% is a
     dead zone the page cannot be scrolled through. Only about 160px of
     page was left to swipe on. Liam: "when i'm on gardhus and the model
     loads it takes up most of the screen that i can't swipe down, we need
     it to finish a bit shorter perhaps because it's the first thing you
     see."

     5/4 rather than merely smaller, because these are long sites seen in
     three-quarter view - the model is wider than it is tall, so a
     landscape box wastes less of itself on empty sky, and the phone
     re-framing in aps-viewer.js fits the model to whatever box it is
     given. The svh cap catches the short-and-wide case (a small phone in
     landscape) where the ratio alone would still fill the screen. */
  /* Liam, 2026-09-10 evening: "we reduced the height of the viewer for
     gardhus last night, now it's too short, make it a bit taller please -
     perhaps people can scroll on the sides of the page (frame) to go
     down."

     That second half is the part that makes the first half possible. The
     model was cut to 5/4 because the Viewer swallows vertical drags, so a
     tall canvas was a dead zone with no way past it. Giving the page back
     a strip down each side removes that constraint: the canvas can be
     taller because it is no longer the full width of the screen, and a
     thumb on either margin scrolls the page as it always did.

     4/3 rather than 5/4, and the gutters come off the width, so the box
     is taller AND narrower - which suits the model anyway, since it is a
     three-quarter view of a long site rather than a square.

     The gutters are 2rem: comfortably past the 44px minimum for a touch
     target when doubled across both sides, and narrow enough that the
     drawing still reads as sitting in the page rather than boxed in it. */
  /* The gutters are the PARENT's padding, not the viewer's margin: a
     later rule sets margin on .aps-viewer and quietly won, measured as
     0px against the 2rem asked for. Padding on .proj__model cannot be
     argued with and produces the same strip.

     And the height is explicit rather than an aspect ratio. With
     aspect-ratio the height comes FROM the width, so taking width away
     for the gutters made the box shorter - 272px against the 289 it was
     replacing, the exact opposite of what was asked. A height in svh is
     the thing being specified, so it is the thing written down. */
  .proj__model {
    padding-inline: 2rem;
    touch-action: pan-y;
  }
  .aps-viewer {
    aspect-ratio: auto;
    height: 46svh;
    min-height: 18rem;
    max-height: 64svh;
  }
}

/* A phone held sideways is 932px WIDE, so it misses every max-width phone
   rule and takes the desktop sizing - 523px of model in a 430px screen.
   The constraint that actually matters here is the height of the screen,
   not its width, so this is keyed to that, and desktops never see it.

   A fixed cap rather than a percentage of the viewport: svh resolves
   against the real window, which is what is wanted everywhere else but
   makes this rule unmeasurable. 20rem clears any landscape phone with
   room left to scroll. */
@media (max-height: 34rem) {
  .aps-viewer {
    /* min-height beats max-height in the CSS sizing algorithm, so the
       26rem floor the viewer carries everywhere else has to come down
       too, or the cap below it never takes effect. */
    min-height: 0;
    max-height: 20rem;
  }
}

/* ---- The swipe hint, in the fullscreen gallery ----
   Liam: "a little swipe animation plays so it's clear to people they have
   to swipe to go through photos."

   A hand that sweeps twice and leaves. Centred low, over the photo but
   under the close button, and never in the way of a tap: pointer-events
   are off, so a swipe that lands on the hint still reaches the gallery
   underneath. */
/* The hint now lives on the wrap rather than inside the fullscreen
   chrome, so it appears in both states - on the page when the gallery
   scrolls into view, and again on first fullscreen if it was never used.
   On the page it sits lower, since there is no progress bar under it. */
.gallery-fs__hint {
  position: absolute;
  left: 50%;
  bottom: 1.25rem;
  transform: translateX(-50%);
  display: none;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  /* Always white, per Liam, never the page's ink: this sits over a
     photograph in both places it appears, and the photograph is not the
     page. The shadow is what keeps it legible over a bright sky. */
  color: #fff;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6));
  pointer-events: none;
  opacity: 0;
}
.gallery-fs__hint.is-playing { display: flex; animation: fs-hint-fade 5.2s ease forwards; }
/* Clear of the progress bar once the overlay is open. */
.ref-photos__main-wrap.is-fullscreen .gallery-fs__hint { bottom: 3.25rem; }
.gallery-fs__hint svg { width: 3rem; height: 2rem; }
.gallery-fs__hint span {
  font-family: var(--font);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
/* The hand itself travels; the block fades in and out around it. */
.gallery-fs__hint.is-playing svg { animation: fs-hint-swipe 1.6s ease-in-out 2; }

@keyframes fs-hint-fade {
  0%   { opacity: 0; }
  8%   { opacity: 0.85; }
  80%  { opacity: 0.85; }
  100% { opacity: 0; }
}
@keyframes fs-hint-swipe {
  0%   { transform: translateX(1.1rem); }
  50%  { transform: translateX(-1.1rem); }
  100% { transform: translateX(1.1rem); }
}

/* A hint about a gesture is worthless to someone who has asked for less
   motion, and the gallery works without it. */
@media (prefers-reduced-motion: reduce) {
  .gallery-fs__hint.is-playing,
  .gallery-fs__hint.is-playing svg { animation: none; }
  .gallery-fs__hint.is-playing { opacity: 0.85; }
}

/* Liam: "Disable zoom on journal pages please. No zoom anywhere on site."

   The viewport meta on every page now carries user-scalable=no, which
   stops pinch. It does NOT stop iOS Safari's double-tap-to-zoom, which is
   a separate gesture with its own opt-out: touch-action manipulation
   means "pan and pinch-zoom the page normally, but no double-tap zoom",
   and it is the only thing that turns that gesture off.

   Set on the scroll containers rather than on body, so it cannot fight
   the pan-y already set on the model frame or the pan-y the photo
   carousel sets on itself. */
html, body { touch-action: manipulation; }

/* ==================================================================
   Icons at 1.5x on desktop
   ------------------------------------------------------------------
   Liam: "the icons on desktop are far too small and fine ... they should
   be bigger, 1.5x", then "make them bigger" - so 2x.

   Every icon on the site was sized for a phone held close, then shown
   unchanged on a screen a metre away. A 0.95rem glyph drawn with a 1px
   hairline is legible in the hand and nearly invisible across a desk -
   "small AND fine" is one cause, not two: the stroke is a fraction of
   the box, so a box that is too small takes the line down with it.

   Scaling the BOX rather than restating each stroke is what fixes the
   fineness too. These are SVGs on a viewBox, so the stroke scales with
   the geometry: 1.5x the icon is 1.5x the line, and every icon keeps
   the weight it was drawn at relative to itself.

   Collected here rather than edited in place at nine different rules,
   so the multiplier is one number in one block and desktop is the only
   thing it touches.
   ================================================================== */
@media (min-width: 60rem) {
  /* The menu bar and the hamburger are scaled in landing-page.css, which
     loads after this file - their rules live there and would beat
     anything written here. Same block, same multiplier, see the end of
     that file. */

  /* Video controls: sound, captions, fullscreen. */
  /* The button is a fixed 2.25rem box, so an icon scaled past it is
     simply clipped to the content area - measured 24px against the 26.4
     asked for. The box grows with its contents. */
  /* Liam: "the full screen toggle on the video and the toggle on the
     gallery photos should be the same size." They were 2.2rem and 1.9rem.
     All three video controls come down to the gallery's 1.9rem rather
     than only the fullscreen one, or the video's own three buttons would
     stop matching each other. */
  .video-sound-toggle,
  .video-fullscreen-toggle,
  .video-captions-toggle { width: 3.5rem; height: 3.5rem; }
  .video-sound-toggle svg,
  .video-fullscreen-toggle svg,
  .video-captions-toggle svg { width: 1.9rem; height: 1.9rem; }

  /* Gallery and drawings: the fullscreen toggles and the close cross. */
  .ref-pagination__fullscreen svg,
  .proj__fullscreen svg { width: 1.9rem; height: 1.9rem; }
  .gallery-fs__x { width: 2.1rem; height: 2.1rem; }

  /* Liam: "thicker line weights too ... all of this is desktop only."

     Scaling the box scaled the stroke with it, which kept each icon
     looking exactly as drawn - and "as drawn" was the hairline he was
     objecting to. So the weight is lifted independently of the size:
     1.5 to 2.4 where the stroke is set in CSS, and 1 to 1.8 on the paths
     that carry it as a presentation attribute, which any stylesheet
     outranks. */
  .ref-pagination__fullscreen svg,
  .proj__fullscreen svg,
  .video-sound-toggle svg,
  .video-fullscreen-toggle svg,
  .video-captions-toggle svg { stroke-width: 2.4; }
  .gallery-fs__x path { stroke-width: 1.8; }
  .share-btn__icon { stroke-width: 2.2; }
  .gallery-fs__hint svg { stroke-width: 2.2; }

  /* The share symbol, which is the one icon on a project page anybody is
     asked to press. */
  .share-btn__icon { width: 2.1rem; height: 2.1rem; }

  /* The swipe hand is deliberately NOT scaled: it only ever plays on a
     touch device, and this block is desktop. Listed so the omission
     reads as a decision rather than an oversight. */
}


/* ---- Skjul Hus: the site paragraph beside the axonometrics ----
   Liam: "move the text paragraph that starts with The block falls away...
   to the bottom left of sect 4. so it sits left of the axonometrics."
   Desktop only. The note column stretches to the drawing's full height and
   the paragraph takes margin-top:auto, which sets it on the column's
   bottom edge - level with the foot of the drawing beside it. Below 60rem
   the copy here is hidden and the original shows where it always has. */
.drawing-carousel__prose { display: none; }
@media (min-width: 60rem) {
  /* Liam, second pass: "should be at the top a line or two below 4
     ARCHITECTURAL DRAWINGS and take 1/4 width of page". So it sits at the
     top of the note column (whose 2.25rem padding-top is the line or two)
     and fills that column - the column IS the grid's 1fr of 1fr 3fr, the
     page's left quarter - instead of the 34ch measure that held it to
     about 210px, pushed to the bottom. */
  .drawing-carousel__prose {
    display: block;
    margin: 0;
    max-width: none;
  }
  /* The note column's 2.25rem padding-top on top of the carousel's own
     2.5rem margin put the paragraph about four lines under the heading.
     Without the padding it is the margin alone - two lines. Only where
     the prose is present; the per-drawing note keeps its padding. */
  .drawing-carousel__note:has(.drawing-carousel__prose) { padding-top: 0; }
  .ref-copy--moved { display: none; }
}


/* ---- Journal: one column per section, changing side at each title ----
   Liam: "text paragraphs only in the second OR 3rd quarter. not going
   across the two." Then: "keep all paragraphs in the same column after a
   title. only change the side when there is a new title. so titles and
   their paragraphs read in one column each." And "book a meeting must be
   on one side". Then: "put all journal articles paragraph right if there
   is an image. so meet liam can have the intro paragraph to the right of
   the portrait of liam". Desktop only.

   The article spans quarters two and three; each title, its paragraphs,
   subheadings and quotes take half of it - one quarter of the page - and
   the side swaps at every h2. Tables and pictures keep the full width.
   The closing "Book a meeting" box is one unit: it takes its section's
   side whole, and nothing inside it is counted.

   Two orders:
     no photo    - intro left, first h2 right, then alternating.
     with photo  - intro RIGHT, first h2 left, then alternating. A
                   portrait floats in the left column, so the intro sits
                   beside it (Meet Liam); a full-width photo simply has
                   the intro to its right-hand column above or below it.

   CSS cannot count "how many h2s came before me", so each rule says "from
   the nth h2 on, this side", and the next takes over from its own h2.
   Every rule in a family ties on specificity, so source order alone
   decides; the photo family carries :has() and so outranks the plain one
   wholesale. Twelve sections is well past any entry so far.

   The doubled class is deliberate: :where() contributes nothing, and the
   article's own ".journal-article p", "h2" and "h3" rules (0,1,1) set the
   whole margin shorthand - a single class (0,1,0) lost to them and held
   every section on the left. Only direct children are placed. */
@media (min-width: 60rem) {
  .journal-article.journal-article > :where(p:not([class]), h2, h3, blockquote, .journal-cta) {
    box-sizing: border-box;
    width: 50%;
    margin-left: 0;
    padding-right: 1.5rem;
  }
  .journal-article.journal-article > :where(blockquote) { margin-right: 0; }
  .journal-article.journal-article > :where(h2:nth-of-type(1), h2:nth-of-type(1) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article > :where(h2:nth-of-type(2), h2:nth-of-type(2) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article > :where(h2:nth-of-type(3), h2:nth-of-type(3) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article > :where(h2:nth-of-type(4), h2:nth-of-type(4) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article > :where(h2:nth-of-type(5), h2:nth-of-type(5) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article > :where(h2:nth-of-type(6), h2:nth-of-type(6) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article > :where(h2:nth-of-type(7), h2:nth-of-type(7) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article > :where(h2:nth-of-type(8), h2:nth-of-type(8) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article > :where(h2:nth-of-type(9), h2:nth-of-type(9) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article > :where(h2:nth-of-type(10), h2:nth-of-type(10) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article > :where(h2:nth-of-type(11), h2:nth-of-type(11) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article > :where(h2:nth-of-type(12), h2:nth-of-type(12) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }

  /* With a photo: the intro (everything before the first h2) on the right. */
  .journal-article.journal-article:has(> .journal-article__photo) > :where(p:not([class]), h3, blockquote) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(1), h2:nth-of-type(1) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(2), h2:nth-of-type(2) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(3), h2:nth-of-type(3) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(4), h2:nth-of-type(4) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(5), h2:nth-of-type(5) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(6), h2:nth-of-type(6) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(7), h2:nth-of-type(7) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(8), h2:nth-of-type(8) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(9), h2:nth-of-type(9) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(10), h2:nth-of-type(10) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(11), h2:nth-of-type(11) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 0; padding-right: 1.5rem; padding-left: 0; }
  .journal-article.journal-article:has(> .journal-article__photo) > :where(h2:nth-of-type(12), h2:nth-of-type(12) ~ :is(p:not([class]), h3, blockquote, .journal-cta)) { margin-left: 50%; padding-right: 0; padding-left: 1.5rem; }

  /* The portrait takes the left column and the intro wraps beside it.
     Anything full width after it clears the float, so a table or the
     next photograph never tucks up beside the face. */
  .journal-article .journal-article__photo--portrait {
    float: left;
    box-sizing: border-box;
    width: 50%;
    max-width: none;
    padding-right: 1.5rem;
  }
  .journal-article > :where(h2, table, div, section, figure:not(.journal-article__photo--portrait)) {
    clear: both;
  }
}

/* A paragraph with its quote beside it - Liam: "put the quote right of the
   paragraph on meet liam page". The pair spans both quarters: paragraph in
   the left, quote level with its first line in the right. Desktop only; on
   a phone the div is plain flow. Its children are not direct children of
   the article, so the section-column rules above leave them alone. */
@media (min-width: 60rem) {
  .journal-article > .journal-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: start;
  }
  .journal-pair > p {
    margin-top: 0;
    padding-right: 1.5rem;
  }
  .journal-pair > blockquote {
    margin: 0 0 1rem 1.5rem;
  }
}

/* A plain reading page (privacy.html): one column, no alternating sides.
   Three classes (0,3,0) so it outranks the section-column families above
   (0,2,0) without !important. */
@media (min-width: 60rem) {
  .journal-article.journal-article.journal-article--plain > :where(p, h2, h3, blockquote) {
    width: 100%;
    margin-left: 0;
    padding-left: 0;
    padding-right: 0;
  }
}

/* "Book a meeting" at the foot of a project page - Liam: "lets add the book
   a meeting at bottom of palme page on phone only". The ads land on this
   page, and on a phone it had no way to book short of opening the menu;
   desktop already has the footer's own Book a meeting. Same blue and
   weight as that one. */
.ref-book {
  display: block;
  margin: 1.5rem 0 1rem;
  /* 16px text on ~40px of height: --proj-size-md is 10px on a phone,
     which drew a 15px-tall tap target - too small for the one thing an
     ad visitor has come to press. */
  padding: 0.75rem var(--side, 1rem);
  font-family: var(--font);
  font-size: 1rem;
  font-weight: 400;
  color: var(--fsmenu-accent, #232fb0);
  text-decoration: none;
}
.ref-book:active { text-decoration: underline; text-underline-offset: 0.2em; }
@media (min-width: 60rem) {
  .ref-book { display: none; }
}

/* ---- Palme Hus: sections 3 and 4 at a quarter width ----
   Liam: "lets make those axos and floor plans 3 and 4 quarter only not 3/4."

   Both ran at roughly three quarters of the sheet - the model at 72%
   (set when he asked to "bring the widths of the model in a bit more"),
   the drawing carousel on a 1fr 3fr grid from "architectural drawings
   should take up 3/4 with room for text on the left". Both come in to a
   quarter: the grid inverts to 3fr 1fr, which leaves the note column the
   wide cell and the drawing the narrow one on the right.

   Desktop only. Below 60rem the carousel is already a single column and
   the viewer is on its own phone sizing, where a quarter is nothing.

   The viewer's 26rem min-height has to come down with the width, or it
   beats the 16/10 ratio and a quarter-width box comes out taller than it
   is wide. Keyed to the project, not the page, so the other models keep
   the width chosen for them. */
@media (min-width: 60rem) {
  .aps-viewer[data-aps-project="palme-hus"] {
    width: 25%;
    min-height: 0;
  }
  .drawing-carousel {
    grid-template-columns: 3fr 1fr;
  }
}
