/* ------------------------------------------------------------------
   FLUO — Scroll Cinema (motion study)
   Standalone: no dependencies on the Next.js app.

   Viewport model (matters on phones, invisible on desktop):
   - The STAGE and everything that paints a background (video, light
     panels, the contact section) fill the LARGE viewport (100lvh): the
     full screen, under the status bar and under the browser toolbar.
     The stage clips.
   - Everything the user reads or taps (headlines, buttons, paragraphs)
     is positioned inside the SMALL viewport (100svh) with the safe-area
     insets added, so a browser bar never covers it.
   - Both sizes are fixed for a given viewport width. Nothing here reacts
     to a toolbar collapsing or expanding.

   Desktop scale (768px and wider):
   - The design is drawn at 1920px wide. script.js sets --u to
     min(viewport width, 2560) / 1920 px, updated only when the width
     changes. Every desktop size below is "reference px × var(--u)",
     with max() floors for anything that must stay legible.
   - Above 2560px the content sits in a centred 2560px-wide .grid
     wrapper; backgrounds (video, panels, contact) stay edge to edge.
   - Grid: 6 columns, 184px side margins, 40px gutters, 225.33px
     columns. A 3-column block is 756px wide.
   ------------------------------------------------------------------ */

/* Manrope, self-hosted (docs/port-plan.md §7, phase 4): the variable woff2
   of the Google Fonts release (v20), the same two subset files Google served
   this page before, so nothing about the shaping or the metrics below
   changed. Weights 200–800 in one file; the pages use 400 / 700 / 800.
   font-display: swap, as the old Google link asked for. The head preloads
   the latin file (src/site/html.ts fontsHead). */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url(/fonts/manrope-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url(/fonts/manrope-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* The hero wall before any script runs: WALL.color in src/lib/hero/config.ts.
     hero/pick.js overrides it on <html> with the chosen version's own wall
     colour before the first paint (view.html head); nothing else may name
     the wall grey. */
  --hero-wall: #cfcecc;

  --light: #F7F6F5;
  --light-grey: #E5E3E0;
  --grey: #A9A6A2;
  --dark-grey: #5C5B58;
  --dark: #22211F;
  --text: #52504C;           /* paragraphs, tab words, section 8 text */
  --section-light: #F7F6F4;  /* light panels */
  --section-warm: #F1EEE9;   /* section 2, warmer */

  --font: 'Manrope', system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
  --gutter: clamp(20px, 4vw, 56px);   /* fallback for anything not scaled. Mobile overrides it (40 @393, below :root); desktop uses --gx */

  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);

  /* Nav shrink: one duration for all three layers, so the copies stay on
     top of each other for the whole transition. */
  --nav-ms: 400ms;

  /* Distance from the top of a line box (line-height: 1) down to the top
     of a capital letter, as a fraction of the font size. Manrope: ascent
     1.066em, descent 0.300em, cap height 0.720em, so the baseline sits
     (1 + 1.066 - 0.300) / 2 = 0.883em down and the cap top 0.163em down.
     Lets the nav links be positioned by their cap top, which is how the
     design measures them. The font sets USE_TYPO_METRICS and its hhea,
     typo and win metrics agree, so this is the same on every platform. */
  --cap: 0.163;
  --cap-h: 0.72;   /* Manrope's cap height, same units */

  /* Bottom edge of the COMPACT nav. script.js reads it as a number
     through #probe-nav-bottom, so the nav's sizes are only written once. */
  --nav-bottom-compact: calc(var(--safe-top) + 8px + 24px);

  /* How far the 3D hero's wall continues ABOVE its box (see .home-hero):
     what shows above the hero when the page rubber-bands at the top and
     every fixed layer moves down with it. The bottom edge needs none: the
     box is the whole large viewport, and once it lifts its bottom edge is
     the wipe. The fallback is for browsers without lvh. */
  --home-hero-overscan: 20lvh;

  /* The same for the contact section (fixed, the whole large viewport): a
     bounce past the END of the page shows this much more dark, never a gap.
     Spread on every side; only the bottom one ever shows. */
  --contact-overscan: 20vh;
  --contact-overscan: 20lvh;

  /* Desktop scale unit. script.js overwrites this with a px value on
     every width change; the calc below only covers the first paint. */
  --u: calc(min(100vw, 2560px) / 1920);
  --gx: calc(184 * var(--u));      /* grid start = first column's left edge */
  --gut: calc(40 * var(--u));      /* gutter */
  --col: calc(225.33 * var(--u));  /* column */
  --w3: calc(756 * var(--u));      /* 3 columns + 2 gutters */

  --ease: cubic-bezier(0.2, 0, 0, 1);
  /* Reduced motion (spec §10.6): the one length of every cross-fade, with
     the plain `ease` curve. identity.js holds the same value for the
     scripts (REDUCED_FADE_MS). */
  --rm-fade: 200ms;

  /* ---- The identity block: tokens (its rules are further down) ---- */
  /* The identity block (spec §4), design px @1920. Written once, used by
     the homepage's hero, the list, the detail and the flight.
     80 / 80 / -0.06em / 800: the mock's cap height is 57.6px = 0.72 × 80,
     and at that size and tracking all six titles match list-desktop.png's
     widths to the pixel. The gap under the logo is --id-gap, declared on
     the identity itself (below) because a logo may override it.
     Mobile values: the block right after :root. */
  --id-size: 80;      /* title font-size */
  --id-pitch: 80;     /* title line pitch */
  --id-gap-default: 39;  /* logo bottom → title cap top, in desktop design px like a logo's own gap */
  --id-gap-k: 1;         /* × either of them: 1 on desktop; mobile scales both (39 → 12) */
  --logo-k: 1;           /* × a logo's drawn height, which is stored in desktop design px */
  --id-cta-gap: 38;   /* title's last baseline → `view project` cap top */
  --id-cta-size: 17;
  --id-track: -0.06em; /* title letter-spacing */

  /* Manrope, from style.css's --cap note: ascent 1.066, descent 0.300,
     cap 0.720. In a line box of height P (in em) the cap top sits
     P/2 - 0.337em below the box top, and the baseline P/2 - 0.383em
     above the box bottom. These two let the title and the CTA be placed
     by cap top and baseline, which is how the design measures them. */
  --cap-drop: 0.337;
  --base-rise: 0.383;

  /* The identity's own scale unit. Desktop: --u. Mobile: 1px on a 393pt
     phone, straight from the viewport width, because script.js only writes
     the DESKTOP --u (projects.js writes the same value into --u on mobile,
     so the two pages agree). */
  --iu: var(--u);
}

/* The identity on a phone, re-derived off list-mobile-top / detail-mobile-top
   (see projects.css's mobile block for the measurements): 28/28, logos at
   0.655 of their desktop height, logo bottom → cap top 39 → 12. */
@media (max-width: 767px) {
  :root {
    /* THE mobile side margin, for all content on every page: 40 at a 393pt
       phone, scaling with the screen like the rest of the mobile design. It
       is written here ONCE. --gutter carries it to the hero, the nav, the
       panels, the client logos, the service tabs and the contact section
       (they all read --gutter already); projects.css reads --gutter too, and
       identity.js reads --mx for the detail identity's slot. */
    --mx: 40;
    --gutter: calc(var(--mx) * var(--iu));

    --iu: calc(100vw / 393);
    --id-size: 28;
    --id-pitch: 28;
    --id-gap-k: calc(12 / 39);   /* a logo's own gap scales with it: Levi's 48 → 14.8 (no mobile frame shows it) */
    --logo-k: 0.655;
  }
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--dark);
  color: var(--light);
  font-family: var(--font);
  font-weight: 400;
  line-height: 1.5;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }

/* The <symbol> source for the FLUO mark: never painted itself. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ---- Viewport probes ------------------------------------------------
   Invisible, zero-width, measured once per layout by script.js.
   100lvh / 100svh fall back to 100vh where unsupported (= desktop). */

.probe {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  visibility: hidden;
  pointer-events: none;
}
.probe--large { height: 100vh; height: 100lvh; }
.probe--small { height: 100vh; height: 100svh; }
.probe--safe-top { height: var(--safe-top); }
.probe--safe-bottom { height: var(--safe-bottom); }
.probe--nav-bottom { height: var(--nav-bottom-compact); }

/* ---- Content wrapper ------------------------------------------------
   Every readable block lives in a .grid: full width up to 2560px, then
   centred. Side padding is the grid margin on desktop, the gutter on
   mobile. Backgrounds never use it. */

.grid {
  width: 100%;
  max-width: 2560px;
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* ---- Nav (fixed; three stacked copies) ------------------------------
   The header itself takes no space: the three layers below are the boxes.
   A layer is never transformed and always starts at the viewport's top
   left corner, so the clip-path script.js writes on the two paint layers
   is in plain viewport px and stays correct while the shrink transition
   is still running on their children. */

.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 0;
  z-index: 40;
  pointer-events: none;
}

.nav__layer {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  height: 100lvh;
}
.nav__layer--light { color: var(--light); }
.nav__layer--dark  { color: var(--dark); }

/* Paints nothing: it carries the real links, their hit areas and focus.
   Its text is transparent, so the site's currentColor focus ring (below,
   "Focus") would be invisible here: --nav-focus stands in for currentColor,
   written by script.js from the band under the link (dark over a light
   panel, light everywhere else). The projects pages' flat nav gives this
   layer a real colour and needs no such stand-in. */
.nav__layer--live { color: transparent; }
.nav__layer--live a {
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
}
.nav__layer--live a:focus-visible { outline-color: var(--nav-focus, currentColor); }

/* Masked by the contact section (script.js, .nav--masked): from the moment
   the last light panel starts to leave, what is under its edge is the dark
   contact, and there the nav is not painted at all: the light copy is
   clipped away along with the panel, so the panel's edge cuts the nav
   exactly as it cuts the panel's content. The links stay in the tab order:
   a KEYBOARD focus brings the light copy back over the contact
   (.nav--revealed, the site's cross-fade), so a focus is never invisible,
   and it goes again when the focus leaves.
   Where the links take the pointer is not decided here: script.js clips
   the live layer to exactly the band where a copy is painted
   (applyNavClip), so wherever the menu is visible it is clickable and
   wherever it is masked it blocks nothing. (An earlier pointer-events:
   none on the links while masked also killed the clicks on the dark copy
   painted over the last panel as it comes back down.) */
.nav--masked .nav__layer--light { opacity: 0; transition: opacity var(--rm-fade) ease; }
.nav--masked.nav--revealed .nav__layer--light { opacity: 1; }

.nav__logo {
  position: absolute;
  left: var(--gutter);
  top: calc(var(--safe-top) + 12px);
  display: block;
  transition: transform var(--nav-ms) var(--ease);
}
/* The mark sits UNDER the hero panel (the hero is the mark). script.js keeps
   .nav--no-mark while any of the panel is on screen: the dark copy, the one
   painted inside the hero's band, shows no mark, so nothing is drawn over the
   panel; the light copy is untouched and its band clip already paints it only
   below the panel's bottom edge, so the mark is uncovered by that edge as the
   panel leaves, with no fade of its own. The live layer's link goes with the
   dark copy (visibility), so it is neither clickable nor in the tab order
   while it is under the panel. */
.nav--no-mark .nav__layer--dark .nav__logo,
.nav--no-mark .nav__layer--live .nav__logo { visibility: hidden; }
.nav__mark {
  display: block;
  width: auto;
  height: 32px;
  transition: height var(--nav-ms) var(--ease);
}
.nav--compact .nav__logo { transform: translate(0, -4px); }
.nav--compact .nav__mark { height: 24px; }

/* On a phone the links share the mark's box, and their CAPITALS are centred
   on the mark's centre line (see "the phone nav's centre line" at the end of
   this file). On desktop they are placed by their cap top (768px block). */
.nav__links {
  position: absolute;
  right: var(--gutter);
  top: calc(var(--safe-top) + 12px);
  height: 32px;
  display: flex;
  align-items: center;
  gap: 24px;
  /* One rule for all three layers, so the copies stay identical. */
  font-weight: 700;
  font-size: 17px;
  line-height: 1;
  letter-spacing: 0.01em;
  transition: transform var(--nav-ms) var(--ease),
              height var(--nav-ms) var(--ease),
              font-size var(--nav-ms) var(--ease),
              gap var(--nav-ms) var(--ease),
              letter-spacing var(--nav-ms) var(--ease);
}
/* The site's ONE compact phone nav, as on the projects frames: 12px at
   -0.05em, 32 apart. The homepage reaches it past NAV_SHRINK_AT, the
   projects pages are always in it. */
.nav--compact .nav__links {
  transform: translate(0, -4px);
  height: 24px;
  gap: 32px;
  font-size: 12px;
  letter-spacing: -0.05em;
}

.nav__link { position: relative; display: block; }

/* 44px tap target, centred on the text. Paints nothing, and only the live
   layer has one, so the three copies stay identical. */
.nav__layer--live .nav__link::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}

/* ---- The flat nav: one self-painting layer ---------------------------
   The projects pages' nav (.nav--flat): always compact, one background
   colour under it, so the live layer paints itself in --dark-grey. The
   homepage draws this same nav for its hand-over to those pages. */

.nav--flat .nav__layer--live { color: var(--dark-grey); --nav-track: -0.05em; }
.nav--flat .nav__links, .nav--flat .nav__label { letter-spacing: var(--nav-track); }

/* The projects pages paint this nav by what is under it (projects.js, "the
   nav, coloured by what is under it"): .nav--paint copies, dark under the
   content and light inside every media frame, and the real one a ghost. */
.nav--flat.nav--ghost .nav__layer--live,
.nav--flat.nav--ghost .nav__layer--live .nav__pill { color: transparent; background: none; }
.nav--paint { z-index: 0; }
/* The ghost nav's keyboard focus ring, drawn by its painted copies
   (projects.js, mirrorNavFocus): the site's ring, in each copy's colour. */
.nav--paint[data-focus="nav__logo"] .nav__logo,
.nav--paint[data-focus="nav__back"] .nav__back,
.nav--paint[data-focus="nav__projects"] .nav__projects,
.nav--paint[data-focus="nav__contact"] .nav__contact { outline: 1.5px solid currentColor; outline-offset: 4px; }
.nav--flat.nav--paint-light .nav__layer--live { color: var(--light); }

/* List only, desktop only. It is one of the list's pieces: projects.js
   fades it out and in with the content. */
.nav__label { display: none; }

/* The back link: both labels share one grid cell, right-aligned, and the
   view picks which one shows. They cross-fade when the views swap. */
.nav__back { display: grid; justify-items: end; align-items: center; }
.nav__back-label {
  grid-area: 1 / 1;
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  transition: opacity 300ms ease;
}
.nav__arrow { display: block; flex: none; width: 1em; height: 1em; margin-right: 0.35em; overflow: visible; }
/* data-view is on <body> on the projects pages, and on the hand-over nav
   itself on the homepage. */
[data-view="list"] .nav__back-label--projects,
[data-view="detail"] .nav__back-label--home { opacity: 0; }

/* The back link and `Projects` each sit in a SLOT that can close: its one
   grid column goes from 1fr to 0fr and it gives its flex gap back, so the
   neighbours slide instead of jumping, and the link fades with it. Which
   slots are open (spec §9, "detail nav depends on how you arrived"):
     list, and a detail reached from the list   back
     detail from the homepage, desktop          back (`Back to home`) + Projects
     detail from the homepage, phone            Projects (the homepage's own items)
   data-arrival sits next to data-view: on <html> on the projects pages, on the
   hand-over nav itself on the homepage. */
.nav__slot {
  display: grid;
  grid-template-columns: 1fr;
  transition: grid-template-columns 300ms var(--ease), margin-right 300ms var(--ease),
              opacity 300ms ease, visibility 0s;
}
.nav__slot > * { min-width: 0; }
.nav__slot--projects {   /* closed unless the rule below opens it */
  grid-template-columns: 0fr;
  margin-right: calc(-1 * var(--slot-gap, 0px));
  opacity: 0;
  visibility: hidden;
  transition: grid-template-columns 300ms var(--ease), margin-right 300ms var(--ease),
              opacity 300ms ease, visibility 0s 300ms;
}
[data-view="detail"][data-arrival="home"] .nav__slot--projects {
  grid-template-columns: 1fr;
  margin-right: 0;
  opacity: 1;
  visibility: visible;
  transition: grid-template-columns 300ms var(--ease), margin-right 300ms var(--ease),
              opacity 300ms ease, visibility 0s;
}
/* `Back to home` on a detail: the arrival outranks the view's label. */
[data-view="detail"][data-arrival="home"] .nav__back-label--home { opacity: 1; }
[data-view="detail"][data-arrival="home"] .nav__back-label--projects { opacity: 0; }
/* A detail opened directly (no homepage and no list behind it): `← Projects`,
   no "Back to" wording. Shorter than the other labels in the same cell, so
   the cell's width, and with it everything to its left, does not change. */
.nav__back-label--direct { opacity: 0; }
[data-view="detail"][data-arrival="direct"] .nav__back-label--direct { opacity: 1; }
[data-view="detail"][data-arrival="direct"] .nav__back-label--projects { opacity: 0; }


/* ---- The hand-over from the homepage to the projects pages (handoff.js) --
   Three fixed layers that exist only while the homepage is leaving, all
   ABOVE the stage and the contact section (z 30) and removed again if the
   page comes back from the bfcache:
   - the cover: the projects pages' --light, fading in. The stage itself is
     never faded: body is --dark, so that would pass through a dark flash.
   - the hand-over nav: the projects pages' exact nav, cross-fading with the
     cinema's (z 40).
   - the travelling identity, on top of the cover.
   Small or background-only fixed layers: AGENTS.md rule 5. The html
   background goes to --light with them, so Safari's bands match (rule 4). */
.handoff-cover {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100vh;
  height: 100lvh;
  z-index: 35;
  background: var(--light);
  opacity: 0;
  pointer-events: none;
}

/* The Contact link's fade-jump (contact.js fadeJump, used by script.js): a
   full-viewport cover in the colour of what the jump lands on, the last
   light panel or, under reduced motion, the contact section itself. In,
   jump, out. Above the stage and the hero, under the nav, like the
   hand-over's cover. */
.jump-cover {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100vh;
  height: 100lvh;
  z-index: 35;
  opacity: 0;
  pointer-events: none;
}
.handoff-identity {
  position: fixed;
  z-index: 37;
  pointer-events: none;
}
.handoff-identity .identity { color: #fff; }   /* it leaves the video white and lands dark-grey */
/* The travelling identity's logo as inline SVG (handoff.js): the same box as
   the masked one, filled with currentColor, but with nothing to decode. */
.identity__logo--inline { background: none; -webkit-mask: none; mask: none; }
.identity__logo--inline svg { display: block; width: 100%; height: 100%; overflow: visible; }
.nav.handoff-nav { z-index: 41; opacity: 0; }

/* Returning (view.html's head script sets .is-returning before the first
   paint): the same --light cover, as a pseudo-element so it needs no script
   to exist, and the html background light under Safari's bands. handoff.js
   adds .is-returning-out to fade both into the film over 300ms, then removes
   the classes. */
html.is-returning { background-color: var(--light); transition: background-color 300ms ease-out; }
html.is-returning body::before {
  content: '';
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100vh;
  height: 100lvh;
  z-index: 35;
  background: var(--light);
  opacity: 1;
  transition: opacity 300ms ease-out;
  pointer-events: none;
}
html.is-returning .nav { opacity: 0; transition: opacity 300ms ease-out; }
/* Failsafe with no script involved: if handoff.js never gets to uncover, the
   cover and the nav still resolve by themselves after 7s. */
@keyframes returning-failsafe-out { to { opacity: 0; } }
@keyframes returning-failsafe-in  { to { opacity: 1; } }
html.is-returning body::before { animation: returning-failsafe-out 300ms ease-out 7s forwards; }
html.is-returning .nav { animation: returning-failsafe-in 300ms ease-out 7s forwards; }
html.is-returning.is-returning-out { background-color: transparent; }
html.is-returning.is-returning-out body::before { opacity: 0; }
html.is-returning.is-returning-out .nav { opacity: 1; }

/* ---- The 3D hero: the first section, a held light panel above the stage --
   One fixed box the size of the LARGE viewport, exactly like a .panel in the
   stage (z-index 1 over the stage's 0; the nav stays at 40, the hand-over
   layers at 35+). It is the light panel that is already covering the screen
   when the page opens: script.js moves the word inside it 1:1 with scroll
   (hero.js, setContentOffset) and, once the word's top edge meets the compact
   nav, lifts the whole box off at PANEL_SPEED_MULT, the same exit as every
   other light section. The word only exists inside this box, so the box's
   edge cuts it off as it leaves (overflow: hidden, set by mount.ts too).
   Gone above the viewport it is display: none, so the hero's own observers
   see it as off-screen (no rendering, no model fetch on a return).

   Its background is --hero-wall: the page's default grey from :root
   (WALL.color in src/lib/hero/config.ts) until hero/pick.js, in the head,
   sets the chosen version's own wall colour on <html> before the first
   paint, so the first paint already matches what hero.js sets on mount and
   never flashes another grey. The word's width/position mirror config.ts
   (FIT 52% / 86%). The cue inside it is styled by hero.js from config.ts;
   the margin below lifts it out of the strip a phone's expanded toolbar
   covers (the box is lvh, it is placed by svh, like every other control in
   the cinema).

   The wall reads as taller than the screen in both directions. Below, the
   box is the whole large viewport. Above, it continues --home-hero-overscan
   as a hard-edged box-shadow (a pseudo-element would be clipped by the
   overflow), painted in --hero-wall, which mount.ts sets to the mounted
   version's wall colour, so the two never step: a rubber-band bounce at the
   top of the page moves every fixed layer down together and shows this,
   never the video underneath. And while the hero covers, the html
   background is the same colour (html.hero-covers, below the nav rules):
   Safari tints its bars' bands with it (AGENTS.md rule 4), and it is what a
   bounce shows past the overscan. */

.home-hero {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  height: 100lvh;
  z-index: 1;
  overflow: hidden;
  background: var(--hero-wall);
  box-shadow: 0 calc(-1 * var(--home-hero-overscan, 20vh)) 0 0 var(--hero-wall);
  color: #52504C;
  transform: translate3d(0, 0, 0);
  will-change: transform;
}
/* script.js keeps this on <html> while any of the hero panel is on screen
   (view.html also sets --hero-wall on <html>, from the hero module). Not
   while returning (above): a return opens under --light. The hand-over's
   leaving animation writes the html background itself and out-ranks this. */
html.hero-covers:not(.is-returning) { background-color: var(--hero-wall); }
/* ...and while the contact section is uncovered (script.js, the same signal
   that masks the nav): Safari's bands and a bounce past the end are dark. */
html.contact-covers:not(.hero-covers):not(.is-returning) { background-color: var(--dark); }
.home-hero.is-hidden { display: none; }
.home-hero > .hero-cue { margin-bottom: calc(100lvh - 100svh); }

/* Reduced motion (script.js adds .home-hero--reduced): the word does not
   travel and the box does not lift; at the point where it would have started
   to leave it cross-fades out instead, opacity only, --rm-fade. */
.home-hero--reduced { transition: opacity var(--rm-fade) ease, visibility 0s; }
.home-hero--reduced.is-faded { opacity: 0; visibility: hidden; transition: opacity var(--rm-fade) ease, visibility 0s var(--rm-fade); }
.home-hero__fallback {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 52%;
  height: auto;
  max-width: none;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
@media (pointer: coarse), (max-width: 767px) { .home-hero__fallback { width: 86%; } }

.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ---- Stage: the one clipping box ----------------------------------- */

.stage {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  height: 100lvh;
  overflow: hidden;
  z-index: 0;
  /* No background of its own: once the last panel has left, the contact
     section (fixed, before it in the DOM at the same z-index, so under it)
     shows through. And no hit area of its own either, so the uncovered
     contact's email can be clicked through it; the panels (links, tabs)
     take the pointer back themselves. */
  transform: translate3d(0, 0, 0);
  pointer-events: none;
}

/* ---- Video beats: pinned video --------------------------------------
   One .video + one .hero per video beat; script.js toggles .is-hidden
   so a pair is painted from the moment it starts scrubbing (behind the
   previous panel) until the next panel fully covers it. */

.video {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--dark);
  pointer-events: none;
}

/* ---- Video beats: text block over the video --------------------------
   The .hero box is the SMALL viewport and only provides the coordinate
   system. The group inside it (the identity block) is positioned by
   script.js in viewport coordinates: top is 0 here, so its transform IS
   its position. Nothing clips it, so it can travel past either edge. */

.hero {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  height: 100svh;
  z-index: 10;
  pointer-events: none;
}

.video.is-hidden,
.hero.is-hidden,
.panel.is-hidden { visibility: hidden; }

.hero__inner {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  pointer-events: auto;
  will-change: transform;
}

/* The hero IS the identity block (client logo, title, `view project`),
   built by identity.js from projects-data.js with the .identity rules below:
   the same metrics as the projects pages, white on the video. */
.hero .identity { color: #fff; }

/* ---- Identity: logo + title (+ view project) -------------------------
   ONE component, the same markup and rules wherever it appears. Its box
   starts at the logo's top edge and its left edge is the grid's. */

/* --logo-gap is set inline only for a logo with its own gap (Levi's 48, in
   desktop design px). Declared on the identity (and, in projects.css, on the detail head), not on :root: a
   custom property is resolved where it is declared. */
.identity { --id-gap: calc(var(--logo-gap, var(--id-gap-default)) * var(--id-gap-k)); }

.identity {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  color: var(--dark-grey);   /* the cinema's hero overrides this with white */
}

/* Painted through a mask, so the logo takes currentColor whatever fills
   the file uses. --lw / --lh / --ls come from LOGOS in projects-data.js. */
.identity__logo {
  display: block;
  height: calc(var(--ls) * var(--logo-k) * var(--iu));
  width: calc(var(--ls) * var(--logo-k) * var(--lw) / var(--lh) * var(--iu));
  background: currentColor;
  -webkit-mask: var(--logo) no-repeat center / 100% 100%;
          mask: var(--logo) no-repeat center / 100% 100%;
}

.identity__title {
  margin: calc((var(--id-gap) - (var(--id-pitch) / 2 - var(--cap-drop) * var(--id-size))) * var(--iu)) 0 0;
  font-weight: 800;
  font-size: calc(var(--id-size) * var(--iu));
  line-height: calc(var(--id-pitch) / var(--id-size));
  letter-spacing: var(--id-track);
}
.identity__line { display: block; white-space: nowrap; }

.identity__cta {
  margin-top: calc((var(--id-cta-gap) - (var(--id-pitch) / 2 - var(--base-rise) * var(--id-size)) - var(--cap) * var(--id-cta-size)) * var(--iu));
  font-weight: 700;
  font-size: calc(var(--id-cta-size) * var(--iu));
  line-height: 1;
  letter-spacing: var(--id-track);
  text-decoration: underline;
  text-decoration-thickness: max(1px, calc(2 * var(--iu)));
  text-underline-offset: 0.04em;
  -webkit-tap-highlight-color: transparent;
}

/* ---- Focus ------------------------------------------------------------
   One ring for the whole site, in the colour of the text it belongs to, so
   it is light over media and dark over a light page: 1.5px, 4px out, and
   only for a focus the browser judges keyboard-driven. Nothing else
   defines an outline; a pointer focus never shows one. */
:focus-visible { outline: 1.5px solid currentColor; outline-offset: 4px; }
:focus:not(:focus-visible) { outline: none; }

/* A focus that followed a POINTER (a click, a tap, or the browser putting
   focus back on the clicked link when the user returns with Back): focused,
   for the keyboard and for screen readers, but with no ring. identity.js
   sets and clears this class. Engines disagree on whether such a focus is
   :focus-visible (WebKit says yes), so the page decides: a ring only ever
   follows the keyboard. */
.no-ring:focus, .no-ring:focus-visible { outline: none !important; }

/* ---- Buttons ------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 14px 26px;
  border: 1px solid currentColor;
  font-weight: 800;
  font-size: 12px;
  letter-spacing: 0.16em;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.btn--outline { color: #fff; border-color: #fff; }
.btn--filled  { color: #fff; background: #5A5956; border-color: #5A5956; }
.btn--light   { color: var(--dark); background: var(--light); border-color: var(--light); }

/* ---- Panel beats: light panel ---------------------------------------- */

.panel {
  position: absolute;
  inset: 0;
  z-index: 20;
  background: var(--section-bg, var(--section-light));
  color: var(--text);
  overflow: hidden;
  pointer-events: auto;   /* the stage itself takes none (above) */
  /* Parked below the stage until script.js positions it. */
  transform: translate3d(0, 100%, 0);
  will-change: transform;
}

/* Reduced motion, the last panel (script.js adds .panel--reduced): it does
   not wipe. Where its exit would start it cross-fades out instead, opacity
   only, --rm-fade, and the contact section under it is simply there. */
.panel--reduced { transition: opacity var(--rm-fade) ease, visibility 0s; }
.panel--reduced.is-faded { opacity: 0; visibility: hidden; transition: opacity var(--rm-fade) ease, visibility 0s var(--rm-fade); }

/* The paragraph layer. script.js translates this by MINUS the panel's
   own offset, so it stands still in viewport coordinates while the panel
   sweeps past; the panel's overflow still clips it to the panel's
   rectangle. Nothing is masked here: the nav has no background, and text
   is meant to stay visible as it passes under it. */
.panel__clip {
  position: absolute;
  inset: 0;
  will-change: transform;
}

.panel__content {
  /* Positioned by script.js in viewport coordinates. Top: 0 here so the
     transform IS the viewport position. */
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  will-change: transform;
}

/* A section's content group: measured by script.js so it can be centred
   in the viewport at rest. */
.group { display: flex; flex-direction: column; align-items: flex-start; }

.para {
  margin: 0;
  font-weight: 800;
  font-size: 22px;
  line-height: 1.12;
  letter-spacing: -0.02em;
  color: var(--text);
}

/* Section 2: client logos. Natural SVG size, coloured by CSS. BMW's
   inner shapes read --section-bg from the panel. */
.clients {
  list-style: none;
  margin: 28px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, max-content);
  gap: 28px 32px;
  color: var(--grey);
}
.clients__item { display: block; }
.clients__logo { display: block; }
.clients__logo svg {
  display: block;
  height: calc(var(--h) * 0.7px);
  width: auto;
}

/* Section 4: service tabs. Panels share one grid cell so the block is as
   tall as the tallest variant; the swap is a crossfade with a 6px rise. */
.tabs {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
  margin: 0 0 24px;
}
.tabs__tab {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-weight: 800;
  font-size: 34px;
  line-height: 1;
  letter-spacing: -0.02em;
  cursor: pointer;
  transition: opacity 200ms var(--ease);
  -webkit-tap-highlight-color: transparent;
}
.tabs--picked .tabs__tab[aria-selected="false"] { opacity: 0.35; }

.tabs__panels { display: grid; width: 100%; }
.tabs__panel {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  transform: translate3d(0, 6px, 0);
  transition: opacity 160ms var(--ease), transform 0s linear 160ms, visibility 0s linear 160ms;
}
.tabs__panel.is-active {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity 240ms var(--ease), transform 240ms var(--ease), visibility 0s;
}

/* Section 8 */
.group--projects .btn { margin-top: 32px; }

/* ---- Spacer -------------------------------------------------------- */

.spacer {
  position: relative;
  width: 1px;
  height: 500vh; /* placeholder; script.js sets the real height */
  pointer-events: none;
}

/* ---- Contact (fixed under everything; the page's last light section
   uncovers it) --------------------------------------------------------
   One fixed, dark box the size of the LARGE viewport, like the stage. It
   never moves. On the homepage it comes BEFORE the stage in the DOM at the
   same z-index, so it is under it: the last light panel leaves at
   PANEL_SPEED_MULT like every other (script.js, every panel exits now) and
   its bottom edge is the mask that uncovers this. On the projects pages
   the same box is clipped from the top instead (projects.css): the same
   edge at the same speed, over natively scrolling content.
   A bounce past the end of the page shows dark, never a gap: the overscan
   (box-shadow, the hero's approach) and the html background while it is
   uncovered (html.contact-covers, by the nav rules; AGENTS.md rules 1 and 4).
   The markup is built by contact.js, once, for both pages. Everything
   readable is placed in the SMALL viewport (svh); the box is the large one
   (rule 3). */

.contact {
  /* The headline: the identity's display size (80 at 1920, 28 on a phone)
     at the contact's own tight pitch, 0.82: the mock's 66px line pitch. */
  --contact-size: calc(var(--id-size) * var(--iu));
  --contact-lh: 0.82;

  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  height: 100lvh;
  z-index: 0;
  background: var(--dark);
  color: var(--light);
  box-shadow: 0 0 0 var(--contact-overscan) var(--dark);
}

/* The grid wrapper only supplies the side padding; the blocks are placed
   absolutely at that padding (the gutter, the grid margin on desktop), so
   headline, email and mark share the content column's left edge. */
.contact__inner {
  position: relative;
  height: 100%;
}

/* Headline + email, one block. Its top puts the headline's CAP TOP where the
   mock has it, 311 of 1080 = 28.8% of the viewport height: the line box
   starts (P/2 - cap drop) above the cap top, P being the pitch in em. */
.contact__lead {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  top: calc(28.8vh - (var(--contact-lh) / 2 - var(--cap-drop)) * var(--contact-size));
  top: calc(28.8svh - (var(--contact-lh) / 2 - var(--cap-drop)) * var(--contact-size));
}

/* Over everything for the Contact link's mask reveal (contact.js
   maskReveal): above the nav, clipped from the top at --mask-edge, which
   the reveal writes as an inline custom property; back to the section's
   own place when the class goes. The clip is !important on purpose: on the
   projects pages the section's clip-path is an INLINE style written by
   their scroll-driven wipe (projects.js syncContact), and the mask must win
   over it while the class is on and leave it untouched for afterwards. The
   overscan under the box (box-shadow) stays inside the clip. */
.contact--over {
  z-index: 42;
  clip-path: inset(var(--mask-edge, 100%) 0 calc(-1 * var(--contact-overscan)) 0) !important;
}
/* The in-flow dark bands contact.js puts under a phone's browser bars: two
   for the mask's length (coverBars there) and one above the viewport's top
   for as long as the contact covers the whole screen (topBand). Absolutely
   placed in the document where the bars are, never inside the visible
   area. Above the nav (40), under the mask (42). */
.contact__band {
  position: absolute;
  left: 0;
  width: 100%;
  z-index: 41;
  background: var(--dark);
  pointer-events: none;
}
/* The heading takes focus after a reveal so Tab goes on to the email; it
   is not a control and shows no ring. */
.contact__title:focus { outline: none; }

.contact__title {
  margin: 0;
  font-weight: 800;
  font-size: var(--contact-size);
  line-height: var(--contact-lh);
  letter-spacing: var(--id-track);
  color: var(--light);
}
.contact__line { display: block; white-space: nowrap; }

/* The email: 40px / 800 at 1920 with no tracking (the desktop block; the
   mock's "you@withfluo.com" is 371 wide and its cap height 28.8), 20 on a
   phone (the projects pages' display copy size), underlined 3px thick, 5.5
   under the baseline. Its cap top sits 85 below the headline's last
   baseline in the mock; take off the two line boxes' own leading and the
   margin is 78 (36 on a phone). */
.contact__email-line {
  position: relative;
  margin: calc(36 * var(--iu)) 0 0;
  font-weight: 800;
  font-size: calc(20 * var(--iu));
  line-height: 1;
  letter-spacing: 0;
}
.contact__email {
  color: var(--light);
  text-decoration: underline;
  text-decoration-thickness: max(2px, 0.075em);   /* 3px at 40px */
  text-underline-offset: 0.12em;
  transition: opacity 200ms var(--ease);
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) { .contact__email:hover { opacity: 0.7; } }

/* "Copied", under the address (contact.js): the nav's small label (12 on a
   phone, 20 at 1920), white at 60%, in 150ms, held 1.2s, out 300ms. Always
   in the DOM (aria-live), empty when idle, never in the way. */
.contact__copied {
  position: absolute;
  left: 0;
  top: 100%;
  margin-top: 10px;
  font-weight: 700;
  font-size: 12px;
  line-height: 1;
  letter-spacing: -0.05em;
  color: rgba(247, 246, 245, 0.6);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 300ms ease;
  pointer-events: none;
}
.contact__copied.is-on { opacity: 1; transition-duration: 150ms; }

/* The mark, bottom left: the compact nav's 24 on a phone, 26 at 1920 (the
   mock). Its bottom sits 204 of 1080 = 18.9% of the SMALL viewport above
   that viewport's bottom, which is (lvh - svh) above the box's own. */
.contact__logo {
  position: absolute;
  left: var(--gutter);
  bottom: 18.9vh;
  bottom: calc(100lvh - 100svh + 18.9svh);
  display: block;
  color: var(--light);
}
.contact__mark {
  display: block;
  width: auto;
  height: 24px;
}
/* The company details (siteSettings.companyDetails, Petr's mock of
   2026-10-12): one line of the small label (12 / 700 / -0.05em, white at
   60%, "Copied"'s style) under the mark, on the mark's left edge, 12 under
   its bottom. Anchored by the top, from the same svh measure as the mark's
   bottom (81.1svh = 100svh - 18.9svh), so it lives inside the SMALL viewport
   and clears a phone's bottom bar; on a phone it may wrap into two or three
   lines inside the 18.9svh under the mark. */
.contact__details {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  top: calc(81.1vh + 12px);
  top: calc(81.1svh + 12px);
  margin: 0;
  font-weight: 700;
  font-size: 12px;
  line-height: 1.4;
  letter-spacing: -0.05em;
  color: rgba(247, 246, 245, 0.6);
}


/* ==================================================================
   DESKTOP (768px and wider): the 1920 design, scaled by --u.
   ================================================================== */

@media (min-width: 768px) {
  .grid { padding: 0 var(--gx); }

  /* Compact nav bottom: logo top 20 + logo height 40. */
  :root { --nav-bottom-compact: calc(20 * var(--u) + max(24px, 40 * var(--u))); }

  /* Nav, full state (homepage, top of the page only): logo 71 tall at
     x 40 / y 24; links 40px with their cap top at 45, 98 apart, the last one
     ending 44 from the right edge. Untouched.

     COMPACT state: the site's ONE compact nav, measured off the projects
     frames (list-desktop, detail-desktop): mark 38 tall at 30 / 21; links
     20px at -0.05em, 28 apart, centred on a 40-tall band from y 20 to 60;
     Contact a 99 × 40 square pill ending 20 from the right edge. The
     projects pages are always in this state; the homepage reaches it past
     NAV_SHRINK_AT. Its bottom edge is still 60, so --nav-bottom-compact
     (above) is unchanged.
     Position moves by transform, size by font-size and height, so nothing
     is left scaled (and blurry) at rest. */
  .nav__logo { left: calc(40 * var(--u)); top: calc(24 * var(--u)); }
  .nav__mark { height: max(24px, calc(71 * var(--u))); }
  .nav--compact .nav__logo { transform: translate(calc(-10 * var(--u)), calc(-3 * var(--u))); }
  .nav--compact .nav__mark { height: max(24px, calc(38 * var(--u))); }

  .nav__links {
    right: calc(44 * var(--u));
    top: calc(45 * var(--u) - var(--cap) * 40 * var(--u));
    height: calc(40 * var(--u));   /* = the 40px line: the same box the compact band needs */
    gap: calc(98 * var(--u));
    font-size: max(14px, calc(40 * var(--u)));
    transition: transform var(--nav-ms) var(--ease),
                height var(--nav-ms) var(--ease),
                font-size var(--nav-ms) var(--ease),
                gap var(--nav-ms) var(--ease),
                letter-spacing var(--nav-ms) var(--ease);
  }
  /* From right 44 → 20, and from the full state's top to y 20. */
  .nav--compact .nav__links {
    height: calc(40 * var(--u));
    gap: calc(28 * var(--u));
    font-size: max(14px, calc(20 * var(--u)));
    letter-spacing: -0.05em;
    transform: translate(calc(24 * var(--u)), calc(20 * var(--u) - (45 * var(--u) - var(--cap) * 40 * var(--u))));
  }

  /* Contact: plain text in the full state, a pill in the compact one. All
     three layers carry the same box, so the two paint layers' clip cuts
     through the pill exactly as it cuts through the letters. Over the video
     (light layer) it is a --light pill with a --dark-grey label; over a
     panel (dark layer) a --dark-grey pill with a --light label, as on the
     projects pages. The live layer paints nothing, as always. */
  .nav__pill {
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    transition: background-color var(--nav-ms) var(--ease), color var(--nav-ms) var(--ease),
                padding var(--nav-ms) var(--ease), min-width var(--nav-ms) var(--ease),
                height var(--nav-ms) var(--ease);
  }
  .nav--compact .nav__pill {
    height: calc(40 * var(--u));
    min-width: calc(99 * var(--u));
    padding: 0 calc(13 * var(--u));
  }
  .nav--compact .nav__layer--light .nav__pill { background: var(--light); color: var(--dark-grey); }
  .nav--compact .nav__layer--dark .nav__pill  { background: var(--dark-grey); color: var(--light); }

  .btn {
    padding: calc(17 * var(--u)) calc(20 * var(--u));
    font-size: max(12px, calc(17 * var(--u)));
  }
  .btn--filled { min-width: calc(255 * var(--u)); min-height: calc(55 * var(--u)); padding: 0 calc(20 * var(--u)); }
  .btn--light  { min-width: calc(115 * var(--u)); min-height: calc(58 * var(--u)); padding: 0 calc(20 * var(--u)); }

  /* Paragraphs: 36px, 3 columns wide, starting at the first column. */
  .para {
    width: var(--w3);
    max-width: 100%;
    font-size: max(18px, calc(36 * var(--u)));
    line-height: 1.08;
  }

  /* Section 2: one row of logos at natural size, 140px apart, 100px under the text. */
  .clients {
    display: flex;
    flex-wrap: nowrap;
    gap: calc(140 * var(--u));
    margin-top: calc(100 * var(--u));
  }
  .clients__logo svg { height: calc(var(--h) * var(--u)); }

  /* Section 4: words across the full 6-column width, 80px above the text. */
  .tabs {
    flex-direction: row;
    justify-content: space-between;
    align-items: baseline;
    margin: 0 0 calc(80 * var(--u));
  }
  .tabs__tab {
    min-height: 0;
    font-size: calc(66 * var(--u));
    line-height: 1.1;
  }
  .tabs__panels { width: var(--w3); max-width: 100%; }

  /* Section 8: 100px between the text and the button. */
  .group--projects .btn { margin-top: calc(100 * var(--u)); }

  /* Contact (style.css base block below the spacer): on the grid's left
     edge (184), the 80px display headline, the 40px email with its 3px
     underline, the 26px mark. All measured off the mock at 1920 x 1080. */
  .contact__lead, .contact__logo { left: var(--gx); }
  .contact__lead { right: var(--gx); }
  .contact__email-line {
    margin-top: calc(78 * var(--u));
    font-size: max(18px, calc(40 * var(--u)));
  }
  .contact__copied { margin-top: calc(14 * var(--u)); font-size: max(12px, calc(20 * var(--u))); }
  .contact__mark { height: max(20px, calc(26 * var(--u))); }
  .contact__details { left: var(--gx); right: var(--gx); top: calc(81.1svh + 14 * var(--u)); font-size: max(12px, calc(20 * var(--u))); }

  /* ---- The flat nav on desktop (measured off the projects frames). This
     nav has a single self-painting layer, so the pill's colours are its own.
     Text widths fitted at -0.05em: "Back to home" 117, "Back to projects" 139,
     "Projects of Fluo" 160 (24px), "Contact" 72. */
  .nav--flat .nav__layer--live .nav__pill { background: var(--dark-grey); color: var(--light); }
  .nav--flat { --slot-gap: calc(28 * var(--u)); }   /* = the compact links' gap */
  .nav--flat.nav--paint-light .nav__layer--live .nav__pill { background: var(--light); color: var(--dark-grey); }
  .nav__arrow { width: calc(16 * var(--u)); height: calc(16 * var(--u)); margin-right: calc(14 * var(--u)); }

  [data-view="list"] .nav__label {
    display: flex;
    align-items: center;
    position: absolute;
    left: 50%;
    top: calc(20 * var(--u));
    height: calc(40 * var(--u));
    transform: translateX(-50%);
    font-weight: 700;
    font-size: max(14px, calc(24 * var(--u)));   /* 24 in the frame (160 wide, cap 17.3), not the links' 20 */
    line-height: 1;
    white-space: nowrap;
  }
}

/* ---- The phone nav's centre line (spec §9) ------------------------------
   On every page the link text's capitals are centred on the FLUO mark's
   centre line. (The mark's ink fills its viewBox, so its box centre is its
   visual centre.) Not computed from the font's metrics: browsers round a
   font's ascent and descent to whole px before they place the baseline in
   its line box, which at 12px moved the capitals 0.65px off. Glyphs ARE
   exact relative to their baseline, so the links are baseline-aligned to a
   strut instead: an empty flex item, whose baseline is its bottom edge, as
   tall as half the mark's box plus half a cap height. The baseline then sits
   exactly half a cap height under the mark's centre, in any engine.
   One more step: Blink paints a line's baseline on a whole CSS px, so the
   strut is a whole number of px tall (half a cap height rounded down: 6 of
   6.12 at 17px, 4 of 4.32 at 12px) and the remaining fraction is a transform,
   which is not snapped. At 12px that is 0.32px, one device px on a 3x phone.
   The back link's arrow (0.8em tall) stands on that baseline too and is
   lowered by (0.8 - cap height) / 2 em, which centres it on the capitals. */
@media (max-width: 767px) {
  .nav__links { align-items: baseline; }
  .nav__links::before {
    content: '';
    width: 0;
    margin-right: calc(-1 * 24px);   /* gives its flex gap back */
    height: calc(16px + 6px);   /* half the mark's box (32) + the whole px of half a cap height */
    transition: height var(--nav-ms) var(--ease), margin-right var(--nav-ms) var(--ease);
  }
  .nav--compact .nav__links::before {
    margin-right: calc(-1 * 32px);
    height: calc(12px + 4px);   /* mark box 24 */
  }
  .nav__links { transform: translate(0, calc(var(--cap-h) / 2 * 1em - 6px)); }
  .nav--compact .nav__links { transform: translate(0, calc(-4px + var(--cap-h) / 2 * 1em - 4px)); }
  .nav__back-label { align-items: baseline; }
  .nav__arrow { transform: translateY(calc((0.8 - var(--cap-h)) / 2 * 1em)); }
}

/* The flat nav on a phone. Its size and position are the compact nav's, above
   (12px, 32 apart, capitals centred on the mark); Contact is plain text, there
   is no pill in either mobile frame. Only the slots differ here: a detail
   reached from the homepage shows the homepage's own two items, so the back
   link's slot closes. */
@media (max-width: 767px) {
  .nav--flat { --slot-gap: 32px; }
  .nav__arrow { width: 0.8em; height: 0.8em; margin-right: 0.3em; }
  /* A detail opened directly shows the same two items: `Projects` · `Contact`. */
  [data-view="detail"][data-arrival="direct"] .nav__slot--projects {
    grid-template-columns: 1fr;
    margin-right: 0;
    opacity: 1;
    visibility: visible;
    transition: grid-template-columns 300ms var(--ease), margin-right 300ms var(--ease),
                opacity 300ms ease, visibility 0s;
  }
  [data-view="detail"][data-arrival="home"] .nav__slot--back,
  [data-view="detail"][data-arrival="direct"] .nav__slot--back {
    grid-template-columns: 0fr;
    margin-right: calc(-1 * var(--slot-gap));
    opacity: 0;
    visibility: hidden;
    transition: grid-template-columns 300ms var(--ease), margin-right 300ms var(--ease),
                opacity 300ms ease, visibility 0s 300ms;
  }
}

/* ---- Reduced motion (spec §10.6) -------------------------------------
   Last in the file on purpose: these must out-rank every transition above,
   including the breakpoint blocks.
   Opacity only: nothing translates, scales, widens or flies. Every visual
   change that can fade is a --rm-fade cross-fade with the plain `ease`
   curve; a change of geometry alone, with nothing to fade, is instant. */

@media (prefers-reduced-motion: reduce) {
  /* The nav's geometry (size, position, gaps, the pill's box) snaps; its
     colours cross-fade. */
  :root { --nav-ms: 0s; }
  .nav__pill { transition: background-color var(--rm-fade) ease, color var(--rm-fade) ease; }

  /* The nav's slots: the column opens and closes at once (nothing slides),
     the link inside cross-fades. Closing keeps it visible for the fade. */
  .nav__back-label { transition: opacity var(--rm-fade) ease; }
  .nav__slot, .nav__slot--projects,
  [data-view="detail"][data-arrival="home"] .nav__slot--projects,
  [data-view="detail"][data-arrival="direct"] .nav__slot--projects,
  [data-view="detail"][data-arrival="home"] .nav__slot--back,
  [data-view="detail"][data-arrival="direct"] .nav__slot--back {
    transition: grid-template-columns 0s, margin-right 0s, opacity var(--rm-fade) ease, visibility 0s var(--rm-fade);
  }
  .nav__slot:not(.nav__slot--projects),
  [data-view="detail"][data-arrival="home"] .nav__slot--projects,
  [data-view="detail"][data-arrival="direct"] .nav__slot--projects { transition-delay: 0s; }

  /* Tabs: the words and the panels cross-fade; the panel's 6px rise is gone. */
  .tabs__tab { transition: opacity var(--rm-fade) ease; }
  .tabs__panel { transform: none; transition: opacity var(--rm-fade) ease, visibility 0s linear var(--rm-fade); }
  .tabs__panel.is-active { transition: opacity var(--rm-fade) ease, visibility 0s; }

  /* The contact section: "Copied" and the email's hover fade at the one
     length too. */
  .contact__copied, .contact__copied.is-on, .contact__email { transition-duration: var(--rm-fade); }

  /* Returning from the projects pages: the cover and the nav cross-fade. */
  html.is-returning { transition-duration: var(--rm-fade); transition-timing-function: ease; }
  html.is-returning body::before, html.is-returning .nav { transition-duration: var(--rm-fade); transition-timing-function: ease; animation-duration: var(--rm-fade); animation-timing-function: ease; }
}
