/* technoon.ai — the Pearl Floor.

   ONE world, ONE camera, ONE continuous story. The page is a bright, high-key
   room, so LEGIBILITY IS INVERTED here and that trips people up: every scrim is
   a light pearl wash, never a dark one, and every text shadow is a light halo.
   When auditing contrast, hunt the DARKEST pixel under the type, not the
   lightest.

   The boldness budget is spent in exactly one place: the light ribbon, cyan to
   violet to magenta, which threads the seven business functions together. It is
   lifted from the storyboard's own rows. Everything around it stays quiet. */

/* ════════════════════════════════════════════════════════════════════════
   1 · TOKENS
   Every value sampled from the storyboard, none invented.
   ════════════════════════════════════════════════════════════════════════ */

:root {
  --pearl:    #F2F4F8;   /* the page canvas. Never pure white. */
  --pearl-2:  #E8ECF4;   /* recessed surfaces */
  --ink:      #0E1330;   /* primary type */
  --ink-soft: #2A3154;   /* secondary type */
  --slate:    #5A628A;   /* tertiary, small labels */
  --violet:   #6C4CE0;   /* primary accent — rare doses only */
  --cyan:     #35B8F2;   /* ribbon cool end */
  --magenta:  #C64BE8;   /* ribbon warm end */
  --plate:    #080918;   /* the logo badge, sampled from logo-source.jpg itself */

  --ribbon: linear-gradient(90deg, var(--cyan), var(--violet) 52%, var(--magenta));

  --display: 'Familjen Grotesk', 'Segoe UI', system-ui, sans-serif;
  --body:    'Hanken Grotesk', 'Segoe UI', system-ui, sans-serif;

  --gut:  clamp(20px, 5vw, 96px);   /* the rail inset */
  /* Floor raised 64 → 72 when the supplied stacked lockup replaced the rebuilt
     one: the mark got taller relative to its width, and 64px left it no room. */
  --navh: clamp(72px, 8vh, 88px);

  /* the light halo that keeps dark ink readable over bright moving footage */
  --halo: 0 0 14px rgba(242,244,248,.95),
          0 0 34px rgba(242,244,248,.82),
          0 0 72px rgba(242,244,248,.55);
}

/* ════════════════════════════════════════════════════════════════════════
   2 · RESET AND PAGE
   ════════════════════════════════════════════════════════════════════════ */

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

html, body {
  margin: 0;
  padding: 0;
  overflow-x: hidden;   /* declared first, as the fallback */
  overflow-x: clip;
}

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

body {
  background: var(--pearl);
  color: var(--ink);
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.5;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img { max-width: 100%; display: block; }

/* a hidden tab must cost nothing; play-state does not inherit, so it has to
   reach pseudo-elements explicitly */
body.paused,
body.paused *,
body.paused *::before,
body.paused *::after { animation-play-state: paused !important; }

::selection { background: rgba(108,76,224,.18); color: var(--ink); }

:focus-visible {
  outline: 3px solid var(--violet);
  outline-offset: 3px;
  border-radius: 4px;
}

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

.skip {
  position: fixed;
  top: 10px; left: 10px;
  z-index: 999;
  transform: translateY(-160%);
  padding: 12px 20px;
  background: var(--plate);
  color: #fff;
  font-family: var(--display);
  font-weight: 500;
  font-size: 14px;
  letter-spacing: .02em;
  text-decoration: none;
  border-radius: 8px;
  transition: transform .22s ease;
}
.skip:focus { transform: translateY(0); }

/* ════════════════════════════════════════════════════════════════════════
   3 · THE ENVIRONMENT LAYER
   One fixed field behind everything, so scrolling moves through a place
   rather than past a stack of sections.
   ════════════════════════════════════════════════════════════════════════ */

.env {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(120% 85% at 50% -10%, #FFFFFF 0%, rgba(255,255,255,0) 58%),
    radial-gradient(70% 55% at 12% 88%, rgba(53,184,242,.10) 0%, rgba(53,184,242,0) 70%),
    radial-gradient(70% 55% at 88% 82%, rgba(198,75,232,.09) 0%, rgba(198,75,232,0) 70%),
    var(--pearl);
}

/* ════════════════════════════════════════════════════════════════════════
   4 · NAV, LOGO, CTA

   THE MARK IS THE SUPPLIED FILE. Nothing about it is drawn here. Client
   2026-08-17: "don't change the logo I have given, if possible just increase
   the resolution."

   assets/logo-mark.png is 1249x814 RGBA, produced by review/key-logo.py from
   brief/logo-source-1672.png. That original ships its transparency as a baked
   checkerboard, so the one step is an uncomposite of that checkerboard. No
   recolour, no re-set type, no trace — every pixel is the client's own. The
   earlier rebuild and trace pipelines existed only because the file before it
   was 190x112, and they are superseded.

   Still forbidden: re-colouring outside the token palette, particle-ising, or
   regenerating the mark through any AI tool. Animate opacity/position/scale
   only.
   ════════════════════════════════════════════════════════════════════════ */

.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 60;
  height: var(--navh);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 clamp(14px, 3.6vw, 40px);
  background: linear-gradient(180deg, rgba(242,244,248,.86), rgba(242,244,248,0));
  backdrop-filter: blur(10px) saturate(120%);
  -webkit-backdrop-filter: blur(10px) saturate(120%);
}

.brand { display: inline-flex; text-decoration: none; }

/* No background, no padding, no border radius, no shadow. The wrapper survives
   only as the transform hook for the hover lift; the mark is now the whole
   visible object. Give this a background again and the box the client asked to
   be rid of comes straight back. */
.brand-plate {
  display: inline-flex;
  transition: transform .3s cubic-bezier(.2,.7,.2,1);
}
/* Sized by HEIGHT, not width, and this is deliberate. The supplied lockup is
   stacked — sphere over wordmark — so it is 0.65 as tall as it is wide, and a
   fixed width lets the nav's own height clip the top of the sphere the moment
   --navh sits at its floor. Driving off --navh means the mark cannot clip at
   any viewport, and the width simply follows. The ratio has now moved twice
   with three supplied originals; height-driven is what survives the next one. */
/* <picture> is a source-selection wrapper, not a box. display:contents takes
   it out of the layout entirely so the <img> stays the direct flex item of
   .brand-plate and the height-driven sizing below is unchanged by the WebP
   fallback being added around it. */
.brand-plate picture { display: contents; }

.brand-plate img {
  height: clamp(44px, calc(var(--navh) - 20px), 60px);
  width: auto;
}
.brand:hover .brand-plate { transform: translateY(-1px) scale(1.015); }

.brand-plate--lg img { height: clamp(88px, 12vh, 116px); }

.nav-actions {
  display: flex;
  align-items: center;
  gap: clamp(8px, 1vw, 14px);
}

/* The address, top right. Client instruction 2026-08-16 was "subtle", and
   subtle here means it is not a third button: no pill, no fill, no border, no
   backdrop filter. Body face rather than display, --slate rather than --ink,
   and the only thing that happens on hover is the underline arriving. It reads
   as a detail printed on the bar rather than as something to press, which is
   exactly what an address is next to two calls to action.

   It is NOT --violet. The accent budget on this page is spent on "moves",
   "strategy", the kickers, the CTA, the focus ring, the hero rule, the ribbon
   and the lit chain chips. An email address is not on that list. */
.nav-mail {
  font-family: var(--body);
  font-size: clamp(11px, .88vw, 13px);
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  color: var(--slate);
  padding: 6px 2px;
  border-bottom: 1px solid transparent;
  transition: color .3s ease, border-color .3s ease;
}
.nav-mail:hover {
  color: var(--ink-soft);
  border-bottom-color: rgba(90,98,138,.36);
}

/* A narrow bar carries the mark and two actions and is already tight at that.
   The address is the thing that gives way, because it is the only one of the
   four with a permanent home elsewhere: .foot-mail. Declared here rather than
   in §13 so it travels with the rule it turns off. */
@media (max-width: 860px) {
  .nav-mail { display: none; }
}

/* The ghost pill's qualifier gives way well before that, and for a different
   reason: it is the only one of the four things in the bar that is optional
   TEXT rather than a whole control. Below ~1120px `Book Free Audit for your
   business` and `Build Your Intelligent Business` are two long labels fighting
   over the same row, and the mark is what loses. The lead survives at every
   width, so the instruction never does. */
@media (max-width: 1120px) {
  .ghost-tail { display: none; }
}

/* The quiet one. It sits beside the primary rather than competing with it: no
   accent fill, no accent text, just a glass pill and one lit dot borrowed from
   the ribbon.

   Given a longer label on 2026-08-17 it also got the primary's CURVATURE pass,
   in the same vocabulary and at a fraction of the volume — because premium on
   this page is light behaviour, not more colour, and the accent budget has
   nothing left to spend on the second button in the bar:

   1. A crown. The radial lights the upper face and falls off before the middle,
      so the pill has a top rather than being an evenly bright rectangle.
   2. A rim INSIDE the fill. A hairline of white one pixel in from the border
      is what glass has and what a flat pill does not.
   3. A foot. The inset dark at the bottom rolls the lower edge away from the
      reader instead of stopping it dead at the border.
   4. Three shadows at three radii — contact, drop, bloom — rather than one.
      One shadow is a sticker; three read as an object above a surface.

   The dot is still the only thing that moves on its own. */
.ghost {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 46px;
  padding: 12px clamp(15px, 1.7vw, 21px);
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(12px, .95vw, 14px);
  letter-spacing: .05em;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  color: var(--ink-soft);
  border-radius: 999px;
  border: 1px solid rgba(90,98,138,.18);
  background:
    radial-gradient(128% 160% at 50% -34%,
                    rgba(255,255,255,.96), rgba(255,255,255,0) 58%),
    linear-gradient(180deg, rgba(255,255,255,.60), rgba(255,255,255,.32));
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.96),
    inset 0 0 0 1px rgba(255,255,255,.44),
    inset 0 -7px 11px -9px rgba(14,19,48,.34),
    0 1px 1px rgba(14,19,48,.04),
    0 3px 8px -2px rgba(14,19,48,.06),
    0 12px 24px -10px rgba(14,19,48,.12);
  backdrop-filter: blur(12px) saturate(150%);
  -webkit-backdrop-filter: blur(12px) saturate(150%);
  transition: color .3s ease, border-color .3s ease, background .3s ease,
              transform .3s cubic-bezier(.2,.7,.2,1), box-shadow .3s ease;
}
.ghost:hover {
  color: var(--ink);
  border-color: rgba(108,76,224,.34);
  background:
    radial-gradient(128% 160% at 50% -34%,
                    rgba(255,255,255,1), rgba(255,255,255,0) 60%),
    linear-gradient(180deg, rgba(255,255,255,.82), rgba(255,255,255,.54));
  transform: translateY(-1.5px);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,1),
    inset 0 0 0 1px rgba(255,255,255,.58),
    inset 0 -7px 11px -9px rgba(14,19,48,.30),
    0 1px 1px rgba(14,19,48,.05),
    0 6px 14px -4px rgba(14,19,48,.08),
    0 18px 34px -14px rgba(14,19,48,.16);
}
/* Pressed IN, not moved down: the bloom collapses to a contact shadow and the
   foot deepens, which is the same account of the press the primary gives. */
.ghost:active {
  transform: translateY(0);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.8),
    inset 0 0 0 1px rgba(255,255,255,.4),
    inset 0 -9px 13px -9px rgba(14,19,48,.44),
    0 1px 2px rgba(14,19,48,.07);
}

/* Two parts, two weights. The verb is the thing being pressed and takes --ink;
   the qualifier is context and takes --slate, one step back and a hair tighter.
   That is what stops a 33-character label reading as one long grey sentence.
   --slate on the pearl bar is 5.3:1, well over the 4.5 bar. */
.ghost-lead { color: var(--ink); }
.ghost-tail {
  color: var(--slate);
  font-weight: 400;
  letter-spacing: .02em;
  transition: color .3s ease;
}
.ghost:hover .ghost-tail { color: var(--ink-soft); }

.ghost-dot {
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 999px;
  background: var(--ribbon);
  box-shadow: 0 0 0 0 rgba(108,76,224,.34);
  animation: dot 3.4s cubic-bezier(.4,0,.2,1) infinite;
}
@keyframes dot {
  0%, 68%, 100% { box-shadow: 0 0 0 0 rgba(108,76,224,.34); }
  84%           { box-shadow: 0 0 0 6px rgba(108,76,224,0); }
}

/* The primary. Premium here means depth, not volume, and this is the second
   pass at it: the first had a diagonal grade, a lit top edge, a shaded bottom
   and a specular sweep, which made it a nice flat pill. What it lacked was
   CURVATURE — every edge was a hairline and the face between them was flat, so
   it read as a coloured rectangle with rounded ends rather than as an object
   with a top and a bottom.

   Four things carry the weight now, and every one of them is light behaviour
   rather than more colour, because the accent budget on this page is small:

   1. A crown. The radial in the background stack is a soft light landing on
      the upper face and falling off before the middle, which is what actually
      makes a surface read as convex.
   2. A foot. The inset shadow at the bottom rolls the lower edge away from the
      viewer. A lit top edge alone is only half of a curve.
   3. A hairline ring, inside the fill rather than as a border, so the pill has
      a defined rim without spending a pixel of layout.
   4. Three shadows instead of two — a tight contact shadow that sits it ON the
      page, a mid shadow for form, and a wide violet bloom for the ambient. One
      shadow at one radius is the thing that makes a button look printed.

   The sheen deliberately passes OVER the label, the way light crosses a real
   surface, which is why it is an ::after with no z-index. It is narrower and
   skewed now: a wide soft band reads as the whole button flashing, a narrow
   raked one reads as a highlight travelling across glass.

   Deliberately NOT the ribbon. The ribbon's appearance list is closed and the
   CTA is not on it; this stays in the violet family and gets its richness from
   the depth of the grade, #8A6BF2 down to a near-indigo #4527B8. */
.cta {
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 46px;
  padding: 13px clamp(18px, 2.4vw, 30px);
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(13px, 1.05vw, 15px);
  letter-spacing: .015em;
  line-height: 1;
  text-align: center;
  text-decoration: none;
  color: #fff;
  background:
    radial-gradient(118% 150% at 50% -32%,
                    rgba(255,255,255,.40) 0%,
                    rgba(255,255,255,.10) 42%,
                    rgba(255,255,255,0)   64%),
    linear-gradient(135deg, #8A6BF2 0%, var(--violet) 46%, #4527B8 100%);
  border-radius: 999px;
  box-shadow:
    inset 0 0 0 1px rgba(255,255,255,.16),
    inset 0 1px 0 rgba(255,255,255,.46),
    inset 0 -1px 0 rgba(14,19,48,.30),
    inset 0 -12px 20px -14px rgba(9,12,34,.75),
    0 1px 2px rgba(14,19,48,.16),
    0 8px 18px -6px rgba(69,39,184,.32),
    0 20px 44px -14px rgba(108,76,224,.42);
  transition: transform .3s cubic-bezier(.2,.7,.2,1), box-shadow .3s ease, filter .3s ease;
}
.cta::after {
  content: '';
  position: absolute;
  top: -20%;
  bottom: -20%;
  left: 0;
  right: 0;
  pointer-events: none;
  background: linear-gradient(100deg,
              rgba(255,255,255,0)   38%,
              rgba(255,255,255,.10) 45%,
              rgba(255,255,255,.44) 50%,
              rgba(255,255,255,.10) 55%,
              rgba(255,255,255,0)   62%);
  transform: translateX(-118%) skewX(-14deg);
  transition: transform 1s cubic-bezier(.24,.6,.16,1);
}
.cta:hover {
  transform: translateY(-2px);
  filter: saturate(110%) brightness(1.03);
  box-shadow:
    inset 0 0 0 1px rgba(255,255,255,.22),
    inset 0 1px 0 rgba(255,255,255,.54),
    inset 0 -1px 0 rgba(14,19,48,.30),
    inset 0 -12px 20px -14px rgba(9,12,34,.70),
    0 2px 4px rgba(14,19,48,.16),
    0 12px 26px -8px rgba(69,39,184,.38),
    0 30px 62px -16px rgba(108,76,224,.52);
}
.cta:hover::after { transform: translateX(118%) skewX(-14deg); }
/* Pressed: the lift goes, the bloom collapses to a contact shadow and the foot
   deepens, so the pill reads as pushed INTO the page rather than just moved. */
.cta:active {
  transform: translateY(1px);
  box-shadow:
    inset 0 0 0 1px rgba(255,255,255,.14),
    inset 0 2px 5px rgba(9,12,34,.42),
    inset 0 -1px 0 rgba(255,255,255,.12),
    0 1px 2px rgba(14,19,48,.18),
    0 4px 10px -4px rgba(69,39,184,.28);
}

.cta--hero { margin-top: clamp(20px, 3vh, 32px); }
.cta--arrival { min-height: 56px; font-size: clamp(15px, 1.3vw, 18px); padding: 18px 36px; }

/* ════════════════════════════════════════════════════════════════════════
   5 · THE JOURNEY
   1800vh of scroll owned by one pinned stage. Minimum band width is 0.055 of
   progress, about 99vh, so every beat survives seven or eight normal flicks.
   ════════════════════════════════════════════════════════════════════════ */

main { display: block; }
main:focus { outline: none; }

.journey {
  position: relative;
  height: 1800vh;
}

.stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  isolation: isolate;
}

/* ── the media stack: footage below, real-time 3D above ─────────────────── */

.media { position: absolute; inset: 0; z-index: 0; }

.poster,
.media video,
.media canvas { position: absolute; inset: 0; width: 100%; height: 100%; }

/* The room is declared here, not left to JS. app.js only assigns this image
   inside initHeroOnce(), which never runs when the static-hero gate disarms the
   scrub — so on every phone the office silently failed to load and the beats
   floated on blank pearl. The room is the premise of the whole page; it cannot
   depend on the scrub being armed. (app.js still sets the same URL, harmlessly.) */
.poster {
  background: var(--pearl-2) url('hero-poster.jpg') center/cover no-repeat;
  transform: scale(1.02);
}

.media video {
  object-fit: cover;
  opacity: 0;
  transition: opacity .7s ease;
}
.stage.video-ready video { opacity: 1; }

/* if the video never arrives the poster carries the room and the 3D layer
   carries the whole story. Nothing is broken, nothing is stuck. */
.stage.video-failed .poster { transform: scale(1.05); }

.media canvas { z-index: 2; pointer-events: none; }

/* ── global wash: keeps the nav and the cue legible without dimming the room */

.scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(242,244,248,.72) 0%, rgba(242,244,248,0) 22%),
    linear-gradient(0deg,   rgba(242,244,248,.58) 0%, rgba(242,244,248,0) 20%);
}

.grain {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: .038;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/></filter><rect width='140' height='140' filter='url(%23n)'/></svg>");
}

/* ════════════════════════════════════════════════════════════════════════
   6 · THE CAPTION BANDS
   --o is the band's own opacity, driven by scroll. --k is its entrance ramp.
   Both are written by app.js only when they actually change.
   ════════════════════════════════════════════════════════════════════════ */

.bands { position: absolute; inset: 0; z-index: 5; pointer-events: none; }

.band {
  position: absolute;
  opacity: var(--o, 1);
  pointer-events: none;
}
.band > * { pointer-events: auto; }

/* before the drive loop arms, hide everything but the opening beat, so the
   sixteen beats never flash stacked on top of each other */
.js .band { --o: 0; --k: 0; visibility: hidden; }
.js .band--hero { --o: 1; visibility: visible; }
.no-js .band { --o: 1; --k: 1; }

/* the local scrim: a light pearl wash that deepens only while its own band is
   active, centred on that band's rail */
.band::before {
  content: '';
  position: absolute;
  inset: -14vh -12vw;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(58% 58% at var(--sx, 50%) var(--sy, 50%),
              rgba(242,244,248,.94) 0%,
              rgba(242,244,248,.74) 42%,
              rgba(242,244,248,0)   76%);
}

/* ── the rails. Pain on the left, resolution on the right. The mirrored side
   plus a calm entrance instead of an agitated one is how before-and-after is
   expressed structurally, rather than announced in words. ────────────────── */

.band--hero {
  left: var(--gut);
  top: 50%;
  transform: translateY(-50%);
  max-width: min(56ch, 62vw);
  --sx: 34%; --sy: 50%;
}

.band--pain {
  left: var(--gut);
  top: 50%;
  transform: translateY(-50%);
  max-width: min(42ch, 46vw);
  --sx: 30%; --sy: 50%;
}

.band--fix {
  right: var(--gut);
  top: 50%;
  transform: translateY(-50%);
  max-width: min(40ch, 44vw);
  text-align: right;
  --sx: 70%; --sy: 50%;
}

/* The three remaining functions surface at three different points and three
   different depths, so they read as independent systems, not a list.

   Pulled in off the frame edge on 2026-08-16: at the old rail positions they
   sat in the reader's periphery through the fastest stretch of the scroll and
   were the easiest beats on the page to skim past. They still hold three
   distinct quadrants, so the composition is tightened rather than changed in
   kind. Type size and depth are the storyboard's original values; only the
   anchors moved.

   Moving a caption moves the footage underneath it, so the crop windows in
   review/verify-contrast.sh were re-aimed to match. That file samples the
   real frame at the real position, and leaving it pointed at the old rail
   would audit a patch of room that no caption sits on any more. */
.band--fn.pos-a { left:  calc(var(--gut) + 7vw);  top:    26vh; --sx: 38%; --sy: 36%; --depth: 1.00; }
.band--fn.pos-b { right: calc(var(--gut) + 7vw);  top:    30vh; --sx: 62%; --sy: 40%; --depth: .82; text-align: right; }
.band--fn.pos-c { left:  calc(var(--gut) + 11vw); bottom: 27vh; --sx: 44%; --sy: 68%; --depth: .90; }

.band--chain {
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(1180px, 90vw);
  text-align: center;
  --sx: 50%; --sy: 50%;
}

.band--close {
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(880px, 88vw);
  text-align: center;
  --sx: 50%; --sy: 50%;
}

/* ════════════════════════════════════════════════════════════════════════
   7 · TYPE
   Two faces only. The site carries a short fixed set of strings, so a third
   face would be pure decoration.
   ════════════════════════════════════════════════════════════════════════ */

.lede {
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(30px, 5.4vw, 78px);
  line-height: 1.04;
  letter-spacing: -.025em;
  color: var(--ink);
  text-shadow: var(--halo);
}
.lede--close { font-size: clamp(26px, 4.1vw, 58px); }

.say {
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(24px, 3.5vw, 50px);
  line-height: 1.08;
  letter-spacing: -.02em;
  color: var(--ink);
  text-shadow: var(--halo);
}

.fn {
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  font-size: calc(clamp(19px, 2.5vw, 38px) * var(--depth, 1));
  line-height: 1.05;
  letter-spacing: .01em;
  color: var(--ink-soft);
  text-shadow: var(--halo);
}

.kicker {
  margin: 0 0 clamp(10px, 1.4vh, 18px);
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(11px, .92vw, 14px);
  letter-spacing: .30em;
  text-transform: uppercase;
  color: var(--slate);
  text-shadow: var(--halo);
}
.kicker--on { color: var(--violet); }

.sub {
  margin: clamp(14px, 2vh, 22px) 0 0;
  font-family: var(--body);
  font-weight: 400;
  font-size: clamp(15px, 1.35vw, 20px);
  line-height: 1.55;
  color: var(--ink-soft);
  text-shadow: var(--halo);
}

/* the hero rule draws itself in as the opening beat assembles */
.rule {
  display: block;
  height: 3px;
  width: calc(var(--k, 1) * clamp(64px, 8vw, 104px));
  margin-top: clamp(18px, 2.4vh, 28px);
  border-radius: 999px;
  background: var(--ribbon);
}

/* ── split reveal ───────────────────────────────────────────────────────
   Every token carries its own threshold --th. --rev is that token's local
   0→1, so one scalar (--k) staggers a whole headline with no per-frame JS. */

.split { display: block; }

.w, .c {
  display: inline-block;
  --rev: clamp(0, calc((var(--k, 1) - var(--th, 0)) / .30), 1);
  will-change: transform, opacity;
}

.w.em { color: var(--violet); }

/* calm arrival: the resolution beats and the close */
[data-enter="rise"] .w {
  opacity: var(--rev);
  transform: translateY(calc((1 - var(--rev)) * .8em));
}

/* agitated arrival: the type itself comes apart further as the overload builds.
   --spread grows beat by beat, which is why MARKETING lands gently and
   CUSTOMER SUCCESS lands scattered. */
[data-enter="scatter"] .w { opacity: 1; transform: none; }
[data-enter="scatter"] .c {
  opacity: var(--rev);
  transform:
    translate(calc(var(--jx, 0px) * (1 - var(--rev))),
              calc(var(--jy, 0px) * (1 - var(--rev))))
    rotate(calc(var(--jr, 0deg) * (1 - var(--rev))));
}

/* ════════════════════════════════════════════════════════════════════════
   8 · THE DOCK POP

   The macOS task bar effect the client asked for. It lives on the pill row in
   section 9, which is the one thing on this page that is genuinely a dock, and
   on the function card in section 10 that opens from it.

   INVERTED, like everything else here, and this is the part that catches
   people. The reference material for a glass bezel is built for dark UI: it
   stacks near-black insets, which on a dark surface read as a lit rim and on
   this pearl page read as soot. The roles are swapped rather than the colours
   dropped. WHITE carries the lit top-left edge, INK carries the shaded
   bottom-right one, so the pill still reads as a solid piece of glass catching
   light from above.
   ════════════════════════════════════════════════════════════════════════ */

/* --pop and --rise are registered so they can be TRANSITIONED, and that is the
   whole reason this is not a plain `transition: transform`.

   A dock pill's transform already carries the scroll-driven --lit rise from
   the wave. Transitioning the composed transform would put a third of a second
   of lag between the scroll and the light, which is exactly the kind of drift
   this page is built to avoid. Animating the two custom properties leaves the
   --lit term instant and makes only the hover pop smooth. Browsers without
   @property ignore these and simply snap the pop, which still reads correctly. */
@property --pop  { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --rise { syntax: '<length>'; inherits: false; initial-value: 0px; }

/* ── the plate: REMOVED FROM THE CAPTION BEATS ──────────────────────────
   Tried and rejected 2026-08-16. A white glass plate behind each caption was
   the literal reading of "make the function cards glassmorphic", and it was
   wrong for this page: the beats are captions floating in a room, not cards
   sitting on a page, and boxing them turned a cinematic frame into a component
   gallery. Client called it exactly right — "I do not want that white
   background, it's looking odd."

   The glass material now lives only where a real macOS dock puts it: on the
   pill row, which IS a dock, and on the function card that opens from it.

   .glass survives as a pass-through wrapper so the markup keeps one hook per
   beat. It must stay visually inert: no background, no padding, no backdrop
   filter, no transform. Adding any of those brings the box back. */

.glass {
  display: contents;
}

/* ── the clickable function words ───────────────────────────────────────
   .detail-ready is added by detail.js. Until it lands the labels carry no
   affordance at all, so a failed module leaves plain text rather than words
   that look clickable and are not. Same law as the video and the 3D layer:
   any layer may fail alone. */

.fn-open {
  position: relative;
  display: inline-block;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-align: inherit;
  cursor: default;
}
.detail-ready .fn-open { cursor: pointer; }

/* The affordance is one hairline in the label's OWN colour, drawn only on
   approach. It started as a ribbon gradient, which parked a bar of cyan and
   violet under every clickable heading; the client asked for the stray purple
   shapes to go, and they were right on the palette rule too. The accent budget
   on this page belongs to the ribbon and nothing else. */
.fn-open::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -.16em;
  height: 1.5px;
  border-radius: 999px;
  background: currentColor;
  opacity: 0;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .34s cubic-bezier(.2,.7,.2,1), opacity .34s ease;
}
.detail-ready .fn-open:hover::after,
.detail-ready .fn-open:focus-visible::after { opacity: .5; transform: scaleX(1); }

.band--fix .fn-open::after,
.band--fn.pos-b .fn-open::after { transform-origin: right center; }

/* ════════════════════════════════════════════════════════════════════════
   9 · THE CONNECTED SYSTEM
   All six functions, threaded by the light ribbon, lighting in sequence as
   the wave passes. --lit is written per chip by app.js.
   ════════════════════════════════════════════════════════════════════════ */

.chain-title {
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(22px, 3.2vw, 46px);
  line-height: 1.06;
  letter-spacing: -.02em;
  color: var(--ink);
  text-shadow: var(--halo);
}

.chain-head { margin-bottom: clamp(24px, 4vh, 44px); }

.chain {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(8px, 1vw, 14px);
}

/* The light ribbon used to cross behind this row as a blurred band. Removed
   2026-08-16 at the client's call: on the page it read as a stray purple shape
   floating in the middle of the frame rather than as the thread joining the
   functions. The ribbon motif still does its job where it is legible as a
   deliberate mark, on the hero rule and on the left edge of each beat in the
   phone layout. The pills carry their own connection now, through the wave
   that lights them in sequence. */

/* The dock. Padding lives on the button, not the pill, so the whole pill is
   the hit target and the magnified pill has no dead border around it.

   NOTHING driven by --lit is transitioned. The wave writes --lit every frame
   from the scroll position, and a transition on background, border, box-shadow
   or the composed transform would smear the light a third of a second behind
   the scroll. Only --pop and --rise animate; see the @property note above. */
.chain-item {
  --lit: 0;
  --pop: 1;
  --rise: 0px;
  position: relative;
  padding: 0;
  border-radius: 999px;
  border: 1px solid rgba(108,76,224, calc(.10 + .46 * var(--lit)));
  background: rgba(255,255,255, calc(.34 + .46 * var(--lit)));
  box-shadow:
    inset 1px 1px 1px -.5px rgba(255,255,255,.88),
    inset -1px -1px 1px -.5px rgba(14,19,48,.14),
    0 calc(var(--lit) * 10px) calc(var(--lit) * 26px) rgba(108,76,224, calc(var(--lit) * .20));
  transform: translateY(calc(var(--lit) * -5px + var(--rise))) scale(var(--pop));
  transform-origin: 50% 100%;   /* a dock icon grows up out of the bar */
  backdrop-filter: blur(6px) saturate(140%);
  -webkit-backdrop-filter: blur(6px) saturate(140%);
  transition: --pop .3s cubic-bezier(.2,.7,.2,1),
              --rise .3s cubic-bezier(.2,.7,.2,1);
}

/* the lift glow. Its own layer, so it can transition freely without touching
   the --lit shadow it sits beside. */
.chain-item::after {
  content: '';
  position: absolute;
  inset: -7px;
  z-index: -1;
  border-radius: 999px;
  pointer-events: none;
  background: radial-gradient(60% 60% at 50% 60%, rgba(108,76,224,.30), rgba(108,76,224,0) 72%);
  opacity: 0;
  transition: opacity .3s ease;
}

.chain-open {
  display: block;
  width: 100%;
  margin: 0;
  padding: 11px clamp(13px, 1.4vw, 20px);
  border: 0;
  border-radius: inherit;
  background: none;
  font: inherit;
  color: inherit;
  cursor: default;
}
.detail-ready .chain-open { cursor: pointer; }

.chain-item span {
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(10px, .88vw, 13px);
  letter-spacing: .16em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink-soft);
  opacity: calc(.46 + .54 * var(--lit));
}

/* Dock magnification: the pointed-at pill takes the full pop and its
   neighbours take a decaying share, which is what makes the row feel like one
   elastic surface rather than six separate buttons. :has() reaches backwards
   to the pills BEFORE the hovered one; the sibling combinator reaches
   forwards. Each rule matches a different pill, so none of them collide. */
@media (hover: hover) and (pointer: fine) {
  .chain-item:hover                                  { --pop: 1.26; --rise: -10px; }
  .chain-item:hover + .chain-item,
  .chain-item:has(+ .chain-item:hover)               { --pop: 1.12; --rise: -5px; }
  .chain-item:hover + .chain-item + .chain-item,
  .chain-item:has(+ .chain-item + .chain-item:hover) { --pop: 1.05; --rise: -2px; }
  .chain-item:hover::after { opacity: 1; }
}
.chain-item:focus-within { --pop: 1.26; --rise: -10px; }
.chain-item:focus-within::after { opacity: 1; }
.chain-item:active { --pop: 1.18; --rise: -6px; }

/* ════════════════════════════════════════════════════════════════════════
   10 · THE FUNCTION CARD
   A native <dialog>, so the focus trap, Escape, the top layer and the
   inertness of the page behind it all come from the platform.

   THE ONE STRUCTURAL RULE HERE: .detail-card carries no backdrop-filter, no
   filter and no opacity of its own. Every one of those is a grouping property,
   and a grouping property forces transform-style back to flat — which would
   silently collapse the whole card into a single plane and throw away the
   depth that makes it immersive. The frosting lives on ::backdrop and on the
   .detail-glow pane instead, both of which sit beside the 3D layers rather
   than above them.
   ════════════════════════════════════════════════════════════════════════ */

.detail {
  margin: 0;
  padding: 0;
  border: 0;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  overflow: visible;
  background: transparent;
  perspective: 1400px;
}
.detail:not([open]) { display: none; }
/* THE SCROLLER LIVES HERE, NOT ON THE CARD, and that is not a style choice.
   `overflow` on the card would make it a scroll container, and a scroll
   container forces transform-style back to flat, which silently collapses the
   card's translateZ layers and throws away the depth this whole section is
   for. It also grew a horizontal scrollbar from the ribbon's bleed.
   Scrolling the dialog instead leaves the card free to stay in 3D.

   `safe center` so a card taller than the viewport scrolls from its top edge
   instead of having the top half clipped, which is what plain centring does
   inside a scroll container. Plain `center` is declared first for browsers
   that do not know the keyword. */
.detail[open] {
  display: grid;
  place-items: center;
  place-items: safe center;
  overflow: auto;
  padding: clamp(16px, 4vh, 48px) clamp(16px, 5vw, 48px);
}

/* pearl, never a dark scrim: the world on this page is bright and the type is
   dark ink, so dimming to black would invert the whole design language */
.detail::backdrop {
  background: rgba(242,244,248,.74);
  backdrop-filter: blur(16px) saturate(120%);
  -webkit-backdrop-filter: blur(16px) saturate(120%);
  animation: veil .4s ease both;
}
@keyframes veil { from { opacity: 0; } to { opacity: 1; } }

.detail-card {
  --rx: 0deg;
  --ry: 0deg;
  position: relative;
  /* widened from 680px when the panels grew a capability artifact and a 2x2
     offerings grid. The dialog owns the scroller (see the note above), so a
     taller card simply scrolls inside the veil. */
  width: min(880px, 100%);
  padding: clamp(30px, 4.4vw, 56px);
  border-radius: clamp(22px, 2.2vw, 32px);
  border: 1px solid rgba(255,255,255,.55);
  background: linear-gradient(150deg,
              rgba(255,255,255,.86) 0%,
              rgba(255,255,255,.66) 46%,
              rgba(232,236,244,.72) 100%);
  box-shadow:
    inset 3px 3px .5px -3px rgba(255,255,255,1),
    inset -3px -3px .5px -3px rgba(14,19,48,.24),
    inset 1px 1px 1px -.5px rgba(255,255,255,.9),
    0 2px 10px rgba(14,19,48,.08),
    0 44px 90px rgba(14,19,48,.16);
  transform-style: preserve-3d;
  transform: rotateX(var(--rx)) rotateY(var(--ry));
  transition: transform .18s ease-out;
  animation: card-in .55s cubic-bezier(.2,.7,.2,1);
}
/* MOVEMENT ONLY. This deliberately does not animate opacity, and that is the
   load-bearing part: an animation that starts at opacity 0 makes the card
   invisible for as long as the timeline is not advancing, and a timeline can
   fail to advance for reasons CSS cannot see. Chrome does not run animations
   in a tab it considers hidden, and `document.hidden` is true in more places
   than people expect (background tabs, prerender, some embedded webviews).
   The failure mode was an open dialog, a dimmed page and nothing on it.

   Moving only means the worst case is a card sitting slightly rotated instead
   of a card that is not there. The fade is carried by the ::backdrop veil
   behind it, which can be missing without costing the reader anything. */
@keyframes card-in {
  from { transform: rotateX(13deg) translateY(30px) scale(.93); }
  to   { transform: rotateX(0deg) translateY(0) scale(1); }
}

/* the depth stack. These are what make the card a space rather than a panel:
   the ribbon sits behind the surface, the type floats in front of it. */
/* Held INSIDE the card on purpose. Bleeding it past the edge pushed a
   horizontal scrollbar onto the dialog, and a ribbon escaping its surface is
   the exact "stray purple shape" the client asked to be rid of. Kept low
   enough to read as light on a surface rather than as an object. */
.detail-ribbon {
  position: absolute;
  left: 0;
  right: 0;
  top: 34%;
  height: clamp(80px, 12vw, 130px);
  border-radius: 999px;
  background: var(--ribbon);
  opacity: .12;
  filter: blur(34px);
  transform: translateZ(-40px);
  pointer-events: none;
}
.detail-glow {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(120% 80% at 20% 0%, rgba(255,255,255,.6), rgba(255,255,255,0) 60%);
  transform: translateZ(-2px);
}

.detail-panel[hidden] { display: none; }

.detail-kicker {
  margin: 0 0 clamp(10px, 1.4vh, 16px);
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(11px, .95vw, 14px);
  letter-spacing: .30em;
  text-transform: uppercase;
  color: var(--slate);
  transform: translateZ(38px);
}
.detail-kicker--on { color: var(--violet); }
.detail-panel .detail-kicker + .detail-line + .detail-kicker { margin-top: clamp(26px, 4vh, 40px); }

.detail-line {
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(21px, 2.6vw, 34px);
  line-height: 1.12;
  letter-spacing: -.02em;
  color: var(--ink);
  transform: translateZ(22px);
}
/* The resolved line is differentiated by DEPTH, not colour. The card is a real
   preserve-3d stage, so bringing this line 8px closer than the pain line above
   it does the work an accent colour would have done, without spending another
   dose of violet on a page whose accent budget is nearly all committed. */
.detail-line--fix { transform: translateZ(30px); }

/* ── the capability artifact ──────────────────────────────────────────────
   One per function. Pure SVG, drawn only from primitives this page already
   owns: lanes, pills, cells, hairlines, nodes and the ribbon. No rings, no
   orbits, no arcs — the dot-ring motif is rejected as a site device (see the
   design system in CLAUDE.md). No numbers, invented or otherwise.

   THE AUTHORING RULE, and the reason these survive `prefers-reduced-motion`:
   the BASE css state of every artifact is its RESOLVED frame. The keyframes
   travel away from that state and return to it. So killing the animation
   lands on the picture that carries the meaning, never on a broken first
   frame. Exactly one element per artifact moves; the rest is always drawn. */
.fn-art {
  margin: clamp(20px, 3vh, 30px) 0 0;
  padding: clamp(12px, 1.6vw, 18px);
  border: 1px solid rgba(255,255,255,.55);
  border-radius: clamp(14px, 1.4vw, 18px);
  background: linear-gradient(160deg, rgba(255,255,255,.52), rgba(232,236,244,.58));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.75);
  transform: translateZ(8px);
}
.fn-art svg { display: block; width: 100%; height: auto; }

/* Shared ink for the "already there" scaffolding of every artifact.

   SCOPED TO THE SCAFFOLDING GROUPS ON PURPOSE, and this is a trap worth
   naming. The lit part of each artifact takes its paint from a gradient
   referenced in the markup as fill="url(#…)" / stroke="url(#…)". Two things
   would break it if this rule were a blanket `.fn-art path, rect, circle`:

     1. CSS beats a presentation attribute, so `fill: none` would erase the
        filled shapes and `stroke: var(--slate)` would grey the ribbon ones.
     2. Moving the paint into CSS does NOT fix it. A url(#fragment) inside an
        external stylesheet resolves against the STYLESHEET's URL, so
        `url(#art-rib-mk)` written here means assets/app.css#art-rib-mk, which
        does not exist, and the element paints nothing at all.

   So the gradients stay in the markup and CSS never names a fill or stroke on
   an element that carries one. Only geometry, opacity and motion below. */
.fn-art .art-lane path,
.fn-art .art-step rect,
.fn-art .art-cell rect,
.fn-art .art-gate path,
.fn-art .art-node circle,
.fn-art .art-link path,
.fn-art .art-base { fill: none; stroke: var(--slate); stroke-width: 1.5; stroke-opacity: .30; }

/* stroked-not-filled, so they still need fill:none — never a stroke */
.fn-art .art-merge,
.fn-art .art-curve { fill: none; }
/* filled-not-stroked */
.fn-art .art-closed,
.fn-art .art-sweep,
.fn-art .art-pass { stroke: none; }

/* MARKETING · four channels converging into one campaign.
   The merge line redraws; the four lanes are always there. */
.fn-art--converge .art-merge {
  stroke-width: 3.5;
  stroke-opacity: 1;
  stroke-linecap: round;
  stroke-dasharray: 100;   /* the paths carry pathLength="100" */
  stroke-dashoffset: 0;
  animation: art-draw 4.2s cubic-bezier(.4,0,.2,1) infinite;
}
@keyframes art-draw { 0% { stroke-dashoffset: 100; } 55%, 100% { stroke-dashoffset: 0; } }

/* SALES · leads stepping down instead of sitting still.
   The staircase is always drawn; each tread lights in turn. */
.fn-art--steps .art-step rect { animation: art-lift 3.6s ease-in-out infinite; }
.fn-art--steps .art-step rect:nth-child(2) { animation-delay: .22s; }
.fn-art--steps .art-step rect:nth-child(3) { animation-delay: .44s; }
.fn-art--steps .art-step rect:nth-child(4) { animation-delay: .66s; }
@keyframes art-lift {
  0%, 60%, 100% { stroke-opacity: .30; }
  22%           { stroke-opacity: .95; }
}

/* OPERATIONS · the queue clearing faster than it fills.
   Base is the CLEARED grid with the sweep parked at the far edge. The bar
   travels back across and each column brightens as it is passed. Column index
   is (n-1) % 8 in a row-major 8x3 grid, so eight rules cover all 24 cells. */
.fn-art--queue .art-sweep { transform: translateX(309px); animation: art-sweep 4.4s ease-in-out infinite; }
@keyframes art-sweep { 0% { transform: translateX(0); } 70%, 100% { transform: translateX(309px); } }

.fn-art--queue .art-cell rect { animation: art-clear 4.4s ease-in-out infinite; }
.fn-art--queue .art-cell rect:nth-child(8n+2) { animation-delay: .09s; }
.fn-art--queue .art-cell rect:nth-child(8n+3) { animation-delay: .18s; }
.fn-art--queue .art-cell rect:nth-child(8n+4) { animation-delay: .27s; }
.fn-art--queue .art-cell rect:nth-child(8n+5) { animation-delay: .36s; }
.fn-art--queue .art-cell rect:nth-child(8n+6) { animation-delay: .45s; }
.fn-art--queue .art-cell rect:nth-child(8n+7) { animation-delay: .54s; }
.fn-art--queue .art-cell rect:nth-child(8n)   { animation-delay: .63s; }
@keyframes art-clear {
  0%          { stroke-opacity: .74; }
  16%         { stroke-opacity: .74; }
  30%, 100%   { stroke-opacity: .18; }
}

/* HR · one person crossing three gates, each lighting as they pass.
   Base is past the last gate with every gate lit. */
/* rests fully CLEAR of the last gate at x=224, not straddling it: the resting
   frame is the one reduced motion freezes on, and half-through a gate reads
   as stuck rather than as passed. */
.fn-art--gates .art-pass { transform: translateX(228px); animation: art-pass 4.6s cubic-bezier(.5,0,.3,1) infinite; }
@keyframes art-pass { 0% { transform: translateX(0); } 75%, 100% { transform: translateX(228px); } }

.fn-art--gates .art-gate path { stroke-opacity: .85; animation: art-gate 4.6s ease-in-out infinite; }
.fn-art--gates .art-gate path:nth-child(2) { animation-delay: .5s; }
.fn-art--gates .art-gate path:nth-child(3) { animation-delay: 1s; }
@keyframes art-gate { 0%, 16% { stroke-opacity: .18; } 34%, 100% { stroke-opacity: .85; } }

/* CUSTOM · loose parts locking into one lattice on a single beat.
   Base is locked and linked; the nodes scatter and return. */
.fn-art--lattice .art-node circle { stroke-opacity: .8; animation: art-snap 5s cubic-bezier(.2,.8,.2,1) infinite; }
.fn-art--lattice .art-node circle:nth-child(2n)  { animation-delay: .1s; }
.fn-art--lattice .art-node circle:nth-child(3n)  { animation-delay: .2s; }
.fn-art--lattice .art-node circle:nth-child(5n)  { animation-delay: .3s; }
@keyframes art-snap {
  0%        { transform: translate(9px, -7px); stroke-opacity: .22; }
  48%, 100% { transform: translate(0, 0);      stroke-opacity: .8; }
}
.fn-art--lattice .art-link path {
  stroke-dasharray: 100;
  stroke-dashoffset: 0;
  animation: art-link 5s cubic-bezier(.4,0,.2,1) infinite;
}
@keyframes art-link { 0%, 40% { stroke-dashoffset: 100; } 70%, 100% { stroke-dashoffset: 0; } }

/* CUSTOMER SUCCESS · the dip caught on the way down, not after.
   Base is the recovered curve with the marker sitting on the onset. */
.fn-art--signal .art-base { stroke-dasharray: 4 7; stroke-opacity: .22; }
.fn-art--signal .art-curve {
  stroke-width: 3;
  stroke-opacity: 1;
  stroke-linecap: round;
  stroke-dasharray: 100;
  stroke-dashoffset: 0;
  animation: art-signal 4.8s cubic-bezier(.4,0,.2,1) infinite;
}
@keyframes art-signal { 0% { stroke-dashoffset: 100; } 62%, 100% { stroke-dashoffset: 0; } }

.fn-art--signal .art-mark {
  fill: var(--violet);
  stroke: #fff;
  stroke-width: 2;
  stroke-opacity: .9;
  animation: art-mark 4.8s cubic-bezier(.3,.7,.2,1) infinite;
}
@keyframes art-mark {
  0%, 26%   { transform: translateX(-104px); opacity: 0; }
  46%, 100% { transform: translateX(0);      opacity: 1; }
}

/* ── the offerings ────────────────────────────────────────────────────────
   The named work behind each function. Every string is markup, never a JS
   object, so the spelling and stock-word audits still see it. */
.offer-eyebrow {
  margin: clamp(24px, 3.4vh, 34px) 0 clamp(11px, 1.5vh, 15px);
  padding-top: clamp(18px, 2.6vh, 26px);
  border-top: 1px solid rgba(90,98,138,.16);
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(10px, .82vw, 12px);
  letter-spacing: .30em;
  text-transform: uppercase;
  color: var(--slate);
  transform: translateZ(30px);
}

/* ONE column, always. The panel puts this in its own half of the card (see the
   two-column rule below), where a single file of cards is what makes the deal
   read as a deck rather than as a grid filling in. It also lets the ribbon walk
   run top to bottom, which is the direction the reader is already moving. */
.offers {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(8px, .9vw, 11px);
  margin: 0;
  padding: 0;
  list-style: none;
  transform: translateZ(16px);
  transform-style: preserve-3d;
}

/* ── the deal ─────────────────────────────────────────────────────────────
   The offerings arrive as a deck being dealt: collected in one pile, tilted
   like a held hand, then ejected one by one into their places. It is the same
   idea as the sticky stack further down the page, played at card scale and
   forwards instead of in reverse.

   THE AUTHORING RULE APPLIES HERE TOO, and it is the whole reason this is an
   entrance and not a loop: the BASE state of every tile is its RESOLVED place
   in the grid. The keyframe travels away from that and back. `backwards` fill
   holds the pile only while the tile is waiting for its turn, so killing the
   animation — reduced motion, a paused tab, a browser that never ran it —
   lands on the finished grid rather than on a heap in the corner.

   The pile sits on the FIRST tile's slot, so every later tile travels back up
   the column by its own index. The offset is a percentage of the tile itself,
   which is why this needs no measurement and no script: the deck lands in the
   right place at any card width, and it does not have to be exact — a deck is
   not a grid. */
.offers > * {
  --rot: 0deg;
  --nth: 0;
  animation: offer-deal .62s cubic-bezier(.2,.86,.28,1.02) backwards;
  animation-delay: calc(.18s + var(--nth) * .08s);
}
@keyframes offer-deal {
  from {
    opacity: 0;
    transform: translate3d(calc(var(--nth) * -6px), calc(var(--nth) * -106%), -70px)
               rotate(var(--rot))
               scale(.92);
  }
  24% { opacity: 1; }
}

/* the pile's fan. Alternating, small, and never the same angle twice running,
   so it reads as cards held in a hand rather than as a rotation applied. */
.offers > :nth-child(1) { --nth: 0; --rot: -4deg; }
.offers > :nth-child(2) { --nth: 1; --rot:  3deg; }
.offers > :nth-child(3) { --nth: 2; --rot: -2.2deg; }
.offers > :nth-child(4) { --nth: 3; --rot:  4.4deg; }
.offers > :nth-child(5) { --nth: 4; --rot: -3deg; }

/* Each tile samples a consecutive quarter of the ribbon along its left edge,
   so a function's four offerings read as one spectrum rather than four
   unrelated boxes. Painted as a BACKGROUND LAYER, not border-image: an
   ordinary gradient border would ring the whole tile. */
.offer {
  --pop: 1;
  --rise: 0px;
  --c1: var(--cyan);
  --c2: var(--violet);
  padding: clamp(12px, 1.4vw, 16px) clamp(13px, 1.5vw, 17px)
           clamp(13px, 1.5vw, 17px) clamp(17px, 1.9vw, 21px);
  border: 1px solid rgba(255,255,255,.62);
  border-radius: clamp(11px, 1.1vw, 14px);
  background:
    linear-gradient(180deg, var(--c1), var(--c2)) left center / 3px 100% no-repeat,
    linear-gradient(155deg, rgba(255,255,255,.80), rgba(255,255,255,.50));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.85), 0 1px 5px rgba(14,19,48,.05);
  /* lifts TOWARD the reader on the card's own 3D stage, rather than only up
     the page. --pop and --rise are the @property pair section 8 registers for
     the dock, reused here so the page has one interaction vocabulary and the
     transition never touches a transform carrying another term. */
  transform: translateZ(calc(12px - 1.6 * var(--rise)))
             translateY(var(--rise))
             scale(var(--pop));
  transition: --pop .3s cubic-bezier(.2,.7,.2,1),
              --rise .3s cubic-bezier(.2,.7,.2,1),
              box-shadow .3s ease,
              border-color .3s ease;
}
/* four tiles walk the ribbon in four equal steps, cyan to magenta exactly
   once. A function with a different number of offerings needs its own walk,
   or the set stops reading as one spectrum — hence the --five variant. */
.offers > :nth-child(1) { --c1: #35B8F2; --c2: #4E86EA; }
.offers > :nth-child(2) { --c1: #4E86EA; --c2: #6C4CE0; }
.offers > :nth-child(3) { --c1: #6C4CE0; --c2: #9A4AE4; }
.offers > :nth-child(4) { --c1: #9A4AE4; --c2: #C64BE8; }

.offers--five > :nth-child(1) { --c1: #35B8F2; --c2: #467FEB; }
.offers--five > :nth-child(2) { --c1: #467FEB; --c2: #5866E5; }
.offers--five > :nth-child(3) { --c1: #5866E5; --c2: #834CE2; }
.offers--five > :nth-child(4) { --c1: #834CE2; --c2: #A54AE5; }
.offers--five > :nth-child(5) { --c1: #A54AE5; --c2: #C64BE8; }

@media (hover: hover) and (pointer: fine) {
  .offer:hover {
    --pop: 1.028;
    --rise: -5px;
    border-color: rgba(108,76,224,.28);
    box-shadow: inset 0 1px 0 rgba(255,255,255,.9), 0 14px 30px rgba(14,19,48,.10);
  }
}

.offer-name {
  display: block;
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(14px, 1.15vw, 16px);
  letter-spacing: -.005em;
  color: var(--ink);
}
.offer-line {
  display: block;
  margin-top: 5px;
  font-family: var(--body);
  font-weight: 400;
  font-size: clamp(12.5px, 1vw, 14px);
  line-height: 1.42;
  color: var(--ink-soft);
}

/* ── the panel's two columns ──────────────────────────────────────────────
   The story on the left, the deck on the right. Before this the card was one
   tall column: the reader clicked a function and landed on a headline and a
   diagram, with every named offering below the fold. What the card is FOR was
   the part you had to scroll to find.

   The two zones are WRAPPED, not laid out from the flat sequence of paragraphs
   they used to be, and that is not tidiness. With every paragraph its own grid
   item, the offerings block has to span the story's rows, and a spanning item
   distributes its height back across the rows it spans — so a five-offering
   function stretched the gap between its own kicker and its own headline to
   four hundred pixels. Two wrappers give each column its own flow and the
   problem disappears.

   Declared as a width query, not a phone query: a narrow desktop window needs
   the same single-column answer a phone does. */
.detail-story > :first-child { margin-top: 0; }

@media (min-width: 800px) {
  .detail-panel:not([hidden]) {
    display: grid;
    grid-template-columns: minmax(0, .92fr) minmax(0, 1fr);
    column-gap: clamp(26px, 3.2vw, 44px);
    align-items: start;
    transform-style: preserve-3d;
  }
  .detail-story,
  .detail-run { transform-style: preserve-3d; }

  .detail-run .offer-eyebrow {
    /* the rule above it belonged to the stacked layout, where it separated the
       story from the offerings. Side by side there is nothing above it to
       separate from, and the border reads as a stray line. */
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
  }

  /* the headline now lives in half the width it was measured for */
  .detail-story .detail-line { font-size: clamp(20px, 1.85vw, 28px); }
}

.detail-cta {
  margin-top: clamp(28px, 4.4vh, 44px);
  transform: translateZ(46px);
}
/* The base .cta hover/active rules are a bare translateY, which would drop
   this button out of its Z-plane and animate the fall over 300ms inside a
   preserve-3d card. Both states have to restate the depth. */
.detail-cta:hover  { transform: translateZ(46px) translateY(-2px); }
.detail-cta:active { transform: translateZ(46px) translateY(1px); }

.detail-close {
  position: absolute;
  top: clamp(12px, 1.4vw, 18px);
  right: clamp(12px, 1.4vw, 18px);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  padding: 0;
  border: 1px solid rgba(90,98,138,.20);
  border-radius: 999px;
  background: rgba(255,255,255,.6);
  color: var(--ink-soft);
  cursor: pointer;
  transform: translateZ(46px);
  transition: background .25s ease, color .25s ease, border-color .25s ease;
}
.detail-close svg { width: 18px; height: 18px; }
.detail-close:hover {
  background: #fff;
  color: var(--ink);
  border-color: rgba(108,76,224,.38);
}

/* A modal on screen is BY DEFINITION being looked at, so the hidden-tab pause
   in section 2 must not reach it.

   This is not a nicety. `body.paused *` freezes every animation at whatever
   frame it was on, and card-in starts at opacity 0. A card opened while that
   class is set therefore renders as an open dialog with nothing inside it:
   the veil dims the page, and the reader is looking at an empty screen with no
   way to tell anything went wrong. Seen for real, because body.paused is
   seeded at boot from document.hidden and a page can be opened into a
   background tab.

   The wider rule this is an instance of: NOTHING the reader has to see may
   depend on an animation being allowed to run. Decoration can be paused; the
   content underneath it cannot.

   More specific than `body.paused *`, so it wins among the !important
   declarations rather than fighting them. */
body.paused .detail[open],
body.paused .detail[open] * { animation-play-state: running !important; }

/* showModal() blocks interaction with the page behind it but NOT scrolling,
   and what is behind this card is an 1800vh scroll-driven journey. Without
   this, a wheel flick over the veil scrubs the film underneath and the reader
   is dropped somewhere they never chose when the card closes. detail.js stops
   Lenis at the same moment; this pins the native scroller too. */
html.detail-open,
html.detail-open body { overflow: hidden; }

/* the tilt is pointer sugar; a reader who asked for less motion gets a card
   that simply sits still, and the entrance stops rotating too.

   Killing the artifact animations is safe precisely because each one was
   authored with its RESOLVED frame as the base CSS state — see the authoring
   rule above .fn-art. What is left is six finished pictures, not six broken
   first frames. The tiles keep their depth; they only lose the lift. */
@media (prefers-reduced-motion: reduce) {
  .detail-card { transform: none !important; animation: none; }
  .fn-art *    { animation: none !important; }
  .offer       { transition: none; }
  /* the deck is already dealt: the base state IS the finished grid */
  .offers > *  { animation: none !important; }
}

/* ════════════════════════════════════════════════════════════════════════
   10b · THE BOOKING CARD

   Cal.com in an iframe, carried in a second native <dialog>. It borrows the
   veil, the pearl surface and the close button from the function card above
   and then deliberately parts company on one point: NO preserve-3d, no tilt,
   no translateZ layers. An iframe inside a 3D context is a rendering coin
   toss across browsers, and there is nothing here to give depth to — the
   panel is a window onto someone else's page, not a surface of our own.

   Flat is also what lets this one own a scroller safely, which the function
   card explicitly cannot (see the note above .detail[open]).
   ════════════════════════════════════════════════════════════════════════ */

.cal {
  margin: 0;
  padding: 0;
  border: 0;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  overflow: visible;
  background: transparent;
}
.cal:not([open]) { display: none; }
.cal[open] {
  display: grid;
  place-items: center;
  padding: clamp(14px, 3.4vh, 40px) clamp(14px, 4vw, 40px);
}

/* pearl, never a dark scrim — same inversion as everything else on this page */
.cal::backdrop {
  background: rgba(242,244,248,.74);
  backdrop-filter: blur(16px) saturate(120%);
  -webkit-backdrop-filter: blur(16px) saturate(120%);
  animation: veil .4s ease both;
}

.cal-panel {
  position: relative;
  width: min(920px, 100%);
  height: min(86vh, 760px);
  border-radius: clamp(18px, 1.8vw, 26px);
  border: 1px solid rgba(255,255,255,.55);
  background: linear-gradient(150deg,
              rgba(255,255,255,.92) 0%,
              rgba(255,255,255,.78) 46%,
              rgba(232,236,244,.84) 100%);
  box-shadow:
    inset 3px 3px .5px -3px rgba(255,255,255,1),
    inset -3px -3px .5px -3px rgba(14,19,48,.24),
    inset 1px 1px 1px -.5px rgba(255,255,255,.9),
    0 2px 10px rgba(14,19,48,.08),
    0 44px 90px rgba(14,19,48,.16);
  overflow: hidden;
  animation: card-in .55s cubic-bezier(.2,.7,.2,1);
}

/* MOVEMENT ONLY, for the reason spelled out over @keyframes card-in: an
   entrance that starts at opacity 0 leaves an empty lit dialog on screen for
   as long as the timeline is not advancing, and timelines stall in more
   places than people expect. Worst case here is a panel that arrives already
   in position, which costs the reader nothing. */

.cal-frame {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  opacity: 0;
  transition: opacity .45s ease;
}
.cal.is-ready .cal-frame { opacity: 1; }

/* The wait. A skeleton rather than a spinner, and specifically NOT a ring:
   the ring was rejected as a motif on this site and a loading ring would walk
   it straight back in through the side door. A soft pearl sweep says the same
   thing and says it in this page's own language. */
.cal-skeleton {
  position: absolute;
  inset: 0;
  z-index: 0;
  background:
    linear-gradient(100deg,
      rgba(255,255,255,0)   28%,
      rgba(255,255,255,.72) 48%,
      rgba(255,255,255,0)   68%),
    var(--pearl-2);
  background-size: 220% 100%, auto;
  animation: cal-sweep 1.5s ease-in-out infinite;
  transition: opacity .35s ease;
}
@keyframes cal-sweep {
  from { background-position: 140% 0, 0 0; }
  to   { background-position: -40% 0, 0 0; }
}
.cal.is-ready .cal-skeleton { opacity: 0; pointer-events: none; }

.cal-close {
  position: absolute;
  top: clamp(10px, 1.2vw, 14px);
  right: clamp(10px, 1.2vw, 14px);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  padding: 0;
  border: 1px solid rgba(90,98,138,.20);
  border-radius: 999px;
  /* opaque, unlike the function card's: this one sits over a third party's
     page whose colours we do not control, so a translucent chip could land on
     anything. It has to stay findable. */
  background: rgba(255,255,255,.94);
  color: var(--ink-soft);
  cursor: pointer;
  box-shadow: 0 2px 12px rgba(14,19,48,.14);
  transition: background .25s ease, color .25s ease, border-color .25s ease;
}
.cal-close svg { width: 18px; height: 18px; }
.cal-close:hover {
  background: #fff;
  color: var(--ink);
  border-color: rgba(108,76,224,.38);
}

/* same reasoning as the function card: a modal on screen is by definition
   being looked at, so the hidden-tab freeze must not reach it */
body.paused .cal[open],
body.paused .cal[open] * { animation-play-state: running !important; }

/* the booking triggers are real links to the booking page until cal.js says
   otherwise, so the pointer affordance waits for the wiring the same way the
   function labels do */
html.cal-ready [data-cal] { cursor: pointer; }

/* phones get a bottom sheet, which is where a thumb already is */
@media (max-width: 600px) {
  .cal[open] { place-items: end center; padding: 0; }
  .cal-panel {
    height: 94vh;
    border-radius: clamp(18px, 4vw, 24px) clamp(18px, 4vw, 24px) 0 0;
    border-bottom: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cal-panel { animation: none; }
  .cal-skeleton { animation: none; }
}

/* ════════════════════════════════════════════════════════════════════════
   10b · THE ENQUIRY CARD
   The third dialog. Same veil, same panel material and same bottom sheet on a
   phone as the booking card above, because the two are the same object to a
   reader: a card that came up over the film and takes their details. Only the
   contents differ.

   Note what is NOT here: backdrop-filter, filter, opacity or mask on the panel
   itself. Those are the four grouping properties, and this panel does not need
   a 3D context so it does not have one to flatten. The frosting lives on
   ::backdrop, exactly as it does for the function card and for the same
   reason spelled out in CLAUDE.md.
   ════════════════════════════════════════════════════════════════════════ */

.enq {
  margin: 0;
  padding: 0;
  border: 0;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  overflow: visible;
  background: transparent;
}
.enq:not([open]) { display: none; }
.enq[open] {
  display: grid;
  place-items: center;
  padding: clamp(14px, 3.4vh, 40px) clamp(14px, 4vw, 40px);
}

.enq::backdrop {
  background: rgba(242,244,248,.74);
  backdrop-filter: blur(16px) saturate(120%);
  -webkit-backdrop-filter: blur(16px) saturate(120%);
  animation: veil .4s ease both;
}

.enq-panel {
  position: relative;
  width: min(560px, 100%);
  max-height: 86vh;
  overflow-y: auto;
  overscroll-behavior: contain;   /* a scroll that runs out here stops here */
  /* Tight on purpose. At 1568x745 the untightened version overflowed its own
     86vh by a few pixels and put a scrollbar down the side of a card that had
     nothing to scroll to, which reads as a bug rather than as a long form. It
     still scrolls when it genuinely has to, which is what a short laptop and a
     landscape phone both need. */
  padding: clamp(22px, 3.2vw, 34px) clamp(20px, 3.4vw, 38px) clamp(20px, 2.8vw, 28px);
  border-radius: clamp(18px, 1.8vw, 26px);
  border: 1px solid rgba(255,255,255,.55);
  background: linear-gradient(150deg,
              rgba(255,255,255,.94) 0%,
              rgba(255,255,255,.82) 46%,
              rgba(232,236,244,.88) 100%);
  box-shadow:
    inset 3px 3px .5px -3px rgba(255,255,255,1),
    inset -3px -3px .5px -3px rgba(14,19,48,.24),
    inset 1px 1px 1px -.5px rgba(255,255,255,.9),
    0 2px 10px rgba(14,19,48,.08),
    0 44px 90px rgba(14,19,48,.16);
  /* MOVEMENT ONLY. Same rule as .cal-panel: an entrance that starts at
     opacity 0 leaves an empty lit dialog on screen whenever the animation
     does not run, and animations fail to run in more places than people
     expect. Worst case here is a panel that arrives already in place. */
  animation: card-in .55s cubic-bezier(.2,.7,.2,1);
}

.enq-h {
  margin: 0 0 6px;
  padding-right: 46px;          /* clear of the close button */
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(21px, 2.2vw, 28px);
  line-height: 1.12;
  letter-spacing: -.015em;
  color: var(--ink);
}
.enq-sub {
  margin: 0 0 clamp(14px, 2.2vh, 22px);
  font-family: var(--body);
  font-size: clamp(13px, 1vw, 15px);
  color: var(--slate);
}

/* ── THE FIELDS ───────────────────────────────────────────────────────────
   No boxes. A rounded, filled, bordered input is the single most dated thing
   a form can put on a page, and it is also the one shape this entire site
   does not otherwise contain: everything here is hairlines, drawn strokes and
   type on pearl. So the fields are hairlines and type on pearl too.

   The whole interaction is one gesture: the label takes the accent, and the
   grey hairline is replaced by the ribbon DRAWING left to right in the
   reading direction. Nothing else moves. That is the same restraint the dock
   and the offering tiles are built on.

   This is the ribbon's sixth sanctioned appearance and, like every one added
   since the glow was removed, it is a hard-edged solid stroke. It is 2px, it
   has an edge, and it never blurs. */

.enq-row {
  position: relative;
  margin-bottom: clamp(16px, 2.3vh, 24px);
}

.enq-label {
  display: block;
  margin-bottom: 7px;
  font-family: var(--display);
  font-weight: 500;
  font-size: 10px;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--slate);
  transition: color .3s ease;
}
.enq-row:focus-within .enq-label { color: var(--violet); }

.enq-input {
  display: block;
  width: 100%;
  padding: 6px 0 11px;
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(16px, 1.35vw, 19px);   /* 16px floor is not taste: below it
                                             iOS zooms the viewport on focus,
                                             and this page is a pinned film
                                             that does not come back. */
  line-height: 1.35;
  letter-spacing: -.005em;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid rgba(90,98,138,.26);
  border-radius: 0;
  transition: border-color .3s ease;
}
.enq-input:hover { border-bottom-color: rgba(90,98,138,.44); }
.enq-input:focus {
  outline: none;
  /* handed over to the ribbon below, which is arriving as this leaves */
  border-bottom-color: transparent;
}

/* And straight back again for the keyboard.
   `.enq-input:focus` is two components of specificity against the page's
   global one-component `:focus-visible` rule, so without this it silently
   won a fight it was never meant to be in and left keyboard readers with no
   focus indicator at all inside a modal that traps focus. The ribbon is a
   state change, not an indicator: it is 2px, it is decorative at the cyan
   end, and it is not what anyone tabbing through this form is looking for.
   Same ring as everywhere else on the page on purpose. */
.enq-input:focus-visible {
  outline: 3px solid var(--violet);
  outline-offset: 3px;
}

/* The ribbon, drawn. transform-origin is left because the field is read left
   to right and a stroke that grows from the centre reads as a decoration
   rather than as an answer to the caret landing. scaleX only, so this is one
   composited property and never a layout. */
.enq-row::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--ribbon);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .5s cubic-bezier(.2,.7,.2,1);
}
.enq-row:focus-within::after { transform: scaleX(1); }

/* Height is set here rather than left to the rows attribute, and that is what
   keeps the whole card inside 86vh on a 745px-tall laptop. The optional field
   is the one that gives up the room, and resize:vertical hands it straight
   back to anyone who actually wants to write something long. */
.enq-area {
  resize: vertical;
  height: 64px;
  min-height: 64px;
}

/* The select keeps its native menu on purpose: a hand-rolled listbox is a
   focus-management problem inside a dialog that already has a focus trap, and
   the platform one is correct on every device this page will meet. Only the
   closed state is restyled, down to a chevron drawn from two gradients so it
   costs no markup and no request. */
.enq-select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 28px;
  cursor: pointer;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--slate) 50%),
    linear-gradient(135deg, var(--slate) 50%, transparent 50%);
  background-position: calc(100% - 11px) calc(50% - 1px), calc(100% - 6px) calc(50% - 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

/* Off screen, not display:none. A bot that renders CSS skips hidden fields and
   fills positioned ones, which is the entire trick. */
.enq-pot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.enq-error,
.enq-status {
  margin: 0 0 12px;
  font-family: var(--body);
  font-size: 13px;
  line-height: 1.45;
}
.enq-error  { color: #B3283F; }   /* 5.6:1 on the panel. The one red on this
                                     page, and it is a state, not an accent. */
.enq-status { color: var(--ink-soft); }

.enq-send {
  width: 100%;
  margin-top: clamp(6px, 1.2vh, 12px);
  border: 0;
  cursor: pointer;
  font-family: var(--display);
}
.enq-send:disabled {
  cursor: default;
  filter: saturate(60%) brightness(1.05);
}

.enq-close {
  position: absolute;
  top: clamp(10px, 1.2vw, 14px);
  right: clamp(10px, 1.2vw, 14px);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  padding: 0;
  border: 1px solid rgba(90,98,138,.20);
  border-radius: 999px;
  background: rgba(255,255,255,.72);
  color: var(--ink-soft);
  cursor: pointer;
  transition: background .25s ease, color .25s ease, border-color .25s ease;
}
.enq-close svg { width: 18px; height: 18px; }
.enq-close:hover {
  background: #fff;
  color: var(--ink);
  border-color: rgba(108,76,224,.38);
}

/* a modal on screen is by definition being looked at, so the hidden-tab
   freeze must not reach it */
body.paused .enq[open],
body.paused .enq[open] * { animation-play-state: running !important; }

/* the enquiry triggers are real mailto links until form.js says otherwise, so
   the pointer affordance waits for the wiring exactly as the other two do */
html.form-ready [data-form] { cursor: pointer; }

@media (max-width: 600px) {
  .enq[open] { place-items: end center; padding: 0; }
  .enq-panel {
    width: 100%;
    max-height: 94vh;
    border-radius: clamp(18px, 4vw, 24px) clamp(18px, 4vw, 24px) 0 0;
    border-bottom: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .enq-panel { animation: none; }
}

/* ════════════════════════════════════════════════════════════════════════
   11 · CUE AND LOADER
   ════════════════════════════════════════════════════════════════════════ */

.cue {
  position: absolute;
  left: 50%;
  bottom: clamp(20px, 3.4vh, 38px);
  z-index: 8;
  width: 22px; height: 36px;
  margin-left: -11px;
  border: 1.5px solid rgba(90,98,138,.42);
  border-radius: 999px;
  opacity: 1;
  transition: opacity .5s ease;
}
.cue span {
  position: absolute;
  left: 50%; top: 8px;
  width: 3px; height: 7px;
  margin-left: -1.5px;
  border-radius: 999px;
  background: var(--violet);
  animation: cue 1.9s cubic-bezier(.4,0,.2,1) infinite;
}
@keyframes cue {
  0%, 12%   { transform: translateY(0);    opacity: 0; }
  28%       { opacity: 1; }
  72%, 100% { transform: translateY(13px); opacity: 0; }
}
.stage.moved .cue { opacity: 0; }

.loader {
  position: absolute;
  left: 50%; top: 50%;
  z-index: 9;
  width: 44px; height: 44px;
  margin: -22px 0 0 -22px;
  color: var(--violet);
  opacity: .9;
  transform: rotate(-90deg);
  transition: opacity .5s ease;
}
.loader circle { transition: stroke-dashoffset .2s linear; }
.stage.video-ready .loader,
.stage.video-failed .loader { opacity: 0; pointer-events: none; }

/* ════════════════════════════════════════════════════════════════════════
   12 · EVERYTHING WE RUN, HOW IT RUNS, ARRIVAL AND FOOTER

   Everything in this section sits AFTER the pinned stage. Nothing here can
   reach the spine, the bands or the video clock, and nothing here may be
   moved above .journey: a sticky child inside an 1800vh ScrollTrigger pin
   fights the pin.
   ════════════════════════════════════════════════════════════════════════ */

.index {
  padding: clamp(72px, 13vh, 140px) var(--gut) clamp(40px, 7vh, 80px);
}

.index-h,
.process-h,
.faq-h {
  max-width: 940px;
  margin: 0 auto clamp(30px, 5vh, 56px);
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(26px, 3.4vw, 46px);
  line-height: 1.1;
  letter-spacing: -.022em;
  color: var(--ink);
}

/* ── THE STACK ────────────────────────────────────────────────────────────
   Six cards collecting into a real deck.

   There is no script here and no timeline. Each card is position:sticky and
   parks a fixed step lower than the one before it, so the scroll itself does
   the stacking. That is the whole reason it reads as paper rather than as an
   effect playing: nothing is being animated, the cards are just physically in
   the way of each other.

   --i is set in the markup, 0 to 5. It drives three things at once, and they
   have to agree or the deck stops looking like a deck:
     top    · later cards park lower, leaving a readable strip of the one below
     scale  · earlier cards end up DEEPER, so they sit fractionally smaller
     shadow · depth grows with the stack, which is what sells the thickness */
.stack {
  max-width: 940px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

.stack-card {
  position: sticky;
  top: calc(var(--navh) + clamp(10px, 2vh, 22px) + var(--i) * clamp(12px, 1.7vh, 20px));
  margin: 0 0 clamp(26px, 5vh, 58px);
  padding: clamp(24px, 3.2vw, 44px);
  border: 1px solid rgba(255,255,255,.75);
  border-radius: clamp(18px, 2vw, 28px);
  /* OPAQUE, unlike every other surface on this page. A translucent card lets
     the card beneath it read straight through, and two overlapping headlines
     at once is not a deck, it is a rendering fault. Paper is opaque. */
  background: linear-gradient(150deg, #FFFFFF 0%, #FAFBFD 48%, #EDF0F6 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.92),
    0 1px 2px rgba(14,19,48,.05),
    0 calc(10px + var(--i) * 4px) calc(26px + var(--i) * 9px)
      rgba(14,19,48, calc(.07 + var(--i) * .011));
  transform: scale(calc(1 - (5 - var(--i)) * .008));
}
.stack-card:last-child { margin-bottom: 0; }

.stack-kicker {
  margin: 0 0 clamp(8px, 1.2vh, 13px);
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(11px, .95vw, 13px);
  letter-spacing: .30em;
  text-transform: uppercase;
  color: var(--slate);
}

.stack-line {
  margin: 0 0 clamp(18px, 2.6vh, 26px);
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(19px, 2.1vw, 28px);
  line-height: 1.14;
  letter-spacing: -.018em;
  color: var(--ink);
}

.stack-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: clamp(7px, 1vh, 11px) clamp(14px, 2vw, 28px);
  margin: 0;
  padding-top: clamp(15px, 2.2vh, 22px);
  border-top: 1px solid rgba(90,98,138,.16);
  list-style: none;
}
.stack-list li {
  position: relative;
  padding-left: 18px;
  font-family: var(--body);
  font-weight: 400;
  font-size: clamp(13.5px, 1.05vw, 15px);
  color: var(--ink-soft);
}
.stack-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .62em;
  width: 9px;
  height: 1.5px;
  border-radius: 1px;
  background: var(--slate);
  opacity: .5;
}

/* ── HOW IT RUNS ──────────────────────────────────────────────────────────
   Numbered because it genuinely is a sequence and the order is information.
   A counter, not a hand-typed 01/02/03, so inserting a step cannot desync. */
.process {
  padding: clamp(50px, 9vh, 100px) var(--gut) clamp(60px, 10vh, 110px);
}

.steps {
  display: grid;
  /* 180px, not 210: at the 940px measure with a 40px gap each column gets
     205px, so a 210px floor silently drops the row to three and orphans Run
     on a second line. A four-step sequence has to read as one row. */
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: clamp(22px, 3vw, 40px);
  max-width: 940px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
  counter-reset: step;
}

.step {
  counter-increment: step;
  padding-top: clamp(16px, 2.4vh, 22px);
  border-top: 1px solid rgba(90,98,138,.20);
}

/* ── 12b · THE FLOW LINE ──────────────────────────────────────────────────
   Audit → Design → Build → Run is a sequence, and until now the only thing
   saying so was the numbering. This draws it.

   THE AUTHORING RULE, which is the same one the function card's artifacts are
   built on and the reason this works with motion off: the BASE state below is
   the RESOLVED frame. The line is fully drawn, the arrow has landed, the four
   steps are lit. The keyframes travel AWAY from that and back to it. So a
   browser with no scroll timelines (Safari and Firefox, today), a reader who
   asked for reduced motion, and a tab that never ticks all land on the
   finished picture rather than on an empty rail.

   No script, no rAF, no timeline object, in the same spirit as the sticky
   stack above: the browser already knows where the section is on screen, and
   asking it is cheaper and more robust than telling it.

   --flow is registered so it can be interpolated at all; an unregistered
   custom property animates as a discrete swap and the line would jump rather
   than draw. */

@property --flow { syntax: '<percentage>'; inherits: true; initial-value: 100%; }

.flow {
  --flow: 100%;
  position: relative;
  max-width: 940px;
  margin: 0 auto clamp(22px, 3vh, 34px);
  height: 2px;
  background: rgba(90,98,138,.20);   /* the unrun track */
}

/* The drawn part. clip-path rather than width, so this is composited and
   never a layout, and so the gradient stays anchored to the FULL length: the
   ribbon has to read as one spectrum being revealed, not as a short gradient
   being stretched. Changing this to `width` breaks that and it is not
   obvious from a screenshot. */
.flow-run {
  position: absolute;
  inset: 0;
  background: var(--ribbon);
  clip-path: inset(0 calc(100% - var(--flow)) 0 0);
}

/* The arrowhead. Two borders rotated 45deg: no SVG, no request, no markup
   beyond the span already there. It rides the SAME --flow the run is clipped
   by, so it cannot drift out of sync with the end of the line.

   It sits OUTSIDE .flow-run on purpose. Inside it, the clip that draws the
   line would cut the arrow in half all the way along. */
.flow::after {
  content: '';
  position: absolute;
  top: 50%;
  left: var(--flow);
  width: 8px;
  height: 8px;
  margin: -5px 0 0 -6px;
  border-top: 2px solid var(--magenta);
  border-right: 2px solid var(--magenta);
  transform: rotate(45deg);
}

@keyframes flow-draw {
  from { --flow: 0%; }
  to   { --flow: 100%; }
}

/* How lit a step is, 0 unreached and 1 passed. Registered as a <number> so it
   interpolates; ::before reads it to take its accent, which is how the numeral
   warms from --slate to --violet under the arrowhead without needing a second
   animation-range list kept in sync with the one on .step. Base is 100% — the
   resolved frame — so nothing about this depends on the animation running.
   A <percentage> rather than a <number>, because it is fed straight to
   color-mix as its mixing percentage and Chrome rejects a calc() there — a
   `calc(var(--land) * 100%)` on a <number> makes the whole colour declaration
   invalid at computed-value time, and the numeral silently inherits --ink
   instead. It looks like nothing is wrong until you compare it to --violet. */
@property --land { syntax: '<percentage>'; inherits: true; initial-value: 100%; }

/* dim, a few pixels low, and its numeral still grey, so the step settles and
   takes its accent exactly as the line reaches it */
@keyframes step-land {
  from { --land: 0%;   opacity: .34; transform: translateY(7px); }
  to   { --land: 100%; opacity: 1;   transform: none; }
}

/* The line only exists where the four steps are one row. Below 860px the grid
   has wrapped to a stack and a horizontal rail through it would be saying
   something untrue, so it goes away and each step takes its own rule back. */
@media (max-width: 859px) {
  .flow { display: none; }
}
@media (min-width: 860px) {
  .step { border-top: 0; }
}

/* Everything above is the resolved state. THIS is the only block that moves,
   and it is gated twice: once on the browser having scroll timelines at all,
   once on the reader not having asked for less. */
@supports (animation-timeline: view()) {
  @media (min-width: 860px) and (prefers-reduced-motion: no-preference) {
    /* ONE named timeline, belonging to the line, hoisted to the section so the
       steps can borrow it. This is the whole trick, and it replaced four
       hand-guessed `view()` ranges that only LOOKED synced.
       `view()` is per-element: .flow and each .step are at different heights
       and different points down the page, so each got its own timeline and
       "cover 22%" meant a different scroll position for every one of them. The
       arrow and the step it was meant to light drifted apart, and by exactly
       how much depended on the window height. Sharing one timeline makes the
       sync arithmetic instead of eyeballing. */
    .process { timeline-scope: --flowline; }
    .flow    { view-timeline-name: --flowline; view-timeline-axis: block; }

    .flow {
      animation: flow-draw linear both;
      animation-timeline: --flowline;
      animation-range: cover 0% cover 55%;
    }

    /* Now that the steps share the line's clock, each one's window is just the
       slice of the draw during which the arrowhead is crossing its column.
       The draw spans cover 0-55%, the arrow travels 0-100% of a 940px measure
       and the four columns are quarters of it, so step N runs from
       (N-1)/4 * 55% to N/4 * 55%. Quarters rather than the real column edges
       because .steps has a fluid gap: quarters are within a couple of percent
       at every width and cannot go stale, and the slack lands in the gutters
       between columns where nothing is watching.
       Delays would not work here: a scroll-driven animation has no clock to
       delay against. */
    .step {
      animation: step-land linear both;
      animation-timeline: --flowline;
    }
    .step:nth-child(1) { animation-range: cover 0%     cover 13.75%; }
    .step:nth-child(2) { animation-range: cover 13.75% cover 27.5%;  }
    .step:nth-child(3) { animation-range: cover 27.5%  cover 41.25%; }
    .step:nth-child(4) { animation-range: cover 41.25% cover 55%;    }
  }
}
.step::before {
  content: counter(step, decimal-leading-zero);
  display: block;
  margin-bottom: clamp(10px, 1.6vh, 15px);
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(11px, .95vw, 13px);
  letter-spacing: .22em;
  color: color-mix(in oklab, var(--violet) var(--land), var(--slate));
}
.step-name {
  margin: 0 0 6px;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(17px, 1.7vw, 22px);
  letter-spacing: -.015em;
  color: var(--ink);
}
.step-line {
  margin: 0;
  font-family: var(--body);
  font-weight: 400;
  font-size: clamp(13.5px, 1.05vw, 15px);
  line-height: 1.5;
  color: var(--ink-soft);
}

/* A reader who asked for less motion gets the deck laid out flat. The stack
   is layout rather than animation, but it is still scroll-coupled movement,
   and the content does not need it to be understood. */
@media (prefers-reduced-motion: reduce) {
  .stack-card { position: static; transform: none; }
}

/* ── 12c · QUESTIONS ────────────────────────────────────────────
   Seven answers, all of them visible, none of them behind a click.

   Nothing new is invented here: the measure is the page's own 940px, the
   heading is the same rule as EVERYTHING WE RUN and FROM FIRST CALL, the
   hairline is the same rgba(90,98,138,.20) the process steps carry, and the
   type sizes are the step name and the step line one notch up for reading
   length. No accent, no ribbon, no card, no shadow. The ribbon's appearance
   list is closed and a Q&A block is not on it.

   Two columns above 860px because seven short answers in one column is a very
   long thin strip on a wide screen. The wrap is by column-count rather than
   grid, so the pairs stay together and the break falls between items. */
.faq {
  padding: clamp(50px, 9vh, 100px) var(--gut) clamp(20px, 3vh, 40px);
}

.faq-list {
  max-width: 940px;
  margin: 0 auto;
}

.faq-item {
  padding-top: clamp(16px, 2.4vh, 22px);
  margin-bottom: clamp(22px, 3.4vh, 34px);
  border-top: 1px solid rgba(90,98,138,.20);
  /* keeps a question and its answer in the same column when the list wraps */
  break-inside: avoid;
}

.faq-q {
  margin: 0 0 8px;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(16px, 1.5vw, 20px);
  line-height: 1.25;
  letter-spacing: -.015em;
  color: var(--ink);
}

.faq-a {
  margin: 0;
  font-family: var(--body);
  font-weight: 400;
  font-size: clamp(14px, 1.1vw, 16px);
  line-height: 1.6;
  color: var(--ink-soft);
  max-width: 62ch;
}

@media (min-width: 861px) {
  .faq-list {
    columns: 2;
    column-gap: clamp(28px, 4vw, 56px);
  }
  /* the first item in a column must not lose its rule to the column break */
  .faq-item { margin-bottom: 0; padding-bottom: clamp(22px, 3.4vh, 34px); }
}

.arrival {
  position: relative;
  min-height: 92vh;
  display: grid;
  place-items: center;
  padding: clamp(64px, 12vh, 140px) var(--gut);
  text-align: center;
}

.arrival-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(28px, 4.5vh, 48px);
}

.arrival-mark { margin: 0; }

.foot {
  padding: clamp(26px, 4vh, 44px) var(--gut) clamp(34px, 5vh, 56px);
  text-align: center;
  border-top: 1px solid rgba(90,98,138,.16);
}
.foot p {
  margin: 0;
  font-family: var(--body);
  font-size: 13px;
  letter-spacing: .04em;
  color: var(--slate);
}

/* The address again, down here. The nav copy is hidden on a narrow bar (see
   §13) because a phone nav cannot carry a mark, an address and two actions at
   once, and an address that only exists on a desktop is an address half the
   readers never get. This is the one that is always reachable. */
.foot-mail {
  color: var(--slate);
  text-decoration: none;
  border-bottom: 1px solid rgba(90,98,138,.28);
  transition: color .3s ease, border-color .3s ease;
}
.foot-mail:hover {
  color: var(--ink-soft);
  border-bottom-color: rgba(90,98,138,.6);
}

/* ════════════════════════════════════════════════════════════════════════
   13 · THE PHONE CUT

   NOT a different site. Client instruction 2026-08-16: "the mobile is
   completely different from what you have made ... I want to see the exact
   scroll-driven website, the scrolling effect that we are seeing on the
   desktop, on the mobile as well."

   They were right, and the old build earned it: every phone hit the
   static-hero gate, which disarmed the scrub, unpinned the stage, hid the
   video and stacked the sixteen beats as pearl cards. A reader on a phone was
   looking at a different product.

   So this block reframes and never replaces. It does not touch
   position: sticky, does not set .journey height to auto, and does not hide
   the video. The pinned stage, the scrubbed footage, the sixteen beats and
   the piecewise VIDEO_MAP are all exactly what the desktop runs. What changes
   is composition, because a 375x812 slot is not a 16:9 one:

     · the left and right rails give up their horizontal OFFSET but keep their
       ALIGNMENT, which is the thing that still reads pain from resolution;
     · the three chaos beats trade three quadrants for three distinct HEIGHTS,
       since a narrow frame has thirds to spend vertically and none across;
     · the type grows, because the frame shrank;
     · the local scrim widens and deepens, because the footage now fills a
       much tighter window behind much larger words.

   The static stacked fallback still exists. It just belongs to
   prefers-reduced-motion alone now (13b), which is the only reader who ever
   asked not to be moved.
   ════════════════════════════════════════════════════════════════════════ */

@media (max-width: 860px), (orientation: portrait) and (max-width: 1024px) {

  /* svh, not vh. On a phone `vh` is the LARGE viewport and stays frozen there
     while the URL bar collapses, but .stage is already sized in svh — so the
     spine and the pinned frame would be measured against two different
     heights and the scrub would drift against the film. */
  .journey { height: 1800svh; }

  /* The footage is a 16:9 plate seen through a tall slot, so `cover` throws
     away most of its width. Aim the surviving column slightly above centre,
     where the room's window wall and the conference glass actually are,
     rather than at the floor. */
  .poster { background-position: 50% 44%; }
  .media video { object-position: 50% 44%; }

  /* one column between the gutters */
  .band--hero {
    left: var(--gut); right: var(--gut);
    top: 50%; transform: translateY(-50%);
    max-width: none;
    --sx: 50%; --sy: 50%;
  }
  .band--pain {
    left: var(--gut); right: var(--gut);
    top: 50%; transform: translateY(-50%);
    max-width: none;
    --sx: 46%; --sy: 50%;
  }
  .band--fix {
    left: var(--gut); right: var(--gut);
    top: 50%; transform: translateY(-50%);
    max-width: none;
    --sx: 54%; --sy: 50%;
  }

  /* Two classes each, and that is not decoration. .band--fn.pos-a out-specifies
     any bare .band rule, so anything meant to reach these beats has to match
     that weight or the desktop quadrant offsets survive into the phone frame
     and shove the words off the edge. Same trap CLAUDE.md flags for the
     reduced-motion block below; it applies here for the same reason. */
  .band--fn.pos-a { left: var(--gut); right: var(--gut); top: 21vh; bottom: auto; --sx: 40%; --sy: 30%; --depth: 1;   }
  .band--fn.pos-b { left: var(--gut); right: var(--gut); top: 46vh; bottom: auto; --sx: 60%; --sy: 50%; --depth: .92; }
  .band--fn.pos-c { left: var(--gut); right: var(--gut); top: auto; bottom: 19vh; --sx: 42%; --sy: 74%; --depth: 1;   }

  .band--chain,
  .band--close {
    left: var(--gut); right: var(--gut);
    top: 50%; transform: translateY(-50%);
    width: auto;
  }

  /* The wash has to work harder here: the same footage now fills a window a
     third as wide, so a dark object under the type has nowhere to fall out of
     frame. Wider bleed, deeper centre, still pearl and never a dark scrim. */
  .band::before {
    inset: -20vh -26vw;
    background: radial-gradient(76% 62% at var(--sx, 50%) var(--sy, 50%),
                rgba(242,244,248,.94) 0%,
                rgba(242,244,248,.80) 46%,
                rgba(242,244,248,0)   84%);
  }

  .lede        { font-size: clamp(32px, 9vw, 52px); }
  .lede--close { font-size: clamp(26px, 7.2vw, 40px); }
  .say         { font-size: clamp(26px, 7.6vw, 40px); }
  .chain-title { font-size: clamp(22px, 6.4vw, 34px); }
  .sub         { font-size: clamp(15px, 4.2vw, 19px); }
  .kicker      { font-size: 11px; letter-spacing: .24em; }
  .band--fn .fn { font-size: calc(clamp(22px, 6.6vw, 34px) * var(--depth, 1)); }

  /* the halo STAYS. Unlike the reduced-motion cut below, these words really
     are sitting on live footage. */

  .chain-head { margin-bottom: clamp(16px, 3vh, 28px); }
  .chain { gap: 7px; }
  .chain-open { padding: 9px 12px; }
  .chain-item span { font-size: 10px; letter-spacing: .11em; }

  /* the nav has to fit a mark and two actions on a narrow bar */
  .nav-actions { gap: 8px; }
  .ghost { min-height: 42px; padding: 11px 14px; font-size: 12px; letter-spacing: .04em; }
  .cta--nav { padding: 11px 16px; font-size: 12px; min-height: 42px; }
  .brand-plate img { height: 44px; }
  .brand-plate--lg img { height: min(96px, 38vw); }

  .detail-card { width: 100%; }
  .arrival { min-height: auto; padding-top: clamp(48px, 9vh, 90px); }
}

/* A phone on its side: the frame keeps its width and loses its height, so the
   only thing that has to give is vertical room. The composition is the
   desktop one again; just smaller. */
@media (orientation: landscape) and (max-height: 560px) {
  .lede        { font-size: clamp(24px, 4.8vw, 38px); }
  .lede--close { font-size: clamp(20px, 3.8vw, 30px); }
  .say         { font-size: clamp(20px, 3.9vw, 30px); }
  .chain-title { font-size: clamp(18px, 3.4vw, 26px); }
  .sub         { font-size: clamp(14px, 1.7vw, 16px); }
  .band--fn.pos-a { top: 15vh; }
  .band--fn.pos-b { top: 42vh; }
  .band--fn.pos-c { bottom: 11vh; }
  .cue { display: none; }
  .chain-head { margin-bottom: 14px; }
}

/* ════════════════════════════════════════════════════════════════════════
   13b · THE STATIC CUT — reduced motion only

   This query is duplicated CHARACTER-FOR-CHARACTER from GATES in app.js, and
   it is now the ONLY entry in that list. If the two sides drift, the CSS
   un-hides a stage the JS never armed and the hero goes blank.

   It used to carry four more queries and every phone matched one of them.
   That is what made mobile a different site, and it was removed on 2026-08-16
   at the client's instruction. A reader who has asked their operating system
   for less motion is a different case entirely: for them the pinned scrub
   becomes a real vertical journey, one beat per screen, each on its own pearl
   card over the room.
   ════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {

  /* the drive loop is disarmed, but it may have left inline values behind on a
     live preference flip, so these have to win outright */
  .band {
    --o: 1 !important;
    --k: 1 !important;
    visibility: visible !important;
  }
  .chain-item { --lit: 1 !important; }
  .w, .c { opacity: 1 !important; transform: none !important; will-change: auto; }

  .journey { height: auto; }

  .stage {
    position: static;
    height: auto;
    overflow: visible;
  }

  /* the room becomes the fixed backdrop the beats travel over */
  .media { position: fixed; z-index: -1; }
  .media video, .media canvas { display: none; }
  .poster { position: fixed; transform: none; }

  /* Each beat already sits on its own card, so the global wash only has to keep
     the nav and the page edges calm. Kept light on purpose: the room is the
     premise of the page, and a heavy scrim erases it. */
  .scrim {
    position: fixed;
    z-index: -1;
    background: linear-gradient(180deg,
                rgba(242,244,248,.62) 0%,
                rgba(242,244,248,.34) 40%,
                rgba(242,244,248,.62) 100%);
  }
  .grain { position: fixed; z-index: -1; }

  .cue, .loader { display: none; }

  .bands {
    position: relative;
    inset: auto;
    display: flex;
    flex-direction: column;
    gap: clamp(18px, 4vh, 34px);
    padding: calc(var(--navh) + 6vh) clamp(16px, 5vw, 32px) 10vh;
  }

  /* Every rail collapses to one honest column.

     `position: static` and `inset: auto !important` are both load-bearing. The
     chaos beats are addressed as .band--fn.pos-a to .pos-c, which is a
     two-class selector and therefore OUT-SPECIFIES a bare .band reset: without
     these, their desktop left/right offsets survive into the phone layout and
     shove each card sideways off the screen edge. Static position makes the
     offsets inert; the !important covers anything added later. */
  .band {
    position: static;
    inset: auto !important;
    transform: none !important;
    width: 100%;
    max-width: 100%;
    text-align: left;
    padding: clamp(22px, 4.5vw, 32px);
    border-radius: 18px;
    background: rgba(255,255,255,.72);
    border: 1px solid rgba(90,98,138,.13);
    box-shadow: 0 10px 30px rgba(14,19,48,.06);
    backdrop-filter: blur(14px) saturate(120%);
    -webkit-backdrop-filter: blur(14px) saturate(120%);
  }
  .band::before { display: none; }   /* the card is the scrim now */

  .band--hero {
    min-height: calc(58vh - var(--navh));
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  /* The ribbon becomes the left edge of each function beat, so the device that
     threads the six is still doing its job on a phone. Painted as a background
     layer rather than border-image, which replaces ALL four borders and rings
     every card in gradient. */
  .band--pain, .band--fix, .band--fn {
    background:
      var(--ribbon) left center / 4px 100% no-repeat,
      rgba(255,255,255,.72);
  }
  /* .pos-b still carries text-align:right at two-class specificity, so it has
     to be named here or that beat stays right-aligned on a phone. Only three
     chaos positions exist now; if a fourth ever returns, it belongs in this
     list too, and so does its underline origin below. */
  .band--fix, .band--fn,
  .band--fn.pos-b { text-align: left; }
  .band--fix .fn-open::after,
  .band--fn.pos-b .fn-open::after { transform-origin: left center; }
  .band--fn .fn { font-size: clamp(18px, 5.4vw, 26px); }

  /* the nav has to fit a logo and two actions on a narrow bar */
  .nav-actions { gap: 8px; }
  .ghost { min-height: 42px; padding: 11px 14px; font-size: 12px; letter-spacing: .04em; }

  .detail-card { width: 100%; }

  .band--chain, .band--close { text-align: center; }
  .chain { justify-content: flex-start; }
  .chain-item span { font-size: 11px; letter-spacing: .12em; }

  .lede       { font-size: clamp(28px, 8.4vw, 44px); }
  .lede--close{ font-size: clamp(24px, 7.2vw, 38px); }
  .say        { font-size: clamp(22px, 6.6vw, 34px); }
  .chain-title{ font-size: clamp(21px, 6.2vw, 32px); }
  .sub        { font-size: clamp(15px, 4.1vw, 18px); }
  .kicker     { font-size: 11px; letter-spacing: .22em; }

  /* the halo is only needed over live footage; over a card it just muddies */
  .lede, .say, .fn, .kicker, .sub, .chain-title { text-shadow: none; }

  .cta--hero { align-self: flex-start; }
  .brand-plate img { height: 46px; }
  .brand-plate--lg img { height: min(100px, 40vw); }
  .cta--nav { padding: 11px 16px; font-size: 12px; min-height: 42px; }

  .arrival { min-height: auto; padding-top: clamp(48px, 9vh, 90px); }

  /* the narrowest cards want their inset back off the gutter */
  @media (max-width: 400px) { .band { padding: 20px 18px; } }
}

/* The narrowest real phones: the long primary CTA would push the mark
   off-screen, so only the quiet one survives here. The big primary is still
   one scroll away in the arrival section, and the function card carries one
   as well.

   Note what is NOT here any more: `.band { padding }`. That belonged to the
   static cut's cards and has moved inside it. In the phone cinematic cut the
   beats are captions floating in a room with no box around them, and padding
   here would push the type in off a gutter it is already sitting on. */
@media (max-width: 400px) {
  .cta--nav { display: none; }
  .ghost { padding: 10px 13px; }
  .chain-open { padding: 9px 12px; }
}

/* ════════════════════════════════════════════════════════════════════════
   14 · REDUCED MOTION
   Honoured live and in both directions. .pinned is written by app.js when the
   preference flips on mid-session, so the final states hold without a reload.
   ════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .cue span { animation: none; opacity: 1; }
}

.pinned .band {
  --o: 1 !important;
  --k: 1 !important;
  visibility: visible !important;
}
.pinned .w, .pinned .c { opacity: 1 !important; transform: none !important; }
.pinned .chain-item { --lit: 1 !important; }
