/* =============================================================
   UNLOCK AI VALUE — Infosys kiosk

   V4 look and feel, from INF_FSM_GLO_359.2 in Figma. The build was
   dark glass on a deep navy ground; the design is the inverse:
   near-white, blue type, and a baked 3D glass render carrying each
   screen instead of a photographic plate.

   Type   Geist throughout, 300/400/500/600. The design file uses
          Inter for two labels on the attract screen and Geist for
          everything on the other fourteen, so Geist wins.
   Colour Brand blue #007cc3 leads. Text runs from #061838 through
          #01183b, #004c77 and #00446d, all on white.
   Surface One card treatment (pale blue gradient, white hairline,
          soft blue shadow) and one control treatment (pill, white
          hairline, same shadow family).

   Content and flow are OURS, not the file's: six slider questions,
   email capture rather than a badge tap, three result beats, masked
   carrier names, the Gartner credit. Where the design and the build
   disagree on words or numbers, the build is right.
   ============================================================= */

@import url('fonts.css');

:root {
  --display: 'Geist', 'Inter', 'Helvetica Neue', sans-serif;
  --body: 'Geist', 'Inter', 'Helvetica Neue', Helvetica, sans-serif;
  --label: 'Geist', 'Inter', 'Helvetica Neue', Helvetica, sans-serif;

  /* PAGE. The renders are flattened onto white, so the page has to be the
     same white or the seams show at the edge of a plate. */
  --page: #FFFFFF;

  /* TEXT. Straight from the file's own values rather than a ramp of my own:
     --ink is the display colour ("AI Value" on attract, every headline),
     --brand is the interactive and eyebrow blue, --mid is body copy and
     slider labels, --btn-ink is control text. */
  --ink: #061838;         /* 16.6:1 on white */
  --deep: #01183B;        /* 16.3:1  lockup */
  /* The design's interactive blue. It measures 4.6:1 on pure white and 4.28:1
     over the washed renders, so it fails AA for text at body size. It stays as
     the fill and accent colour; --blue-lift is one step darker and is what
     carries eyebrows and links. Anshul asked for WCAG compliance explicitly,
     which is the one place the build deviates from the file. */
  --brand: #007CC3;       /*  4.6:1 on white - fills and accents only */
  --mid: #004C77;         /*  9.4:1  body copy, scale labels */
  --btn-ink: #00446D;     /* 10.4:1  controls */

  /* Kept as aliases so the screens I have not reworked yet still render
     while this lands. Each one now resolves to a light-ground value. */
  --fg: var(--ink);
  --fg-2: var(--mid);
  --fg-3: var(--btn-ink);
  --fg-4: #2E5C80;        /*  7.2:1  was 5.6, which read as hazed on the washes */
  --blue: var(--brand);
  --blue-lift: #0069A8;   /*  6.0:1  the blue that carries text on white */
  --blue-deep: #00568C;
  --ink-lift: #E8F2FB;    /* pale wash, was the dark ground's lift */
  --hair: rgba(0, 76, 119, 0.16);

  /* SURFACES, verbatim from the file.
     Card:  76.16deg, rgba(156,209,255,.30) 3.09% -> rgba(91,165,230,.09) 85.6%
            1px white border, radius 20, shadow 2px 4px 20px 5px #007cc3 @ 5%
     Pill:  transparent, 0.5px white border, radius 30,
            shadow 2px 4px 20px #00416C @ 15%
     The attract pill carries a fill the others do not. */
  --card: linear-gradient(76.16deg, rgba(156, 209, 255, 0.30) 3.09%, rgba(91, 165, 230, 0.09) 85.6%);
  --card-line: #FFFFFF;
  --card-shadow: 2px 4px 20px 5px rgba(0, 124, 195, 0.05);
  --pill-line: rgba(255, 255, 255, 0.9);
  --pill-shadow: 2px 4px 20px 0 rgba(0, 65, 108, 0.15);
  --pill-fill: linear-gradient(101.84deg, rgba(123, 194, 255, 0.25) 1.96%, rgba(0, 136, 255, 0.25) 100%);
  --radius: 20px;
  --radius-pill: 30px;

  /* The two white washes the design lays over every render, so type never
     sits on the busiest part of the glass. */
  --wash-top: linear-gradient(180deg, rgba(255,255,255,0.80) 25.19%, rgba(255,255,255,0) 86.02%);
  --wash-side: linear-gradient(270deg, rgba(255,255,255,0.80) 7.48%, rgba(255,255,255,0) 86.02%);

  /* glass tokens retained: the panels that have not been reworked read them */
  --glass: rgba(255, 255, 255, 0.72);
  --glass-2: rgba(255, 255, 255, 0.60);
  --glass-line: rgba(255, 255, 255, 0.9);
  --glass-lip: rgba(255, 255, 255, 0.8);

  /* spacing scales with the stage, which is always 16:9 and width-driven */
  --s1: clamp(4px, 0.32cqw, 7px);
  --s2: clamp(8px, 0.62cqw, 13px);
  --s3: clamp(12px, 1.05cqw, 22px);
  --s4: clamp(18px, 1.70cqw, 34px);
  --s5: clamp(26px, 2.50cqw, 50px);
  --s6: clamp(34px, 3.60cqw, 74px);
  --s7: clamp(44px, 5.20cqw, 106px);

  --pad-x: clamp(26px, 4.20cqw, 84px);
  --pad-t: clamp(46px, 6.40cqw, 116px);
  --pad-b: clamp(26px, 4.20cqw, 76px);

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  /* Expo-out: leaves fast, lands slowly. Entrances only. */
  --ease-in-out-soft: cubic-bezier(0.16, 1, 0.30, 1);
  --ease-mech: cubic-bezier(0.65, 0, 0.35, 1);
}

/* -------------------------------------------------------------
   COPY PANEL

   Anshul's readability note and a 40+ audience point the same way: stop
   setting long copy over artwork. Copy moves onto a light surface, and the
   surface re-points the text tokens for everything inside it.

   That re-pointing is the whole trick. Rules elsewhere already say
   `color: var(--fg-3)`; inside a panel the same rule resolves dark-on-light
   instead of light-on-dark, so the flip costs no per-rule edits and cannot
   drift out of sync as the design moves.

   The reference was the GCC deck's clarity, not its styling — no drop
   shadows, no heavy rounding, no illustration. Our hairlines and our type
   hierarchy, on a light ground.
   ------------------------------------------------------------- */
/* No card. The copy sits straight on the render, held by the white wash and
   the page margin rather than by a glass container. */

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  height: 100%;
  background: var(--page);
  color: var(--ink);
  font-family: var(--body);
  font-weight: 400;
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
input, textarea { font: inherit; }
:focus-visible { outline: 1px solid var(--blue-lift); outline-offset: 4px; }

/* ---------- stage ------------------------------------------- */

#shell { position: fixed; inset: 0; display: grid; place-items: center; background: var(--page); }
/* The stage fills the window; only the composition inside it is locked to
   16:9. Pinning the stage itself to 16:9 letterboxed the whole thing on any
   display that is not 16:9 — a laptop is usually 16:10 — and read as broken.
   Now the plates, the film, the 3D and the vignette all bleed to the edges,
   and #frame is the 16:9 safe area centred inside them. */
#stage {
  position: relative;
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  /* The room. Both stops are driven from JS per value pool, so entering a
     pool changes the whole space rather than just tinting the object. */
  /* Was a near-black radial. The renders are flattened onto white, so the
     room behind them has to be white too or a seam shows at every plate edge. */
  --room-a: #FFFFFF;
  --room-b: #F4F9FE;
  background: radial-gradient(130% 95% at 30% 20%, var(--room-a) 0%, var(--room-b) 62%, #EDF5FC 100%);
  transition: background 900ms var(--ease);
  isolation: isolate;
}
/* One generated plate per screen, swapped on #stage[data-view]. Screen blending
   means only the lattice and the glow register — the near-black ground of each
   image adds nothing — so the room gradient underneath keeps its colour and
   still shifts per pool.

   Opacity is per screen rather than global, because the plates differ in how
   much light they carry and the screens differ in how much reading they ask
   for: attract is a headline and can take a dominant plate, the diagnostic is
   eighteen questions and cannot. */
/* Every plate is now finished artwork rather than a faint texture meant to sit
   over the room gradient, so the default is a normal blend at full strength.
   Screen blending at 0.24-0.55 was what made the new plates read dull and grey:
   it lifts blacks and desaturates everything it touches. */
/* #bg is now a probe, never painted. It still carries every per-screen
   background-image rule below, so CSS stays the single source of truth for
   which plate belongs to which state; app.js reads its resolved value and
   cross-fades the two real layers. Swapping background-image on one element
   cannot be animated, which is why changing pool used to blip. */
#bg {
  position: absolute; inset: 0;
  background: center / cover no-repeat;   /* never painted: a probe for CSS */
  opacity: 0;
  pointer-events: none;
}
#bgA, #bgB {
  position: absolute; inset: 0;
  /* Full-bleed. The plates cover the whole window and the content stays on
     the 16:9 #frame centred inside it. Sizing the plates to the frame left a
     white band above and below the artwork on any display that is not 16:9
     (a 16:10 laptop, a portrait kiosk), which read as the page not loading.
     On a 16:9 display `cover` is pixel-identical to the frame size, so the
     artwork and the content stay in register there; elsewhere the plate is
     enlarged and cropped at the edges, which is what a background is for. */
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  opacity: 0;
  pointer-events: none;
}
/* Two fixed roles rather than a ping-pong. #bgA is the plate on screen and is
   never animated; #bgB is the one arriving, and it fades in over the top. That
   ordering matters: the layers are stacked, not weighted, so fading the old one
   out at the same time let the page white show through both. At the midpoint of
   a symmetric cross-fade a quarter of the composite was flat white, which is
   the pale flash the switch had. With the old plate held opaque underneath the
   composite is exactly new*t + old*(1-t) and nothing shows between them.

   The curve was --ease-in-out-soft, which is misnamed: cubic-bezier(0.16,1,0.3,1)
   is expo-out. It put the fade 61% through in its first 100ms and then spent
   600ms crawling the last 5% — a snap with a long dead tail. A dissolve wants a
   symmetric curve, so this is sine-in-out. */
#bgB { transition: opacity 800ms cubic-bezier(0.37, 0, 0.63, 1); }
#bgA.on, #bgB.on { opacity: 1; }

/* The design lays two white washes over every render: a band down from the top
   at 25% height, and one in from the right edge across 46% of the width. They
   are what lets type sit anywhere on the glass, so they are not decoration and
   they are on for every screen, not just the two the dark scrim covered.

   Both were dark navy. On a white ground the same job needs white. */
/* z-index 1, not 2. These wash the RENDER so type can sit on it; they must
   never come between the plate and the content. #views is 5. */
#topScrim {
  position: absolute; inset: 0 0 auto 0; height: 25%;
  z-index: 1; pointer-events: none;
  opacity: 1;
  background: var(--wash-top);
}
/* The side wash follows the copy column, so it flips with the layout: the
   diagnostic reads on the right, the pool screens read on the left. */
#sideScrim {
  position: absolute; inset: 0 0 0 auto; width: 46%;
  z-index: 1; pointer-events: none;
  opacity: 0; transition: opacity 700ms var(--ease);
  background: var(--wash-side);
}
/* Only where the copy reads on the right and the view has no wash of its own.
   The explore and pool screens are deliberately absent: they carry their own
   .copy-scrim over the left column, and running both put two white washes on
   the same column and flattened the panel into the plate. */
#stage[data-view='diag'] #sideScrim,
#stage[data-view='identify'] #sideScrim { opacity: 1; }

/* Every plate is now a baked 3D glass render from the design file, flattened
   onto white and shipped at the frame's own 1920 wide. They replace the
   photographic placeholders: the design carries each screen on one of these
   rather than on a texture, so there is nothing left for the room gradient to
   show through. */
/* Backgrounds are named bg-(page)-(number) (Rob, 10 Sep). Pages, in kiosk
   order: start, six-pools (1 intro, 2 hexagon overview, 3-8 the pools in Next
   order: strategy, data, process, legacy, physical, trust), diagnostic (1-6 in
   question order), case-study, results-access, results (1 position and
   benchmark, 2 priorities), thank-you. The film is video/bg-start-1.mp4. */
#stage[data-view='attract']  #bg { background-image: url('../backgrounds/bg-start-1.jpg'); }
#stage[data-view='intro']    #bg { background-image: url('../backgrounds/bg-six-pools-1.jpg'); }
#stage[data-view='explore']  #bg { background-image: url('../backgrounds/bg-six-pools-2.jpg'); }
#stage[data-view='diag']     #bg { background-image: url('../backgrounds/bg-diagnostic-1.jpg'); }
#stage[data-view='identify'] #bg { background-image: url('../backgrounds/bg-results-access-1.jpg'); }

/* Opening a pool swaps to that pool's own render. */
#stage[data-view='explore'][data-pool='strategy'] #bg { background-image: url('../backgrounds/bg-six-pools-3.jpg'); }
#stage[data-view='explore'][data-pool='data']     #bg { background-image: url('../backgrounds/bg-six-pools-4.jpg'); }
#stage[data-view='explore'][data-pool='process']  #bg { background-image: url('../backgrounds/bg-six-pools-5.jpg'); }
#stage[data-view='explore'][data-pool='legacy']   #bg { background-image: url('../backgrounds/bg-six-pools-6.jpg'); }
#stage[data-view='explore'][data-pool='physical'] #bg { background-image: url('../backgrounds/bg-six-pools-7.jpg'); }
#stage[data-view='explore'][data-pool='trust']    #bg { background-image: url('../backgrounds/bg-six-pools-8.jpg'); }

/* And the diagnostic follows the pool being asked about, which the design does
   too: one render per question. app.js sets data-pool on the diagnostic now. */
/* The case study page carries one photograph for every case (Rob, 9 Sep):
   backgrounds/bg-case-study-1.jpg, the Figma 411:1675 image. */
#stage[data-view='case'] #bg { background-image: url('../backgrounds/bg-case-study-1.jpg'); }
#stage[data-view='diag'][data-pool='strategy'] #bg { background-image: url('../backgrounds/bg-diagnostic-1.jpg'); }
#stage[data-view='diag'][data-pool='data']     #bg { background-image: url('../backgrounds/bg-diagnostic-2.jpg'); }
#stage[data-view='diag'][data-pool='process']  #bg { background-image: url('../backgrounds/bg-diagnostic-3.jpg'); }
#stage[data-view='diag'][data-pool='legacy']   #bg { background-image: url('../backgrounds/bg-diagnostic-4.jpg'); }
#stage[data-view='diag'][data-pool='physical'] #bg { background-image: url('../backgrounds/bg-diagnostic-5.jpg'); }
#stage[data-view='diag'][data-pool='trust']    #bg { background-image: url('../backgrounds/bg-diagnostic-6.jpg'); }
/* Part two of each question (Rob, 14 Sep) takes a different plate so the
   left side visibly moves on when the second question appears, but it MUST be
   a diagnostic plate: those are composed with the artwork on the left and
   the copy space on the right. The framework renders (bg-six-pools-*) are
   the mirror of that, art on the right, and put the hexagons behind the
   question. So part two rotates through the same diagnostic set, one step
   on from its pool. When a dedicated second set is exported, point these at
   bg-diagnostic-1b.jpg … bg-diagnostic-6b.jpg. */
#stage[data-view='diag'][data-pool='strategy'][data-sub='1'] #bg { background-image: url('../backgrounds/bg-diagnostic-2.jpg'); }
#stage[data-view='diag'][data-pool='data'][data-sub='1']     #bg { background-image: url('../backgrounds/bg-diagnostic-3.jpg'); }
#stage[data-view='diag'][data-pool='process'][data-sub='1']  #bg { background-image: url('../backgrounds/bg-diagnostic-4.jpg'); }
#stage[data-view='diag'][data-pool='legacy'][data-sub='1']   #bg { background-image: url('../backgrounds/bg-diagnostic-5.jpg'); }
#stage[data-view='diag'][data-pool='physical'][data-sub='1'] #bg { background-image: url('../backgrounds/bg-diagnostic-6.jpg'); }
#stage[data-view='diag'][data-pool='trust'][data-sub='1']    #bg { background-image: url('../backgrounds/bg-diagnostic-1.jpg'); }

/* Nitin's note: soft, near-plain, blue on blue. Each beat gets its own plate,
   each built around where that beat puts its copy — so the pattern is always
   somewhere the reading is not. */
/* The results beats and the closing screen have no design yet, so they take the
   calmest of the new renders rather than keeping a dark plate next to fifteen
   light ones. It is the near-plain one, which is what these screens want
   anyway: six scored rows and a paragraph, with the artwork staying out of the
   reading. Replace per screen when the designs land. */
#stage[data-view='wrap'][data-beat='1'] #bg { background-image: url('../backgrounds/bg-results-1.jpg'); }
#stage[data-view='wrap'][data-beat='2'] #bg { background-image: url('../backgrounds/bg-results-1.jpg'); }
/* Beat 3 has its own plate from the design (Figma 326:181) — the hexagon
   bubbles clear the centre band where the three cards sit. Beats 1 and 2 are
   still standing in with the explore plate; they have no V4 render yet. */
#stage[data-view='wrap'][data-beat='3'] #bg { background-image: linear-gradient(-61deg, rgba(22, 96, 188, 0.2) 28.8%, rgba(225, 239, 255, 0.08) 61.4%), url('../backgrounds/bg-results-2.jpg'); }
#stage[data-view='done'] #bg {
  background-image: url('../backgrounds/bg-thank-you-1.jpg');
}

/* Attract's background film, and the 20% black over it. Both sit above #bg and
   below the canvas, so the 3D and all copy still render on top. Shown only on
   attract — every other screen keeps its still plate. */
#bgVideo {
  /* Same register as the plates: full-bleed over the whole stage, cropped by
     object-fit rather than letterboxed, so the film never shows a white band
     on a display that is not 16:9. */
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
  /* No blur and no desaturation this time. The old film was unrelated footage
     that had to be beaten into submission; this one is generated from the
     attract plate itself, first and last frame identical so the loop does not
     jump, and its centre is already the dark calm field the headline needs. */
  opacity: 0;
  pointer-events: none;
  transition: opacity 700ms var(--ease);
}
#bgVideoScrim {
  position: absolute; inset: 0;
  background: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity 700ms var(--ease);
}
/* -------------------------------------------------------------
   CENTRE SCRIM — identify and done: removed.

   There were two of these, both near-black radials on ::before, and together
   they were dimming identify to the point of being unreadable. They were
   written when the plates were vivid blue and the copy on them was white:
   the note said white fell to 3.19:1 and --fg-3 to 1.97:1, and a dark scrim
   bought that back.

   Neither condition holds now. Identify carries the design's own pale plate
   and dark type, measured against that plate with no scrim at all: the
   heading 13.9:1, the eyebrow 4.70, the button label 7.91, the placeholders
   5.5. A scrim built for white-on-vivid is pure loss here.

   They survived the V4 migration twice because they are pseudo-elements —
   sweeping #stage's elements for dark backgrounds does not see a ::before.
   ------------------------------------------------------------- */

/* --blue-lift measured 2.91:1 over the film. White clears it without
   pushing the scrim so dark the film stops reading as motion. */
/* Attract only: blue sentence, ink accent. The reverse of every other heading,
   and the reverse of what the dark build did. The eyebrow and subhead rules
   that sat here went with the elements. */
#v-attract .d-xl { color: var(--brand); }
#v-attract .d-xl em { color: var(--ink); }

/* The plate is the video's poster, so it must not paint over it as well. */
/* The V4 home screen loop, 8s at 3840x2160. Back on now that it is the light
   film: the dark one was switched off because dark type over it was unreadable.
   Its scrim stays off - that was a 20% black laid over the old dark loop, and
   on this one it would only mute the glass. */
#stage[data-view='attract'] #bgVideo { opacity: 1; }
#stage[data-view='attract'] #bgVideoScrim { opacity: 0; }

#gl { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* The particle layer. z-index stays auto so it keeps its DOM position: above
   the plates and the film, below the two white washes (z-index 1) and below
   #frame (z-index 2). It is on for every screen — that is the point of it —
   and the wash is what keeps it off the brand line and the right-hand copy. */
#px {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
  pointer-events: none;
  opacity: 0; transition: opacity 900ms var(--ease);
}
#stage[data-view] #px { opacity: 1; }
/* Attract runs a film, which already has motion of its own. */
#stage[data-view='attract'] #px { opacity: 0.55; }
/* Off by default, on for exactly one screen. Every V4 render already carries a
   3D object, so a canvas board on top of one is two objects on the same screen
   fighting each other. The framework overview is the exception, because there
   the board is the thing you touch, and the brief was to keep it. */
#gl { opacity: 0; transition: opacity 500ms var(--ease); }
#stage[data-view='explore']:not([data-pool]) #gl { opacity: 1; }
/* With the SVG hexagon the canvas draws nothing anyone can see, and an
   opacity-0 canvas still takes pointer events — it was swallowing taps meant
   for the screen underneath. js/app.js sets this unless ?hex=3d. */
#gl[hidden] { display: none; }

/* The design surface. Fixed at 1920x1080 and scaled to whatever the stage is,
   so the composition never reflows and the clamp floors never bite. --k is set
   from JS on resize. It is the query container too, so every cqw inside
   resolves against 1920 rather than the window — the two only agreed when the
   window happened to be exactly 16:9. */
#frame {
  position: absolute;
  z-index: 2;
  top: 0; left: 0;
  width: 1920px;
  height: 1080px;
  transform-origin: top left;
  /* Scaled to fit and centred, both set from JS. */
  transform: translate(var(--fx, 0px), var(--fy, 0px)) scale(var(--k, 1));
  container-type: size;
  container-name: stage;
  /* Transparent to input. The frame covers the whole stage and sits above the
     canvas, so with the default `auto` it swallowed every tap and the board
     could not be touched. Everything inside that needs events — .view, the
     buttons — sets pointer-events itself. */
  pointer-events: none;
}

/* Below the frame. It was at 3, above the frame's 2, so it sat over every
   glyph on every screen. */
#grain {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  opacity: 0.035; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* Gone. A dark edge falloff is a device for a dark room; on the V4 white it
   greys the corners, and at z-index 3 it was doing that on top of the lockup
   and every other glyph. The element stays in the markup so the layer is
   available again if a light vignette is ever wanted. */
#vignette {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: none;
}

/* ---------- chrome: brand lockup only ----------------------- */

#chrome { position: absolute; z-index: 6; inset: 0; pointer-events: none; }
.bar.top {
  display: flex; align-items: center;
  padding: clamp(26px, 2.4cqw, 40px) var(--pad-x) 0;
}
/* The brand is also the way out: tapping it starts over. #chrome is
   pointer-events: none, so it has to opt back in. */
.brand {
  display: flex; align-items: center; gap: var(--s2);
  pointer-events: auto; cursor: pointer;
  padding: 8px 10px; margin: -8px -10px;
  border-radius: 10px;
  transition: background 0.2s var(--ease);
}
.brand:active { background: rgba(0, 76, 119, 0.10); }
.brand:focus-visible { outline: 1px solid var(--blue-lift); outline-offset: 3px; }
/* Was crushed to black then inverted to white for the dark ground. On white
   that is a white mark on white. The same trick without the invert gives a
   solid dark mark, then a hue rotation lands it on the design's #01183b
   rather than flat black. */
.brand img {
  /* Frame px, so it scales with the stage: the same share of the screen at
     1920, 1440 and 1280. Was clamp(15px, 1.15cqw, 21px) (Rob, 9 Sep). */
  height: 30px; width: auto;
  filter: brightness(0) saturate(100%) invert(9%) sepia(35%) saturate(3200%) hue-rotate(199deg) brightness(94%) contrast(101%);
}
.brand .div { width: 1px; height: 20px; background: rgba(1, 24, 59, 0.35); }
.brand span {
  font-family: var(--label); font-weight: 500;
  font-size: 11px;   /* frame px; was clamp(13.1px, 0.856cqw, 16.6px) (Rob, 9 Sep) */
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--deep);
}

/* One colour now: the lockup is --deep on every screen, because every screen
   is the same white and the top wash guarantees the ground under it. */

/* ---------- views ------------------------------------------- */

#views { position: absolute; inset: 0; z-index: 5; pointer-events: none; }
.view {
  position: absolute; inset: 0;
  padding: var(--pad-t) var(--pad-x) var(--pad-b);
  display: none; pointer-events: auto;
}
.view.on { display: block; }
/* Restoring this: it was dropped when the framework block was rewritten, so
   every `animation: rise` in the file had been referencing a keyframe that no
   longer existed and nothing was animating at all. */
@keyframes rise   { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes riseIn { from { opacity: 0; transform: translateY(14px) scale(0.985); } to { opacity: 1; transform: none; } }
/* Cards travel a little further and settle, so six of them arriving in sequence
   reads as dealing rather than blinking. */
@keyframes cardIn { from { opacity: 0; transform: translateY(26px) scale(0.965); } to { opacity: 1; transform: none; } }

.view.on > * { animation: rise 0.7s var(--ease) both; }

/* Overall progress, at the top of the panel where the eye already is. The
   per-pool clusters stay in the corner as detail; this is the headline. */
/* Thinner, and above the meta row rather than under it. Underneath, its fill
   stopped somewhere inside the theme text and read as an underline of the
   words rather than as progress through the questions. */
/* The progress bar is gone: the hexagon stepper replaces it. Six questions
   are six discrete steps, and a continuous bar was measuring a thing that
   only ever moves in sixths. */
/* The stepper carries the count beside it: the hexagon shows how far along
   you are at a glance, the words say it exactly. */
.qstep-row {
  display: flex; align-items: center; gap: var(--s3);
  margin: 0 0 var(--s4);
}
.qcount {
  font-family: var(--label); font-weight: 400;
  font-size: clamp(13.0px, 0.833cqw, 16px);
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--fg-4);
  white-space: nowrap;
}
.hexstep { display: block; width: auto; height: 68px; overflow: visible; }
/* --ink-lift was the obvious choice and it vanished: at #E8F2FB against a
   plate that is already near-white, the six unfilled capsules were invisible
   and the hexagon read as one stray mark. The empty steps have to hold the
   shape, so this is the brand blue at low alpha — it keeps its identity on
   the pale plates and on the white panels. */
.hs-seg {
  stroke: rgba(0, 105, 168, 0.26);
  transition: stroke 0.4s var(--ease);
}
.hs-seg.on { stroke: var(--brand); }
/* Only relevant if the question count ever drops below six. */
.hs-seg.off { opacity: 0.35; }
/* The part-way fill for the question in hand (Rob, 14 Sep): a capsule over
   the pale one, dashed to --f of its length (pathLength=1), so half the
   edge lights on part one. It is its OWN colour, the sky blue the framework
   uses for AI Strategy, so a half-done question reads differently from a
   finished one; on part two the overlay goes and the edge completes in the
   brand blue like every edge before it. It arrives by transform, growing
   out of the edge's first vertex, rather than by fading in. Idle overlays
   have a zero dash and paint nothing. */
.hs-part {
  stroke: var(--hs-half, #4F9DFF);
  stroke-dasharray: var(--f, 0) 1;
  pointer-events: none;
  transform-box: view-box;   /* origin is set inline in user units (hexstep.js) */
}
.hs-part:not(.live) { opacity: 0; }
.hexstep.enter .hs-part.live {
  animation: hsPartIn 0.55s var(--ease) both, hsPartPulse 0.9s var(--ease) 1;
  animation-delay: calc(0.1s + var(--i, 0) * 0.14s), calc(0.55s + var(--i, 0) * 0.14s);
}
@keyframes hsPartIn { from { transform: scale(0.001); opacity: 0.4; } to { transform: none; opacity: 1; } }
@keyframes hsPartPulse { 0% { filter: drop-shadow(0 0 0 rgba(79, 157, 255, 0)); } 40% { filter: drop-shadow(0 0 6px rgba(79, 157, 255, 0.6)); } 100% { filter: drop-shadow(0 0 0 rgba(79, 157, 255, 0)); } }
.hs-num {
  fill: var(--blue-lift);
  font-family: var(--label); font-weight: 500; font-size: 21px;
  letter-spacing: 0;
  transform-box: fill-box; transform-origin: center;
}
/* Entrance, replayed by HexStep.set on every question (Rob, 10 Sep): each
   filled capsule draws along its length in ring order (pathLength=1 makes a
   dash of 1 the whole capsule), the one just reached pulses as it lands, and
   the number rises in behind them. */
/* The fill builds by colour (Rob, 10 Sep): every capsule starts pale and the
   filled ones turn brand blue one after another round the ring, the one just
   reached last, with a pulse. */
.hexstep.enter .hs-seg.on {
  animation: hsFill 0.5s var(--ease) both;
  animation-delay: calc(0.1s + var(--i, 0) * 0.14s);
}
.hexstep.enter .hs-seg.now { animation: hsFill 0.5s var(--ease) both, hsPulse 0.9s var(--ease) 1; animation-delay: calc(0.1s + var(--i, 0) * 0.14s), calc(0.5s + var(--i, 0) * 0.14s); }
.hexstep.enter .hs-num { animation: hsNum 0.5s var(--ease) 0.25s both; }
@keyframes hsFill { from { stroke: rgba(0, 105, 168, 0.26); } to { stroke: var(--brand); } }
@keyframes hsPulse { 0% { filter: drop-shadow(0 0 0 rgba(0, 124, 195, 0)); } 40% { filter: drop-shadow(0 0 6px rgba(0, 124, 195, 0.55)); } 100% { filter: drop-shadow(0 0 0 rgba(0, 124, 195, 0)); } }
@keyframes hsNum { from { opacity: 0; transform: translateY(6px) scale(0.8); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .hexstep.enter * { animation: none !important; } }

.qprog {
  position: relative; height: 3px; border-radius: 2px;
  background: rgba(0, 76, 119, 0.16);
  margin: 0 0 var(--s4);
  overflow: hidden;
}
/* --blue, not --blue-lift. The lift is reserved for the slider and Next, so
   there are two bright blues on the screen instead of six. */
.qprog i {
  position: absolute; top: 0; bottom: 0; left: 0;
  display: block; width: 0;
  background: var(--blue); border-radius: 2px;
  transition: width 0.45s var(--ease);
}

/* ---------- staggered entrance --------------------------------
   A whole screen arriving as one slab reads rigid. Content blocks stagger
   their own children so a screen assembles instead of appearing. The
   diagnostic rebuilds its options per question, so that one replays for free
   on every question.
   ------------------------------------------------------------- */
.stagger > * { animation: riseIn 0.72s var(--ease-in-out-soft) both; }
.stagger > *:nth-child(1) { animation-delay: 0.04s; }
.stagger > *:nth-child(2) { animation-delay: 0.13s; }
.stagger > *:nth-child(3) { animation-delay: 0.22s; }
.stagger > *:nth-child(4) { animation-delay: 0.31s; }
.stagger > *:nth-child(5) { animation-delay: 0.40s; }
.stagger > *:nth-child(6) { animation-delay: 0.49s; }
.stagger > *:nth-child(7) { animation-delay: 0.58s; }
.stagger > *:nth-child(8) { animation-delay: 0.67s; }
.stagger > *:nth-child(9) { animation-delay: 0.76s; }
.stagger > *:nth-child(10) { animation-delay: 0.85s; }

/* The six pool cards deal out one at a time. 110ms apart is slow enough to
   read as a sequence and short enough that the last one is not a wait. */

@media (prefers-reduced-motion: reduce) {
  .stagger > * { animation: none; }
}
/* Explore drives its own two modes with opacity transitions. `rise` uses
   fill-mode both, so its final opacity:1 would outrank those rules and both
   modes would show at once. */
#v-explore.on > * { animation: none; }

/* Insurance for the copy column. Khai's plates each keep their left side dark,
   but the build should not depend on the next one doing the same. */
#v-explore .copy-scrim {
  position: absolute; inset: 0; z-index: 0;
  /* Two gradients, one element: the left band carries the copy column, the
     bottom band carries the swipe and framework affordances, which otherwise
     sit on bright glass and stop being readable. */
  /* Much lighter than it was: the copy now sits on a panel, so this only has
     to settle the artwork behind the footer chrome, not carry text contrast. */
  background:
    linear-gradient(90deg,
      rgba(255, 255, 255, 0.72) 0%, rgba(255, 255, 255, 0.42) 38%, rgba(255, 255, 255, 0) 62%),
    linear-gradient(0deg,
      rgba(255, 255, 255, 0.84) 0%, rgba(255, 255, 255, 0.38) 10%, rgba(255, 255, 255, 0) 18%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 520ms var(--ease);
}
#v-explore.focus .copy-scrim { opacity: 1; }
/* The overview needs its own, stronger gradient. The focus scrim tops out at
   0.52 alpha because the copy there sits on a white panel and only the footer
   chrome needs help. In overview the intro sits straight on the plate, whose
   left side is luminance 0.18 — the eyebrow measured 2.38:1 and the lede
   3.79:1. Raising the focus scrim's opacity cannot fix that; it needs more
   alpha at the left edge. Full height, so there is no horizontal edge to
   catch the eye the way a boxed backdrop did. */
/* The overview carries no copy of its own — the framework screen does that now.
   It only has to settle the artwork under the two bottom chrome items, so a
   bottom band rather than darkening the whole left of the plate. */
/* Overview: no scrim at all (Rob, 9 Sep). The dark band that sat across the
   bottom quarter was a leftover from the dark build, where it carried the
   footer chrome; on the V4 render it read as a grey gradient over the page.
   The hint is a pill now and needs nothing behind it. */
#v-explore:not(.focus) .copy-scrim { opacity: 0; }

/* ---------- framework screen ----------------------------------
   Its own step between attract and the hexagon. An asymmetric split rather
   than one centred box: the argument reads down the left, the six pools sit
   right as a grid of glass cards you can take in at a glance.
   ------------------------------------------------------------- */
#v-intro { display: none; }
/* The design stacks this screen rather than splitting it: eyebrow, headline and
   lede centred at the top, the six pools in one divided container beneath, and
   the two ways on centred under that. It was a 30/70 split with the copy left
   and six separate glass cards right. */
#v-intro.on {
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--s5);
  padding: var(--pad-t) var(--pad-x) var(--pad-b);
}
.fw-lead {
  position: relative; text-align: center;
  animation: rise 0.6s var(--ease) both;
}
.fw-title {
  font-family: var(--display); font-weight: 300;
  font-size: clamp(34px, 3.1cqw, 62px);
  line-height: 1.06; letter-spacing: -0.02em;
  color: var(--brand);
  margin: var(--s3) 0 var(--s4);
}
/* Blue sentence, ink accent, the same inversion the attract headline uses. */
.fw-title em { font-style: normal; color: var(--ink); }
/* Figma 412:199: 22px Geist, 1.6 line height, black, 973px column, centred,
   three paragraphs with a blank line between them. */
.fw-lede {
  font-size: 22px; line-height: 1.6; color: #000;
  width: 973px; max-width: 100%; margin: 0 auto;
}
.fw-lede p + p { margin-top: 1.6em; }
.fw-lead .label { color: var(--blue-lift); }

/* Two ways on: read the pools first, or go straight to the diagnostic.
   Anshul's note was that the diagnostic read as secondary to the playbook
   content, so the diagnostic is the filled pill and reading is the quiet one. */
.fw-actions {
  display: flex; flex-direction: row; align-items: center;
  justify-content: center; gap: 30px;
}
/* "Explore the six pools" is an underlined text link in the design
   (Figma 412:252), the diagnostic is the pill beside it. */
.fw-skip {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 0 4px;
  font-family: var(--label); font-weight: 500; font-size: 16px; line-height: 1.4;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--brand);
  cursor: pointer;
}
.fw-skip u { text-decoration: underline; text-underline-offset: 0.2em; }
.fw-skip .arrow { font-family: var(--body); font-size: 1.15em; }
.fw-skip:active { color: var(--blue-deep); }

/* The .fw-grid / .fw-card rules that were here are gone with the six cards
   they styled (see index.html #v-intro). The framework screen is now the lede
   and the two calls to action. */

/* ---------- type primitives --------------------------------- */

.label {
  font-family: var(--label); font-weight: 600;
  font-size: clamp(13.1px, 0.856cqw, 16.6px);
  letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--blue-lift);
}
/* --brand measures 4.00:1 at worst over the renders. That clears the 3:1 a
   headline needs and fails the 4.5 a small label needs, so labels take the
   darker step and only display type keeps the brand blue. */
.label.cool { color: var(--blue-lift); }

.d-xl, .d-l, .d-m {
  font-family: var(--display); font-weight: 300;
  line-height: 1.02; letter-spacing: -0.022em; color: var(--fg);
  text-wrap: balance;
}
.d-xl { font-size: clamp(40px, 4.6cqw, 88px); }
.d-l  { font-size: clamp(32px, 3.4cqw, 64px); }
.d-m  { font-size: clamp(24px, 2.4cqw, 44px); }
.d-xl em, .d-l em, .d-m em { font-style: normal; color: var(--brand); }

p.lede {
  color: var(--fg-2); font-weight: 300;
  font-size: clamp(14.8px, 1.081cqw, 21.2px); line-height: 1.6;
}
p.body { color: var(--fg-2); font-size: clamp(14.2px, 0.98cqw, 18.2px); line-height: 1.62; }

/* centred statement block — the default composition */
.statement { width: min(1120px, 64cqw); margin: 0 auto; text-align: center; }
.statement .label { display: block; margin-bottom: var(--s3); }
.statement .lede { max-width: 48ch; margin: var(--s3) auto 0; }

/* ---------- buttons: no boxes, a rule and a colour ---------- */

/* A pill, from the design: 0.5px white hairline, radius 30, the shared blue
   shadow, transparent fill. It was an underlined text link, which on a light
   ground read as neither a control nor a link. Height comes from padding rather
   than the design's fixed 71px so a longer label still fits.
   The design uses --btn-ink for every control, primary included; primary is
   distinguished by the fill it carries rather than by its colour. */
.btn {
  font-family: var(--label); font-weight: 500;
  font-size: clamp(13.0px, 0.857cqw, 16.4px);
  letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--btn-ink);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  padding: clamp(14px, 1.35cqw, 26px) clamp(20px, 2.0cqw, 40px);
  min-height: clamp(48px, 4.6cqw, 71px);
  border: 0.5px solid var(--pill-line);
  border-radius: var(--radius-pill);
  box-shadow: var(--pill-shadow);
  position: relative;
  transition: background 0.24s var(--ease), color 0.24s var(--ease), box-shadow 0.24s var(--ease);
}
.btn:active { background: rgba(255, 255, 255, 0.55); }

.btn.primary { background: var(--pill-fill); color: var(--btn-ink); }
.btn.primary:active { background: rgba(0, 136, 255, 0.32); }

/* No fill, so it reads as the secondary of a pair. */
.btn.quiet { background: transparent; color: var(--btn-ink); }
.btn.quiet:active { background: rgba(255, 255, 255, 0.55); }
.btn:disabled { opacity: 0.4; pointer-events: none; }
.btn .arrow { font-family: var(--body); font-size: 1.15em; }

/* ---------- attract ----------------------------------------- */

/* Centred. The old plate had a horizon the copy had to clear; the current
   one does not. */
#v-attract { display: none; place-items: center; }

#v-attract.on { display: grid; }
#v-attract .statement { max-width: none; }
#v-attract .d-xl { max-width: 21ch; margin: var(--s3) auto 0; }

.touch-cue {
  margin-top: var(--s5); white-space: nowrap;
  /* inline-flex so the parent's text-align keeps it centred */
  display: inline-flex; align-items: center; gap: var(--s3);
  font-family: var(--label); font-weight: 500;
  font-size: clamp(13.1px, 0.856cqw, 16.6px);
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--fg-3);
}
/* Only the wording breathes. The beacon keeps its own ping, which is the part
   that says "touch"; fading the two together would mute exactly that. */
.touch-cue .tc-text { animation: breathe 3.4s var(--ease) infinite; }
@keyframes breathe { 0%, 100% { opacity: 0.4 } 50% { opacity: 1 } }

/* ---------- explore: framework hexagon left, reading right --- */

#v-explore { pointer-events: none; }

/* Sits directly under the hexagon, 44px below its lowest vertex (Rob, 9 Sep).
   It used to be pinned to the bottom of the frame, which put it 137px below
   the shape in an otherwise empty band and level with the diagnostic button
   on the right — so it read as a second footer control rather than as a label
   for the thing above it.

   The 889 is measured, not guessed: the board's own ink runs y 194 to 845 in
   frame pixels (its six paths, bounding-boxed), and the hexagon is centred on
   x 960, so auto margins still centre the hint on the shape. Re-measure if the
   board's viewBox changes. */
.hex-hint {
  /* Figma 287:748/749: a 387x71 pill at y 897, the design's pill fill and
     hairline, "TAP A VALUE POOL TO EXPLORE" in brand blue. Centred on the
     hexagon (x 960) by the auto margins. */
  position: absolute; left: 0; right: 0; top: 897px; bottom: auto;
  margin: 0 auto; width: 387px; height: 71px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-pill);
  border: 0.5px solid #FFFFFF;
  background: var(--pill-fill);
  box-shadow: var(--pill-shadow);
  font-family: var(--label); font-weight: 500;
  font-size: 16px; line-height: 1.4;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--brand);
  text-align: center;
  pointer-events: none;
}
.touch-dot { position: relative; width: 26px; height: 26px; flex: none; }
.touch-dot i {
  position: absolute; inset: 9px; border-radius: 50%;
  background: var(--blue-lift); display: block;
}
.touch-dot::before, .touch-dot::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  border: 1.5px solid var(--blue-lift);
  animation: touchPing 2.4s var(--ease) infinite;
}
.touch-dot::after { animation-delay: 1.2s; }
@keyframes touchPing {
  0% { transform: scale(0.35); opacity: 0.9; }
  70%, 100% { transform: scale(1.15); opacity: 0; }
}

/* three quick facts, tick per pool colour */
.pr-facts { list-style: none; margin-top: var(--s4); }
.pr-facts li {
  --seg: var(--blue);
  position: relative; padding: var(--s2) 0 var(--s2) calc(var(--s3) + 8px);
  border-bottom: 1px solid var(--hair);
  font-size: clamp(14.2px, 1.026cqw, 18.2px); color: var(--fg-2);
}
/* The facts land one by one (Rob, 10 Sep): each row rises in after the
   panel has arrived, 180ms apart, and its marker pops in with it. The list
   is rebuilt for every pool, so the run replays on each change. */
.pr-facts li { animation: rise 0.6s var(--ease) both; animation-delay: calc(0.25s + var(--i, 0) * 0.18s); }
.pr-facts li:nth-child(1) { --i: 0; }
.pr-facts li:nth-child(2) { --i: 1; }
.pr-facts li:nth-child(3) { --i: 2; }
.pr-facts li:nth-child(4) { --i: 3; }
.pr-facts li:first-child { border-top: 1px solid var(--hair); }
/* The footer below carries its own top rule, so a bottom border on the last
   fact drew a second line a few pixels above it. Rules go between the facts
   only. */
.pr-facts li:last-child { border-bottom: none; }
/* Pointy-top hexagon marker, the same silhouette as the framework board.
   It pops in per item as the list renders, then a second hexagon pings out
   of it on a slow loop — enough movement to read as alive, not enough to
   compete with the copy. */
.pr-facts li::before,
.pr-facts li::after {
  content: ''; position: absolute; left: 0; top: 50%;
  width: 12px; height: 13px; margin-top: -6.5px;
  clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  background: var(--seg);
}
.pr-facts li::before {
  animation: hexPop 0.52s var(--ease) both;
  animation-delay: calc(0.35s + var(--i, 0) * 0.18s);   /* just behind its row */
}
.pr-facts li::after {
  opacity: 0;
  animation: hexPing 3.4s var(--ease) infinite;
  animation-delay: calc(0.35s + var(--i, 0) * 0.18s);
}

@keyframes hexPop {
  from { opacity: 0; transform: scale(0.3) rotate(-30deg); }
  to   { opacity: 1; transform: none; }
}
@keyframes hexPing {
  0%   { opacity: 0.55; transform: scale(1); }
  55%  { opacity: 0;    transform: scale(2.1); }
  100% { opacity: 0;    transform: scale(2.1); }
}

@media (prefers-reduced-motion: reduce) {
  .pr-facts li::before { animation: none; }
  .pr-facts li::after { display: none; }
}

/* ---------- explore: framework overview vs one open pool ----------
   The board is one object doing two jobs. Overview shows the whole framework
   centred with nothing beside it; opening a pool clears the other five, zooms
   into that sector on the right, and brings the copy in on the left. The two
   modes are a class on the view, so nothing has to be rebuilt between them. */

#v-explore .col {
  pointer-events: auto; z-index: 1;
  position: absolute; top: var(--pad-t); left: var(--pad-x); bottom: var(--pad-b);
  /* Keep in step with the 0.46 the scene solves the free width from. */
  width: 42%; max-width: 620px;
  display: flex; flex-direction: column; justify-content: center;
  opacity: 0; pointer-events: none;
  transition: opacity 0.5s var(--ease);
}
#v-explore.focus .col { opacity: 1; pointer-events: auto; }

/* ---------- the framework hexagon (SVG) ---------------------
   Replaces the WebGL board. Sits in frame pixels, so the SVG viewBox is
   1920x1080 and every coordinate in js/hexagon.js is the Figma frame's own.

   On for the overview only, exactly as the canvas was: opening a pool swaps
   the plate to that pool's render and the hexagon gets out of the way. */
#hexWrap {
  position: absolute; inset: 0;
  pointer-events: none;
  opacity: 0; transition: opacity 520ms var(--ease);
}
#stage[data-view='explore']:not([data-pool]) #hexWrap { opacity: 1; }
#stage[data-view='explore']:not([data-pool]) #hexWrap .hex-hit { pointer-events: auto; }
#hexSvg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* Wedges and labels animate about the hexagon's own centre (frame 960, 520):
   the SVG groups through transform-box: view-box so the origin is in viewBox
   units, the HTML labels through a matching origin on #hexWrap's box. Each
   carries --i (0-5 round the ring, 6 for the centre mark) for the stagger. */
.hex-seg {
  transform-box: view-box; transform-origin: 960px 520px;
  transition: opacity 320ms var(--ease), transform 550ms cubic-bezier(0.22, 1, 0.36, 1);
}
.hex-labels img { transition: opacity 320ms var(--ease), transform 550ms cubic-bezier(0.22, 1, 0.36, 1); }
/* Hover: the wedge drifts 4.5% out from the centre and back (js/hexagon.js
   moves its labels with it). Only while the board is the thing on screen. */
#stage[data-view='explore']:not([data-pool]) .hex-seg.hover { transform: scale(1.045); }
#stage[data-view='explore']:not([data-pool]) .hex-seg.hover .hex-hit { cursor: pointer; }
.hex-hit { fill: transparent; cursor: pointer; }

/* Entrance: each wedge scales in from the centre and fades, one after the
   other round the ring, the labels riding with their wedge and the centre
   mark last. Replayed by hexagon.enter() every time the overview comes on. */
/* Slow and deliberate (Rob, 9 Sep): each wedge takes 1.4s to settle and the
   next starts 420ms behind it, so the ring builds over about 3.5 seconds
   with the centre mark landing last. */
#hexWrap.enter .hex-seg {
  animation: hexIn 1400ms var(--ease-in-out-soft) both;
  animation-delay: calc(var(--i, 0) * 420ms);
}
#hexWrap.enter .hex-labels img {
  animation: hexIn 1400ms var(--ease-in-out-soft) both;
  animation-delay: calc(220ms + var(--i, 0) * 420ms);
  /* The base transform is inline (centre + rotate), so the keyframe scales
     with it rather than replacing it. */
  transform-origin: center;
}
@keyframes hexIn {
  from { opacity: 0; scale: 0.72; }
  to   { opacity: 1; scale: 1; }
}

/* Exit: the tapped wedge lifts and holds while the other five draw back
   into the centre and fade, in the 520ms before #hexWrap fades out. */
#hexWrap.picked .hex-seg { opacity: 0; transform: scale(0.82); }
#hexWrap.picked .hex-seg.on { opacity: 1; transform: scale(1.06); }
#hexWrap.picked .hex-labels img { opacity: 0; }
#hexWrap.picked .hex-labels img.on { opacity: 1; }

/* Labels are the file's own Tungsten outlines, exported from Figma node
   287:710 as SVG (images/six-pools/labels/) and placed by js/hexagon.js at the
   file's geometry: left/top are the label's centre, width/height its box,
   and the transform centres and rotates it. Nothing here sizes type any more
   — Tungsten was never licensed into the build and Geist squeezed to 0.72
   was the stand-in; this is the design's actual lettering. */
.hex-labels { position: absolute; inset: 0; pointer-events: none; }
.hex-labels img {
  position: absolute; display: block;
  transform-origin: center;
  user-select: none; -webkit-user-drag: none;
}

/* overview only */
#v-explore.focus .hex-hint { opacity: 0; pointer-events: none; }
#v-explore .hex-hint { transition: opacity 0.4s var(--ease); }

/* focus only — the pool screen's chrome, from Figma 287:1051. One pill
   treatment for all three controls: 71px tall, radius 30, 0.5px white
   hairline, the design's pill shadow, transparent fill. */

/* "Back to the framework": top right, 361x71 at (1517, 30), 18px brand blue
   with the design's 8px dot in front of it. */
#v-explore .focus-foot {
  position: absolute; top: 30px; right: 42px;
  opacity: 0; pointer-events: none;
  transition: opacity 0.5s var(--ease);
}
#v-explore.focus .focus-foot { opacity: 1; pointer-events: auto; }
.ff-back {
  display: inline-flex; align-items: center; justify-content: center; gap: 20px;
  width: 361px; height: 71px; padding: 0 26px;
  border-radius: var(--radius-pill);
  border: 0.5px solid #FFFFFF;
  box-shadow: var(--pill-shadow);
  background: transparent;
  font-family: var(--label); font-weight: 400; font-size: 18px; line-height: 1.4;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--brand);
  white-space: nowrap; cursor: pointer;
  transition: background 0.2s var(--ease);
}
.ff-back:active { background: rgba(255, 255, 255, 0.45); }
.ff-back:focus-visible { outline: 1px solid var(--blue-lift); outline-offset: 3px; }
.ff-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--brand); flex: none; }

/* Previous / Next: bottom left, 70x71 at (82, 937) and 143x71 at (168, 937). */
.pool-nav {
  position: absolute; left: 82px; top: 937px; z-index: 2;   /* bottom left, as the design has them (Figma 287:1083) */
  display: flex; align-items: center; gap: 16px;
  opacity: 0; transition: opacity 0.5s var(--ease); pointer-events: none;
}
#v-explore.focus .pool-nav { opacity: 1; }
#v-explore.focus .pn-arrow { pointer-events: auto; }
.pn-arrow {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  height: 71px;
  border-radius: var(--radius-pill);
  border: 0.5px solid #FFFFFF;
  box-shadow: var(--pill-shadow);
  background: transparent;
  font-family: var(--label); font-weight: 500; font-size: 16px; line-height: 1.4;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--btn-ink);
  cursor: pointer;
  transition: background 0.2s var(--ease), color 0.2s var(--ease);
}
.pn-arrow[hidden] { display: none; }   /* the pill's display beat the hidden attribute */
.pn-prev { width: 70px; }
.pn-next { width: 143px; }
.pn-arrow .arrow { font-size: 18px; }
.pn-arrow:active { background: rgba(255, 255, 255, 0.45); }
.pn-arrow:focus-visible { outline: 1px solid var(--blue-lift); outline-offset: 3px; }
/* Hover, for anyone driving this with a mouse rather than a finger: the pill
   fills white and lifts a touch. On touch there is no hover, so :active
   above is what the finger gets. */
@media (hover: hover) {
  .pn-arrow:hover:not([disabled]) {
    background: #FFFFFF;
    box-shadow: 2px 6px 24px 0 rgba(0, 65, 108, 0.22);
    transform: translateY(-1px);
  }
}
.pn-arrow { transition: background 0.2s var(--ease), color 0.2s var(--ease), box-shadow 0.2s var(--ease), transform 0.2s var(--ease); }

.pool-read.swap { animation: swapIn 0.46s var(--ease) both; }
@keyframes swapIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

.pr-rule { height: 2px; width: 44px; margin-bottom: var(--s3); background: var(--blue); }
.pr-verb {
  font-family: var(--label); font-weight: 500; font-size: clamp(13.1px, 0.856cqw, 16.6px);
  letter-spacing: 0.13em; text-transform: uppercase;
}
.pr-name {
  font-family: var(--display); font-weight: 300;
  font-size: clamp(26px, 2.6cqw, 48px); line-height: 1.04; letter-spacing: -0.022em;
  margin-top: var(--s2); text-wrap: balance;
}
.pr-blurb { margin-top: var(--s3); font-size: clamp(14.3px, 1.018cqw, 20.1px); line-height: 1.55; color: var(--fg-2); max-width: 46ch; }
.pr-tiles-head { margin-top: var(--s5); }
.pr-tiles { margin-top: var(--s3); display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; }
.tile {
  --tile-accent: var(--blue);
  text-align: left; display: flex; flex-direction: column; gap: var(--s1);
  padding: var(--s3) var(--s3) var(--s4);
  border-top: 2px solid var(--tile-accent);
  background: rgba(0, 76, 119, 0.028);
  transition: background 0.22s var(--ease), transform 0.22s var(--ease);
}
.tile:active { background: rgba(0, 76, 119, 0.075); }
.tile.pending { border-top-color: var(--fg-4); background: rgba(0, 76, 119, 0.014); }
.tile .t-n {
  font-family: var(--label); font-weight: 500; font-size: clamp(13.0px, 0.759cqw, 14.5px);
  letter-spacing: 0.13em; color: var(--fg-4);
}
.tile .t-client {
  font-family: var(--display); font-weight: 500;
  font-size: clamp(14.8px, 1.145cqw, 21.2px); line-height: 1.15; letter-spacing: -0.014em; color: var(--fg);
}
.tile.pending .t-client { color: var(--fg-3); }
.tile .t-title { font-size: clamp(13.1px, 0.889cqw, 16.0px); color: var(--fg-2); line-height: 1.35; }
.tile .t-metric {
  margin-top: auto; padding-top: var(--s2);
  font-family: var(--label); font-weight: 500; font-size: clamp(13.0px, 0.828cqw, 15.9px);
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--tile-accent);
}
.tile.pending .t-metric { color: var(--fg-4); }

/* ---------- the case study reader ---------------------------
   Full screen, floating over whatever plate is showing. Figma node 333:276.
   Geometry is in frame pixels because #frame is a fixed 1920x1080 surface
   scaled by --k, the same way .qcol is placed.

   What was here before was an un-migrated V3 component: a dark navy card
   (#101B2E -> #070C16) left over from the dark theme, which on a light build
   put dark blue type on a dark blue ground. That was a missed migration, not
   a design decision. */
#tileSheet {
  position: absolute; z-index: 8; inset: 0;
  opacity: 0; pointer-events: none;
  transition: opacity 0.32s var(--ease);
}
#tileSheet.on { opacity: 1; pointer-events: auto; }
/* The render stays visible underneath: this only takes it back far enough
   that the sheet reads as the front layer. */
.ts-scrim { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.46); }

.ts-card {
  /* Centred in the frame vertically whatever its height (Rob, 9 Sep): the
     short form used to hang from the design's top edge. */
  position: absolute; left: 200px; top: 50%; transform: translateY(-50%);
  width: 1520px; height: 936px;
  padding: 70px 84px 62px;
  /* A case with no article is a lead and nothing else. Held at the design's
     fixed height that is three lines of text in a 936px sheet, which reads as
     a loading failure rather than a short case. Eleven of the eighteen cases
     are in that state today, so the short form is the common one. */
  border-radius: var(--radius);
  border: 1px solid var(--card-line);
  /* White first so type has something to sit on, then the design's card
     gradient over it. The white cannot go much lower without the small
     labels dropping under 4.5:1 — see the note in openTile. */
  background:
    linear-gradient(76.16deg, rgba(156, 209, 255, 0.30) 3.09%, rgba(91, 165, 230, 0.09) 85.6%),
    rgba(255, 255, 255, 0.93);
  box-shadow: 2px 12px 48px 2px rgba(0, 124, 195, 0.18);
  /* The -50% is the vertical centring; the rest is the entrance, which
     settles to the centred position when the sheet is on. */
  transform: translateY(calc(-50% + 14px)) scale(0.988);
  transition: transform 0.36s var(--ease-mech);
  display: flex; flex-direction: column;
  overflow: hidden;
}
#tileSheet.on .ts-card { transform: translateY(-50%); }
.ts-card.short { height: auto; }

.ts-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s4); }
.ts-eyebrow, #tileClose {
  font-family: var(--label); font-weight: 500;
  font-size: clamp(13.0px, 0.833cqw, 16px);
  letter-spacing: 0.13em; text-transform: uppercase;
}
.ts-eyebrow { color: var(--blue-lift); }
#tileClose {
  color: var(--btn-ink); cursor: pointer;
  display: inline-flex; align-items: center; gap: 10px;
  min-height: 44px; padding: 0 4px;   /* real target, not just its text */
}
#tileClose:active { color: var(--ink); }
#tileClose:focus-visible { outline: 1px solid var(--blue-lift); outline-offset: 3px; }

.ts-hero { margin-top: 40px; }
.ts-metric {
  font-family: var(--display); font-weight: 300;
  font-size: clamp(38px, 4.0625cqw, 78px); line-height: 1.02; letter-spacing: -0.026em;
  color: var(--ink);
}
.ts-title {
  margin-top: 10px;
  font-size: clamp(16px, 1.302cqw, 25px); line-height: 1.3; color: var(--fg-4);
}
.ts-rule { margin-top: 68px; height: 1px; background: var(--hair); }

.ts-body { display: flex; gap: 92px; margin-top: 50px; min-height: 0; }
.ts-article { width: 800px; flex: 0 0 800px; }
.ts-stats   { width: 460px; flex: 0 0 460px; }

.ts-lead {
  font-family: var(--display); font-weight: 300;
  font-size: clamp(16px, 1.302cqw, 25px); line-height: 1.5; letter-spacing: -0.005em;
  color: var(--ink);
}
.ts-sections { margin-top: 26px; display: flex; flex-direction: column; gap: 26px; }
.ts-sect h4 {
  font-family: var(--label); font-weight: 600;
  font-size: clamp(13.0px, 0.781cqw, 15px);
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--fg-4);
  margin-bottom: 10px;
}
.ts-sect p {
  font-size: clamp(14px, 0.99cqw, 19px); line-height: 1.6; letter-spacing: -0.002em;
  color: var(--mid);
}
.ts-stat + .ts-stat { margin-top: 24px; }
.ts-stat b {
  display: block;
  font-family: var(--display); font-weight: 300;
  font-size: clamp(24px, 2.1875cqw, 42px); line-height: 1.05; letter-spacing: -0.02em;
  color: var(--brand);          /* large text: 3.6:1 against a 3.0 floor */
}
.ts-stat span {
  display: block; margin-top: 2px;
  font-size: clamp(13.0px, 0.885cqw, 17px); line-height: 1.35; color: var(--mid);
}
/* Nothing to say in the right column when a case has no figures yet. */
.ts-stats:empty { display: none; }
.ts-stats:empty + * { flex: 1 1 auto; }
#tsFlag { margin-top: auto; }

.pr-sect { margin-top: var(--s4); }
.pr-sect h4 {
  font-family: var(--label); font-weight: 600; font-size: clamp(13.0px, 0.8cqw, 15.2px);
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--fg-3);
  margin-bottom: var(--s2);
}
.pr-sect p { font-size: clamp(13.7px, 0.935cqw, 17.7px); line-height: 1.62; color: var(--fg-2); max-width: 58ch; }
.pr-sect ul { list-style: none; display: flex; flex-direction: column; gap: var(--s2); }
.pr-sect li { font-size: clamp(13.7px, 0.935cqw, 17.7px); color: var(--fg-2); }

#v-explore .foot {
  flex: none; margin-top: var(--s4);
  padding-top: var(--s4); border-top: 1px solid var(--hair);
  display: flex; align-items: center; gap: var(--s5);
}
#v-explore .foot .hint {
  font-family: var(--label); font-weight: 500; font-size: clamp(13.0px, 0.828cqw, 15.9px);
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--fg-3);
}
/* The numeral was --blue-lift, which at 23.8px / 500 measured 4.78:1 over the
   busiest plate: a pass, but the thinnest margin on the screen, on the one
   number that tells a visitor how far through they are. Emphasis moves from
   hue to weight and the darkest ink, which takes it to about 13:1. */
#seenCount { color: var(--ink); font-weight: 600; }

/* ---------- diagnostic -------------------------------------- */

#v-diag { pointer-events: none; }
/* The design's own footprint, in the frame's own pixels: the content box sits
   at x 1096, y 156 and is 727 wide. Ours was half the frame at up to 900px, so
   the same copy spread across a third more width, the composition stopped
   matching the render behind it, and that read as the artwork being zoomed. */
#v-diag .qcol {
  pointer-events: auto; z-index: 1;
  position: absolute; top: 156px; left: 1096px; right: auto; bottom: 75px;
  width: 727px; max-width: none;
  display: flex; flex-direction: column;
}

/* ---------- fine print --------------------------------------
   The benchmark credit. Small, but it is a factual claim about where a number
   came from, so it still has to clear AA: 13px is the floor everywhere in this
   build and it holds here.
   ------------------------------------------------------------- */
.fine-print {
  margin-top: var(--s4);
  font-size: clamp(13px, 0.82cqw, 15.6px); line-height: 1.5;
  color: var(--fg-3);
}
#v-done .fine-print { margin-top: var(--s5); max-width: 62ch; margin-left: auto; margin-right: auto; }

/* ---------- case study, inside the question panel --------------
   A strip rather than a card, sitting between the answer and Next: someone has
   just decided and is about to move on, which is the moment a proof point is
   worth reading. Metric first because that is what carries at kiosk distance,
   and tapping opens the same detail sheet the pool screens use. Hidden outright
   on a pool with no cleared case study. */
.qcase {
  margin-top: var(--s5);
  display: flex; align-items: center; gap: clamp(14px, 1.2cqw, 24px);
  text-align: left; width: 100%;
  padding: clamp(14px, 1.3cqw, 26px) clamp(18px, 1.7cqw, 34px);
  background: var(--card);
  border: 1px solid var(--card-line);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  transition: box-shadow 0.22s var(--ease);
}
.qcase[hidden] { display: none; }
.qcase:active { box-shadow: 2px 4px 14px 2px rgba(0, 124, 195, 0.12); }
/* The pool's colour, as an edge rather than a fill: at this size a coloured
   panel competes with the slider for attention. */
.qc-bar { flex: none; width: 2px; align-self: stretch; min-height: 30px; background: var(--blue); border-radius: 2px; }
.qc-read { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* White, not blue. Blue-lift is the instrument and the primary action; a
   coloured number here read as a second headline competing with the question. */
.qc-metric {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(18px, 1.35cqw, 26px); line-height: 1.16; letter-spacing: -0.01em;
  color: var(--ink);
}
.qc-sub {
  display: flex; flex-wrap: wrap; gap: 0 8px;
  font-size: clamp(13.4px, 0.9cqw, 17.3px); line-height: 1.35; color: var(--fg-3);
}
.qc-sub b { font-weight: 500; color: var(--ink); }
.qc-sub b::after { content: ' ·'; color: var(--fg-4); font-weight: 400; }
.qc-more {
  margin-left: auto; flex: none;
  font-family: var(--display); font-weight: 300;
  font-size: clamp(13.4px, 0.94cqw, 18px); color: var(--blue-lift);
  display: inline-flex; align-items: center; gap: 10px; min-height: 44px;
}
.qcase:active .qc-more { color: var(--blue-lift); }

/* One height for all six questions, with the action row pinned to the bottom.
   Questions run to one line or two and one pool has no case study, so the panel
   used to change height and move Next with it; on a kiosk the hand should find
   the same place six times. The slack lands in the reading area under the
   slider, which is the one part of the panel that is meant to be quiet. */
/* The column is the footprint now, so the panel fills it rather than carrying
   its own height. The action row still lands in the same place on all six
   questions, which is what the min-height was for. */
#v-diag .qpanel {
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
}
#v-diag .mslider { margin-bottom: auto; }
/* One row: which pool, what it is about, where you are. The theme used to be a
   second uppercase heading of its own directly above the question, saying in
   two words what the question says in a sentence. */
/* One line, always, and always the same height.
   ---
   Five of the six questions fit this row comfortably: they need 422 to 657px
   of the column's 716. Question 4 needs 751 — "Agentic Legacy Modernization"
   with "The core platforms underneath" — and when it overflowed all three
   items wrapped independently. Aligned on their first baselines the second
   lines dangled, and "Question 4 of 6" broke after "of", orphaning the 6.

   Letting it wrap tidily instead was tempting and wrong: a two-line row on
   one question of six moves everything under it down by a line on that
   question only, which is the same fault the reserved height on .qtext
   already exists to prevent. So the row holds one line and its own height.

   The 35px comes from a tighter gap and slightly less tracking on the pool
   tag, which reads better at 28 characters anyway. The kind is the only item
   allowed to give: it can ellipsis rather than wrap, so a longer pool name
   later degrades instead of breaking the layout. Nothing ellipsises today. */
/* Two lines (Rob, 9 Sep): the pool name on one, the question's theme under
   it. In one row the long pool names pushed the theme into an ellipsis. */
.qmeta {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  min-height: 1.7em;
}
.qmeta > * { white-space: normal; }
.qkind { min-width: 0; }
.qmeta .pool-tag {
  font-family: var(--label); font-weight: 500; font-size: clamp(13.1px, 0.856cqw, 16.6px);
  letter-spacing: 0.10em; text-transform: uppercase; color: var(--blue-lift);
}
/* Sentence case on purpose. Seven uppercase letterspaced labels on one screen
   is what "everything is shouting" looks like; this is now one of three. */
/* Matched to the pool tag's size rather than sitting a point above it. Three
   items in one quiet row want one size, and at 17.3px this was the largest
   text in the row for no reason — which was also the last 10px question 4
   needed to stop ellipsising. */
.qkind {
  /* 25px in frame pixels (Rob, 10 Sep): the frame scales uniformly, so it is
     the same share of the screen at 1920, 1440 and 1280. */
  font-size: 25px; line-height: 1.3; color: var(--mid);
}
/* The separating dot went with the single row. */
/* Off-screen but readable. Used by the identify fields' labels. */
.sr-only {
  position: absolute !important; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}
.qtext {
  font-family: var(--display); font-weight: 300;
  /* Grown with the column. At 26ch the question was leaving a third of the
     panel empty to its right. */
  /* The design's 54px does not fit. Measured against the real Geist metrics in
     the 716px column, two of the six questions wrap to three lines at 54 —
     "How much of a full claims or underwriting journey does AI run?" needs
     1520px on one line — and a third line overruns the two-line reserve below
     and closes the gap to the slider's end labels to almost nothing.
     48px is the largest size at which all six fit two lines: 50 and 52 still
     leave the claims question on three. */
  font-size: clamp(26px, 2.5cqw, 48px); line-height: 1.06; letter-spacing: -0.014em;
  /* A clear step down from the meta row above it. At --s4 the gap was smaller
     than the question's own line height, so the row read as part of it rather
     than as the label it is. The panel is a fixed height with its slack under
     the slider, so this comes out of the quiet space and Next does not move. */
  margin: var(--s6) 0 0; max-width: none; text-wrap: balance;
  /* Exactly two lines, which is now what every question takes. Reserving it
     keeps Next in the same place between questions. If a future question
     needs a third line, this and the size above have to move together —
     a third line silently eats the gap under the question. */
  min-height: 2.12em;
}

/* options: rows separated by one hairline, no boxes */
/* ---------- maturity slider -----------------------------------
   Replaces the five tap-options. The five stops carry the same copy, so the
   expert anchoring survives; what changes is that it takes one interaction
   instead of five competing targets, and it reads as an instrument.
   ------------------------------------------------------------- */
/* Full width of the panel, so it lines up with the progress bar above it and
   the case study below. The cap it used to carry was there because a 1080px
   column made the drag most of an arm's length; at an even split the panel is
   narrow enough that the whole width is a comfortable reach. */
.mslider { margin-top: var(--s5); outline: none; }
.mslider:focus-visible .ms-track::before { box-shadow: 0 0 0 2px var(--blue-lift); }

/* Scale anchors, not headings. They were set exactly like the pool name, so
   the eye had no way to tell a label from a heading. */
/* The design sets these in Geist Medium, uppercase, tracked, in --mid. They
   were sentence case and the palest token, which on the washes read as haze. */
.ms-ends {
  display: flex; justify-content: space-between; gap: var(--s4);
  font-family: var(--label); font-weight: 500;
  font-size: clamp(13px, 0.84cqw, 16px); line-height: 1.4;
  letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--mid);
}
.ms-ends span:last-child { text-align: right; }

/* 56px tall so the whole strip is the touch target; the rail it draws is 6px. */
.ms-track {
  position: relative; height: 56px; margin: var(--s1) 0 var(--s2);
  cursor: pointer; touch-action: none;
}
.ms-track::before {
  content: ''; position: absolute; left: 0; right: 0; top: 50%;
  height: 6px; margin-top: -3px; border-radius: 3px;
  background: rgba(0, 76, 119, 0.20);
}
.ms-fill {
  position: absolute; left: 0; top: 50%; width: 0;
  height: 6px; margin-top: -3px; border-radius: 3px;
  background: var(--brand);
  transition: width 0.18s var(--ease);
}
.ms-ticks {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: space-between;
  pointer-events: none;
}
.ms-ticks i {
  display: block; width: 12px; height: 12px; border-radius: 50%;
  background: rgba(0, 76, 119, 0.34);
  transition: background 0.18s var(--ease), transform 0.18s var(--ease);
}
.ms-ticks i.on { background: var(--brand); }
.ms-thumb {
  position: absolute; top: 50%; left: 0;
  width: 34px; height: 34px; border-radius: 50%;
  transform: translate(-50%, -50%) scale(0.2);
  background: #FFFFFF;
  box-shadow: 0 2px 10px rgba(0, 65, 108, 0.28), 0 0 0 6px rgba(0, 124, 195, 0.14);
  opacity: 0; pointer-events: none;
  transition: left 0.18s var(--ease), transform 0.2s var(--ease), opacity 0.2s var(--ease);
}
.ms-thumb.set { opacity: 1; transform: translate(-50%, -50%) scale(1); }

/* Reserves the tallest reading in the whole question set, measured rather than
   guessed: "Nothing in production" plus its detail runs to 89px at 16px, which
   is 5.58em. At the old 5.4em the panel grew by 3px on that one stop. */
.ms-read { min-height: 5.7em; }
.ms-hint {
  font-size: clamp(14px, 0.96cqw, 17px);
  color: var(--mid);
}
.ms-hint.gone { display: none; }
.ms-label {
  font-size: clamp(16.6px, 1.15cqw, 22.3px);
  line-height: 1.3; color: var(--fg);
}
.ms-detail {
  font-size: clamp(15.5px, 1.02cqw, 19px);
  line-height: 1.58; color: var(--fg-2); margin-top: 5px;
}

/* ---- slider motion (Rob, 10 Sep) ----
   Entrance, replayed by buildSlider each time a question comes on: the track
   draws from the left, the five stops land in turn, the end labels rise. */
.mslider.enter .ms-track { animation: msTrackIn 0.9s var(--ease-in-out-soft) both; }
.mslider.enter .ms-ticks i { animation: msTickIn 0.5s var(--ease-in-out-soft) both; }
.mslider.enter .ms-ticks i:nth-child(1) { animation-delay: 0.20s; }
.mslider.enter .ms-ticks i:nth-child(2) { animation-delay: 0.32s; }
.mslider.enter .ms-ticks i:nth-child(3) { animation-delay: 0.44s; }
.mslider.enter .ms-ticks i:nth-child(4) { animation-delay: 0.56s; }
.mslider.enter .ms-ticks i:nth-child(5) { animation-delay: 0.68s; }
.mslider.enter .ms-ends span { animation: rise 0.7s var(--ease) both; }
.mslider.enter .ms-ends span:last-child { animation-delay: 0.12s; }
.mslider.enter .ms-hint { animation: rise 0.7s var(--ease) 0.5s both; }
@keyframes msTrackIn { from { transform: scaleX(0); opacity: 0; } to { transform: scaleX(1); opacity: 1; } }
.ms-track { transform-origin: left center; }
@keyframes msTickIn { from { transform: scale(0.2); opacity: 0; } to { transform: scale(1); opacity: 1; } }
/* The reading, each time the stop changes. */
.ms-read.swap .ms-label,
.ms-read.swap .ms-detail { animation: riseIn 0.42s var(--ease) both; }
.ms-read.swap .ms-detail { animation-delay: 0.06s; }

@media (prefers-reduced-motion: reduce) {
  .ms-fill, .ms-thumb, .ms-ticks i { transition: none; }
  .mslider.enter *, .ms-read.swap * { animation: none !important; }
}

/* Back and Next were two stacked left-aligned links of nearly equal weight, so
   neither read as the way forward. One row, a rule to separate the action from
   the reading, back on the left, forward on the right where a right-handed
   visitor's hand already is. */
.qactions {
  margin-top: var(--s5); padding-top: var(--s4);
  border-top: 1px solid var(--hair);
  display: flex; align-items: center; gap: var(--s4);
}
/* Back and Next are the pool screens' pills now, side by side on the left. */
.qactions { gap: 16px; justify-content: flex-end; }   /* Back and Next on the right, as on the pool screens */
.qactions .pn-arrow { pointer-events: auto; }
.qactions .pn-arrow[disabled] { opacity: 0.34; pointer-events: none; }
.qactions .pn-arrow[hidden] { display: none; }
.btn .arrow.back { font-size: 1.15em; }


/* Titles gone: the panel already names the pool and counts the questions, so
   down here they were saying it a third time. What is left is a six-step
   position indicator. */

/* ---------- results ----------------------------------------- */

#v-wrap { display: none; }
#v-wrap.on { display: block; }
.beat {
  position: absolute; inset: var(--pad-t) var(--pad-x) var(--pad-b);
  display: none; padding-bottom: clamp(34px, 3.4cqw, 62px);
}
/* content sits centred in the leftover height; the nav keeps its own band */
.beat.on {
  display: flex; flex-direction: column; justify-content: center; gap: var(--s6);
  animation: rise 0.72s var(--ease) both;
}

/* The white scrims that sat behind each results beat are gone (Rob, 9 Sep):
   they covered the frame's own width and showed as a white panel with a
   hard edge either side on any display wider than 16:9. The V4 plates are
   calm enough for the copy to sit straight on them. */

.beat-nav {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; justify-content: space-between;
}
.beat-pips { display: flex; gap: var(--s1); }
.beat-pips i { width: 26px; height: 2px; background: var(--fg-4); transition: background 0.3s var(--ease); }
.beat-pips i.on { background: var(--blue-lift); }

/* beat 1 — your position ----------------------------------------
   Heading section from Figma 454:92 (icon slot, name, tagline), the body
   and the featured benchmark panel as before. */
.arch-wrap { width: min(1320px, 88cqw); margin: 0 auto; display: flex; flex-direction: column; gap: var(--s6); }
#beat1 .b1 { text-align: left; }
.pos-label { display: block; font-size: 14px; letter-spacing: 0.13em; color: var(--brand); margin-bottom: 45px; }
.pos-row { display: flex; align-items: center; gap: 57px; }
/* The design's 247x219 image slot. */
.pos-glyph { flex: none; width: 247px; height: 219px; display: flex; align-items: center; justify-content: center; }
.pos-icon { display: block; max-width: 247px; max-height: 219px; width: auto; height: auto; }
.pos-icon[hidden] { display: none; }
.pos-glyph .arch-glyph { width: 200px; }
.pos-glyph .arch-glyph[hidden] { display: none; }
.arch-hex { display: block; width: 100%; height: auto; overflow: visible; }
.ag-seg { stroke: rgba(0, 105, 168, 0.26); }
.ag-seg.on { stroke: var(--brand); }
.pos-text { min-width: 0; display: flex; flex-direction: column; gap: 22px; }
.pos-name {
  font-family: var(--display); font-weight: 300;
  font-size: 54px; line-height: 1.06; letter-spacing: -0.014em; color: var(--ink); text-wrap: balance;
}
.pos-tag { font-weight: 500; font-size: 20px; line-height: 1.6; color: var(--blue-lift); }

/* Copy on the left, the strongest benchmark result on the right. The two
   paragraphs share ONE column (Rob, 9 Sep): side by side, the risk line kept
   ending on a single orphan word. */
#beat1 .arch-cols {
  display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: var(--s5);
  align-items: start;
}
#beat1 .arch-cols:has(.arch-stat[hidden]) { grid-template-columns: minmax(0, 1fr); }
#beat1 .arch-text { display: flex; flex-direction: column; gap: var(--s4); max-width: 74ch; }
#beat1 .arch-body { font-size: clamp(14.2px, 0.958cqw, 18.2px); }
#beat1 .arch-risk {
  padding-left: var(--s3); border-left: 2px solid var(--blue);
  font-size: clamp(14.2px, 0.98cqw, 18.2px); line-height: 1.6; color: var(--fg-2);
}

/* The featured benchmark number. On the card surface so it reads as a lifted
   panel rather than as a third paragraph. */
.arch-stat {
  background: var(--card); border: 1px solid var(--card-line);
  border-radius: var(--radius); box-shadow: var(--card-shadow);
  padding: clamp(18px, 1.72cqw, 33px) clamp(16px, 1.354cqw, 26px);
}
.arch-stat[hidden] { display: none; }
.as-num {
  font-family: var(--display); font-weight: 300;
  font-size: clamp(38px, 3.6cqw, 69px); line-height: 1.0; letter-spacing: -0.03em;
  color: var(--blue-lift);
}
.as-unit {
  margin-top: var(--s2);
  font-family: var(--label); font-weight: 600; font-size: clamp(12px, 0.75cqw, 14.4px);
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--fg-3);
}
.as-pool { margin-top: var(--s3); font-size: clamp(15px, 1.05cqw, 20px); color: var(--fg); }
.as-band { margin-top: 2px; font-size: clamp(13px, 0.82cqw, 15.4px); color: var(--fg-4); }

/* beat 2 — benchmark */
.bench-head { text-align: left; }
.bench-head .d-m { margin-top: var(--s2); }
.bench-praise { margin-top: var(--s3); font-size: clamp(13.8px, 0.996cqw, 19.1px); max-width: 68ch; }
.bench-praise b { color: var(--blue-lift); font-weight: 500; }
.bench-list { display: flex; flex-direction: column; }
/* Tighter, and the right column narrower now that it carries a delta and a
   small raw score rather than two stacked lines. */
.bench-row {
  display: grid; grid-template-columns: 13.5em 1fr 6.2em; align-items: center; gap: var(--s4);
  padding: clamp(8px, 0.7cqw, 15px) 0; border-bottom: 1px solid var(--hair);
}
.bench-row:first-child { border-top: 1px solid var(--hair); }
.bench-row .nm { font-size: clamp(13.6px, 0.92cqw, 17.2px); line-height: 1.25; color: var(--fg); }
/* Sentence case. Six uppercase band labels stacked down the left was a column
   of shouting next to the names they belong to. */
/* Red through green across the five bands (Rob, 9 Sep), so the column can be
   read at a glance instead of word by word.

   Measured against the PLATE, not against white. The row sits on the render at
   about #D5E7FD, whose luminance is 0.78 — so a colour picked on white reads
   roughly 13% worse here, and the first set of these values failed at 4.24 to
   4.43:1 while measuring 5.2 to 5.9 on paper. These are the lightest tint of
   each hue that still clears 4.60:1 on that plate (5.80 to 5.92 on white).
   Re-measure if the beat-2 render is ever replaced.

   The five are within 0.005 of each other in luminance, deliberately: at equal
   weight no band shouts louder than its neighbours. That does mean the ramp is
   carried by hue alone, which a red-green colour blind reader will not get —
   what carries it for them is the label, which spells the band out in words
   beside its own colour, so the hue is never the only thing saying it
   (WCAG 1.4.1). */
.bench-row .bd {
  font-size: clamp(13.0px, 0.82cqw, 15.4px); margin-top: 1px;
  font-weight: 500; color: var(--fg-4);
}
.bench-row .bd-0 { color: #C0271D; }   /* Absent      4.70:1 on plate */
.bench-row .bd-1 { color: #A34D00; }   /* Emerging    4.61:1 */
.bench-row .bd-2 { color: #7A6200; }   /* Developing  4.67:1 */
.bench-row .bd-3 { color: #0E7264; }   /* Established 4.62:1 */
.bench-row .bd-4 { color: #1A743A; }   /* Leading     4.63:1 */
.track { position: relative; height: 6px; background: rgba(0, 76, 119, 0.07); }
.track .fill { position: absolute; inset: 0 auto 0 0; width: 0; transition: width 1.1s var(--ease-mech); }
.track .bmark { position: absolute; top: -7px; bottom: -7px; width: 1px; background: var(--fg-3); }
.track .bmark.labelled::after {
  content: 'BENCHMARK'; position: absolute; top: -21px; left: 6px;
  font-family: var(--label); font-weight: 500; font-size: 13px;
  letter-spacing: 0.11em; color: var(--fg-3); white-space: nowrap;
}
/* The delta is what this screen is for, so it is the number that carries. The
   raw score sits under it, quiet, for anyone who wants the absolute. */
.bench-row .val { text-align: right; }
.bench-row .val b {
  display: block; font-family: var(--display); font-weight: 300;
  font-size: clamp(19px, 1.32cqw, 25px); line-height: 1.1; letter-spacing: -0.015em;
}
.bench-row .val .raw {
  font-family: var(--label); font-weight: 400; font-size: clamp(13.0px, 0.8cqw, 14.6px);
  letter-spacing: 0.1em; color: var(--fg-3);
}
.delta-up { color: var(--blue-lift); }
.delta-dn { color: var(--fg-3); }

/* beat 3 — forecast, columns divided by rules not boxes */
/* head and content share one column, left-aligned on the same edge.
   Scoped by class, not by beat id — the beats get reordered. */
.bench-wrap {
  width: min(1320px, 88cqw); margin: 0 auto;
  display: flex; flex-direction: column; gap: var(--s6);
}
/* This beat carries six rows and a source credit under them, which is more
   than the shared wrap was spaced for. */
.bench-wrap { gap: var(--s5); }
.bench-wrap .fine-print { margin-top: 0; }
/* ---------- beat 3: your result ------------------------------
   Figma 496:194. Copy column at the file's own coordinates (160, 274,
   760 wide), the framework ring on the right at (1194, 215) — the overview
   drawing moved 517 right and 21 down, so js/hexagon.js is reused as is and
   the host is simply translated. Tint per tier from 509:194: the texture at
   100 / 42 / 16% and the pool name dark on the two faded tiers.
   ------------------------------------------------------------- */
#beat3.on { display: block; }
.res-left {
  position: absolute;
  left: calc(160px - var(--pad-x)); top: calc(274px - var(--pad-t));
  width: 760px;
  display: flex; flex-direction: column; gap: 44px;
}
.res-left > * { animation: riseIn 0.72s var(--ease-in-out-soft) both; }
.res-left > :nth-child(1) { animation-delay: 0.10s; }
.res-left > :nth-child(2) { animation-delay: 0.22s; }
.res-left > :nth-child(3) { animation-delay: 0.34s; }
.res-left > :nth-child(4) { animation-delay: 0.46s; }
.res-eyebrow { font-size: 18px; font-weight: 400; letter-spacing: 0.13em; color: var(--brand); }
.res-head {
  font-family: var(--display); font-weight: 300;
  font-size: 54px; line-height: 1.08; letter-spacing: -0.014em; color: var(--ink);
  margin: 0; text-wrap: balance;
}
.res-body { max-width: 680px; font-weight: 300; font-size: 20px; line-height: 1.6; color: var(--ink); margin: 0; }
.res-legend { display: flex; flex-direction: column; gap: 22px; }
.res-row {
  display: flex; align-items: center; gap: 18px;
  animation: riseIn 0.6s var(--ease-in-out-soft) both;
  animation-delay: calc(0.62s + var(--i, 0) * 0.14s);
}
.res-swatch {
  flex: none; width: 30px; height: 30px; border-radius: 5px;
  border: 1px solid #fff; background: var(--brand);
  box-shadow: 0 2px 10px rgba(0, 124, 195, 0.18);
}
.t-maintain .res-swatch { background: rgba(0, 124, 195, 0.42); }
.t-resolve  .res-swatch { background: rgba(0, 124, 195, 0.16); }
.res-tier {
  flex: none; width: 128px;
  font-family: var(--label); font-weight: 600; font-size: 14px;
  letter-spacing: 0.13em; text-transform: uppercase; color: #00568C;
}
.res-pools { font-weight: 300; font-size: 20px; line-height: 1.4; color: var(--ink); white-space: nowrap; }
.res-pools b { font-weight: 300; margin: 0 0.6em; }

/* The ring. A full 1920x1080 layer like #hexWrap, anchored to the frame
   (the beat's own inset is undone) and slid to the design's position. */
.res-hex {
  position: absolute; width: 1920px; height: 1080px;
  left: calc(-1 * var(--pad-x)); top: calc(-1 * var(--pad-t));
  transform: translate(517px, 21px);
  pointer-events: none;
}
.res-hex .hex-hit { pointer-events: none; cursor: default; }
.res-hex .hex-seg[data-tier='maintain'] image { opacity: 0.42; }
.res-hex .hex-seg[data-tier='resolve']  image { opacity: 0.16; }
.res-hex .hex-seg image { transition: opacity 0.5s var(--ease); }
/* Builds in the same way the overview does, a touch quicker. */
.res-hex.enter .hex-seg {
  animation: hexIn 1000ms var(--ease-in-out-soft) both;
  animation-delay: calc(0.2s + var(--i, 0) * 240ms);
}
.res-hex.enter .hex-labels img {
  animation: hexIn 1000ms var(--ease-in-out-soft) both;
  animation-delay: calc(0.36s + var(--i, 0) * 240ms);
  transform-origin: center;
}
@media (prefers-reduced-motion: reduce) {
  .res-left > *, .res-row, .res-hex.enter .hex-seg, .res-hex.enter .hex-labels img { animation: none; }
}

/* three retrieval routes on the closing screen */
.retrieve { list-style: none; margin: var(--s4) auto 0; max-width: 42ch; text-align: left; }
/* Flex, not grid (Rob, 9 Sep). As a grid with baseline alignment Safari sized
   the text column at its min-content width, so every line broke one word per
   row and the list ran down over the fine print. A flex row with the number
   fixed and the text taking the rest lays out the same everywhere. */
.retrieve li {
  display: flex; gap: var(--s2); align-items: baseline;
  padding: var(--s2) 0; border-bottom: 1px solid var(--hair);
  font-size: clamp(13.8px, 0.954cqw, 18.0px); color: var(--fg-2);
}
.retrieve li > span:last-child { flex: 1 1 auto; min-width: 0; }
.retrieve li:first-child { border-top: 1px solid var(--hair); }
.retrieve .r-n {
  flex: 0 0 2.4em;
  font-family: var(--label); font-weight: 600; font-size: clamp(13.0px, 0.8cqw, 15.2px);
  letter-spacing: 0.12em; color: var(--blue-lift);
}
.retrieve b { color: var(--fg); font-weight: 500; }

/* beat 5 — proof */
.pending-note {
  margin-top: var(--s4); padding-left: var(--s3); border-left: 2px solid var(--blue-deep);
  font-family: var(--label); font-weight: 400; font-size: clamp(13.0px, 0.828cqw, 15.2px);
  letter-spacing: 0.08em; line-height: 1.9; color: var(--fg-3);
}
.pending-note b { color: var(--blue-lift); letter-spacing: 0.13em; }

/* ---------- delivery ---------------------------------------- */

#v-identify { display: none; }
#v-identify.on { display: grid; place-items: center; }
/* A near-black radial used to sit on both of these screens, from the dark
   theme. It survived the V4 migration because it is a pseudo-element: a sweep
   for dark backgrounds over #stage's elements does not see it, which is how it
   got missed twice. Both screens sit on real plates now, so it goes — it was
   dimming the whole identify screen to the point of being unreadable. */
/* Wider than the other statement screens: the reader needs room to be the
   subject rather than a detail under the copy. */
.deliver-wrap { text-align: center; width: min(860px, 58cqw); }

/* ---------- identify, from Figma 326:280 ---------------------
   Geometry in frame pixels, like the diagnostic column and the case reader:
   #frame is a fixed 1920x1080 surface scaled by --k, so the file's own
   coordinates transfer directly. Vertical gaps are the file's — 29 under the
   artwork, 10 to the heading, 24 to the form, 18 between fields, 47 to the
   button — rather than a scale step, because the file spaces them by eye.

   Two colours are not the file's, both measured against its own plate:

   - the placeholders are #A7BFCF in the design, which is 1.51:1 there. They
     carry the only instruction on the screen, so they take --fg-4 at 5.5:1,
     which is what the rest of the build already uses for a placeholder.
   - the eyebrow is #007cc3, which is 3.61:1 at 18px. It takes --blue-lift at
     4.70:1 — the same substitution the token block already documents for
     --brand carrying text.
   ------------------------------------------------------------- */
/* No view padding here: everything on this screen is placed at the file's own
   frame coordinates, so the padding would only offset them. #idAsk is a direct
   child of the view for the same reason — inside .deliver-wrap it was
   resolving against that instead of the frame and landing 578px low. */
#v-identify { padding: 0; }
/* Full-bleed so the affirmation centres on the frame — but it sits after
   #idAsk in the DOM, so it covered the form and swallowed every tap on the
   fields. It only needs to catch input when it is the thing on screen. */
#v-identify .deliver-wrap {
  position: absolute; inset: 0; width: auto;
  display: grid; place-items: center;
  pointer-events: none;
}
#v-identify .id-done { pointer-events: auto; }
#v-identify .id-ask {
  position: absolute; left: 0; right: 0; top: 198px;
  display: flex; flex-direction: column; align-items: center;
  text-align: center;
}
#v-identify .id-ask[hidden] { display: none; }
.id-art { display: block; width: 401px; height: 272px; }
.id-eyebrow {
  margin-top: 29px;
  font-family: var(--label); font-weight: 400; font-size: 18px; line-height: 1.4;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--blue-lift);
}
#v-identify .id-title {
  margin-top: 10px;
  font-family: var(--display); font-weight: 300;
  font-size: 54px; line-height: 1.06; letter-spacing: -0.014em; color: var(--ink);
}

.id-form { margin-top: 24px; display: flex; flex-direction: column; align-items: center; }
.id-field {
  position: relative; display: block;
  width: 384px; height: 69px;
  border: 2px solid var(--card-line); border-radius: 5px;
  box-shadow: 2px 4px 20px 0 rgba(0, 65, 108, 0.05);
}
.id-field + .id-field { margin-top: 18px; }
.id-field input {
  display: block; width: 100%; height: 100%;
  background: transparent; border: none; border-radius: inherit;
  text-align: center; color: var(--ink);
  font-family: var(--body); font-weight: 300; font-size: 24px;
  padding: 0 var(--s3);
}
.id-field input:focus { outline: none; }
.id-field:focus-within { border-color: var(--blue-lift); }
/* The real placeholder is hidden: the visible one is the row below, so the
   icon and the words travel together and stay centred as a pair. */
.id-field input::placeholder { color: transparent; }
.id-ph {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  pointer-events: none; opacity: 0;
  transition: opacity 0.18s var(--ease);
  font-family: var(--label); font-weight: 500; font-size: 16px;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--fg-4);
}
.id-field input:placeholder-shown ~ .id-ph { opacity: 1; }
.id-ph-ico { width: 19px; height: 19px; display: block; }
.id-ph-at { font-weight: 500; font-size: 18px; }

/* The shared .btn padding takes it to 81px; the file draws this one at 71. */
.id-form .id-go {
  margin-top: 47px;
  width: 287px; height: 71px; min-height: 71px;
  padding-top: 0; padding-bottom: 0;
}
.id-form .mf-error { margin-top: var(--s3); }
.deliver-wrap .lede { margin: var(--s3) auto 0; max-width: 46ch; }
.id-title { margin-top: var(--s2); }

/* ---------- email capture ----------------------------------
   One field, centred, sized like the rest of the build rather than like a form
   control. The badge reader that used to own this screen is gone: ITC Vegas has
   no NFC cards, and the brief is email only.
   ------------------------------------------------------------- */
/* Centred like everything else on the screen. Left-aligning a lone field under
   a centred headline read as a form that had wandered off the axis. */
.email-form { margin: var(--s6) auto 0; max-width: 520px; text-align: center; }
.ef-field { display: block; }
.ef-label {
  display: block; font-family: var(--label); font-weight: 500;
  font-size: clamp(13.0px, 0.8cqw, 15.2px); letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--fg-3);
}
.email-form input {
  display: block; width: 100%; margin-top: var(--s2); text-align: center;
  background: transparent; border: none; border-bottom: 1px solid var(--hair);
  color: var(--fg); font-family: var(--body); font-weight: 300;
  font-size: clamp(17px, 1.35cqw, 26px); letter-spacing: normal;
  /* 56px of field, so it is a real target for a finger on a touchscreen. */
  padding: var(--s3) 0; min-height: 56px;
  transition: border-color 0.24s var(--ease);
}
.email-form input::placeholder { color: var(--fg-4); }
.email-form input:focus { outline: none; border-bottom-color: var(--blue-lift); }
.email-form .btn.primary { margin-top: var(--s5); }
.mf-error {
  font-family: var(--label); font-weight: 400; font-size: clamp(13.0px, 0.8cqw, 15.2px);
  letter-spacing: 0.12em; color: #E0736B; margin-top: var(--s3); min-height: 15px;
}

/* ---------- the acknowledgement ------------------------------
   Shown in place of the field. It advances by itself, so this is read rather
   than acted on, and the tick is the only thing that moves.
   ------------------------------------------------------------- */
.id-done { display: flex; flex-direction: column; align-items: center; }
.id-done[hidden] { display: none; }
.id-done .lede { margin-top: var(--s3); }
.id-done #idDoneEmail { color: var(--blue-lift); }
.id-done .btn.primary { margin-top: var(--s5); }
.id-tick {
  width: clamp(56px, 4.6cqw, 88px); height: clamp(56px, 4.6cqw, 88px);
  margin-bottom: var(--s4);
  display: grid; place-items: center;
  border: 1px solid var(--glass-line); border-radius: 50%;
  background: rgba(0, 76, 119, 0.05);
}
.id-tick svg { width: 58%; height: 58%; overflow: visible; }
.id-tick path {
  fill: none; stroke: var(--blue-lift); stroke-width: 3.2;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 44; stroke-dashoffset: 44;
  animation: tickIn 0.5s var(--ease-in-out-soft) 0.12s both;
}
@keyframes tickIn { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
  .id-tick path { animation: none; stroke-dashoffset: 0; }
}

/* ---------- the badge reader ----------
   Sized to be the first thing seen, and centred so it lands on the middle of
   the identify plate, whose hexagon rings radiate from exactly there — plate
   and target then read as one object rather than two. */

/* ---------- done -------------------------------------------- */

/* ---------- done — Figma 487:195 -------------------------------- */
#v-done { display: none; padding: 0; }
#v-done.on { display: block; }
.done-left {
  position: absolute; left: 80px; top: 274px; width: 820px;
  display: flex; flex-direction: column; gap: 40px;
}
.done-head { display: flex; flex-direction: column; gap: 10px; }
.done-eyebrow {
  font-family: var(--label); font-weight: 400; font-size: 18px; line-height: 1.4;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--brand);
}
.done-title {
  font-family: var(--display); font-weight: 300; font-size: 54px; line-height: 1.06;
  letter-spacing: -0.014em; color: #000;
}
.done-body { width: 700px; font-weight: 300; font-size: 20px; line-height: 1.62; color: #000; }
.done-email { color: var(--blue-lift); }
/* The design's card surface, shared by both cards. */
.done-card {
  border-radius: var(--radius); border: 1px solid var(--card-line);
  background: linear-gradient(90deg, rgba(156, 209, 255, 0.30) 3.1%, rgba(91, 165, 230, 0.09) 85.6%);
  box-shadow: var(--card-shadow);
}
.done-specialist { display: flex; flex-direction: column; gap: 14px; padding: 36px 40px 74px; }
.done-label {
  font-family: var(--label); font-weight: 600; font-size: 14px; line-height: 1.4;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--blue-deep);
}
.done-card-title { font-weight: 300; font-size: 30px; line-height: 1.4; color: #000; }
.done-card-body { font-weight: 300; font-size: 20px; line-height: 1.62; color: #000; }
.done-report {
  position: absolute; left: 1039px; top: 274px; width: 640px;
  display: flex; flex-direction: column; align-items: center; gap: 24px;
  padding: 36px 40px 44px;
}
.done-report .done-label { width: 100%; }
#v-done .qr {
  width: 240px; height: 240px; display: grid; place-items: center;
  border: 1px solid var(--brand); border-radius: 6px;
  background: rgba(255, 255, 255, 0.10);
  font-family: var(--label); font-weight: 500; font-size: 15px; line-height: 1.4;
  letter-spacing: 0.13em; text-transform: uppercase; text-align: center; color: var(--brand);
  padding: 0 40px;
}
#v-done .qr.live { padding: 14px; background: #FFFFFF; }
#v-done .qr svg { width: 100%; height: 100%; display: block; }
.done-url { margin-top: -10px; font-family: var(--label); font-size: 13px; letter-spacing: 0.06em; color: var(--blue-lift); word-break: break-all; text-align: center; }
.done-url[hidden] { display: none; }
#v-done .retrieve { width: 100%; max-width: none; margin: 0; }
#v-done .retrieve li { padding: 20px 0; border-bottom: 1px solid #9CD1FF; font-size: 20px; font-weight: 300; color: #000; }
#v-done .retrieve li:first-child { border-top: none; }
#v-done .retrieve li:last-child { border-bottom: 1px solid #9CD1FF; }
#v-done .retrieve .r-n { flex: 0 0 26px; font-weight: 500; font-size: 15px; color: var(--brand); }
.done-foot { position: absolute; left: 80px; top: 937px; }
#v-done .restart-btn {
  width: auto; padding: 0 34px;
  background: #FFFFFF;   /* solid white (Rob, 9 Sep) */
  pointer-events: auto;
}
#v-done .restart-btn:active { background: #EEF6FD; }

/* ---------- narrow preview windows -------------------------- */

@container stage (max-width: 1100px) {
  #v-diag .qcol, #v-explore .col { width: 56%; }
  .pr-tiles { grid-template-columns: 1fr; }
  .bench-row { grid-template-columns: 11em 1fr 5em; gap: var(--s3); }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* -------------------------------------------------------------
   FULL-BLEED WASHES

   The plates and the film cover the whole window now, with the 16:9 #frame
   centred inside them. Two washes live INSIDE the frame — the explore
   screen's copy scrim and the case-study sheet's scrim — and at frame size
   they stopped 45px short of the window edge on a 16:10 laptop and showed a
   hard horizontal seam across the artwork. They are pushed out to the window
   edge instead: #frame carries --fx/--fy (the letterbox offset, in window
   pixels) and --k (its scale), so the offset back in frame pixels is fx / k.
   On a 16:9 display both are zero and this is a no-op.
   ------------------------------------------------------------- */
#v-explore .copy-scrim,
.ts-scrim {
  inset: calc(var(--fy, 0px) / var(--k, 1) * -1) calc(var(--fx, 0px) / var(--k, 1) * -1);
}

/* ---------- the case study page (#v-case) ----------------------
   Figma 411:1675. Positions are the file's own, in frame pixels. The
   photograph, the 46% white scrim and the left wash bleed to the window
   edge by the same offset as the other full-bleed layers.
   ------------------------------------------------------------- */
#v-case { display: none; padding: 0; }
#v-case.on { display: block; }
#v-case .case-photo,
#v-case .case-scrim,
#v-case .case-wash {
  position: absolute; z-index: 0; pointer-events: none;
  inset: calc(var(--fy, 0px) / var(--k, 1) * -1) calc(var(--fx, 0px) / var(--k, 1) * -1);
}
#v-case .case-photo { width: auto; height: auto; max-width: none; object-fit: cover; object-position: center; }
#v-case .case-photo[hidden] { display: none; }
#v-case .case-scrim { background: none; }   /* the design's 46% white is off (Rob, 9 Sep): the photo shows clean */
#v-case .case-wash {
  right: auto; width: calc(1178px + var(--fx, 0px) / var(--k, 1));
  background: linear-gradient(90deg, #E9F6FF 0%, rgba(233, 246, 255, 0) 100%);
}
/* Everything else sits above the three layers; each block positions itself. */
#v-case > :not(.case-photo):not(.case-scrim):not(.case-wash) { z-index: 1; }

.case-stack {
  position: absolute; left: 80px; top: 120px; width: 942px;
  display: flex; flex-direction: column; align-items: flex-start;
}
.case-eyebrow {
  margin-bottom: 26px;
  font-family: var(--label); font-weight: 500; font-size: 16px; line-height: 1.45;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--blue-lift);
  white-space: nowrap;
}
.case-hero { width: 942px; display: flex; flex-direction: column; gap: 10px; }
.case-title {
  font-family: var(--display); font-weight: 300; font-size: 72px; line-height: 1.02;
  letter-spacing: -0.026em; color: var(--ink); text-wrap: balance;
}
.case-sub { font-size: 25px; line-height: 1.3; color: var(--fg-4); }
.case-col {
  margin-top: 34px; width: 580px;
  display: flex; flex-direction: column; gap: 22px;
}
.case-lead { font-weight: 300; font-size: 24px; line-height: 1.45; letter-spacing: -0.005em; color: var(--ink); }
.case-sections { display: flex; flex-direction: column; gap: 22px; }
.case-sect { display: flex; flex-direction: column; gap: 8px; }
.case-sect h4 {
  font-family: var(--label); font-weight: 600; font-size: 15px; line-height: 1.3;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--fg-4);
}
.case-sect p { font-size: 18px; line-height: 1.55; color: var(--mid); }
.case-rule { margin: 28px 0 0 -6px; width: 750px; height: 1px; background: rgba(0, 76, 119, 0.22); }
#v-case.no-outcome .case-rule { display: none; }
.case-outcome { margin-top: 22px; width: 826px; }
.case-outcome[hidden] { display: none; }
.case-outcome-label {
  font-family: var(--label); font-weight: 700; font-size: 18px; line-height: 1.5;
  letter-spacing: -0.005em; text-transform: uppercase; color: var(--brand);
}
.case-stats { display: flex; align-items: flex-start; }
.case-stat {
  display: flex; flex-direction: column; width: 220px; margin-right: -18px; flex: none;
  /* One by one (Rob, 10 Sep): the figures rise in after the copy above has
     landed, 180ms apart. Rebuilt per case, so it replays on each page. */
  animation: rise 0.6s var(--ease) both;
  animation-delay: calc(0.6s + var(--i, 0) * 0.18s);
}
.case-stat:nth-child(1) { --i: 0; }
.case-stat:nth-child(2) { --i: 1; }
.case-stat:nth-child(3) { --i: 2; }
.case-stat:nth-child(4) { --i: 3; }
.case-stat:last-child { margin-right: 0; }
.case-stat b { font-weight: 400; font-size: 45px; line-height: 1.4; letter-spacing: -0.005em; color: var(--brand); }
.case-stat span { font-weight: 300; font-size: 16px; line-height: 1.2; letter-spacing: -0.005em; color: var(--ink); max-width: 190px; }
.case-flag { display: none; }   /* the pending/substituted note is off the case pages (Rob, 9 Sep) */
.case-foot { position: absolute; right: var(--pad-x); top: 937px; }   /* bottom right (Rob, 9 Sep) */
.case-foot .case-go {
  pointer-events: auto; width: auto; padding: 0 30px;
  background: #FFFFFF;   /* solid, not the see-through pill: it sits over the photograph */
}
.case-foot .case-go:active { background: #EEF6FD; }
