/* ============================================================
   AURVIZ — homepage
   Design language modelled on the thin-frame smart-glasses genre:
   near-white canvas, light-weight grotesque, very tight tracking,
   generous vertical rhythm, monochrome line art.
   ============================================================ */

/* ---------- 1. Tokens ---------- */
:root {
  /* Neutrals */
  --er-white:     #ffffff;
  --er-gray-02:   #fafafa;
  --er-gray-05:   #f5f5f5;
  --er-gray-10:   #e6e6e6;
  --er-gray-20:   #d4d4d4;
  --er-gray-30:   #c4c4c4;
  --er-gray-50:   #919191;
  --er-gray-70:   #5c5c5c;
  --er-gray-80:   #404040;
  --er-gray-90:   #232323;
  --er-gray-92:   #333333;

  /* Warm band tone. Light enough that #232323 headings sit at 12.9:1 on it
     and #5c5c5c body at 5.5:1 — both clear of AA. */
  --beige:        #efe8dc;


  /* App tone. Was #ececec, a deliberate 1.18:1 step off white so the phone
     mockups read as objects standing on a surface. Now the same grey every
     product card sits on, so the light sections of the page are one tone rather
     than two near-whites that do not quite agree. The step off white drops to
     about 1.04:1 — the section is much closer to the page than it was, which is
     the trade. Type is unaffected: headings measure 15.3:1 on this and body
     6.4:1, both further clear of AA than before.
     Aliased rather than copied, so the two can never drift apart. */
  --app:          var(--er-gray-05);

  /* HUD accent — the monochrome projected display */
  --hud:          #b8f5cf;
  --hud-dim:      rgba(184, 245, 207, .55);
  --hud-bg:       #12161a;

  /* Semantic */
  --bg:              var(--er-white);
  --fg:              var(--er-gray-90);
  --fg-muted:        var(--er-gray-50);
  --rule:            var(--er-gray-10);

  --header-height:   3rem;
  --header-bg:       rgba(255, 255, 255, .91);

  /* THE ONLY TYPEFACE ON THE SITE. Inter Tight, and the generic keyword behind
     it for the case where Google Fonts does not answer.

     Three names came out of this stack. "FKGroteskNeue" and "Geom" were slots
     held for commercially licensed faces to be self-hosted into later, and
     neither was ever served — measured with a canvas width test, FKGroteskNeue
     fell straight through, so the page had always rendered in Inter Tight while
     the CSS named something else first. "SourceHanSansSC" covered CJK glyphs
     the site has no copy in. A --font-display token pointed at Poppins and is
     gone with them; every rule that asked for it now simply inherits.

     If a licensed face is ever bought, it goes at the FRONT of this line and
     nowhere else — that is the whole point of there being one declaration. */
  --font-body: "Inter Tight", sans-serif;

  /* Section rhythm */
  /* Applied top AND bottom, so adjacent sections stack it: at 9vw that put
     ~230px between every pair. Halved — boundaries now land near 128px. */
  --section-y:       clamp(2.75rem, 5vw, 5.5rem);
  /* Narrow page margin: headings and full-width plates sit close to the edge
     rather than inside a wide rail. The 1rem floor keeps body copy off the
     screen edge on a phone, where 1.45vw would fall to about 5px. */
  --gutter:          clamp(1rem, 1.45vw, 1.3125rem);
  --maxw:            90rem;
  --gutter-wide:     clamp(1rem, 1.45vw, 2.25rem);

  /* Motion */
  --ease:            cubic-bezier(.22, .61, .36, 1);
  --ease-out:        cubic-bezier(.16, 1, .3, 1);

  /* Glass ------------------------------------------------------------------
     Every chat bubble on the page is cut from the same material, so the recipe
     lives here rather than being re-typed at each of the five places they
     appear. Four things make it read as a thick slab of glass rather than a
     translucent rectangle, and all four have to be present:

       blur + saturation   what is behind shows through, and comes through
                           richer rather than washed out
       a gradient fill     brighter along the top edge, as glass is where it
                           catches the light
       an inset top rim    the bright line along the upper edge, which is what
                           gives it thickness
       a soft cast shadow  lifts it off whatever it is lying on

     Three grounds to sit on, so three variants: clear over a scrimmed picture,
     on paper, and the one veiled exception the hero clip forces.

     No bubble is tinted dark. Where type needs a dark ground to sit on, that
     ground is the SCRIM on the picture — a photographic gradient — never a shade
     inside the bubble. The bubble stays clear and the outline carries it. */
  --glass-blur:      blur(24px) saturate(180%);
  --glass-radius:    16px;

  /* Clear, over a scrimmed picture. Barely a fill at all — the rim is the shape.
     White type on this needs the scrim at about .7 where the bubble sits, which
     is what .case-scrim provides. */
  --glass-clear-fill: linear-gradient(rgba(255, 255, 255, .12),
                                      rgba(255, 255, 255, .05));
  --glass-clear-rim:  1px solid rgba(255, 255, 255, .45);
  --glass-clear-lift: inset 0 1px 0 rgba(255, 255, 255, .38);

  /* On paper. Nothing behind to refract, so the rim does the whole job and the
     fill is only there to separate it from the page. */
  --glass-paper-fill: linear-gradient(rgba(255, 255, 255, .55),
                                      rgba(255, 255, 255, .25));
  --glass-paper-rim:  1px solid rgba(16, 20, 26, .13);
  --glass-paper-lift: inset 0 1px 0 rgba(255, 255, 255, .9),
                      0 6px 18px -14px rgba(12, 16, 22, .35);

  /* The hero is the exception, and it is the footage that forces it. Measured
     across the whole clip, the ground under those bubbles runs from rgb(81,55,36)
     to rgb(193,187,176) — bright and dark in the same shot — so clear glass sits
     at 1.7:1 for white type and for dark type alike. It needs a real fill.

     .46 at the floor, which is the end of the road on this clip. Swept every
     0.1s reading every pixel, the darkest ground under these bubbles is
     rgb(43,39,35). .46 measures 4.61:1 there; .45 measures 4.46 and misses AA,
     so the usable range is one hundredth wide.

     Worth knowing why it cannot go further, because it looks like it should.
     The chat sits over an evenly lit wall whose median luminance is about 169,
     but the block also straddles darker detail: at the 5th percentile the ground
     is still rgb(63,59,59). Bright and dark inside one bubble. Dark type on no
     fill at all measures 1.4:1 on that darker part; white type on no fill
     measures 1.7:1 on the brighter part. Neither polarity survives clear glass
     here — the fill is what bridges them, and 45% is the least that does.

     Tied to THIS footage. Recut the hero and measure again; a different clip
     moves this number and nothing fails loudly when it does. */
  --glass-veil-fill:  linear-gradient(rgba(255, 255, 255, .57),
                                      rgba(255, 255, 255, .46));
  --glass-veil-rim:   1px solid rgba(255, 255, 255, .62);
  --glass-veil-lift:  inset 0 1px 0 rgba(255, 255, 255, .8),
                      0 8px 22px -16px rgba(12, 16, 22, .35);
}

/* ---------- 2. Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: var(--header-height);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  /* The site's one font-family declaration. Everything else inherits it —
     that is what keeps the page to a single typeface rather than eleven rules
     agreeing to. */
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 1rem;
  line-height: 1.6;
  letter-spacing: -.01em;
  /* Weight and small-caps may still be synthesised; slant may not. Inherited,
     so this covers the page. Stops a browser faking an oblique for any family
     that ships no italic — which is now every family the site loads. */
  font-synthesis: weight small-caps;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

img, svg, video { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
ul { margin: 0; padding: 0; list-style: none; }
h1, h2, h3, p, blockquote, figure { margin: 0; }

/* Nothing on this site is set in italic. These tags carry a UA italic by
   default, so they are reset here rather than at each use — <i> is only ever a
   decorative dot in this markup, and the rest would slant if the copy ever
   grew one. */
i, em, cite, var, dfn, address, q, blockquote { font-style: normal; }

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

/* ---------- Brand display type ----------
   The wordmark face is a display cut: heavy, geometric, upright.
   It marks brand/label moments — eyebrows, tickers, product designators,
   section labels — while headings and body stay on the grotesque, which is
   what keeps the page calm. Do not set paragraphs in this. */
.u-display {
  font-style: normal;
  font-weight: 500;
  letter-spacing: .01em;
  font-synthesis: none;   /* never fake the slant if the real face is present */
}

.skip-link {
  position: absolute; top: -100%; left: 1rem; z-index: 200;
  background: var(--fg); color: var(--er-white);
  padding: .625rem 1rem; font-size: .8125rem;
}
.skip-link:focus { top: .5rem; }

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

.container {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* ---------- 3. Type scale ----------
   Light weight + aggressive negative tracking is the whole signature. */
.d-h1 {
  font-size: clamp(1.875rem, 5.6vw, 5rem);
  font-weight: 300;
  line-height: 1.06;
  letter-spacing: -.05em;
}
.d-h2 {
  font-size: clamp(1.875rem, 4.4vw, 4rem);
  font-weight: 300;
  line-height: 1.07;
  letter-spacing: -.048em;
}
.d-h3 {
  font-size: clamp(1.25rem, 1.9vw, 1.875rem);
  font-weight: 300;
  line-height: 1.15;
  letter-spacing: -.035em;
}
.lede {
  font-size: clamp(.9375rem, 1.15vw, 1.125rem);
  font-weight: 300;
  line-height: 1.65;
  letter-spacing: -.012em;
  color: var(--er-gray-70);
  max-width: 34em;
}

/* ---------- 4. Header ---------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  width: 100%;
  height: var(--header-height);
  background: var(--header-bg);
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  border-bottom: 1px solid transparent;
  transition: border-color .3s var(--ease), transform .4s var(--ease);
}
.site-header.is-stuck { border-bottom-color: var(--rule); }
.site-header.is-hidden { transform: translateY(-100%); }

.header-inner {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 3rem);
  height: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Logo sprite host — must stay in flow-free isolation, never painted itself. */
.svg-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

.brand {
  display: flex;
  align-items: center;
  flex: none;
  /* The tap target does not shrink with the artwork. The mark is 19px tall on
     desktop and 17 on a phone, and the link wrapping it was exactly that — a
     17px-tall target against the 44px floor. min-height decouples the two: the
     mark keeps its size, the hit area does not. It costs no layout, because the
     header bar is 47px and this sits inside it. */
  min-height: 44px;
}
.brand-logo {
  /* The MARK, not the lockup, so the ratio changes from 8.345:1 to 2.112:1 and
     the height has to be reset with it. 16px was a sensible LOCKUP height
     because 16px there is the wordmark's cap height; the same 16 on the mark
     alone is a 34px squiggle, which reads as an icon someone forgot to finish.

     19px, down from 22. That first figure was set to match the optical presence
     the old lockup had; this is a step back from it — 40px of bar rather than
     46, and 40% of the header's height. Still well clear of the size where the
     mark's thin stroke starts to break up, which is around 12px. */
  height: 1.1875rem;
  width: auto;
  aspect-ratio: 194.78882 / 92.222599;   /* ≈ 2.112, the mark */
  fill: currentColor;
  color: var(--fg);
  transition: opacity .25s var(--ease);
}
.brand:hover .brand-logo { opacity: .62; }

.nav-primary {
  display: flex;
  align-items: center;
  gap: clamp(.875rem, 1.9vw, 1.875rem);
  margin-inline: auto;
}
.nav-primary a {
  position: relative;
  font-size: .8125rem;
  font-weight: 300;
  letter-spacing: -.005em;
  color: var(--fg);
  padding-block: .25rem;
  transition: color .25s var(--ease);
}
.nav-primary a::after {
  content: "";
  position: absolute; left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .35s var(--ease);
}
.nav-primary a:hover { color: var(--fg-muted); }
.nav-primary a:hover::after { transform: scaleX(1); }

.header-actions { display: flex; align-items: center; gap: .75rem; flex: none; }

.cart-link { display: flex; align-items: center; gap: .3125rem; }
.cart-link svg {
  width: 18px; height: 18px;
  fill: none; stroke: var(--fg); stroke-width: 1.2;
  stroke-linecap: round; stroke-linejoin: round;
}
.cart-count { font-size: .75rem; color: var(--fg-muted); }

.menu-toggle {
  display: none;
  /* The bars stay 24x18; the BUTTON is 44x44. This is the only control on a
     phone that opens navigation and it was a 24x18 target, well under the 44px
     touch minimum. Padding does the growing so the icon is untouched, and the
     negative margin puts the icon back on the gutter line it sat on before, so
     nothing in the header moves. 44px still clears the 48px header. */
  box-sizing: border-box;
  width: 44px; height: 44px;
  padding: 13px 10px;
  margin-right: -10px;
  flex-direction: column; justify-content: center; gap: 5px;
}
.menu-toggle span {
  display: block; height: 1px; width: 100%;
  background: var(--fg);
  transition: transform .3s var(--ease), opacity .2s;
}
.menu-toggle[aria-expanded="true"] span:first-child { transform: translateY(3px) rotate(45deg); }
.menu-toggle[aria-expanded="true"] span:last-child  { transform: translateY(-3px) rotate(-45deg); }

.mobile-menu {
  position: fixed;
  inset: var(--header-height) 0 auto 0;
  z-index: 49;
  background: var(--er-white);
  border-bottom: 1px solid var(--rule);
  padding: 1rem var(--gutter) 2rem;
  animation: menuIn .35s var(--ease-out);
}
.mobile-menu nav { display: flex; flex-direction: column; }
.mobile-menu a {
  padding: .875rem 0;
  font-size: 1.125rem;
  letter-spacing: -.03em;
  border-bottom: 1px solid var(--rule);
}
@keyframes menuIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }

/* ---------- 5. Buttons & links ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2.75rem;
  padding-inline: 1.75rem;
  border-radius: 999px;
  font-size: .875rem;
  font-weight: 400;
  letter-spacing: -.015em;
  white-space: nowrap;
  transition: background-color .3s var(--ease), color .3s var(--ease),
              border-color .3s var(--ease), transform .3s var(--ease);
}
.btn:active { transform: scale(.98); }

.btn-solid { background: var(--fg); color: var(--er-white); }
.btn-solid:hover { background: var(--er-gray-92); }

.btn-ghost { border: 1px solid var(--er-gray-20); color: var(--fg); }
.btn-ghost:hover { border-color: var(--fg); background: var(--er-gray-05); }

/* ---------- 6. Section rhythm ---------- */
.section {
  position: relative;
  width: 100%;
  padding-block: var(--section-y);
}
.section-full {
  min-height: calc(100svh - var(--header-height));
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* ---------- 7. Hero ---------- */
.hero {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: calc(100svh - var(--header-height));
  overflow: hidden;
  padding-bottom: clamp(1rem, 2vw, 1.75rem);
}

/* Hero ticker removed. The keyframes stay — .app-rail-track and .press-track
   both animate with it. */
@keyframes ticker { to { transform: translateX(-50%); } }

.hero-stage {
  position: relative;
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  align-items: center;
  justify-items: center;
  gap: clamp(1rem, 3vw, 3rem);
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  min-height: 0;
  padding: clamp(.5rem, 1.4vw, 1.125rem) var(--gutter);
}

.hero-glow {
  position: absolute;
  width: min(120vw, 1400px);
  aspect-ratio: 2 / 1;
  background: radial-gradient(ellipse at center,
              rgba(35, 35, 35, .055) 0%,
              rgba(35, 35, 35, .02) 42%,
              transparent 68%);
  filter: blur(6px);
  pointer-events: none;
}

/* ---- Hero video ----
   The clip keeps its native 16:9 so the frame is filled exactly: no crop, no
   letterbox. At 1280 wide that is 720px, close to the whole fold budget — so the
   copy has to overlay it, not stack beneath. */
/* Copy is overlaid, so the hero needs no trailing gutter — that 26px was the
   difference between fitting the fold and overshooting it. */
.hero:has(.has-video) { padding-bottom: 0; }

/* Two hero slides on a scroll-snap track.

   Native overflow scrolling rather than a JS drag: a touch swipe, a trackpad
   two-finger flick, shift+wheel and a keyboard all work without a line of
   code, and momentum feels like the platform because it IS the platform. The
   dots below only mirror where the track already is. */
.hero-carousel { position: relative; flex: 1 1 auto; display: flex; flex-direction: column; }
.hero-track {
  display: flex;
  flex: 1 1 auto;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  overscroll-behavior-x: contain;
  /* The bar would sit across the bottom of the video. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.hero-track::-webkit-scrollbar { display: none; }
.hero-slide {
  flex: 0 0 100%;
  min-width: 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Over the video, not under it: the hero fills the viewport and a control
   below the fold is a control nobody finds. */
.hero-dots {
  position: absolute;
  left: 50%;
  bottom: clamp(.75rem, 2vw, 1.25rem);
  transform: translateX(-50%);
  z-index: 3;
  display: flex;
  gap: .5rem;
  padding: .375rem .5rem;
  border-radius: 999px;
  /* The clip runs from near-white to near-black across its length, so the dots
     need their own ground to stay visible against both. */
  background: rgba(18, 20, 22, .28);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.hero-dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, .45);
  cursor: pointer;
  transition: background .25s var(--ease), transform .25s var(--ease);
}
.hero-dot.is-on { background: #fff; transform: scale(1.25); }
.hero-dot:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

/* The dot is 7px of ink but needs a finger's worth of target, so the tap area
   is grown with a pseudo-element rather than by making the dot bigger. */
.hero-dot::before {
  content: "";
  position: absolute;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
  margin-left: 3.5px;
  margin-top: 3.5px;
}

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

/* A PHONE NOW GETS BOTH SLIDES, the same two the desktop does: the product film
   first, the studio clip with the spoken exchange second. It used to get one
   video, no swipe and no dots, and this block was three rules enforcing that —
   the film slide hidden, the dots hidden, and the track's scroll-snap switched
   off so it could not be swiped. All three are gone.

   Nothing here replaces them, and that is the point: the carousel the desktop
   already uses is width-agnostic, so removing the overrides IS the change. The
   phone gets its own 9:16 cut of the film, swapped in by the inline script under
   #heroFilm exactly as the studio clip has always been swapped under #heroVideo.

   The narrow block further down already shapes a portrait clip correctly — a
   9:16 stage with the video contained inside it — and the new film is 720x1280
   like the studio cut, so it needs nothing of its own. */

.hero-stage.has-video {
  display: block;
  padding: 0;
  width: 100%;
  /* NO --maxw. This is the fix for the white bars down both sides of the hero on
     any laptop wider than about 1455px, which is most of them once scaling is
     counted - 1536, 1600, 1680, 1728, 1920 all land past it. .hero-stage caps at
     90rem because it is a CONTENT column and content wants a measure; the video
     variant is not a content column, it is the frame, and a frame that stops
     232px short of the window on each side reads as a border round the page.

     Measured at 1920x1080 before: hero 1904.7 wide, stage 1440, 232.3px of page
     white on each flank.

     It also fixes the framing, which is the part that is easy to miss. The video
     is object-fit: cover in a box whose height is the fold. Capped at 1440 on a
     1920 window that box is 1.40 wide-to-tall against the clip's 1.78, so cover
     was matching HEIGHT and throwing away a fifth of the frame's width. Full
     width the box is 1.85, cover matches width instead, and the trim goes back
     to the few pixels of height the 28% object-position was chosen for. Wider
     windows now show MORE of the shot rather than less. */
  max-width: none;
  position: relative;
  /* flex:1 1 auto would stretch this to fill the column and defeat the aspect
     ratio — it did, cropping half the frame at 375px. */
  flex: 0 0 auto;
}

/* Aspect lives on the VIDEO, not the container, so the box is 16:9 at every
   width regardless of what the parent does. contain guarantees no crop ever. */
.hero-video {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: contain;
  display: block;
  background: #e9e7e3;   /* matches the plate's wall, so any edge is invisible */
}

/* Lifts the right side toward white so dark copy stays legible over the wall,
   while leaving the subject untouched. */
/* Only needed where copy is overlaid; below 60rem everything stacks. */
.hero-scrim { display: none; }

@media (min-width: 60rem) {
  .hero-scrim { display: block; }}

/* One scrim for every view: dark, from the foot, clearing by half height.

   It used to be a light diagonal that whitened the right-hand 40% of the frame,
   because the headline sat over there in near-black type. The headline is now
   bottom-left in white on both phone and desktop, so that wash lit up a corner
   nothing needs and faded the footage doing it — the same fault that was removed
   from the phone.

   Dark rather than light because the type is white. The two views now sit on
   different footage, so they no longer share one set of stops — the phone block
   near the foot of this file keeps its own.

   This base pair serves DESKTOP, which carries the studio clip: a light seamless
   backdrop, not the dark plate this scrim was first cut for. Sampled through the
   headline's own band the footage reads 142-188, where the old clip read 33-49.
   At the previous .62/.34/50% the top line of the headline composited to 132 and
   measured 3.74:1 — under the 4.5:1 AA floor, and failing at the top because
   that is where the gradient has almost cleared.

   So the ramp is deeper and it clears later. Measured against the rendered
   frame, not against the file: the headline spans 27% to 5% up from the foot,
   and across that band white now runs 5.7:1 to 9.9:1.

   These stops are tied to the clip. Re-measure if the hero footage changes
   again — sample the video pixels under the headline through the cover crop
   rather than the raw file, because object-position shifts which part of the
   frame lands there. */
.hero-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top,
              rgba(18, 20, 22, .70) 0%,
              rgba(18, 20, 22, .48) 30%,
              rgba(18, 20, 22, 0) 58%);
}

/* Hero headline answers to viewport HEIGHT as well as width — on a 800px-tall
   laptop a purely vw-based size pushes the CTA off screen. */
.hero .d-h1 { font-size: clamp(1.875rem, min(5.6vw, 8svh), 5rem); }

/* White on EVERY view, and it belongs here in the base rather than inside a
   breakpoint. Both phone and desktop now sit this headline at the foot of the
   same clip, and that band is dark in both: sampled across the clip it runs a
   median luminance of 33-49, where gray-90 measures 1.01:1 and white measures
   10.5:1. It was briefly declared inside the 60rem block, which left the phone
   falling back to the dark base colour — invisible against the footage. */
.hero .d-h1 { color: var(--er-white); }

/* ---- Hero chat: makes the invisible interaction legible ---- */
.hero-chat {
  width: 100%;
  max-width: 24rem;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  /* Stacked below the video on narrow screens, so it needs its own gutters —
     it is not a .container. */
  padding-inline: var(--gutter);
  margin-top: clamp(1.5rem, 4vw, 2.5rem);
}

@media (min-width: 60rem) {
  .hero-chat { padding-inline: 0; margin-top: 0; }}

/* Over-video placement. 47% was right for the old plate, where the subject sat
   around the left third — on the studio clip he is centred and 47% put the
   bubbles squarely on his cheek, covering 98.7% of the box with face.

   Moved to the right of him, which also balances the headline in the opposite
   corner and matches the arrangement the phone block already uses.

   76% rather than something looser because the clip is object-fit:cover against
   a stage sized by 100svh, so the WINDOW's aspect ratio decides how much of the
   frame is visible and therefore how wide he is on screen: he spans 34-66% of
   the stage on a 1280x720 window but 25-76% on a 960x900 one, where the crop
   zooms in. Scored against the worst of eight desktop sizes, 72% still catches
   him 8% of the time and 74% 0.9%; 76% is the first that is clean everywhere.

   Re-measure if the footage changes — and measure across window shapes, not
   just the one in front of you. */
@media (min-width: 60rem) {
  .hero-stage.has-video .hero-chat {
    position: absolute;
    left: 76%;
    top: 50%;
    transform: translateY(-50%);
    width: 17%;
    max-width: 17rem;
    z-index: 2;
  }
  /* The clip is sized by the WINDOW's height, not by its own width.

     It was 16:9 with object-fit:contain, so its height came from the stage width
     — pinned at --maxw, that is 810px whatever the window does. Any window under
     858px tall therefore pushed the headline past the fold, which is most laptops
     once browser chrome is counted. It fit at 900px and failed at 850.

     Filling the available height instead means the hero is one frame at every
     window size, the same guarantee the phone already had. */
  .hero-stage.has-video { height: calc(100svh - var(--header-height)); }
  .hero-stage.has-video .hero-video {
    aspect-ratio: auto;
    width: 100%;
    height: 100%;
    max-height: none;
    object-fit: cover;
    /* 28% down, not centred. Once the box is wider than 16:9 — which it is on any
       window under about 1110px tall — cover starts trimming the height, and the
       subject's head sits only 8% from the top of the frame. A centred crop opens
       at 11% and takes the top off his head. 28% keeps the window opening near 7%
       across the range these windows actually span, and spends the trim on his
       chest instead, which nothing depends on. */
    object-position: center 28%;
  }

  /* Bottom left, matching the phone. It used to sit right of centre and
     vertically middled; anchoring it to the foot of the frame puts both views on
     the same composition instead of two unrelated ones.

     Anchored to the STAGE's left edge, not the viewport's. The copy is a child
     of .hero, which runs the full window, while .hero-stage stops at --maxw.
     Past about 1480px those two edges separate, and a plain `left: var(--gutter)`
     would strand the headline out on the background — the mirror of the bug the
     right-hand version had, where it ended up 164px clear of the video at 2560.
     STILL TRUE, AND FOR A SECOND REASON NOW. The video stage is no longer capped
     at --maxw, so this calc is not giving back a dead margin any more - it is
     what keeps the headline on the page's content column. Every .container on
     this page still stops at --maxw, so on a 1680 window their text begins at
     133px; plain gutter would start the headline at 21 and leave it 112px left
     of everything below it. Measured both ways before choosing. */
  .hero .hero-copy {
    position: absolute;
    left: max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter)));
    right: auto;
    top: auto;
    bottom: clamp(2rem, 4.5vw, 3.25rem);
    transform: none;
    /* Wide enough for "Human-AI interface." to hold one line at the larger size,
       and it stops well short of the conversation, whose left edge sits at 1083
       of a 1425 stage.
       The rem cap tracks the type: every time the headline grows, that line
       needs more room than min(42%, N) gives at the top end, and the column has
       to follow or the type grows into a wrap instead of into the frame. At the
       current cap it measures 597px. 38rem left 11px of slack, which is under
       2% of the line and close enough that a different font fallback could wrap
       it, so 39rem — 27px. Still 42% of a 1440 window and 33% of a 1920 one. */
    width: min(42%, 39rem);
    max-width: none;
    padding-inline: 0;
    z-index: 3;
  }

  /* Up from 3.1vw. At the old size it was 40px on a 1265px stage, which read as
     a caption once it moved off the middle of the frame and down to the corner. */
  /* The binding measurement is "Human-AI interface." against the column, not
     the headline against the stage — there is empty frame above it either way.
     Checked at each step rather than scaled by feel. */
  .hero .d-h1 { font-size: clamp(2.5rem, 4.9vw, 4.75rem); }}

/* ---- Phone hero ----
   Below 60rem the desktop rules above do not apply, and what was left was three
   things stacked in a column: a 16:9 video 211px tall, the conversation beneath
   it, then the headline. The clip read as a thumbnail and the exchange had no
   visible connection to it.

   The video becomes a portrait plate and the conversation sits ON it, which is
   the arrangement the desktop layout already uses — this only brings the phone
   into line rather than inventing a second design.

   Everything here is scoped under .hero-stage.has-video (0,3,0) so it outranks
   the base .hero-scrim / .hero-chat rules that follow later in the file, and
   the query stops at 59.9375rem so the 60rem desktop block is never touched. */
@media (max-width: 59.9375rem) {
  /* .hero is the positioning context for the headline (it is position:relative
     and the headline is absolute with a bottom offset), and it carries
     min-height: 100svh - header. Once the stage stopped filling that height,
     the section stayed 760 while the stage became 667 and the headline anchored
     to the section — 91px BELOW the video, white type on white paper.

     The section sizes to the stage now. This is the pair: a stage on the clip's
     own ratio and a section that follows it. Change one without the other and
     the copy comes off the picture. */
  .hero { min-height: 0; }

  /* The clip's OWN shape, not the screen's. It used to be held to
     100svh - header and filled with object-fit:cover — and a 9:16 file in a
     375x760 box is 0.5625 against 0.493, so cover was trimming 12% off the
     sides. Everything the camera framed is meant to be visible here, so the
     stage takes the file's ratio instead and nothing is cropped or scaled up.

     The cost is that the hero no longer fills the viewport: 375 wide makes it
     667 tall against the 760 it had, so about 90px of the next section shows
     under it. That is the trade "no cropping" asks for — a 9:16 clip cannot
     also fill a 9:19 screen. */
  .hero-stage.has-video {
    height: auto;
    aspect-ratio: 9 / 16;
  }
  .hero-stage.has-video .hero-video {
    aspect-ratio: auto;
    width: 100%;
    height: 100%;
    max-height: none;
    /* contain, not cover. With the box now matching the file the two are
       identical, but contain is the one that cannot crop if a rounded pixel
       ever puts them a hair apart. */
    object-fit: contain;
    /* Centre, now that phones load a purpose-cut 9:16 file. The old 31% existed
       only to aim a narrow window at the subject inside the landscape clip — the
       file is already framed on him, so anything other than centre would push
       him back off. cover still trims a little from the sides, because the box
       runs about 0.49 wide-to-tall against the file's 0.5625, and that trim
       should come off both edges evenly. */
    object-position: center;
  }

  /* Same dark bottom gradient as every other view — only the reach differs. The
     phone stacks the conversation above the headline, so it needs holding a
     little higher up the frame than the desktop, where the two sit side by side.
     The colour and the reasoning are shared; see the base .hero-scrim. */
  .hero-stage.has-video .hero-scrim {
    display: block;
    z-index: 1;
    background: linear-gradient(to top,
                rgba(18, 20, 22, .62) 0%,
                rgba(18, 20, 22, .40) 26%,
                rgba(18, 20, 22, 0) 52%);
  }

  /* The white headline, the white divider and its chip are shared rules now —
     both views put white type over the foot of the same clip, so they live in
     the base and this block no longer repeats them. */

  /* Sized up hard. It was sitting on the .d-h1 clamp floor of 30px, which is a
     body-copy decision applied to a headline: on a 375px screen it read as a
     caption. Sandbar runs 44px at this width and that is what makes their hero
     look certain rather than tentative. 9.4vw lands at 35px here and keeps
     climbing to 52px on a tablet. Not 44px flat — this headline is 36 characters
     across three lines where theirs is 20 across two, so the same size overruns
     the measure. */
  .hero .d-h1 { font-size: clamp(2.375rem, 11.2vw, 3.75rem); }

  /* The headline moves onto the clip, at the foot of it where the scrim is
     strongest. Absolute, so the hero stops being video-plus-a-block-of-white and
     becomes one screen. Reading order down the glass: the exchange, then what
     the thing is. */
  .hero .hero-copy {
    position: absolute;
    left: var(--gutter);
    right: var(--gutter);
    /* Lifted off the foot. It sat at 20px, which left the headline 2px clear of
       the bottom of the stage — fine in a desktop emulator and wrong on a real
       phone, where the browser's own bottom bar OVERLAYS the viewport and the
       safe-area inset makes the header about 110px rather than 52. The stage
       foot lands under Safari's toolbar and the last line goes with it.

       svh is what normally handles that, but the stage is sized by the clip's
       aspect ratio now, not by svh, so it cannot know about the toolbar. 52px
       of clearance is the fix that keeps the video uncropped.

       Sized from the reported screenshot rather than from this emulator, which
       has no browser chrome to hide behind: there the stage foot sat about 36px
       under Safari's toolbar, so anything less than that is still cut. 88px of
       offset leaves ~70px clear below the headline, which covers that overlap
       with room for the taller bars on other browsers. */
    bottom: clamp(5.5rem, 15vw, 6.5rem);
    width: auto;
    max-width: 30rem;
    margin-top: 0;
    padding-inline: 0;
    z-index: 3;
  }

  .hero-stage.has-video .hero-chat {
    position: absolute;
    /* Right side. left:auto is what does it — with left, right and a max-width
       all set, LTR honours left and ignores right, so the block stayed pinned
       to the left however narrow it got. */
    left: auto;
    right: var(--gutter);
    /* Lifted clear of the copy, which now occupies the foot of the clip. That
       block is just the headline at 112px sitting 20px off the bottom, so 132px
       in all, and the conversation starts a gap above it. Dropped from 14.5rem
       when the buttons came out — they were 68px of the block this was clearing,
       and without them the two drifted 100px apart. */
    /* Follows the headline up. At 10.25rem the two would have overlapped once
       the copy was lifted — the exchange's foot landed 4px past the headline's
       top. This tracks it up and keeps ~50px between them. */
    bottom: 15.625rem;
    width: auto;
    /* 26rem let the bubbles run the full width of the phone, which is what made
       them crowd the frame. 17rem is wide enough that the longest line still
       sets without wrapping to a third line, and narrow enough to leave the
       subject's face clear on the left. */
    max-width: 17rem;
    padding-inline: 0;
    margin-top: 0;
    z-index: 2;
    /* Back out to .5rem. It was tightened to .3125rem because five items at the
       looser spacing read as five separate objects rather than one exchange;
       with a question and an answer there is nothing to bind together, and the
       tight gap just made the pair look cramped. */
    gap: .5rem;
  }

  /* Scaled down with the block. Type stays at 11px — the floor everything else
     on the page was raised to — and the size comes off the padding and the
     avatar instead, which is where it can go without costing legibility. */
  .hero-chat .chat-line p {
    font-size: .6875rem;
    padding: .3125rem .5625rem;
    border-radius: 10px;
  }
  /* Less transparent than the wide layout, because this view plays a DIFFERENT
     clip. hero-portrait.mp4 is a much darker cut — its darkest ground under
     these bubbles is rgb(6,6,8), against rgb(43,39,35) on the landscape one —
     so the fill that clears AA over there does not clear it here.

     This was already wrong before the glass work and was never caught, because
     the hero was only ever measured at desktop width: the .51 floor shipping
     until now scored 4.26 on this clip. .56 is the floor for 5:1 against
     rgb(6,6,8); the wide layout keeps the thinner .47. Two clips, two floors. */
  .hero-chat .chat-you p {
    background: linear-gradient(rgba(255, 255, 255, .67),
                                rgba(255, 255, 255, .56));
  }
  .hero-chat .chat-aira p {
    background: linear-gradient(rgba(255, 255, 255, .72),
                                rgba(255, 255, 255, .61));
  }
  .hero-chat .chat-avatar {
    width: 1.125rem; height: 1.125rem;
    margin-top: .0625rem;
  }
  .hero-chat .chat-avatar svg { width: 9px; height: 9px; }
  .hero-chat .chat-avatar-aira svg { width: 10px; height: 6px; }
  .hero-chat .chat-line { gap: .375rem; }

  /* The headline is its own beat now, arriving after the clip rather than
     tucked under it. It needs real air above it or it reads as a caption to the
     video instead of the start of the page. */
  .hero .hero-copy { margin-top: clamp(2.25rem, 8vw, 3.5rem); }}
.chat-line {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
}

/* The beats used to run on fixed animation-delays, which drifted out of step
   with the plate the moment autoplay started late. They are driven off the
   video's own currentTime now (see script.js), so the first line lands on the
   frame his mouth opens and the whole exchange re-syncs on every loop.

   This hidden state is what makes that possible, and it is easy to delete by
   accident: it was written as ".chat-line, .chat-divider { }", so removing the
   divider by its selector took the declarations with it and left every bubble
   permanently visible. */
.chat-line {
  opacity: 0;
  transform: translateY(6px);
}
/* Transition lives on the revealed state only, so lines ease in but snap back
   when the clip loops — bubbles fading out together read as a glitch. */
.chat-line.is-said {
  opacity: 1;
  transform: none;
  transition: opacity .55s var(--ease-out), transform .55s var(--ease-out);
}

.chat-avatar {
  flex: none;
  width: 1.375rem; height: 1.375rem;
  border-radius: 50%;
  display: grid; place-items: center;
  margin-top: .125rem;
}
.chat-avatar svg { width: 11px; height: 11px; }
.chat-avatar-you {
  background: var(--er-gray-10);
}
.chat-avatar-you svg {
  fill: none; stroke: var(--er-gray-70);
  stroke-width: 1.3; stroke-linecap: round;
}
.chat-avatar-aira {
  background: var(--er-gray-90);
}
.chat-avatar-aira svg { fill: var(--er-white); width: 12px; height: 7px; }

.chat-line p {
  font-size: clamp(.75rem, 1vw, .875rem);
  line-height: 1.45;
  letter-spacing: -.015em;
  padding: .4375rem .75rem;
  /* Softer than the 12px it was, short of the 16px the full-size plates use —
     these bubbles are half the size, so the same radius would round them away. */
  border-radius: 14px;
}
/* Frosted glass, both speakers white. The bubbles sample the clip behind them
   rather than sitting on flat fills, so the exchange reads as something laid
   over the footage instead of pasted onto it.

   backdrop-filter needs something to filter: these sit above .hero-scrim in the
   stacking order, so what they blur is the scrim-over-video composite, which is
   why the effect survives even where the frame behind is dark.

   AIRA is a few points more opaque than the wearer. With both bubbles white the
   colour no longer says who is speaking, so that difference plus the avatar and
   the bold lead-in ("Saved.", "Drafted.") carry it — worth keeping in mind if
   the copy in them ever loses those opening words. */
.chat-line p {
  /* The gradient runs .70 down to .56. The floor is the number that matters:
     these sit on raw footage, and on the darkest frame the glass lets the ground
     drag the effective background down, so anything under about .55 puts dark
     type below 4.5:1. The top of the bubble can afford to be more transparent
     than the bottom, which is also where glass catches light, so the gradient
     buys the look without spending the contrast. */
  background: var(--glass-veil-fill);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: var(--glass-veil-rim);
  color: var(--er-gray-90);
  box-shadow: var(--glass-veil-lift);
}
/* AIRA reads a touch more solid than the wearer. With both bubbles white the
   colour no longer says who is speaking, so that difference, the avatar and the
   bold lead-in carry it. */
.chat-aira p {
  background: linear-gradient(rgba(255, 255, 255, .62),
                              rgba(255, 255, 255, .50));
}
.chat-aira p b { font-weight: 600; }

/* The avatars go to glass with them. The AIRA mark was white-on-near-black and
   would have vanished into a white bubble. */
.chat-avatar-you,
.chat-avatar-aira {
  background: var(--glass-veil-fill);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: var(--glass-veil-rim);
  box-shadow: var(--glass-veil-lift);
}
.chat-avatar-aira svg { fill: var(--er-gray-90); }
.chat-avatar-you svg { stroke: var(--er-gray-90); }

/* .chat-divider is gone with the "next morning" beat it labelled, and so are
   its chip and rule. The hero exchange ends on Saved now. */

.glasses { width: 100%; height: auto; }
.glasses-lines {
  fill: none;
  stroke: var(--fg);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.glasses .lens   { stroke-width: 2.2; }
.glasses .temple { stroke: var(--er-gray-50); }
.glasses .module { fill: var(--er-gray-10); stroke: var(--er-gray-30); stroke-width: 1; }
.glasses.sm .glasses-lines { stroke-width: 1.8; }

/* draw-on animation */
.hero-art .glasses-lines > * {
  stroke-dasharray: 1400;
  stroke-dashoffset: 1400;
  animation: draw 2.2s var(--ease-out) forwards;
}
.hero-art .glasses-lines > *:nth-child(2) { animation-delay: .08s; }
.hero-art .glasses-lines > *:nth-child(3) { animation-delay: .16s; }
.hero-art .glasses-lines > *:nth-child(4) { animation-delay: .24s; }
.hero-art .glasses-lines > *:nth-child(5) { animation-delay: .32s; }
.hero-art .glasses-lines > .module { stroke-dasharray: none; stroke-dashoffset: 0; animation: fadeIn .8s .9s backwards; }
@keyframes draw   { to { stroke-dashoffset: 0; } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

.hero-hud {
  position: absolute;
  left: 12%;
  top: 24%;
  display: flex;
  flex-direction: column;
  gap: .25rem;
  font-size: clamp(.625rem, .95vw, .8125rem);
  letter-spacing: -.005em;
  color: var(--er-gray-70);
  opacity: 0;
  animation: fadeIn .9s 1.5s forwards;
}
.hero-hud .hud-line b { font-weight: 500; }
.hero-hud .dim { color: var(--er-gray-30); }

.hero-copy {
  display: flex;
  flex-direction: column;
  /* Opened up from .875-1.5rem now the sub-line is gone. That gap was spacing
     three items, with the paragraph doing the real separating; left as it was,
     the buttons sat 14px under the headline and read as part of it. */
  gap: clamp(1.5rem, 3.5vw, 2rem);
}
.hero-cta { display: flex; flex-wrap: wrap; gap: .75rem; }

/* .scroll-hint is gone from every view — the 1px rule with the travelling dot
   that sat at the foot of the hero. A full-bleed clip already reads as
   something you scroll past, and the mobile layout had stopped showing it
   anyway. Its keyframes went with it; nothing else used them. */

/* ---------- 8. Statement / film ---------- */
/* A single centred line, not a two-column heading-and-gloss. The gloss is gone,
   so the grid goes with it.

   The line gets a band of its own, the way Sandbar's text divider does — theirs
   is 160px of padding above and below on an otherwise empty section, and that
   space is most of why one sentence reads as a statement rather than a caption.
   The padding sits on the container rather than the section because the graph
   below is full-bleed and must stay flush to the section's edge. */
.section-statement { background: var(--er-white); padding-block: 0; }
/* A full screen with one sentence in the middle of it. svh rather than vh so a
   phone's collapsing address bar does not leave the line off-centre. */
.section-statement .container {
  display: grid;
  place-items: center;
  min-height: 100svh;
  padding-block: clamp(3rem, 8vw, 6rem);
}
/* The container's bottom padding is the gap now; the block's own margin would
   stack on top of it and push the graph 238px down. */
.section-statement .graph-block { margin-top: 0; }

/* Sized to hold one line rather than to be as large as possible. The old .d-h2
   clamp reached 4rem, at which 32 characters need about 1020px and the line
   broke on anything narrower than a desktop. 3.2vw keeps it whole down to
   roughly 1100px, and text-wrap:balance shares the load evenly on the phone,
   where one line is not physically possible. */
.section-statement .d-h2 {
  text-align: center;
  font-size: clamp(1.5rem, 3.2vw, 3rem);
  margin-inline: auto;
  /* No max-width — the line is 32 characters and any ch cap short of that would
     force the wrap this rule exists to prevent. balance only takes effect once
     it does wrap, which on a phone it must. */
  text-wrap: balance;
}

/* The line darkens as you scroll it up the screen: light grey at the bottom of
   the viewport, near-black by the time it settles in the middle. script.js reads
   the scroll position and writes --statement-ink; this rule only says where that
   value goes.

   No opacity and no movement — the whole effect is the ink. So the .reveal it
   carries is neutralised rather than removed: the class stays for the observer's
   sake, but it must not fade or slide underneath the colour scrub.

   The fallback matters and is deliberately the END state. If script.js never
   runs, --statement-ink is unset and the line renders at gray-90 — fully
   readable, simply unanimated. Failing to the light grey instead would leave a
   headline at 1.7:1 on white. */
.section-statement .d-h2 {
  color: var(--statement-ink, var(--er-gray-90));
}
.section-statement .d-h2.reveal {
  opacity: 1;
  transform: none;
  transition: none;
}

/* This section ends on the full-bleed graph panel, so the panel IS its bottom
   edge — the container carries the statement's own padding instead, so nothing
   leaves white below the black. */

/* ---------- 9. Feature triptych ---------- */
/* The heading is followed by a lede now, not the grid, so it only needs the
   normal heading-to-body gap. The large separation moves below the lede. */
.section-features .d-h2 { margin-bottom: 0; }
.feature-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1.5rem, 3vw, 3rem);
}
.feature { display: flex; flex-direction: column; gap: 1.125rem; }
.feature-visual {
  /* Height follows the app mock inside it. A fixed aspect-ratio here clips the
     tall phone screen (it did, by 139px at 375px wide). */
  border-radius: 4px;
  background: linear-gradient(168deg, #f7f8f8 0%, #eceeef 100%);
  border: 1px solid var(--rule);
  display: grid;
  place-items: center;
  padding: clamp(1.5rem, 3vw, 2.25rem);
  margin-bottom: .5rem;
}
/* App surface — the glasses have NO display, so this must read as a phone
   screen, never as a projected overlay. Tall aspect + rounded frame do that. */
.app-screen {
  width: 100%;
  max-width: 11.5rem;
  aspect-ratio: 9 / 15;
  background: var(--hud-bg);
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, .08);
  padding: .875rem .875rem 1rem;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  box-shadow: 0 16px 40px -18px rgba(20, 24, 28, .6);
}
.app-screen-top {
  display: flex;
  align-items: center;
  gap: .375rem;
  font-size: .625rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  /* At this size 4.5:1 is not enough — that threshold assumes body text. */
  color: rgba(255, 255, 255, .78);
  padding-bottom: .5rem;
  margin-bottom: .125rem;
  border-bottom: 1px solid rgba(255, 255, 255, .14);
}
.rec-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: #ff5a4d; flex: none;
  box-shadow: 0 0 0 3px rgba(255, 90, 77, .18);
}
.app-row {
  font-size: .6875rem;
  line-height: 1.4;
  letter-spacing: -.005em;
  color: rgba(255, 255, 255, .9);
}
.app-row.dim { color: rgba(255, 255, 255, .42); }
.app-row.accent {
  color: var(--hud);
  font-size: 1.125rem;
  letter-spacing: -.03em;
  margin-block: .125rem;
}
.feature p { font-size: .9375rem; line-height: 1.65; color: var(--er-gray-70); }

.memory-lede {
  margin-top: 1.5rem;
  margin-bottom: clamp(2.5rem, 5vw, 4.5rem);
}
.feature-grid { list-style: none; }

/* Numbered so the three read as stages of one process, not parallel features. */
.feature-step {
  display: flex;
  align-items: baseline;
  gap: .625rem;
  font-style: normal;
  font-weight: 500;
  font-size: .6875rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.feature-step span {
  font-size: .8125rem;
  color: var(--fg);
  padding-right: .625rem;
  border-right: 1px solid var(--rule);
}

/* Who does what. Naming the actor on each line is what turns a description of
   the feature into an explanation of the mechanism. */
.feature-flow {
  margin: .75rem 0 0;
  border-top: 1px solid var(--rule);
}
.feature-flow > div {
  display: grid;
  grid-template-columns: 3.75rem 1fr;
  gap: .75rem;
  padding: .75rem 0;
  border-bottom: 1px solid var(--rule);
}
.feature-flow dt {
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-muted);
  padding-top: .1875rem;
}
.feature-flow dd {
  margin: 0;
  font-size: .875rem;
  line-height: 1.55;
  color: var(--er-gray-70);
}

/* ---------- 10. Store ---------- */
.store-head { display: flex; flex-direction: column; gap: 1.5rem; align-items: flex-start; }

/* ---- AIRA routing diagram ---- */
.aira-flow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.25fr);
  align-items: center;
  gap: clamp(1.5rem, 3.5vw, 3.5rem);
  margin-top: clamp(2.5rem, 5vw, 4rem);
}

.af-label {
  font-style: normal;
  font-weight: 500;
  font-size: .6875rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin-bottom: .625rem;
}
.af-quote {
  font-size: clamp(1rem, 1.6vw, 1.375rem);
  font-weight: 300;
  line-height: 1.35;
  letter-spacing: -.03em;
  color: var(--fg);
}

/* The hub. A line runs in from the instruction and out to the agent list. */
.af-hub {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .25rem;
  padding: 1rem .875rem;
  border: 1px solid var(--rule);
  border-radius: 10px;
  background: var(--er-white);
}
.af-hub::before,
.af-hub::after {
  content: "";
  position: absolute;
  top: 50%;
  width: clamp(.75rem, 1.75vw, 1.75rem);
  height: 1px;
  background: var(--er-gray-20);
}
.af-hub::before { right: 100%; }
.af-hub::after { left: 100%; }
.af-hub-mark {
  display: grid;
  place-items: center;
  width: 2rem; height: 2rem;
  border-radius: 50%;
  background: var(--er-gray-90);
}
.af-hub-mark svg { width: 15px; height: 8px; fill: var(--er-white); }
.af-hub-name {
  font-size: .9375rem;
  font-weight: 400;
  letter-spacing: -.02em;
  margin-top: .25rem;
}
.af-hub-sub { font-size: .6875rem; color: var(--fg-muted); }

/* Agents hang off a spine, so the fan-out is visible rather than implied. */
.af-out {
  display: flex;
  flex-direction: column;
  gap: .625rem;
  padding-left: clamp(.75rem, 1.75vw, 1.75rem);
  border-left: 1px solid var(--er-gray-20);
}
.af-out li {
  position: relative;
  display: grid;
  grid-template-columns: 5.25rem 1fr auto;
  align-items: center;
  gap: .75rem;
  padding: .625rem .875rem;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--er-white);
}
.af-out li::before {          /* tick joining the spine */
  content: "";
  position: absolute;
  right: 100%;
  top: 50%;
  width: clamp(.75rem, 1.75vw, 1.75rem);
  height: 1px;
  background: var(--er-gray-20);
}
.af-agent {
  font-style: normal;
  font-weight: 500;
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.af-did {
  font-size: .875rem;
  line-height: 1.4;
  letter-spacing: -.01em;
  color: var(--er-gray-70);
}
.af-state {
  font-style: normal;
  font-size: .75rem;
  color: var(--er-gray-30);
}
.af-state.is-done { color: #2fbf71; }

.af-rail-label {
  font-style: normal;
  font-weight: 500;
  font-size: .6875rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin-top: clamp(2.5rem, 5vw, 4rem);
}
.app-rail {
  margin-top: clamp(2.5rem, 6vw, 4.5rem);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.app-rail-track {
  display: flex;
  gap: 1rem;
  width: max-content;
  animation: ticker 46s linear infinite;
}
.app-rail:hover .app-rail-track { animation-play-state: paused; }
.app-tile {
  width: clamp(9rem, 14vw, 12rem);
  aspect-ratio: 1 / 1.12;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: linear-gradient(168deg, #fbfbfb, #f1f2f2);
  padding: 1.125rem;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  transition: border-color .3s var(--ease), transform .3s var(--ease);
}
.app-tile:hover { border-color: var(--er-gray-30); transform: translateY(-4px); }
.app-glyph { font-size: 1.5rem; line-height: 1; color: var(--er-gray-70); }
.app-tile b { font-size: .9375rem; font-weight: 400; letter-spacing: -.02em; }
.app-tile em { font-style: normal; font-size: .75rem; color: var(--fg-muted); display: block; margin-top: .125rem; }

/* ---------- 11. Technology / HUD ---------- */
.section-tech { gap: clamp(2.5rem, 5vw, 4rem); }
.tech-stage {
  display: grid;
  place-items: center;
  padding: clamp(1.5rem, 4vw, 3rem) var(--gutter);
}
.tech-lens {
  position: relative;
  width: min(100%, 48rem);
  aspect-ratio: 16 / 8;
  border-radius: 999px / 220px;
  background:
    radial-gradient(130% 120% at 30% 20%, #2c3238 0%, #1b1f23 55%, #101214 100%);
  display: grid;
  place-items: center;
  overflow: hidden;
  box-shadow: 0 40px 90px -50px rgba(16,18,20,.7);
}
.tech-lens::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(115deg, transparent 38%, rgba(255,255,255,.07) 50%, transparent 62%);
}
.tech-hud {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  text-align: center;
  color: var(--hud);
  font-size: clamp(.75rem, 1.3vw, 1rem);
  letter-spacing: -.01em;
  text-shadow: 0 0 14px rgba(184,245,207,.35);
}
.tech-hud-time { font-size: clamp(1.75rem, 4vw, 3.25rem); font-weight: 200; letter-spacing: -.05em; line-height: 1; }
.tech-hud-row.dim, .tech-hud .dim { color: var(--hud-dim); }
.tech-hud-divider { height: 1px; background: var(--hud-dim); opacity: .4; margin: .375rem auto; width: 60%; }
.tech-copy { display: flex; flex-direction: column; gap: 1.5rem; align-items: flex-start; }

/* ---------- 12. Frames A / B ---------- */

/* ---------- 13. Loop (ring) ---------- */
.loop-stage { display: grid; place-items: center; padding: clamp(2rem, 5vw, 4rem) var(--gutter); }
.ring {
  position: relative;
  width: clamp(11rem, 26vw, 20rem);
  aspect-ratio: 1;
  border-radius: 50%;
  background: conic-gradient(from 210deg, #f2f3f3, #cfd3d5, #8f9599, #d8dcde, #f2f3f3);
  display: grid;
  place-items: center;
  box-shadow: 0 40px 80px -46px rgba(35,35,35,.55);
  animation: ringSpin 26s linear infinite;
}
.ring-band {
  width: 62%; aspect-ratio: 1; border-radius: 50%;
  background: var(--er-white);
  box-shadow: inset 0 2px 14px rgba(35,35,35,.14);
}
.ring-sheen {
  position: absolute; inset: 0; border-radius: 50%;
  background: linear-gradient(140deg, rgba(255,255,255,.85) 0%, transparent 38%, transparent 62%, rgba(255,255,255,.4) 100%);
  mix-blend-mode: screen;
}
@keyframes ringSpin { to { transform: rotate(360deg); } }
.loop-copy { display: flex; flex-direction: column; gap: 1.5rem; align-items: flex-start; }

/* ---------- 14. Testimonials ---------- */
.section-voices { background: var(--er-white); }
.section-voices .d-h2 { margin-bottom: clamp(2.5rem, 5vw, 4rem); }
.voices { max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
.voices-viewport { overflow: hidden; }
.voices-track {
  display: flex;
  transition: transform .7s var(--ease-out);
  will-change: transform;
}
.voice {
  flex: 0 0 100%;
  display: grid;
  grid-template-columns: minmax(0, 22rem) 1fr;
  gap: clamp(1.5rem, 4vw, 4rem);
  align-items: center;
  opacity: .35;
  transition: opacity .6s var(--ease);
}
.voice.is-active { opacity: 1; }
.voice-portrait {
  aspect-ratio: 4 / 5;
  border-radius: 4px;
  background: linear-gradient(168deg, #eef0f1 0%, #d9dcde 100%);
  display: grid;
  place-items: center;
  border: 1px solid var(--rule);
}
.voice-portrait span {
  font-size: clamp(2rem, 4vw, 3.5rem);
  font-weight: 200;
  letter-spacing: -.05em;
  color: var(--er-gray-30);
}
.voice-body { display: flex; flex-direction: column; gap: .75rem; max-width: 40em; }
.voice-role { font-size: .8125rem; color: var(--fg-muted); letter-spacing: .01em; }
.voice-body > p:last-child { font-size: clamp(.9375rem, 1.05vw, 1.0625rem); line-height: 1.7; color: var(--er-gray-70); }

.voices-controls {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: clamp(1.75rem, 4vw, 3rem);
}
.voice-nav {
  width: 2.5rem; height: 2.5rem;
  border: 1px solid var(--er-gray-20);
  border-radius: 50%;
  display: grid; place-items: center;
  transition: border-color .3s var(--ease), background-color .3s var(--ease);
}
.voice-nav svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.2; stroke-linecap: round; stroke-linejoin: round; }
.voice-nav:hover { border-color: var(--fg); background: var(--er-gray-05); }
.voice-dots { display: flex; gap: .5rem; margin-inline: auto 0; }
.voice-dot {
  width: 1.75rem; height: 2px;
  background: var(--er-gray-20);
  transition: background-color .35s var(--ease);
}
.voice-dot.is-active { background: var(--fg); }

/* ---------- 15. Press ---------- */
.section-press { background: var(--er-white); }
.section-press .d-h2 { margin-bottom: clamp(2rem, 4vw, 3.5rem); }
.press-marquee {
  overflow: hidden;
  border-block: 1px solid var(--rule);
  padding-block: clamp(1.25rem, 2.5vw, 2rem);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.press-track {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  width: max-content;
  animation: ticker 40s linear infinite;
}
.press-logo {
  font-size: clamp(1rem, 1.8vw, 1.625rem);
  font-weight: 300;
  letter-spacing: -.035em;
  color: var(--er-gray-30);
  white-space: nowrap;
  transition: color .3s var(--ease);
}
.press-logo:hover { color: var(--fg); }
.press-dot { color: var(--er-gray-20); }

.press-quotes {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1.5rem, 3vw, 3rem);
  margin-top: clamp(2.5rem, 5vw, 4rem);
}
.press-quote { display: flex; flex-direction: column; gap: 1rem; }
.press-quote p {
  font-size: clamp(.9375rem, 1.15vw, 1.125rem);
  line-height: 1.6;
  letter-spacing: -.02em;
  color: var(--fg);
}
.press-quote cite { font-style: normal; font-size: .75rem; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-muted); }

/* ---------- 16. Footer ---------- */
.site-footer {
  background: var(--er-gray-90);
  color: var(--er-white);
  padding-block: clamp(3rem, 6vw, 5.5rem) clamp(2rem, 4vw, 3rem);
}
.footer-brand {
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
  padding-bottom: clamp(1.75rem, 3.5vw, 2.75rem);
  border-bottom: 1px solid rgba(255, 255, 255, .12);
}
.footer-logo {
  height: clamp(1.375rem, 2.6vw, 2.125rem);
  width: auto;
  aspect-ratio: 569.9667 / 68.3;   /* ≈ 8.345 */
  fill: currentColor;
  color: var(--er-white);
  transition: opacity .25s var(--ease);
}
.footer-brand a:hover .footer-logo { opacity: .7; }

.footer-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: clamp(1.5rem, 2.5vw, 2.5rem);
}
.footer-col { display: flex; flex-direction: column; gap: .625rem; }
.footer-col h3 {
  font-size: .75rem;
  font-weight: 400;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--er-gray-50);
  margin-bottom: .375rem;
}
.footer-col a {
  font-size: .875rem;
  font-weight: 300;
  letter-spacing: -.015em;
  color: rgba(255,255,255,.82);
  transition: color .25s var(--ease);
}
.footer-col a:hover { color: var(--er-white); }

.footer-bottom-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-end;
  gap: 2rem;
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: clamp(1.5rem, 3vw, 2.5rem);
  border-top: 1px solid rgba(255,255,255,.12);
}
.footer-social h3, .footer-region h3 {
  font-size: .75rem; font-weight: 400; letter-spacing: .1em;
  text-transform: uppercase; color: var(--er-gray-50); margin-bottom: .75rem;
}
/* The store row: where we sell, in what, how you pay. .footer-bottom-row and
   .footer-region above were written for a row of this shape and had sat
   unused; this is the markup they were waiting for.

   align-items goes to flex-start. The row set flex-end, and with three groups
   of unequal height - the payment list wraps to two rows on a narrow screen -
   bottom-aligning left the two short groups floating below their labels. */
.footer-bottom-row { align-items: flex-start; }
.footer-region p {
  margin: 0;
  font-size: .875rem;
  font-weight: 300;
  letter-spacing: -.015em;
  color: rgba(255, 255, 255, .82);
}
/* The language control. A trigger carrying the code and the current language,
   and a list that opens on click.

   The Devanagari, Han and Hangul glyphs are not in Inter Tight, so the stack
   names faces for those scripts ahead of the system default - a deliberate
   fallback rather than the browser's last resort. */
.picker { position: relative; }
.picker-trigger {
  display: flex;
  align-items: center;
  gap: .5rem;
  /* Padding out and margin back in: the hit area grows to a finger's size
     without the control moving a pixel. The bare button was 22px tall against
     the 44 the social icons in this same footer are built to. */
  padding: .6875rem .625rem;
  margin: -.6875rem -.625rem;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: .875rem;
  font-weight: 300;
  letter-spacing: -.015em;
  color: rgba(255, 255, 255, .82);
  transition: color .25s var(--ease);
}
.picker-trigger:hover { color: var(--er-white); }
.picker-code {
  font-size: .6875rem;
  letter-spacing: .08em;
  color: rgba(255, 255, 255, .58);
  border: 1px solid rgba(255, 255, 255, .2);
  border-radius: 3px;
  padding: .0625rem .3125rem;
}
.picker-chev {
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform .22s var(--ease);
}
.picker.is-open .picker-chev { transform: rotate(180deg); }
/* Set by script.js when a left-aligned menu would spill past the viewport.
   The footer row wraps with space-between, so on a phone this control sits at
   x=260 of 375 and a 200px menu opening leftward is the only one that fits. */
.picker-menu.is-flipped { left: auto; right: 0; }

/* Opens UPWARD: this sits at the bottom of the page and a menu dropping down
   would open past the end of the document. max-height is what makes it scroll
   rather than grow to ten rows. */
.picker-menu {
  position: absolute;
  bottom: calc(100% + .5rem);
  left: 0;
  z-index: 20;
  min-width: 12.5rem;
  max-height: 12.5rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0;
  padding: .3125rem 0;
  list-style: none;
  background: var(--er-gray-92);
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: .5rem;
  box-shadow: 0 14px 34px rgba(0, 0, 0, .5);
  font-family: "Inter Tight", "Noto Sans Devanagari", "Noto Sans SC",
               "PingFang SC", "Microsoft YaHei", "Noto Sans KR", system-ui, sans-serif;
}
.picker-menu li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
  padding: .5rem .75rem;
  font-size: .875rem;
  font-weight: 300;
  color: rgba(255, 255, 255, .82);
  cursor: pointer;
}
.picker-menu li[aria-selected="true"] { color: var(--er-white); }
/* Not yet a destination, so it does not behave like one. */
.picker-menu li[aria-disabled="true"] { color: rgba(255, 255, 255, .38); cursor: default; }
.picker-menu li:not([aria-disabled="true"]):hover,
.picker-menu li.is-active { background: rgba(255, 255, 255, .08); }
.picker-tag {
  font-size: .625rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .3);
}
/* The default scrollbar on a near-black panel is a bright bar. */
.picker-menu { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.22) transparent; }
.picker-menu::-webkit-scrollbar { width: 8px; }
.picker-menu::-webkit-scrollbar-track { background: transparent; }
.picker-menu::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, .18);
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: content-box;
}
.picker-trigger:focus-visible,
.picker-menu li:focus-visible { outline: 2px solid var(--er-white); outline-offset: 2px; }
/* Payment methods as cards: white tiles on the dark footer, in a grid, the
   treatment the reference uses.

   A tile is a fixed box and its contents are centred, so a word today and a
   logo tomorrow occupy the same space and the grid does not reflow when the
   artwork lands. min-height rather than height: a two-word label wraps inside
   the tile instead of overflowing it.

   White, not a tint. These marks are drawn for a white field and the moment a
   real one goes in, anything else would need per-brand background rules. */
.pay-cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(4.75rem, 1fr));
  gap: .5rem;
  max-width: 21rem;
}
.pay-card {
  display: grid;
  place-items: center;
  min-height: 2.5rem;
  padding: .375rem .5rem;
  background: #fff;
  border-radius: .5rem;
  font-size: .6875rem;
  line-height: 1.2;
  letter-spacing: .01em;
  text-align: center;
  color: var(--er-gray-90);
}
.pay-card img {
  display: block;
  max-width: 100%;
  max-height: 1.375rem;
  width: auto;
  height: auto;
}

/* Each icon sits in a circular button of its own rather than floating loose.
   Three things at once: the mark is bigger and easier to read, the button gives
   a 40px touch target against the 20x20 the bare anchors used to have, and the
   ring makes the target visible so it reads as something to press rather than
   decoration. 40px is the size Apple and Android ask for; the earlier 24 only
   cleared the WCAG floor. */
.social-icons { display: flex; gap: .875rem; }
.social-icons a {
  display: grid;
  place-items: center;
  /* 44px, which is both the touch-target floor and the ratio the reference
     runs: its glyph is about 0.43 of the circle, where this was 22-in-40, or
     0.55. The extra air inside the ring is most of what made the reference read
     as a set of buttons rather than three icons with a line drawn round them. */
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .14);
  /* One property drives the glyph now. Two of the three marks are FILLED (the X
     and the LinkedIn letters are solid shapes, not line drawings) and one is
     stroked, so a rule that only animated `stroke` left two of them dead on
     hover. Both fill and stroke read currentColor, and only this moves. */
  color: rgba(255, 255, 255, .72);
  transition: border-color .25s var(--ease), background-color .25s var(--ease),
              color .25s var(--ease);
}
.social-icons a:hover {
  border-color: rgba(255, 255, 255, .38);
  background: rgba(255, 255, 255, .07);
  color: var(--er-white);
}
/* The global :focus-visible is a dark outline, which is invisible on this
   footer — it is the one place on the page where the ground is near-black. */
.social-icons a:focus-visible {
  outline: 2px solid var(--er-white);
  outline-offset: 2px;
}
.social-icons a svg {
  width: 19px; height: 19px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The X mark and the LinkedIn letters are solid shapes. Stroking them would
   trace their outline and fill nothing, which is a different glyph. */
.social-icons a svg.is-solid { fill: currentColor; stroke: none; }

.region-select {
  appearance: none;
  background: transparent;
  color: var(--er-white);
  border: 1px solid rgba(255,255,255,.22);
  border-radius: 3px;
  padding: .5rem 2.25rem .5rem .75rem;
  font-size: .8125rem;
  font-weight: 300;
  letter-spacing: -.01em;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 4.5 6 8.5l4-4' fill='none' stroke='%23ffffff' stroke-width='1.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .625rem center;
  background-size: 12px;
}
.region-select option { color: var(--fg); }

.legal-bar {
  background: var(--er-gray-92);
  color: var(--er-gray-50);
  min-height: 3.5rem;
  display: flex;
  align-items: center;
}
.legal-inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  font-size: .75rem;
  letter-spacing: -.005em;
}
.legal-inner nav { display: flex; gap: 1.25rem; }
.legal-inner a:hover { color: var(--er-white); }

/* ---------- 16b. Where the brand display face is applied ----------
   Deliberately limited to labels, eyebrows and product designators.
   Headings and body copy stay on the grotesque — a heavy display cut
   set at paragraph length is unreadable and would fight the layout.
   Declared here, after the section rules, so it wins the cascade. */
.voice-role,
.app-tile b,
.press-quote cite,
.footer-col h3,
.footer-social h3,
.footer-region h3 {
  font-style: normal;
  font-weight: 500;
  font-synthesis: none;
}

/* The wordmark sets its caps quite open; match it on the uppercase labels. */
.press-quote cite,
.footer-col h3,
.footer-social h3,
.footer-region h3 {
  letter-spacing: .12em;
}

/* ═══════════ 16c. AURVIZ sections ═══════════ */

/* Eyebrow label above section headings */
.eyebrow {
  /* Upright everywhere. The italic was the site's stock caption voice and it
     sits wrong next to the upright light headings these sections use. */
  font-style: normal;
  font-weight: 500;
  font-size: .75rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin-bottom: .875rem;
}

/* ---- Hero ---- */
.hero-art img {
  width: 100%;
  height: auto;
  border-radius: 4px;
}


.btn-sm {
  height: 2.125rem;
  padding-inline: 1.125rem;
  font-size: .8125rem;
}


/* ---- Use-case carousel ---- */
/* .cases-lede is gone — the section header is a label and a heading now, and a
   third title between them left the hierarchy flat. */
.cases {
  position: relative;
  max-width: var(--maxw);
  margin: clamp(2.25rem, 4.5vw, 3.5rem) auto 0;
  padding-inline: var(--gutter);
}
/* Slides share one grid cell so the section keeps a fixed height and nothing
   reflows as they change. */
.case-track {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
}
.case {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  transition: opacity .6s var(--ease-out), visibility 0s linear .6s;
}
.case.is-active {
  opacity: 1;
  visibility: visible;
  transition: opacity .6s var(--ease-out), visibility 0s;
}
.case-media {
  position: relative;
  margin: 0;
  overflow: hidden;
  /* 2:1, matching the reference. The photographs are 16:9, so the box crops
     about 5.5% off the top and bottom rather than the frame being letterboxed —
     at 16:9 the plate stood 80px taller at the same width and read as a squarer
     block than the long strip it is meant to be. The phone rule below puts this
     back to auto: there the overlay sits under the picture and the whole
     uncropped scene is worth more than the shape. */
  aspect-ratio: 2 / 1;
  /* Softer than the 6px the smaller plates use: this is the largest image on the
     page, and at that size 6px reads as a square corner. */
  border-radius: clamp(8px, 1vw, 14px);
  background: var(--er-gray-05);
}
.case-media img,
.case-media .case-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;      /* fill the 2:1 box from a 16:9 source */
}

/* ---- Heading beside the carousel ----
   Stacked, this section came to 1051px against an 800px viewport, so the arrows
   and dots fell below the fold and the carousel read as one static photograph.
   Shrinking the frame to fit under the heading solved the height but left the
   picture small and floating in ~195px of dead space either side.

   Beside it, the frame takes the height of the whole section rather than what is
   left under the heading, and it fills its column instead of being centred in
   empty space. Nothing is cropped — the photograph keeps its 16:9. */
/* The plate is bright at the bottom-left where the copy sits, so the overlay
   needs its own ground rather than relying on the photograph. */
.case-scrim {
  position: absolute;
  inset: 0;
  background:
    /* Deepened from .5 to .74 at the 26% stop. The bubbles are clear glass now,
       so this gradient is the only thing standing between white type and a
       bright patch of photograph. Measured: white type on clear glass needs
       about .7 here to clear AA against the brightest pixel under the overlay. */
    linear-gradient(to top, rgba(16, 18, 20, .88) 0%, rgba(16, 18, 20, .74) 26%,
                    rgba(16, 18, 20, 0) 58%),
    linear-gradient(to right, rgba(16, 18, 20, .5) 0%, rgba(16, 18, 20, 0) 46%);
}
.case-overlay {
  position: absolute;
  left: clamp(1.25rem, 3.5vw, 3rem);
  bottom: clamp(1.25rem, 3.5vw, 2.75rem);
  right: clamp(1.25rem, 3.5vw, 3rem);
  max-width: 30rem;
  color: var(--er-white);
}
/* ---- Full-width plate, copy on the picture ----
   The heading introduces the scene rather than sitting beside it, and the
   photograph takes the whole measure. Its height is capped so the heading and
   the plate are on screen together; the track narrows to suit rather than the
   image being cropped, so the whole 16:9 frame stays visible. */
/* The section carries one line of words, the heading, and nothing else. Both
   .cases-lede and the caption that briefly replaced it are gone: the plate is
   the argument here, and a list of everything the glasses can catch was doing
   the picture's job in prose either side of it. */
/* .cases sits inside .container now, and both carried the page gutter. Doubled
   up, that cost the plate 32px of width on a phone. */
.cases-split .cases { max-width: none; margin: 0; padding-inline: 0; }

/* One line above the plate, sharing its left edge.

   This was a two-column grid — words left, plate right — which is why the
   heading needed a paragraph under it to fill its column. With the paragraph
   gone a column of text holding a single line looked like a mistake, and the
   plate was pushed into 70% of the width for no reason. Stacked, the plate
   takes the full measure and the heading sits over it as a caption would.

   Both are children of .container, so they share its gutter and line up on the
   left by construction, with no width formula to keep in sync. */
/* Sized against the SCREEN, not the page width, and centred.

   At full container width the plate was 1383x778 on a 1440x900 laptop, which
   made the section 1016px tall — taller than the viewport. The overlay
   animation plays at the bottom of the frame, so the one thing worth watching
   was the part that could not be on screen at the same time as the heading.

   The cap is whatever 16:9 box still leaves room for the section padding, the
   heading and the gap under it, so the whole plate and its overlay land in one
   view. min() keeps it from ever exceeding the measure, and on a phone the
   height term is far larger than the width, so nothing changes there.

   The cap sits on the wrapper rather than on .cases so the heading narrows
   with it — otherwise centring the plate would break the left edge they share. */
.cases-split {
  display: block;
  /* As large as one screen allows. The subtrahend is everything that has to
     share the viewport with the plate — the section's padding top and bottom,
     the heading, and the gap under it — so the rest of the height goes to the
     picture. Both were trimmed below to buy the plate room, and the subtrahend
     is a little more than they actually add up to, which is where the breathing
     space at the bottom of the screen comes from.

     78rem is the flat ceiling, set from the reference: that plate measures
     1200px in a 1920 window, so the wrapper wants 1200 plus the 43px the track
     insets by, which is 1243. The multiplier is 2, not 16/9, because the plate
     is a 2:1 strip now — get that wrong and the height budget is computed for a
     taller box than the one being drawn. */
  max-width: min(100%, 78rem, calc((100svh - 11.5rem) * 2));
  margin-inline: auto;
}
.cases-copy { margin: 0; }
.section-cases .d-h2 {
  margin: 0;
  /* Small enough to hold one line. It carried the section when it had a column
     to itself; introducing the plate it can sit back. */
  font-size: clamp(1.0625rem, 2.05vw, 1.875rem);
  line-height: 1.3;
}
/* Clear of the memory bubbles above and of the plate below. The margin has to
   be written on the .cases-split descendant, not on plain .cases: the rule
   above zeroes the margin there to stop the gutter being applied twice, and at
   two classes it outranks a bare .cases whatever the order. */
/* Both trimmed from what they were. Every pixel here is one the plate cannot
   have, and at 5rem of padding plus a 2.5rem gap the overhead came to 238px of
   a 900px screen — a quarter of the viewport spent on space around a picture
   that then had to shrink to fit what was left. Still clear of the memory
   bubbles above and still separated from the plate. */
.section-cases { padding-block: clamp(2rem, 4vw, 3.5rem); }
.cases-split .cases { margin-top: clamp(1rem, 2vw, 1.75rem); }

/* .case-stage is gone — the section eyebrow already names the three stages, so
   repeating "01 Memory" on the plate was labelling the same thing twice. The
   data-stage attribute stays on each <li>: it is unseen, but the generated dot
   labels read from it. */
/* Chat bubbles rather than stacked lines. Same shape language as the hero
   exchange, but inverted: these sit on a dark scrim, so the spoken side is a
   translucent chip and AIRA answers on solid white. The tail corner marks who
   is speaking without needing avatars at this size. */
.case-say,
.case-did {
  display: inline-block;
  max-width: 100%;
  padding: .5rem .8125rem;
  font-size: clamp(.875rem, 1.1vw, 1rem);
  font-weight: 300;
  line-height: 1.4;
  letter-spacing: -.012em;
}
/* Over the photograph. The spoken side is smoked glass with white type — the
   scrim under it is what keeps that legible — and the reply is the same
   material turned bright, rather than the flat white card it was. */
.case-say {
  border-radius: var(--glass-radius) var(--glass-radius) var(--glass-radius) 5px;
  background: var(--glass-clear-fill);
  border: var(--glass-clear-rim);
  color: var(--er-white);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-clear-lift);
}
.case-did {
  border-radius: var(--glass-radius) var(--glass-radius) 5px var(--glass-radius);
  /* The same clear glass as the question. It was a solid white card, which is
     the one thing on the plate that did not look like glass at all. Both sides
     match now, and the speaker is carried by which corner is squared off and by
     AIRA's bold opening word. */
  background: var(--glass-clear-fill);
  color: rgba(255, 255, 255, .88);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: var(--glass-clear-rim);
  box-shadow: var(--glass-clear-lift);
}
.case-did b { font-weight: 500; color: var(--er-white); }

/* Dots and reply share a cell so the reply does not shift when they clear. */
.case-reply {
  display: grid;
  justify-items: start;
  align-items: start;
  margin-top: .4375rem;
}
.case-reply > * { grid-area: 1 / 1; }
.case-typing {
  display: inline-flex;
  gap: .25rem;
  padding: .6875rem .8125rem;
  border-radius: var(--glass-radius) var(--glass-radius) 5px var(--glass-radius);
  /* Same glass as the reply it shares a cell with, so the dots do not read as a
     different object swelling into the answer. */
  background: var(--glass-clear-fill);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: var(--glass-clear-rim);
  box-shadow: var(--glass-clear-lift);
}
.case-typing i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .7);
}

/* Sized in em so it tracks the quote at every breakpoint, and inline so it
   costs no height — the block labels it replaced pushed the overlay to half the
   plate on a phone. */
.case-mic {
  width: .82em;
  height: .82em;
  margin-right: .38em;
  vertical-align: -.04em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
  opacity: .78;
}

/* On a 375px phone the plate is ~190px tall and the overlay wanted half of it —
   text on a picture, with neither readable. Overlaying is the wrong pattern at
   this size, so the caption drops below instead: the photograph is shown whole
   and uncropped, and the words get the page to sit on. An earlier attempt
   cropped to 4:3 to buy height, which only traded away a quarter of the scene. */
@media (max-width: 40rem) {
  .case-media { aspect-ratio: auto; overflow: visible; }
  .case-media img,
  .case-media .case-video { border-radius: 10px; height: auto; object-fit: fill; }
  .case-scrim { display: none; }
  .case-overlay {
    position: static;
    max-width: none;
    margin-top: 1rem;
    color: var(--er-gray-90);
  }
  /* The caption is on the page rather than on the picture at this size, so the
     bubbles invert to the same pair the white sections use. */
  .case-say {
    background: var(--glass-paper-fill);
    border: var(--glass-paper-rim);
    color: var(--er-gray-70);
    box-shadow: var(--glass-paper-lift);
  }
  .case-did,
  .case-typing {
    background: var(--glass-paper-fill);
    border: var(--glass-paper-rim);
    box-shadow: var(--glass-paper-lift);
  }
  .case-did { color: var(--er-gray-70); }
  .case-did b { color: var(--er-gray-90); }
  .case-typing i { background: var(--er-gray-50); }}

/* Plays as a conversation: she speaks, AIRA thinks for a beat, then answers.
   Animations rather than transitions, because the dots have to appear AND
   clear on their own schedule. Nothing runs until the section is on screen. */
.case-say, .case-did, .case-typing { opacity: 0; }

.cases.is-playing .case.is-active .case-say {
  animation: caseBubble .45s var(--ease-out) .25s both;
}
/* 1s + 1.35s lands the dots' exit exactly on the reply's entrance at 2.35s.
   At 1.45s they were still fading while the reply faded in, and since the two
   share a cell that read as one white shape swelling. */
.cases.is-playing .case.is-active .case-typing {
  animation: caseThinking 1.35s linear 1s both;
}
/* Bounded, not infinite: two cycles covers the thinking window, and there is no
   reason to keep three elements animating for the life of the page. */
.cases.is-playing .case.is-active .case-typing i {
  animation: caseDot 1.1s ease-in-out 1s 2;
}
.cases.is-playing .case.is-active .case-typing i:nth-child(2) { animation-delay: 1.15s; }
.cases.is-playing .case.is-active .case-typing i:nth-child(3) { animation-delay: 1.3s; }
.cases.is-playing .case.is-active .case-did {
  animation: caseBubble .45s var(--ease-out) 2.35s both;
}

@keyframes caseBubble {
  from { opacity: 0; transform: translateY(8px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
/* Fades up, holds while the dots pulse, then clears just as the reply lands. */
@keyframes caseThinking {
  0%   { opacity: 0; transform: translateY(8px); }
  14%  { opacity: 1; transform: none; }
  86%  { opacity: 1; transform: none; }
  100% { opacity: 0; transform: none; }
}
@keyframes caseDot {
  0%, 60%, 100% { opacity: .3; transform: translateY(0); }
  30%           { opacity: 1;  transform: translateY(-2px); }
}

/* ---- Recall ---- */
/* White, like the sections either side of it. It was a dark band, set against
   the carousel's light plates so the section read as a different kind of beat;
   the page runs light throughout now and the black stripe was the odd one out.
   Everything the darkness was carrying — the heading colour, the eyebrow, and
   the chat bubbles, which had been left unscrimmed because the band was already
   dark — has to be handed back. */
.section-recall { background: var(--er-white); color: var(--er-gray-90); }
.section-recall .eyebrow { color: var(--er-gray-70); }
.section-recall .d-h2 { color: var(--er-gray-90); }

/* Inverted on the white sections: light chip for the person, dark chip for
   AIRA. Over footage the spoken side is smoked glass and the reply is bright,
   and on paper both of those disappear.

   Written once for Recall and Act together — they are the same problem, and
   they had drifted into two copies of the same block. There is nothing behind
   either to refract here, so the rim and the cast shadow carry the material on
   their own. */
.section-recall .case-say,
.act-copy .case-say {
  background: var(--glass-paper-fill);
  border: var(--glass-paper-rim);
  color: var(--er-gray-70);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-paper-lift);
}
.section-recall .case-did,
.section-recall .case-typing,
.act-copy .case-did,
.act-copy .case-typing {
  background: var(--glass-paper-fill);
  border: var(--glass-paper-rim);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-paper-lift);
}
/* Both sides are the same clear glass now, so the speaker is carried by the
   corner that is squared off, the mic on the spoken line, and AIRA's bold
   opening word — not by one of them being a dark slab. */
.section-recall .case-did,
.act-copy .case-did { color: var(--er-gray-70); }
.section-recall .case-did b,
.act-copy .case-did b { color: var(--er-gray-90); }
.section-recall .case-typing i,
.act-copy .case-typing i { background: var(--er-gray-50); }

/* The two timestamps and the dashed rule between them are gone, along with
   .rs-point and .rs-rule. They spelled out the gap between saving and asking,
   which is what the heading says in words and the exchange demonstrates. */

.recall-grid {
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  align-items: center;
  gap: clamp(1.5rem, 3.5vw, 3.25rem);
  /* Sits under the heading now that the timestamps are not between them, at the
     same distance the plate keeps from its heading in the section above, so the
     two beats are set the same way down the page. */
  margin-top: clamp(1rem, 2vw, 1.75rem);
}
.recall-media { margin: 0; border-radius: 6px; overflow: hidden; }
.recall-video { display: block; width: 100%; height: auto; }

/* No scrim now: the words sit on the band, not on her face. */
.recall-copy { max-width: 26rem; }

/* Not a chat bubble: this is the system showing its working, so it reads as a
   quiet note between the two sides of the conversation rather than a third
   speaker. */
.recall-found {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: .625rem 0 0;
  font-size: .8125rem;
  line-height: 1.4;
  color: var(--er-gray-70);
  /* Starts hidden and is brought in by the is-playing animation below, the same
     bargain the two bubbles make. Reduced motion and browsers without an
     IntersectionObserver get is-playing outright, so nothing is stranded. */
  opacity: 0;
}
.recall-found b { font-weight: 500; color: var(--er-gray-90); }
.recall-found-icon {
  flex: none;
  width: .875rem; height: .875rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
  opacity: .7;
}

/* Delays measured from the moment SHE SPEAKS, not from the clip's first frame.
   script.js now adds is-playing when the video's own currentTime passes 0.7s,
   so these start from there — the 0.45s that used to sit in front of each of
   them was doing that waiting, and would double-count it now.

   The old note, kept because the measurement is still the basis for the 0.7: Mouth-region motion was measured
   frame by frame against a control patch of empty background: it sits at 0.7
   for the opening frames, crosses its own noise floor at frame 8 (0.38s) and is
   sustained above 6 from frame 12 (0.54s). At the old .25s the question was on
   screen before she had opened her mouth.

   0.6s, and everything after it moved by the same +0.35 so the run-on holds:
   asked, found, thinking, answered.

   Honest limit on the measurement: she is WALKING through a doorway and turning
   as she speaks, so the mouth box carries head movement as well as speech and
   the two cannot be fully separated at this resolution. The control patch reads
   0.01 against the mouth's 1.33 baseline, so the camera is not what is moving —
   but treat 0.54s as "she is clearly speaking by here", not as the exact
   syllable. Re-measure if the clip is ever recut. */
.recall-panel.is-playing .case-say,
.recall-grid.is-playing .case-say {
  animation: caseBubble .45s var(--ease-out) .15s both;
}
/* Lands between the question and the dots, so the order reads: asked, found,
   thinking, answered. The dots and the reply were pushed back to make room —
   at the old timings the answer arrived before this had finished appearing. */
.recall-grid.is-playing .recall-found {
  animation: caseBubble .45s var(--ease-out) .9s both;
}
.recall-grid.is-playing .case-typing {
  animation: caseThinking 1.35s linear 1.65s both;
}
.recall-grid.is-playing .case-typing i {
  animation: caseDot 1.1s ease-in-out 1.65s 2;
}
.recall-grid.is-playing .case-typing i:nth-child(2) { animation-delay: 1.8s; }
.recall-grid.is-playing .case-typing i:nth-child(3) { animation-delay: 1.95s; }
.recall-grid.is-playing .case-did {
  animation: caseBubble .45s var(--ease-out) 3s both;
}

@media (max-width: 60rem) {
  .recall-grid { grid-template-columns: minmax(0, 1fr); }
  .recall-copy { max-width: none; }}

/* ---- Act ---- */
/* Light, against the Recall band's dark — the three sections read as three
   different beats rather than one long stretch. */
.act-grid {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  align-items: center;
  gap: clamp(1.5rem, 3.5vw, 3.25rem);
  margin-top: clamp(2rem, 4vw, 3rem);
}
.act-media { margin: 0; border-radius: 6px; overflow: hidden; }
.act-media img,
.act-video {
  display: block;
  width: 100%;
  height: auto;
  /* Stated so the panel reserves its height before the clip decodes and the
     copy beside it does not shift when it arrives. */
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* The bubbles invert on this light section too — see the shared rule up in the
   Recall block, which styles both. */

/* The recall -> connect -> draft chain. Ticked rows rather than a bubble: they
   are a log of what happened, not something AIRA said. */
.act-steps {
  list-style: none;
  margin: .625rem 0 .5rem;
  display: grid;
  gap: .4375rem;
}
.act-steps li {
  display: flex;
  /* Not centred: the first label wraps to two lines on a phone, and a centred
     tick then floats between them instead of marking the line it belongs to. */
  align-items: flex-start;
  gap: .5rem;
  font-size: .8125rem;
  line-height: 1.45;
  color: var(--er-gray-70);
  opacity: 0;
}
.act-steps li::before {
  content: "";
  flex: none;
  width: 14px;
  height: 14px;
  /* (line-height 19px - tick 14px) / 2, so it sits on the first line's centre. */
  margin-top: 2.5px;
  border-radius: 50%;
  background: var(--er-gray-90);
  /* Tick drawn as a mask so it inherits the dot colour and needs no markup. */
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M4 7.2l2.1 2.1L10 5.4' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='7' cy='7' r='6.4' fill='none' stroke='%23000' stroke-width='1.2'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M4 7.2l2.1 2.1L10 5.4' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='7' cy='7' r='6.4' fill='none' stroke='%23000' stroke-width='1.2'/%3E%3C/svg%3E") center / contain no-repeat;
}
.act-steps b { font-weight: 500; color: var(--er-gray-90); }

/* Steps stand in for the typing dots here, so the dots would only be noise —
   and they sit below the steps, which would have them appearing out of order. */
.act-copy .case-typing { display: none; }

.act-grid.is-playing .act-steps li {
  animation: caseBubble .4s var(--ease-out) both;
  /* 2.85s is just after he stops speaking (his line runs 1.65-2.70s). */
  animation-delay: calc(2.85s + var(--n) * .7s);
}

.act-note {
  opacity: 0;
  margin-top: 1rem;
  font-size: .75rem;
  letter-spacing: .01em;
  color: var(--fg-muted);
}

/* One ask, one reply — the same three-beat rhythm the carousel and the Recall
   band use, so all three sections read as the same product speaking. */
.act-copy .case-say, .act-copy .case-did, .act-copy .case-typing { opacity: 0; }

.act-grid.is-playing .case-say {
  animation: caseBubble .45s var(--ease-out) 1.65s both;
}
.act-grid.is-playing .case-typing {
  animation: caseThinking 1.35s linear 2.85s both;
}
.act-grid.is-playing .case-typing i {
  animation: caseDot 1.1s ease-in-out 2.85s 2;
}
.act-grid.is-playing .case-typing i:nth-child(2) { animation-delay: 3s; }
.act-grid.is-playing .case-typing i:nth-child(3) { animation-delay: 3.15s; }
/* After the exchange has finished: the reply lands at 4.4s and settles by
   4.85s, so the caveat arrives once there is something to apply it to. */
.act-grid.is-playing .act-note {
  animation: caseBubble .45s var(--ease-out) 5s both;
}

.act-grid.is-playing .case-did {
  animation: caseBubble .45s var(--ease-out) 4.4s both;
}

@media (max-width: 60rem) {
  .act-grid { grid-template-columns: minmax(0, 1fr); }}

/* ---- Connections ---- */
.section-connect {
  /* White, like the sections either side. It was a full-bleed warm band, on the
     reasoning that a tint is what set it apart from its white neighbours — that
     separation is given up deliberately here; the heading and the funnel carry
     the section on their own now.

     The chips are what this costs. They were white boxes lifting off the beige;
     on white they would be a 7% border around nothing. They take a grey fill
     below, which is also what the reference does. */
  background: var(--er-white);
  /* Tighter than the shared section rhythm, and only this section. It is the one
     that has to hold heading, lede, a rail, a funnel and a landscape panel
     inside a single screen; at the usual 72px top and bottom it measured 1080px
     against a 900px viewport. Scoped here so no other section pays for it. */
  padding-block: clamp(1.5rem, 3vw, 2.25rem);
}

/* One centred column: heading, lede, then the rail. The old two-up grid put a
   spinning ring beside the copy; there is nothing beside anything now. */
.connect-inner { display: block; }

.connect-head {
  /* No max-width, unlike every other centred block on the page. The lede here
     is meant to hold ONE line and it is 139 characters; a 44rem measure wrapped
     it to three. The heading above it is short enough that the wide column
     costs it nothing. */
  margin-inline: auto;
  text-align: center;
}
/* The heading comes down too, off the shared .d-h2 clamp. Both it and the lede
   are paying for the panel: this section is budgeted against a single screen
   height, so type here converts directly into millimetres of memory field. It
   still opens the section — it is the largest thing in it — just not by as
   much as a heading that had a whole screen to itself. */
.section-connect .d-h2 {
  /* Back up. It was clamped small when this section was carrying a lede, a rail
     and a full-width panel against a one-screen budget; the lede is gone and the
     section is 550px now, so the heading no longer has to pay for anything. */
  font-size: clamp(1.875rem, 4vw, 2.75rem);
  line-height: 1.15;
}

/* Just the sources now. This was a three-column grid — chip, icons, hidden
   mirror chip — so the middle took whatever the two chips left and the funnel
   measured itself against that. With the "Apps" chip gone there is nothing to
   sit beside, so the rail is one centred block and .rail-flow's own 28rem cap
   is what sets the row's width. The mirror went with it: it existed only to
   make the two outer columns equal, and there are no outer columns. */
.connect-rail {
  /* 4rem originally, then 2.5, now 1.75. Every pixel here comes straight off the
     thing the funnel points at, and air above a row of 26px icons is the
     cheapest thing in the section to give up. */
  margin-top: clamp(1.25rem, 2.5vw, 1.75rem);
}

/* Capped and centred, which is what closes the gaps between the marks. The grid
   is seven equal columns and the funnel's path x values are derived from that
   same width, so narrowing the COLUMNS would slide the lines off the icons they
   belong to. Narrowing the shared parent moves both together: icon i is still
   at (i + 0.5)/7 of the row and the line still meets it. */
.rail-flow {
  position: relative;
  min-width: 0;
  /* 19rem, down from 28. The reference sets thirteen marks at roughly a 36px
     pitch against 20px icons — gaps a little under an icon wide. Seven icons at
     26px in 28rem gave a 64px pitch and 38px gaps, which read as seven separate
     buttons rather than one strip of sources. 19rem puts the pitch at 43px and
     the gaps at 17px, which is the reference's proportion.

     The funnel rides on this width, so the two cannot drift apart: its viewBox
     x values are 100i + 50 out of 700, which is (i + 0.5)/7 of whatever this
     resolves to. */
  /* Was 19rem for seven marks, which gave each a 43px column. Nine marks at
     that same density need 391px, so this widens rather than cramming nine
     into the old width — the air between the marks is what makes the row read
     as a strip of sources rather than a toolbar. */
  width: min(100%, 24.5rem);
  margin-inline: auto;
}

/* One column per mark. This was hard-coded to seven, and the funnel that used
   to hang below derived its path from that seven. The funnel is gone (see the
   note in index.html), so the count is free to follow the markup — but it still
   has to MATCH it: nine <li> in a seven-column grid wraps the last two onto a
   ragged second row. */
.rail-icons {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(9, minmax(0, 1fr));
  align-items: end;
  justify-items: center;
}
.rail-icons img {
  display: block;
  /* Down from a 2.125rem cap. Two reasons pulling the same way: the row reads
     as a strip of sources rather than seven buttons, and every pixel off the
     icons is a pixel the panel below can have — the whole section is budgeted
     against one screen height. */
  width: clamp(1.25rem, 2.3vw, 1.625rem);
  height: auto;
}

/* This block used to drop the chips onto a second row here: three items across a
   375px bar left the seven marks about 20px each, under the size a logo is
   identifiable at. With no chips there is nothing to drop, and the rail is one
   centred row at every width — so the whole override is gone rather than left
   behind pointing at classes that no longer exist. */

/* One arrow at each side of the frame. 44x44 is the touch minimum, and they sit
   ON the picture, so each carries a hairline and a near-opaque fill — a bare
   glyph over a photograph is unreadable against half the frames it can land on.
   Vertically centred on the frame, which is where the portrait is widest and
   the heading (top-left) is not. */
.hw-model-arrow {
  position: absolute;
  top: calc(50% - 1.375rem);
  z-index: 2;
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid rgba(16, 18, 20, .1);
  border-radius: 50%;
  background: rgba(255, 255, 255, .88);
  color: var(--fg);
  cursor: pointer;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.hw-model-arrow.is-prev { left: clamp(.5rem, 1.5vw, 1.25rem); }
.hw-model-arrow.is-next { right: clamp(.5rem, 1.5vw, 1.25rem); }
.hw-model-arrow svg {
  width: 1.125rem;
  height: 1.125rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.hw-model-arrow:hover { background: #fff; }
.hw-model-arrow:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }

/* ---- Connected: the two screens ---- */

/* Side by side, the field narrower than the transcript beside it: one is a
   texture you glance at, the other is a list you read. */
.connect-screens {
  margin-top: clamp(1.25rem, 3vw, 2rem);
  /* Inset from the container's own edges, so the pair sits in the section
     rather than running to its sides. The rail above is capped at 19rem and
     centred; two panels bleeding to the full width under it made the section
     look like two different layouts stacked. */
  width: min(100%, 66rem);
  /* Both panels sit right of centre rather than centred. auto on the left and a
     small fixed margin on the right pushes the pair over without letting it
     touch the edge — the left margin absorbs whatever is left, so this holds at
     every width instead of being a fixed offset that runs out. */
  margin-left: auto;
  margin-right: clamp(0rem, 4vw, 4rem);
  display: grid;
  /* Equal halves. They were 4fr/6fr, on the reasoning that one is a texture you
     glance at and the other a list you read — but they sit side by side in
     matching glass, and two panels that are almost the same width read as a
     mistake rather than as a deliberate difference. Same size, or clearly
     different; this is the former. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Tighter than it was (was 1.6vw capped at 1.25rem). The sphere sits right
     of its panel's centre and the transcript starts at the left edge of its
     own, so the two contents were already leaning toward each other across a
     gutter sized for two panels that were not. */
  gap: clamp(.375rem, .8vw, .625rem);
  align-items: start;
}
.cscreen { margin: 0; min-width: 0; }

/* Glass. A translucent fill, a hairline, and a blur behind it — the reference's
   material, in the light rather than the dark it uses there. The inset highlight
   is what stops it reading as flat tint: real glass catches light on its top
   edge, and one inset line is the cheapest way to say so. */
.cscreen-body {
  position: relative;
  border-radius: 16px;
  border: 1px solid rgba(16, 18, 20, .08);
  background: rgba(255, 255, 255, .55);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9),
              0 1px 2px rgba(16, 18, 20, .04);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  backdrop-filter: blur(14px) saturate(1.4);
  overflow: hidden;
}

/* One height for both, set here and not by an aspect ratio. A 4:5 aspect on
   panels this WIDE made them 709px and 1050px tall and took the section to
   1308px — an aspect ratio sizes off the width, and these are wide. A height
   sizes off what the content needs, which is what "not so big" means. */
.connect-screens { --cs-h: clamp(16rem, 23vw, 20.5rem); }
.cscreen-field { height: var(--cs-h); }
.cscreen-canvas,
.cscreen-labels { position: absolute; inset: 0; width: 100%; height: 100%; }
.cscreen-canvas { display: block; }
.cscreen-labels { pointer-events: none; }

.cscreen-chat {
  height: var(--cs-h);
  padding: clamp(.875rem, 1.5vw, 1.25rem);
  display: flex;
  flex-direction: column;
  gap: .625rem;
  overflow-y: auto;
}

/* Liquid glass, on the actions panel only. The difference from the plain glass
   both panels share is that this one is lit rather than just tinted: a gradient
   from a brighter top-left to a cooler foot, a specular line inside the top
   edge, a darker line inside the bottom to give the pane thickness, and enough
   saturation behind it that whatever passes under the glass colours it.

   The fill drops to .38 from .55, which is the whole point — but text sits on
   this, so the drop is bought back with the blur rather than left to chance.
   The rows below carry their own, brighter pane so the words never sit on the
   backdrop directly. */
.cscreen-chat {
  background:
    linear-gradient(150deg,
      rgba(255, 255, 255, .62) 0%,
      rgba(255, 255, 255, .34) 42%,
      rgba(244, 247, 250, .40) 100%);
  border-color: rgba(255, 255, 255, .55);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .95),
    inset 0 -1px 0 rgba(16, 18, 20, .06),
    inset 0 0 24px rgba(255, 255, 255, .35),
    0 6px 20px rgba(16, 18, 20, .07);
  -webkit-backdrop-filter: blur(26px) saturate(1.9);
  backdrop-filter: blur(26px) saturate(1.9);
}

/* The action rows read as panes floating ON that glass rather than boxes drawn
   in it: brighter than their ground, a lit top edge of their own, and their own
   blur so they thicken what is behind them instead of inheriting it. */
.cscreen-chat .cs-acts li {
  background: linear-gradient(180deg,
    rgba(255, 255, 255, .78), rgba(255, 255, 255, .58));
  border-color: rgba(255, 255, 255, .7);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .9),
    0 1px 2px rgba(16, 18, 20, .05);
  -webkit-backdrop-filter: blur(8px) saturate(1.3);
  backdrop-filter: blur(8px) saturate(1.3);
}

.cs-say {
  display: flex;
  align-items: flex-start;
  gap: .4375rem;
  margin: 0;
  font-size: clamp(.8125rem, 1.05vw, .9375rem);
  line-height: 1.4;
}
.cs-mic {
  flex: 0 0 auto;
  width: .875rem; height: .875rem;
  margin-top: .15em;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round;
}
.cs-recall {
  display: flex;
  align-items: flex-start;
  gap: .4375rem;
  margin: 0;
  font-size: clamp(.75rem, .95vw, .8125rem);
  line-height: 1.45;
  color: var(--muted, #6f6b60);
}
.cs-recall b { color: var(--fg); font-weight: 500; }
.cs-recall-icon {
  flex: 0 0 auto;
  width: .8125rem; height: .8125rem;
  margin-top: .2em;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round;
}

/* One row per service. The logo is what makes each line scannable — the four
   read as four different places before a word of them is read. */
.cs-acts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: .375rem;
}
.cs-acts li {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .5rem .625rem;
  border-radius: 10px;
  border: 1px solid rgba(16, 18, 20, .06);
  background: rgba(255, 255, 255, .6);
  font-size: clamp(.75rem, .95vw, .8125rem);
  line-height: 1.35;
}
.cs-acts img { flex: 0 0 auto; width: 1rem; height: 1rem; }
.cs-acts b { font-weight: 500; }

.cs-note {
  margin: auto 0 0;
  padding-top: .25rem;
  font-size: clamp(.6875rem, .9vw, .75rem);
  color: var(--muted, #6f6b60);
}

@media (max-width: 48rem) {
  /* Stacked, and the field goes short and wide. Side by side on a 375px screen
     gives each panel about 165px, which is too narrow for the action rows to
     hold their logo and their line together. */
  .connect-screens {
    grid-template-columns: minmax(0, 1fr);
    /* Stacked, the pair is already the full container width — there is no room
       to shift into, and an asymmetric margin would only narrow it. */
    margin-right: auto;
  }
  /* Square-ish, not the short strip it was. A sphere needs its height: at
     343x176 the ball came out 106x132 — not even round — and touched the top
     and right edges. The ball is sized off the panel's SHORT side, so a short
     panel makes a small ball and then crops it anyway. */
  .cscreen-field { height: clamp(15rem, 72vw, 19rem); }
  /* The marks sit closer on a phone. What matters is the PITCH, not the width:
     at a 43px pitch the icons read as a row of separate buttons, at ~34px they
     read as one strip of sources. 15rem gave nine marks a 27px pitch and a 7px
     gap, which is past a strip and into a smear, so this is 19rem — nine at a
     33.8px pitch and a 13.8px gap, the same rhythm seven had at 15rem.

     Rederive it if a mark is added or removed: width = count x 34px. */
  .rail-flow { width: min(100%, 19rem); }
  /* The transcript grows to its content on a phone rather than scrolling inside
     a fixed box — four action rows in a 160px window is a scrollbar, not a
     screen, and nobody scrolls a decorative panel to find the fourth item. */
  .cscreen-chat { height: auto; }}

/* Sequenced, not all at once. Each beat starts a little low and faded and is
   released by script.js in turn: the request first, then what it recalled, then
   the four actions, then the note. The delay lives in JS rather than in
   transition-delay here because the sequence has to start when the SPHERE has
   formed next door, which is a time only the field knows.

   No transform on the note — it is the one line that should not draw the eye by
   moving; it fades in and stays put. */
.cs-say, .cs-recall, .cs-acts li, .cs-note {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity .5s var(--ease-out), transform .5s var(--ease-out);
}
.cs-note { transform: none; }
.cs-say.is-in, .cs-recall.is-in, .cs-acts li.is-in, .cs-note.is-in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  /* Everything present, nothing travelling. The sequence is a nicety; the four
     actions are the content, and they must not depend on motion to be read. */
  .cs-say, .cs-recall, .cs-acts li, .cs-note {
    opacity: 1;
    transform: none;
    transition: none;
  }}

/* ---- Glasses ---- */
.section-hardware .d-h2 { margin-bottom: clamp(2.25rem, 4.5vw, 3.5rem); }
/* The card itself is the grey box — not just the image panel. The picture bleeds
   to the top and side edges and the caption sits inside the same box, so a card
   reads as one object rather than a tile with a label under it. */
/* The card is no longer a box. The grey moved onto the ART, and the caption sits
   below it on the page's own ground — which is what the reference does and what
   makes a row of these read as pictures with captions rather than as tiles. */
.hw-card {
  display: flex;
  flex-direction: column;
  background: none;
  border-radius: 0;
  overflow: visible;
  padding-bottom: 0;
}

/* Portrait, not square. A 1:1 panel made these read as thumbnails; 3:4 gives the
   card presence and suits a head-and-shoulders shot, which is what the first
   card now holds. The three product shots are cropped to fit. */
.hw-card-art {
  margin: 0 0 clamp(.75rem, 1vw, 1rem);
  /* Portrait, at the same 3:4 the mic plates use, so the two rows read as one
     family. It was 1:1 to keep the row inside a screen; that constraint is
     lifted here, and the trade is real — the row goes from about 510 tall to
     675.

     3:4 is also the crop the PHONE has always used, so every picture in the row
     was already verified against it: the lens stack, the fingertip and the
     silver key all survive a 75%-of-width window. Nothing needed re-cutting. */
  aspect-ratio: 3 / 4;
  background: var(--er-gray-05);
  border-radius: clamp(12px, 1.4vw, 18px);
  overflow: hidden;
  /* Positioned, not grid. The panel's height comes from aspect-ratio, and a
     percentage height cannot resolve against that — as a grid item the media
     fell back to its own intrinsic size, which letterboxed the stills and
     overflowed the video by 28px wherever the two ratios disagreed. An absolute
     inset:0 gives a definite box however the height was derived, and it stacks
     the faces for free. */
  position: relative;
}
.hw-card-art picture { display: contents; }
/* <source> does not compute to display:none here, and it laid out as a real box
   of its own once <picture> stopped generating one. */
.hw-card-art source { display: none; }

.hw-card-art img,
.hw-card-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The product stills are landscape and the panel is portrait, so roughly half
     the width is cropped away. --focus picks which half survives: centred, the
     side view lost the frame front — and with it the camera its caption is
     about. Set per image in the markup. */
  object-position: var(--focus, 50%) center;
  display: block;
}

/* ---- Models band ---- */

/* Landscape, above the card row rather than the first cell in it. In the row
   the portrait was cropped to a 3:4 cell and came out as a head filling the
   frame; here it is a wide band and the shot is shown whole. */
.hw-models {
  position: relative;
  margin-bottom: clamp(1rem, 2vw, 1.5rem);
}
.hw-models .hw-faces {
  position: relative;
  margin: 0;
  aspect-ratio: 5 / 2;
  border-radius: clamp(14px, 1.6vw, 22px);
  background: var(--er-gray-05, #f5f5f5);
  overflow: hidden;
}
.hw-models .hw-faces img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* contain, not cover, and that is the whole point of moving it out here. A 3:4
   portrait inside a 5:2 box on cover would crop to a band across the eyes —
   MORE zoomed than the card it left, not less. contain fits the frame's height
   and shows the shot as taken, standing on the base of the band. */
/* Centred, and whole. contain already guarantees nothing is cut — it fits the
   picture inside the box rather than filling it — so the portrait is shown
   entire at every width; what changed is where in the band it sits. It was
   pushed to 78% to clear the heading, which put the subject in the right third
   of an otherwise empty frame. Centre is where it belongs, and the heading is
   made to fit beside it instead. */
.hw-models .hw-faces img {
  object-fit: contain;
  object-position: 50% 100%;
}

/* The section's label and heading, inside the frame. Absolutely placed rather
   than in flow because the pictures are absolute too — this is the one box they
   all share, and the text has to sit ON the ground beside the portrait, not
   above or below it. */
/* "AURVIZ o1", the hardware section's own label. It sits in a bare .container
   above the model frame, which is the one place on this page that treatment
   appears, so one selector reaches exactly one element - checked, not assumed.

   IT WAS 32px, THE SHARED STRONG TIER, AND THAT WAS TOO CLOSE TO THE HEADING
   INSIDE THE FRAME. That heading is capped at 2.125rem, so at 1440 the label
   read 32 against its 34 and the two were the same size: nothing said which
   one was the section and which was the picture's own line. This is the
   collision .eyebrow-strong's own comment warns about, happening at the desktop
   end rather than the phone end it was written for.

   TWO TIERS, SPLIT AT 48rem, WHICH IS WHERE THE FRAME'S HEADING ALREADY
   SWITCHES. One continuous clamp does not work here, because the heading is not
   on a continuous slope either: it holds a flat 26px from about 420 to 768 and
   drops to 22 above it. A label rising on one slope crosses 26 at 722px and
   spends the fifty widths either side of that within two pixels of the heading.
   Split, they change rank AT the breakpoint instead of drifting through equality
   on the way past it.

   THE LABEL IS NOW THE LARGER OF THE TWO AT EVERY WIDTH, which is a reversal of
   what this rule first did. It used to hold the label under the heading below
   48rem - 18 against 24 at 390 - on the reasoning that a section label should
   not outweigh a heading. Desktop never obeyed that and never looked wrong for
   it: 48 against 34 at 1440. The two are not competing for one job. The label
   names the section and the heading is the picture's own line, so the label
   leading is the correct order and the phone was the view that had it backwards.

   Measured now: 26 against 24 at 390, 28 against 26 at 768, 28 against 22 at
   769, 48 against 34 at 1440. Never equal, and never within two pixels.

   The inversion is deliberate. The two are not competing for one job - the
   label names the section, the heading is the picture's own line, and that line
   is capped at 2.125rem so it stays inside a frame whose left column is 484px
   wide. A label held under that cap could not grow at all, which was the
   request. Every width is larger than the 32px shared tier this started from:
   15 to 26 at 390, 15 to 28 at 768, 21.6 to 32.4 at 900, 32 to 48 at 1440. */
/* CENTRED, and the whole block with it. The label used to sit alone and flush
   left, which is where a lone section label belongs. It is not alone now - it
   introduces the line under it - and a left-flush label over a centred line, or
   two left-flush lines of very different size, both read as a mistake. The pair
   is one unit, so it is centred as one. */
.section-hardware > .hw-intro { text-align: center; }

/* THE INTRODUCING LINE IS THE BIGGER OF THE TWO. That inverts the usual order a
   second time. This section already inverts label against heading - see the note
   above - and this goes further: the label names the section, the h2 inside the
   frame is the picture's own line, and THIS is the section's opening statement,
   so it outsizes both.

   400 against the label's 300. One step, not two: 500 beside a 300 at this size
   read as two different voices rather than one emphasis, and every other large
   .eyebrow-strong on the page is 300, so the label has to stay there or the set
   it belongs to stops being one.

   Size does the rest. 62px against the label's 52 at 1440, 32.8 against 28.1 at
   390 - about 1.18x at both ends, which holds the relationship across the range
   instead of letting it collapse on a phone. */
.hw-intro-line {
  font-size: clamp(1.625rem, 7.7vw, 2rem);
  /* 300, down from 400, and THE LABEL ABOVE WENT DOWN WITH IT for a reason
     worth writing down: the page loads Inter Tight at 200, 300, 400, 500 and
     600 and there is nothing in between. A 350 resolves to 300.

     So "lighter than 400" has exactly one answer, and taking it alone would put
     this line at the label's own weight and undo the contrast asked for a few
     commits ago -- the line is meant to be the heavier of the two as well as the
     larger. Both drop a step instead: label 300 to 200, line 400 to 300. The
     line is lighter than it was, and still the heavier of the pair. */
  font-weight: 300;
  letter-spacing: -.048em;
  line-height: 1.07;
  color: var(--er-gray-90);
  /* margin-inline, NOT left+right+width. A width alongside both insets
     over-constrains the box and the browser drops one of them - in LTR it is
     `right` - so the block ends up nudged off centre by exactly the amount the
     two insets differ. auto margins resolve symmetrically by definition. */
  max-width: 22ch;
  margin-inline: auto;
  /* NOT 0, which is what this was and it put the line flush against the band -
     measured, a 0px gap. The separation used to come from the label's own
     .625rem bottom margin, and moving the label up a slot took it away. Scales
     with the type rather than inheriting the label's fixed 10px, because the
     line it is now clearing is 62px tall at the desktop end, not 32. */
  margin-bottom: clamp(.75rem, 1.8vw, 1.5rem);
}
@media (min-width: 48.0625rem) {
  .hw-intro-line {
    font-size: clamp(2rem, 4.1vw, 3.4375rem);
    max-width: 26ch;
  }}

/* THE SCROLL-SCRUBBED STAGE.

   .o1-scroll is nothing but height. That height IS the scroll distance the clip
   is scrubbed against: 240vh means the reader travels a bit under two and a half
   screens to get through 9.58 seconds of footage, which lands the turn and the
   camera move at a pace that reads as deliberate rather than frantic. Change this
   number and you change the speed of the animation; there is no other control.

   .o1-scroll-stage is sticky, so it parks in the viewport and holds while that
   height goes past underneath. top is the header height, because a sticky box
   that starts at 0 slides under the fixed header. */
.o1-scroll {
  position: relative;
  /* SCROLL DISTANCE IS THE ONLY SPEED CONTROL HERE. The clip's 20.33s is fixed;
     how fast it goes past depends entirely on how much thumb it is spread over.

     It has grown with the footage -- 240vh for the first 9.58s cut, 340 when
     leg 3 took it to 13.83s, 480 once leg 4 and the speaker beat landed -- and
     620 is the one change that is not about length. It was asked for slower, so
     the same 488 frames now take about 30% more scrolling, which also puts nine
     or ten pixels between one frame and the next instead of seven. */
  height: 620vh;
}
/* COPY LEFT, PICTURE RIGHT. The reading and the thing being read about sit side
   by side, and the frame is whole and entirely on the right rather than carrying
   type across it.

   The picture column is the wider of the two by a long way -- the labels are a
   line and a half, the picture is the subject. minmax on the copy stops it
   collapsing under a long translation while still letting the picture take what
   is left. */
.o1-scroll-stage {
  position: sticky;
  top: var(--header-height);
  height: calc(100svh - var(--header-height));
  display: grid;
  grid-template-columns: minmax(12rem, 0.62fr) 1.38fr;
  align-items: center;
  gap: clamp(1.5rem, 3vw, 3.5rem);
  /* Padding on the left only. The picture is meant to run to the right edge --
     that is what "completely at the right" means -- so nothing insets it there. */
  padding-left: clamp(1.25rem, 4vw, 4rem);
}

/* The three labels stack in ONE grid cell rather than flowing, because only one
   shows at a time and they must not shift the column as they swap. Centred on
   the picture's middle, not the stage's, which is the same thing here since the
   row is aligned centre. */
.o1-scroll-copy {
  display: grid;
  align-content: center;
}
.o1-scroll-copy > .o1-scroll-tag { grid-area: 1 / 1; }

/* FULL BLEED, sized against the STAGE rather than the viewport.
   
   100%, NOT 100vw. It was 100vw, and that put a horizontal scrollbar on the whole
   page: 100vw counts the vertical scrollbar's own width, so on a 1440 window with
   a 15px bar the picture came out 1440 wide inside a 1425 content box and hung 15
   past the edge. Measured -- documentElement.scrollWidth 1440 against clientWidth
   1425. The stage already spans the full content width, so 100% of it is the
   full-bleed measure and it is the correct one.

   Height follows from the clip's own 16:9, so there is no crop and no letterbox
   at any width. min() against the height keeps it inside the stage on a short or
   wide window: at 1440x852 a full-width 16:9 picture wants 810px and fits, but at
   1920x700 it would want 1080 and the top would go under the header. Whichever
   constraint binds first wins, and the aspect ratio survives either way.

   It is also the positioning context for the tag, so the tag is placed against
   the PICTURE rather than the stage -- the two are different boxes at most
   widths, and a tag pinned to the stage would drift off the glasses as the
   window changed shape. */
.o1-scroll-frame {
  position: relative;
  /* Fills its column, unless the window is short enough that a 16:9 box that
     wide would not fit the stage -- then the height binds instead and the
     picture shrinks rather than overflowing under the header. */
  width: min(100%, calc((100svh - var(--header-height)) * 16 / 9));
  aspect-ratio: 16 / 9;
  /* Pushed to the right edge of its column when the height is what is binding,
     so it stays hard against the page edge rather than drifting inward. */
  margin-left: auto;
  /* THE PICTURE ARRIVES IN THE MIDDLE AND MOVES RIGHT AS THE READER STARTS.
     
     Its size never changes; only its position does. script.js writes --o1-shift
     in pixels, from half the width of the empty column at the start down to zero
     once the move is done, so at rest the frame sits centred on the page and it
     slides into its column under the first tenth of the scroll.

     A translate rather than an animated grid column: column widths cannot be
     interpolated smoothly and would reflow the copy on every frame. This moves
     one composited layer and leaves the grid alone.

     No transition. The shift follows the scrub frame by frame, and easing it
     would make the picture lag the thumb. */
  transform: translateX(calc(var(--o1-shift, 0px) * -1));
  /* One element, transformed on every scroll frame while the section is on
     screen, so it is worth its own compositor layer. Kept to this one box:
     will-change is cheap once and expensive scattered. */
  will-change: transform;
}
.o1-scroll-video {
  display: block;
  width: 100%;
  height: 100%;
  /* contain, not cover. The two are identical while the box holds the file's own
     16:9, and contain is the one that cannot crop if a rounded pixel ever puts
     them a hair apart. Cover would trim the product, which runs to x 0.077 and
     0.934 of the frame in leg 2 with very little margin to give up. */
  object-fit: contain;
  /* Matches the ground baked into the clip (254.3), so the poster, the video and
     the page all sit on the same white and no rectangle shows. */
  background: #fff;
}

/* THE LABELS, upper left, over the white above the frame.
   
   BOTH SIT IN THE SAME PLACE, because only one is ever showing: the weight while
   the frame turns, the camera once the lens draws out. Stacking them in one spot
   keeps the reader's eye still while the picture underneath changes.

   Percentages, not pixels: the picture scales with the window and so does the
   empty ground the labels sit on. Measured across the clip, the product's top
   edge never rises above y 0.225 of the frame, so y 0.11 is clear white at every
   point in the scrub.

   They fade rather than appearing; script.js holds the progress figures. */
.o1-scroll-tag {
  margin: 0;
  letter-spacing: -.03em;
  color: var(--er-gray-90);
  opacity: 0;
  transform: translateY(8px);
  /* OUT IS FASTER THAN IN, and asymmetric on purpose. Both labels sit in the
     same corner, so anything that crosses both thresholds at once -- a flick,
     a jump to an anchor, a trackpad throw -- would otherwise run both
     transitions together and print one on top of the other for half a second.
     Seen, mid-crossover: "38 g / So light..." ghosting under "Sony 12 MP".
     A quarter-second out against a half-second in delayed by .15s means the
     outgoing label has cleared before the incoming one is legible. */
  transition: opacity .25s var(--ease-out), transform .25s var(--ease-out);
  pointer-events: none;
}
.o1-scroll-tag.is-on {
  opacity: 1;
  transform: none;
  transition: opacity .5s var(--ease-out) .15s, transform .5s var(--ease-out) .15s;
}

/* THE WAVE OFF THE BRIDGE.
   
   Positioned on the picture, not the stage, and the numbers are measured off
   this clip rather than judged: across its closing frames the saddle between the
   rims sits at x 0.501 and its top edge at y 0.341 of the frame, holding to
   within a pixel from 0.93 of the scrub onward. 50.2% / 39% puts the centre on
   the bridge itself rather than on the notch above it.

   UPRIGHT. The bone band that used to sit below this had a flat marker, because
   that shot looked at the frame from behind at a low angle and its wave spread
   sideways; this one is square-on to the front, where the bridge is a vertical
   column, so the wave travels along it. The band and its .bone-marker are gone;
   the note stays because it is the reason for the aspect-ratio here.

   Sized so the rings clear the bridge without reaching the rims: 16% of the
   frame wide is about two and a half bridge widths. */
.o1-bridge-wave {
  position: absolute;
  /* x holds at the centreline through the whole beat -- measured at 0.4997 on
     every frame of it -- so only y and size are driven. script.js writes them
     from the scrub, because the bridge is still moving here: leg 4's zoom-out is
     finishing, so the saddle DROPS from 0.284 of the frame to 0.346 while its
     height falls from 0.144 to 0.106. A fixed marker would slide off it.

     (These replace an earlier set taken across the closing zoom, where the
     saddle rose from 0.389 to 0.251 and doubled. That beat sat inside the leg 3
     -> leg 4 dissolve -- a double exposure with two nose bridges visible at
     once -- and the rings were moved off it onto clean frames.) */
  left: 50%;
  top: var(--wave-y, 39%);
  /* 14%, not 16. A circle is taller than the ellipse was at the same width once
     the box is square in pixels, and at full scale on the last frame the outer
     ring ran about 7px past the top of the picture. Working it back: the box is
     square, so its half-height is 0.5 * w * (16/9) of the frame HEIGHT, and the
     wave sits at 0.251 of that height, so w * 0.888 * scale must stay under
     0.502. At scale 2 that caps w at 0.14. The re-measured beat peaks at scale
     1.55, well inside that: checked, the ring spans 0.091 to 0.488 of the frame
     height at its largest. */
  width: calc(14% * var(--wave-scale, 1));
  /* CIRCULAR. It was .55, taller than wide, on the argument that the bridge is
     a vertical column so a wave off it travels up and down. Asked for round
     instead, and round is the more honest shape anyway: what the sensor picks
     up spreads through bone in every direction, not along an axis.

     aspect-ratio: 1 makes the box square in PIXELS, not in percentages -- the
     frame is 16:9, so a box that was 16% of the width and 16% of the height
     would be a wide ellipse. Square pixels is what draws a circle.

     It also fixed an overflow nobody had noticed: at full scale the old ellipse
     was 456px wide and 829 tall inside an 802px frame, so the outermost ring
     ran off the top and bottom. A circle at the same width is 456 square and
     stays inside it. */
  aspect-ratio: 1;
  translate: -50% -50%;
  pointer-events: none;
  opacity: 0;
  /* No transition on top/width: those follow the scrub frame by frame and any
     easing on them would lag the picture. Only the fade is timed. */
  transition: opacity .45s var(--ease-out);
}
.o1-bridge-wave.is-on { opacity: 1; }
.o1-bridge-wave i {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* Dark stroke with a light hairline outside it, the same two-tone the band's
     marker uses and for the same reason: the ring crosses white ground AND the
     near-black frame, and neither tone survives both on its own. */
  border: 2px solid rgba(35, 35, 35, .55);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .6);
  opacity: 0;
  scale: .28;
}
/* Only while it is showing. An animation left running under opacity 0 is work
   the compositor does for nobody, and it would arrive mid-cycle. */
.o1-bridge-wave.is-on i { animation: bonePing 3.2s cubic-bezier(.16, .7, .3, 1) infinite; }
.o1-bridge-wave.is-on i:nth-child(2) { animation-delay: 1.067s; }
.o1-bridge-wave.is-on i:nth-child(3) { animation-delay: 2.133s; }

@media (prefers-reduced-motion: reduce) {
  /* The rings are the one part of this that is motion for its own sake. */
  .o1-bridge-wave { display: none; }}

/* THE TWO CONTROL MARKERS -- a swipe on the flat of the temple, and a dot above
   the monogram where the action button is.

   They work exactly like the bridge wave: absolutely positioned inside the
   frame, with centre, size and (for the swipe) rotation written per scroll
   frame by script.js from a table measured off the footage. Nothing here is a
   fixed coordinate, because nothing they point at holds still -- across the
   sixty frames they are on screen the temple swings from level to forty degrees
   and the monogram crosses from x 0.61 to x 0.45 while it more than doubles.

   Percentages resolve against the frame, which is locked to the clip's own 16:9
   with the video set to contain, so a percentage here IS a fraction of the
   picture. Same reason aspect-ratio is used for the shapes rather than a second
   percentage: a box that is n% of the width and n% of the height is not square,
   it is 16:9. */
.o1-touch,
.o1-action,
.o1-speaker-wave {
  position: absolute;
  left: var(--mx, 50%);
  top: var(--my, 50%);
  width: var(--mw, 10%);
  pointer-events: none;
  opacity: 0;
  /* Only the fade is timed. Position and size follow the scrub frame by frame,
     and easing those would make the marker lag the picture it is pinned to. */
  transition: opacity .4s var(--ease-out);
}
.o1-touch.is-on,
.o1-action.is-on,
.o1-speaker-wave.is-on { opacity: 1; }

/* OUTGOING RIPPLES OFF THE SPEAKER GRILLE. Mechanically the bridge wave: three
   rings on one staggered ping, expanding and fading, so something is always
   leaving the grille and nothing arrives mid-cycle.

   Positioned from a table rather than pinned, because the speaker beat is a
   photograph with an 8% push-in and a centre zoom moves everything outward: the
   grille drifts from x 0.635 to 0.642 and rises from 0.396 to 0.390 while it
   grows 5%. Small, but a fixed marker would creep off it.

   Same two-tone stroke as the bridge rings, and needed more here: these start
   ON the near-black temple and expand onto white ground, so a single tone would
   vanish at one end of the cycle or the other. */
.o1-speaker-wave {
  /* Square in PIXELS. A box that is n% of a 16:9 frame's width and n% of its
     height is not a circle, it is a wide ellipse. */
  aspect-ratio: 1;
  translate: -50% -50%;
}
.o1-speaker-wave i {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid rgba(35, 35, 35, .55);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .6);
  opacity: 0;
  scale: .28;
}
/* Only while showing: an animation under opacity 0 is work done for nobody. */
.o1-speaker-wave.is-on i { animation: bonePing 3.2s cubic-bezier(.16, .7, .3, 1) infinite; }
.o1-speaker-wave.is-on i:nth-child(2) { animation-delay: 1.067s; }
.o1-speaker-wave.is-on i:nth-child(3) { animation-delay: 2.133s; }

/* The swipe: a capsule lying along the temple with a dot travelling across it.
   2.375 is the measured shape -- 1.9 mark-widths long by 0.8 tall -- and it is
   an aspect-ratio so the box keeps that shape in pixels while it grows. */
.o1-touch {
  aspect-ratio: 2.375;
  translate: -50% -50%;
  rotate: var(--ma, 0deg);
  border-radius: 999px;
  /* Two-tone, like the wave's rings and for the same reason: the capsule sits
     on the near-black temple but its ends can cross onto white ground, and
     neither a dark nor a light stroke alone survives both. */
  border: 2px solid rgba(255, 255, 255, .72);
  box-shadow: 0 0 0 1px rgba(35, 35, 35, .35);
}
.o1-touch i {
  position: absolute;
  /* A third of the capsule's height, centred on its long axis. */
  top: 50%;
  width: 33%;
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  background: rgba(255, 255, 255, .92);
  left: 26%;
}
/* Only while it shows: an animation running under opacity 0 is work done for
   nobody, and it would arrive mid-stroke. */
.o1-touch.is-on i { animation: o1Swipe 2.6s cubic-bezier(.5, 0, .3, 1) infinite; }
@keyframes o1Swipe {
  0%, 8% { left: 26%; opacity: 0; }
  16% { left: 26%; opacity: 1; }
  56% { left: 74%; opacity: 1; }
  70%, 100% { left: 74%; opacity: 0; }
}

/* The action button: a filled dot inside a ring, the ring being the whole
   measured width so the dot itself lands on the button and the ring reads as a
   callout around it. */
.o1-action {
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .72);
  box-shadow: 0 0 0 1px rgba(35, 35, 35, .35);
}
.o1-action::after {
  content: "";
  position: absolute;
  inset: 24%;
  border-radius: 50%;
  background: rgba(255, 255, 255, .92);
}
.o1-action.is-on::after { animation: o1Press 2.6s cubic-bezier(.4, 0, .3, 1) infinite; }
@keyframes o1Press {
  0%, 62%, 100% { scale: 1; opacity: .92; }
  74% { scale: .62; opacity: 1; }
  86% { scale: 1; opacity: .92; }
}

@media (prefers-reduced-motion: reduce) {
  /* Same call as the rings: the markers are motion for its own sake, and the
     merged label beside them says both things in words. */
  .o1-touch, .o1-action, .o1-speaker-wave { display: none; }}

.o1-tag-lead {
  display: block;
  /* The floor is what a phone gets, and it is high because the phone no longer
     has to fit this above the glasses -- the labels sit under the picture below
     48rem, with about 275px of empty stage to use. */
  font-size: clamp(1.25rem, 2.5vw, 2.125rem);
  font-weight: 300;
  line-height: 1.15;
}
/* Smaller and lighter than the line above it, and held to a measure so it wraps
   to two or three short lines rather than running across the picture. */
/* ONE TYPE SCALE FOR ALL FOUR LABELS.

   The bone and speaker labels wear their own sections' classes, which is what
   makes their colour, weight, tracking and structure right without duplicating
   anything. What they must NOT inherit is size, because the two sections do not
   agree: bone sets its heading at 51.84px and speakers at 42, and the two
   plain labels sit at 34. Mirrored exactly that is three heading sizes in one
   stack, in a column where only one shows at a time -- so the type would appear
   to jump every time the scrub crossed a threshold.

   42 / 22 / 17.28 at 1440 is the speaker section's own tier, chosen over bone's
   because it is the one that fits: at 51.84 "Bone conduction microphone" wraps
   to two lines in a 411px column and crowds the picture beside it.

   THE COST, stated plainly: the bone label no longer matches its band pixel for
   pixel. It matches in everything except size, and it now matches its three
   siblings, which is what consistency across the stack costs. */
/* THREE CLASSES DEEP, and that is not decoration. The section rules this has to
   beat are `.o1-tag-bone .eyebrow` and the speaker's twin,
   both (0,2,0). A two-class override ties with them and loses on source order --
   measured, the bone heading stayed at 51.84 and the speaker at 42 while the two
   plain labels moved. Naming the block as well makes it (0,3,0) and it wins. */
.o1-scroll-copy .o1-tag-bone .eyebrow,
.o1-scroll-copy .o1-tag-speaker .eyebrow,
.o1-scroll-copy .o1-scroll-tag .o1-tag-lead {
  font-size: clamp(1.375rem, 2.9vw, 2.625rem);
  /* -.048em and 1.07, WHICH IS THE PAGE'S HEADING SETTING, not a number picked
     for this block. Every large heading on the page is tracked at -.048 -- the
     spec panels, d-h2, the section's own "AURVIZ o1" and its introducing line --
     and .spec-h pairs it with 1.07.

     Four of these six labels were not. The bone and speaker headings borrow
     their sections' .eyebrow, which already carried -.048; the other four are
     the block's own .o1-tag-lead, which set no tracking at all and inherited
     -.0129 from the body. Measured at 1280px: -1.78px against -0.48px on type
     of the same size, weight, colour and family, side by side down one column.
     That is the whole difference and it is why they read as a different face.

     Set here rather than on .o1-tag-lead so all six are stated in one place and
     cannot drift apart again. */
  letter-spacing: -.048em;
  line-height: 1.07;
}
.o1-scroll-copy .o1-scroll-tag .bone-h,
.o1-scroll-copy .o1-scroll-tag .audio-h,
.o1-scroll-copy .o1-scroll-tag .o1-tag-sub {
  font-size: clamp(1rem, 1.53vw, 1.375rem);
  /* The same split one level down: bone-h and audio-h came in at -.048/1.2 from
     their sections and .o1-tag-sub at -.01/1.3, so the second lines disagreed
     too. These take the sections' setting, which is the one that matches the
     heading above them. */
  letter-spacing: -.048em;
  line-height: 1.2;
}
.o1-scroll-copy .o1-scroll-tag .bone-p,
.o1-scroll-copy .o1-scroll-tag .speaker-p,
.o1-scroll-copy .o1-scroll-tag .o1-tag-body {
  font-size: clamp(.875rem, 1.2vw, 1.08rem);
}

.o1-tag-sub {
  display: block;
  margin-top: .35em;
  max-width: 20ch;
  font-size: clamp(.9375rem, 1.5vw, 1.3125rem);
  font-weight: 300;
  line-height: 1.3;
  color: var(--er-gray-70);
  letter-spacing: -.01em;
}

/* THE SCROLL BLOCK IS REMOVED ON PHONES AND UNDER REDUCED MOTION.

   NOTHING IS LOST BY REMOVING IT, and that is what makes this the right answer
   rather than a compromise. Every one of its six labels is a copy of content
   that already has its own place further down this page -- the weight, the
   camera, touch and the action button on hardware cards, bone conduction on its
   band, the speakers in the audio section. The picture is decorative and the
   words are duplicates, so a reader who does not get this block reads the same
   product on the same page.

   ON PHONES it was the weaker treatment anyway: a 16:9 picture on a 375px screen
   is 211px tall, the labels sit under it rather than beside it, and holding it
   all costs 2.8MB of video that now is not fetched at all.

   UNDER REDUCED MOTION this replaces a rule that had gone stale. It named
   #o1TagWeight and #o1TagCamera and picked one of them to show, which was right
   when there were two labels and wrong as soon as there were six -- the other
   four would have sat at opacity 0 with nothing deciding between them. Hiding
   the block needs no such decision and cannot go stale again.

   script.js checks the same two conditions and returns before loading anything. */
@media (max-width: 47.99rem), (prefers-reduced-motion: reduce) {
  .o1-scroll { display: none; }}

/* A phone gets less scroll distance for the same footage. 240vh of a 812px
   screen is a long way to push a thumb, and the picture is smaller so there is
   less detail to take in on the way. */
@media (max-width: 47.99rem) {
  /* Scaled from 180 by the same ratio the clip grew, for the same reason. */
  .o1-scroll { height: 370vh; }}

/* THE LABELS SIT BELOW THE PICTURE UNTIL THERE IS ROOM ABOVE IT, and 62rem is
   where the geometry says that is -- not 48rem where this started, and not the
   site's usual 60rem next door.

   Overlaid, a label needs the strip between the top of the frame and the top of
   the product. That strip is a FRACTION of the frame height; the label is a
   fixed stack of type. Solving one against the other: the label starts at 0.11
   of the frame and stands about 75px tall, and the product's top edge is never
   higher than 0.288, so clearing it by a comfortable 24px wants

       0.11H + 75 < 0.288H - 24   ->   H > 556   ->   frame wider than ~990px.

   MEASURED AT 769 BEFORE THIS EXISTED: the label ended at 0.287 of the frame and
   the product began at 0.288. A one-pixel margin, which any longer translation
   would have eaten.

   Below the switch the label goes under the picture, where the stage has room
   going spare: viewport height against a 16:9 picture leaves roughly 270px of
   white at 375 wide. top: 100% is the frame's bottom edge, the frame being the
   containing block. */
@media (max-width: 61.99rem) {
  /* ONE COLUMN, PICTURE OVER COPY. Side by side needs width this does not have:
     a 16:9 picture in 60% of 375px is 127px tall, which is not a picture of
     anything. Stacked, it keeps the full width and the copy sits under it.

     order, not source order. The copy comes FIRST in the markup, because it is
     the content and the video is aria-hidden decoration -- a screen reader
     should meet the words, not be told there is a video it cannot see. The
     design wants the picture on top, so the visual order is set here and the
     reading order is left alone. */
  .o1-scroll-stage {
    grid-template-columns: 1fr;
    align-content: center;
    padding-left: 0;
    gap: clamp(.75rem, 4vw, 1.5rem);
  }
  .o1-scroll-frame {
    order: 1;
    width: 100%;
    margin-left: 0;
  }
  .o1-scroll-copy {
    order: 2;
    padding-inline: clamp(1rem, 5vw, 2rem);
  }
  /* No measure of its own down here. A 28ch cap was narrower than the column --
     it broke "It hears through nasal vibrations, not air" after "not" and left
     "air" alone on a line. At the column's width both subtitles set on one. */
  .o1-tag-sub { max-width: none; }}

.section-hardware > .container > .eyebrow-strong {
  /* Down a step, as asked, and it stays the SMALLER of the pair: 46px against
     the introducing line's 55 at 1440, 25.7 against 30 at 390. Roughly 1.20x at
     both ends, so the relationship holds across the range. */
  font-size: clamp(1.375rem, 6.6vw, 1.75rem);
  /* 200, and this ONE label leaves the shared 300 tier deliberately.

     The set argument still stands for the rest of them -- "Bone conduction
     microphone", "Open-ear speakers", "Custom lenses" and the others are a set
     at 300 and should move together or not at all. This one is already outside
     that set in every other respect: it has its own size ramp, its own
     letter-spacing and its own case, because it is the only one paired with a
     line beneath it rather than standing alone over a picture.

     It moves because the line under it had to get lighter and the font has no
     weight between 300 and 400. Holding the label at 300 while the line came
     down to 300 would have flattened the pair; 200 keeps the line the heavier
     of the two, which is the relationship that was asked for. */
  font-weight: 200;
  text-transform: none;
  letter-spacing: -.048em;
  line-height: 1.07;
}
@media (min-width: 48.0625rem) {
  .section-hardware > .container > .eyebrow-strong {
    font-size: clamp(1.75rem, 3.5vw, 2.875rem);
  }}

.hw-models-head {
  position: absolute;
  z-index: 1;
  left: clamp(1.25rem, 4vw, 3.5rem);
  /* Top-left, not centred on the band's middle. Two reasons pulling together:
     it is where a caption on a frame is read first, and with the portrait back
     in the centre the vertical middle is exactly where the subject now is. */
  top: clamp(1.25rem, 3vw, 2.25rem);
  /* 22rem, down from 30, and the heading below is smaller to match. A centred
     portrait on a 1383px band occupies 484-899px, so the text has 484px of left
     to live in — at the old width and size its box ran to 480 and cleared the
     shoulder by 4px, which is not clearance, it is a coincidence. */
  max-width: min(46%, 22rem);
}
.hw-models-head .d-h2 {
  margin: 0;
  /* Room to grow, and the box is what says so: the heading shrink-wraps to
     174px inside a 352px allowance, and its right edge sits at 251 against a
     portrait whose subject starts near 499. At 34px the widest line comes to
     228 and the edge to 284 — still clear of both. */
  font-size: clamp(1.375rem, 2.7vw, 2.125rem);
  line-height: 1.15;
}

/* ---- Connected: the two screens ---- */

/* Side by side, the field narrower than the transcript beside it: one is a
   texture you glance at, the other is a list you read. */
.connect-screens {
  margin-top: clamp(1.25rem, 3vw, 2rem);
  /* Inset from the container's own edges, so the pair sits in the section
     rather than running to its sides. The rail above is capped at 19rem and
     centred; two panels bleeding to the full width under it made the section
     look like two different layouts stacked. */
  width: min(100%, 66rem);
  /* Both panels sit right of centre rather than centred. auto on the left and a
     small fixed margin on the right pushes the pair over without letting it
     touch the edge — the left margin absorbs whatever is left, so this holds at
     every width instead of being a fixed offset that runs out. */
  margin-left: auto;
  margin-right: clamp(0rem, 4vw, 4rem);
  display: grid;
  /* Equal halves. They were 4fr/6fr, on the reasoning that one is a texture you
     glance at and the other a list you read — but they sit side by side in
     matching glass, and two panels that are almost the same width read as a
     mistake rather than as a deliberate difference. Same size, or clearly
     different; this is the former. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Tighter than it was (was 1.6vw capped at 1.25rem). The sphere sits right
     of its panel's centre and the transcript starts at the left edge of its
     own, so the two contents were already leaning toward each other across a
     gutter sized for two panels that were not. */
  gap: clamp(.375rem, .8vw, .625rem);
  align-items: start;
}
.cscreen { margin: 0; min-width: 0; }

/* Glass. A translucent fill, a hairline, and a blur behind it — the reference's
   material, in the light rather than the dark it uses there. The inset highlight
   is what stops it reading as flat tint: real glass catches light on its top
   edge, and one inset line is the cheapest way to say so. */
.cscreen-body {
  position: relative;
  border-radius: 16px;
  border: 1px solid rgba(16, 18, 20, .08);
  background: rgba(255, 255, 255, .55);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9),
              0 1px 2px rgba(16, 18, 20, .04);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  backdrop-filter: blur(14px) saturate(1.4);
  overflow: hidden;
}

/* One height for both, set here and not by an aspect ratio. A 4:5 aspect on
   panels this WIDE made them 709px and 1050px tall and took the section to
   1308px — an aspect ratio sizes off the width, and these are wide. A height
   sizes off what the content needs, which is what "not so big" means. */
.connect-screens { --cs-h: clamp(16rem, 23vw, 20.5rem); }
.cscreen-field { height: var(--cs-h); }
.cscreen-canvas,
.cscreen-labels { position: absolute; inset: 0; width: 100%; height: 100%; }
.cscreen-canvas { display: block; }
.cscreen-labels { pointer-events: none; }

.cscreen-chat {
  height: var(--cs-h);
  padding: clamp(.875rem, 1.5vw, 1.25rem);
  display: flex;
  flex-direction: column;
  gap: .625rem;
  overflow-y: auto;
}

/* Liquid glass, on the actions panel only. The difference from the plain glass
   both panels share is that this one is lit rather than just tinted: a gradient
   from a brighter top-left to a cooler foot, a specular line inside the top
   edge, a darker line inside the bottom to give the pane thickness, and enough
   saturation behind it that whatever passes under the glass colours it.

   The fill drops to .38 from .55, which is the whole point — but text sits on
   this, so the drop is bought back with the blur rather than left to chance.
   The rows below carry their own, brighter pane so the words never sit on the
   backdrop directly. */
.cscreen-chat {
  background:
    linear-gradient(150deg,
      rgba(255, 255, 255, .62) 0%,
      rgba(255, 255, 255, .34) 42%,
      rgba(244, 247, 250, .40) 100%);
  border-color: rgba(255, 255, 255, .55);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .95),
    inset 0 -1px 0 rgba(16, 18, 20, .06),
    inset 0 0 24px rgba(255, 255, 255, .35),
    0 6px 20px rgba(16, 18, 20, .07);
  -webkit-backdrop-filter: blur(26px) saturate(1.9);
  backdrop-filter: blur(26px) saturate(1.9);
}

/* The action rows read as panes floating ON that glass rather than boxes drawn
   in it: brighter than their ground, a lit top edge of their own, and their own
   blur so they thicken what is behind them instead of inheriting it. */
.cscreen-chat .cs-acts li {
  background: linear-gradient(180deg,
    rgba(255, 255, 255, .78), rgba(255, 255, 255, .58));
  border-color: rgba(255, 255, 255, .7);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .9),
    0 1px 2px rgba(16, 18, 20, .05);
  -webkit-backdrop-filter: blur(8px) saturate(1.3);
  backdrop-filter: blur(8px) saturate(1.3);
}

.cs-say {
  display: flex;
  align-items: flex-start;
  gap: .4375rem;
  margin: 0;
  font-size: clamp(.8125rem, 1.05vw, .9375rem);
  line-height: 1.4;
}
.cs-mic {
  flex: 0 0 auto;
  width: .875rem; height: .875rem;
  margin-top: .15em;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round;
}
.cs-recall {
  display: flex;
  align-items: flex-start;
  gap: .4375rem;
  margin: 0;
  font-size: clamp(.75rem, .95vw, .8125rem);
  line-height: 1.45;
  color: var(--muted, #6f6b60);
}
.cs-recall b { color: var(--fg); font-weight: 500; }
.cs-recall-icon {
  flex: 0 0 auto;
  width: .8125rem; height: .8125rem;
  margin-top: .2em;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round;
}

/* One row per service. The logo is what makes each line scannable — the four
   read as four different places before a word of them is read. */
.cs-acts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: .375rem;
}
.cs-acts li {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .5rem .625rem;
  border-radius: 10px;
  border: 1px solid rgba(16, 18, 20, .06);
  background: rgba(255, 255, 255, .6);
  font-size: clamp(.75rem, .95vw, .8125rem);
  line-height: 1.35;
}
.cs-acts img { flex: 0 0 auto; width: 1rem; height: 1rem; }
.cs-acts b { font-weight: 500; }

.cs-note {
  margin: auto 0 0;
  padding-top: .25rem;
  font-size: clamp(.6875rem, .9vw, .75rem);
  color: var(--muted, #6f6b60);
}

@media (max-width: 48rem) {
  /* Stacked, and the field goes short and wide. Side by side on a 375px screen
     gives each panel about 165px, which is too narrow for the action rows to
     hold their logo and their line together. */
  .connect-screens {
    grid-template-columns: minmax(0, 1fr);
    /* Stacked, the pair is already the full container width — there is no room
       to shift into, and an asymmetric margin would only narrow it. */
    margin-right: auto;
  }
  /* Square-ish, not the short strip it was. A sphere needs its height: at
     343x176 the ball came out 106x132 — not even round — and touched the top
     and right edges. The ball is sized off the panel's SHORT side, so a short
     panel makes a small ball and then crops it anyway. */
  .cscreen-field { height: clamp(15rem, 72vw, 19rem); }
  /* The marks sit closer on a phone. What matters is the PITCH, not the width:
     at a 43px pitch the icons read as a row of separate buttons, at ~34px they
     read as one strip of sources. 15rem gave nine marks a 27px pitch and a 7px
     gap, which is past a strip and into a smear, so this is 19rem — nine at a
     33.8px pitch and a 13.8px gap, the same rhythm seven had at 15rem.

     Rederive it if a mark is added or removed: width = count x 34px. */
  .rail-flow { width: min(100%, 19rem); }
  /* The transcript grows to its content on a phone rather than scrolling inside
     a fixed box — four action rows in a 160px window is a scrollbar, not a
     screen, and nobody scrolls a decorative panel to find the fourth item. */
  .cscreen-chat { height: auto; }}

/* Sequenced, not all at once. Each beat starts a little low and faded and is
   released by script.js in turn: the request first, then what it recalled, then
   the four actions, then the note. The delay lives in JS rather than in
   transition-delay here because the sequence has to start when the SPHERE has
   formed next door, which is a time only the field knows.

   No transform on the note — it is the one line that should not draw the eye by
   moving; it fades in and stays put. */
.cs-say, .cs-recall, .cs-acts li, .cs-note {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity .5s var(--ease-out), transform .5s var(--ease-out);
}
.cs-note { transform: none; }
.cs-say.is-in, .cs-recall.is-in, .cs-acts li.is-in, .cs-note.is-in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  /* Everything present, nothing travelling. The sequence is a nicety; the four
     actions are the content, and they must not depend on motion to be read. */
  .cs-say, .cs-recall, .cs-acts li, .cs-note {
    opacity: 1;
    transform: none;
    transition: none;
  }}

/* ---- Glasses ---- */
.section-hardware .d-h2 { margin-bottom: clamp(2.25rem, 4.5vw, 3.5rem); }
/* The card itself is the grey box — not just the image panel. The picture bleeds
   to the top and side edges and the caption sits inside the same box, so a card
   reads as one object rather than a tile with a label under it. */
/* The card is no longer a box. The grey moved onto the ART, and the caption sits
   below it on the page's own ground — which is what the reference does and what
   makes a row of these read as pictures with captions rather than as tiles. */
.hw-card {
  display: flex;
  flex-direction: column;
  background: none;
  border-radius: 0;
  overflow: visible;
  padding-bottom: 0;
}

/* Portrait, not square. A 1:1 panel made these read as thumbnails; 3:4 gives the
   card presence and suits a head-and-shoulders shot, which is what the first
   card now holds. The three product shots are cropped to fit. */
.hw-card-art {
  margin: 0 0 clamp(.75rem, 1vw, 1rem);
  /* Portrait, at the same 3:4 the mic plates use, so the two rows read as one
     family. It was 1:1 to keep the row inside a screen; that constraint is
     lifted here, and the trade is real — the row goes from about 510 tall to
     675.

     3:4 is also the crop the PHONE has always used, so every picture in the row
     was already verified against it: the lens stack, the fingertip and the
     silver key all survive a 75%-of-width window. Nothing needed re-cutting. */
  aspect-ratio: 3 / 4;
  background: var(--er-gray-05);
  border-radius: clamp(12px, 1.4vw, 18px);
  overflow: hidden;
  /* Positioned, not grid. The panel's height comes from aspect-ratio, and a
     percentage height cannot resolve against that — as a grid item the media
     fell back to its own intrinsic size, which letterboxed the stills and
     overflowed the video by 28px wherever the two ratios disagreed. An absolute
     inset:0 gives a definite box however the height was derived, and it stacks
     the faces for free. */
  position: relative;
}
.hw-card-art picture { display: contents; }
/* <source> does not compute to display:none here, and it laid out as a real box
   of its own once <picture> stopped generating one. */
.hw-card-art source { display: none; }

.hw-card-art img,
.hw-card-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The product stills are landscape and the panel is portrait, so roughly half
     the width is cropped away. --focus picks which half survives: centred, the
     side view lost the frame front — and with it the camera its caption is
     about. Set per image in the markup. */
  object-position: var(--focus, 50%) center;
  display: block;
}

/* ---- Models band ---- */

/* Landscape, above the card row rather than the first cell in it. In the row
   the portrait was cropped to a 3:4 cell and came out as a head filling the
   frame; here it is a wide band and the shot is shown whole. */
.hw-models {
  position: relative;
  margin-bottom: clamp(1rem, 2vw, 1.5rem);
}
.hw-models .hw-faces {
  position: relative;
  margin: 0;
  aspect-ratio: 5 / 2;
  border-radius: clamp(14px, 1.6vw, 22px);
  background: var(--er-gray-05, #f5f5f5);
  overflow: hidden;
}
.hw-models .hw-faces img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* contain, not cover, and that is the whole point of moving it out here. A 3:4
   portrait inside a 5:2 box on cover would crop to a band across the eyes —
   MORE zoomed than the card it left, not less. contain fits the frame's height
   and shows the shot as taken, standing on the base of the band. */
/* Centred, and whole. contain already guarantees nothing is cut — it fits the
   picture inside the box rather than filling it — so the portrait is shown
   entire at every width; what changed is where in the band it sits. It was
   pushed to 78% to clear the heading, which put the subject in the right third
   of an otherwise empty frame. Centre is where it belongs, and the heading is
   made to fit beside it instead. */
.hw-models .hw-faces img {
  object-fit: contain;
  object-position: 50% 100%;
}

/* The section's label and heading, inside the frame. Absolutely placed rather
   than in flow because the pictures are absolute too — this is the one box they
   all share, and the text has to sit ON the ground beside the portrait, not
   above or below it. */
.hw-models-head {
  position: absolute;
  z-index: 1;
  left: clamp(1.25rem, 4vw, 3.5rem);
  /* Top-left, not centred on the band's middle. Two reasons pulling together:
     it is where a caption on a frame is read first, and with the portrait back
     in the centre the vertical middle is exactly where the subject now is. */
  top: clamp(1.25rem, 3vw, 2.25rem);
  /* 22rem, down from 30, and the heading below is smaller to match. A centred
     portrait on a 1383px band occupies 484-899px, so the text has 484px of left
     to live in — at the old width and size its box ran to 480 and cleared the
     shoulder by 4px, which is not clearance, it is a coincidence. */
  max-width: min(46%, 22rem);
}
.hw-models-head .d-h2 {
  margin: 0;
  /* Room to grow, and the box is what says so: the heading shrink-wraps to
     174px inside a 352px allowance, and its right edge sits at 251 against a
     portrait whose subject starts near 499. At 34px the widest line comes to
     228 and the edge to 284 — still clear of both. */
  font-size: clamp(1.375rem, 2.7vw, 2.125rem);
  line-height: 1.15;
}

/* The faces already share one box, so they stack with no further help: the panel
   never reflows and the outgoing portrait fades under the incoming one. */
.hw-faces img {
  opacity: 0;
  /* .28s, down from .7. Three quarters of a second is right for a crossfade
     that runs itself on a timer; on a button it is long enough that the press
     feels unanswered and people tap again. .18s after a second pass — at .28
     the blend was still readable as a blend rather than as a switch. */
  transition: opacity .18s var(--ease-out);
}
.hw-faces img.is-on { opacity: 1; }

@media (max-width: 48rem) {
  /* Taller on a phone. At 5:2 a 343px-wide band is 137px high, and a portrait
     fitted to that height is 103px wide — a person the size of a thumbnail in
     the middle of an empty strip. 4:3 gives the shot room to be looked at AND
     gives the heading beside it room to set on more than one word a line. */
  /* FULL SCREEN on a phone. The band bleeds past the container's gutters to the
     screen edges and takes the height the viewport has left after the section's
     own padding and the heading above it, so the model is the screen rather
     than a picture on it.

     cover, not contain, and that is the whole difference. contain fits the
     3:2 cutout inside a portrait box and it comes out 375 wide by 250 tall with
     two thirds of the screen empty under it. cover fills both axes — it scales
     to the height and lets the transparent margins, then the shoulders, run off
     the sides. The face ends up large, which is what the reference does and what
     "appear in the entire phone screen" asks for.

     object-position 50% 22% keeps the head in the upper half as it crops rather
     than centring the crop on the chest. */
  /* Inside the gutters, not bleeding past them. The full-bleed version came out
     asymmetric on a real handset — flush left and short of the right edge —
     because the negative margin and the container's own centring were fighting
     over the same axis. Sitting inside the gutter is symmetric by construction:
     the container centres it and the same gap falls on both sides. */
  .hw-models { margin-inline: auto; }
  .hw-models .hw-faces {
    aspect-ratio: auto;
    /* 100svh less 7.5rem, which is what the section spends above it: its own
       top padding and the GLASSES label. At 11rem it filled 78% of the screen
       and read as a tall card rather than as the screen. 88% is as close as it
       gets while the label above it still has somewhere to sit. */
    height: calc(100svh - 7.5rem);
    border-radius: 0;
  }

  .section-hardware .hw-models .hw-faces img {
    /* The picture starts BELOW the heading rather than under it. cover fills
       whatever box it is given, so with inset:0 the crop ran to the top of the
       frame and the words sat in his hair. Insetting the box by the heading's
       own height gives the text a band of its own and the picture a clean top
       edge; the gap between them is the difference between the two.

       None of the overflow arithmetic the contained version needed applies here
       — no negative left, no 134% width, no per-file exception. Those grew a
       picture that was fitting inside its box; this one is larger than its box
       by construction. */
    /* Explicit width and height, NOT auto. With auto the image ignored the
       insets and laid itself out at its intrinsic 1600x1067 inside a 343px
       frame — overflow:hidden then showed only its top-left corner, which on a
       cutout is transparent, so the models vanished entirely. A percentage
       height against a definite frame resolves; auto does not. */
    top: 5.25rem;
    left: 0;
    width: 100%;
    height: calc(100% - 5.25rem);
    max-width: none;
    object-fit: cover;
    object-position: 50% 18%;
  }


  .hw-models-head {
    top: clamp(.875rem, 3.5vw, 1.25rem);
    right: clamp(1.25rem, 4vw, 3.5rem);
    max-width: none;
  }
  /* The phone had its own, smaller size and that is what was actually showing —
     18px, not the 22 the shared clamp floor would have given. The budget here is
     vertical, not horizontal: the portrait starts at 5.25rem from the band's top
     and the heading at about 15px into it, so two lines have roughly 69px to sit
     in. At 26px they take 60. */
  .hw-models-head .d-h2 { font-size: clamp(1.25rem, 6.2vw, 1.625rem); }}

/* Inset from the box edge. The figure above is deliberately not inset — it runs
   to the card's sides — so the padding lives on the text, not on the card. */
/* No inset. The text was padded in because it sat inside a grey card; it sits on
   the page now and lines up with the panel's left edge, which is what makes the
   column read as one thing. */
.hw-card h3,
.hw-card p { padding-inline: 0; }

/* Lead-in and body read as one paragraph, the way the reference sets them: the
   claim in a heavier dark face, the explanation in grey directly under it, with
   only a hairline of space between so the eye takes them as one block. */
.hw-card h3 {
  /* The floor is 1.125rem so it stays a step above the caption below it: the
     phone sets that paragraph to 1rem flat, and at a 1rem floor here the two
     landed on the same 16px and the hierarchy went with it. */
  font-size: clamp(1.125rem, 1.35vw, 1.25rem);
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: -.018em;
  color: var(--er-gray-90);
  margin-bottom: .1875rem;
}
.hw-card p {
  /* The ceiling is one line, and the line that sets it is "Powers them on.
     Press to take a photo, hold to record." — measured, it takes 347px of a
     429px card at 15px, so 18.6px is where it wraps. 1.2vw lands at 17.3 and
     leaves 29px of that card unused. */
  font-size: clamp(.875rem, 1.2vw, 1.0625rem);
  line-height: 1.55;
  /* One step up from the 300 the page inherits, for the same reason as the
     colour below: these are paragraphs now, not one-line captions. NOT 500 —
     that is the heading's weight, and matching it would flatten the two into
     one block the way gray-90 would have. 300 to 400 against a 500 heading
     keeps both steps.

     It also costs no width, which is not obvious. Inter Tight's width curve is
     a V with its minimum at 400: measured on one caption at 16px, the grades
     run 298.8 / 292.2 / 285.6 / 292.4 / 299.2 px at 200 through 600. So 400 is
     the most compact weight in the family, and the hardware row's "Light enough
     that you stop noticing them." stops wrapping on the phone — at 300 it came
     to 285.6px in a 285.3px column and broke by three tenths of a pixel. */
  font-weight: 400;
  /* One step darker than the 70 this was. The captions had grown from a line to
     a short paragraph, and at 6.7:1 that much text read as a footnote to the
     heading rather than as the thing you are meant to stop and read. 10.4:1
     against the heading's 15.7 keeps the two clearly ranked. */
  color: var(--er-gray-80);
}
/* The mic row takes keyboard focus, so it needs a ring — on :focus-visible
   only, so someone who clicks a card never sees it. (It said "both rows" until
   the glasses card row above it was removed.) */
.mic-cards:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 4px;
  border-radius: 6px;
}
.hw-card {
  scroll-snap-align: start;
  /* One card per swipe. Without this the momentum from a hard flick carries
     through several before the snap catches it, which reads as the row sliding
     freely rather than stepping. */
  scroll-snap-stop: always;
}
@media (max-width: 30rem) {
  /* centre, to match the padding above: a card settles in the middle of the
     screen rather than against its left edge. */
  .hw-card { scroll-snap-align: center; }
  /* Taller than the desktop's square, and taller again at 3:4. On a phone the
     card is most of the screen rather than a third of a wide row, so it has the
     width to carry a portrait panel and the product reads bigger in it. */
  .hw-card-art { aspect-ratio: 3 / 4; }}

/* ---- Two-panel spec pair ---- */
.spec-pair {
  /* THE HEIGHT OF THE PICTURE IS THE HEIGHT OF THE SECTION. Both panels are one
     grid row, so whichever shot is taller sets both, and that was the portrait
     water crop: at 100% of a 624px column it rendered 670px tall, took the panel
     to 921px against a 900px viewport, and left the caption below the fold on a
     laptop. It stranded its neighbour too — the landscape case is 462px in the
     same 654px box, so 192px of dead centring space opened under that heading,
     which is the gap that reads as wrong.

     Capping fixes both at once: the panel shortens, and the slack the shorter
     picture floats in shortens with it.

     The cap cannot go on .spec-art. max-height there is a percentage away from
     the images, and a percentage max-height does not resolve against a parent
     whose own height is auto — the pictures ignored it, overflowed the box and
     were clipped by overflow:hidden. It has to be an absolute length on the
     images themselves, which is what this is. */
  --spec-art-h: clamp(12rem, 26vw, 23.75rem);
  /* The scale applied to the non-bleed shot, kept here so the cap below can
     divide by it. It differs by breakpoint, so it is a variable rather than a
     number written twice. */
  --spec-art-scale: 1.45;
  list-style: none;
  margin: clamp(.75rem, 1.4vw, 1.25rem) 0 0;
  padding-inline: var(--gutter);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(.75rem, 1.4vw, 1.25rem);
}
.spec-panel {
  background: var(--er-gray-05);
  border-radius: 4px;
  padding: clamp(1.5rem, 2.6vw, 2.25rem);
  display: flex;
  flex-direction: column;
  /* Keeps both panels the same shape whatever the copy does, so the two product
     shots sit on the same line. */
  min-height: clamp(20rem, 30vw, 27rem);
}
/* Set in the .d-h2 voice — same face, weight and tracking as "Designed to fit
   into everyday life." at the top of the hardware section. Size is unchanged:
   this is a panel heading, not the section's. */
.spec-h {
  /* Floor raised from 1.125rem. On a phone 2vw is 7.5px, so these headings sat
     on the floor at 18px — smaller than the lens pair's 22 and the speakers
     eyebrow's 24, which are the blocks either side of them. 21px closes most of
     that without letting a panel heading match a block heading.
     Only the floor moves, so nothing changes above roughly 656px where 2vw takes
     over, and the 1.75rem ceiling still governs the desktop end. */
  font-size: clamp(1.3125rem, 2vw, 1.75rem);
  font-weight: 300;
  /* d-h2's own leading. Safe here where .bone-h needed loosening, because both
     of these break on an explicit <br /> rather than wrapping, so the pairing of
     descender over ascender is known rather than whatever the column produces. */
  line-height: 1.07;
  letter-spacing: -.048em;
  color: var(--er-gray-90);
  margin: 0;
}
/* The picture takes the space left between the heading and the caption, so both
   panels centre their product on the same axis. */
.spec-art {
  flex: 1;
  /* Asymmetric on purpose. Even margins centred the product at 53% of the panel,
     which left a wide dead band under the heading and pushed the shot low. A
     shallow top inset and a deep bottom one slide the whole picture area up so
     the product sits nearer the heading, still centred across the panel.

     The bottom inset was 3-4.75rem, which put 209px between the case and its
     own caption. Halved. */
  margin: clamp(.375rem, .8vw, .625rem) 0 clamp(1.25rem, 2.4vw, 2.25rem);
  display: grid;
  place-items: center;
  min-height: 0;
}
.spec-art picture { display: contents; }
.spec-art source { display: none; }
.spec-art { overflow: hidden; }
.spec-art img {
  max-width: 84%;
  /* Divided by the scale below, so what the eye sees is --spec-art-h and not
     1.45x it. Both pictures are capped to the same VISUAL height that way, which
     is what puts them on one line. Change the scale and change this with it. */
  max-height: calc(var(--spec-art-h) / var(--spec-art-scale));
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
  /* Zoomed into the product rather than showing it small in its own backdrop.
     The case has 19% of spare margin, so 1.45 crops empty space and stops short
     of clipping the case itself (1.59 would touch it).
     translateY lifts it toward the heading: the file carries 18.8% of empty
     headroom above the case and only 14.7% below, so the product sat low in its
     own frame. Margins could not fix that — they move the box, not the picture
     inside it. Applied before the scale, so -8% renders as roughly -11.6%. */
  transform: scale(1.45) translateY(-8%);
}
/* Water panel: same shape as its neighbour — heading, then the picture, then
   the caption — but the shot runs larger and its foot fades into the panel
   instead of ending on a hard edge. */
.spec-panel.is-bleed .spec-art {
  overflow: hidden;
  /* The frame runs off the foot of this crop, so the fade carries it out rather
     than letting it stop on the area's edge. */
  -webkit-mask-image: linear-gradient(to bottom, #000 68%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 68%, transparent 100%);
}
.spec-panel.is-bleed .spec-art img {
  max-width: 100%;
  /* No divisor: this one carries no scale, so its layout height is its visual
     height and it takes the figure directly. */
  max-height: var(--spec-art-h);
  /* No extra scale: the shot is already cropped tight to the frame, so it fills
     the area's height on its own. The previous 1.26 was compensating for a much
     wider source with empty margins around the subject. Lifted like its
     neighbour — this file carries 19.8% of headroom. What it exposes at the foot
     falls inside the mask's fade, so nothing shows a cut edge. */
  transform: translateY(-8%);
}

/* Both sit above the picture. The shots are later siblings and paint over
   anything static, and lifting them brought the water crop's white headroom up
   across the heading — which would have erased it. The note already needed this
   for the same reason at the foot. */
.spec-h,
.spec-note {
  position: relative;
  z-index: 1;
}
/* The box now starts at 47% of the panel and the frame's own edge at 62%, so
   the copy has room again — it no longer needs the narrow columns it did when
   the picture ran the full width. */
.spec-panel.is-bleed .spec-h { max-width: min(42%, 17rem); }
.spec-panel.is-bleed .spec-note {
  margin-top: auto;
  max-width: min(40%, 16rem);
}

.spec-note {
  /* Was .6875-.75rem — 11px on a phone, 12px on a desktop, below the floor for
     comfortable reading — then .875-1rem, which was legible but still the
     smallest type on the section. These two lines carry the only numbers on
     their panels (a working day, 2,500 mAh, five more charges), so they are
     read rather than glanced at. 16px on a phone, 18px on a desktop.

     The measure is in em, so raising the size keeps the same characters per
     line and the block neither moves nor re-aligns. The bleed panel's note is
     capped in REM though, so there it takes more lines instead — see the note
     on the panel height below it. */
  font-size: clamp(1rem, 1.25vw, 1.125rem);
  line-height: 1.55;
  /* Deliberately darker than the reference's caption grey, which would land
     around 3:1 on this panel. --er-gray-70 measures 6.4:1 here. */
  color: var(--er-gray-70);
  max-width: 28em;
  margin: 0;
}

@media (max-width: 48rem) {
  .spec-pair {
    grid-template-columns: minmax(0, 1fr);
    /* Stacked, the panels are separate rows and each sized to its own content,
       so the landscape case shot (1.608) made one 359px tall against the
       portrait water shot's (0.932) 535. 1fr on the auto rows makes every row
       the height of the tallest, and .spec-art's flex:1 absorbs the difference —
       no magic number, and it stays right if either shot is ever replaced. */
    grid-auto-rows: 1fr;
  }
  .spec-panel { min-height: 0; }
  .spec-art { margin: 1.5rem 0; }
  /* With the rows matched the art area is about 335px rather than the 160px
     strip it was, so the case can run near its desktop size. Full column width
     and 1.35 rather than 82% and 1.15: at that scale the crop takes 12.96% off
     each side against the file's 16.1% of empty margin, which leaves about 13px
     before it would touch the case itself. 1.475 is where it would.
     The lift is smaller than desktop's 8% because this crop is wider and the
     case sits only about 3% low in its own frame. */
  .spec-art img { max-width: 100%; transform: scale(1.35) translateY(-3%); }
  /* Matches the 1.35 above, so both shots still cap to the same visual height. */
  .spec-pair { --spec-art-scale: 1.35; }
  /* The water panel's heading and caption are held to 42% and 40% so they stay
     clear of the picture bleeding up the right of the panel. Stacked, there is
     no picture beside them to stay clear of — the shot is above — and the caps
     only strangle the text: 118px of column, which set this caption as four
     lines of about five words. They come off here. */
  .spec-panel.is-bleed .spec-h,
  .spec-panel.is-bleed .spec-note { max-width: none; }}

/* ---- Lifestyle band ---- */

/* Full bleed, edge to edge, breaking out of the page gutter the way the bone
   band does. No radius for the same reason: a rounded corner at the window's
   edge reads as a card that failed to reach it. */
.lifestyle-band {
  /* Standing clear of the plate above it and the copy below. At 56px the band
     read as the bottom half of the speaker plate rather than as its own thing;
     80px at 1440 is the air the other section breaks on this page take. */
  margin: clamp(3rem, 6vw, 5rem) 0;
  width: 100%;
  overflow: hidden;
  /* The heading and its scrim are positioned against this. */
  position: relative;
  /* Behind the picture while it loads, so the band does not flash white on a
     page whose next section is dark. Sampled from the file's own asphalt. */
  background: #3b3a38;
  /* THE SCREEN, not a ratio. This band has been 2.4 (594 tall), 3.0 (475), 2.7
     (528) and 5/2 (570), climbing back the whole way, and every one of those
     left a strip of white under it before the next section began. A height
     rather than an aspect ends that: the picture is as tall as the window it is
     read in, and cover takes whatever shape that window happens to be.

     Minus the sticky header, so when the band's top meets the header its bottom
     meets the fold, rather than sitting 48px past it. svh rather than vh for
     the reason the bone band uses svh — a phone browser's retractable bottom
     bar must not push the fold off screen.

     NO max-height. The cap existed to stop a ratio running away on a tall
     monitor, and there is no ratio to run away now; a cap here would be the one
     thing stopping the band filling the screen, which is the request. It was
     also the binding constraint last time this moved — 60svh held the band at
     540 while the ratio asked for 570, so the stylesheet and the page disagreed
     about its shape until it was measured.

     GROWN AT THE BOTTOM to carry the three-point row, which is the only
     reason this is no longer exactly one screen. The extra is roughly the
     row's own height, so the picture gains the air the row sits in rather than
     the row being laid over her: at 1440 the band is 1104 against 1008.

     It costs something, and the cost is sideways. This box is already NARROWER
     than the file's 1.589 at desktop widths, so cover is cropping the sides,
     not the top and bottom — every pixel of height throws away more of the
     crowd. Measured at 1440: the visible slice falls from 89.9% of the file to
     83.6%, and her figure spans 42.3% to 71.7%, so she stays well inside it.
     That slice is what a further increase spends. */
  height: calc(100svh - var(--header-height) + clamp(5rem, 7vw, 7rem));
}
/* The scrim the heading sits on. ::after rather than ::before, because ::before
   is inserted ahead of the picture and would be painted under it; the caption
   takes a z-index so it stays above this.

   Falling from the TOP, following the heading. The top of this picture is the
   harder half to sit white text on: its mean is darker than the bottom (68 to
   78) but it holds the file's brightest pixels — 253 against 177 — in the sky
   and the lit shopfronts behind the crowd. So this end is a shade stronger and
   a shade shorter than the bottom scrim it replaces, dense where the words are
   and gone before it reaches her. */
.lifestyle-band::after {
  content: "";
  position: absolute;
  /* Anchored to the top-left CORNER rather than run across the full width,
     because that is where the words are. A band-wide curtain heavy enough to
     carry body copy would have to hold 0.53 alpha down to half the picture's
     height, and the photograph is the point of the band.

     A heading could live on the old vertical ramp: 43px is large text, it owes
     3:1, and it sits in the first 90px where the gradient is still dense. Body
     copy cannot. Four lines of 16px reach 31% of the way down on desktop and
     53% on a phone, by which point that ramp had thinned to about 0.20 — and
     the photograph peaks at 253 under exactly that region, a blown highlight in
     the glass behind her. Measured 2.51:1, 2.33:1 and 2.39:1 against a 4.5
     floor: failing at every breakpoint, not marginal at one.

     These numbers are the lightest combination that clears both floors across
     all three crops, found by search rather than by eye — worst heading 9.2:1,
     worst paragraph 5.17:1. Lighter fails the paragraph somewhere. */
  inset: 0;
  /* TWO layers now, one per block of words, compositing over each other:
     the corner for the heading, and a bottom ramp for the three-point row.
     The corner gradient is no help down there — it is at zero by 85% of the
     way out, which is the whole point of it, and the row sits past that.

     The bottom half runs full width because the row does, and it has to be
     real rather than token: the lower third of the file is the LIT half of the
     picture, mean 74 against the top's 68 but with the crossing's pavement in
     it, and 1% of those pixels are above 170.

     .45 at 26%, up from .30 and then .38. This stop has moved twice, both
     times for the same reason: the row grew, its top edge climbed, and 26% is
     where the ramp starts giving up. One line per point put the top edge at
     23%, two lines at 26%, three lines at 38% on the smallest phone - and at
     360 the stacked row now begins 105px below the paragraph above it, which
     is the tightest this picture has been.

     Every move was searched rather than guessed, minimising mean alpha over the
     element so the picture pays as little as it can. Worst block 4.98:1.

     ONE THING TO WATCH IF THIS IS TOUCHED. The search and the checker sample
     text boxes at their own densities, and while they disagreed the search
     reported 4.95:1 on a ramp the checker failed at 4.44:1 - the coarser grid
     simply missed the bright pixel. They step at bw/2 now, the same. If a
     result here ever looks better than the checker's, that is the first thing
     to compare. */
  background:
    radial-gradient(140% 100% at 0% 0%,
      rgba(0, 0, 0, .80) 0%, rgba(0, 0, 0, .60) 50%, rgba(0, 0, 0, 0) 85%),
    linear-gradient(to top,
      rgba(0, 0, 0, .85) 0%, rgba(0, 0, 0, .62) 12%,
      rgba(0, 0, 0, .45) 26%, rgba(0, 0, 0, 0) 42%);
  pointer-events: none;
}
/* The caption box, which is the whole picture. It exists so the figure has
   exactly one figcaption and that caption is its LAST child, which is where the
   parser wants it; the two blocks of words are positioned against this instead
   of against the figure, and inset:0 makes those the same box.

   pointer-events:none because it covers the photograph edge to edge and there
   is nothing in it to click. Without it this would swallow every event the
   picture gets, including a text selection dragged across it. */
.band-cap {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

.band-copy {
  position: absolute;
  z-index: 1;
  /* The page gutter the cards and the plate use, so the heading starts on the
     same line they do. */
  left: clamp(1.25rem, 3.2vw, 3.5rem);
  /* Top-left corner. Near enough square with the left gutter at every width —
     48 against 46 at 1440 — so it reads as an inset from the corner rather than
     as two unrelated offsets.

     It clears the sticky header, which is the reason this is not smaller: the
     band is exactly `100svh - header` tall, so at the moment its top meets the
     header's bottom the whole picture is on screen, and that is the moment this
     heading is read. It slides under the header afterwards, which is what every
     other thing on the page does. */
  top: clamp(1.5rem, 3.4vw, 3rem);
  max-width: min(52%, 32rem);
  color: var(--er-white);
  margin: 0;
  /* A container now rather than the heading itself: it holds a heading and a
     paragraph, and the two need different type. Flex so the gap between them is
     one declaration instead of a margin that has to be cancelled somewhere. */
  display: flex;
  flex-direction: column;
  gap: clamp(.6rem, 1.1vw, .9rem);
}

.band-h {
  display: block;
  font-size: clamp(1.75rem, 3.4vw, 3rem);
  font-weight: 300;
  line-height: 1.05;
  letter-spacing: -.048em;
}

/* Deliberately much smaller than the heading and no wider. Two sentences at
   heading size would cover a third of the picture and stop being a caption on a
   photograph — it would be a slide. The measure is capped in ch rather than by
   the parent's 52%, because at 1440 that 52% is 700px and a 15px line running
   700px is about 95 characters, well past comfortable reading. */
.band-p {
  display: block;
  max-width: 42ch;
  font-size: clamp(.875rem, 1.25vw, 1rem);
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: -.008em;
  /* PURE white, not the 82% this started at. The scrim above was solved for
     white at 4.5:1; dropping the text to 82% lets the picture back through it
     and lands the same pixel at about 3.6:1. The saving in "quietness" was not
     worth a fifth of the contrast, and the hierarchy here is carried by size
     anyway — 43px against 16px is not a difference anyone needs help seeing. */
  color: var(--er-white);
}

/* The three points along the bottom edge. Same left gutter as the heading, so
   the two blocks of words on this picture start on one line rather than on two.

   Grid rather than flex, with equal fractions: three columns that size
   themselves to their content would be three different widths, and the row
   would read as three unrelated notes rather than as a set. minmax(0, 1fr)
   rather than 1fr because a grid item's default min-width is auto, and one long
   unbroken word would push the column past its share. */
.band-points {
  position: absolute;
  z-index: 1;
  left: clamp(1.25rem, 3.2vw, 3.5rem);
  right: clamp(1.25rem, 3.2vw, 3.5rem);
  bottom: clamp(1.5rem, 3.4vw, 2.75rem);
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--er-white);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Was 1.75rem, which set the two notes 28px apart inside a 768px row and read
     as one block broken in half rather than as two points. */
  gap: clamp(.9rem, 4.4vw, 4rem);
  /* CAPPED, and this is what pulls the two together. Left and right are both
     set, so without it the row spans the full gutter-to-gutter width and the
     two columns come out 658px at 1440 - each holding about 250px of text and
     400px of nothing, which reads as two notes pinned to opposite ends of the
     bottom edge rather than as a pair.

     40rem puts them at 306px, near enough the measure the text wants, so the
     gap between them is a gap rather than a gulf. An absolutely positioned
     box with left, right AND a width that resolves narrower than the two of
     them is over-constrained, and in a left-to-right direction the value CSS
     drops is `right` - so the row keeps the left gutter and the slack all falls
     on the right, which is where the picture is. */
  /* 48rem, up from 40 with the type. Widened by the same fifth, so each column
     holds the same NUMBER of characters as before and the descriptions stay
     three lines. Left at 40 the bigger type would have wrapped each of them to
     four, and the row would have grown by a line rather than by its type - which
     costs picture and pushes the top edge into the thin part of the scrim, the
     thing that has broken this element twice already. */
  /* 52rem, up from 48. The gap above took 64px out of the row, and at 48rem
     that came off the columns — 352px each against 370 — which is exactly the
     direction the note above warns about, since a narrower column wraps each
     description to a fourth line and grows the row by a line. Widening by more
     than the gap took puts the columns at 384px, wider than they were, so the
     descriptions stay at three lines and the row is no taller. */
  max-width: min(100%, 52rem);
  /* NOT centred. auto margins were tried here and reverted: the box is
     over-constrained — left, right and a width all set — so CSS drops `right`
     and the row keeps the left gutter with all the slack on the right, and that
     is the wanted behaviour rather than a bug. Centred, the second column moved
     onto the subject. The slack falls where the picture is, which is the point.

     The widened gap and 52rem above are from that same change and stay: the
     complaint was where the pair sat, not how far apart they were. */
}
.band-points li {
  display: grid;
  gap: .45rem;
  align-content: start;
}
.band-points svg {
  /* Follows the type. At 22 against 19px names the icon had been the larger of
     the two; 26 keeps the ratio it was drawn at. */
  width: 26px;
  height: 26px;
  /* The page's icon convention: stroke only, 1.4 at a 16 viewBox. */
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.band-points b {
  font-weight: 500;
  /* Up a fifth, 16-19px against 14-16. */
  font-size: clamp(1rem, 1.35vw, 1.1875rem);
  letter-spacing: -.014em;
  line-height: 1.2;
}
/* PURE white, and the same white as the name above it. The reference sets its
   descriptions in grey, and grey is the wrong move on a photograph for the
   reason already written into .band-p: the scrim under this row is solved for
   white at 4.5:1, and dropping the text to 70% hands most of that back. The
   hierarchy is carried by weight instead - 500 against 300 - which costs no
   contrast at all. */
.band-points span {
  font-weight: 300;
  /* 15-18px against 13-15. The gap to the name above stays proportional, so the
     pair still reads as a name and its line rather than as two sentences. */
  font-size: clamp(.9375rem, 1.25vw, 1.125rem);
  line-height: 1.45;
  letter-spacing: -.006em;
}

.lifestyle-band img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Measured on the file, not chosen. The top of her hair sits at 3.8% of the
     picture's height, so ANY downward shift crops her head: the vertical anchor
     is 0, which keeps every pixel of headroom the file has. Her figure spans
     42.3% to 71.7% across, centred on 57%, and 55% is where that sits with the
     crowd still reading on the left. Only one of the two ever applies — the
     band crops vertically above the phone and horizontally on it. */
  object-position: 55% 0;
}
@media (max-width: 64rem) {
  /* Not full height below this. A window this narrow is a tablet held upright
     or a small laptop, where a whole screen of photograph between two sections
     reads as a wall rather than as a picture — and the taller the box gets
     against the file, the more of the crowd cover throws away at the sides.
     Back to a ratio here: 2/1 keeps the top 79% of the file. */
  /* 9/5 rather than the 2/1 it was, the same downward growth the desktop
     height takes: 569 tall at 1024 against 512. Still WIDER than the file's
     1.589, so cover is still cropping the top and bottom here and the sides
     are untouched - it keeps the top 88% of the file against 79%. */
  .lifestyle-band { height: auto; aspect-ratio: 9 / 5; }}
@media (max-width: 48rem) {
  /* Narrower than the file, so this is the one width where cover crops the
     SIDES.

     18/25, or 0.72, from 4/5: 542 tall at 390 against 487. This is the one
     breakpoint where growing the band costs picture, because it is the one
     where the box is narrower than the file and cover crops the SIDES. The
     visible slice goes from 50.4% of the width to 45.3%, running 30.1% to
     75.4% with the anchor at 55%. Her figure spans 42.3% to 71.7%, so she
     still clears both edges - by 3.7% on the right, which is the margin any
     further increase here would be spending. */
  /* THE WHOLE SCREEN, not a ratio. 18/25 gave 542px of an 844px phone, so the
     band sat in the middle of the fold with the sections above and below it
     showing white on both sides. Minus the header for the same reason the
     desktop rule subtracts it: the heading is inset 24px from the band's top,
     and a full 100svh would slide that under the sticky bar.

     115svh, not 100. A screen-and-a-bit, so the picture carries on past the fold
     rather than ending exactly at it - 918px against the 542 it started at.

     The whole of that is paid for sideways, because this is the one breakpoint
     where the box is narrower than the file and cover crops the SIDES: the
     visible slice falls from 45.3% of the file width to 26.8%.

     THE RULE THAT USED TO CAP THIS WAS WRONG AND IS WORTH SAYING SO. It was
     "her whole figure has to fit", which put a ceiling of 835px on this element
     and made a purpose-cut portrait file look pointless too - at these aspects
     no crop of this photograph holds her silhouette entire. Rendering the
     candidates and looking at them says otherwise: what crops past 835 is the
     outer edge of her bag, while her face and the GLASSES - the product, and the
     reason the photograph is here - get larger and read better the tighter it
     goes. The constraint belongs on the product, not on her outline. */
  .lifestyle-band { height: calc(115svh - var(--header-height)); aspect-ratio: auto; }
  /* 60%, not the 55% every wider view uses. Her figure spans 42.3% to 71.7% of
     the file and the slice left at this height is 30.8%, so there is 1.4% of
     width spare in total and where it sits is no longer a matter of taste. At
     55% the window runs 38.0% to 68.9% and takes the right edge off her.
     Centring it on her instead - p = (0.57 - f/2) / (1 - f) - gives 60%, and the
     window runs 41.5% to 72.3% with her inside it at both ends.

     There is a floor under this. On a 412x960 handset the slice is 28.4%, which
     is NARROWER than the 29.4% she occupies, so no anchor keeps her whole and
     the outer edge of her jacket goes. That is the geometry of a 1.59 file in a
     0.45 box rather than a value to tune; if it ever matters, it wants a
     purpose-cut portrait file, the way the hero already has one. */
  .lifestyle-band img { object-position: 60% 0; }}

/* 34rem, NOT the 48rem the band's own crop changes at. The two are different
   questions and they have different answers: the picture stops being wider than
   the file at 768, but three columns of words are still perfectly readable
   there - 157px each at 544, which is about 22 characters a line. They stop
   being readable somewhere below that. At 390 each column is 106px, a 13px line
   holds fifteen characters, and every description would break four ways; the
   row would end up taller than the block of copy at the top of the same
   picture. So the row stacks on its own terms, one item per line with the icon
   beside the words rather than above them, and each description gets a single
   line back. */
@media (max-width: 34rem) {
  .band-points { grid-template-columns: 1fr; gap: .85rem; }
  .band-points li {
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: .6rem;
    row-gap: .2rem;
  }
  .band-points svg { width: 20px; height: 20px; grid-row: span 2; align-self: start; }
  .band-points span { max-width: 46ch; }}
/* The two technical lines, under the pair above them. Smaller and greyer than
   both: the component is named first, the claim second, and this is the
   evidence for it. Same rank the bone band's paragraph takes on its own plate,
   inverted for a near-white ground. */
.speaker-p {
  /* Up a step: 17.3px at 1440 against 15.1, and 14 on a phone against 13. The
     copy lost six words at the same time, so the block is shorter even at the
     larger size. Still a clear step under the 22px line above it. */
  font-size: clamp(1rem, 1.2vw, 1.125rem);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: -.012em;
  color: var(--er-gray-70);
  /* 34px under the claim at 1440, against 20. It is a separate thought from the
     line above it and was reading as that line's second half. The bone band's
     paragraph takes the same step for the same reason. */
  margin: clamp(1.125rem, 2.4vw, 2.125rem) 0 0;
  max-width: 42ch;
}

/* Both lines set in the .d-h2 voice, the same treatment the bone band and the
   spec panels take: same face, weight and tracking as "Designed to fit into
   everyday life." Sizes are unchanged — this block has its own hierarchy. */
.o1-tag-speaker .eyebrow {
  /* The component name is the headline; the line under it explains it. */
  font-style: normal;
  text-transform: none;
  font-weight: 300;
  font-size: clamp(1.5rem, 3vw, 2.625rem);
  line-height: 1.07;
  letter-spacing: -.048em;
  color: var(--er-gray-90);
  /* Scaled, not flat. At .75rem a 42px title and a 24px one both had 12px under
     them, which is close set under the big one — the block read as three lines
     stacked rather than as a title, a claim and the evidence. 22px at 1440. */
  margin-bottom: clamp(.75rem, 1.5vw, 1.375rem);
}
/* The supporting line under the title, not the title itself. */
.audio-h {
  font-size: clamp(1rem, 1.55vw, 1.375rem);
  font-weight: 300;
  /* 1.2, not d-h2's 1.07, for the same reason .bone-h keeps it: that figure is
     leading for a 63px display line that breaks where it is told, and this one
     is 16-22px wrapping on its own, where 1.07 runs descenders into the next
     line's ascenders. */
  line-height: 1.2;
  letter-spacing: -.048em;
  color: var(--er-gray-70);
  /* Nothing follows it on the plate. */
  margin-bottom: 0;
}
@media (max-width: 48rem) {
  /* 13px, not 14. The arm crosses this crop diagonally and the copy's LAST line
     is the one that meets it — at 14px the paragraph ran five lines and that
     last one ended over the temple, grey text on a dark arm. A smaller size
     fits more words per line, which lifts the block rather than widening it. */
  .speaker-p { font-size: .875rem; }}

/* ---- The two microphone cards ---- */

/* Two-up, in the shape .hw-card already gives: grey rounded plate, caption on
   the page's own white below it. The classes are reused rather than copied so
   the caption sizes, the plate's radius and its grey all stay tied to the
   product cards — if those move, these move with them. */
.mic-cards {
  list-style: none;
  margin: clamp(1.75rem, 3.4vw, 3rem) auto 0;
  /* Inset further than the page gutter, which is about 21px and left the outer
     plates almost against the window. Standing the plates up made that read as
     crowding rather than as full width, so the row is held in — 46px at 1440,
     and never less than the gutter it replaces. */
  padding-inline: clamp(1.25rem, 3.2vw, 3.5rem);
  max-width: var(--maxw);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(.75rem, 1.4vw, 1.25rem);
}
/* Portrait, at the reference's proportion. This has been 4:3, then square, and
   is now 3:4 — at 448 across that stands a plate 597 tall.

   THE svh CAP IS GONE, deliberately. It existed to keep the block inside one
   screen on a short window, and at this height it cannot: plate plus caption
   plus the row's margin comes to about 725, against 648 under the header on a
   700px window. Standing the plates up and fitting one screen were always
   incompatible; this picks the height. */
.mic-cards .hw-card-art {
  aspect-ratio: 3 / 4;
  display: grid;
  place-items: center;
}
/* The photographic plate. contain rather than the cover .hw-card-art img gives
   everything else: this plate is capped by svh on short screens, and cover would
   crop the picture's top and bottom as the box flattens — taking the pulses with
   it. contain keeps the whole frame in view, and the letterbox cannot be seen
   because the picture's ground and the plate's are both 245. */
.mic-plate img { object-fit: contain; }

/* 88%. The drawings are 420x240 and the plate is now portrait, so there is a
   great deal of air above and below them either way — the width is all that
   stops them looking stranded in it. Wider than this and they crowd the sides,
   which is worse than the air. */
.mic-art svg {
  width: 88%;
  height: auto;
  overflow: visible;
}
/* The frame outline: a line drawing, not a filled shape, so it reads as a
   diagram of where the microphones are rather than as a picture of glasses
   competing with the photograph above it. */
.mic-frame { stroke: var(--er-gray-30); stroke-width: 3; }
.mic-dot { fill: var(--er-gray-50); }
/* The bone sensor is the one being singled out, so it is the page's ink where
   the other four are grey, and it carries the rings the band's marker uses. */
.mic-bone { fill: var(--fg); }
.mic-bone-ring { fill: none; stroke: var(--fg); stroke-width: 2; opacity: .28; }
.mic-bone-ring:nth-of-type(2) { opacity: .14; }
/* Rounded joins so the noisy left-hand end does not render as a field of
   spikes; the line is one continuous path from noise into voice. */
.mic-wave {
  stroke: var(--fg);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Airflow. The same grey as the frame outline in the first plate: in both, the
   grey is context and the ink is the sensor being pointed at. */
.mic-flow { stroke: var(--er-gray-30); stroke-width: 2.4; }

@media (max-width: 48rem) {
  /* A SCROLLER, not a stack. Three across is impossible at this width — each
     plate would be under 170px and the five dots in the first one stop being
     separable — but stacked they came to 1017px against an 812px screen, so the
     block could not be taken in at all. One card per frame is the same answer
     the product row uses, and the same rules with it: the swipe stays inside
     the row, momentum cannot skip a card, and the y-axis is closed because
     .reveal parks each card 18px down and that is scrollable overflow. The
     padding gives those 18px back so no caption is clipped while a card waits
     to animate. */
  .mic-cards {
    grid-auto-flow: column;
    grid-auto-columns: 100%;
    grid-template-columns: none;
    gap: .75rem;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 18px;
    margin-bottom: -18px;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    padding-inline: 12%;
    scroll-padding-inline: 12%;
    scrollbar-width: none;
  }
  .mic-cards::-webkit-scrollbar { display: none; }
  .mic-cards .hw-card { scroll-snap-align: center; scroll-snap-stop: always; }
  .mic-cards .hw-card-art { aspect-ratio: 3 / 4; }
  /* The plate is about 285px here, so at 76% the five dots came out 3.3px
     across and stopped reading as marked positions. */
  .mic-art svg { width: 88%; }}

/* ---- Prescription lenses ---- */
/* Mirror of the speakers block: copy left, plate right. Heading at the top of
   the column and the caption at its foot, with the picture between them
   vertically — the arrangement the reference layout uses. */
.lens-split {
  margin-top: clamp(.75rem, 1.4vw, 1.25rem);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(.75rem, 1.4vw, 1.25rem);
  align-items: stretch;
}
/* space-between is kept, so the heading holds the top of the column and the
   paragraph its foot. The bottom inset is zero so the paragraph's last line
   lands on the same baseline as the foot of the lens image beside it — the
   column and the image are the same height, so with no inset the two ends meet
   exactly. The top inset is untouched, so the heading does not move. */
.lens-copy {
  display: flex;
  flex-direction: column;
  /* Was space-between, which pinned the paragraph to the foot of the column and
     left it floating a long way under the two lines it belongs to. It reads as
     one thought — "Custom lenses / from -6.00 to +6.00." and then how that is
     made — so it now follows the heading with a gap rather than the height of
     the picture.

     Only desktop is affected. Below 48rem .lens-copy is display:contents and its
     children are ordered around the plate (heading, picture, caption) to mirror
     the speakers block above, so justify-content does not apply there and that
     arrangement is untouched. */
  justify-content: flex-start;
  gap: clamp(1.25rem, 2.2vw, 2rem);
  padding: clamp(1rem, 2.4vw, 2.5rem) clamp(0rem, 2vw, 2.25rem) 0 0;
  max-width: 34rem;
}
/* Set in the same voice as the speakers block above, which is the .d-h2 voice:
   the page's grotesque at 300 with -.048em.

   Unlike the speakers pair, these two are the same size as each other and share
   a colour — they are one sentence broken across two lines ("Custom lenses /
   from -6.00 to +6.00."), not a heading with a smaller line under it. So they
   also share d-h2's 1.07 leading rather than one of them loosening to 1.2:
   at this scale neither wraps, and holding them tight is what keeps the two
   lines reading as a single statement. */
.lens-copy .eyebrow {
  font-style: normal;
  text-transform: none;
  font-weight: 300;
  font-size: clamp(1.375rem, 2.5vw, 2.25rem);
  line-height: 1.07;
  letter-spacing: -.048em;
  color: var(--er-gray-90);
  margin-bottom: .375rem;
}
.lens-h {
  font-size: clamp(1.375rem, 2.5vw, 2.25rem);
  font-weight: 300;
  line-height: 1.07;
  letter-spacing: -.048em;
  color: var(--er-gray-90);
  margin: 0;
}
/* Small and at the foot of the column, the way the reference sets its caption —
   a footnote to the picture, not a second paragraph. */
.lens-p {
  font-size: clamp(.9375rem, 1.2vw, 1.0625rem);
  line-height: 1.6;
  color: var(--er-gray-70);
  max-width: 26rem;
  margin: 0;
}
.lens-foot {
  display: grid;
  gap: clamp(1.125rem, 1.8vw, 1.625rem);
  max-width: 26rem;
}

.lens-art {
  margin: 0;
  aspect-ratio: 1 / 1;
  background: var(--er-gray-05);
  border-radius: 4px;
  overflow: hidden;
  display: grid;
  place-items: stretch;
}
.lens-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

@media (max-width: 48rem) {
  /* Same lead-in as the speakers block above it: this opens its own subject with
     its own heading, and at 12px "Custom lenses" sat tight under the block
     before it. Declared here rather than beside the audio rule because
     .lens-split's base margin comes later in the file than that block. */
  .lens-split { grid-template-columns: minmax(0, 1fr); margin-top: 2.5rem; }
  /* Same arrangement as the speakers block above: name and claim, then the
     picture, then the caption that qualifies it. display:contents drops
     .lens-copy's box so its two children become grid items beside .lens-art and
     order can put the plate between them.
     Easier here than it was up there, because the heading pair already lives in
     its own .lens-head wrapper — it stays in block flow, so the eyebrow's margin
     cannot stack with the grid's row gap the way .audio-copy's did. Nothing to
     re-tune. */
  .lens-copy { display: contents; }
  .lens-head { order: 1; }
  .lens-art  { order: 2; margin-block: .5rem; }
  /* Was .875rem against the copy that used to follow the heading; the row gap
     plus the plate's own margin give the 20px either side now, matching the
     speakers block. */
  .lens-foot { order: 3; margin-top: 0; }}

/* Both lines here are set in the .d-h2 voice — the same face, weight and
   tracking as "Designed to fit into everyday life." at the top of this section.
   Sizes are unchanged: this is the band's own hierarchy, not the section
   heading's. */
/* The scroll block is named alongside the band here because this is the ONLY one
   of the bone rules that is scoped rather than standalone -- bone-h, bone-stat,
   bone-stat-* and bone-p all carry their own styling and are reused directly.
   So this is the only rule that has to be told the copy now appears twice. */
.o1-tag-bone .eyebrow {
  /* Set the way these technology sections name a component: sentence case
     rather than caps. The base .eyebrow is an uppercase caption, which is a
     different register entirely. */
  font-style: normal;
  text-transform: none;
  font-weight: 300;
  /* This is the headline: the component is what the band is about, and the line
     under it explains it. Wrapping is fine at this size — it reads as a
     deliberate two-line title rather than a label that ran out of room.
     It was sized when it was one of only two lines on the band. There are three
     now, so it comes down a step — 52px at 1440 against 63 — and the paragraph
     under it has somewhere to sit. The binding unit is still "Bone conduction",
     which has to hold one line for the title to break where it is meant to; at
     52px it measures well inside the column. */
  font-size: clamp(1.75rem, 3.6vw, 3.25rem);
  letter-spacing: -.048em;
  line-height: 1.05;
  color: var(--er-gray-90);
  /* Was a flat .875rem, so a 52px title and a 34px one had the same 14px under
     them. Scaled now: 22px at 1440, and 16 on a phone where the title is 34. */
  margin-bottom: clamp(1rem, 1.5vw, 1.375rem);
}
/* Now the supporting line under the title, not the title itself. It has to hold
   ONE line: at 34.6px it measures 449px against a 480px column. */
.bone-h {
  /* Down a step with the title above it: 28px at 1440 against 34. */
  font-size: clamp(1.125rem, 2vw, 1.75rem);
  font-weight: 300;
  /* d-h2's 1.07 is leading for a 63px display line that breaks where it is told.
     This one is 22px in a 30% column and wraps on its own, where 1.07 runs the
     descenders of one line into the caps of the next. 1.2 is the same register
     without that collision. */
  line-height: 1.2;
  letter-spacing: -.048em;
  color: var(--er-gray-70);
  margin: 0;
}
/* The figure and what it buys, under the two display lines. Smaller and dimmer
   than both: the title names the part, the line under it says what the part
   does, and this is the evidence — it should be read third, not first.
   .62 rather than .78 for that reason. On a black ground the same step reads
   further apart than it would on white, so the drop is smaller than it looks. */
.bone-p {
  /* Up a step from 16.56px to 18.7 at 1440, and 16 rather than 15 on a phone. */
  font-size: clamp(1rem, 1.3vw, 1.1875rem);
  /* 400, one step off the 300 the two lines above it are set in. Bolder than
     its own headings looks wrong written down and reads right on the page: they
     are 52px and 28px of light display type and this is 19px of body, so the
     size carries the hierarchy and the weight only has to make the small line
     hold its own on a black ground. 500 was measured too and crowds the 28px
     line above it. */
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: -.014em;
  color: var(--er-gray-70);
  /* 34px under the figure block at 1440, against 17. The paragraph is a
     separate thought from the number and was reading as its fourth line. */
  margin: clamp(1.5rem, 2.4vw, 2.125rem) 0 0;
  /* 44ch, and it has to stay 44ch. Set at 16.56, 18.7 and 19px and at weights
     300, 400 and 500, this is the only measure of the three tried that holds
     three lines with nothing stranded at every one of them; 40ch leaves a
     17-character orphan as soon as the type grows, and 38ch costs a fourth
     line. It comes to 476px in a 480px column at this size, so the paragraph
     now fills the width the two display lines set rather than sitting inside
     it — which is the trade for the larger type. */
  max-width: 44ch;
}
/* The figure, set the way this category sets one: a hairline, a lead-in, the
   number at display size, and the comparison beneath it. The three parts are one
   sentence broken over three lines, so only the first is capitalised and none of
   them takes a full stop. */
.bone-stat {
  /* 43px above the rule at 1440, against 27. The rule is a section break inside
     the column and needs to read as one. */
  margin: clamp(2rem, 3vw, 2.75rem) 0 0;
  /* 32px between the rule and the lead-in, against 16. This is the gap the
     block was judged on: at 16 the rule sat on top of the words and the four
     lines below it ran together as one grey mass. */
  padding-top: clamp(1.375rem, 2.2vw, 2rem);
  /* The rule is the part that makes it read as a figure rather than as more
     copy. The page's own hairline now that the band is light. */
  border-top: 1px solid var(--rule);
  max-width: 44ch;
}
.bone-stat-lead,
.bone-stat-sub {
  /* The same size as .bone-p below, deliberately, and the two clamps have to
     stay equal. These two read as one sentence wrapped around the figure —
     "Removes up to / 30x more / ambient noise than an acoustic mic." — so at
     15px against the paragraph's 18.7 they looked like a caption on the number
     rather than the claim the number is evidence for. The figure still carries
     the emphasis: it is 42px, in 500, and the only thing on the band in full
     white. */
  font-size: clamp(1rem, 1.3vw, 1.1875rem);
  font-weight: 400;
  line-height: 1.35;
  letter-spacing: -.01em;
  color: var(--er-gray-70);
  margin: 0;
}
.bone-stat-fig {
  /* 42px at 1440, down from 72 and then 64, and 28 on a phone.
     48 was tried first and measured wrong: against a 52px title it is a 7% step,
     which is the near-tie that reads as one size badly chosen rather than as two
     ranks. The column now steps 52 / 42 / 28 / 19, which is 19%, 33% and 33% —
     even the whole way down and no pair close enough to argue.
     What keeps the figure loud at this size is not scale any more: it is the
     only thing on the band set in 500 and in full white, between a hairline and
     two grey labels. */
  font-size: clamp(1.75rem, 3vw, 2.625rem);
  /* 400. It was 500, and there is no step between the two: the page requests
     200, 300, 400, 500 and 600 as discrete cuts, so anything in between snaps
     to one of them and would have rendered as 500 again.

     That leaves the figure at the same weight as its own two labels and the
     paragraph under them, so weight no longer separates it from anything. What
     does is the other two axes, and they are still wide open: 42px against 15
     and 19, and full white against .58 and .62 on a black ground. */
  font-weight: 400;
  line-height: 1;
  letter-spacing: -.045em;
  color: var(--er-gray-90);
  /* In ems, so the air around the number tracks its own size. Raised with the
     rest of the block: .24 and .28 give 10px and 12 at 42px, 7 and 8 at 28.
     They were .17 and .2, which was fitted when the figure was 72px and left
     the three lines of the stat crowded once it came down to 42. */
  margin: .24em 0 .28em;
}
@keyframes bonePing {
  /* Fades up rather than starting at full: a ring that appears already solid
     looks stamped on. Runs to 100% so the three overlap with no dead gap. */
  0%   { scale: .28; opacity: 0; }
  9%   { opacity: .9; }
  100% { scale: 1;   opacity: 0; }
}
@media (max-width: 48rem) {
  .o1-tag-bone .eyebrow { white-space: normal; }

  /* The phone had been living on the clamp FLOOR. Below 48rem the copy comes
     out from over the picture and takes the full width — 343px at 375 against
     the 428 it gets on a desktop — but the shared vw term is tiny at that
     viewport, so both lines fell to their minimums and sat small in a column
     with room to spare. Their own vw terms, sized against that full width:
     "Bone conduction" measures 268px of 343 at 39.75px, and the line under it
     301px of 343, so the title still breaks in two and the line under it still
     holds one. */
  /* The same step down the desktop takes. "Bone conduction" measured 268px of
     343 at 39.75px, so at 34 it still holds one line and the title still breaks
     in two. */
  .o1-tag-bone .eyebrow { font-size: clamp(1.875rem, 9vw, 2.375rem); }
  .bone-h { font-size: clamp(1.125rem, 5.2vw, 1.375rem); }
  /* Full width here, where the copy is not in a 30% column. */
  .bone-p { max-width: none; }}

/* ---- The app ---- */
/* Tinted so the three phone mockups read as objects standing on a surface rather
   than floating on the same white as the page. */
.section-app { background: var(--app); }

/* ---- Privacy ---- */
/* Pulled up toward the app section: the two were held apart by the app's 33px
   foot plus a full 64px head here. Halving the head closes the seam; the space
   below is untouched, so the section's own rhythm is unchanged. */
.section-privacy { padding-top: clamp(1.25rem, 2.4vw, 2.5rem); }

/* One object: the copy sits on the plate rather than stacked above and below it,
   which is what lets the whole section be taken in at once. The plate carries the
   full container width — the figure is 2.36:1 with the bubbles out at the extreme
   edges, so anything narrower would crop them away. */
.privacy-plate {
  position: relative;
  margin: 0 auto;
  max-width: var(--maxw);
  padding-inline: var(--gutter);
  /* Type below is sized in cqw so it tracks the PLATE, not the viewport. The
     plate stops growing at --maxw while the viewport does not, and vw units
     would keep inflating the copy past the corner that was cleared for it. */
  container-type: inline-size;
}
.privacy-plate picture { display: block; }
.privacy-plate img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  background: var(--er-white);
}


/* Phone: nothing is overlaid. At 375px the plate is 343px wide and a 38% column
   inside it would be 130px — narrower than the words. The copy drops below. */
.privacy-overlay {
  margin: 1.25rem auto 0;
  max-width: 32rem;
  text-align: center;
}
.privacy-overlay .d-h2 { margin: .25rem 0 0; }
.privacy-sub {
  margin: .75rem auto 0;
  max-width: 32rem;
  text-align: center;
  font-size: clamp(.9375rem, 1.2vw, 1.0625rem);
  line-height: 1.65;
  color: var(--er-gray-70);
}

/* Phone: the heading leads, the picture carries the middle, the caption closes.
   The same arrangement the speakers and lens blocks take further up — name and
   claim, then the media, then what qualifies it. Block flow puts the picture
   first, because that is the order the overlay layout needs above 64rem, so the
   plate becomes a flex column here and order says otherwise.

   Declared AFTER the two rules it overrides, not beside .privacy-plate where it
   reads better. Both carry `margin: … auto`, and on a flex item an auto cross
   margin beats align-items: stretch — so the heading shrink-wrapped to its text
   and sat 90px in. A media query adds no specificity, so from further up the
   file this simply lost. */
@media (max-width: 30rem) {
  /* Padding moves off the plate and onto the words so the picture can run the
     full width. Inset, it was 343 wide and 520 tall — 64% of the screen. Edge to
     edge it is 375 and 569, which is 70%. The copy keeps the gutter. */
  .privacy-plate {
    display: flex;
    flex-direction: column;
    padding-inline: 0;
  }
  .privacy-overlay {
    order: 1;
    margin: 0;
    max-width: none;
    padding-inline: var(--gutter);
  }
  .privacy-plate picture { order: 2; margin-top: 1.25rem; }
  /* Full bleed, so the rounded corners have nothing to sit against. */
  .privacy-plate img { border-radius: 0; }
  .privacy-sub {
    order: 3;
    margin: 1.25rem 0 0;
    max-width: none;
    padding-inline: var(--gutter);
  }}

/* Was 48rem, which put the copy on top of the plate long before the plate was
   wide enough to carry it: at 768px the 1.32cqw caption computed to 9.5px and
   was simply unreadable. The overlay layout only works once the plate can give
   that line about 13px, which is a plate of roughly 985px, so it now waits for
   64rem and reads as a centred block below the image until then. The max()
   floors underneath are a backstop, not the mechanism — cqw is still what makes
   the type track the plate rather than the window. */
@media (min-width: 64rem) {
  /* Upper left. That corner is cleared from 3.5% to 42% across and down to 44%,
     measured at 0 non-white pixels and 14.8:1 for the heading colour. */
  .privacy-overlay {
    position: absolute;
    left: calc(var(--gutter) + 3.5%);
    top: 6%;
    width: 37%;
    max-width: none;
    margin: 0;
    text-align: left;
  }
  .privacy-overlay .eyebrow { font-size: max(1rem, 2.15cqw); margin-bottom: .35em; }
  /* "The app" is the same rank of label as "Privacy" above — both are
     .eyebrow.eyebrow-strong introducing a section — but they did not match.
     Privacy sits inside .privacy-overlay and is re-sized from its CONTAINER by
     the rule directly above, so it renders 21.0px at 1024, 26.4 at 1280 and
     30.06 from 1440 up, while "The app" stayed on .eyebrow-strong's own clamp
     at 24.6 / 30.7 / 32 — up to 17% larger, which is enough to read as a
     different rank rather than the same one.

     This tracks the measured curve to within about 0.4px at every width. It is
     a vw clamp rather than the same cqw expression because .app-split is not a
     query container and making it one would change what the rest of that block
     is sized against.

     Scoped to 64rem for the same reason the rule above it is: below that the
     overlay rule does not apply, Privacy falls back to .eyebrow-strong, and the
     two already match. */
  .app-split .eyebrow-strong { font-size: clamp(.9375rem, 2.09vw, 1.879rem); }
  .privacy-overlay .d-h2 { font-size: max(1.75rem, 4.1cqw); }

  /* Diagonally opposite the heading, so the two blocks bracket the figure.
     The right side is only clear BELOW 78% of the height — above that the
     largest sphere is in the way and copy over it read 2.0:1. Anchored to the
     bottom, this block tops out around 86%. */
  .privacy-sub {
    position: absolute;
    right: calc(var(--gutter) + 3.5%);
    bottom: 4%;
    width: 34%;
    max-width: none;
    margin: 0;
    /* Anchored right, but set left: at three lines a ragged left edge makes the
       reader hunt for the start of each one. The block sits in the right corner;
       the words inside it still run off a straight edge. */
    text-align: left;
    font-size: max(.8125rem, 1.32cqw);
  }}

/* ---- What's included ----
   A grid of light plates on the page's own white, so the tiles read as the
   content and the gaps as the page showing through. The hero item spans two
   columns; every cell shares one row height, which is what makes five different
   objects sit as a single block. */
.kit-grid {
  list-style: none;
  margin: clamp(1rem, 2vw, 1.75rem) 0 0;
  padding: 0;
  display: grid;
  /* Six columns rather than three, so the hero can take two thirds and the rest
     a third each without fractional spans. */
  grid-template-columns: repeat(6, minmax(0, 1fr));
  /* Narrow, like the reference: the plates are the surface, the gap is a seam. */
  gap: clamp(.5rem, .85vw, .75rem);
  grid-auto-rows: clamp(11rem, 23vw, 19rem);
}
.kit-cell {
  position: relative;
  background: var(--er-gray-05);
  border-radius: 4px;
  overflow: hidden;
  /* The caption sits in the corner rather than under the picture, so the art
     gets the whole plate and every label lands on the same line. */
  padding: clamp(.875rem, 1.6vw, 1.25rem);
  display: grid;
  place-items: center;
}

.kit-cell.is-wide { grid-column: span 4; }
.kit-cell:not(.is-wide) { grid-column: span 2; }

.kit-art {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  /* Room for the label, so nothing sits on top of it. */
  padding-bottom: 1.5rem;
  min-height: 0;
}
.kit-art picture { display: contents; }
.kit-art source { display: none; }
/* Filled and contained, rather than sized by max-width with max-height as a
   guard. That guard did not hold: a percentage max-height needs a definite
   containing height to resolve against and quietly computed to none, so
   max-width alone decided the size. Wide shots looked fine by luck; the square
   cable overshot its box by 31px and ran 20px into the label. Filling a box that
   already has a definite size and letting object-fit letterbox cannot overflow.
   Breathing room comes from the art's own padding, per cell. */
.kit-art img {
  width: 100%;
  height: 100%;
  /* Grid items default to min-height:auto and will not shrink below their
     intrinsic size. At 261px wide the square cable's intrinsic height is also
     261, so that floor beat height:100% and pushed it 31px past its box and
     20px into the label. The wide shots never hit it — their intrinsic heights
     were already under the box. */
  min-width: 0;
  min-height: 0;
  object-fit: contain;
  display: block;
}
/* Each shot is framed differently in its own file, so the inset differs to make
   them read at the same weight: the glasses are cropped tight, the case and the
   cable carry their own margin. */
.kit-art { padding-inline: 14%; }
.kit-cell.is-wide .kit-art { padding-inline: 24%; }

/* The app tile, the way an icon actually appears on a phone. */
.kit-appicon {
  display: grid;
  place-items: center;
  width: clamp(3.75rem, 6.5vw, 5.25rem);
  aspect-ratio: 1;
  border-radius: 22%;
  background: var(--er-gray-90);
  color: var(--er-white);
}
.kit-appicon svg { width: 58%; height: auto; display: block; }

.kit-label {
  position: absolute;
  left: clamp(.875rem, 1.6vw, 1.25rem);
  bottom: clamp(.75rem, 1.4vw, 1rem);
  right: clamp(.875rem, 1.6vw, 1.25rem);
  font-size: clamp(.8125rem, 1vw, .9375rem);
  line-height: 1.35;
  color: var(--er-gray-70);
}

@media (max-width: 48rem) {
  /* Two columns on a phone, and the hero takes both — five thirds would put the
     glasses in a 100px box. */
  /* Two columns: the glasses take a row, then the remaining four pair off
     exactly, so nothing is left beside a hole.

     The selector mirrors the desktop one rather than being simplified. A plain
     .kit-cell here loses to .kit-cell:not(.is-wide) above — a media query
     changes when a rule applies, never how specific it is. */
  .kit-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: clamp(9rem, 34vw, 13rem);
  }
  .kit-cell:not(.is-wide) { grid-column: span 1; }
  .kit-cell.is-wide { grid-column: span 2; }}

/* ---- Detailed specifications ----
   Five columns of label/value pairs on hairlines. Monochrome rather than the
   reference's accent colour: this site has no accent except the HUD mint, which
   belongs to a projected display AURVIZ does not have. Weight separates the two
   sides instead — muted label, near-black value. */
.spec-cols {
  margin-top: clamp(1.75rem, 3.5vw, 3rem);
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  /* Tighter at six across than it was at five: the gap is the only slack left,
     and every pixel taken out of it goes into the columns, which is what keeps
     the values off a second line. */
  gap: clamp(1rem, 1.6vw, 1.5rem);
  align-items: start;
}
.spec-col { margin: 0; }
.spec-col-head {
  padding-bottom: .625rem;
  border-bottom: 1px solid var(--fg);
}
.spec-col-head h3 {
  margin: 0;
  font-size: clamp(.9375rem, 1.15vw, 1.0625rem);
  font-weight: 400;
  letter-spacing: -.012em;
  color: var(--er-gray-90);
}
/* Label above value, not beside it. Side by side worked at five columns; at six
   the column is 188px and the label was taking most of it — "Up to 48 hours" was
   wrapping inside a 74px box and the microphone row ran to three lines. Stacked,
   each side gets the full column and almost nothing wraps. */
.spec-col > div:not(.spec-col-head) {
  display: grid;
  gap: .125rem;
  padding: .625rem 0;
  border-bottom: 1px solid var(--rule);
}
/* A named capability with no value: the term takes the whole row rather than
   leaving an empty cell beside it. */
.spec-col > div.spec-feature { grid-template-columns: minmax(0, 1fr); }

.spec-col dt {
  font-size: clamp(.75rem, .92vw, .8125rem);
  line-height: 1.4;
  color: var(--er-gray-70);
}
.spec-col dd {
  margin: 0;
  font-size: clamp(.75rem, .92vw, .8125rem);
  line-height: 1.4;
  color: var(--er-gray-90);
}

@media (max-width: 64rem) {
  /* Five columns of small type is unreadable before it is narrow — at this width
     each would be under 150px and every value would wrap. */
  .spec-cols { grid-template-columns: repeat(3, minmax(0, 1fr)); }}
@media (max-width: 40rem) {
  .spec-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5rem 1rem; }}

/* ---- Memory graph ----
   Full-bleed: no container, no gutters, no radius. The section's own padding is
   cancelled below so it meets the edges of the viewport. */
.graph-block {
  margin-top: clamp(2.5rem, 6vw, 5rem);
  width: 100%;
}

.graph-panel {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 2;
  /* Cap at the viewport so the panel can fill the screen without ever pushing
     past it. The canvas simply re-lays out — nothing is cropped. */
  max-height: 100svh;
  min-height: 34rem;
  overflow: hidden;
  /* White, not --hud-bg. The HUD token is for surfaces that are meant to read as
     a device screen; this is a room the spheres float in. The canvas paints its
     own white underneath, so this only matters before the first frame lands. */
  background: var(--er-white);
}
.graph-canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block;
  touch-action: pan-y;          /* let the page still scroll over the canvas */
}
.graph-canvas.is-over { cursor: pointer; }

/* Gone. This sank the corners so the cluster read as lit from the centre, which
   is a black-room idea: on black a dark vignette touches only the background.
   Inverted onto white it lands on the spheres instead, because they are the
   light thing here, so all it did was bleach the outer ring of the field. The
   white panel already falls away to a white page with no help. */
.graph-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: none;
}

.graph-hint {
  display: flex;
  align-items: center;
  gap: .5rem;
  font-size: .6875rem;
  letter-spacing: .04em;
  color: rgba(255, 255, 255, .38);
  transition: opacity .4s var(--ease);
}
.graph-hint.is-hidden { opacity: 0; }
.hint-dot {
  width: 5px; height: 5px; border-radius: 50%;
  background: rgba(255, 255, 255, .45);
  animation: hintPulse 2.4s var(--ease) infinite;
}
@keyframes hintPulse {
  0%, 100% { opacity: .25; transform: scale(1); }
  50%      { opacity: .9;  transform: scale(1.5); }
}

/* Settles the right side so the overlay copy holds contrast over the nodes.
   Pulled back hard and started later. At .58 it was washing out every sphere in
   the right third of the field, and it was doing that to buy contrast the copy
   did not need: the text is near-black on a white panel, so it clears AA on the
   bare field already. What is left only calms the few spheres that drift
   directly under the words. */
.graph-fade {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(95deg,
              transparent 0%,
              transparent 66%,
              rgba(255, 255, 255, .12) 84%,
              rgba(255, 255, 255, .26) 100%);
}

.graph-ui {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(1rem, 2vw, 1.75rem);
  margin-left: auto;
  width: min(42%, 26rem);
  /* Now the panel is full-bleed, keep the text on the same vertical line as the
     rest of the page rather than pinned to the viewport edge. */
  padding-right: max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter)));
  box-sizing: content-box;
  color: var(--er-white);
}
.graph-ui .eyebrow { color: var(--er-gray-70); margin-bottom: .5rem; }
.graph-ui .d-h3 { color: var(--er-gray-90); }

/* The concept has to be the thing people leave with, so it carries the weight
   of a section heading rather than sitting in an eyebrow. */
.graph-title {
  font-size: clamp(2.25rem, 4vw, 3.5rem);
  font-weight: 300;
  line-height: .98;
  letter-spacing: -.048em;
  color: var(--er-gray-90);
}
.graph-kicker {
  /* No margin-top. The gap on .graph-ui is the one spacing rule for this
     column now; a margin here would put the kicker back on a rhythm of its
     own, which is the fault this replaced. */
  font-size: clamp(1.0625rem, 1.5vw, 1.375rem);
  font-weight: 300;
  line-height: 1.35;
  letter-spacing: -.025em;
  color: var(--er-gray-70);
}

.graph-lede {
  /* Up from clamp(.8125rem, 1vw, .9375rem). This line is the reason the
     section exists, and at 14.4px it was set smaller than the body copy
     everywhere else on the page. */
  font-size: clamp(.875rem, 1.1vw, 1rem);
  line-height: 1.65;
  color: var(--er-gray-70);
  max-width: 26em;
}

/* Entity names tracked to nodes. transform is set per frame from JS; keeping
   them as DOM text means they stay crisp at any zoom or DPR. */
.graph-labels { position: absolute; inset: 0; pointer-events: none; }
.graph-label {
  position: absolute;
  top: 0; left: 0;
  white-space: nowrap;
  transform: translate3d(-9999px, -9999px, 0);
  opacity: 0;
  transition: opacity .5s var(--ease);
}
/* will-change ONLY while a label is up. There are eighteen of these per field
   and the property is a standing instruction to the compositor to keep each one
   on its own layer for the whole life of the page - eighteen layers held ready
   for elements parked at -9999px with opacity 0, which on these fields is nearly
   always all of them: labelsOnFocusOnly means nothing appears until a bubble is
   clicked, and most visits never click one.

   The translate3d stays on the base rule and is doing the real work; it already
   keeps the transform off the paint path when a label IS moving. This just stops
   the browser being told to reserve for seventeen others that are not. */
.graph-label.is-focused { will-change: transform, opacity; }
/* Flipped labels sit to the left of their bubble, so they read right-aligned. */
.graph-label.is-flipped { text-align: right; }
.graph-label.is-flipped.is-focused {
  padding-left: 0;
  padding-right: .75rem;
  border-left: 0;
  border-right: 1px solid rgba(255, 255, 255, .3);
}
.graph-label b {
  display: block;
  font-size: clamp(.6875rem, .95vw, .8125rem);
  font-weight: 400;
  letter-spacing: -.01em;
  color: var(--er-gray-90);
  text-shadow: 0 1px 10px rgba(255,255,255,.95), 0 0 3px rgba(255,255,255,.9);
}
.graph-label span {
  display: block;
  font-size: clamp(.5625rem, .78vw, .6875rem);
  letter-spacing: .07em;
  text-transform: lowercase;
  color: var(--er-gray-70);
  text-shadow: 0 1px 8px rgba(255,255,255,.95), 0 0 3px rgba(255,255,255,.9);
}

/* The held bubble's label leads — larger, with a rule tying it to the sphere. */
.graph-label.is-focused {
  padding-left: .75rem;
  border-left: 1px solid rgba(35,35,35,.28);
}
/* Black once the camera is in, both lines of it. At rest these sit at #232323
   and a grey, which is right against the pale field; flown in, the label lands
   over a bubble filled edge to edge with a photograph, and near-black on a
   photograph is the first thing to disappear. The type line was the worse of
   the two — it was --er-gray-70. */
.graph-label.is-focused b {
  font-size: clamp(1rem, 1.7vw, 1.5rem);
  font-weight: 300;
  letter-spacing: -.03em;
  color: #000;
}
.graph-label.is-focused span {
  font-size: clamp(.625rem, .85vw, .75rem);
  color: rgba(0, 0, 0, .78);
  margin-top: .125rem;
}

/* The inspector card's rules went with its markup — .graph-spotlight,
   .spot-badge, .spot-name, .spot-link and .spot-from. Nothing else on the page
   used them, and --spot was set only by the code that filled that card. */



/* ---- The app ---- */

/* At .75rem in muted grey the section's own name was the faintest thing on the
   screen, so the section read as headline-only. It gets size and full contrast
   here — still an eyebrow above the h2, no longer a whisper. Tracking comes
   down from .14em because uppercase display type needs far less of it once it
   is this big. */
.eyebrow-strong {
  /* The weight below is honoured because Inter Tight ships 200 through 600 and
     the page loads all five. This used to be a trap: a second face was loaded
     at 500 and 600 only, so asking it for 400 got the nearest cut it had — the
     500 the rule was trying to get away from. One face, every weight, no
     surprise.

     300, down from 400, and that used to be the whole point of this rule: it
     was what made "The app" and "Glasses" read bold against the light headings
     under them. Asked for lighter, and lighter it is. The seven labels this
     covers are a set — AURVIZ o1, Bone conduction microphone, Open-ear
     speakers, Custom lenses, The app, Privacy, FAQ — so the weight moves for
     all of them or the set stops being one; "The app" was matched to "Privacy"
     deliberately a few commits ago and scoping this to one of them would undo
     that.

     What still separates a label from its heading, now the weight does not:
     it is uppercase, tracked at .05em against the headings' -.048em, and a
     different size. That is three signals against the one this gives up. */
  font-weight: 300;
  /* Floor kept low: the h2 bottoms out at 1.875rem on phones, so a 1.0625rem
     eyebrow left only 1.76x between them and the two started competing. The
     desktop ceiling can grow freely - it is only the phone end that collides. */
  font-size: clamp(.9375rem, 2.4vw, 2rem);
  letter-spacing: .05em;
  color: var(--er-gray-90);
  margin-bottom: .625rem;
}

.phone-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1.5rem, 3.5vw, 3.5rem);
  margin-top: clamp(2.5rem, 5vw, 4.5rem);
}
.phone { margin: 0; display: flex; flex-direction: column; }
.phone-frame {
  /* 9/16, down from 9/18 and 9/19 before that. The same rule applies each time
     and it is the reason this cannot be changed on its own: the pairing screen's
     CONTENT sets the height, and the ratio is only the resting shape for when
     that content fits inside it. At 9/18 the frame asked for 576 and measured
     586, so the content was already binding and lowering the ratio alone would
     have moved nothing at all.

     So it is lowered with two trims that pay for it, both on the pairing screen
     because that is the tallest of the three: the gallery tiles go from square
     to 16/10 and the paired-glasses image from 92% to 74%. That takes 80px out
     of a 570px screen, which puts the content at 490 and the frame at 506 -
     under the 512 the ratio now asks for, so the ratio binds again and all
     three handsets sit at exactly 512.

     ALL THREE OF THOSE ARE DESKTOP-ONLY and are undone below 64rem, where the
     handsets stop being a grid and become a swipe rail. The reasoning for
     shortening them does not survive that change - see the note beside the
     restore in the 64rem block. Anything done to the height here has to be
     considered there too, or the two will drift. */
  aspect-ratio: 9 / 17;
  /* The three handsets have to end up identical, and aspect-ratio alone did not
     do it. .phone-frame is a flex item in a column, so its automatic minimum
     size is its content's: the pairing screen needs 606px of content, which
     forced that one frame to 622 while the other two sat at the 608 the ratio
     asks for — a 14px difference, visible as one phone being taller.
     flex-grow lets every frame take the full height of its .phone, and the grid
     row already makes those equal, so the tallest content sets one height and
     all three adopt it. The ratio stays as the resting shape for when no screen
     overflows it. */
  flex-grow: 1;
  border-radius: 1.75rem;
  padding: .5rem;
  background: linear-gradient(160deg, #2c3238, #14181c);
  box-shadow: 0 30px 60px -35px rgba(16, 18, 20, .75);
}
/* All three screens are white. The handset body stays dark — it is the device,
   not the display. Everything below that used white ink was written when two of
   these were on --hud-bg; the pairing screen was already light, which is why so
   many .ps-* rules are in rgba(35,35,35) already. */
.phone-screen {
  height: 100%;
  border-radius: 1.375rem;
  background: var(--er-white);
  /* Measured before trimming: the tallest screen came to 606px as 464 of
     children, 72 of gaps, 40 of child margins and 30 of padding. The children
     are the content and are left alone; the other 142px is spacing, which is
     where the height was taken from. Nine gaps at 8px was the single biggest
     line in that list. */
  padding: .6875rem .8125rem .75rem;
  display: flex;
  flex-direction: column;
  gap: .375rem;
  overflow: hidden;
  color: var(--er-gray-90);
}
.phone-screen .ps-bar { color: rgba(35, 35, 35, .5); }
.phone-screen .ps-dots { background: rgba(35, 35, 35, .2); }
/* .phone figcaption is gone — the three screens carry their own labels, so the
   captions underneath were repeating them. */

/* ---- Fit the app section to one screen ----
   Copy in a narrow left column, screens top-right. Stacked, this could not be
   done: the heading, lede and padding cost 445px on their own, and the phones
   are 9:19, so fitting an 800px viewport meant zooming them to 44% — at which
   point their content stopped being readable, which is the whole point of
   showing them. Beside the copy they get the section's full height instead of
   what is left beneath the heading, and no zoom is needed at all.

   Columns are 32/68: 32% is the narrowest the left column can be before
   "you can reach it." wraps to a third line at the heading size below. */
@media (min-width: 64rem) {
  .section-app { padding-block: clamp(1.5rem, 2.6vw, 2.5rem); }
  .app-split {
    display: grid;
    grid-template-columns: minmax(0, 32fr) minmax(0, 68fr);
    gap: clamp(1.25rem, 2.5vw, 2.5rem);
    align-items: start;              /* screens sit at the top, not centred */
  }
  /* Sized for the narrow column rather than the full width. */
  .section-app .d-h2 { font-size: clamp(1.5rem, 2.6vw, 2.75rem); }
  .phone-row {
    margin-top: 0;
    gap: clamp(.875rem, 1.8vw, 1.75rem);
  }}

/* The pairing screen is light, so the device sits on white the way it will in
   the app — and it separates the connection step from the memory screens. */
/* .is-light is gone: every screen is light now, so it said nothing. Its three
   declarations moved onto .phone-screen itself. */

.ps-pair-hero {
  display: grid;
  place-items: center;
  padding: .25rem 0 .125rem;
}
/* <picture> is the grid item, so place-items centres IT, not the image inside —
   which then sat left-aligned. Centre the img on its own. */
.ps-pair-hero picture { display: block; width: 100%; }
.ps-pair-hero img {
  display: block;
  /* 74%, from 92%. The file is 750x332, so every point of width costs 0.44 of
     height: 92% of a 246px screen stood 100px tall, 74% stands 81. The glasses
     are a cut-out on transparency and the screen is 246px wide, so at 74% they
     are still 182px across - wider than the paired-device name under them and
     read as the hero of the screen rather than as a thumbnail. */
  width: 84%;
  height: auto;
  margin-inline: auto;
  /* The cut-out already carries its own alpha, so no blend mode is needed —
     multiply would only muddy the frame against the light screen. */
  filter: drop-shadow(0 10px 14px rgba(20, 24, 28, .22));
}
.ps-pair-name {
  text-align: center;
  font-size: 1rem;
  font-weight: 400;
  letter-spacing: -.03em;
}
.ps-pair-state {
  display: flex; align-items: center; justify-content: center; gap: .3125rem;
  font-size: .625rem;
  /* Was .55, which measured 3.64:1 — already failing on the old light screen,
     not something this change introduced. Fixed while the theme was being set. */
  color: rgba(35, 35, 35, .64);
  margin-top: .125rem;
}
.ps-pair-state i {
  width: 5px; height: 5px; border-radius: 50%;
  background: #2fbf71;
  box-shadow: 0 0 6px rgba(47, 191, 113, .8);
}
/* Battery sits directly under the connection state — the only device stat that
   earns a place on the first screen. */
.ps-battery {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .375rem;
  font-size: .625rem;
  color: rgba(35, 35, 35, .6);
  margin-top: .25rem;
}
.ps-batt-icon {
  position: relative;
  width: 18px; height: 9px;
  border: 1px solid rgba(35, 35, 35, .35);
  border-radius: 2.5px;
  padding: 1px;
}
.ps-batt-icon::after {
  content: "";
  position: absolute; right: -3px; top: 50%;
  transform: translateY(-50%);
  width: 1.5px; height: 4px;
  background: rgba(35, 35, 35, .35);
  border-radius: 0 1px 1px 0;
}
.ps-batt-icon i {
  display: block; height: 100%;
  background: #2fbf71;
  border-radius: 1.5px;
}

.ps-sec-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  /* 10 + 8 was 18px of clear air above a hairline that already separates this
     from what is above it. Halved; the rule is doing the work. */
  margin-top: .375rem;
  padding-top: .25rem;
  border-top: 1px solid rgba(35, 35, 35, .09);
  font-size: .5rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: rgba(35, 35, 35, .45);
}
.ps-sec-count { letter-spacing: 0; color: rgba(35, 35, 35, .32); }

/* Thumbnails as abstract tiles rather than stock photography — a gallery of
   invented "captures" would be pretending to show something that isn't real. */
.ps-thumbs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 3px;
  margin-top: .375rem;
}
.ps-thumbs i {
  position: relative;
  display: block;
  /* 16/10, not square. Two rows of squares in a 240px-wide grid is 163px of a
     570px screen - the single largest block on it, and the one the shorter
     frame is paid for out of. At 16/10 the same six tiles come to 103.

     They are object-fit: cover, so nothing is scaled or letterboxed; the crop
     just moves. A landscape thumbnail is also what a phone gallery actually
     shows for video, and two of these six are marked as video. */
  aspect-ratio: 4 / 3;
  border-radius: 4px;
  background: var(--er-gray-20);
  overflow: hidden;
}
.ps-thumbs i img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* a small play marker distinguishes clips from stills */
.ps-thumbs i.vid::after {
  content: "";
  position: absolute;
  right: 3px; bottom: 3px;
  width: 0; height: 0;
  border-left: 5px solid rgba(255, 255, 255, .85);
  border-top: 3px solid transparent;
  border-bottom: 3px solid transparent;
}

.ps-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .3125rem;
  margin-top: .4375rem;
  padding: .4375rem .625rem;
  border: 1px solid rgba(35, 35, 35, .18);
  border-radius: 8px;
  font-size: .5625rem;
  letter-spacing: -.005em;
  color: rgba(35, 35, 35, .82);
}
.ps-btn svg {
  width: 10px; height: 10px;
  fill: none; stroke: currentColor;
  stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round;
}
/* The memory-bubbles button navigates, so it reads as a row with an affordance
   on the right rather than a centred action. */
.ps-btn-nav { justify-content: space-between; }
.ps-btn-l { display: flex; align-items: center; gap: .375rem; }
.ps-btn-nav em { font-style: normal; font-size: .75rem; color: rgba(35, 35, 35, .35); }

/* Entry point to the chat screen, pinned to the bottom of the phone. */
.ps-chat-fab {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .375rem;
  margin-top: auto;
  padding: .5rem;
  border-radius: 999px;
  background: var(--er-gray-90);
  color: var(--er-white);
  font-size: .625rem;
  letter-spacing: -.01em;
}
.ps-chat-fab svg {
  width: 12px; height: 12px;
  fill: none; stroke: currentColor;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
}

.ps-search-mini {
  display: flex;
  align-items: center;
  gap: .375rem;
  font-size: .5625rem;
  /* Glass too, so the memory screen and the chat screen are made of the same
     thing. It was a flat .05 grey fill with no rim; the rim is what the material
     is, and without one this pill read as a different system to the bubbles two
     screens along. */
  color: rgba(35, 35, 35, .64);
  background: var(--glass-paper-fill);
  border: var(--glass-paper-rim);
  box-shadow: var(--glass-paper-lift);
  border-radius: 999px;
  padding: .375rem .5625rem;
  margin-bottom: .25rem;
}
.ps-search-mini svg {
  width: 9px; height: 9px;
  fill: none; stroke: currentColor;
  stroke-width: 1.5; stroke-linecap: round;
}

/* The field takes the whole screen: it is the point of this view.
   overflow:hidden is load-bearing — while the camera is zoomed in, tracked
   labels sit far outside the frame and would otherwise stretch the screen. */
.ps-graph-wrap { position: relative; flex: 1; min-height: 0; overflow: hidden; }
/* Same canvas element as the full-size field, just smaller. */
.ps-graph-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* Labels come from the shared renderer, so they arrive at full-panel scale.
   Inside a ~300px screen that is enormous, so they are pinned small — but 8px
   and 6px were past the point of being readable at any viewport, and the phone
   is a fixed ~270-290px at every width, so nothing was going to rescue them
   later. Raised to 9px and 7.5px, the smallest that still resolve on a normal
   display, which the panel has room for. */
/* Only the sizes are overridden now. The halo behind these was black because
   this screen used to be dark; on white it would have read as a smudge under
   every label. .graph-label's own white halo is correct here, so it is left to
   apply — the base rule is written for the memory section, which is also on
   white. */
.ps-labels { position: absolute; inset: 0; pointer-events: none; }
.ps-labels .graph-label b {
  font-size: 9px;
  letter-spacing: -.01em;
}
.ps-labels .graph-label span {
  font-size: 7.5px;
  letter-spacing: .04em;
}

.ps-btn-nav[aria-pressed="true"] {
  background: var(--er-gray-90);
  border-color: var(--er-gray-90);
  color: var(--er-white);
}
.ps-btn-nav[aria-pressed="true"] em { color: rgba(255, 255, 255, .5); }


.ps-graph-foot {
  display: flex;
  justify-content: space-between;
  font-size: .5rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  /* .64, not the .4 that carried over from the dark version. Ink at alpha a over
     white composites to 255 - 220a, and AA at 4.5:1 needs that at or below 118.7
     — so a >= .62. Same arithmetic sets the three labels below. */
  color: rgba(35, 35, 35, .64);
  padding-top: .375rem;
  border-top: 1px solid rgba(35, 35, 35, .1);
}

.ps-chat-top {
  font-style: normal; font-weight: 500;
  font-size: .5625rem; letter-spacing: .14em; text-transform: uppercase;
  color: rgba(35, 35, 35, .64);
  padding-bottom: .4375rem;
  border-bottom: 1px solid rgba(35, 35, 35, .1);
}
.ps-chat { display: flex; flex-direction: column; gap: .3125rem; margin-top: .375rem; }
.ps-chat .msg {
  max-width: 86%;
  font-size: .625rem;
  line-height: 1.45;
  padding: .375rem .5rem;
  border-radius: 9px;
}
/* FLAT, and that is the whole point of this block. Every other panel on this
   page is the site's glass; a phone's messaging screen is the one surface that
   must not be, because no real one is. Solid fills, no rim, no inset highlight,
   no cast shadow - the shapes carry it, the way they do in the app anyone
   reading this has open on their own phone.

   It follows the convention every messaging app shares rather than inventing
   one: the person speaking is the filled bubble on the right, the other side is
   grey on the left. That is the reverse of what was here, where AIRA had the
   colour and "you" had the pale glass, and the reversal is the reason this now
   reads as a chat rather than as two labelled panels.

   The 3px tail corner stays. It is the one detail that says "message" before a
   word is read, and it is the same proportion at 10px type that --glass-radius
   is at 16px. */
.ps-chat .msg.you {
  align-self: flex-end;
  background: var(--er-gray-90);
  color: var(--er-white);
  border-bottom-right-radius: 3px;
}
/* AIRA's side: the grey every incoming bubble is. It keeps NO green - the tint
   and the green ink both went with the glass, because a coloured incoming
   bubble is the tell that this is a mockup rather than a screen.

   #ededed against #232323 measures 15.4:1. The old green on white measured 6.5,
   which passed and was the right call while the bubble was pale; there is no
   reason to spend nine points of contrast on a hue now that the side of the
   screen and the fill are both saying who is speaking. */
.ps-chat .msg.aira {
  align-self: flex-start;
  background: #ededed;
  color: var(--er-gray-90);
  border-bottom-left-radius: 3px;
}
.ps-chat .msg.aira span {
  display: block;
  font-size: .5rem;
  /* .68, not the .5 it was. The bubble under it used to be a pale green tint
     and is #ededed now, which is darker, so the same ink lost contrast: .5
     measures 3.1:1 and .58 measures 3.78:1, both under the 4.5 that 8px type
     owes. Solving it rather than guessing at it - the composite is a*35 +
     (1-a)*237 and it has to land at or under 108 - gives a >= 0.639. .68
     measures 5.08:1. */
  color: rgba(35, 35, 35, .68);
  margin-top: .125rem;
}
/* Agent work shown as tasks, not prose — it reads as things happening. */
/* NOT bubbles. These are the agent's work, not something anybody said, so they
   carry no fill and no rim; a third bubble style would have made the screen read
   as three speakers.

   LEFT, not centred. Centred is where a real messaging screen puts the things
   that belong to nobody - date separators, "joined the call", delivery states -
   and that was the reasoning for putting them there. It is the wrong read here.
   These lines are AIRA's: they are what it did after saying "On it.", and they
   answer the message directly above them. Sharing that edge is what says so.
   Down the middle they floated free of both speakers and looked like system
   notices about the conversation rather than a part of it. */
.ps-chat .task {
  display: flex; align-items: center; gap: .375rem;
  font-size: .5625rem;
  color: rgba(35, 35, 35, .64);
  align-self: flex-start;
  /* The bubbles above carry .5rem of side padding, so an unboxed line at 0
     would start 8px left of AIRA's text and the column would step outward at
     the exact point the eye is following it down. This puts the ink on the same
     line as the ink above it rather than the box on the same line as the box. */
  padding: .0625rem .5rem;
}
.ps-chat .task .t-ico {
  position: relative;
  flex: none;
  width: .5rem; height: .5rem;
  font-style: normal;
}
.ps-chat .task .t-ico em {
  position: absolute;
  inset: 0;
  font-style: normal;
  font-size: .5rem;
  line-height: .5rem;
}
.ps-chat .task .t-ico .w { color: rgba(35, 35, 35, .35); }
.ps-chat .task .t-ico .d { color: #0b6b45; opacity: 0; }
/* Resting state: the first two have already completed. */
.ps-chat .task.done { color: rgba(35, 35, 35, .8); }
.ps-chat .task.done .t-ico .w { opacity: 0; }
.ps-chat .task.done .t-ico .d { opacity: 1; }

/* ---- replay, driven by the Ask AIRA button on the pairing screen ----
   `backwards` fill holds each line at opacity 0 through its delay, so the
   sequence can start from a screen that is otherwise fully rendered. */
.ps-chat.is-playing .msg,
.ps-chat.is-playing .task {
  animation: chatLineIn .42s var(--ease-out) backwards calc(var(--i) * .52s);
}
@keyframes chatLineIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* Tasks land as pending, then tick over a beat later — the work completing is
   the point, so it should be visible rather than pre-resolved. */
.ps-chat.is-playing .task.done .t-ico .w {
  animation: tickOut .3s ease forwards calc(var(--i) * .52s + .75s);
}
.ps-chat.is-playing .task.done .t-ico .d {
  animation: tickIn .3s ease backwards calc(var(--i) * .52s + .75s);
}
@keyframes tickOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes tickIn  { from { opacity: 0; } to { opacity: 1; } }

.ps-chat-fab[aria-pressed="true"] { background: #000; }

@media (prefers-reduced-motion: reduce) {
  .ps-chat.is-playing .msg,
  .ps-chat.is-playing .task,
  .ps-chat.is-playing .task.done .t-ico em { animation: none; }}

.ps-bar {
  display: flex; align-items: center; justify-content: space-between;
  font-size: .5625rem; color: rgba(255, 255, 255, .5); margin-bottom: .25rem;
}
.ps-dots { width: 22px; height: 4px; border-radius: 2px; background: rgba(255,255,255,.22); }
.ps-title { font-size: 1.375rem; font-weight: 300; letter-spacing: -.04em; }
.ps-title.sm { font-size: 1.125rem; }
.ps-sub { font-size: .625rem; color: rgba(255, 255, 255, .42); margin-top: -.1875rem; }

.ps-list { display: flex; flex-direction: column; gap: .5rem; margin-top: .375rem; }
.ps-list li {
  display: grid;
  grid-template-columns: 2.25rem 1fr;
  gap: .125rem .5rem;
  padding-bottom: .5rem;
  border-bottom: 1px solid rgba(255, 255, 255, .07);
}
.ps-list b { font-size: .625rem; font-weight: 400; color: var(--hud); }
.ps-list span { font-size: .75rem; letter-spacing: -.01em; }
.ps-list em {
  grid-column: 2; font-style: normal; font-size: .5625rem;
  color: rgba(255, 255, 255, .38);
}
.ps-list li.dim b { color: rgba(255, 255, 255, .3); }
.ps-list li.dim span { color: rgba(255, 255, 255, .35); }

.ps-search {
  font-size: .6875rem;
  color: rgba(255, 255, 255, .82);
  background: rgba(255, 255, 255, .07);
  border-radius: 999px;
  padding: .4375rem .6875rem;
}
.ps-answer {
  font-size: 1.5rem; font-weight: 300; letter-spacing: -.04em;
  color: var(--hud); margin-top: .375rem;
}
.ps-source {
  display: flex; align-items: center; gap: .375rem;
  font-size: .5625rem; color: rgba(255, 255, 255, .5);
}
.ps-chip {
  font-style: normal;
  font-size: .5rem; letter-spacing: .1em; text-transform: uppercase;
  padding: .125rem .375rem; border-radius: 999px;
  background: rgba(255, 255, 255, .1); color: rgba(255, 255, 255, .7);
}
.ps-quote {
  font-size: .625rem; line-height: 1.5; color: rgba(255, 255, 255, .45);
  border-left: 1px solid rgba(255, 255, 255, .18);
  padding-left: .5rem; margin-top: .375rem;
}

.ps-badge {
  align-self: flex-start;
  font-style: normal;
  font-size: .5rem; letter-spacing: .12em; text-transform: uppercase;
  padding: .1875rem .4375rem; border-radius: 999px;
  background: #e8756b; color: #12161a;
}
.ps-label {
  font-size: .5rem; letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255, 255, 255, .35); margin-top: .5rem;
}
.ps-links { display: flex; flex-direction: column; gap: .4375rem; }
.ps-links li {
  display: grid; grid-template-columns: 8px 1fr; gap: .125rem .4375rem;
  align-items: center; font-size: .6875rem;
}
.ps-links i {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--c); box-shadow: 0 0 7px -1px var(--c);
}
.ps-links em {
  grid-column: 2; font-style: normal; font-size: .5625rem;
  color: rgba(255, 255, 255, .38);
}
.ps-actions {
  display: flex; gap: .375rem; margin-top: auto;
}
.ps-actions span {
  flex: 1; text-align: center;
  font-size: .5625rem; padding: .375rem 0;
  border: 1px solid rgba(255, 255, 255, .16); border-radius: 6px;
  color: rgba(255, 255, 255, .6);
}

/* ---- Company ----
   The section moved to /company (company.html) and took its rules with it:
   .section-company, .company-grid, .company-copy and .company-story are gone,
   and so are .spec-list / .spec, whose only use on the site was the three
   founding facts in it. The letter page styles those as .letter-facts in
   section 21 instead.

   Nothing else used .spec-list. The glasses spec table is .spec-pair,
   .spec-cols, .spec-col-head and friends, which are untouched. */

/* ---- FAQ ---- */
/* The gap below the head used to hang off .d-h2, which this section no longer
   has — it now belongs to the label that replaced it. Sized above the shared
   strong tier too: everywhere else that class labels a heading, here it IS the
   heading, so 31px read as a caption with nothing under it. */
.section-faq .eyebrow {
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
  /* The same clamp .d-h2 uses, because that is what this is — the section's
     heading, not a label above one. It now matches "Why we are building this."
     and every other section heading rather than sitting a tier below them. */
  font-size: clamp(1.875rem, 4.4vw, 4rem);
  letter-spacing: -.048em;
  line-height: 1.07;
}
.faq-list {
  max-width: 52rem;
  margin-inline: auto;
}
/* Rules live on the items, not the padded container — a border on .faq-list
   spans its gutters and overhangs every item rule by one gutter each side. */
.faq-item { border-bottom: 1px solid var(--rule); }
.faq-item:first-of-type { border-top: 1px solid var(--rule); }

.faq-item summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: 1.25rem;
  padding: clamp(1rem, 2vw, 1.375rem) 0;
  font-size: clamp(1rem, 1.5vw, 1.25rem);
  font-weight: 300;
  letter-spacing: -.03em;
  line-height: 1.35;
  transition: color .25s var(--ease);
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary:hover { color: var(--fg-muted); }
.faq-item summary:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }

/* Plus that rotates into a minus when open */
.faq-item summary::after {
  content: "";
  flex: none;
  width: 11px; height: 11px;
  margin-left: auto;
  align-self: center;
  background:
    linear-gradient(var(--fg), var(--fg)) center/100% 1px no-repeat,
    linear-gradient(var(--fg), var(--fg)) center/1px 100% no-repeat;
  transition: transform .3s var(--ease);
}
/* Open state drops the vertical stroke, leaving a minus, and spins to it. */
.faq-item[open] summary::after {
  background: linear-gradient(var(--fg), var(--fg)) center/100% 1px no-repeat;
  transform: rotate(180deg);
}

.faq-answer { padding: 0 3rem clamp(1.25rem, 2.5vw, 1.75rem) 0; }
.faq-answer p {
  font-size: clamp(.9375rem, 1.05vw, 1.0625rem);
  line-height: 1.7;
  color: var(--er-gray-70);
  max-width: 44em;
}

/* Anything still carrying invented content is visibly flagged in-page, so it
   cannot be shipped by accident. Delete the class once real content lands. */
.is-placeholder { position: relative; }
.is-placeholder::before {
  content: "PLACEHOLDER";
  position: absolute;
  top: .5rem; right: .5rem;
  z-index: 2;
  font-style: normal;
  font-size: .5625rem;
  letter-spacing: .12em;
  padding: .1875rem .4375rem;
  border-radius: 2px;
  background: #ffe8a3;
  color: #6b4e00;
  pointer-events: none;
}

/* ---- Waitlist ---- */
.section-waitlist { background: var(--er-gray-90); color: var(--er-white); }
.section-waitlist .lede { color: rgba(255, 255, 255, .7); }
.waitlist-inner { display: flex; flex-direction: column; align-items: flex-start; gap: 1.25rem; }

/* The homepage preorder section is a heading, a line and a door. The email
   field and its whole form are gone — everything they collected now happens on
   /preorder, where the card is actually charged.

   .btn-preorder rather than the old `.waitlist-form .btn-solid`: that selector
   described the button's PARENT, and the parent no longer exists. Naming the
   thing itself means the next person to move it does not silently lose its
   colours the way this nearly did. */
.btn-preorder {
  margin-top: .75rem;
  background: var(--er-white);
  color: var(--er-gray-90);
  border-color: var(--er-white);
}
.btn-preorder:hover { background: var(--er-gray-10); border-color: var(--er-gray-10); }

/* Still used by /preorder, which restates the colours for the light ground it
   sits on — see .preorder-page .form-status. The values here are the dark-
   section ones and are now only a fallback. */
.form-status { font-size: .8125rem; min-height: 1.25rem; color: rgba(255, 255, 255, .75); }
.form-status.is-error { color: #ffb4a8; }
.form-status.is-ok { color: #b8f5cf; }

/* The panel that replaces the whole checkout form once the money is in and the
   signature has been verified. It REPLACES rather than sits beside: a filled-in
   form left on screen after a successful payment is an invitation to send a
   second one. */
.order-done { display: flex; flex-direction: column; gap: 1rem; }
.order-done .policy-title { margin: 0 0 .25rem; }
.order-done p {
  margin: 0;
  font-size: .875rem;
  font-weight: 300;
  line-height: 1.78;
  color: #4b4941;
}
.order-done a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.order-ref {
  font-size: .75rem !important;
  color: #6f6b60 !important;
  /* The one string somebody will read back over a phone or paste into an
     email, so it is set in a face where 0 and O differ. */
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: -.02em;
}

/* ============================================================
   PREORDER PAGE — /preorder
   ============================================================
   The checkout. It sits on .page-letter's paper ground and reuses .policy as
   its card, because the reader arrives here from a button and leaves through
   /terms, and a third visual language between those two would be one too many.

   The .price-* rules used to live on the dark homepage section. That block is
   gone and these render only here now, so they were repointed at the light
   ground rather than forked into two variants — one set of price styles, on one
   background, is one fewer thing to keep in step.

   ── The order the page is in ──
   Money first, then fields. Somebody who followed a button labelled "Preorder
   now" has not been told the price on THIS page yet, and asking for a home
   address before naming the figure is the wrong way round. */
.preorder-page { display: block; }

.order-summary {
  margin: 0 0 clamp(1.75rem, 3.5vw, 2.5rem);
  padding: clamp(1.1rem, 2.4vw, 1.5rem);
  background: #f2efe4;
  border-radius: 10px;
}

.order-line {
  margin: 0 0 1rem;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .35rem .7rem;
  letter-spacing: -.03em;
}
.price-was {
  font-size: clamp(1rem, 2vw, 1.1875rem);
  font-weight: 300;
  /* #6f6b60, measured at 4.62:1 on this panel. It was #8a8578 with a comment
     claiming 4.6:1 — that figure was asserted, not measured, and the real
     number was 3.2. At this size and weight it is not large text by WCAG, so it
     owes 4.5, and it is the figure the whole "save ₹5,000" claim is measured
     against: a reference price nobody can read is not a reference. */
  color: #6f6b60;
  text-decoration-thickness: 1.5px;
}
.price-now {
  font-size: clamp(1.625rem, 3.6vw, 2.25rem);
  font-weight: 300;
  line-height: 1;
  letter-spacing: -.048em;
  color: var(--er-gray-90);
}
.price-save {
  font-size: .75rem;
  font-weight: 500;
  letter-spacing: -.01em;
  color: #14432a;
  background: #cdf3dd;
  padding: .28rem .55rem;
  border-radius: 999px;
  white-space: nowrap;
}

.order-split {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(8rem, auto) 1fr;
  gap: .35rem 1.25rem;
  font-size: .8125rem;
  font-weight: 300;
  line-height: 1.5;
  color: #4b4941;
}
.order-split dt { color: #6f6b60; }
.order-split dd { margin: 0; }
.order-split strong { font-weight: 500; color: var(--er-gray-90); }

/* The figure actually leaving the account today, given the most weight of the
   three rows. Everything else on this panel is context for it. */
.pay-now { font-size: 1.0625rem; }

/* Ruled off, because it is a sum of the rows above rather than another one of
   them. Without this line the reader has to add 4,999 and 20,000 themselves to
   find out what a preorder costs, and a price the buyer computes is a price
   they can get wrong. */
.order-split .is-total {
  border-top: 1px solid #ddd8c9;
  padding-top: .45rem;
  margin-top: .2rem;
}
.order-split dt.is-total { color: var(--er-gray-90); font-weight: 400; }

/* The one condition attached to the struck-through price. It sits under the
   price line rather than inside it: "Save ₹5,000 · 17% off · available only
   before general release" is a badge that has become a sentence. */
.order-note {
  margin: .5rem 0 0;
  font-size: .75rem;
  font-weight: 300;
  line-height: 1.55;
  color: #6f6b60;
}

/* The four things that answer "what is the catch". Set as a row of separated
   phrases rather than a list, because a bulleted list here reads as terms and
   conditions and these are the opposite: they exist to remove hesitation. */
.order-assure {
  margin: 1.1rem 0 0;
  padding-top: .9rem;
  border-top: 1px solid #ddd8c9;
  display: flex;
  flex-wrap: wrap;
  gap: .3rem .75rem;
  font-size: .75rem;
  font-weight: 400;
  letter-spacing: -.01em;
  color: #4b4941;
}

/* The separator belongs to the phrase BEFORE it, as an ::after inside that
   span. It started as an absolutely-positioned ::before on the following span,
   which looks identical until the row wraps — then the dot travels to the new
   line with the span it belongs to and the second line opens with a floating
   interpunct. Trailing it instead means a wrap can only ever break after a dot,
   which is where a break belongs.

   Spacing is .75rem either side rather than 1rem: at 1rem the four phrases
   needed 573px in the 552px the card gives them, and three separators were
   108px of that. Tightening them and shortening "One-year" to "1-year" brings
   it to 532px, which fits on one line with 20px to spare. */
.order-assure span:not(:last-child)::after {
  content: "·";
  margin-left: .75rem;
  color: #b3ac99;
}

/* ── The form ──
   No heading above it any more. The labels say what each field is, and a page
   whose entire purpose is one form does not need a heading announcing that the
   form is a form. The summary panel's bottom margin carries the gap. */

/* Two columns where two fit, one where they do not. City and PIN belong side
   by side because that is how an address is written; name and address get the
   full width because a truncated one is a returned parcel. */
.field-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.1rem 1rem;
}
.field-wide { grid-column: 1 / -1; }

.field { margin: 0; display: flex; flex-direction: column; gap: .35rem; }
.field label {
  font-size: .8125rem;
  font-weight: 400;
  letter-spacing: -.01em;
  color: var(--er-gray-90);
}
.field .opt { font-weight: 300; color: #8a8578; }

.field input[type="text"],
.field input[type="email"],
.field input[type="tel"],
.field select,
.field textarea {
  font: inherit;
  /* 16px, and it has to STAY at or above 16px. iOS Safari zooms the whole page
     in when a field smaller than that takes focus, and does not zoom back out
     — the page is left scaled up and pannable sideways with the header and the
     left edge cut off. It reads as a broken layout rather than as a zoom, which
     is exactly how it was reported the last time it happened on this site: see
     the same note on the field this form replaced.

     This was .875rem. Fourteen pixels looks better beside 13px labels and would
     have shipped a checkout that visibly breaks on an iPhone the moment anyone
     tapped the first field. */
  font-size: 1rem;
  font-weight: 300;
  color: var(--er-gray-90);
  background: #fffefb;
  border: 1px solid #ddd8c9;
  border-radius: 8px;
  padding: .6rem .7rem;
  width: 100%;
  /* Without this the padding is added OUTSIDE the 100% and every field
     overflows its column by 1.4rem. */
  box-sizing: border-box;
}
.field textarea { resize: vertical; min-height: 3.5rem; }
.field select { appearance: none; background-image: none; }

.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  outline: 2px solid var(--er-gray-90);
  outline-offset: 1px;
  border-color: transparent;
}

/* Empty until it has something to say, and taking no space while empty —
   otherwise every field carries a permanent blank line under it and the form
   is a third taller for nothing. */
.field-error {
  font-size: .75rem;
  font-weight: 400;
  color: #a3341f;
}
.field-error:empty { display: none; }

.field.is-invalid input,
.field.is-invalid select,
.field.is-invalid textarea {
  border-color: #c2472e;
  /* Colour alone does not carry an error state — about one man in twelve would
     not see it. The thicker rule does. */
  border-width: 2px;
  padding: calc(.6rem - 1px) calc(.7rem - 1px);
}

.field-check {
  margin: 1.5rem 0 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .1rem .65rem;
  align-items: start;
}
.field-check input { margin-top: .2rem; width: 1rem; height: 1rem; accent-color: var(--er-gray-90); }
.field-check label {
  font-size: .8125rem;
  font-weight: 300;
  line-height: 1.6;
  color: #4b4941;
}
.field-check a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.field-check .field-error { grid-column: 2; }

/* The margin above is declared on .field-check and never arrived: `.policy-body
   p` is (0,1,1) against its (0,1,0) and sets margin to 0 0 1.15rem, so the
   tickbox has been sitting flush against the last field this whole time. Two
   classes outrank it. Ticking a box is a different act from filling a field in
   and should not look like the next row of the form. */
.policy-body .field-check { margin: 1.5rem 0 0; }

.order-actions {
  margin-top: clamp(1.5rem, 3vw, 2rem);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .75rem;
}
.order-actions .btn-solid {
  background: var(--er-gray-90);
  color: var(--er-white);
  border-color: var(--er-gray-90);
}
.order-actions .btn-solid:hover { background: #000; }
.order-actions .btn-solid[disabled] { opacity: .55; cursor: default; }

/* The shared .form-status is written for the dark homepage section it no longer
   appears in; on cream those colours are unreadable, so this page restates
   them. Measured on #fbfaf5: 7.4:1, 5.1:1 and 4.9:1. */
.preorder-page .form-status { color: #4b4941; font-size: .8125rem; }
.preorder-page .form-status.is-error { color: #a3341f; }
.preorder-page .form-status.is-ok { color: #1c6b40; }

.order-foot {
  margin: clamp(1.5rem, 3vw, 2rem) 0 0;
  padding-top: 1.1rem;
  border-top: 1px solid var(--rule);
  font-size: .75rem;
  font-weight: 300;
  line-height: 1.6;
  color: #6f6b60;
}

@media (max-width: 34rem) {
  /* One column. At this width a two-column grid leaves "PIN code" wrapping its
     own label. */
  .field-grid { grid-template-columns: 1fr; }
  .order-split { grid-template-columns: 1fr; gap: 0; }
  /* One per line, so the separators go: a dot trailing each item in a stacked
     list is a full stop that is not one. */
  .order-assure { flex-direction: column; gap: .3rem; }
  .order-assure span:not(:last-child)::after { content: none; }
  /* Only the label carries the rule once the pair stacks. Both did, which drew
     a second line BETWEEN "Total" and the figure — a divider in the one place
     the eye needs the two joined. */
  .order-split dd.is-total { border-top: 0; padding-top: 0; margin-top: 0; }
  .order-split dt { margin-top: .5rem; color: #6f6b60; }
  .order-split dt:first-child { margin-top: 0; }
  .order-actions .btn-solid { width: 100%; text-align: center; }}

/* ---- Design frames (now figures, not buttons) ---- */

/* ---- Footer additions ---- */
.footer-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.footer-tagline {
  font-size: .875rem;
  color: var(--er-gray-50);
  margin-top: .75rem;
  letter-spacing: -.01em;
}

/* ---------- 17. Reveal-on-scroll ---------- */
.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .85s var(--ease-out), transform .85s var(--ease-out);
}
.reveal.is-in { opacity: 1; transform: none; }
.reveal[data-delay="1"] { transition-delay: .1s; }
.reveal[data-delay="2"] { transition-delay: .2s; }
.reveal[data-delay="3"] { transition-delay: .3s; }

/* ---------- 18. Responsive ---------- */

/* ---- Phones ----
   Everything below was measured on a 375px screen. Three separate faults, all
   of which read as "this was not designed for a phone":

   1. Body copy bottomed out at 12-13px in five places. Those clamp floors were
      set against desktop columns of 20-30 characters; at full width the line is
      50 characters and needs the size of real body text.
   2. The kit stills rendered 101x92 inside a 144px row — too small to make out
      what is in the box, which is the entire job of that section.

   The art boxes are deliberately NOT touched here. Shortening them looked like
   free height, but the ratios are load-bearing. The speaker still no longer has
   this problem — it is shown at its own 16:9 now, so .audio-waves is a
   percentage of the uncropped file and nothing can slide it. The card panels
   are 429px on a phone and still do. On the cards, glasses-led is
   900x1200: a 1/1 box crops 25% of its height rather than 4/5's 6%, which is
   enough to cut into the indicator light the card exists to show. Any change
   here has to move the marker percentages with it. */
@media (max-width: 48rem) {
  .hw-card p { font-size: 1rem; }
  .graph-lede { font-size: 1rem; }
  .act-steps li { font-size: .875rem; }
  .spec-col dt,
  .spec-col dd { font-size: .8125rem; }

  /* Tall enough for the contents of the box to be legible as objects, and the
     side padding cut back hard. 14% of a 1398px desktop cell is comfortable
     breathing room; 14% of a 140px phone cell is 39px of the 140 gone, which
     left the cable rendering 101px wide inside a 168px tile. */
  .kit-grid { grid-auto-rows: clamp(11rem, 44vw, 15rem); }
  .kit-art { padding-inline: 6%; }
  .kit-cell.is-wide .kit-art { padding-inline: 12%; }}

/* Large desktop monitors. 90rem is the right cap for a laptop — at 1920 it fills
   75% of the window — but on a 2560 display it leaves the whole site sitting in
   56% of the glass with 560px of empty white either side. Raised to 100rem past
   2000px, which is 62% at 2560 and still a measured column rather than a
   full-bleed stretch. Everything inside is percentage or fr based and simply
   scales; the images have the resolution for it (the privacy plate is 2400px
   wide, the case photographs 1600px). The gutter grows a little here too, since
   a 21px margin against a 1600px slab looks like a mistake. */
@media (min-width: 125rem) {
  :root { --maxw: 100rem; --gutter: var(--gutter-wide); }}

@media (max-width: 64rem) {
  .footer-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .feature-grid { grid-template-columns: 1fr; gap: 3rem; }
  .press-quotes { grid-template-columns: 1fr; gap: 2.5rem; }
  .hw-grid { grid-template-columns: 1fr; }
  /* Two per row was the old compromise, and on a real phone it failed: at 375px
     each mock came out 160px wide with 6px labels inside it, so the one thing
     this section exists to show could not be read at all. Swiping keeps them at
     roughly the width they have on a laptop. The page itself never scrolls
     sideways because the overflow is owned by this row. */
  .phone-row {
    display: flex;
    gap: clamp(1rem, 4vw, 1.75rem);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Stops a swipe that runs past the last phone from chaining into the page
       and the browser's own back gesture. Without it iOS hands the leftover
       movement upward, which reads as the whole page sliding sideways. */
    overscroll-behavior-x: contain;
    /* Bleed to the window edges so a half-visible next phone signals the swipe,
       then pad the ends back in so the first and last still line up with the
       body copy above them. */
    margin-inline: calc(var(--gutter) * -1);
    padding-inline: var(--gutter);
    padding-bottom: .5rem;
  }
  .phone-row::-webkit-scrollbar { display: none; }
  .phone-row .phone {
    flex: 0 0 min(78%, 17.5rem);
    scroll-snap-align: center;
  }

  /* THE ORIGINAL HEIGHT, BACK, for every width that swipes. The three trims
     that shortened these handsets - 9/18 to 9/16, square gallery tiles to
     16/10, and the hero image from 92% to 74% - were made to fit three phones
     side by side on a desktop, where they stand in a grid and their height is
     the section's height. Below 64rem none of that applies: they are cards on a
     rail, only one and a half are on screen at a time, and the height they cost
     is height nothing else was competing for. What the trims bought there was
     nothing, and what they cost was a shorter screen with a gap under its
     content.

     All three come back together, and they have to. Restoring the ratio alone
     would leave the pairing screen's content 77px short of the frame, and since
     none of its rows grow, that shortfall would open as dead space under the
     last button rather than distributing. Measured at 390: 558px against the
     499 it had been cut to, which is where it started. */
  .phone-frame { aspect-ratio: 9 / 18; }
  .ps-thumbs i { aspect-ratio: 1; }
  .ps-pair-hero img { width: 92%; }

  /* Stack: graph on top, its readout beneath — overlaying text on a 211px-tall
     panel would be unreadable.

     The FIELD gets a whole screen to itself, and the copy begins under it. The
     panel used to be the thing that was 100svh and the two shared it out as a
     flex column, which left the canvas about 45% of the screen — a band with
     white above and words below, so the animation was never seen whole. Giving
     the canvas the 100svh directly and letting the panel run as tall as it needs
     means one screenful is the field and nothing else. */
  .graph-panel {
    aspect-ratio: auto;
    max-height: none;
    min-height: 0;
    display: block;
  }
  .graph-canvas {
    position: relative;
    aspect-ratio: auto;
    width: 100%;
    height: 100svh;
  }
  /* The copy overlays the field down here, so this earns its keep again: it is
     what keeps the words legible where the disc's lower edge runs under them.
     Starts below the disc (which now sits high) and only reaches full strength
     behind the text block. */
  .graph-fade {
    background: linear-gradient(180deg,
                transparent 0%,
                transparent 52%,
                rgba(255, 255, 255, .72) 64%,
                rgba(255, 255, 255, .94) 74%,
                var(--er-white) 84%);
  }
  /* Held to the canvas, not the panel — the panel is the same height here now,
     but stating it keeps these three locked to the field if the panel ever grows
     past it again. */
  .graph-fade,
  .graph-vignette,
  .graph-labels {
    height: 100svh;
    bottom: auto;
  }
  /* Back to an overlay. Stacking it under the canvas gave the field a clean
     screen of its own but pushed every word below the fold, so the section could
     never be taken in at once. Anchored to the bottom of that same screen, over
     the fade, with the disc sitting high above it. */
  .graph-ui {
    position: absolute;
    inset: auto 0 0 0;
    width: 100%;
    justify-content: flex-end;
    box-sizing: border-box;
    margin: 0;
    padding: 0 clamp(1.25rem, 5vw, 2rem) clamp(1.25rem, 4vw, 1.75rem);
    gap: .5rem;
  }
  .footer-grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }}

@media (max-width: 48rem) {
  :root { --header-height: 3.25rem; }

  .nav-primary { display: none; }
  .menu-toggle { display: flex; }
  /* nav-primary carried the auto margin that pushed the actions right; restore it */
  .header-actions { margin-left: auto; }

  /* Menu on the outside edge, waitlist button inboard of it — which is the
     markup's own order, so nothing needs reordering. The base rule's -10px on
     .menu-toggle stands: it pulls the icon out to the gutter line, which is
     only correct while the menu is the last thing in the bar. */
  .header-actions { gap: .5rem; }

  /* Taken down a step so the bar reads as logo-first rather than button-first.
     Scoped to the header: .btn-sm is used nowhere else, but the hero and
     waitlist CTAs share .btn and must keep their full size. */
  .header-actions .btn-sm {
    height: 2rem;
    padding-inline: .875rem;
    font-size: .75rem;
  }

  /* The width problem this rule existed for is gone with the wordmark: the
     lockup was 117px of a 375px bar even after being stepped down twice, and
     the mark is 42px. So this is no longer about making room, only about the
     mark not overpowering a smaller bar. 17px against the desktop 19, keeping
     the same step down between the two. */
  .brand-logo { height: 1.0625rem; }

  .voice { grid-template-columns: 1fr; }
  .voice-portrait { max-width: 14rem; aspect-ratio: 4 / 4.4; }

  .footer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2rem 1.5rem; }

  /* Stack the routing vertically: the spine moves to the top of the agent list
     so the fan-out still reads as coming from AIRA. */
  .aira-flow { grid-template-columns: 1fr; gap: 1.25rem; }
  .af-hub { flex-direction: row; justify-content: flex-start; gap: .625rem; padding: .75rem .875rem; }
  .af-hub-name { margin-top: 0; }
  .af-hub::before, .af-hub::after { display: none; }
  .af-out { padding-left: 0; border-left: 0; }
  .af-out li::before { display: none; }
  .af-out li { grid-template-columns: 4.5rem 1fr auto; gap: .5rem; }
  /* Full-width tap target on a phone, where the button is now the only thing
     in this section. The rules it replaces sized a form that no longer exists. */
  .btn-preorder { width: 100%; text-align: center; }

  .hero-hud { left: 6%; top: 18%; }
  .hero-cta .btn { flex: 1 1 auto; }

  /* Chat leads on mobile — it is what explains the product. The product shot
     drops to a supporting strip above it. */
  .hero-stage {
    grid-template-columns: 1fr;
    gap: .875rem;
  }
  .hero-art img { max-height: min(18svh, 150px); }
  .hero-chat { max-width: 100%; }

  .tech-lens { border-radius: 999px / 120px; aspect-ratio: 16 / 10; }
  .legal-inner { flex-direction: column; align-items: flex-start; }}

@media (max-width: 30rem) {
  .footer-grid { grid-template-columns: 1fr; }
  .hero-hud { display: none; }}

/* ---------- 19. Motion / print ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  .hero-art .glasses-lines > * { stroke-dashoffset: 0; }
  .hero-hud { opacity: 1; }
  /* Show the whole exchange at once rather than staggering it in. */
  .chat-line { opacity: 1; transform: none; }
  /* The connect ring's overrides lived here. Its tiles started at opacity 0 and
     span on a keyframe, so both needed stating outright once the durations were
     collapsed. The rail that replaced it animates nothing of its own — it rides
     .reveal, which this block already handles — so there is nothing left to say.
     Put an override back here if the rail ever gains motion. */
  /* Show the exchange outright; a thinking indicator with no wait is noise. */
  .case-say, .case-did,
  .act-steps li, .act-note { animation: none; opacity: 1; transform: none; }
  .case-typing { display: none; }}

@media print {
  .site-header, .mobile-menu { display: none; }
  body { color: #000; }}

/* ---------- 20. The card reel ----------
   Memory, Recall and Act as one swipeable row of cards, at EVERY width.

   It was phones only: .reel-wrap and .reel were display:contents above 60rem,
   so the three <section> elements laid out as full-bleed bands and the row's
   heading, captions and arrows were not rendered at all. They are the layout
   now, and the only thing that changes across the breakpoint is the shape —
   portrait media on a phone, LANDSCAPE on a desktop — plus the sizes that hang
   off it. Those overrides are in one block at the foot of this section.

   The media files are 16:9 to begin with, so the desktop card shows them
   uncropped; it is the phone that takes a 3:5 window out of them.

   The block below is wrapped in `@media all` rather than unwrapped entirely.
   It keeps every rule at its existing indentation and keeps the diff to the two
   lines that decide WHERE it applies — the alternative was re-indenting 330
   lines and hoping nothing moved. */
@media all {
  /* One screen, one heading, three cards. */
  /* One screen, and it fills one. Held to the viewport height with the contents
     centred in it, so the leftover reads as margin around a composition rather
     than as dead white under a card that stopped early. */
  .reel-wrap {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: calc(100svh - var(--header-height));
    background: var(--er-white);
    padding-block: clamp(1.25rem, 4vw, 2rem);
  }
  /* Whichever card is centred, this shows that section's own heading. They are
     different lengths, so the box is reserved at the height of the tallest -
     without it the row jolts up and down as you swipe, which reads as the whole
     layout being unstable rather than the words changing. */
  .reel-heading {
    display: block;
    margin: 0 0 clamp(.625rem, 2vw, 1rem);
    padding-inline: var(--gutter);
    font-size: clamp(1.25rem, 5.6vw, 1.75rem);
    line-height: 1.2;
    /* Two lines reserved, not three: with the lines balanced none of the three
       headings needs a third, and the row would otherwise sit a line lower than
       it has to on every card. */
    min-height: calc(2 * 1.2em);
    /* Splits the longest heading evenly instead of letting it wrap where it
       runs out of room — without this "AURVIZ remembers everything you ask it"
       fills line one and leaves "to" stranded on line two. */
    text-wrap: balance;
    color: var(--er-gray-90);
  }
  /* The row. Snapping to centre rather than to the start is what produces the
     reference's look: the card you are on sits in the middle of the screen with
     its neighbours showing at both edges, instead of one card pinned left. The
     scroll-padding is what keeps the first and last cards centred too, since a
     flex row would otherwise butt them against the ends. */
  .reel {
    display: flex;
    gap: clamp(.625rem, 3vw, 1rem);
    /* Act renders FIRST, so there is a card to the left of the one you open on.
       Only the paint order moves — the DOM stays Capture, Recall, Act, which is
       the order the story is told in and the order a screen reader and the tab
       key still get. Without this the row opened with empty padding on the left
       and a neighbour only on the right, which is not the shape it is copying.
       The row is scrolled to Capture at startup; see script.js. */
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    /* Same reason as .phone-row: keep the swipe inside the reel rather than
       letting the overshoot move the page. */
    overscroll-behavior-x: contain;
    /* The card's width, named once, so the padding can be DERIVED from it
       rather than guessed alongside it.

       This was 12vw, chosen to pair with a 76vw card — the two summing to 100.
       That only holds while the width term of the min() below actually wins.
       It often does not: the card is min(76vw, 62svh*3/5), and on a phone with
       browser chrome showing, svh is far short of the screen height, so the
       HEIGHT term wins and the card comes out narrower. The padding stayed at
       12vw, and card one sat 12px left of centre on a 375x700 window with a
       45px peek on one side and 70 on the other. On a real handset it was
       nearer 20pt.

       Half the leftover, whichever term wins, is centred by construction. */
    --reel-card: min(76vw, calc(62svh * 3 / 5));
    padding-inline: calc((100% - var(--reel-card)) / 2);
    scroll-padding-inline: calc((100% - var(--reel-card)) / 2);
    scrollbar-width: none;
  }
  .reel::-webkit-scrollbar { display: none; }

  /* Each section is now a card, so everything that made it a full-bleed band of
     the page has to come off: its own width, its own padding, its own heading. */
  /* Whichever is smaller: most of the width, or whatever a 3:4 card can be
     without pushing the screen past one. The height term is what stops a tall
     narrow phone turning the card into something that has to be scrolled. */
  .reel > #act { order: -1; }

  .reel > .section {
    /* The same value the padding is derived from. Keep it as the variable —
       writing the min() out again here is how the two drifted apart before. */
    flex: 0 0 var(--reel-card);
    scroll-snap-align: center;
    min-width: 0;
    padding: 0;
    margin: 0;
    background: none;
  }
  .reel > .section > .container,
  .reel > .section .cases-split {
    width: 100%;
    max-width: none;
    padding: 0;
    margin: 0;
  }
  .reel > .section > .container > .d-h2,
  .reel > .section .cases-copy { display: none; }

  /* Only the centred card is in focus; its neighbours sit back, blurred and
     dimmed. --focus is written per card by script.js from how near that card is
     to the middle of the row, 1 at dead centre falling to 0 a card's width away,
     so the change is continuous under a finger rather than snapping between two
     states.

     Applied to the media rather than the whole card so the labels underneath
     stay sharp, which is how the reference reads: three legible names, one
     picture in focus. Defaulting to 1 means a card is sharp if the script never
     runs. */
  /* The side cards are clickable — script.js brings the one you tap to the
     middle — so they say so. The centred card keeps the default cursor: a click
     there does nothing, and a pointer that promises otherwise is a lie.

     Driven by a class the script already sets, not by sniffing --focus out of
     the style attribute. A custom property cannot be matched on its VALUE in
     CSS, and [style*="--focus: 1"] would have been a substring match on a
     number — it breaks the moment the value is formatted differently. */
  .reel > .section { cursor: pointer; }
  .reel > .section.is-centred { cursor: default; }

  .reel > .section .case-media,
  .reel > .section .recall-media,
  .reel > .section .act-media {
    filter: blur(calc((1 - var(--focus, 1)) * 7px));
    opacity: calc(.42 + var(--focus, 1) * .58);
    transform: scale(calc(.93 + var(--focus, 1) * .07));
    transform-origin: 50% 50%;
    will-change: filter, opacity, transform;
  }

  /* The media, portrait and rounded. 3:4 rather than 9:16 — a full 9:16 card at
     64vw is 114% of the screen height on a 390x844 phone, which is the one thing
     this layout exists to avoid. */
  .reel .case-media,
  .reel .recall-media,
  .reel .act-media {
    position: relative;
    /* 3:5 rather than 3:4. At 3:4 the card left about 220px of the screen empty
       below it on a 393x853 phone, because a 3:4 box can only be as tall as the
       width allows and the width is capped by the peek. Taller uses the height
       that was going spare.

       It cannot go much past this: these are 16:9 files, and cover shows
       aspect / 1.778 of their width — 42% at 3:4, 34% at 3:5. Narrower than
       that and the crop starts cutting into the person wearing the glasses. */
    aspect-ratio: 3 / 5;
    width: 100%;
    margin: 0;
    border-radius: var(--reel-radius, 18px);
    overflow: hidden;
  }

  /* Recall and Act round on the GRID as well, and the reason is structural:
     their overlay is a SIBLING of the media, not a child of it. Capture's sits
     inside .case-media and is clipped by its overflow:hidden, so its corners
     come out round; these two are positioned against .recall-grid / .act-grid
     and hung below the picture, painting their gradient straight through the
     bottom corners and squaring them off.

     Rounding and clipping the grid puts all three on one shape without moving
     anything in the markup. */
  .reel .recall-grid,
  .reel .act-grid {
    border-radius: var(--reel-radius, 18px);
    overflow: hidden;
  }

  /* Same structural split, second symptom. The side-card treatment further up —
     blur, fade, scale to .93 — is set on the MEDIA. On Capture that carries the
     overlay with it, because the overlay is inside the media. On these two the
     overlay stayed full size and perfectly sharp on top of a picture that had
     shrunk and blurred underneath it, so its gradient overhung the photo on
     every side and read as a second, squarer panel behind the card.

     Matched by inset rather than by transform. Scaling the overlay would need
     the GRID's centre as its origin, and the overlay is bottom-anchored — its
     own centre sits far lower, so a 50% 50% origin would slide it up the card.
     Insetting is exact and needs no magic numbers: a box scaled to s about its
     centre pulls in by (1-s)/2 on each side, which is 3.5% at s = .93, and a
     percentage inset already resolves against the grid — width for left/right,
     height for bottom. Picture and overlay land on the same rectangle.

     The text inside does not shrink with it. At 7% under a 7px blur, on a card
     the reel has already cropped past half, there is nothing to see. */
  .reel > .section .recall-copy,
  .reel > .section .act-copy {
    filter: blur(calc((1 - var(--focus, 1)) * 7px));
    opacity: calc(.42 + var(--focus, 1) * .58);
    left: calc((1 - var(--focus, 1)) * 3.5%);
    right: calc((1 - var(--focus, 1)) * 3.5%);
    bottom: calc((1 - var(--focus, 1)) * 3.5%);
    will-change: filter, opacity;
  }
  .reel .case-media img,
  .reel .case-media .case-video,
  .reel .recall-video,
  .reel .act-video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 0;
  }
  /* Where the crop sits. These are 16:9 files in a 3:5 box, so cover keeps the
     full height and shows 34% of the width, and the default centre crop misses
     the person wearing the glasses in three of the four. The window runs from
     position x (1 - 0.338) to that plus 0.338.

     Aim at the FACE, not the head. Measured across each file, the face and
     glasses sit at:

       c01  50-74%    c02  66-82%    c03  70-88%    c04  22-38%

     c01 was set to 88% from the whole head, 50-95%, which put the window on her
     ear and the bun and cut her face off the left edge entirely — she is in
     profile facing left, so her face is the left end of her head, not the
     middle of it. 66% frames the face and glasses with looking room in the
     direction she faces. */
  .reel #cases .case:nth-child(1) img { object-position: 66% 50%; }
  .reel #cases .case:nth-child(2) img { object-position: 85% 50%; }
  .reel .recall-video { object-position: 90% 50%; }
  /* 26 rather than 21: at the 3:5 crop the window is 34% of the width, and 21%
     put its right edge at 0.476 while this subject reaches 0.48. */
  .reel .act-video    { object-position: 26% 50%; }
  /* The two grids that put media and words side by side collapse to one cell —
     the words go on top of the picture here. */
  .reel .recall-grid,
  .reel .act-grid {
    display: block;
    margin: 0;
    gap: 0;
  }
  .reel .cases,
  .reel .cases-split .cases { margin: 0; padding: 0; max-width: none; }
  .reel .case-track { display: grid; }

  /* Chat over the picture, on all three. Memory already overlaid it before the
     40rem rule pushed it below; Recall and Act never did, so they get the same
     treatment: pinned to the foot of the card, over a scrim. */
  .reel .case-scrim { display: block; }
  .reel .case-overlay,
  .reel .recall-copy,
  .reel .act-copy {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    max-width: none;
    margin: 0;
    /* 10px of inset was the same distance the bubbles' own radius spans, so
       they looked stuck to the corner. 13 gives the group a margin of its own
       inside the card. */
    padding: 1.75rem .8125rem .8125rem;
    color: var(--er-white);
    background: linear-gradient(to top,
                rgba(14, 16, 20, .88) 0%,
                rgba(14, 16, 20, .74) 34%,
                rgba(14, 16, 20, 0) 100%);
    z-index: 2;
  }
  .reel .recall-media,
  .reel .act-media { position: relative; }
  .reel .recall-grid,
  .reel .act-grid { position: relative; }

  /* Bubbles back to glass-on-a-picture: the 40rem rule had inverted them to the
     paper pair because the caption used to sit on the page down here. It sits on
     the photograph again, so the clear variant is right again. */
  .reel .case-say,
  .reel .recall-copy .case-say,
  .reel .act-copy .case-say {
    background: var(--glass-clear-fill);
    border: var(--glass-clear-rim);
    color: var(--er-white);
    box-shadow: var(--glass-clear-lift);
  }
  .reel .case-did,
  .reel .case-typing {
    background: var(--glass-clear-fill);
    border: var(--glass-clear-rim);
    box-shadow: var(--glass-clear-lift);
  }
  .reel .case-did { color: rgba(255, 255, 255, .9); }
  .reel .case-did b { color: var(--er-white); }
  .reel .case-typing i { background: rgba(255, 255, 255, .7); }

  /* Measured before touching: 10px type, 5x8 padding, and the reply bubble
     running 264px of a 285px card — 93% of the width against the question's
     54%. A bubble that reaches both edges is not a bubble, it is a caption bar,
     and the pair had no common rhythm.

     max-width is the fix that matters. Capping it makes the long line wrap
     instead of stretching, so both bubbles sit in the same column and read as
     one exchange. 84% leaves the tail corner visibly inset from the card edge,
     which is what makes the shape legible as a speech bubble at all.

     The size comes up with it: 11.5px is still small for a mockup inside a
     mockup, but 10 was under the floor the rest of this site was raised to. */
  .reel .case-say,
  .reel .case-did {
    max-width: 84%;
    padding: .4375rem .625rem;
    font-size: .71875rem;
    line-height: 1.42;
  }
  .reel .case-typing { padding: .5rem .625rem; }
  /* The reply is a separate turn, not a second line of the same one. 3px read
     as a line break; 6 reads as an answer. */
  .reel .case-reply { margin-top: .375rem; }
  .reel .recall-found {
    margin-top: .25rem;
    font-size: .5rem;
    line-height: 1.3;
    color: rgba(255, 255, 255, .72);
  }
  .reel .recall-found b { color: var(--er-white); }
  /* Act carries the most: a question, two steps, a reply and the note. At the
     spacing the other two use it covered 85% of its own picture on a 360px
     phone, so its rows are set tighter than theirs. */
  .reel .act-steps { margin: .25rem 0; font-size: .5rem; line-height: 1.3; }
  .reel .act-steps li { margin: 0; padding-left: .75rem; }
  .reel .act-steps li { color: rgba(255, 255, 255, .72); }
  .reel .act-steps li b { color: var(--er-white); }
  .reel .act-note {
    margin: .25rem 0 0;
    font-size: .5rem;
    line-height: 1.3;
    color: rgba(255, 255, 255, .6);
  }

  /* The name of the beat, under its card.

     It was a ::after carrying one word at 11px in grey — legible only if you
     already knew what it said. Real markup now and sized to be read: it names
     the beat the card is demonstrating, so it is set as a label rather than a
     footnote.

     A description line ran under it for one revision and is gone by request.
     Nothing reserves height here any more and nothing needs to: all three
     captions are a single word on a single line, so there is no jolt to absorb
     as the row is swiped. Put the reserve back if a longer caption returns. */
  .reel-cap {
    display: block;
    margin: .875rem 0 0;
    text-align: center;
    font-size: clamp(1.125rem, 5.2vw, 1.4375rem);
    font-weight: 400;
    line-height: 1.25;
    letter-spacing: -.02em;
    color: var(--er-gray-90);
  }

  /* Prev / next, centred under the caption. Quiet by default because the swipe
     is the primary gesture; these are the affordance that says the row moves at
     all, which a static screenshot otherwise never shows. */
  .reel-nav {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 1.5rem;
    margin-top: .875rem;
  }
  .reel-arrow {
    /* 44px, the touch-target floor, with the glyph drawn smaller inside it. A
       24px button is the right SIZE and the wrong TARGET. */
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--er-gray-50);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color .2s var(--ease), background-color .2s var(--ease);
  }
  .reel-arrow svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  .reel-arrow:hover { color: var(--er-gray-90); }
  .reel-arrow:active { background: var(--er-gray-05); }
  /* Ends of the row: dimmed rather than removed, so the pair never reflows and
     the caption above it does not shift sideways on the first and last card. */
  .reel-arrow[disabled] { color: var(--er-gray-10); cursor: default; }}

/* ---- Desktop: the same cards, landscape ----
   Only what has to change with the shape. Everything else — the centring, the
   focus blur, the snap, the caption, the arrows — is shared with the phone and
   is not repeated here.

   The media goes 3:5 to 16:9, which is the files' own ratio, so the desktop
   card crops nothing at all. object-position is left alone: it exists to aim
   the phone's 3:5 window and does nothing once the box matches the file. */
@media (min-width: 60rem) {
  .reel-wrap {
    /* Sized so the whole section — heading, card, caption, arrows — lands in one
       frame with room to spare, not by luck. At --section-y it measured 887px
       against a 900px viewport: it fitted, but by 13px, and anything a little
       shorter pushed the arrows under the fold. */
    min-height: 0;
    /* 36px rather than 52. The chrome around the card — padding, heading,
       caption, arrows — costs 258px of the frame, and the padding is 104 of
       that. Taking 32 back is 32 more of card, and the section still has air
       above and below it. */
    padding-block: clamp(1.5rem, 3vw, 2.25rem);
  }
  .reel-heading {
    /* Down again, for the same reason and to the same place: the picture. Two
       reserved lines went 118px at 3.4vw, then 83 at 2.4, and are 65 here. The
       heading is a label for the card under it, not the thing being looked at. */
    font-size: clamp(1.125rem, 1.9vw, 1.75rem);
    padding-inline: 0;
    /* Wide enough that the longest of the three sets on ONE line. It is 41
       characters; at 46rem it had the room and was still breaking, because the
       break was a <br> in the markup rather than a wrap — see script.js. */
    max-width: 60rem;
    margin-inline: auto;
    text-align: center;
    /* One line reserved, not two. The box is held at a fixed height so the row
       does not jolt as the heading changes between cards; two lines of reserve
       was 32px of air above the card that nothing ever used.

       nowrap because the reserve alone is not a guarantee. The box here is
       shrink-to-fit, and the longest of the three measured 466px of text into
       465px of box — it happened to render on one line, by a pixel. A rounding
       difference, a font fallback or a slightly wider word would have tipped it
       to two and pushed the card down. This is safe at these lengths: the
       longest is a third of the viewport, and the phone rules below do not
       inherit it, so the narrow view still wraps as it must. */
    min-height: 1.2em;
    white-space: nowrap;
  }

  /* Three terms, and each one is doing something.

     62vw is the proportion, and it sets how much of a NEIGHBOUR shows, because
     the peek is (100vw - card) / 2 and the gap is taken out of it. The run so
     far: 46vw showed 61% of a neighbour, 54 showed 43%, and 62 with a wider gap
     shows about a quarter. Each step hides more of the sides and gives the
     middle more room, which is the same lever pulled twice.

     56rem caps it on a wide monitor. Past that the card stops reading as one
     object and starts reading as a band again, which is what this replaced.

     54svh is the one that keeps the section in a single frame. The card is
     landscape, so its HEIGHT is width * 9/16, and on a short window the width
     terms alone would push the arrows under the fold. Expressed against svh so
     it shrinks with the viewport rather than being tuned for one screen. */
  .reel {
    /* Up again. The section's padding paid for this one, the heading paid for
       the last: chrome is 226px now, down from 258.

       The three terms mean the same things throughout — 74vw the proportion,
       68rem the cap on a wide monitor, 66svh the guarantee that heading, card,
       caption and arrows land in one frame.

       66 is derived, not chosen. Chrome costs about 226px whatever the window
       height, so a 700px one can give the card 474 of it — 67.7% — and 66
       leaves a margin under that. Raising it further is what would finally push
       the arrows off a laptop, so it is the term to check first if this grows
       again. At 1440x900 it yields 1056px of card against the 960 before. */
    --reel-card: min(74vw, 68rem, calc(66svh * 16 / 9));
    /* Air between the centred card and the two beside it. 56px was the last
       step; this is 79 on a 1440px window.

       On a clamp rather than a fixed value, and that matters more the wider the
       gap gets: it is taken out of the peek, not out of the card, so a fixed
       80px would keep eating the same amount off a narrower desktop where there
       is less peek to spend. At 1440 it leaves 113px of each neighbour showing;
       at 1024, where the card is 758 and the peek only 133, it scales back to
       56 and still leaves 77. A fixed 80 would have left 53. */
    gap: clamp(2.5rem, 5.5vw, 5rem);
  }

  /* A bigger card wants a bigger corner. One property, read by the media AND by
     the two grids that clip an outboard overlay, so the whole card changes
     radius together rather than the picture rounding away from its own frame. */
  .reel { --reel-radius: 22px; }

  .reel .case-media,
  .reel .recall-media,
  .reel .act-media {
    aspect-ratio: 16 / 9;
  }

  /* The card is about three times the phone's width, so the overlay that sits
     on it scales with it — at the phone's 11.5px the exchange read as a stamp
     in the corner of a large photograph. */
  .reel .case-overlay,
  .reel .recall-copy,
  .reel .act-copy { padding: 4rem 1.5rem 1.5rem; }
  .reel .case-say,
  .reel .case-did {
    max-width: 58%;
    padding: .5625rem .875rem;
    font-size: .9375rem;
    line-height: 1.45;
  }
  .reel .case-typing { padding: .625rem .875rem; }
  .reel .case-reply { margin-top: .5rem; }
  .reel .recall-found { margin-top: .4375rem; font-size: .75rem; }
  .reel .act-steps { margin: .4375rem 0; font-size: .75rem; }
  .reel .act-steps li { padding-left: 1rem; }
  .reel .act-note { margin-top: .4375rem; font-size: .75rem; }

  .reel-cap {
    margin-top: 1.25rem;
    /* One word centred under a card that runs past 1000px wide. At the 20px
       this replaces it read as a caption on the picture rather than the name
       of the beat, which is what it is.

       The FLOOR here is the base rule's ceiling, 1.4375rem, and the two have to
       stay equal. Below 60rem the base clamp has long since hit its maximum, so
       any lower floor makes the caption SHRINK as the window widens past the
       breakpoint — 23px at 959, 20px at 960. The pair this replaced was matched
       at 17px for the same reason; keep them matched. */
    font-size: clamp(1.4375rem, 2vw, 1.625rem);
  }
  .reel-nav { margin-top: 1rem; gap: 2rem; }}


/* ---------- 21. Letter page (company.html) ----------

   One card on a warm ground, in the shape of a letter. It is the only page on
   the site that is not on the white canvas, which is deliberate: it is a note
   from the founders rather than a product surface, and the change of ground is
   what says so before a word is read.

   Everything here is scoped under .page-letter or .letter, so none of it can
   reach the homepage. The two tokens that are overridden are overridden on the
   body, for the same reason. */
.page-letter {
  /* The sticky header is otherwise rgba(255,255,255,.91), which reads as a
     white bar laid on cream rather than part of the page. Retinted to the
     ground, keeping the same alpha so the blur behind it still works. */
  --header-bg: rgba(249, 247, 242, .91);
  --rule: #e7e4da;

  /* The quiet grey, for the date and the fact labels. It started at #9b978c,
     which is what the reference looks like and what a letterhead wants — and
     measured 2.79:1 on the card, well under the 4.5:1 AA floor. Both are real
     text at 10-11px, which is the size that can least afford it.

     #6f6b60 is 5.09:1 against #fbfaf5. Still clearly a step back from the body
     copy's 8.63:1, so the hierarchy survives; it just no longer buys the effect
     with legibility. */
  --letter-muted: #6f6b60;

  /* Cool in one corner, warm in the other, over a cream base — the whole effect
     is under 10 levels, so it reads as paper rather than as a gradient. Fixed
     attachment would fight the scroll on iOS; this simply covers the body box. */
  background:
    radial-gradient(115% 85% at 88% 80%, rgba(246, 224, 203, .55) 0%, rgba(246, 224, 203, 0) 60%),
    radial-gradient(105% 75% at 6% 3%,  rgba(231, 238, 221, .50) 0%, rgba(231, 238, 221, 0) 55%),
    #f7f6f1;
  background-attachment: scroll;
}

.letter-page {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  /* Fills the viewport so the footer sits at the bottom on a short letter
     instead of floating halfway up the screen. */
  min-height: calc(100svh - var(--header-height));
  padding-inline: var(--gutter);
  padding-block: clamp(2rem, 5.5vw, 4.5rem) clamp(2.75rem, 7vw, 6rem);
}

.letter {
  width: 100%;
  max-width: 34rem;
  background: #fbfaf5;
  border-radius: 14px;
  padding: clamp(2rem, 4.4vw, 3.25rem);
  /* Two shadows: a wide soft one for the lift off the page, and a tight one so
     the card still has an edge where it meets a ground almost its own colour. */
  box-shadow: 0 28px 64px -24px rgba(74, 66, 52, .18),
              0 3px 10px rgba(74, 66, 52, .05);
}

.letter-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: clamp(1.75rem, 3.5vw, 2.75rem);
}

/* Set like the page headings, not like a letterhead.

   This began as Georgia bold at 20px, which is what the reference it was drawn
   from uses. It has been moved onto the site's own heading treatment instead:
   the body grotesque at weight 300 with -.048em tracking, which is exactly what
   .d-h2 does for "Designed to fit into everyday life." That combination is the
   whole typographic signature of this site, per the README, and a serif here was
   the one place it broke.

   No font-family declared, deliberately — it inherits from body, which is what
   every other heading on the site does and what keeps the page to one face. */
.letter-title {
  font-weight: 300;
  font-size: clamp(1.75rem, 4.4vw, 2.375rem);
  line-height: 1.07;
  letter-spacing: -.048em;
  color: var(--er-gray-90);
}

.letter-date {
  font-size: .8125rem;
  font-weight: 300;
  letter-spacing: -.01em;
  color: var(--letter-muted);
  white-space: nowrap;
}

.letter-body {
  font-size: .875rem;
  font-weight: 300;
  line-height: 1.78;
  letter-spacing: -.005em;
  color: #4b4941;
}
.letter-body p { margin: 0 0 1.15rem; }
.letter-body p:last-child { margin-bottom: 0; }

/* The same treatment as the title, a size down. These mark the letter's two
   halves, who we are and then why, and at this size the step down from the
   title and up from the body is what makes them read as headings — there is no
   weight or colour doing that work, because at 300 there is none to spend. */
.letter-h {
  font-weight: 300;
  font-size: clamp(1.375rem, 3.4vw, 1.75rem);
  line-height: 1.1;
  letter-spacing: -.045em;
  color: var(--er-gray-90);
  margin: 2.75rem 0 1.05rem;
}
.letter-h:first-child { margin-top: 0; }

.letter-signoff {
  margin-top: 1.75rem;
  color: #6a675d;
}

/* The founding facts used to sit here as a .letter-facts colophon, ruled off
   under the letter. They are in the prose now, so the rules are gone with the
   markup: founded 2025 and IHFC open the letter, New Delhi closes it.

   --letter-muted survives because .letter-date still uses it. */

@media (max-width: 30rem) {
  /* The card loses its inset on the narrowest screens: at 375px a 14px radius
     and a gutter either side leaves the letter looking like a receipt. It keeps
     the padding, which is what the reading measure actually needs. */
  .letter { padding: 1.75rem 1.375rem; }
  /* No .letter-title override any more: both headings are on clamps now, so
     they size themselves down here and a fixed value would fight the clamp. */
  .letter-body { font-size: .8125rem; line-height: 1.75; }}

/* ============================================================
   POLICY PAGES — terms, refunds, shipping
   ============================================================
   Built on .page-letter, not beside it. These share the letter's paper ground
   and its card, because they are the same KIND of page — a document, read
   straight through, with nothing else on the screen — and giving them their own
   ground would say they were something else.

   What they do not share is the measure. .letter is 34rem, which is a reading
   width for continuous prose and is the right call for a letter. A policy is
   not continuous prose: it is numbered clauses, nested lists and the occasional
   definition, and at 34rem an indented sub-list has about eleven words a line
   left. 44rem gives the indents somewhere to go without the top-level text
   running past a comfortable line.

   Everything here inherits the body face. No serif, no second family — same
   rule the letter page follows, and the reason .letter-title stopped being
   Georgia. */
/* ---- Research pages: wider phone gutters ----
   The rest of the site runs a 1rem gutter on a phone, which is right for the
   homepage: full-bleed sections stacked edge to edge, where the padding only
   has to keep text off the glass. A page that is nothing but a column of prose
   wants more air than that. The reference sets its articles at roughly 8vw a
   side, about 32px on a 393px screen, which is double ours.

   Redefined on --gutter itself rather than on .article-page, because the header
   reads the same variable. Pad only the article and the logo hangs 16px to the
   left of the first word of the text under it; move the variable and the mark,
   the menu button, the prose, the table full-bleed and the footer all shift
   together, which is the arrangement the reference has.

   Phone only. 8.2vw at a laptop width would be a 100px gutter. */
@media (max-width: 48rem) {
  .research-doc { --gutter: clamp(1.25rem, 8.2vw, 2.25rem); }}

/* ---- Research article ----
   The reference's shape: a small line of date and category, an oversized title,
   a byline, then one narrow column. Narrow is the point - this piece is 2,300
   words of argument and the measure is what makes that readable. 42rem holds
   about 75 characters at 18px, which is the top of the comfortable range.

   The tables are the exception and they are why .article is not simply a
   max-width on everything: six columns of benchmark numbers do not fit a 42rem
   measure at any font size worth reading, so they break out wider and scroll
   inside themselves. */
.article-page {
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: clamp(2.5rem, 6vw, 4.5rem) clamp(3.5rem, 9vw, 7rem);
  /* positioning context for .sidenote - absolute, not fixed, so a note stays
     pinned to its own paragraph and scrolls with it rather than sitting
     stationary the way .article-toc does */
  position: relative;
}

/* Section tracker, the way the reference keeps a long piece navigable: a
   column of section links pinned in the page's own side gutter, the current
   one marked as the reader scrolls. Fixed against the VIEWPORT rather than
   any element on the page, deliberately - .article-page centres itself with
   plain margin:auto, and .article-fig breaks out of that with its own
   translateX trick; a toc computed relative to either would have to track
   both. Fixed sidesteps that at the cost of one fact this rule leans on: the
   figure breakout stays centred on the viewport's own midpoint at every
   width, capped or not, because margin:auto centres .article-page on the
   viewport first and the breakout centres on .article-page after.

   The floor was 1560px and almost nobody ever saw this. That number was set
   when FIGURES broke out to 58rem and had to be cleared; figures now sit at
   the text measure, and the only thing still breaking out is the tables. So
   the constraint is just: clear the 58rem table edge, which sits at
   viewport/2 - 464px.

   Rather than pin this column at a fixed 48px and pick a breakpoint big
   enough for that to be safe, it tracks the table edge directly -
   50vw - 464px(half the breakout) - 24px(gap) - 168px(this column's width),
   which is 50vw - 656px. The gap to the table stays constant at every width
   instead of growing from 68px to whatever, and the floor falls out of the
   arithmetic: 20px of left margin needs 50vw >= 676, so 1352px, rounded
   to 1360.

   THE DEFAULT IS NO LONGER display:none. Twice now the answer to "the
   sidebar isn't there" has been that the gutter needs about 1360px and a
   great many screens do not have it - a 1920x1080 monitor at 150% display
   scaling reports 1280 CSS pixels, and every laptop below that reports less.
   A gutter this thing cannot fit into is a fact about the layout, but
   showing NOTHING because of it was a choice, and the wrong one: it made
   navigation a reward for owning a wide monitor.

   So below the sidebar width it is an ordinary contents list, in the flow at
   the top of the article, in two columns to stay short. Above it, the same
   markup becomes the pinned column. Nothing is hidden at any width; only its
   position changes. This is also why the nav had to move inside
   .article-page in the markup - a fixed element ignores its place in the
   flow, an in-flow one very much does not, and before the move it would have
   rendered above the masthead. */
.article-toc {
  max-width: 42rem;
  margin: 0 auto clamp(2rem, 4vw, 3rem);
  padding-top: 1.1rem;
  border-top: 1px solid var(--rule);
}
.article-toc::before {
  content: "Contents";
  display: block;
  margin-bottom: .75rem;
  font-size: .6875rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--er-gray-50);
}
.article-toc ul { list-style: none; margin: 0; padding: 0; }
/* Two columns so eleven sections do not push the first paragraph off the
   screen; break-inside keeps a section and its rule together. */
.article-toc > ul { columns: 2; column-gap: 2rem; }
.article-toc > ul > li { break-inside: avoid; margin-bottom: .45rem; }
.article-toc a {
  display: block;
  padding-left: .7rem;
  border-left: 2px solid transparent;
  font-size: .8125rem;
  line-height: 1.4;
  color: var(--er-gray-70);
  text-decoration: none;
  transition: color .2s var(--ease-out), border-color .2s var(--ease-out);
}
.article-toc a:hover { color: var(--er-gray-90); }
.article-toc a.is-active {
  color: var(--er-gray-90);
  border-left-color: var(--er-gray-90);
  font-weight: 500;
}

/* At the side, in a column of its own.

   The fixed-position version this replaces only appeared when the page
   happened to have spare gutter left over after the 58rem tables, which is
   why it kept not being there: it was scavenging space rather than being
   given any. A reserved column cannot fail that way. .article-page becomes a
   two-column grid, everything that was in it moves to column 2, and the
   tracker takes column 1 - so the space is allocated before anything else is
   laid out and no width can take it away.

   The tracks are symmetric - spare, centre, spare - which is what keeps the
   masthead, hero, lede and body on the page's centre line. A single column on
   the left would shove all of them right by its width and gap. The reference
   does exactly this: its shell is 139px / 660px / 139px, tracker in the left
   track, right track empty and there only to balance. Sidenotes need no track
   of their own; they are positioned absolutely against .article-page.

   position:sticky, not fixed, so it travels with the page and then holds -
   the behaviour the reference has. Sticky needs somewhere to travel, which is
   what the grid area gives it.

   It starts level with the first paragraph, not at the top of the page. The
   reference does the same: nothing sits beside its masthead or its hero, and
   the tracker only turns up once the body does. So the rows are assigned
   explicitly - masthead, hero and lede take rows 1 to 3 on their own, and the
   tracker shares row 4 with the article body. Sticky then travels the height
   of that row, which is the whole of the article. Spelling the rows out
   rather than leaving them to auto-placement is what keeps the tracker off
   row 1: it is first in the markup, so auto-placement would put it there.

   1240px, and the number is forced rather than picked. Centring costs double:
   whatever the tracker takes on the left has to be matched by empty space on
   the right, so the viewport must cover 928(centre) + 2x(track + gap) +
   42(gutters). At 1240 that leaves each track about 114px, which is the least
   the list reads at. Below it the contents stay in the flow at the top, where
   they are still visible - just not at the side. */
@media (min-width: 1240px) {
  .article-page {
    display: grid;
    grid-template-columns: minmax(0, 1fr) min(58rem, 100%) minmax(0, 1fr);
    /* The tracker and the gap divide one fixed allowance between them - the
       track is (viewport - 928 - gutters) / 2 and nothing widens it - so every
       pixel given to the gap is taken off the list's measure. This is set as
       far as it goes before the longest label starts breaking badly. */
    column-gap: clamp(1.5rem, 2.8vw, 3.25rem);
  }
  .article-page > *:not(.article-toc) { grid-column: 2; }
  .article-page > .article-head { grid-row: 1; }
  .article-page > .article-hero { grid-row: 2; }
  .article-page > .article-lede { grid-row: 3; }
  .article-page > .article      { grid-row: 4; }
  .article-toc {
    grid-column: 1;
    grid-row: 4;
    align-self: start;
    /* Sits at the inner edge of the left track, so the gap between it and the
       body is the grid gap and nothing else. It fills the track up to 10.5rem
       and simply narrows below that on smaller screens rather than spilling
       out of it, which would push the page wider than the viewport. */
    justify-self: end;
    position: sticky;
    top: calc(var(--header-height) + 2rem);
    width: 100%;
    max-width: 10.5rem;
    /* Same top margin as .article, so the first tracker item starts level
       with the first paragraph rather than at the bare row line above it. */
    margin: clamp(2.25rem, 5vw, 3.5rem) 0 0;
    padding-top: 0;
    border-top: 0;
    max-height: calc(100vh - var(--header-height) - 4rem);
    overflow-y: auto;
  }
  /* The label earns its place inline, where the list needs introducing
     against the body copy around it. Pinned in an empty gutter it does not. */
  .article-toc::before { content: none; }
  .article-toc > ul {
    columns: auto;
    display: flex;
    flex-direction: column;
    gap: .65rem;
  }
  .article-toc > ul > li { margin-bottom: 0; }}

/* Citations in the margin, the way the reference sets its own inline
   references: the full "Author, Title, Venue, Year" sitting beside the
   paragraph that names the system, rather than only a hyperlink on the name.
   Positioned by script.js, not CSS float - .article-fig breaks out of
   .article on its own translateX trick and .article-toc sits fixed to the
   viewport, so a note computed by simple float:right margin arithmetic would
   have to know about both. JS instead reads the widest element's real right
   edge and each note's own paragraph's real top, at the moment it runs, and
   places the note there directly - correct regardless of how either of those
   two centre themselves.

   position:absolute against .article-page (which is why that rule now
   carries position:relative), not position:fixed against the viewport - a
   sidenote is meant to travel WITH its paragraph as the page scrolls, unlike
   the section tracker, which is meant to stay put.

   A much higher floor than .article-toc, and it went UP when the tracker took
   a grid column of its own. That column pushes the article 196px right, and
   196px off the left is 196px taken from the side the notes live on. Past
   the 90rem cap the breakout's right edge lands at (V-1440)/2 + 1282, and
   that plus 40(standoff) + 226(note) + 16(margin) has to stay inside V -
   which needs V >= 1688, rounded to 1700.

   So notes are rarer than they were, and that is the honest cost of the
   tracker being reliably at the side rather than sometimes: a note rendering
   78px off the right edge of the window is worse than a note that waits for
   room. Below the floor the inline citation link is the only one a reader
   sees - already a complete citation on its own, carrying the full reference
   in its title attribute, not a fallback missing something. */
.sidenote { display: none; }

@media (min-width: 1700px) {
  .sidenote {
    display: block;
    position: absolute;
    width: 226px;
    font-size: .8125rem;
    line-height: 1.6;
    color: var(--er-gray-70);
  }
  .sidenote-entry { display: block; }
  .sidenote-entry + .sidenote-entry { margin-top: .9em; }}

/* CENTRED, and heavy. This is the one block on the site set that way, which is
   the point: it is a masthead for a single piece rather than a page heading, and
   the body underneath stays left-aligned in its own column.

   Wider than the 42rem measure below it. A centred display line wants to break
   where the words want to break, and at 42rem this title breaks after "Notes"
   and leaves a two-word second line. */
.article-head {
  max-width: 52rem;
  margin-inline: auto;
  text-align: center;
}
/* The category alone, above the rule of the title. The date used to live up here
   with it - "Research - August 2026" on one line - and has moved below, which is
   the arrangement the reference uses and the better one: the category says what
   kind of thing this is before you read the title, the date is a footnote to it
   and belongs after. */
.article-kicker {
  margin: 0 0 clamp(1rem, 2.4vw, 1.75rem);
  font-size: clamp(.8125rem, 1vw, .9375rem);
  font-weight: 500;
  letter-spacing: -.005em;
}
.article-kicker a {
  color: var(--fg);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color .2s var(--ease-out);
}
.article-kicker a:hover,
.article-kicker a:focus-visible { border-bottom-color: currentColor; }

/* 300, the same light cut as every other heading on this site. It was tried at
   600 to match the reference, which sets its mastheads heavy, and taken back:
   one page in a bold face is a second voice, and the arrangement below - centred,
   category above, date beneath - is what makes this read as a masthead. Size and
   placement carry it, not weight.

   -.045em goes back with the weight. The tighter -.03 existed only because 600
   closes the letterforms up on its own; at 300 that value reads loose. */
.article-title {
  margin: 0;
  font-weight: 300;
  font-size: clamp(2.125rem, 5.8vw, 4rem);
  line-height: 1.06;
  letter-spacing: -.045em;
}
.article-date {
  margin: clamp(1.25rem, 3vw, 2rem) 0 0;
  font-size: clamp(.9375rem, 1.1vw, 1rem);
  color: var(--er-gray-70);
}

/* The hero, where the reference puts it: under the masthead, above the body.
   58rem, which is the figure and table width rather than the 42rem measure -
   so the page has two widths on it and not three.

   No border and no radius. The three charts below sit on bare white the same
   way, and framing this one would make it the only boxed thing on the page.

   It stays inside the gutters on a phone rather than bleeding to the edges,
   which is what .article-fig does; only the tables bleed, and they bleed
   because they have to scroll, not for effect. */
.article-hero {
  width: min(58rem, 100%);
  /* Tight to the masthead above it: the title block and the hero read as one
     unit, and the air belongs below them rather than between. */
  margin: clamp(1rem, 2.2vw, 1.5rem) auto 0;
}
.article-hero img {
  display: block;
  width: 100%;
  height: auto;
}

/* The one-sentence statement of what the thing is, between the hero and the
   body. The reference sets it centred, large and on a short measure, and the
   short measure is the point: it breaks to three lines and reads as a stated
   claim rather than as a first paragraph that happens to be big.

   36rem, not the 42 the body runs at. text-wrap: balance evens the three lines
   instead of leaving one word stranded on the last, which centred type shows up
   far more than left-aligned type does.

   400 sits between the title (300) and the h2s (500). It has to read as a
   second voice without competing with the masthead directly above it. */
.article-lede {
  max-width: 36rem;
  margin: clamp(1.125rem, 2.6vw, 1.75rem) auto 0;
  text-align: center;
  font-size: clamp(1.3125rem, 2.5vw, 1.9375rem);
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: -.022em;
  text-wrap: balance;
}

.article {
  max-width: 42rem;
  margin: clamp(2.25rem, 5vw, 3.5rem) auto 0;
  font-size: clamp(1rem, 1.2vw, 1.125rem);
  line-height: 1.62;
}
.article > p { margin: 0 0 1.15em; }
.article > h2 {
  margin: clamp(2.25rem, 4.4vw, 3rem) 0 .85em;
  font-weight: 500;
  font-size: clamp(1.125rem, 1.5vw, 1.3125rem);
  line-height: 1.3;
  letter-spacing: -.018em;
}
/* The article's thesis, set apart. Not a quotation - nobody said it - so it is
   a paragraph with weight rather than a <blockquote> with a rule and a citation
   slot that would sit empty. */
.article-pull {
  margin: clamp(1.75rem, 3.4vw, 2.5rem) 0 !important;
  padding-left: clamp(.875rem, 1.6vw, 1.125rem);
  border-left: 2px solid var(--er-gray-90);
  font-size: clamp(1.0625rem, 1.35vw, 1.25rem);
  line-height: 1.45;
  letter-spacing: -.012em;
}

/* Figures sit at the text measure, not the table breakout, which is how the
   reference arranges its own: a diagram the width of the column it interrupts,
   caption directly beneath it.

   The breakout they used to have was working against them. An .article-fig is
   an <svg width:100%> over its own viewBox, so the ratio between viewBox units
   and rendered pixels decides how big the type inside a diagram comes out - and
   at 720 units stretched over 928px, 12.5px type was being rendered at 16px,
   larger than the caption under it and larger than the body copy beside it, for
   no reason anyone chose. The diagrams are now drawn 660 units wide into a
   42rem (672px) column, near enough 1:1, so 13px type inside a figure is the
   same 13px as the caption below it. Tables keep the breakout - seven columns
   of benchmark numbers genuinely need it; a diagram does not. */
.article-fig {
  width: 100%;
  margin: clamp(1.75rem, 3.4vw, 2.5rem) 0;
}
/* --fig-w is set on each svg and is the width of what it draws, in px. The
   viewBoxes are cropped to their content, so without the cap a figure would
   simply be scaled up to fill the measure - the retrieval one would run its
   13px type at 18. Capped, the drawing keeps the size it had and only the dead
   margin that used to surround it is gone. */
.article-fig svg {
  display: block;
  width: 100%;
  max-width: var(--fig-w, 100%);
  height: auto;
  margin-inline: auto;
  overflow: visible;
}
/* Caption sits under the drawing at the same size as the type inside it, in a
   warmer grey than the body ink so it reads as apparatus rather than argument. */
.article-fig figcaption {
  margin-top: 13px;
  font-size: .8125rem;
  line-height: 1.4;
  color: #6b6866;
  max-width: 100%;
}

/* Wider than the measure, because six columns of figures inside 42rem are
   unreadable at any size. Centred on the column rather than left-aligned to it,
   so the text either side stays symmetrical around them. */
.article-tablewrap {
  width: min(58rem, 92vw);
  margin: clamp(1.75rem, 3.4vw, 2.5rem) auto;
  margin-inline-start: 50%;
  transform: translateX(-50%);
  /* The scroll lives HERE, on the table's own box. Without it a six-column
     table sets the page's minimum width and the whole document scrolls
     sideways on a phone - one table breaking every section above and below it. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.article-table {
  width: 100%;
  border-collapse: collapse;
  font-size: clamp(.8125rem, 1vw, .9375rem);
  line-height: 1.4;
  /* Numbers stop wrapping mid-figure and columns stop resizing row to row. */
  white-space: nowrap;
}
.article-table th,
.article-table td {
  padding: .6875rem .875rem;
  text-align: left;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
.article-table th {
  font-weight: 500;
  border-bottom-color: var(--er-gray-30);
}
/* The first column carries the row's name and is prose, so it alone may wrap. */
.article-table td:first-child,
.article-table th:first-child { white-space: normal; min-width: 11rem; }
.article-table tbody tr:last-child td { border-bottom: 0; }

/* The bar chart that used to sit under the question-type table, folded into
   it. The two carried the same six numbers; the table also carried the
   descriptions, which the chart had no width for, so the chart was a second
   drawing of one column. The meter runs the full 0-100 the chart's axis did,
   which is what keeps 92.6 and 77.8 honestly close rather than exaggerated by
   a cropped scale. It is decorative - aria-hidden, with the figure beside it
   left to carry the value to a screen reader. */
.qt-score { white-space: nowrap; }
.qt-meter {
  display: inline-block;
  width: 6.5rem;
  height: .5rem;
  margin-right: .625rem;
  background: var(--er-gray-10);
  vertical-align: baseline;
}
.qt-meter > span { display: block; height: 100%; background: var(--er-gray-90); }
.qt-num { font-weight: 500; }

@media (max-width: 48rem) {
  /* Full-bleed to the gutters on a phone: at 390 the breakout is wider than the
     screen, and centring it on a 50% origin then pulling it back puts its left
     edge off-screen. */
  .article-tablewrap {
    width: auto;
    margin-inline: calc(var(--gutter) * -1);
    transform: none;
    padding-inline: var(--gutter);
  }

  /* No contents list on a phone. It earns its place at the side of a wide
     screen, where it tracks position while you read. Stacked in the flow it
     tracks nothing - it has scrolled away by the first paragraph - and eight
     entries in two columns is most of a screen to get past before the article
     starts. The scroll-spy is left alone: it reads the links' targets, and a
     display:none list simply has nothing to mark. */
  .article-toc { display: none; }

  /* The hero to the full width of the screen. It was 314 of 375, so a fifth of
     the width was going to margin that nothing else on a phone respects.

     One negative --gutter, which is exactly .article-page's own padding, and
     not a 50%/50vw bleed. vw counts the scrollbar and the percentage does not,
     so the vw form was tried and landed 7px left of true at 768 - overhanging
     one edge while stopping short of the other. Backing out the padding cannot
     drift, because it is the same number that put it there.

     Nothing is cropped. Width grows, height follows from the intrinsic 1856x634
     via the img's own height:auto, so the whole frame stays in view and simply
     runs 314x107 -> 375x128. Filling more than the screen would mean cropping,
     so this is the ceiling. */
  .article-hero {
    width: auto;
    margin-inline: calc(var(--gutter) * -1);
  }

  /* Whole diagram on screen, nothing to scroll sideways. The figure runs to
     both edges and its drawing fills that width.

     width:auto matters as much as the margin. .article-fig is width:100%, and
     a negative margin on a fixed-width box slides it left instead of widening
     it - it sat at 0..314 rather than 0..375 for no gain, the same trap the
     hero needed width:auto for.

     The desktop cap is left in place and simply does not bind here: a phone is
     narrower than any figure's drawn width, so width:100% wins. Lifting it was
     tried and is wrong - it let a 768px screen inflate the retrieval diagram
     to 744px and 19.4px type. Cropping the viewBoxes is what buys the size
     back: a 375px phone now scales that diagram at 0.75 rather than 0.57.

     This is a real trade against the scrolling version, which reached 10.1px.
     Fitting a diagram drawn 500-640 units wide into 375px cannot do better
     than about 8-10px on its labels; the way past that is narrower drawings
     laid out for a phone, not more scaling. */
  .article-fig {
    width: auto;
    margin-inline: calc(var(--gutter) * -1);
  }
  .article-fig figcaption { padding-inline: var(--gutter); }}

/* ---- Consolidated benchmark table ----
   The reference lays its benchmarks out transposed: metrics down the side,
   systems across the top, in a wrapper that scrolls sideways with the two
   leading label columns pinned. That orientation is what puts one system's
   retrieval AND answer numbers on the same line - the two tables this
   replaces had them on different screens, one system per row in each.

   Eleven systems will not fit a 42rem column at a readable size and never
   could, so this scrolls rather than shrinks. What makes that bearable is
   the pinning: lose the metric name as you scroll right and the numbers stop
   meaning anything. */
.benchmark-scroll {
  width: min(58rem, 92vw);
  margin: clamp(1.75rem, 3.4vw, 2.5rem) auto .75rem;
  margin-inline-start: 50%;
  transform: translateX(-50%);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.benchmark-scroll:focus-visible { outline: 2px solid var(--er-gray-50); outline-offset: 3px; }

.benchmark-table {
  border-collapse: separate;
  border-spacing: 0;
  font-size: .8125rem;
  line-height: 1.4;
  white-space: nowrap;
}
/* No rules anywhere, the way the reference sets it - the pinned label columns
   and the row shading carry the structure instead. */
.benchmark-table th,
.benchmark-table td {
  padding: .5rem .7rem;
  text-align: center;
  font-weight: 400;
  border: 0;
  background: var(--bg);
}
.benchmark-table thead th { color: var(--er-gray-70); }
.benchmark-table thead th a { color: inherit; }

/* The two pinned columns. bm-metric's offset has to equal bm-group's rendered
   width exactly or the two overlap mid-scroll, so the width is set here rather
   than left to the content. */
.benchmark-table .bm-group,
.benchmark-table .bm-metric {
  position: sticky;
  text-align: left;
  background: var(--bg);
}
.benchmark-table .bm-group {
  left: 0;
  z-index: 3;
  width: 5.25rem;
  min-width: 5.25rem;
  vertical-align: top;
  color: var(--er-gray-70);
}
.benchmark-table .bm-metric {
  left: 5.25rem;
  z-index: 2;
  color: var(--er-gray-90);
  font-weight: 500;
}
.benchmark-table thead .bm-group,
.benchmark-table thead .bm-metric { z-index: 4; }

/* Ours, and the best value in each row. The leader is computed from the
   numbers rather than assumed - Recall@10 shades MemGAS, which is the row
   Memtwin does not win. */
.benchmark-table .bm-ours { background: #fbf7ec; }
.benchmark-table td.bm-best { background: #f6efd6; font-weight: 500; color: var(--er-gray-90); }

.benchmark-note {
  font-size: .8125rem;
  line-height: 1.4;
  color: #6b6866;
  margin: 0 0 clamp(1.75rem, 3.4vw, 2.5rem);
}

@media (max-width: 48rem) {
  .benchmark-scroll {
    width: auto;
    margin-inline: calc(var(--gutter) * -1);
    transform: none;
    /* NOT padding-inline. A sticky column pins to the PADDING edge, so a
       padded scrollport left the pinned pair standing one gutter in from the
       screen edge with nothing covering the strip beside them - and the table
       scrolls THROUGH its own padding, so sliding left ran the numbers into
       that strip, in front of the labels. Measured at scrollLeft 300: 85.92
       painting at -66..7 and 78.51 at 7..82, both left of a pair pinned at 31.

       The gutter is carried by the pinned column itself instead. It reaches
       left: 0 at the true edge, its background covers the whole strip, and the
       inset survives because the cell's own padding provides it. */
    padding-inline: 0;
  }
  .benchmark-table .bm-group {
    width: calc(5.25rem + var(--gutter));
    min-width: calc(5.25rem + var(--gutter));
    padding-left: calc(.7rem + var(--gutter));
  }
  .benchmark-table .bm-metric { left: calc(5.25rem + var(--gutter)); }
  /* The trailing gutter the scrollport used to give the last column. */
  .benchmark-table tr > *:last-child { padding-right: calc(.7rem + var(--gutter)); }}

/* ---- Article body elements ----
   h3, lists, a rule and a block quote. They exist because the benchmark piece
   is reproduced from its source document as written, and that document is
   built out of them; before it, the article template had paragraphs, h2s,
   figures and tables and nothing else.

   All of it sits on the same 42rem measure and the same scale as
   `.article > p`, so an enumerated passage reads as body copy that happens to
   be numbered rather than as a second typographic voice. */
.article > h3 {
  margin: clamp(1.75rem, 3.4vw, 2.25rem) 0 .6em;
  font-weight: 500;
  font-size: clamp(1rem, 1.25vw, 1.0625rem);
  line-height: 1.35;
  letter-spacing: -.012em;
}

/* Markers hang in the padding so the text column lines up with the paragraphs
   above and below rather than stepping in and out around every list. */
.article > ul,
.article > ol {
  margin: 0 0 1.15em;
  padding-left: 1.35rem;
}
.article > ul { list-style: disc; }
.article > ol { list-style: decimal; }
.article li { margin: 0 0 .45em; }
.article li:last-child { margin-bottom: 0; }
.article li::marker { color: var(--er-gray-50); }

/* The stated objective, which the source sets as a quotation. Same rule and
   indent as .article-pull but at body size: it is a definition rather than the
   article's thesis, and does not want the display treatment. */
.article > blockquote {
  margin: clamp(1.5rem, 3vw, 2rem) 0;
  padding-left: clamp(.875rem, 1.6vw, 1.125rem);
  border-left: 2px solid var(--er-gray-90);
}
.article > blockquote p { margin: 0; }


/* 500, not 700. Inter Tight is loaded at 200/300/400/500 and nothing heavier,
   so a <strong> left at its default weight is synthesised by the browser -
   smeared rather than bold. The source emphasises heavily and every one of
   those would have shown it. */
.article strong { font-weight: 500; color: var(--er-gray-90); }

/* Citations to the papers behind each named baseline. Coloured off the body
   ink rather than left to inherit it, with the underline always on - the
   treatment the reference uses for its own inline citations, measured off
   its rendered page rather than approximated: link text sits at #676767
   against #282828 body copy, underlined at 30% of that same grey rather
   than full strength, which is what keeps a paragraph or a table full of
   them from reading as a row of scored-out words. Hover/focus brings both
   up to full ink and a solid rule, so the quiet default still answers when
   touched. */
.article a {
  color: #676767;
  text-decoration: underline;
  text-decoration-color: rgba(103, 103, 103, .3);
  text-underline-offset: 2px;
  transition: color .2s var(--ease-out), text-decoration-color .2s var(--ease-out);
}
.article a:hover,
.article a:focus-visible {
  color: var(--er-gray-90);
  text-decoration-color: currentColor;
}

/* The pipeline written out as a line of stages, which is how the source states
   it before any diagram appears. Set apart from the paragraph rather than run
   into it; the stage names hold together and only the arrows may break, so it
   wraps on a phone instead of scrolling. */
.article > .article-flow {
  margin: clamp(1.5rem, 3vw, 2rem) 0;
  font-weight: 500;
  line-height: 1.75;
  letter-spacing: -.008em;
}
.article > .article-flow span { white-space: nowrap; }
.article > .article-flow .flow-arrow {
  color: var(--er-gray-50);
  font-weight: 400;
  padding: 0 .2em;
}

/* ---- Research index ----
   A wide list rather than the centred card the policies use. The two pages want
   opposite things: a policy is one document read top to bottom, so it is capped
   at 44rem and set on a panel; an index is a set of separate items scanned for
   the one you want, which needs the full measure and a rule between each.

   Light, like every other page here. The reference this was taken from is white
   type on black; inverting one page would give the site a second theme to keep
   consistent for the sake of a single template. */
.research-page {
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: clamp(2.5rem, 7vw, 6rem) clamp(3.5rem, 9vw, 7rem);
}

/* The one piece of the reference worth copying exactly: the title is enormous
   and sits alone, and that is what says "index" before a word is read. Bigger
   than .policy-title's 38px ceiling by a long way. */
.research-title {
  font-weight: 300;
  font-size: clamp(2.75rem, 8.4vw, 5.5rem);
  line-height: 1.02;
  letter-spacing: -.05em;
  margin: 0;
}
/* Rule ON the list and on each row, so the first item has a line above it and
   the last has one below without a special case at either end. */
.research-list {
  list-style: none;
  margin: clamp(2.5rem, 6vw, 4.5rem) 0 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}
.research-row {
  display: grid;
  /* The meta column is fixed rather than fractional. Left as a fraction it grows
     with the window and drags the titles toward the middle of a 1920 screen,
     which is the one thing the reference's layout does not do.

     20rem, not the 13 it was. Measured against the reference at 1440: its titles
     begin 27.3% of the way across the window and mine began at 18.9%, which is
     the whole of the difference between the two - the summary measure was
     already close. 20rem plus the 3rem gap puts the content at 27.0%. */
  grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
  gap: clamp(.75rem, 3vw, 3rem);
  /* Positioning context for the stretched hit area on the title link. */
  position: relative;
  padding-block: clamp(1.5rem, 3.2vw, 2.5rem);
  border-bottom: 1px solid var(--rule);
}
.research-meta {
  font-size: clamp(.9375rem, 1vw, 1rem);
  line-height: 1.45;
}
.research-cat { display: block; color: var(--fg); }
/* The date slot. Nothing carries one yet - see the note in the markup - and it
   is left in so the first dated entry needs markup and no CSS. */
.research-when { display: block; color: var(--er-gray-70); margin-top: .1875rem; }

/* The title is still the only <a>, but its hit area covers the entire row.
   Those are two separate things and this splits them deliberately.

   Wrapping the whole row in an anchor would have made the link's accessible
   name the title PLUS the category, the date and three lines of summary, which
   a screen reader then announces as one enormous label. Stretching a
   pseudo-element out of the title link instead leaves the accessible name as
   the title alone while giving a finger the whole entry to land on.

   Hover comes along free: a pseudo-element is part of its originating element,
   so pointing anywhere in the row fires a:hover and runs the underline.

   The cost is that the summary text can no longer be selected with a mouse,
   because the overlay sits above it. On an index where every row is a
   destination rather than something to quote, that is the right trade. */
.research-h a {
  color: inherit;
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  transition: background-size .28s var(--ease-out);
}
/* The stretched hit area. Invisible, no background, no z-index - it only has
   to sit above the row's own text, and being later in the box order it does.
   Add anything else interactive to a row and it will need z-index above 0. */
.research-h a::after {
  content: "";
  position: absolute;
  inset: 0;
}
.research-h a:hover,
.research-h a:focus-visible { background-size: 100% 1px; }
.research-h {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.3125rem, 2.1vw, 1.75rem);
  line-height: 1.2;
  letter-spacing: -.032em;
}
.research-sum {
  margin: .5rem 0 0;
  /* 80ch, up from 62. At 62 the summary used 613px of an 1142px column on a
     1830 window and stopped 752px short of the right edge, so the row read as a
     narrow block of text with half its column empty beside it. The reference
     runs its summaries to about 75% of the window; 80ch lands close to that
     while still being a measure rather than the full column, which at this
     width would be 112 characters.

     ch, not a percentage, so the line length is set by the TYPE rather than by
     the window - the cap binds at every width above about 1200 and the column
     takes over below it.

     Back to 70 from 80, because the meta column widened at the same time and the
     two compound: the reference ends its summaries at 73.9% of the window, and
     80ch behind a 20rem meta column overshot that to 80.6%. 70ch lands at 73.6. */
  max-width: 70ch;
  font-size: clamp(.9375rem, 1.15vw, 1.0625rem);
  line-height: 1.55;
  color: var(--er-gray-70);   /* 6.69:1 on white; --fg-muted would be 3.15 */
}

/* The note under the list. On the row grid so it lands in the content column
   and reads as a continuation of the list rather than a stray paragraph, but
   quieter than a summary - it is apparatus, not an entry. */
.research-note {
  display: grid;
  grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
  gap: clamp(.75rem, 3vw, 3rem);
  margin: clamp(1.5rem, 3.2vw, 2.5rem) 0 0;
}
.research-note > span {
  grid-column: 2;
  max-width: 60ch;
  font-size: clamp(.9375rem, 1vw, 1rem);
  line-height: 1.55;
  color: var(--er-gray-70);
}

@media (max-width: 56rem) {
  /* One column, and this breakpoint moved with the meta column. It was 40rem
     when that column was 13rem wide; at 20rem the two-column layout stops
     working much sooner. Measured at 660, just above the old breakpoint: the
     meta column was 320px and the content column 288 - the label wider than the
     thing it labels, and the titles starting 54% of the way across the screen.

     56rem is where the content column still gets ~490px, which is the point
     below which a title and a three-line summary stop having room. */
  .research-row { grid-template-columns: 1fr; gap: .5rem; }
  /* Same collapse as the rows, so the note stays aligned with them. */
  .research-note { grid-template-columns: 1fr; gap: 0; }
  .research-note > span { grid-column: 1; }
  /* The separator between these two was a middle-dot escape that lost its
     backslash somewhere and left a raw NUL plus the letters b7 in the file,
     which phones rendered literally as "b7" between the category and the
     date. Removed rather than repaired: a gap separates two short fields
     perfectly well, and there is no glyph left to corrupt. */
  .research-meta { display: flex; gap: .75rem; }
  .research-cat, .research-when { display: inline; margin: 0; }}

.policy-page {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  min-height: calc(100svh - var(--header-height));
  padding-inline: var(--gutter);
  padding-block: clamp(2rem, 5.5vw, 4.5rem) clamp(2.75rem, 7vw, 6rem);
}

.policy {
  width: 100%;
  max-width: 44rem;
  background: #fbfaf5;
  border-radius: 14px;
  padding: clamp(2rem, 4.4vw, 3.25rem);
  box-shadow: 0 28px 64px -24px rgba(74, 66, 52, .18),
              0 3px 10px rgba(74, 66, 52, .05);
  /* Section numbers come from here, so the markup never hard-codes "1." and
     cannot drift when a clause is inserted in the middle. */
  counter-reset: policy-section;
}

.policy-head {
  margin-bottom: clamp(1.75rem, 3.5vw, 2.5rem);
  padding-bottom: clamp(1.25rem, 2.4vw, 1.75rem);
  border-bottom: 1px solid var(--rule);
}

.policy-title {
  font-weight: 300;
  font-size: clamp(1.75rem, 4.4vw, 2.375rem);
  line-height: 1.07;
  letter-spacing: -.048em;
  color: var(--er-gray-90);
  margin: 0;
}

/* The preorder page opens with a sentence rather than a date line, so it needs
   a size between the title and the body — .policy-updated's 13px reads as a
   caption, which is not what a lede is. */
.policy-lede {
  margin: .95rem 0 0;
  max-width: 34em;
  font-size: .9375rem;
  font-weight: 300;
  line-height: 1.65;
  letter-spacing: -.008em;
  color: #4b4941;
}

.policy-updated {
  margin: .85rem 0 0;
  font-size: .8125rem;
  font-weight: 300;
  letter-spacing: -.01em;
  color: var(--letter-muted);
}

.policy-body {
  font-size: .875rem;
  font-weight: 300;
  line-height: 1.78;
  letter-spacing: -.005em;
  color: #4b4941;
}
.policy-body p { margin: 0 0 1.15rem; }
.policy-body > *:last-child { margin-bottom: 0; }

/* Numbered, via the counter on .policy. The number sits in the margin on wide
   screens so the heading text still starts on the same line as the body below
   it; below 40rem it comes inline, because a hanging indent at that width eats
   the measure it was meant to protect. */
.policy-h {
  counter-increment: policy-section;
  position: relative;
  font-weight: 400;
  font-size: clamp(1.0625rem, 2.2vw, 1.1875rem);
  line-height: 1.25;
  letter-spacing: -.03em;
  color: var(--er-gray-90);
  margin: 2.5rem 0 .9rem;
}
.policy-h:first-child { margin-top: 0; }
.policy-h::before {
  content: counter(policy-section) ".";
  position: absolute;
  left: -2.25rem;
  width: 1.75rem;
  text-align: right;
  font-weight: 300;
  color: var(--letter-muted);
}

.policy-sub {
  font-weight: 400;
  font-size: .9375rem;
  letter-spacing: -.02em;
  color: var(--er-gray-90);
  margin: 1.75rem 0 .65rem;
}

.policy-body ul,
.policy-body ol {
  margin: 0 0 1.15rem;
  padding-left: 1.25rem;
}
.policy-body li { margin-bottom: .5rem; }
.policy-body li:last-child { margin-bottom: 0; }
.policy-body strong { font-weight: 500; color: var(--er-gray-90); }
.policy-body a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* Entity details, contacts, grievance officer — label/value pairs that would be
   a table if a table were not overkill at three rows. */
.policy-dl {
  margin: 0 0 1.15rem;
  display: grid;
  grid-template-columns: minmax(7.5rem, auto) 1fr;
  gap: .4rem 1.25rem;
}
.policy-dl dt { font-weight: 400; color: var(--letter-muted); }
.policy-dl dd { margin: 0; }

/* ── The unfilled-fact marker ──
   Deliberately ugly. Every one of these is a fact only the business can supply
   — a registered address, a ship date, a grievance officer's name — and each is
   REQUIRED, either by the Consumer Protection (E-Commerce) Rules or by Razorpay's
   own activation check. A tasteful grey placeholder is one that ships.

   It also carries its own label in ::before rather than relying on the author to
   type "TO FILL" every time, so they are impossible to miscount: search the repo
   for policy-fill and the list is exhaustive. */
.policy-fill {
  display: inline;
  background: #ffe8a3;
  color: #4a3c00;
  font-weight: 500;
  padding: .1em .4em;
  border-radius: 3px;
  box-shadow: inset 0 0 0 1px #d8ac00;
}
.policy-fill::before { content: "TO FILL: "; font-weight: 600; letter-spacing: .02em; }

/* A standing note at the head of each document, for things the reader should
   know before the clauses start. */
/* Shared by /shipping, /terms, /refunds and /privacy. The rule of a plain
   panel now, not a quote bar: the tinted ground is already doing the work of
   marking it as an aside, and the 3px rule on the left was a second marker
   saying the same thing. With it gone the radius runs all four corners rather
   than the right-hand two, and the padding is symmetric again — the bar used to
   add 3px to the left inset that the right side never had. */
.policy-callout {
  margin: 0 0 1.75rem;
  padding: 1rem 1.15rem;
  background: #f2efe4;
  border-radius: 8px;
}
.policy-callout p:last-child { margin-bottom: 0; }

/* Sideways links to the other two documents. They are a set, and a reader who
   lands on the refund policy from a search result should be able to reach the
   terms without going via the footer. */
.policy-siblings {
  margin-top: clamp(2rem, 4vw, 3rem);
  padding-top: clamp(1.25rem, 2.4vw, 1.75rem);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: .6rem 1.5rem;
  font-size: .8125rem;
  font-weight: 300;
}
.policy-siblings a { color: var(--letter-muted); text-decoration: none; }
.policy-siblings a:hover { color: var(--er-gray-90); text-decoration: underline; }
.policy-siblings [aria-current="page"] { color: var(--er-gray-90); }

/* The hanging number comes inline below 60rem, and the number is where that
   figure comes from rather than a guess at where a tablet starts.

   The number hangs 2.25rem — 36px — into the card's padding, and that padding is
   clamp(2rem, 4.4vw, 3.25rem). Those two only clear each other once 4.4vw is
   comfortably past 36px, which is a 960px viewport; below roughly 730px the
   clamp is pinned to its 2rem floor of 32px and the number sits OUTSIDE the
   card entirely, floating on the paper ground four pixels clear of the edge.
   Measured at 700px, where it did exactly that.

   40rem was the first guess and it was wrong for the whole 640–960 band —
   every small laptop and every tablet in landscape. */
@media (max-width: 60rem) {
  .policy-h { padding-left: 1.75rem; }
  .policy-h::before { left: 0; width: 1.5rem; text-align: left; }}
@media (max-width: 40rem) {
  /* The label/value grid collapses later than the headings do: two columns are
     still comfortable at 900px and only run out of room when the value column
     drops under about 12rem. */
  .policy-dl { grid-template-columns: 1fr; gap: 0 0; }
  .policy-dl dt { margin-top: .55rem; }
  .policy-dl dt:first-child { margin-top: 0; }}
@media (max-width: 30rem) {
  .policy { padding: 1.75rem 1.375rem; }
  .policy-body { font-size: .8125rem; line-height: 1.75; }}