/* dorn-tokens.css is deliberately NOT @imported here. An @import cannot be
   discovered until this file has been fetched and parsed, so it serialises
   the tokens, and the @font-face rules that consume them, behind this file's
   own round trip. Every page loads it as its own stylesheet link instead,
   ordered before this one so the cascade is unchanged, with preloads for the
   two woff2 files alongside. If you ever reinstate the @import, remove those
   links or the tokens are fetched twice. */

/* Self-hosted per canon/brand-system.md §BS-9a.12: serving Marcellus/Geist from
   Google's CDN sends every visitor's IP to a third party (EU personal-data
   processing). Both files below are the Latin-subset statics Google's own
   css2 endpoint serves for this site's content; Geist ships as a variable
   font, so 400 and 500 both pin the same file to a named weight instance,
   matching Google's own approach exactly. */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/geist-latin.woff2') format('woff2');
}

@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('fonts/geist-latin.woff2') format('woff2');
}

@font-face {
  font-family: 'Marcellus';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/marcellus-latin.woff2') format('woff2');
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Light only, declared explicitly. Without this, Chrome's Android auto dark
   mode is free to invert the palette, which would push pure white and pure
   black into the computed styles and fail the palette rule (site/CLAUDE.md
   checklist item 3). The footer's dark band is a scoped inversion, not a
   theme, so there is no dark variant to declare here. */
:root {
  color-scheme: light;
}

/* Scroll mechanism note: earlier builds animated native window.scrollTo
   frame-by-frame under CSS scroll-snap. That fought the browser (JS easing
   re-smoothed on top of native snap correction) and produced a visible
   stutter. Reproducing vincentvanduysen.com's actual mechanism (confirmed
   by inspecting its fullPage.js bundle: it never touches native scroll,
   it disables page scrolling and animates transform: translate3d(...) on
   a wrapper under a plain CSS transition) fixes this at the root: the
   compositor drives the motion, so there's nothing left to fight. */
html, body {
  height: 100%;
  overflow: hidden;
  /* The page no longer scrolls at all; without this, mobile browsers are
     still free to interpret a vertical swipe as pull-to-refresh or a
     back/forward navigation gesture, competing with the custom touch-swipe
     handler in script.js. */
  overscroll-behavior: none;
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text-primary);
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

.scroll-track {
  transition: transform 1000ms cubic-bezier(0.25, 0.1, 0.25, 1);
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .scroll-track {
    transition: none;
  }
}

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--color-bg);
  color: var(--color-text-primary);
  padding: 0.75em 1.25em;
  z-index: 100;
  font-family: var(--font-body);
  font-weight: 500;
}

.skip-link:focus {
  left: 1em;
  top: 1em;
}

/* ---------- Header ---------- */

.stack-section {
  height: 100vh;
  overflow: hidden;
}

.site-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  padding: 2rem;
  text-align: center;
}

/* ---------- Wordmark (static site chrome) ---------- */

/* V2: no longer part of the header's landing composition - it's fixed,
   double the old display size, and persists at the same top-left position
   across every section (including the header itself), rather than
   scrolling away with the landing content. z-index keeps it above the
   scroll-track's images at all times: on wide viewports there's enough
   paper margin that images never visually reach it, but on narrower
   viewports where the image's left edge would otherwise pass beneath this
   corner, the wordmark stays the top layer and the image glides underneath
   rather than the two fighting for the same stacking position. */
/* The <h1> stays for page-heading semantics, but a fixed-position button
   inside it would otherwise leave the heading's own (empty) box sitting in
   normal flow, adding unwanted space via its UA-stylesheet margins. display:
   contents removes that box entirely - the button becomes the only thing
   rendered, while the <h1> is still present for the accessibility tree. */
.wordmark-heading {
  display: contents;
}

.wordmark {
  /* Interim placeholder: live text standing in for the outlined-SVG
     wordmark specified in brand-execution-brief.md §BE-2, which has not been produced yet.
     Replace with the SVG asset once tracking/outline/kerning are locked. */
  position: fixed;
  top: 2rem;
  left: 2rem;
  z-index: 20;
  margin: 0;
  border: none;
  background: transparent;
  padding: 0;
  cursor: pointer;
  font-family: var(--font-display);
  font-size: var(--text-display-1);
  /* Explicit, not inherited: a <button> (main site) and an <a> (privacy
     page) resolve the UA default line-height differently - the button's
     font shorthand resets it to "normal", the anchor inherits the body's
     1.65 - so without this, the two pages render the same top offset but
     a different glyph position inside the box. Fixing it here keeps both
     pages pixel-identical regardless of element type. */
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-primary);
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  text-decoration: none;
  /* top/left join the transition here (previously colour-only) so the
     footer-band slide (below) animates on the same curve as the section
     snap it's synced to. Colour stays at the universal 150ms (§BS-4) as its
     *persistent* default - always, including hover in/out - matching every
     other hover state on the site. The slower, curve-matched colour swap
     used specifically when crossing into/out of the footer band is applied
     by script.js as a temporary inline override just for that crossing
     (see updateWordmarkFooterState), not declared here: declaring it here
     previously meant hover-exit inherited the same 1000ms duration whenever
     it happened to land on this rule, reading as a sluggish revert compared
     to every other hover state on the site. */
  transition: top var(--motion-scroll-duration) var(--motion-scroll-ease),
    left var(--motion-scroll-duration) var(--motion-scroll-ease),
    color 150ms ease;
}

.wordmark:focus-visible {
  color: var(--color-accent);
  outline: 2px solid var(--color-accent);
  outline-offset: 8px;
}

/* :active (not :hover) is the deliberate touch tap-feedback: it's tied to
   the actual press, so it clears on release with no sticky-hover risk, and
   fades out on the same 150ms colour transition as hover. Wordmark-only -
   no other control on the site gets tap colour feedback. */
.wordmark:active {
  color: var(--color-accent);
}

/* Hover only where a device actually has hover (§BS-4 remains the source
   of the slate value and timing - this just scopes when it applies).
   Unscoped :hover sticks on tap: touch has no mouseleave to clear it, so a
   tap on the wordmark would turn it slate and leave it slate until the
   next tap elsewhere on the screen. */
@media (hover: hover) and (pointer: fine) {
  .wordmark:hover {
    color: var(--color-accent);
  }
}

/* Footer closing band (canon/brand-system.md §BS-8a, founder-ratified 25 July
   2026): the wordmark slides down into the band when the footer section
   becomes active - position only here; the crossing's temporary colour-curve
   override lives in script.js (see the .wordmark rule above). */
.wordmark.wordmark--footer {
  top: calc(20vh + 2rem);
  color: var(--color-text-primary-invert);
}

.wordmark.wordmark--footer:focus-visible {
  color: var(--color-accent);
}

.wordmark.wordmark--footer:active {
  color: var(--color-accent);
}

@media (hover: hover) and (pointer: fine) {
  .wordmark.wordmark--footer:hover {
    color: var(--color-accent);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The footer-band slide is a position change, not just colour - under
     reduced motion it jumps instantly like every other section move,
     rather than keeping a quick colour fade the way hover states do. */
  .wordmark {
    transition: none;
  }
}

.tagline-primary {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--text-body-lg);
  line-height: 1.2;
  color: var(--color-text-primary);
  letter-spacing: 0.02em;
}

/* ---------- Portfolio stack ---------- */

.project {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4rem 3rem;
  -webkit-tap-highlight-color: transparent;
}

/* Sized well below the viewport (per founder direction, matching
   vincentvanduysen.com / studiocera.fr) rather than full-bleed: one
   project is still visible per screen, but the image itself is inset
   with paper visible around it, not edge-to-edge cover. The image stays
   centred in the viewport (.project just centres this one flex child);
   .caption is positioned off .image-frame directly (see below) so it reads
   as an addition to the right, not something the image shifts over for. */
.image-frame {
  position: relative;
  max-width: var(--frame-width);
  max-height: var(--frame-height-max);
}

.image-layer.final {
  display: block;
  max-width: var(--frame-width);
  max-height: var(--frame-height-max);
  width: auto;
  height: auto;
}

.image-layer.clay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 200ms ease-in-out;
  filter: grayscale(1) brightness(0.82) contrast(1.05);
}

@media (prefers-reduced-motion: reduce) {
  .image-layer.clay {
    transition: none;
  }
}

.project.revealed .image-layer.clay {
  opacity: 1;
}

/* ---------- Lightbox (trial mechanic, not yet ratified into canon) ----------
   A single shared overlay, reused for every project rather than one per
   image - script.js repoints its <img> src/alt on open. Sits below
   --z-chrome (the fixed wordmark) by design, so the wordmark shows through
   on top of an enlarged image and doubles as the close control - there is no
   separate "x" button; tapping/clicking anywhere on the overlay, including
   the wordmark, closes it the same way. Kept in the DOM at all times (never
   `display: none`) so opacity/transform can transition on both open and
   close; `visibility` is what actually removes it from hit-testing and the
   accessibility tree between uses, delayed on close so it doesn't cut the
   fade short. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Paper, not off-black (trial revision, 30 July 2026): an inverted backdrop
     read as a jarring light/dark flick on open/close against the site's
     paper background everywhere else. Paper also means the letterboxed
     strips either side of a non-matching aspect ratio recede into the page
     rather than reading as a dark frame. */
  background-color: var(--color-bg);
  box-sizing: border-box;
  /* Trial border (30 July 2026): whatever aspect ratio a viewport leaves
     unfilled on the image's short axis reads as an unbalanced two-sided gap
     rather than a deliberate margin. Padding all round makes the image
     inset on every side consistently instead - it costs a bit of maximum
     size, but composes better than a border on two sides only. Reuses the
     existing space-xl token rather than a new magic value; a candidate to
     tune once this trial is judged worth keeping. */
  padding: var(--space-xl);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition:
    opacity var(--motion-lightbox-duration) var(--motion-lightbox-ease),
    background-color var(--motion-lightbox-duration) var(--motion-lightbox-ease),
    visibility 0s var(--motion-lightbox-duration);
}

.lightbox.open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition:
    opacity var(--motion-lightbox-duration) var(--motion-lightbox-ease),
    background-color var(--motion-lightbox-duration) var(--motion-lightbox-ease);
}

.lightbox-image {
  /* Percentages, not vw/vh: sized against .lightbox's own content box (i.e.
     inside its padding above), so the padding reads as a real inset on every
     side rather than the image still reaching the viewport edge on its
     unconstrained axis. min() still composes with the custom property
     script.js sets on open (the clicked image's native pixel width), so the
     image is capped at whichever is smaller - the padded box or its own
     resolution - and is never upscaled past native. */
  max-width: min(100%, var(--lightbox-native-width, 100%));
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

@media (prefers-reduced-motion: reduce) {
  .lightbox,
  .lightbox-image {
    transition: none;
  }
}

.caption {
  /* Positioned directly off .image-frame's own box (not a flex sibling),
     so the bottom edge lands exactly on the image's bottom edge -
     pixel-precise, rather than an approximated flex alignment.
     The -3px counters the descender space line-height reserves below the
     last line's baseline (fonts hold room for descenders even in all-caps
     text), measured via canvas fontBoundingBoxDescent for Geist at this
     size. Starting value - founder to eye-check, same as the old top nudge. */
  position: absolute;
  bottom: -3px;
  left: 100%;
  margin-left: 2rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-caption-pair);
  text-align: left;
  max-width: 20ch;
  white-space: nowrap;
  /* Caption is a DOM child of .image-frame (positioned outside its visual
     box, but still inside its subtree), so the frame's mouseenter/mouseleave
     hover-reveal and click/tap-to-lightbox listeners were firing whenever the
     pointer was over the caption text too, not just the image itself
     (founder-reported 30 July 2026). Neither is currently interactive - it's
     a plain name/location label - so removing it from hit-testing entirely
     is the direct fix rather than working around it in the reveal logic.
     Revisit if the caption ever becomes a genuine click target (e.g. the
     future project-detail entry point discussed the same session). */
  pointer-events: none;
}

.project-name {
  margin: 0;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--text-body);
  line-height: 1.2;
  color: var(--color-text-primary);
  letter-spacing: 0.02em;
}

.project-location {
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.2;
  font-size: var(--text-label);
  color: var(--color-text-primary);
  text-transform: uppercase;
  letter-spacing: var(--ls-caps);
}

/* ---------- About + CTA (merged into the footer) ---------- */

/* A separate about screen plus a footer screen was one screen too many for
   what's a short blurb and a contact line - both now live in the single
   closing section, ordered caption-style block (label + copy) -> CTA ->
   the utility/legal bits. Each block is left-aligned internally (justify
   reads poorly at this measure/length - it earns its keep on long,
   high-volume text, not 4-5 line paragraphs) even though the section as a
   whole stays centred in the viewport; the same shared max-width keeps all
   three blocks - and the hairline before the utility block - on one common
   left/right edge, so the section reads as one aligned column rather than
   three independently-centred fragments.

   Footer closing band (canon/brand-system.md §BS-8a, founder-ratified
   25 July 2026): the section below covers the dark band, the wordmark's
   footer position/colour, and matching Studio/CTA typography and rhythm
   to Privacy statement/Business details. One of these reverses an explicit
   prior rule rather than filling a gap, and it is ratified, not left open:
   .legal-heading's font (was Marcellus display-2, brand-system.md §BS-1)
   now takes the unified Geist label treatment §BS-8a's "Typography and
   rhythm unification" paragraph specifies. .about-label ("Studio") takes
   that same treatment but was never Marcellus - it has always been a Geist
   label (brand-execution-brief.md §BE-5, "never a Marcellus heading") - so
   it is a gap filled, not a rule reversed. */
.about-content,
.cta {
  /* Matches a portrait project image's actual RENDERED width, not just its
     max-width ceiling. A 2:3 portrait almost always hits max-height: 68vh
     first (68vh converted to width via the 2:3 ratio is usually narrower
     than 56vw/760px), so matching only the max-width value left this
     noticeably wider than the images actually render at. This mirrors the
     same three-way constraint the image itself resolves against.
     Fixed `width`, not `max-width`: .site-footer centres its children on
     the cross-axis, so a shorter block (the one-line CTA, e.g.) would
     otherwise shrink to its own content width and re-centre independently,
     breaking the shared left edge the two blocks are meant to share.
     .footer-utility used to share this rule too (a common left edge for all
     three blocks); the footer closing band (§BS-8a, 24 July 2026 build,
     ratified 25 July 2026) detaches it to its own bottom-left position
     instead - see .footer-utility below. */
  width: var(--column-width);
  text-align: left;
}

.about-content {
  /* Anchors the about/CTA block to the wordmark's own footer-band resting
     position, rather than the whole group being bottom- or centre-anchored
     in the section. Same top offset and the same clamp() as
     .wordmark.wordmark--footer's `top` and font-size (its line-height:1
     makes rendered height == font-size) - so this would otherwise sit
     flush with the wordmark's bottom edge at any viewport size, not just
     the one it was eyeballed at. .site-footer's own padding-top is zeroed
     out (below) so this is the only thing governing vertical position.
     The added --space-xl term (27 July 2026) is the wordmark-to-header
     gap: flush anchoring reads fine on wide desktop, where horizontal
     room keeps the wordmark and "Studio" visually apart, but on narrower
     desktop widths - still well above the 640px compact breakpoint - the
     text column runs close enough under the wordmark that the flush gap
     reads as crowding, the same problem the compact breakpoint's own gap
     (below) already solves for phones. One constant applied at every
     width, rather than a third breakpoint tier for the narrow-desktop
     case specifically. */
  margin-top: calc(20vh + 2rem + clamp(4rem, 12vw, 7rem) + var(--space-xl));
}

.about-label {
  /* Geist, matching .legal-heading exactly (canon/brand-system.md §BS-8a,
     "Typography and rhythm unification", founder-ratified 25 July 2026).
     Not a reversal: "Studio" has always been a Geist label, never a
     Marcellus heading (brand-execution-brief.md §BE-5) - only
     .legal-heading changed typeface. Margin-bottom matches
     .legal-heading's own header-to-first-content gap, so Studio to its
     paragraph mirrors Privacy statement/Business details' heading to their
     first line. */
  margin: 0 0 2rem;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--text-label);
  line-height: var(--lh-tight);
  color: var(--color-text-primary-invert);
  text-transform: uppercase;
  letter-spacing: var(--ls-caps);
}

.about-text {
  /* 0.85rem (27 July 2026, founder testing pass) - was 1rem, stepping to
     0.85rem at compact; now flat at every width, no step needed since the
     desktop value already equals the old compact one.
     No max-width here, deliberately (canon/brand-system.md §BS-1, "Text-level
     measure cap withdrawn", 27 July 2026). A 53ch cap was added and withdrawn
     the same day: it detached this column's right edge from the image frame's
     right edge on wide monitors, and made the apparent column width jump when
     clicking through to a utility page. Width is governed solely by
     .about-content's --column-width, which tracks a project image's rendered
     size - that shared edge is a load-bearing design constraint, not a
     coincidence, so do not reintroduce a text-level cap here or on
     .legal-text. The site's measure ceiling was corrected to 126 characters
     against this structure (§BS-1); it is not an unbounded line. */
  margin: 0;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--text-body);
  line-height: 1.65;
  color: var(--color-text-primary-invert);
}

.cta-text {
  /* Weight 400/0.85rem (27 July 2026, was 1rem - see .about-text), matching
     .about-text/.legal-text: the CTA/email read as plain body text, same
     register as Business details/Privacy, no bold or size emphasis. Margin
     matches the header-to-text and paragraph-to-CTA-block rhythm used
     elsewhere in the footer (space-xl, 32px) - literal 28px, not 2rem,
     because .cta-email (an inline link) carries a couple of px of its own
     line-height leading; confirmed in-browser that 28px lands the actual
     rendered gap on 32px exactly at this font size. Was 30px, calibrated
     when this text was 1rem - re-measured after the 27 July size change,
     since the leading overshoot this constant compensates for scales with
     font-size (needs re-checking again if the base size ever changes).
     Distinct from the compact override below, which targets a different
     rhythm value (space-lg, 24px), not a different font size - the two
     no longer differ because of text size, only because the footer's own
     rhythm steps down at that breakpoint regardless of type size. */
  margin: 0 0 28px;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--text-body);
  line-height: 1.65;
  color: var(--color-text-primary-invert);
}

.cta-email {
  /* Weight 400/0.85rem (27 July 2026, was 1rem - see .about-text), not
     sized or weighted up from the surrounding text - matches .legal-link's
     approach on Business details/Privacy, where the link carries no
     distinct typography, only the border-bottom underline marks it
     clickable. Underlined at rest, not just on hover, unlike the quieter
     footer-contact links below it: this is still the one click the whole
     section exists to produce, so it still needs to read as clickable
     without waiting for a hover - that affordance now comes entirely from
     the border, not from size or weight. */
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--text-body);
  color: var(--color-text-primary-invert);
  text-decoration: none;
  border-bottom: 1px solid var(--color-text-primary-invert);
  /* Matches .wordmark and .project, which already suppress this deliberately.
     Left unset, tapping this link flashes the UA default highlight box, a
     non-brand colour appearing on the dark band. Same reasoning applies to
     .legal-line a and .legal-link below. */
  -webkit-tap-highlight-color: transparent;
  transition: color 150ms ease, border-color 150ms ease;
}

.cta-email:hover,
.cta-email:focus-visible {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.cta-email:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 4px;
}

/* ---------- Footer ---------- */

.site-footer {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* flex-start, not center: the about/CTA block is anchored explicitly to
     the wordmark via .about-content's own margin-top (below), not by the
     flex container's own alignment, so justify-content just needs to not
     fight that. padding-top is zeroed for the same reason - .about-content's
     margin-top is the only thing controlling vertical position now.
     .footer-utility (legal line) is pulled out of this flow entirely - see
     its own rule below. */
  justify-content: flex-start;
  /* 2rem (--space-xl) is the ratified footer rhythm (brand-system.md §BS-2);
     --space-2xl is reserved, not deployed. Matches the field-to-field gap
     on the Business details page (.legal-subheading's 2rem top margin),
     per founder direction to mirror that page's rhythm here. */
  gap: var(--space-xl);
  padding: 0 3rem 4rem;
  text-align: center;
}

/* Footer closing band (canon/brand-system.md §BS-8a, founder-ratified 25 July
   2026): a hard-stop gradient gets the crisp 20% paper / 80% off-black split
   with no extra DOM node. */
.site-footer::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    to bottom,
    var(--color-bg) 0%,
    var(--color-bg) 20%,
    var(--color-bg-invert) 20%,
    var(--color-bg-invert) 100%
  );
}

.footer-utility {
  /* Detached from the about/CTA column (canon/brand-system.md §BS-8a,
     founder-ratified 25 July 2026) - KvK/BTW/©/Privacy/Business-details read
     as a distinct register, not just the last block in the same flex column,
     positioned bottom-left of the dark band. Side/bottom offsets match
     .site-footer's own padding scale (space-3xl), so its left edge lines up
     with everything else's margin rather than introducing a new value. The
     hairline-above rhythm rule this used to carry (brand-system.md §BS-2's
     "footer utility rule") no longer applies now it's not sitting directly
     under the CTA - dropped, not replaced. */
  position: absolute;
  left: var(--space-3xl);
  bottom: var(--space-3xl);
  max-width: calc(100% - 2 * var(--space-3xl));
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-align: left;
}

.legal-line {
  /* Umber-grey (secondary), not warm-mid: warm-mid on paper is ~3.3:1,
     below the 4.5:1 body-text threshold at this size.
     -invert token (canon/brand-system.md §BS-8a, founder-ratified 25 July
     2026): on the footer's dark band, warm-light on off-black measures
     13.8:1, comfortably clear of the 4.5:1 body-text threshold.
     0.75rem (27 July 2026, founder testing pass) - was 0.8rem, stepping to
     0.75rem at compact; now flat at every width, no step needed since the
     desktop value already equals the old compact one. */
  color: var(--color-text-secondary-invert);
  font-size: var(--text-micro);
}

.legal-line a {
  color: var(--color-text-secondary-invert);
  text-decoration: underline;
  text-underline-offset: 2px;
  -webkit-tap-highlight-color: transparent;
  transition: color 150ms ease;
}

.legal-line a:hover,
.legal-line a:focus-visible {
  color: var(--color-accent);
}

.legal-line a:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* ---------- Utility pages (static pages, no scroll-track) ---------- */

/* Privacy statement and Business details are the two pages authorised
   outside the single-page structure (brand-execution-brief.md §BE-5a,
   site/CLAUDE.md Scope §1). Every rule in this block is written against
   body.static-page, which both carry. They take no part in the scroll-track
   mechanism above, so html/body's overflow:hidden is switched back to a
   normal document scroll here, and they reuse the same type/colour tokens
   with no further design elaboration, per spec. */
body.static-page {
  overflow: auto;
  /* padding-top is 0, not a wordmark-clearing calc() (canon/brand-system.md
     §BS-8a, founder-ratified 25 July 2026): .legal-content's own margin-top
     handles that anchor instead, matching how .site-footer zeroed its own
     padding-top in favour of .about-content's margin-top. See both rules
     below. Bottom is 4rem (27 July 2026, founder request), matching
     .site-footer's own bottom padding - was 6rem, an unmatched value with
     no equivalent to align to on the footer side. */
  padding: 0 3rem 4rem;
}

/* Wordmark continuity onto the utility pages (canon/brand-system.md §BS-8a,
   founder-ratified 25 July 2026): the wordmark sits at the same position it
   lands at in the footer's closing band (.wordmark--footer on the main
   page), so clicking through from
   there to Privacy/Business details doesn't jump the wordmark's on-screen
   position, even though these pages stay light (an earlier full-dark
   attempt read as too different in character and was reverted). Static -
   no transition needed, these pages have no scroll-track to sync to,
   they just start here. Colour is untouched (default off-black): these
   pages stay light, so no inversion is needed. */
body.static-page .wordmark {
  top: calc(20vh + 2rem);
}

.legal-content {
  /* Width matches .about-content's --column-width exactly (an earlier
     double-width attempt aimed to cut Privacy statement's scroll, but
     there's simply enough copy that it still scrolled regardless - footer
     continuity won out over that). Left edge matches where .about-content's
     left edge lands in the footer: both this page's body and .site-footer
     share the same space-3xl (3rem) side padding, so centering a
     --column-width box in the available space left of that padding,
     then reading off its left margin, reproduces the same on-screen X
     position .about-content gets from its own centered flex layout -
     without duplicating .site-footer's flex context here. At this width
     the box ends up exactly centered anyway (this margin-left is the same
     value true centering would produce), so this reads as ordinary
     centering, just computed to guarantee it matches .about-content's
     actual left edge rather than coincidentally lining up.
     --legal-side-padding is a readability alias mirroring
     body.static-page's DESKTOP side padding (the 3rem in its own rule
     above, written here as the space-3xl token). It is not tracked
     automatically: change that 3rem and this must be changed by hand to
     match. It is also not used at the compact breakpoint below - there
     body.static-page steps to 2rem (space-xl, the phone alignment margin;
     canon/brand-system.md §BS-3 explicitly forbids normalising it to
     space-lg, which would put this text 8px left of the wordmark) and
     .legal-content sets margin-left: 0, so the inset formula does not run
     at all. This variable and the formula below are desktop-only. */
  --legal-side-padding: var(--space-3xl);
  /* --legal-column is one level of indirection so the width and the inset
     that centres it can never be changed apart. The short-height block below
     re-points it at --column-width-short; before 27 July 2026 that block
     changed the footer's column but not this one, so at any viewport 750px
     tall or shorter the footer ran 583px against this page's 326px and the
     click-through jumped by up to 302px (canon/brand-system.md §BS-3, "at
     <=750px height"). Do not re-inline --column-width into either line. */
  --legal-column: var(--column-width);
  --legal-inset: calc((100vw - (var(--legal-side-padding) * 2) - var(--legal-column)) / 2);
  margin-left: var(--legal-inset);
  width: var(--legal-column);
  /* Same anchor formula as .about-content's margin-top (§ site-footer
     rules above), including the wordmark-to-header gap it adds (27 July
     2026, see that rule's comment) - both fixes the same narrow-desktop
     crowding here and keeps the two pages' anchor formulas identical. */
  margin-top: calc(20vh + 2rem + clamp(4rem, 12vw, 7rem) + var(--space-xl));
}

.legal-heading {
  /* Geist, matching .about-label ("Studio") and .legal-subheading exactly.
     Was Marcellus display-2 (canon/brand-system.md §BS-1); the reversal is
     ratified by §BS-8a's "Typography and rhythm unification" paragraph
     (25 July 2026) and nothing is left open here. This is the only one of
     the two unified headings that actually changed typeface - see the
     banner comment at the top of the About+CTA section. */
  margin: 0 0 2rem;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--text-label);
  line-height: var(--lh-tight);
  text-transform: uppercase;
  letter-spacing: var(--ls-caps);
  color: var(--color-text-primary);
}

.legal-subheading {
  /* 0.85rem, matching .about-label ("Studio") exactly, same
     weight/case/tracking already shared - same label register as the
     footer, not a slightly larger one. */
  margin: 2rem 0 0.75rem;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--text-label);
  line-height: var(--lh-tight);
  color: var(--color-text-primary);
  text-transform: uppercase;
  letter-spacing: var(--ls-caps);
}

.legal-text {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 400;
  /* 0.85rem (27 July 2026, founder testing pass) - was 0.8rem, stepping to
     0.75rem at compact; now flat at every width, matching .about-text's
     desktop value exactly. No compact step: measured safe there (see
     below), unlike the interim 0.8rem value this replaces. */
  font-size: var(--text-body);
  line-height: 1.65;
  color: var(--color-text-primary);
  /* No max-width here, deliberately - see .about-text's own comment and
     canon/brand-system.md §BS-1 ("Text-level measure cap withdrawn",
     27 July 2026). A 53ch cap was added and withdrawn the same day; on
     this element the decisive regression was the visible jump in apparent
     column width when arriving here from the footer link, since
     .legal-content's --column-width exists precisely to match the
     footer's. The 24 July 2026 correction removed a different cap for a
     different reason (a layout-width fix for a since-resolved bug where
     .legal-content ran wider than --column-width); neither is a reason to
     add one back. */
}

.legal-link {
  color: var(--color-text-primary);
  border-bottom: 1px solid var(--color-text-primary);
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
  transition: color 150ms ease, border-color 150ms ease;
}

.legal-link:hover,
.legal-link:focus-visible {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.legal-link:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 4px;
}

@media (max-width: 640px) {
  body.static-page {
    /* padding-top stays 0 (inherited from the base rule) - no
       wordmark-clearing calc() lives in padding any more (canon/brand-
       system.md §BS-8a, founder-ratified 25 July 2026: .legal-content's own
       margin-top handles that instead), so there's nothing left to preserve
       here. Left/right step to the phone alignment margin (space-xl, 2rem),
       not the general compact side margin (space-lg) - §BS-8a's alignment
       rule, 25 July 2026, was 1.5rem: .legal-content below sits flush
       against this padding with margin-left: 0, so this value IS the
       alignment boundary for the utility pages, not a separate looser
       margin. */
    padding-left: 2rem;
    padding-right: 2rem;
    /* padding-bottom no longer overridden here (27 July 2026): the base
       rule above is 4rem now too, so this would just be restating the
       inherited value. */
  }

  .legal-content {
    /* Phone alignment rule (§BS-8a, 25 July 2026): fixes the 210px
       regression this page carried - previously no compact rule set width
       here at all, so .legal-content resolved the desktop --column-width
       formula (a ~210px column on phones) at every viewport. margin-left: 0
       plus --column-width-compact (shared with the image frame and footer
       text column above, so all three land on the same edge structurally)
       replaces the desktop rule's --legal-inset centering trick, which does
       not apply here - that trick still governs the desktop rule above. */
    width: var(--column-width-compact);
    margin-left: 0;
  }
}

/* ---------- Small screens ---------- */

/* Side-by-side needs real horizontal room; below this, fall back to a
   stacked layout (caption below the image) rather than squeezing both into
   the same row. .caption stays positioned off .image-frame's own box
   (consistent with desktop), just moved to sit below instead of beside,
   left-aligned to the image's own left edge rather than centered under it. */
@media (max-width: 900px) {
  .caption {
    top: 100%;
    bottom: auto;
    left: 0;
    margin-left: 0;
    margin-top: var(--space-lg);
    align-items: flex-start;
    text-align: left;
    max-width: 80vw;
    white-space: normal;
  }
}

/* First of two <=640px blocks. The second lives inside the closing-section
   viewport-fit region below, deliberately AFTER the max-height:750px block,
   because its rules must win over that block's on narrow-and-short viewports
   (see the note at .about-content/.cta there). Do not consolidate the two:
   hoisting those rules up here moves them ABOVE max-height:750px and silently
   inverts that precedence. The split is load-bearing, not drift. */
@media (max-width: 640px) {
  /* Compact type step (canon/brand-system.md §BS-1, 26 July 2026): the
     tagline reads outsized against the wordmark's fixed display-scale
     footprint at phone widths. .about-text/.cta-text/.cta-email/
     .project-name and .legal-text/.legal-line no longer step in FONT SIZE
     here (27 July 2026, founder testing pass): each was moved to a single
     flat size that already equals its old compact value (0.85rem and
     0.75rem respectively - see their own rules), so a compact size
     override here would just restate the inherited value. */
  .tagline-primary { font-size: var(--text-body-sm); }

  /* CTA-to-email rhythm gap still needs its own compact value (canon/
     brand-system.md §BS-2's "component constants") - unlike the sizes
     above, this was never about font-size stepping. The footer's own
     rhythm steps from space-xl (32px) to space-lg (24px) at this
     breakpoint regardless of type size (.site-footer's gap, .about-label's
     margin, both elsewhere), and .cta-text's margin has to track that same
     step with its own compensated value, since the inline .cta-email link
     always carries a bit of extra leading the plain margin doesn't
     account for. 20px is the measured value for 24px at this element's
     (now constant) 0.85rem font - confirmed in-browser. */
  .cta-text { margin-bottom: 20px; }

  .project {
    padding: 3rem 1.5rem;
  }

  .image-frame,
  .image-layer.final {
    /* Phone alignment rule (canon/brand-system.md §BS-8a, §BS-3, 25 July
       2026): shares --column-width-compact with the footer text column
       (below) rather than restating calc(100vw - 4rem) separately, so the
       two cannot drift apart - the shared-edge guarantee becomes structural
       instead of two values kept equal by hand. Was 88vw. The height cap
       still binds first on shorter phones, where the image sits further
       right - permitted, since the rule forbids only overhanging to the
       left. */
    max-width: var(--column-width-compact);
    max-height: var(--frame-height-max-compact);
  }
}

/* ---------- Closing section: viewport-height fit (canon §BS-8a, 25 July 2026) ---------- */

/* Short viewports, any width: band's paper portion steps 20% -> 10%, and the footer
   column takes its measure floor. The floor is not optional -- at 1280x620 the stock
   formula yields 281px, which is 34.7 rendered characters per line, below §BS-1's
   55-character floor, and the band step alone still does not fit (-66.1px). */
@media (max-height: 750px) {
  .about-content,
  .cta { width: var(--column-width-short); }
  /* Tracks whichever column formula is actually governing the text above, so the
     fallback block's utility line (below) can share its width without duplicating
     the min()/max() logic and risking the two drifting apart. */
  .site-footer { --footer-column-width: var(--column-width-short); }

  .wordmark.wordmark--footer { top: calc(10vh + 2rem); }
  /* Carries the same wordmark-to-header gap as the base rule above (27
     July 2026), but at --space-lg (24px) rather than --space-xl: this
     block's own footer rhythm (.site-footer gap, .about-label's
     margin-bottom, both below) already steps down to --space-lg at this
     height, so the wordmark gap matches it rather than sitting 8px
     larger than the rhythm it's meant to equal. Checked against the
     tightest short-height budget: 72.4px of slack at 1280x620 and 54.4px
     at 1024x600, both well clear of the 25px floor the height thresholds
     are calibrated to (measured 27 July 2026 to the .cta-email bottom
     edge, canon/brand-system.md §BS-8a's convention; 69.0px and 51.0px
     respectively to the .cta block bottom, this file's convention).
     This comment previously pointed at "the ledger entry for the verified
     figure" - no such measurement was ever taken, and no such figure
     existed in the ledger. These are it. */
  .about-content { margin-top: calc(10vh + 2rem + clamp(4rem, 12vw, 7rem) + var(--space-lg)); }

  .site-footer::before {
    background: linear-gradient(
      to bottom,
      var(--color-bg) 0%,
      var(--color-bg) 10%,
      var(--color-bg-invert) 10%,
      var(--color-bg-invert) 100%
    );
  }

  /* The utility pages take the footer's short-height column too (27 July
     2026, founder-ratified). They do NOT take it for the reason the floor
     exists - these pages scroll natively and have no fit budget to protect -
     they take it for continuity, so the column stays pixel-identical to the
     footer's on click-through. Re-points --legal-column so the centring inset
     tracks it automatically. Verified identical at every viewport in §BS-8a's
     viewport-fit matrix. */
  body.static-page .legal-content { --legal-column: var(--column-width-short); }

  body.static-page .wordmark { top: calc(10vh + 2rem); }
  /* Same gap addition as .about-content above (27 July 2026), now at
     --space-lg to match (see .legal-heading/.legal-subheading below) - no
     fit budget concern here, these pages fall back to normal document
     scroll rather than sharing the footer's fixed-viewport constraint. */
  body.static-page .legal-content { margin-top: calc(10vh + 2rem + clamp(4rem, 12vw, 7rem) + var(--space-lg)); }

  .site-footer { gap: var(--space-lg); }
  .about-label { margin-bottom: var(--space-lg); }
  /* Utility-page rhythm now mirrors the footer's own step at this height
     (27 July 2026, founder request): heading-to-paragraph and
     paragraph-to-subheading both drop from --space-xl to --space-lg,
     alongside .about-label above, rather than the two surfaces stepping
     independently. */
  .legal-heading { margin-bottom: var(--space-lg); }
  .legal-subheading { margin-top: var(--space-lg); }
}

/* Phones: no paper portion at all, so the paper->dark transition falls on the snap
   boundary itself, and the wordmark stays where it sits on every other section. That
   collapses the content anchor from 20vh+2rem+wordmark to 2rem+wordmark: 229px -> 96px
   at 375x667. The column shares --column-width-compact with the image frame (phone
   alignment rule, canon/brand-system.md §BS-8a, 25 July 2026): calc(100vw - 4rem),
   landing its left edge on the wordmark's own 2rem offset directly, with no separate
   floor -- supersedes the 280px shared-edge floor ratified earlier the same day. */
/* Second of two <=640px blocks (the first carries the compact type scale and
   image frame, well above). Its position here - after max-height:750px, not
   merged with the first - is deliberate and load-bearing: see the note there. */
@media (max-width: 640px) {
  .site-footer::before { background: var(--color-bg-invert); }

  .wordmark.wordmark--footer { top: 2rem; }
  /* Re-coupled to the wordmark-height clamp (27 July 2026, superseding the
     26 July flat 20vh exception below this comment's old position): the
     flat-vh anchor produced a wordmark-to-header gap that changed with
     every phone height (29px at 360x625, up to 66px at 375x812) against a
     24px internal footer rhythm (label-to-text, text-to-CTA, both
     --space-lg at this breakpoint - canon/brand-system.md §BS-2's
     viewport-fit amendment) that stayed constant regardless of device.
     Same mechanic as the desktop formula above (§BS-8a, 27 July): wordmark
     top + wordmark height (clamp(4rem, 12vw, 7rem), tracking the same
     font-size the wordmark itself uses) + the rhythm constant, so the gap
     now reads as exactly --space-lg on every phone, matching its
     neighbours instead of floating. Measured safer against the tight,
     no-scroll footer budget than the value it replaces: 109.1px slack at
     360x625 (the envelope corner), versus 104.1px before. */
  .about-content { margin-top: calc(2rem + clamp(4rem, 12vw, 7rem) + var(--space-lg)); }

  body.static-page .wordmark { top: 2rem; }
  /* Same re-coupling as .about-content above (27 July 2026). Originally
     kept at --space-xl here on the reasoning that the utility pages had no
     stepped rhythm of their own to match - superseded the same day by a
     founder request that the two surfaces mirror each other in spacing as
     well as type, at which point that reasoning no longer applied. Now
     --space-lg, matching .legal-heading/.legal-subheading's own new step
     below. Superseded the 26 July flat 20vh exception, whose own rationale
     (a fixed proportion of the viewport, decoupled from the wordmark's
     height) is what produced the device-dependent gap this fixes. */
  body.static-page .legal-content { margin-top: calc(2rem + clamp(4rem, 12vw, 7rem) + var(--space-lg)); }

  .about-content,
  .cta { width: var(--column-width-compact); }
  /* Wins over the short-height block's value above wherever both apply (a narrow
     AND short viewport), matching source order: the phone rule already overrides
     .about-content's width the same way. */
  .site-footer { --footer-column-width: var(--column-width-compact); }

  .footer-utility {
    /* Phone alignment rule (canon/brand-system.md §BS-8a, 25 July 2026): left
       edge aligns to the wordmark's own 2rem offset, not the compact side
       margin - was var(--space-lg). bottom stays space-lg; only the left
       edge is governed by the alignment rule. */
    left: var(--space-xl);
    bottom: var(--space-lg);
    max-width: calc(100% - 2 * var(--space-xl));
  }

  .site-footer { gap: var(--space-lg); }
  .about-label { margin-bottom: var(--space-lg); }
  /* Utility-page rhythm now mirrors the footer's own step at this width
     (27 July 2026, founder request) - same reasoning as the max-height:750px
     block above. */
  .legal-heading { margin-bottom: var(--space-lg); }
  .legal-subheading { margin-top: var(--space-lg); }
}

/* At or below 576px height at any width, or at or below 624px height on viewports
   640px or narrower, the section cannot hold its content with acceptable margin at
   any band value (canon/brand-system.md §BS-8a, thresholds corrected 25 July 2026 -
   supersedes the earlier single "600px height or 340px width" rule, whose width
   clause did no work and whose single height figure demoted wide-but-short
   viewports that measurably fit). Abandon the snap mechanism for native document
   scroll -- the same pattern the <noscript> block in index.html already carries.
   script.js gates its handlers on this identical query. Crossing this boundary
   mid-session requires a reload; that is accepted, not a defect. */
@media (max-height: 576px), (max-width: 640px) and (max-height: 624px) {
  html, body { overflow: auto; height: auto; }
  .stack-section { height: auto; min-height: 100vh; overflow: visible; }
  .scroll-track { transform: none !important; }

  /* Not fixed: on the main page a fixed off-black wordmark would sit over the
     off-black band and disappear. Deliberately unscoped, so it also applies to
     the two utility pages - correct there for a different reason: the clamp()
     floors at 4rem, so fixing it would hold ~96px of a 576px viewport with
     legal copy scrolling under it. Both reasons, and the deliberate break in
     the wordmark-continuity rule this creates, are ratified in
     canon/brand-system.md §BS-8a ("Fallback-mode values", 26 July 2026). */
  .wordmark { position: absolute; }

  /* Width tracks --footer-column-width (set above by whichever of the compact/
     short-height rules currently governs .about-content/.cta), so the legal line
     shares the text column's edge instead of carrying an independent formula that
     can silently drift from it. Every fallback viewport is inside max-height:750px,
     so the short-height rule always sets a value at minimum; the phone rule's value
     wins on top of it whenever the viewport is also <=640px wide. */
  .footer-utility {
    /* Flow placement, not the bottom-left absolute placement every other mode
       uses: absolute would remove this block from the section's own height,
       leaving a natively-scrolling section nothing to scroll. Ratified as the
       fallback's own rule (canon/brand-system.md §BS-8a, "Fallback-mode
       values", 26 July 2026) - was previously a flagged canon gap. */
    position: static;
    max-width: none;
    width: var(--footer-column-width);
    text-align: left;
    margin-top: var(--space-xl);
  }
}
