/* ============================================================
   Blink - a dark studio for a small bright object.

   The device's own screen is emissive, so the page is dark: on a
   light ground that screen reads as a hole, on a dark one it reads
   as alive. Orange is the product's colour, not a chosen accent.
   ============================================================ */

/* ── the typefaces, served from here ──────────────────────
   IBM Plex, self-hosted. It used to come from fonts.googleapis.com, which
   handed Google the IP and user agent of every visitor on every page view -
   on a site whose privacy page is otherwise careful about what leaves the
   browser, and which now has no third-party requests at all.

   Plex Sans is the variable file: one download covers 400 through 700, which
   is fewer bytes than the four static weights it replaces. Plex Mono is not
   published as a variable font, so 400 and 500 are separate.

   The unicode-ranges are Google's own, so a visitor still only pays for
   latin-ext if the page actually contains a character from it. The Hebrew in
   legal.html is in neither range and falls back to the system font, which is
   what it did before as well: Plex has no Hebrew on Google.

   Licence: SIL OFL 1.1, in assets/fonts/OFL.txt. Shipping it alongside is a
   condition of the licence, not a courtesy. */

@font-face{
  font-family:"IBM Plex Mono";
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("assets/fonts/plex-mono-400-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:"IBM Plex Mono";
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("assets/fonts/plex-mono-400-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:"IBM Plex Mono";
  font-style:normal;
  font-weight:500;
  font-display:swap;
  src:url("assets/fonts/plex-mono-500-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:"IBM Plex Mono";
  font-style:normal;
  font-weight:500;
  font-display:swap;
  src:url("assets/fonts/plex-mono-500-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:"IBM Plex Sans";
  font-style:normal;
  font-weight:100 700;
  font-display:swap;
  src:url("assets/fonts/plex-sans-var-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:"IBM Plex Sans";
  font-style:normal;
  font-weight:100 700;
  font-display:swap;
  src:url("assets/fonts/plex-sans-var-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root{
  color-scheme: dark;

  --bg:        #0E0F13;
  --bg-2:      #131419;   /* quieter bands */
  --card:      #191A21;
  --card-2:    #20222A;
  --hair:      #262832;
  --hair-2:    #1E2029;

  --ink:       #F3F1ED;   /* warm white, not pure */
  --ink-2:     #A2A4AE;
  --ink-3:     #8A8D98;

  --screen:    #0A0C10;

  --orange:    #E8873A;
  --orange-2:  #F5A868;

  /* The Codex case is printed in this blue. It is the product's colour, not
     a second accent: it appears only where that case does. */
  --codex:     #2F63D2;

  /* what the dials mean - fixed, never themed */
  --ok:   #2ECC71;
  --warn: #F1C40F;
  --bad:  #E74C3C;

  --wrap: 1560px;
  --r: 18px;

  /* How far you scroll for one stop of a pinned reel, and the two reels are
     that many stops long. It was a shared TOTAL, which was right while they
     had two stops and three -- but the reel is a five-stop tour of the screen
     now, and an equal total would have run it at nearly twice the pace of the
     setup beside it. Equal pace is what "the same length" was actually asking
     for; equal totals was the way to get it when the counts were close.

     THIS IS THE SENSITIVITY DIAL. Raise it and a step costs more scrolling;
     lower it and the captions come quicker. One number, and everything that
     steps in the reel is measured from it.

     It was 100vh, then 85, and both were tuned against a mechanism that no
     longer exists.
     While the wheel was intercepted the GESTURE was the unit: one flick moved
     one stop whatever distance it covered, so this number only decided how
     far the page travelled, not what a step cost. Every value here was
     therefore judged on how the travel looked. With the lock gone the browser
     is doing the stepping, and the distance is now the whole of the cost --
     so the old numbers read as a page that jumps at the slightest push.

     115vh: a deliberate swipe moves you on, an idle one does not. A caption
     here is three lines of narration and a camera move, and it should take
     about as much asking as it takes to read. */
  --stop: 115vh;

  /* One family, three roles. Bricolage Grotesque read as "designed by a
     machine last Tuesday" -- it is all over generated work right now. Plex was
     drawn for IBM in 2017 by Bold Monday, has a real brief behind it, and is
     almost never what a generator reaches for. */
  --display: "IBM Plex Sans", system-ui, sans-serif;
  --body: "IBM Plex Sans", system-ui, -apple-system, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

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

/* hidden must win.
   -------------------------------------------------------------
   [hidden] is only `display:none` in the UA sheet, so any class that sets a
   display - and half the shop does: the cart is a grid, the cart badge an
   inline-flex - silently beats it. That is not a cosmetic bug here: the
   empty-cart page showed the empty state AND an empty cart grid, and the
   badge sat there reading 0. Everything the shop shows and hides goes
   through this attribute, so it gets the one !important in the file. */
[hidden]{ display:none !important; }

/* 18px, not the browser's 16. Nearly every size in this file is in rem, and
   rem is the ROOT's size -- body{font-size} never moved any of it. So this one
   line is the type scale for the whole site, spacing included, and the way to
   make the page bigger or smaller is to change it here rather than to chase
   fifty declarations. */
html{ font-size:18px; scroll-behavior:smooth; -webkit-text-size-adjust:100%; }

body{
  margin:0;
  color:var(--ink);
  font-family:var(--body);
  font-size:1.02rem;
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  -webkit-tap-highlight-color:rgba(243,241,237,.08);
  overflow-x:clip;

  /* a photographer's sweep rather than flat black */
  background:
    radial-gradient(120% 62% at 50% -8%, #1C1D25 0%, transparent 62%),
    radial-gradient(80% 40% at 50% 108%, #16171E 0%, transparent 60%),
    var(--bg);
  background-attachment:fixed;
}

img{ max-width:100%; height:auto; }
a{ color:inherit; }
::selection{ background:var(--orange); color:#14100B; }

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

.skip{
  position:absolute; left:-9999px; top:0; z-index:99;
  background:var(--orange); color:#14100B; padding:.7rem 1.1rem;
  font-family:var(--mono); font-size:.8rem;
}
.skip:focus{ left:1rem; top:1rem; }

/* the nav is sticky, so anything you can link to has to clear it */
[id]{ scroll-margin-top:calc(var(--navh, 5.5rem) + .9rem); }

.wrap{
  width:min(100% - clamp(1.25rem, 4vw, 4rem), var(--wrap));
  margin-inline:auto;
}
/* index.html opts into viewport-fit=cover, so in landscape on a notched
   phone the first 44px of each edge is under the notch or the rounded
   corner. Only the footer accounted for the home indicator. */
body{
  padding-left:env(safe-area-inset-left);
  padding-right:env(safe-area-inset-right);
}

/* ── type ─────────────────────────────────────────────── */

.h2{
  font-family:var(--display);
  font-weight:600;
  font-size:clamp(1.75rem, 3.4vw, 2.6rem);
  line-height:1.05;
  letter-spacing:-.023em;
  text-wrap:balance;
  margin:0 0 .7rem;
  max-width:17ch;
}

/* Last lines that carry two words read as a mistake, and on a phone a
   paragraph has few enough lines that every one of them shows. `pretty` costs
   the browser a couple of extra passes over the last few lines and spends
   them avoiding exactly that. Applied to running text only -- headings
   already balance, and neither does anything to a single line. */
p, dd, dt, li, summary, figcaption, .sub, .step__p, .needs__v{
  text-wrap:pretty;
}

.sub{
  color:var(--ink-2);
  max-width:50ch;
  margin:0;
  font-size:1.01rem;
}

/* ── buttons ──────────────────────────────────────────── */

.btn{
  /* gap, not a space in the markup. This is a flex container, so the label and
     a nested <span data-price> become separate flex items and the whitespace
     between them is discarded - "Pre-order $30" rendered as "Pre-order$30".
     An em dash used to sit in that position and hid it. */
  display:inline-flex; align-items:center; justify-content:center; gap:.42em;
  min-height:48px; padding:.8rem 1.6rem;
  background:var(--orange); color:#14100B;
  border:1px solid var(--orange); border-radius:100px;
  font-family:var(--body); font-weight:600; font-size:.96rem;
  text-decoration:none; cursor:pointer;
  touch-action:manipulation;
  transition:transform .18s cubic-bezier(.2,.8,.3,1), background-color .18s, border-color .18s, color .18s;
}
.btn:hover{ transform:translateY(-2px); background:var(--orange-2); border-color:var(--orange-2); }
.btn:active{ transform:translateY(0); }
/* A button that cannot do anything must not look like one that can. This is
   the pre-order button before there is a checkout behind it. */
.btn:disabled{
  background:transparent; color:var(--ink-3); border-color:var(--hair);
  cursor:not-allowed; transform:none;
}
.btn:disabled:hover{ background:transparent; border-color:var(--hair); color:var(--ink-3); transform:none; }

.btn--quiet{
  background:transparent; color:var(--ink); border-color:var(--hair);
}
.btn--quiet:hover{ background:var(--card); border-color:var(--ink-3); color:var(--ink); }

.btn--sm{ min-height:44px; padding:.5rem 1.15rem; font-size:.87rem; }
.btn--wide{ width:100%; }

/* ── nav ──────────────────────────────────────────────── */

/* Full bleed by being full width, not by a pseudo-element stretched to
   -50vw: that made the document 175px wider than a phone screen. */
.nav{
  position:sticky; top:0; z-index:40;
  background:color-mix(in srgb, var(--bg) 78%, transparent);
  -webkit-backdrop-filter:blur(18px) saturate(1.2);
  backdrop-filter:blur(18px) saturate(1.2);
  border-bottom:1px solid transparent;
  transition:border-color .3s;
}
.nav.is-stuck{ border-bottom-color:var(--hair-2); }
.nav__inner{
  display:grid; grid-template-columns:1fr auto; align-items:center;
  gap:1rem;
  width:min(100% - clamp(1.25rem, 4vw, 4rem), var(--wrap)); margin-inline:auto;
  padding:.9rem 0;
}

.brand{
  display:inline-flex; align-items:center; gap:.5rem;
  text-decoration:none; justify-self:start;
  font-family:var(--display);
  font-weight:700; font-size:1.1rem; letter-spacing:-.02em;
}
/* Ink, not orange. The mark is one colour by design - it has to print on a
   case in a single ink - and a nav that draws it in the accent is the site
   quietly using a different logo from the one in assets/. */
.brand__mark{ color:var(--ink); }
.nav .btn{ justify-self:end; }

/* The CLI switcher lived here. Its markup is gone with the Codex case;
   device.js still knows both providers, so this comes back when there is
   a real Codex enclosure to switch to. */

/* ── hero ─────────────────────────────────────────────── */

.hero{
  overflow:clip;
  text-align:center;
  padding:clamp(1.6rem,4vw,3rem) 1.25rem clamp(2.6rem,5vw,4rem);
  position:relative;
}
.hero::before{
  content:""; position:absolute; inset:2% -20% 6% -20%; z-index:-1;
  background:radial-gradient(46% 44% at 50% 52%, rgba(232,135,58,.20), transparent 70%);
}
.hero__line{
  font-family:var(--display);
  font-weight:600;
  font-size:clamp(2.6rem, 7.6vw, 5.1rem);
  line-height:.96;
  letter-spacing:-.033em;
  margin:0 0 1.1rem;
}
.hero__sub{
  color:var(--ink-2);
  /* 54ch, not 40. The line named the audience as well as the tools once it
     said "for developers who work in the terminal", and at 40ch that ran to
     four lines under a two-line headline -- a paragraph where a subtitle
     should be. Still inside the comfortable range for centred text. */
  max-width:54ch; margin:0 auto;
  font-size:clamp(1rem,1.5vw,1.13rem);
  text-wrap:pretty;
}

.cta{ display:flex; flex-wrap:wrap; gap:.75rem; justify-content:center; }

/* ── the product shot ─────────────────────────────────── */

.shot{
  position:relative; margin:0 auto;
  /* 100%, not 88vw. .hero pads itself by 1.25rem a side, and a vw width knows
     nothing about that -- on a phone the figure came out 21.6px wider than the
     box it sits in, hung off the right edge, and .hero{overflow:clip} cut the
     Codex case in half. Every one of these is a share of the CONTAINER now. */
  width:min(100%, 720px);
  margin-block:clamp(1.1rem,2.4vw,1.7rem) clamp(2.6rem,4.5vw,3.4rem);
  will-change:transform;
}
/* max-width matters as much as max-height: on a phone the height cap lets
   the intrinsic width run past .shot, and body{overflow-x:clip} hides that
   without fixing it. */
.shot img{ display:block; width:auto; margin-inline:auto;
           max-width:100%; max-height:min(44vh, 470px); }

/* Two enclosures overlapping, the case in front. Settled in a fitting room
   rather than argued about: 61 / 75 heights, 18% overlap, the Codex case dropped
   5% below the shared baseline. The numbers below are those, translated into
   units that survive the size cap.

   Every attempt to make the pair MOVE relative to each other - trade places,
   orbit, cross - ended with one of them hidden behind the other, in a hero
   whose whole job is to show there are two. So they hold their arrangement
   and breathe as one thing. */
/* Everything here is a share of the pair's own width, which is the only way
   the arrangement survives both ends of the range. Sized by max-height, the
   two became flex items competing for a narrow row, flex handed them EQUAL
   widths, and the height ratio silently went from the 1.23 that was chosen to
   the 1.33 that falls out of the two photographs' aspects. An overlap in vh
   drifted the other way, reaching 28% on a phone while claiming 18%.

   Fixed proportions instead. From heights of 34 and 42 and the photographs'
   aspects (1400/1085 and 1400/1444), the widths are 43.9 and 40.7, and 18% of
   the Codex case is 7.33; over a total of 77.3 those are the percentages below.
   The vh term in the width caps how tall the pair can get on a short screen,
   which is what the max-heights used to do. */
.shot--pair{
  display:flex; align-items:flex-end; justify-content:center;
  width:min(100%, 940px, 77.3vh);
}
.shot--pair .shot__bob{ flex:none; width:56.8%; }
/* Taller than the octopus, not smaller. The Codex case spends none of its height on
   legs, so this is nothing like a 23% bigger product - it reads as the two
   being about the same size, with the Codex case the rounder of them. */
.shot--pair .shot__bob--b{ width:52.6%; margin-left:-9.5%; }
.shot--pair img{ width:100%; max-height:none; }

/* The octopus in front, and the Codex case dropped below the shared ground line.
   Both are deliberate: flex-end puts their feet on one line and the -5%
   takes the Codex case back off it. */
.shot--pair .shot__bob{ position:relative; z-index:2; }
.shot--pair .shot__bob--b{ z-index:1; }

/* Two breaths, on periods that do not divide into each other, so the pair
   never settles into a pattern: 8s against 9.4s comes back into step once
   every 62 seconds, and nobody watches a hero that long.

   Vertical only, and both amplitudes small. That is what keeps them from
   looking like they are bumping: the seam between them is a vertical edge, so
   moving one up and the other down slides the silhouettes past each other
   rather than driving them together. Nothing here changes the overlap. */
.shot--pair .shot__bob{
  animation-duration:6.5s;
  transform:translate3d(0, var(--fy, 0px), 0);
}
.shot--pair .shot__bob--b{
  animation-name:bobBack; animation-duration:7.9s; animation-delay:-2.6s;
  transform:translate3d(0, calc(5% + var(--fy, 0px)), 0);
}
/* Two thirds of the front one's travel. Equal amplitudes read as two objects
   on the same plane; this is the parallax that says one is further away, and
   it is doing the work the depth setting is switched off for. */
@keyframes bobBack{
  from{ --fy:8px; }
  to  { --fy:-9px; }
}
/* The photograph leans toward the pointer. app.js sets --tx/--ty; the eased
   transition is what makes it feel like weight rather than tracking.

   It also breathes on its own, which is the half that does not need a mouse:
   on a phone there is no pointer at all, and on a desktop the object was
   inert until someone happened to move across it. Registering --fy is what
   makes that animatable - a plain custom property cannot be interpolated, so
   the keyframes would snap between the two ends instead of easing. Browsers
   without @property fall through the var() default and simply do not bob. */
@property --fy{ syntax:'<length>'; inherits:false; initial-value:0px; }

/* The bob and the lean are on SEPARATE elements, and that is the whole point
   of .shot__bob existing. Together on one element they were one `transform`
   with a `transition` on it, and the animation rewrote that transform every
   frame -- so every frame started a new half-second ease toward a target that
   had already moved. It read exactly as reported: laggy, and jumpy when the
   pointer moved through it. Nothing eases the bob now; only the lean does. */
.shot__bob{
  transform:translate3d(0, var(--fy, 0px), 0);
  animation:bob 5s ease-in-out infinite alternate;
  will-change:transform;
}
/* Off the beat. In step, two bobbing objects read as one image scrolling. */
.shot__bob--b{ animation-duration:6.2s; animation-delay:-2.6s; }

.shot__frame{
  transform:translate3d(var(--tx, 0px), var(--ty, 0px), 0);
  transition:transform .32s cubic-bezier(.22,.8,.28,1);
  will-change:transform;
}
/* Twenty pixels over five seconds. The first pass moved nine and read as
   nothing at all -- at that amplitude a slow ease is indistinguishable from a
   still image unless you happen to be staring at one edge. */
/* 26px of travel. It has been 9, then 20, then 12 -- 12 was the pair reading
   as almost static. At this size the objects are around 300px tall, so this
   is roughly 8% of them: enough to see without looking like they are being
   shaken. */
@keyframes bob{
  from{ --fy:12px; }
  to  { --fy:-14px; }
}
/* 26px of travel is a breath beside a column of text and a lurch on a phone,
   where the panel is most of what you can see and there is nothing beside it
   to be still. Same motion, a third of the distance. */
@media (max-width:860px){
  @keyframes bob{
    from{ --fy:4px; }
    to  { --fy:-5px; }
  }
}
/* A cast shadow is invisible on a dark ground and a mirrored reflection just
   looked like a second set of legs. A pool of light under the feet grounds it
   without adding anything to count. */
.shot::after{
  content:""; position:absolute; z-index:-1;
  left:50%; bottom:-4%; width:94%; height:24%;
  transform:translateX(-50%);
  background:radial-gradient(50% 50% at 50% 50%, rgba(232,135,58,.30), transparent 72%);
  filter:blur(6px);
  pointer-events:none;
  /* In step with the bob, and opposed to it: the pool tightens as the object
     comes down. Without this the light stayed put and the device read as
     sliding up and down a wall rather than lifting off a desk. */
  /* On the front object's clock, so the light tightens as it comes down and
     spreads as it lifts. On its own timing the shadow contradicted the thing
     casting it, which is worse than no shadow at all. */
  animation:pool 6.5s ease-in-out infinite alternate;
}
@keyframes pool{
  from{ opacity:1;   transform:translateX(-50%) scale(1); }
  to  { opacity:.58; transform:translateX(-50%) scale(.86); }
}

/* ── sections ─────────────────────────────────────────── */

.section{ padding:clamp(3.5rem,7vw,6rem) 0; }

/* A quieter ground, so two sections in a row do not run into each other. The
   page had no band before because no two text sections were ever adjacent;
   splitting the answers in half made a pair. */
.section--band{
  background:var(--bg-2);
  border-top:1px solid var(--hair-2);
  border-bottom:1px solid var(--hair-2);
}

/* ── the scroll reel ──────────────────────────────────────
   The device and its caption live in one pinned block so they can never
   drift apart while you scroll; the captions cross-fade in place. */

.reel{ position:relative; }

/* the scroll distance the pin travels through */
/* Six stops, six equal shares. They were weighted to give
   the boot animation room, and unequal stops are felt rather than noticed:
   the reel reads as arbitrary when one lasts a third longer than its
   neighbour for a reason nobody scrolling can see. The boot gets its time
   from the dwell floor in app.js instead, which every stop shares. */
.reel__track{ height:calc(6 * var(--stop)); }

.reel__pin{
  position:sticky; top:0;
  /* svh, NOT dvh. dvh is the viewport as it is RIGHT NOW, and on a phone that
     changes the moment the URL bar starts collapsing -- so a pinned block
     sized in it resizes mid-scroll and everything inside it jumps, every
     time, on every scroll. svh is the viewport with the browser chrome
     showing: it never changes while you scroll, and a block that fits it fits
     whether the bar is there or not. */
  height:100vh; height:100svh;
  display:flex; flex-direction:column; justify-content:center;
  padding-top:var(--navh, 4.5rem);
  padding-bottom:1.2rem;
}
.reel__inner{
  width:min(100% - clamp(1.25rem,3vw,3rem), 1800px); margin-inline:auto;
  display:grid;
  /* the model takes the larger share: it is the thing being explained, and the
     caption only has to sit beside the part it is talking about */
  /* The caption column is capped rather than proportional: past about 34rem
     the copy stops sitting beside the model and starts drifting away from it,
     which is the whole point of pinning the two together. The model takes
     whatever is left, which on a wide screen is most of it. */
  grid-template-columns:minmax(0,1fr) minmax(0,min(34rem, 34vw));
  gap:clamp(1.5rem,3vw,3rem);
  align-items:center;
  flex:1; min-height:0;
}
.reel__stage{
  position:relative;
  height:min(100%, 820px);
  border-radius:var(--r);
  background:
    radial-gradient(58% 54% at 50% 42%, rgba(232,135,58,.10), transparent 68%),
    radial-gradient(70% 60% at 50% 46%, #191B23, var(--bg) 76%);
  border:1px solid var(--hair-2);
  cursor:grab; overflow:hidden;
}
.reel__stage:active{ cursor:grabbing; }
.reel__stage > img{
  position:absolute; inset:0; margin:auto; max-height:84%; width:auto;
}

/* every caption in the same grid cell, so the box is as tall as the tallest
   and nothing jumps as they swap */
.reel__caps{ display:grid; }

/* The captions travel 2.6rem sideways -- the four that are waiting sit out at
   +2.6rem, and the one arriving is somewhere in between -- and a transform
   still counts toward the scrollable area. On a 390px phone that made the
   document 426 wide and draggable sideways for no reason;  at 1440 it made it
   1465. body{overflow-x:clip} hid it and left it there.

   The clip goes on the SECTION, not on the caption box. On the box, the only
   way to keep the sliding caption from being cut mid-travel was an
   overflow-clip-margin, and that margin is itself part of the overflow region
   -- it moved the number rather than removing it. The section is as wide as
   the window, so there is room for the whole travel inside it and nothing is
   ever visibly cut.

   overflow-x:clip, not hidden. `clip` does not make a scroll container, so
   the sticky pin inside still sticks to the window rather than to this
   element -- with `hidden` the whole reel would stop pinning. */
.reel{ overflow-x:clip; }
/* Out first, then in. A symmetrical half-second cross-fade left both captions
   legible on top of each other for most of it -- and the device behind them
   swaps its screen the instant the threshold is crossed, so the overlap read
   as "Two dials, and you are done reading" over a Settings panel. Fading the
   old one out in 180ms closes that window before anyone can read the wrong
   pairing. */
/* Sideways, not up. The reel is a tour of one object now -- the whole thing,
   then the left gauge, then the right, then the control at the bottom -- and
   a caption that rises reads as a new page, while one that slides reads as
   the next stop. It travels the way the camera does.

   Still out first and in after: a symmetrical cross-fade left both captions
   legible on top of each other for most of it. */
.reel__cap{
  grid-area:1 / 1;
  opacity:0; transform:translateX(-2.6rem);
  /* ONE rhythm, and everything in the reel is tuned to it: the words leave in
     100ms, the next ones are fully in by 360, and the camera, the screen and
     the session number in app.js are set to land inside that same window. The
     tempo problem was never one duration -- it was four of them. The text
     finished at 540, the panel changed at 460 and the camera was still easing
     at 1300, so each stop arrived in instalments.

     460 once, and it was the reason a stop could not change more often than
     every 700ms -- see MIN_DWELL. One gesture now moves exactly one stop, and
     a page that moves in 300ms while its words take 460 to catch up reads as
     lag, so the whole change was tightened to land just after the scroll.

     The floor on how long a stop is held (MIN_DWELL, app.js) has to stay
     above the 360: a caption must be able to finish arriving before anything
     asks it to leave, or it is caught part-way in while its predecessor is
     still part-way out -- two texts at half opacity, which is what the
     "leftovers" were. */
  transition:opacity .1s ease-in, transform .1s ease-in;
  pointer-events:none;
}
/* The one arriving comes FROM the right; the one leaving goes to the left. A
   single class cannot hold both directions, so the arriving state resets the
   transform and the leaving state keeps the -2.2rem above: everything enters
   from +2.2 and exits to -2.2, which is one continuous rightward-to-leftward
   travel across the whole reel. */
.reel__cap.is-next{ transform:translateX(2.6rem); }
.reel__cap.is-on{
  opacity:1; transform:none; pointer-events:auto;
  /* Opacity lands first and the travel finishes just after, so the words are
     readable while the last of the movement settles rather than sliding into
     legibility. */
  transition:opacity .18s ease-out .1s,
             transform .26s cubic-bezier(.22,.85,.3,1) .1s;
}

/* The words change sides; the stage does not move.
   -------------------------------------------------------------
   Swapping the two columns moved BOTH of them -- the stage jumped from one
   side of the block to the other at every change of side, and no amount of
   easing the caption hides a hard cut on the object itself.

   Three columns instead, the stage always in the middle one. The caption
   takes an outer column and the opposite column is left empty, which costs a
   caption's width of stage and buys an object that never jumps.

   A SIDE BELONGS TO A CAPTION, not to the reel. Moving the container between
   column 3 and column 1 moved every caption inside it, including the one on
   its way out -- which was still fully opaque at that instant, because the
   swap was applied the moment the stop changed and only the ARRIVING caption
   is transparent then. So each change of side threw the visible words across
   the screen in a single frame and then faded them. That hard jump is what
   read as a lag going from one side to the other.

   The layer spans the whole block now and each caption justifies itself to
   its own side, which it keeps for its whole life. Nothing is ever moved
   while it can be seen.

   Only above 1200: below that, three columns leave the stage too narrow to be
   worth looking at, and the caption stays on the right. */
@media (min-width:1200px){
  .reel__inner{
    grid-template-columns:
      minmax(0, min(28rem, 27vw)) minmax(0, 1fr) minmax(0, min(28rem, 27vw));
  }
  .reel__stage{ grid-column:2; grid-row:1; }
  /* Full width, and transparent to the pointer: an empty container over the
     middle column would otherwise swallow the drag that turns the model. */
  .reel__caps{ grid-column:1 / -1; grid-row:1; pointer-events:none; }
  .reel__cap{ width:min(28rem, 27vw); justify-self:end; }
  .reel__cap[data-place="left"]{ justify-self:start; }
}

.reel__cap .h2{ max-width:15ch; }

/* ── trust ────────────────────────────────────────────── */

/* a tick, not an empty box: these are things the device does not do, and an
   unchecked checkbox beside each one said the opposite */

/* ── the setup reel ───────────────────────────────────────
   Same shape as the product reel: a pinned block with a track under it, and
   one thing changing as you scroll. Here it is the panel itself, drawn by
   device.js -- the boot you watch is the boot the hardware does. */

.setup{ position:relative; }
/* Three steps, at the same pace per step as the reel above. Step 2 is the
   only one with something to watch -- CONNECTING runs its two segments and
   lands on Ready -- so app.js gives it half of this track rather than a
   third. */
.setup__track{ height:calc(3 * var(--stop)); }

.setup__pin{
  position:sticky; top:0;
  /* svh, NOT dvh. dvh is the viewport as it is RIGHT NOW, and on a phone that
     changes the moment the URL bar starts collapsing -- so a pinned block
     sized in it resizes mid-scroll and everything inside it jumps, every
     time, on every scroll. svh is the viewport with the browser chrome
     showing: it never changes while you scroll, and a block that fits it fits
     whether the bar is there or not. */
  height:100vh; height:100svh;
  display:flex; flex-direction:column; justify-content:center;
  padding-top:var(--navh, 4.5rem);
  padding-bottom:1.2rem;
}
.setup__inner{
  width:min(100% - clamp(1.25rem,3vw,3rem), 1500px); margin-inline:auto;
  display:grid;
  /* The steps column carries a 79-character download URL. At 26rem it was
     breaking that URL at whatever character the edge fell on -- three ragged
     lines of a thing that is meant to be read as one token. Wide enough for
     the URL to sit on a line of its own, and the panel still has 600px-odd to
     draw a 320x240 screen in, which is plenty. */
  grid-template-columns:minmax(0,1fr) minmax(0,min(48rem, 50vw));
  gap:clamp(1.5rem,4vw,4.5rem);
  align-items:center;
  flex:1; min-height:0;
}
.setup__stage{ min-width:0; }
/* the panel, in a bezel, with a plate that lifts as it boots */
.panel{
  position:relative; margin-top:clamp(1.4rem,3vw,2.2rem);
  width:min(100%, 780px); aspect-ratio:320 / 240;
  border-radius:14px; overflow:hidden;
  background:var(--screen);
  border:1px solid var(--hair);
  box-shadow:0 30px 70px -45px rgba(0,0,0,.9);
}
/* height:AUTO, not 100%. The bitmap attach() makes is always 320x240 -- it
   sets canvas.height from canvas.width -- so stretching the element to fill a
   box of any other shape squashes the drawing, and on a phone the panel was
   not 4:3, which is why the dials came out as tall ovals. Left to auto, the
   element takes the bitmap's own ratio and the picture cannot be distorted by
   whatever the box around it is doing. */
.panel__canvas{ display:block; width:100%; height:auto; }
/* A screen that is off is still glass, and glass catches the room. Painted
   flat, this layer was a 780px hole in the middle of the section and read as
   an image that had failed to load rather than a device waiting to be set up.
   The sheen and the vignette are the whole fix: no invented firmware, nothing
   drawn that the panel does not do, just the surface it actually has. */
.panel__off{
  position:absolute; inset:0; overflow:hidden;
  background:
    radial-gradient(115% 85% at 50% 38%,
      color-mix(in srgb, var(--screen) 88%, #FFF 12%) 0%,
      var(--screen) 68%),
    var(--screen);
  opacity:0; transition:opacity .55s ease;
  pointer-events:none;
}
/* The highlight itself, drifting slowly enough to read as a room rather than
   an animation. Wider than the panel so neither edge is ever a hard line. */
.panel__off::after{
  content:''; position:absolute; top:-40%; bottom:-40%; left:-60%; width:120%;
  background:linear-gradient(104deg,
    rgba(255,255,255,0)    0%,
    rgba(255,255,255,.055) 42%,
    rgba(255,255,255,.075) 50%,
    rgba(255,255,255,.055) 58%,
    rgba(255,255,255,0)    100%);
  animation:sheen 14s ease-in-out infinite alternate;
}
@keyframes sheen{
  from{ transform:translateX(-8%)  rotate(-3deg); }
  to  { transform:translateX(26%)  rotate(-3deg); }
}
/* Fully opaque. At .94 the dials ghosted through, which reads as a screen
   that is on and dim rather than one that has not been set up yet -- and it
   spoils the moment where it wakes. */
.panel[data-state="off"] .panel__off{ opacity:1; }

/* The CLI switcher. Back in the page, but in the reel rather than the nav:
   it changes what the panel is showing, so it belongs beside the panel. The
   thumb is a separate element that app.js measures and moves, because
   animating a background across two buttons cannot round its own ends. */
.seg{
  position:relative; display:flex; width:max-content;
  margin:clamp(.9rem,2vw,1.4rem) auto 0; padding:3px;
  background:var(--bg-2); border:1px solid var(--hair); border-radius:100px;
}
.seg__thumb{
  position:absolute; left:3px; top:3px; bottom:3px; width:0;
  background:var(--card-2); border-radius:100px;
  transition:transform .3s cubic-bezier(.2,.8,.3,1), width .3s cubic-bezier(.2,.8,.3,1);
  pointer-events:none;
}
.seg__opt{
  position:relative;               /* over the thumb */
  appearance:none; cursor:pointer;
  min-height:40px; padding:.35rem 1rem;
  background:none; border:0; border-radius:100px;
  font:inherit; font-size:.86rem; font-weight:500; color:var(--ink-3);
  transition:color .2s; touch-action:manipulation;
}
.seg__opt:hover{ color:var(--ink-2); }
.seg__opt.is-on{ color:var(--ink); }

/* The same breath as the hero, on the two sections that explain the product.
   Slower than the hero's 6.5s: those two objects are the page introducing
   itself, these are being read alongside words, and something moving at the
   same rate beside a paragraph pulls the eye off it. */
.reel__stage, .panel{
  transform:translate3d(0, var(--fy, 0px), 0);
  animation:bob 9s ease-in-out infinite alternate;
  will-change:transform;
}
.panel{ animation-duration:10.4s; animation-delay:-2.2s; }

/* ── the steps ────────────────────────────────────────── */

/* The gap is a budget, not just a rhythm. All three steps stand in a box one
   viewport tall, and when they do not fit, fitSetup unpins the section --
   which costs the live panel its whole animation. 2.1rem left seven pixels of
   slack on a 1440x900 laptop, which is no slack at all: a font that falls
   back, a browser zoomed one notch, and the section stops being pinned. */
.steps{ list-style:none; margin:0; padding:0; display:grid; gap:1.45rem; min-width:0; }
.step{
  display:grid; grid-template-columns:2.4rem minmax(0,1fr);
  column-gap:.5rem; row-gap:.6rem;
  min-width:0;
  opacity:.32; transition:opacity .28s ease;
}
.step.is-on{ opacity:1; }
/* AND WHENEVER FOCUS IS INSIDE ONE.
   Above 899px all three steps are on screen and the two that are not current
   sit at .32 -- which was fine while they held only text. Step 03 now has a
   four-tab tablist and a copy button in it, so a keyboard reader tabs into a
   region rendering at about 1.6:1 against the background, and the focus ring
   is dimmed with it (opacity makes a stacking context, so the ring cannot
   escape): they cannot see where they are or read what they landed on. The
   narrow branch never had this because it hides the inactive steps outright
   and takes them out of the tab order with them.

   Raising the dim floor would have to go most of the way to 1 to reach 4.5:1,
   which is the whole visual idea gone. Lighting the step the reader is
   actually in costs nothing and is truer: focus IS attention. */
.step:focus-within{ opacity:1; }
.step > :not(.step__n){ grid-column:2; }
.step__n{
  grid-row:1 / -1; align-self:start;
  margin:0; padding-top:.18rem;
  font-family:var(--mono); font-size:.76rem; letter-spacing:.1em;
  color:var(--orange);
}
.step__h{
  margin:0; font-family:var(--display); font-weight:600;
  font-size:1.28rem; line-height:1.25; color:var(--ink);
}
.step__p{ margin:0; font-size:1rem; line-height:1.6; color:var(--ink-2); }

/* One step quieter than .step__p, for the lines that are a footnote to a step
   rather than part of it: the security dialog someone only meets if they took
   the other route, the version floor, the privacy sentence. Set at the same
   weight they would read as instructions, and every reader would have to
   decide whether each one applied to them. */
/* NO MEASURE ON THIS ONE, deliberately. Everything else in the column is
   held to a reading width, but this sits inside a pinned box whose height is
   the viewport: a line that wraps twice costs 48px of a budget that decides
   whether the section can be pinned at all, and losing the live panel to a
   footnote is a bad trade. The column is already narrow enough to read. */
.step__note{
  margin:.5rem 0 0;
  font-size:.84rem; line-height:1.5; color:var(--ink-3);
}
.step__note em{ font-style:normal; color:var(--ink-2); }
/* The link is the one thing in this note anybody is looking for, so it comes
   up to the body ink while the sentence around it stays quiet. */
.step__note a{ color:var(--ink-2); text-underline-offset:.18em; }
.step__note a:hover{ color:var(--ink); }

/* the one command anyone types, so it copies on click */
.code{
  display:block; width:100%; margin:.15rem 0 0; padding:.7rem .9rem;
  background:var(--bg); border:1px solid var(--hair-2); border-radius:9px;
  min-width:0; overflow-x:auto; text-align:left;
  font-family:var(--mono); font-size:.88rem; line-height:1.7;
  color:var(--ink);
}
/* pre-WRAP, not pre. The download URL is about a hundred characters, and
   under `pre` it ran off into the block's own horizontal scroll -- inside a
   <button> whose job is to copy, where a sideways drag is an odd thing to ask
   for and there is no mark saying the line continues. Wrapping is a display
   choice only: the newline between the two commands is still honoured, and
   textContent, which is what lands on the clipboard, is untouched. */
.code code{ font:inherit; white-space:pre-wrap; overflow-wrap:anywhere; }
.code--copy{
  position:relative; cursor:pointer;
  transition:border-color .2s, background-color .2s;
}

/* THE CONTROL IS THIS, NOT THE BLOCK.
   ------------------------------------
   The whole block used to BE the <button>, with the command and the live
   region inside it. ARIA's children-presentational rule says a button's
   descendants have their roles stripped and its name comes from the label
   alone, so the two things that mattered most were the two that vanished:
   a screen reader heard "Copy the setup command for macOS, button" and could
   not read one character of the curl line it was about to run -- a fair thing
   to want to check before executing a downloaded binary, and there is no
   other copy of that text on the page -- and the aria-live region never
   announced, so when the clipboard is refused (denied permission, or an
   insecure origin) the "Press Cmd-C" fallback was silent and the reader
   pasted nothing.

   So the button is a transparent overlay and the command is its sibling.
   Nothing moves, the whole block is still one click target, and the code and
   the status are back in the accessibility tree where they belong. */
.code__hit{
  position:absolute; inset:0;
  width:100%; margin:0; padding:0;
  border:0; background:none;
  border-radius:inherit;
  cursor:pointer;
  touch-action:manipulation;
}
.code--copy:hover{ border-color:var(--ink-3); }
/* Empty until you press it. The button wore the word "Copy" at rest, which
   is a label on a block of shell script that is already obviously a thing you
   copy. What it is NOT obvious about is whether the press worked, so that is
   the only thing it ever says now. */
/* Pinned to the FIRST LINE, not to the middle of the block. top:50% was
   right when every command was two short lines and 50% fell in the gap
   between them; the install blocks are three lines now, so the middle landed
   inside line two -- the download URL, which is deliberately allowed to run
   the full width of the column -- and pressing Copy printed COPIED across the
   end of it. Every one of these blocks has a short first line. */
.code__hint{
  position:absolute; right:.7rem; top:.7rem;
  font-size:.68rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--ink-3);
}
/* Not display:none when empty. It is an aria-live region, and a region that
   is out of the accessibility tree until the moment text lands in it is the
   classic way to make a live announcement not happen. Empty and absolutely
   positioned, it already draws nothing. */
.code--copy.is-copied{ border-color:var(--orange); }
.code--copy.is-copied .code__hint{ color:var(--orange); }
/* Platform tabs. Deliberately not pills: a pill row beside an orange button
   reads as a second call to action. These are labels with a rule under the
   live one, which is what a tab is. */
.os{ display:flex; gap:.2rem; margin:.4rem 0 .1rem; flex-wrap:wrap; }
.os__tab{
  appearance:none; background:none; border:0; cursor:pointer;
  padding:.5rem .7rem; min-height:44px;
  font:inherit; font-size:.86rem; color:var(--ink-3);
  border-bottom:2px solid transparent;
  transition:color .2s, border-color .2s;
  touch-action:manipulation;
}
.os__tab:hover{ color:var(--ink-2); }
.os__tab.is-on{ color:var(--ink); border-bottom-color:var(--orange); }
/* One cell, four panels. `display:none` on the inactive ones meant the step
   was as tall as whichever tab you had chosen, so switching macOS to Windows
   moved everything below it. Stacked, the cell is as tall as the longest and
   the switch costs no movement at all.

   [data-off] rather than [hidden]: hidden implies display:none, which is what
   collapsed the box in the first place. `inert` is what actually takes the
   inactive panels out of the tab order and the accessibility tree, and app.js
   sets it alongside this attribute. */
.os__panels{ display:grid; }
.os__panels > *{ grid-area:1 / 1; min-width:0; }
.os__panel[data-off], .os__cmd[data-off]{
  visibility:hidden; opacity:0; pointer-events:none;
}

/* Step 03 used to carry a LABEL here saying which computer its command was
   for, because the choice was made once, in step 02. It has its own tabs now:
   a reader who arrives at step 03 from the install link, or comes back to it a
   week later, should not have to scroll up to a control to change what they
   are being shown. Two tab sets, one choice - see show() in app.js. */

/* The commands are the one place on the page where a line breaking in the
   wrong spot changes what it means, so they get a size that fits. The longest
   line is the Windows URL at 86 characters, and the root here is 18px, not
   16 -- .76rem came out four tenths of a pixel too wide and wrapped. */
.os__panel .code, .os__cmd .code{ font-size:.72rem; }
.step__p--lead{ color:var(--ink); margin-bottom:.35rem; }

.step__p code{
  font-family:var(--mono); font-size:.86em;
  color:var(--ink); background:var(--bg);
  border-radius:5px; padding:.08em .36em;
}

/* What arrives, and what you bring.
   -------------------------------------------------------------
   Four attempts at this section, and each one failed on the same thing: the
   material was being grouped by where it came from rather than by what the
   reader is trying to work out.

   First the spec and the questions shared one grammar, on the theory that a
   fact is a question already answered -- thirteen identical rows, and the eye
   slid off the wall. Splitting the questions into their own section fixed
   that half. Then the spec became two panels, "We ship" and "You bring", and
   the symmetry between them was imposed rather than real: only ONE of the
   four rows on the left was about what arrives. The rest were the case
   dimensions, which the drawing above already showed, and how it is powered,
   which is a fact about the object. It was also the only place on the page
   where a company speaks; everything else talks about the thing.

   One list, and where each row comes from is a COLUMN. The distinction has to
   survive -- it is the one thing a buyer must not misread -- but a column can
   carry it without a wall, and reading down the right is one movement instead
   of two passes across a gutter. */

/* Pinned, like the reel and the setup.
   -------------------------------------------------------------
   These two were the only sections left that just scrolled past, and after
   two blocks that hold still and advance, a page that goes back to ordinary
   scrolling reads as having run out rather than finished.

   A SHORTER stop than the reel's. A stop there is a camera move across an
   object; a stop here is one row lighting up, which is over in a fraction of
   that, and six stops at the reel's rate would be four screens of scrolling
   for six one-line rows. Short enough that the section does not drag, long
   enough that an idle scroll does not take a stop -- and it has no dwell
   floor the way the reel does. */
/* The box steps faster than the reel, because a row of a list is not a
   paragraph of narration -- but it is no longer a FRACTION of the reel's
   stop. That chain was there so the two could not drift apart, and it has
   outlived its use: the reel has just been retuned for a reason that does not
   apply here (see --stop), and following it would have dragged the box to
   60vh a row for six one-line rows.

   55vh, up from 44. The same complaint reaches this section more gently --
   what a stop shows is smaller -- so it moves less far. */
:root{ --stop-s: 55vh; }

/* THE BOX IS AN ORDINARY BLOCK NOW.
   It had a track of six stops and a sticky pin, and the scroll lit one row
   at a time. So did the FAQ before it, with eight. Both are lists, and a list
   is read by scanning -- dimming five sixths of it so the sixth feels
   arrived-at is the pattern working against the content, and it cost three
   screens of scrolling in which the page did not move. The reel keeps its
   stops: a tour of a screen you cannot hold really is a sequence. */
.kit__body{ padding-block:clamp(3.5rem, 8vw, 6.5rem); }

.kit__inner{
  width:min(100% - clamp(1.25rem,3vw,3rem), var(--wrap)); margin-inline:auto;
  display:grid; gap:clamp(1.6rem, 3vw, 3rem);
  align-items:center;
  min-height:0;
}
@media (min-width:900px){
  .kit__inner{ grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr); }
}
.kit__stage{ min-width:0; }
.kit__stage .h2{ margin-bottom:clamp(1rem,2vw,1.6rem); }

.qs__body{ padding-block:clamp(3.5rem, 8vw, 6.5rem); }
.qs__inner{
  width:min(100% - clamp(1.25rem,3vw,3rem), var(--wrap)); margin-inline:auto;
}

/* No dimming left: every row is at full strength, because nothing is
   choosing one for you any more. */

/* One column, and one row at a time.
   -------------------------------------------------------------
   Below the two-column breakpoint the drawing and the six rows are 588 and
   579 pixels of content in a 744 pixel pin -- they do not both fit, and the
   grid answered by letting the list run out of the bottom of the block. The
   setup section hit this exact wall with its three steps and solved it by
   stacking them in one cell; this does the same, so both pinned blocks
   behave alike on a phone.

   Opacity alone, no visibility:hidden. Nothing in a row is focusable, so
   there is no tab order to protect, and leaving them in the flow keeps all
   five findable with Cmd-F -- which is what makes hiding four of them
   acceptable in the first place.

   align-items:start, or the cell would stretch every row to the height of the
   tallest and the short ones would carry a hundred pixels of nothing. */
/* On one column the drawing takes whatever is LEFT, rather than a size of its
   own. Width-driven it was 584px of stage on every phone, which fits an 844
   viewport by 8px and overflows a 667 one by 169 -- the drawing does not know
   how tall the screen is, and a pinned block is exactly the place that has to.

   Flex with min-height:0 on the svg hands it the leftover, and the viewBox's
   preserveAspectRatio letterboxes the drawing inside whatever box it gets, so
   it shrinks correctly instead of squashing. Everything under it is flex:none
   -- the small print keeps its size and the picture gives way. */
@media (max-width:899px){
  .kit__inner{ display:flex; flex-direction:column; min-height:0; }
  .kit__stage{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; }
  .kit__stage .h2{ flex:none; }
  .scale{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; }
  .scale__svg{ flex:1 1 auto; min-height:0; height:auto; }
  .scale__seg, .scale__cap{ flex:none; }
  .needs{ flex:none; }
}

/* Nothing to stack any more: the box is not pinned, so the rows run down the
   page like any other list and the phone gets the same three groups the
   desktop does, one under another. The block this replaces squeezed all six
   rows into one grid cell so a pinned box could stay one row tall. */

/* The drawing's caption stack is four things -- switcher, legend, readout,
   note -- and at phone sizes it was 220px under a 264px drawing, which is
   most of a pinned block spent on the small print of a picture. */
@media (max-width:620px){
  .seg{ margin-top:.7rem; }
  .scale__cap{ margin-top:.6rem; }
  .legend{ font-size:.78rem; gap:.25rem 1rem; margin-bottom:.4rem; }
  .scale__read{ font-size:.86rem; }
  .scale__note{ font-size:.72rem; margin-top:.2rem; }
}

/* How wide each of the two closing sections runs.
   -------------------------------------------------------------
   It was one shared 1180px for both, on the reasoning that they should close
   the page as a single block. Against a 1560 wrap that left 380px of dead
   space down the right of each -- space the nav, the hero and the reel do not
   have -- and the end of the page read as shoved to one side.

   They want different widths, it turns out, because they are different
   material. The packing list is a table of short rows and takes the page's
   full width. The QUESTIONS do not: once the verdicts came off the closed
   rows there is nothing in the right half of the line to align to, and a
   question stranded at one end of a metre of rule reads worse than a narrow
   column. That one is prose, so it gets a prose measure -- and centred, so
   the narrower block reads as a deliberately quieter section rather than one
   that failed to reach the edge.

   (This declaration went missing once already: it lived inside the two-panel
   block that the one-list rewrite replaced wholesale, and every
   max-width:var(--measure) below silently resolved to none. The sections
   looked right by accident. It sits on its own now.) */
.kit{ --measure:100%; }
.qs { --measure:52rem; }


/* The scale drawing.
   -------------------------------------------------------------
   Every size claim on this page is a sentence, and a sentence is the one
   thing that cannot answer "how big is that on my desk". The viewBox is in
   centimetres so the drawing cannot drift from the numbers in the row below
   it: 12.5 by 9.5 is written as 12.5 by 9.5.

   Aspect is fixed at 78x52cm by the tallest thing it has to hold -- a 32-inch
   panel ten centimetres off the desk -- so the figure is capped by width
   rather than stretched to the column. */
/* width:100% is not redundant. A grid item stretches to its area only while
   every margin is definite -- give it `margin-inline:auto` and the auto
   margins absorb the free space instead, the stretch is off, and the box
   falls back to fit-content. That is how centring the figure silently shrank
   the drawing from 640px to 374. An explicit width gives the margins
   something to centre. */
/* Big, because it is a scale drawing and the two cases are small inside it
   -- at 640 the Codex case was 110px across and its lobes were mush. The svh cap
   is what keeps it from pushing the rows out of a pinned block on a short
   screen: at 78x56 the figure is 0.72 as tall as it is wide. */
.scale{ width:100%; max-width:min(920px, 96svh); margin-inline:auto; }
.scale__svg{ display:block; width:100%; height:auto; }

.scale__desk{ stroke:var(--hair); stroke-width:.22; }

/* An unfilled rectangle half a metre wide is a hole in the page, not a
   monitor. The screen takes a fill darker than the band it sits on, so it
   reads as a screen that is switched off, and a bezel around it so the shape
   is a monitor rather than an outline.

   The BEZEL is the body and the PANEL is the screen -- the diagonal a monitor
   is sold by measures the screen, so that is the rect the size switcher
   drives, and the body is correctly a little larger than the number. */
.scale__bezel{ fill:var(--hair-2); }
.scale__panel{ fill:var(--bg); stroke:var(--hair); stroke-width:.18; }
.scale__neck, .scale__base{ fill:var(--hair-2); stroke:none; }
.scale__size{
  fill:var(--ink-3); font-family:var(--mono); font-size:2.4px;
  letter-spacing:.12px; text-anchor:middle;
}
/* SVG geometry properties animate like any other. The monitor grows and
   shrinks around a device that does not move, which is the comparison. */
@media (prefers-reduced-motion: no-preference){
  .scale__panel, .scale__bezel, .scale__size{
    transition:x .4s cubic-bezier(.2,.7,.3,1), y .4s cubic-bezier(.2,.7,.3,1),
               width .4s cubic-bezier(.2,.7,.3,1), height .4s cubic-bezier(.2,.7,.3,1);
  }
}

/* Each case in the colour it prints in. The screen is the same 2.8-inch
   panel on both, drawn at the same size on both, because it is the same
   part -- which is the section's other claim made visible. */
.scale__case, .scale__leg{ fill:var(--orange); }
.scale__codex{ fill:#2F63D2; }
.scale__screen{ fill:#0B0D12; }

/* Everything under the drawing centres on it. The switcher and the readout
   were hanging off its left edge while the drawing itself was symmetrical,
   and a left-aligned control under a centred picture is the kind of thing you
   see before you can say what is wrong with it. */

.scale__cap{ margin:.9rem 0 0; text-align:center; }

/* Two entries, each a swatch in the colour its case prints in. The dot is
   what attaches a number to a shape -- without it the reader has to work out
   which of the two figures is 13.4cm wide, which is exactly the job the
   drawing was supposed to be doing for them. */
.legend{
  list-style:none; margin:0 0 .55rem; padding:0;
  display:flex; flex-wrap:wrap; justify-content:center;
  gap:.4rem clamp(1rem, 3vw, 2.2rem);
  font-size:.86rem; line-height:1.5; color:var(--ink-2);
}
.legend li{ display:flex; align-items:center; gap:.5rem; }
.legend__dot{
  flex:none; width:.62rem; height:.62rem; border-radius:3px;
}
.legend__dot--claude{ background:var(--orange); }
.legend__dot--codex { background:#2F63D2; border-radius:50%; }

.scale__read{
  margin:0; font-size:.95rem; line-height:1.6; color:var(--ink-2);
  font-variant-numeric:tabular-nums;
}
.scale__note{
  margin:.3rem 0 0; font-size:.78rem; line-height:1.6; color:var(--ink-3);
}

/* The list. Three columns: what it is, what there is to say about it, and
   where it comes from. The source column is deliberately plain -- two short
   strings in a mono column are already scannable, and the questions section
   below has the page's one pill treatment. */
/* Three groups, and the heading is what a third column used to be. Set as a
   mono eyebrow rather than as a heading-sized heading: it is a label on a
   block, and anything bigger would compete with the h2 the section already
   has. Groups keep their own top and bottom rule, so each one reads as a
   bounded block rather than as one long list with words dropped into it. */
.needs{ margin:0; display:grid; gap:1.5rem; }
.needs__g{ display:grid; }
.needs__list{ margin:0; display:grid; }
.needs__h{
  margin:0 0 .4rem;
  font-family:var(--mono); font-size:.72rem; font-weight:400;
  letter-spacing:.08em; text-transform:uppercase;
  color:var(--ink-3);
}
.needs__row{
  display:grid;
  grid-template-columns:minmax(0, 14rem) minmax(0, 1fr);
  gap:.2rem 1.6rem;
  align-items:baseline;
  padding:1rem 0;
  border-top:1px solid var(--hair-2);
}
.needs__row:last-child{ border-bottom:1px solid var(--hair-2); }

.needs__k{
  font-size:.98rem; font-weight:600; line-height:1.5; color:var(--ink);
}
.needs__v{
  margin:0; max-width:60ch;
  font-size:.95rem; line-height:1.6; color:var(--ink-2);
  font-variant-numeric:tabular-nums;
}
/* Two columns need about 34rem before the detail starts wrapping every four
   words. Under that the name takes its own line and the detail sits beneath
   it.

   There used to be a third column here, repeating "in the box" / "yours" /
   "not supported" against every row, and a phone treatment that turned it
   into a chip beside the name because at that width a right-hand column of
   labels read as unrelated text. The group heading says it once instead. */
@media (max-width:620px){
  .needs__row{
    grid-template-columns:minmax(0,1fr);
    gap:.35rem;
    padding:.85rem 0;
  }
}

/* A pair of cases wherever the product is shown still. The same arrangement
   as the hero -- overlapped, feet on one line, the Codex case taller because it
   spends none of its height on legs -- expressed in shares of the figure so
   it holds at whatever width the figure lands on. Static: these two sit
   inside blocks of text, and the hero has the breathing.

   (Deleted once by a rewrite of the block above this one, which sliced to the
   next section comment and took these with it. If they go missing again, the
   symptom is the two cases stacking vertically.) */
.pair{
  display:flex; align-items:flex-end; justify-content:center;
  margin:0;
}
.pair__a{ width:56.8%; }
.pair__b{ width:52.6%; margin-left:-9.5%; position:relative; z-index:-1; }

/* ── questions ────────────────────────────────────────── */

/* A closed row gives up NOTHING. The verdict used to sit on it -- five short
   answers in an aligned column you could read in one pass -- which made the
   list scannable and also spoiled it: the answer to a question you had not
   asked yet was already on screen. A question you open should still be a
   question, so the verdict went back to the front of its own paragraph where
   it was before, and the row carries the question and nothing else. */

.qs__head, .qs__list{ max-width:var(--measure); margin-inline:auto; }
/* margin-BLOCK. The shorthand `margin:0 0 X` sets all four sides, so writing
   it here reset the auto side margins above and left the heading pinned to
   the page edge while the questions under it sat centred. */
.qs__head{ margin-block:0 clamp(1.6rem, 3vw, 2.4rem); }

/* <details> rather than a hand-rolled accordion: it opens without JavaScript,
   it is a real disclosure widget to a screen reader, and Cmd-F finds the
   answers inside it. */
.qa{ border-top:1px solid var(--hair-2); }
.qa:last-child{ border-bottom:1px solid var(--hair-2); }

.qa summary{
  list-style:none; cursor:pointer;
  display:grid;
  grid-template-columns:minmax(0,1fr) auto;
  align-items:center; gap:1.4rem;
  /* the 44px touch floor, which a one-line question does not reach on its own */
  padding:1rem 0; min-height:44px;
  touch-action:manipulation;
}
.qa summary::-webkit-details-marker{ display:none; }
.qa summary::marker{ content:""; }

.qa__q{
  font-weight:600; font-size:1rem; line-height:1.5; color:var(--ink);
  transition:color .2s ease;
}
.qa summary::after{
  content:""; flex:none; width:9px; height:9px;
  border:solid var(--ink-3); border-width:0 1.8px 1.8px 0;
  transform:rotate(45deg) translate(-2px,-2px);
  transition:transform .25s ease, border-color .25s ease;
}
.qa[open] summary::after{ transform:rotate(-135deg) translate(-3px,-3px); border-color:var(--orange); }
.qa summary:hover .qa__q{ color:var(--orange-2); }

.qa p{
  margin:0 0 1.15rem; max-width:64ch;
  font-size:.95rem; line-height:1.65; color:var(--ink-2);
}

/* REMOVED: a block that shrank the FAQ's type on a short phone. It was there
   because the pin clamped the list -- the longest answer needed 600px and a
   667px phone had 567, so the last two lines of it were simply not there.
   Unpinned, the list is as tall as it needs to be and the page scrolls, so
   there is nothing to make room for. */


/* The answer opens rather than appearing.
   -------------------------------------------------------------
   <details> snaps: the row is one height in one frame and another in the
   next, and the rows below it jump down by however tall the answer is. On a
   list where the whole point is that you open one and read it, that jump is
   the moment the reader loses their place.

   ::details-content and interpolate-size do it with no wrapper element and no
   JavaScript. Where a browser does not have them the answer simply appears,
   which is exactly what happens today -- there is nothing to fall back to.

   The height is what moves; the text fades in behind it, a beat later, so the
   words are not stretching as they arrive. Same shape as the reel: out of the
   way first, then in. */
@media (prefers-reduced-motion: no-preference){
  :root{ interpolate-size:allow-keywords; }

  .qa::details-content{
    block-size:0; overflow:clip;
    opacity:0;
    transition:
      block-size .34s cubic-bezier(.2,.7,.3,1),
      opacity .2s ease,
      content-visibility .34s allow-discrete;
  }
  .qa[open]::details-content{
    block-size:auto;
    opacity:1;
    transition:
      block-size .34s cubic-bezier(.2,.7,.3,1),
      opacity .26s ease .12s,
      content-visibility .34s allow-discrete;
  }
}

/* The whole row is the target, so the whole row takes the hover. The question
   changing colour on its own left the other two thirds of the line inert, and
   on a 1180px row that is most of what the pointer is ever over.

   The summary is already the full width of the list, so the tint needs no
   help to span the row; the inset padding and its matching negative margin
   only give the rounded corners somewhere to sit without moving the text. */
.qa summary{
  margin-inline:-.6rem; padding-inline:.6rem;
  border-radius:9px;
  transition:background-color .2s ease;
}
.qa summary:hover{
  background:color-mix(in srgb, var(--card) 55%, transparent);
}


/* ── the close, and the pre-order page ────────────────── */

/* index.html no longer carries the form. It ends with the offer and a link;
   the order page is where the money conversation happens, with nothing else
   on screen competing for the decision. */
/* .h2 carries max-width:17ch and no auto margin, so inside a centred block
   the box stays left and only its text centres -- which reads as a heading
   nudged off to one side for no reason. Everything centred here says so. */
.close{ position:relative; text-align:center; overflow:clip; }
.close .wrap{ position:relative; z-index:1; }
/* The figure carries the width now, not the image inside it: with two of them
   a `width` on the img was overriding both of the pair's shares and rendering
   them the same size. */
.close__shot{
  margin:0 auto clamp(1.2rem,2.5vw,2rem);
  width:min(82%, 440px);        /* a share of the wrap, not of the window */
}
.close__shot img{ display:block; height:auto; }
.close__h{ max-width:20ch; margin-inline:auto; }
.close__sub{ max-width:38ch; margin-inline:auto; }
.close .btn{ margin-top:1.9rem; }
.btn--big{ min-height:56px; padding:1rem 2.2rem; font-size:1.02rem; }

/* the same warm pool the hero sits the product in, so the page closes the
   way it opened */
.close::before{
  content:""; position:absolute; z-index:0;
  left:50%; top:6%; width:min(120%, 1100px); height:70%;
  transform:translateX(-50%);
  background:radial-gradient(50% 50% at 50% 45%, rgba(232,135,58,.16), transparent 70%);
  pointer-events:none;
}

.section--order{ padding-top:clamp(2.4rem, 6vw, 4.5rem); }

/* The order page is a configurator.
   -------------------------------------------------------------
   It was a photograph beside a form, and the two never spoke: you were shown
   two cases, then asked to choose between them in a pair of small text rows
   with coloured dots. The choice is the only field on the page that changes
   what turns up in the box, so it drives the picture. Pick one and it comes
   forward with its own light; the other steps back.

   No card, either. The page used to sit inside one big rounded slab with a
   shadow, half of it empty around a small floating render -- a centred display
   title over a tinted card is the single most templated shape a page can wear,
   and it was separating this content from nothing. */

/* One measure for the whole page. The nav and the footer stay full width;
   the order itself is a single aligned column set, so both columns and the
   promises start and end on the same two lines. */
.section--order .wrap{ max-width:1280px; }

.buy{
  display:grid;
  gap:clamp(1.8rem, 4vw, 4rem);
  align-items:center;      /* the object floats level with the transaction */
}
@media (min-width:900px){
  .buy{ grid-template-columns:minmax(0,1fr) minmax(0, min(31rem, 46%)); }
}

/* ── the stage ────────────────────────────────────────── */

.buy__stage{ display:grid; justify-items:center; gap:1.1rem; min-width:0; }

/* Same arrangement as the hero -- feet on one line, the Codex case overlapping
   behind -- and the shares come from .pair. The glow is a variable so the
   selected case can light the stage in its own colour. */
.buy__shot{
  --glow: rgba(232,135,58,.15);
  margin:0; position:relative;
  width:min(100%, 620px);
}
.buy__shot::before{
  content:""; position:absolute; inset:-16%; z-index:0;
  background:radial-gradient(50% 50% at 50% 52%, var(--glow), transparent 70%);
  transition:background .5s ease;
}
.buy__shot img{ position:relative; display:block; height:auto; }

/* The one piece of motion on the page, and it is the answer to a question the
   visitor just asked. Origin at the feet, because both cases stand on the
   same line and scaling from the middle would lift them off it. */
.pair__a, .pair__b{
  transform-origin:bottom center;
  transition:opacity .42s ease, transform .42s cubic-bezier(.2,.8,.3,1),
             filter .42s ease;
}
.buy[data-case] .buy__shot{ --glow: rgba(232,135,58,.19); }
.buy[data-case="codex"] .buy__shot{ --glow: rgba(47,99,210,.26); }
.buy[data-case="claude"] .pair__b,
.buy[data-case="codex"]  .pair__a{
  opacity:.34; filter:saturate(.4) brightness(.78); transform:scale(.9);
}
.buy[data-case="claude"] .pair__b{ transform:translateX(6%) scale(.9); }
.buy[data-case="codex"]  .pair__a{ transform:translateX(-6%) scale(.9); }

/* Says in words what the stage says in position and light. */
.stage__cap{
  margin:0; max-width:30ch; text-align:center; text-wrap:balance;
  font-family:var(--mono); font-size:.74rem; line-height:1.6;
  color:var(--ink-3);
}

/* ── the order column ─────────────────────────────────── */

.buy__body{ min-width:0; }

/* The heading used to sit alone above both columns, which put the biggest
   type on the page over the emptiest part of it. It belongs where a product
   name belongs: at the top of the column that names the price and takes the
   order. */
.buy__title{
  font-family:var(--display); font-weight:600;
  font-size:clamp(2rem, 3.6vw, 2.9rem);
  line-height:1.05; letter-spacing:-.028em;
  margin:0 0 .5rem;
}
.buy__lede{
  margin:0 0 1.7rem; max-width:40ch;
  font-size:.98rem; line-height:1.6; color:var(--ink-2);
}
.buy__body > .pre__label{ margin-bottom:.5rem; }

.buy__price{
  display:flex; align-items:baseline; gap:.6rem; flex-wrap:wrap;
  margin:0 0 1.4rem;
}
.buy__amount{
  font-family:var(--display); font-weight:600; font-size:clamp(2.4rem,4.8vw,3.2rem);
  letter-spacing:-.035em; line-height:1;
  font-variant-numeric:tabular-nums;
}
.buy__was{
  font-family:var(--display); font-weight:600; font-size:1.15rem;
  color:var(--ink-3); text-decoration-thickness:1.5px;
  font-variant-numeric:tabular-nums;
}
.buy__plus{ font-size:.9rem; color:var(--ink-3); }
.buy__early{
  margin:-.9rem 0 1.4rem;
  font-family:var(--mono); font-size:.68rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--orange-2);
}

/* REMOVED: .buy__what and .buy__what--note. They were the two paragraphs
   between the price and the button that described what turns up in the box
   and which tools it speaks to. Both are things a buyer looks up rather than
   things that catch them out, so they are behind a fold below the button now
   (see .folds), and the space above the button belongs to the price, the
   shipping date and the choice itself. */

/* REMOVED: .only, the bordered "this is the device, not a plan" paragraph
   that sat between the price and the case picker. Its two facts are the first
   fold below now, under a summary that states the limit rather than filing it
   under "Compatibility". */

/* The date, on its own line and quiet. It is a pre-order, so when it ships is
   a condition of the sale rather than a detail -- but it is one fact, and a
   row in a four-row table was a heavier setting than one fact needs. */
.buy__ships{
  margin:-1.2rem 0 1.6rem;
  font-size:.86rem; color:var(--ink-3);
}

/* THE FOLDS, FULL WIDTH UNDER BOTH COLUMNS.
   -------------------------------------------
   Reusing .qa, the FAQ's disclosure on the other page, rather than drawing a
   second one: the reader has already met that control, and two things that
   open and close the same way should not look different. The rules between
   them come from .qa itself.

   Outside .buy, so they span the page rather than stacking in the narrow
   right-hand column with the whole height of the photograph empty beside
   them. The text inside still gets a measure -- a 1200px line of prose is
   not readable just because there is room for it -- but the control, the
   rule and the hit area are the full width. */
.folds{
  margin-top:clamp(2.5rem, 5vw, 4rem);
  /* Full bleed was too much: a rule 1280px long over a 64ch paragraph reads
     as an underline for the whole page rather than the edge of a control.
     Narrower, and centred, so the three of them sit as one block. */
  max-width:54rem; margin-inline:auto;
}
.folds .terms{ margin-top:0; padding-top:.2rem; border-top:0; }
.folds .qa p{ max-width:64ch; }
.folds .qa p strong{ color:var(--ink); font-weight:600; }

/* ── which case ───────────────────────────────────────── */

/* Two radios drawn as the things they choose between. A select would have
   hidden both behind a menu, and this is the only field on the page that
   changes what is in the box -- the one worth showing rather than listing.

   The real radio is still there and still focusable; it is stretched over the
   card and made invisible, so what you click and what the keyboard lands on
   are the same element. Nothing here is a div pretending to be a control. */
.cases{ border:0; margin:0 0 1.15rem; padding:0; min-width:0; }
.cases__grid{
  display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:.7rem;
}
.case{
  position:relative; display:grid; justify-items:start; gap:.28rem;
  padding:1rem .95rem .95rem;
  border:1px solid var(--hair); border-radius:14px;
  background:var(--bg-2); cursor:pointer;
  transition:border-color .18s ease, background-color .18s ease;
}
.case:hover{ border-color:var(--ink-3); }
.case__radio{
  position:absolute; inset:0; margin:0;
  appearance:none; border:0; background:none;
  border-radius:14px; cursor:pointer;
}
/* The object itself, at icon size: the legged octopus and the six-lobed
   Codex mark, both with their screen cut out of them. A coloured dot said "orange"
   and "blue"; this says which shape you are choosing. */
.case__art{ display:block; margin-bottom:.45rem; line-height:0; }
.case__shell--claude{ fill:var(--orange); }
.case__shell--codex { fill:var(--codex); }
.case__glass{ fill:var(--screen); }
.case__name{ font-size:.92rem; font-weight:600; color:var(--ink); line-height:1.35; }
.case__note{ font-size:.78rem; line-height:1.5; color:var(--ink-3); }

/* :has, so the CARD carries the selection rather than a dot beside it. One
   selected-colour for both, because orange here means "chosen", not "orange
   case" -- the colour of the case is already in the drawing above the name. */
.case:has(.case__radio:checked){
  border-color:var(--orange);
  background:color-mix(in srgb, var(--orange) 9%, var(--bg-2));
}
.case:has(.case__radio:focus-visible){
  outline:2.5px solid var(--orange-2); outline-offset:2px;
}
.cases__note{
  margin:.75rem 0 0; font-size:.78rem; line-height:1.55; color:var(--ink-3);
}

/* Side by side is two four-word columns on a phone. */
@media (max-width:480px){
  .cases__grid{ grid-template-columns:1fr; }
  .case{ grid-template-columns:auto 1fr; column-gap:.9rem; align-items:center; }
  .case__art{ grid-row:1 / 3; margin-bottom:0; }
  .case__name{ align-self:end; }
  .case__note{ align-self:start; }
}

/* ── pre-order form ──────────────────────────────────────── */

.pre{ margin:0; }
.pre__row{ margin-bottom:.95rem; }
.pre__row--half{
  display:grid; grid-template-columns:1fr 5.5rem; gap:.7rem;
}
.pre__row--qty .pre__input{ width:6.5rem; }
.pre__label{
  display:block; margin-bottom:.35rem;
  font-family:var(--mono); font-size:.66rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3);
}
/* "optional" is part of the label, so it obeys the label's rules.
   -------------------------------------------------------------
   It used to cancel all three of them - lowercase, untracked, dimmed -
   which put two type systems on one line and read as a font bug rather
   than as a second word. It is separated by a divider now, not by a
   different typeface, and it keeps the label's ink: it tells you the
   field can be skipped, which is information, not decoration. */
.pre__opt::before{ content:"\00B7"; margin-right:.5em; }
.pre__input{
  width:100%; min-height:46px;
  background:var(--bg); color:var(--ink);
  border:1px solid var(--hair); border-radius:10px;
  padding:.6rem .85rem;
  font-family:var(--body); font-size:1rem;
  transition:border-color .18s, background-color .18s;
}
.pre__input::placeholder{ color:var(--ink-3); }
.pre__input:hover{ border-color:var(--ink-3); }
/* One rule, on plain :focus. Splitting it so only keyboard focus got the
   outline left mouse focus carrying the indicator entirely in box-shadow and
   border-color -- and forced-colors mode forces box-shadow to none and
   border-color to a system colour, so those three fields became the only
   controls on the site with no focus indicator at all. A ring on a focused
   text field is what browsers do natively anyway. */
.pre__input:focus{
  border-color:var(--orange);
  outline:2.5px solid var(--orange-2); outline-offset:2px;
  box-shadow:0 0 0 3px color-mix(in srgb, var(--orange) 22%, transparent);
}
/* appearance:none strips the chevron, which left the Units control looking
   like a read-only text box. Draw one back. */
select.pre__input{
  appearance:none; cursor:pointer;
  padding-right:2rem;
  background-image:linear-gradient(45deg, transparent 50%, var(--ink-3) 50%),
                   linear-gradient(135deg, var(--ink-3) 50%, transparent 50%);
  background-position:calc(100% - 1.05rem) 55%, calc(100% - .72rem) 55%;
  background-size:5px 5px, 5px 5px;
  background-repeat:no-repeat;
}
select.pre__input:hover{ background-image:linear-gradient(45deg, transparent 50%, var(--ink) 50%),
                                          linear-gradient(135deg, var(--ink) 50%, transparent 50%); }

.pre .btn{ margin-top:.45rem; }

.pre__status{
  margin:.8rem 0 0; font-size:.87rem; color:var(--ink-2);
  min-height:1.2em;
}
.pre__status[data-kind="bad"]{ color:#F2836F; }
.pre__status[data-kind="good"]{ color:#5BD69A; }
.pre__status[data-kind="wait"]{ color:var(--orange-2); }

/* ── what the order commits you to ───────────────────────── */

/* Ship date, tax and what the button does right now were three loose
   sentences drifting off the bottom of the column. A term and its answer per
   row, keys in the same mono the field labels use, so the block reads as the
   small print of this order rather than as more page. */
.terms{
  margin:1.4rem 0 0; padding:1.2rem 0 0;
  border-top:1px solid var(--hair-2);
  display:grid; gap:.75rem;
  font-size:.82rem;
}
.terms__row{
  display:grid; grid-template-columns:4.6rem 1fr; gap:.9rem;
  align-items:baseline;
}
.terms dt{
  font-family:var(--mono); font-size:.66rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3);
}
.terms dd{ margin:0; color:var(--ink-3); line-height:1.6; }
.terms dd a{ color:var(--ink-2); text-underline-offset:3px; }
@media (max-width:420px){
  .terms__row{ grid-template-columns:1fr; gap:.2rem; }
}

/* ── the promises ─────────────────────────────────────── */

/* Across the foot of the page, not stranded at the bottom of one column.
   Ruled like a ledger rather than set as three feature cards: this is the
   part someone reads while deciding whether an unknown seller will really
   ship in November, and it should look like terms, not like marketing. */
/* ── company editions ─────────────────────────────────── */

/* A short block for a different reader: someone buying a batch, not one. It
   borrows the .terms table the order page uses for its small print, because
   that is exactly what these three rows are -- what they send, what we send,
   how it is ordered -- and a second list component for three rows would be
   two things that look different doing one job.

   Held to a measure and left-aligned rather than centred like the close
   below it: this is read, not announced. */
.co .wrap{ padding-block:clamp(3.5rem, 7vw, 6rem); }
.co__h{ margin:0 0 .8rem; }
.co__lede{ margin:0; max-width:54ch; }
.co__list{
  max-width:60ch;
  margin-top:clamp(1.8rem, 3vw, 2.6rem);
}
.co__btn{ margin-top:clamp(1.8rem, 3vw, 2.4rem); }

/* Two columns where there is room. Stacked, this section was a narrow ribbon
   of text down the left of a band with half the page empty beside it -- and
   the two halves are a natural pair anyway: the pitch on one side, what the
   exchange actually involves on the other. */
@media (min-width:900px){
  .co .wrap{
    display:grid;
    grid-template-columns:minmax(0, 1fr) minmax(0, 1fr);
    column-gap:clamp(2.5rem, 5vw, 5rem);
    align-items:start;
  }
  .co__h, .co__lede, .co__btn{ grid-column:1; }
  /* a grid item stretches by default, and a call to action as wide as its
     column reads as the primary button on the page, which this is not */
  .co__btn{ justify-self:start; }
  .co__list{
    grid-column:2; grid-row:1 / span 3;
    margin-top:.5rem;
  }
}

/* REMOVED: .assure, the three-column strip of promises across the foot. Two
   of its three were the terms table in other words, so the page printed
   "cancel any time before it ships" twice; all three are rows in the shipping
   fold now. */

/* ── the shop chrome ──────────────────────────────────── */

/* A label a screen reader should hear where the visible words are not enough
   on their own - a button whose word is ambiguous, or a link that opens a new
   tab without saying so. Started on the shop; the tour uses it too now. */
.vh{
  position:absolute; width:1px; height:1px; overflow:hidden;
  clip-path:inset(50%); white-space:nowrap;
}

/* .is-stuck is toggled by app.js, which only the tour loads, so the shop's
   sticky nav floated over scrolling content with no edge at all. It gets a
   permanent one: there is nothing here for it to fade in over. */
body:has(.nav__inner--shop) .nav{ border-bottom-color:var(--hair-2); }
.nav__inner--shop{ grid-template-columns:1fr auto; }
.shopnav{ display:flex; align-items:center; gap:clamp(.8rem, 2vw, 1.4rem); }
.shopnav__back{
  font-size:.87rem; color:var(--ink-3); text-decoration:none;
  padding:.4rem 0;
}
.shopnav__back:hover{ color:var(--ink); }

/* The count is the whole point of the link, so it is set in the mono the
   rest of the shop counts in and it disappears entirely at zero rather than
   sitting there as a grey nought. */
.cartlink{ gap:.5rem; }
.cartlink__n{
  display:inline-flex; align-items:center; justify-content:center;
  min-width:1.35rem; height:1.35rem; padding:0 .3rem;
  background:var(--orange); color:#14100B;
  border-radius:100px;
  font-family:var(--mono); font-size:.7rem; font-weight:500;
  font-variant-numeric:tabular-nums;
}

/* A nav pill never wraps: it is two or three words and the row has room for
   them or it does not. Wrapping just makes the header taller and the target
   an odd shape. */
.shopnav .btn{ white-space:nowrap; }

.nav__how-short{ display:none; }

@media (max-width:560px){
  .shopnav__back{ display:none; }   /* the brand mark already goes back */
  .nav__how-long { display:none; }
  .nav__how-short{ display:inline; }
}

/* ── the shop pages ───────────────────────────────────── */

.section--shop{ padding-top:clamp(1.6rem, 4vw, 2.6rem); }
.wrap--shop{ max-width:1120px; }
.wrap--narrow{ max-width:44rem; }

.shop__h{ margin:0 0 .5rem; }
.shop__sub{
  margin:0 0 clamp(1.6rem, 3.5vw, 2.4rem); max-width:46ch;
  font-size:.98rem; line-height:1.6; color:var(--ink-2);
}

/* Numbered, because this genuinely is a sequence: you cannot say where it
   goes with an empty cart, and you cannot pay before you have said where it
   goes. A step already behind you is a link; one ahead is not, and must not
   look like one. */
.steps-bar{
  display:flex; flex-wrap:wrap; align-items:center;
  gap:.5rem clamp(.7rem, 2vw, 1.4rem);
  list-style:none; margin:0 0 clamp(1.6rem, 4vw, 2.6rem); padding:0 0 1.1rem;
  border-bottom:1px solid var(--hair-2);
}
.steps-bar__i{
  display:flex; align-items:center; gap:.5rem;
  font-size:.86rem; color:var(--ink-3);
}
.steps-bar__i a{
  display:flex; align-items:center; gap:.5rem;
  color:inherit; text-decoration:none; padding:.25rem 0;
}
.steps-bar__i a:hover{ color:var(--ink); }
.steps-bar__i + .steps-bar__i::before{
  content:""; width:clamp(.9rem, 2.4vw, 2rem); height:1px;
  background:var(--hair); margin-right:.2rem;
}
.steps-bar__n{
  display:inline-flex; align-items:center; justify-content:center;
  width:1.6rem; height:1.6rem; flex:none;
  border:1px solid var(--hair); border-radius:100px;
  font-family:var(--mono); font-size:.72rem;
  font-variant-numeric:tabular-nums;
}
.steps-bar__i.is-now{ color:var(--ink); }
.steps-bar__i.is-now .steps-bar__n{
  background:var(--orange); border-color:var(--orange); color:#14100B;
}
.steps-bar__i.is-done{ color:var(--ink-2); }
.steps-bar__i.is-done .steps-bar__n{ border-color:var(--ink-3); color:var(--ink-2); }

/* ── cart and checkout, one shape ─────────────────────── */

.cart, .checkout{
  display:grid; gap:clamp(1.6rem, 3.5vw, 2.8rem);
  align-items:start;
}
/* Stacked, the summary goes above the form and above the pay button: nobody
   should fill six fields, or press a button that places an order, without the
   number they are agreeing to already on screen. The cart keeps its lines
   first, because there the lines ARE the page. */
@media (max-width:879px){
  .checkout .sum{ order:-1; }
}
@media (min-width:880px){
  .cart, .checkout{ grid-template-columns:minmax(0,1fr) minmax(0, 21rem); }
  /* The summary follows you down a long form. It is the number you are
     agreeing to, so it should never be off screen while you fill the fields
     that decide it.

     A fixed offset, not var(--navh): app.js measures that and no shop page
     loads app.js, so the fallback was leaving a 5.5rem gap under a 3.5rem
     nav. The shop nav is one row of one button and does not grow. */
  .sum{ position:sticky; top:4.6rem; }
}

/* ── cart lines ───────────────────────────────────────── */

.lines{ list-style:none; margin:0; padding:0; display:grid; gap:0; }
.line{
  display:grid;
  grid-template-columns:auto 1fr auto auto;
  grid-template-areas:"img id qty sum" "img x x x";
  align-items:center; gap:.4rem 1rem;
  padding:1.1rem 0;
  border-top:1px solid var(--hair-2);
}
.line:last-child{ border-bottom:1px solid var(--hair-2); }
.line__img{
  grid-area:img; width:76px; height:auto; border-radius:10px;
  background:radial-gradient(50% 50% at 50% 55%, rgba(232,135,58,.12), transparent 70%);
}
.line__id{ grid-area:id; min-width:0; }
.line__name{ margin:0; font-weight:600; font-size:.98rem; color:var(--ink); }
.line__note{ margin:.15rem 0 0; font-size:.82rem; color:var(--ink-3); }
.line__qty{ grid-area:qty; display:flex; align-items:center; gap:.55rem; }
.line__qtyl{
  font-family:var(--mono); font-size:.66rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3);
}
.line__qty .pre__input{ width:4.6rem; min-height:44px; }
.line__sum{
  grid-area:sum; margin:0; min-width:4.5rem; text-align:right;
  font-weight:600; font-variant-numeric:tabular-nums;
}
/* A destructive control, so it is quiet until you go for it and it never
   sits next to the primary button. */
.line__x{
  grid-area:x; justify-self:start;
  background:none; border:0; padding:.4rem 0; margin:0;
  color:var(--ink-3); font-family:var(--body); font-size:.82rem;
  text-decoration:underline; text-underline-offset:3px;
  cursor:pointer;
}
.line__x:hover{ color:#F2836F; }

@media (max-width:560px){
  .line{
    grid-template-columns:auto 1fr;
    grid-template-areas:"img id" "img qty" "x sum";
    gap:.5rem .9rem;
  }
  .line__img{ width:64px; }
  .line__sum{ align-self:center; }
}

/* ── the summary ──────────────────────────────────────── */

.sum{
  background:var(--bg-2);
  border:1px solid var(--hair-2); border-radius:16px;
  padding:clamp(1.1rem, 2.4vw, 1.5rem);
}
.sum__h{
  margin:0 0 1rem;
  font-family:var(--mono); font-size:.68rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3); font-weight:400;
}
.sum__lines{ list-style:none; margin:0 0 1rem; padding:0; display:grid; gap:.8rem; }
.sum__line{
  display:grid; grid-template-columns:40px 1fr auto; gap:.7rem; align-items:center;
  font-size:.88rem;
}
.sum__line img{ width:40px; height:auto; border-radius:7px; }
.sum__lname{ min-width:0; color:var(--ink-2); }
.sum__lqty{ color:var(--ink-3); margin-left:.4rem; font-variant-numeric:tabular-nums; }
.sum__lsum{ font-variant-numeric:tabular-nums; color:var(--ink); }

.sum__rows{ margin:0 0 1.1rem; padding:1rem 0 0; border-top:1px solid var(--hair); }
.sum__row{
  display:flex; justify-content:space-between; align-items:baseline; gap:1rem;
  font-size:.88rem; color:var(--ink-3);
}
.sum__row + .sum__row{ margin-top:.5rem; }
.sum__row dt{ font-weight:400; }
.sum__row dd{ margin:0; font-variant-numeric:tabular-nums; color:var(--ink-2); }
.sum__row--total{
  margin-top:.85rem; padding-top:.85rem; border-top:1px solid var(--hair);
  font-size:1.05rem; color:var(--ink);
}
.sum__row--total dt{ font-weight:600; }
.sum__row--total dd{ font-weight:600; color:var(--ink); font-size:1.15rem; }
.sum__plus{ font-size:.8rem; font-weight:400; color:var(--ink-3); }
.sum__note{ margin:.9rem 0 0; font-size:.78rem; line-height:1.55; color:var(--ink-3); }
.sum__back{
  display:inline-block; margin-top:.7rem; padding:.35rem 0;
  font-size:.82rem; color:var(--ink-3); text-underline-offset:3px;
}
.sum__back:hover{ color:var(--ink); }

/* ── empty states ─────────────────────────────────────── */

/* An invitation, not an apology. */
.empty{
  padding:clamp(2.4rem, 6vw, 4rem) 0;
  border-top:1px solid var(--hair-2);
}
.empty__h{ margin:0 0 .35rem; font-size:1.15rem; font-weight:600; color:var(--ink); }
.empty__p{ margin:0 0 1.5rem; color:var(--ink-3); }

/* ── the details form ─────────────────────────────────── */

.det__set{ border:0; margin:0 0 1.9rem; padding:0; min-width:0; }
.det__legend{
  padding:0; margin-bottom:.9rem;
  font-family:var(--display); font-size:1.05rem; font-weight:600; color:var(--ink);
}
.det__grid{ display:grid; gap:.9rem; }
@media (min-width:520px){
  .det__grid{ grid-template-columns:repeat(2, minmax(0,1fr)); }
  .fld{ grid-column:1 / -1; }
  .fld--half{ grid-column:auto; }
}
.fld{ min-width:0; }
/* The error sits with its field, not in a list at the top of the form: by
   the time someone has scrolled back to the field they have forgotten which
   of six messages was about it. */
.fld__err{
  margin:.35rem 0 0; font-size:.8rem; line-height:1.5; color:#F2836F;
  min-height:0;
}
.fld__err:empty{ display:none; }
.fld.is-bad .pre__input{ border-color:#F2836F; }
.pre__input--area{ min-height:auto; resize:vertical; line-height:1.55; }
.det__why{ margin:.7rem 0 0; font-size:.8rem; line-height:1.55; color:var(--ink-3); }

/* The one checkbox in the shop.
   -------------------------------------------------------------
   A real <input type=checkbox>, made invisible and covered by a drawn box, so
   what the pointer hits and what the keyboard lands on are the same element.
   The tick is in the markup rather than a ::before glyph, because a font that
   does not arrive would leave the checked state indistinguishable. */
.chk{
  position:relative; display:flex; align-items:flex-start; gap:.7rem;
  padding:.35rem 0; cursor:pointer; min-height:44px; align-items:center;
}
.chk__box{
  position:absolute; inset:0; width:100%; height:100%; margin:0;
  appearance:none; border:0; background:none; cursor:pointer;
}
.chk__mark{
  display:inline-flex; align-items:center; justify-content:center; flex:none;
  width:1.25rem; height:1.25rem;
  border:1px solid var(--hair); border-radius:6px;
  background:var(--bg-2); color:#14100B;
  transition:background-color .16s, border-color .16s;
}
.chk__mark svg{ opacity:0; transition:opacity .12s; }
.chk:has(.chk__box:checked) .chk__mark{ background:var(--orange); border-color:var(--orange); }
.chk:has(.chk__box:checked) .chk__mark svg{ opacity:1; }
.chk:hover .chk__mark{ border-color:var(--ink-3); }
.chk:has(.chk__box:focus-visible) .chk__mark{
  outline:2.5px solid var(--orange-2); outline-offset:2px;
}
.chk__label{ font-size:.92rem; line-height:1.45; color:var(--ink-2); }
.chk:has(.chk__box:checked) .chk__label{ color:var(--ink); }

/* Indented under the switch that opens it, so it reads as a consequence of
   the tick rather than as another block of the form. */
.det__grid--bill{
  margin-top:1rem; padding-left:clamp(.9rem, 2vw, 1.4rem);
  border-left:1px solid var(--hair-2);
}
.det__fine{ margin:1rem 0 0; font-size:.79rem; line-height:1.55; color:var(--ink-3); }
.det__fine a{ color:var(--ink-2); text-underline-offset:3px; }

/* ── payment ──────────────────────────────────────────── */

.payside{ min-width:0; }
.payside__h{
  margin:1.8rem 0 .6rem;
  font-family:var(--mono); font-size:.68rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3); font-weight:400;
}
.ship{
  margin:0 0 .3rem; font-style:normal;
  font-size:.92rem; line-height:1.7; color:var(--ink-2);
}

/* The honest wall, and the only orange-bordered box on the site: there is
   exactly one thing on this page a visitor must not miss, and it is that
   nothing here charges them. */
.demo{
  background:color-mix(in srgb, var(--orange) 8%, var(--bg-2));
  border:1px solid color-mix(in srgb, var(--orange) 32%, transparent);
  border-radius:16px;
  padding:clamp(1.1rem, 2.6vw, 1.6rem);
}
.demo__h{
  margin:0 0 .6rem;
  font-family:var(--display); font-size:1.12rem; font-weight:600; color:var(--ink);
}
.demo__p{ margin:0 0 .7rem; font-size:.9rem; line-height:1.65; color:var(--ink-2); }
.demo__p:last-child{ margin-bottom:0; }
.demo__p a{ color:var(--orange-2); text-underline-offset:3px; }

/* Where the provider's hosted form mounts. Their fields, their origin: card
   numbers never touch this site. */
.slot{ margin:0 0 1.4rem; min-height:0; }

.payside .btn{ margin-top:1.4rem; }

/* ── the receipt ──────────────────────────────────────── */

.done__tick{ margin:0 0 1rem; color:#5BD69A; line-height:0; }
.done__terms{ margin-top:1.6rem; }
.done__h{
  margin:2.2rem 0 .8rem;
  font-family:var(--display); font-size:1.15rem; font-weight:600; color:var(--ink);
}
.done__next{
  margin:0 0 1.8rem; padding-left:1.2rem;
  display:grid; gap:.6rem;
  font-size:.92rem; line-height:1.6; color:var(--ink-2);
}
.done__next::marker{ color:var(--ink-3); }
.ref{
  font-family:var(--mono); font-size:.95rem; letter-spacing:.04em;
  color:var(--ink);
}
.done__acts{ display:flex; flex-wrap:wrap; align-items:center; gap:1.4rem; margin-top:.4rem; }
/* Quiet, and nowhere near the primary action: it deletes the only copy of
   this order that exists anywhere. */
.done__forget{
  background:none; border:0; padding:.5rem 0; margin:0;
  color:var(--ink-3); font-family:var(--body); font-size:.84rem;
  text-decoration:underline; text-underline-offset:3px; cursor:pointer;
}
.done__forget:hover{ color:#F2836F; }

/* ── footer ───────────────────────────────────────────── */

.foot{
  border-top:1px solid var(--hair-2);
  padding:1.9rem 0 calc(2.2rem + env(safe-area-inset-bottom));
  font-size:.86rem; color:var(--ink-3);
}
.foot__row{
  display:flex; justify-content:space-between; align-items:baseline;
  gap:1rem; flex-wrap:wrap;
}
.foot__brand{
  font-family:var(--display);
  font-weight:700; font-size:1rem; color:var(--ink); letter-spacing:-.02em;
}
.foot__meta{ font-family:var(--mono); font-size:.7rem; letter-spacing:.1em; text-transform:uppercase; }
/* Five links now, not three: the cancellation policy and the accessibility
   statement both have to be reachable from every page, so this wraps rather
   than pushing the row off a narrow phone. */
.foot__links{ display:flex; flex-wrap:wrap; gap:0 1.1rem; justify-content:center; }
/* Bare inline anchors gave Terms / Privacy / Contact a hit box around 20px
   tall, close enough together that a thumb reliably hit the wrong one. */
.foot__links a{
  display:inline-flex; align-items:center; min-height:44px;
  text-decoration:none; color:var(--ink-3);
}
.foot__links a:hover{ color:var(--ink); }
.foot__note a{ color:var(--ink-2); text-underline-offset:3px; }
.foot__note{
  margin:1.4rem 0 0; padding-top:1.2rem;
  border-top:1px solid var(--hair-2);
  font-size:.76rem; line-height:1.6; color:var(--ink-3);
  max-width:78ch;
}

/* ── reveal ───────────────────────────────────────────── */

/* The last thing on the page is the pair, and it gets an arrival rather than
   a fade: the octopus first, the Codex case a beat behind it, which is the same order
   the hero introduces them in. The figure itself does not move -- only its
   two children do -- so the overlap the arrangement depends on is never
   mid-transition, and both end at transform:none, which is exactly how the
   pair renders today. */
.close__shot.reveal{ opacity:1; transform:none; }
.close__shot .pair__a,
.close__shot .pair__b{ opacity:0; transform:translateY(26px); }
.close__shot.is-in .pair__a{
  opacity:1; transform:none;
  transition:opacity .7s ease, transform .9s cubic-bezier(.2,.7,.3,1);
}
.close__shot.is-in .pair__b{
  opacity:1; transform:none;
  transition:opacity .7s ease .18s, transform .9s cubic-bezier(.2,.7,.3,1) .18s;
}

.reveal{ opacity:0; transform:translateY(16px); }
.reveal.is-in{
  opacity:1; transform:none;
  transition:opacity .75s ease var(--d, 0s), transform .75s cubic-bezier(.2,.7,.3,1) var(--d, 0s);
}

/* ── responsive ───────────────────────────────────────── */

@media (max-width:940px){
  /* Stacked, but still inside the one pinned block: the device sits directly
     above the words that describe it and they scroll as a unit. */
  .reel__inner{ grid-template-columns:1fr; gap:1.4rem; align-content:center; }
  .reel__stage{ height:min(40vh, 320px); }
  .reel__cap .h2{ max-width:none; }
  .reel__cap .sub{ font-size:.95rem; }

  /* The setup reel stacks the same way, and the panel gives up height so the
     three steps fit under it inside the pin. Plain stacking, not
     display:contents with order -- that dissolved the stage into the grid and
     the rows collided.

     Flex, not grid, and the panel takes the leftover height rather than a
     width cap: with `width:min(100%,420px)` the panel was 315px tall whatever
     was left for it, so the steps ran out of the bottom of the pinned block on
     any short viewport. It overflowed by 35px before the type went up and 71
     after -- the type change exposed it, the cap caused it. Height now comes
     from what is left and aspect-ratio derives the width. */
  .setup__inner{
    display:flex; flex-direction:column; gap:1.1rem; justify-content:center;
    /* stretch, because the base rule's align-items:center is for the desktop
       GRID, where it means vertical. Carried into a column flex it means
       horizontal, so the stage shrank to the width of its widest line and the
       panel's own width:100% resolved against that -- 302px inside a 367px
       block, for no reason anyone could see. */
    align-items:stretch;
  }
  .setup .h2{ font-size:clamp(1.5rem, 6vw, 2rem); }
  /* The stage has to grow too, or the panel's flex:1 has nothing to grow
     inside: a flex item is only as tall as its content unless it is told
     otherwise, so the panel sat at its 150px floor with 100px going spare
     underneath it. */
  /* Sized by WIDTH here, with aspect-ratio deriving the height, rather than
     by leftover flex height with the width derived from it. The second way
     is what let the box come out taller than 4:3 and squash the dials: a
     height from flex, a width clamped by max-width, and an aspect ratio that
     could not hold both. The svh term is the height cap the flex sizing used
     to provide -- at 4:3, 46svh of width is 34.5svh of height, which leaves
     the three steps and the heading their room on a short phone. */
  .setup__stage{ display:flex; flex-direction:column; min-height:0; flex:none; }
  .panel{
    margin-top:.7rem; margin-inline:auto;
    flex:none; min-height:0;
    width:min(100%, 420px, 46svh);
  }

  /* The steps cross-fade in one cell here, the way the reel's captions
     already do. Three expanded steps plus a panel do not fit a phone-height
     pin -- the panel was being squeezed to a 160px thumbnail to make room for
     two steps nobody is reading yet. Stacked, the block is as tall as the
     longest step and nothing jumps as they swap.

     visibility, not opacity alone: an invisible step still held its copy
     button and its platform tabs in the tab order. */
  /* align-items:start matters here. Stacked in one cell, the cell is as tall
     as the tallest step -- 02, with its tablist and code block -- and a
     stretched grid item stretches its own rows to match. Step 01 came out
     with a hundred pixels of nothing between "Plug it in." and "The cable is
     in the box." */
  .steps{ display:grid; flex:none; gap:0; align-items:start; }
  /* Out fast, in after: a symmetrical cross-fade put a code block and a
     platform tablist on top of a two-line step for a third of a second, and it
     read as a rendering fault rather than a transition. The reel's captions
     can cross-fade because they are the same shape; these are not. */
  .step{
    grid-area:1 / 1;
    visibility:hidden; opacity:0;
    transition:opacity .15s ease, visibility 0s linear .15s;
  }
  .step.is-on{
    visibility:visible; opacity:1;
    transition:opacity .22s ease .15s, visibility 0s;
  }
  /* One scroll hint per page is enough, and the reel above has already
     taught the gesture. On a short screen this one landed on step 03. */
  #setupHint{ display:none; }
  .steps{ gap:1rem; }
  .step__p{ font-size:.86rem; }

  .split{ grid-template-columns:1fr; }
  .buy{ grid-template-columns:1fr; }
  .buy__shot{ width:min(80%, 380px); }
}

@media (max-width:760px){
  body{ font-size:1rem; }
  .nav__inner{ grid-template-columns:1fr auto; row-gap:.7rem; }
  .brand{ grid-area:1 / 1; }
  .nav .btn{ grid-area:1 / 2; }
  .cards{ grid-template-columns:1fr; }
}

@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; }
  .close__shot .pair__a,
  .close__shot .pair__b{ opacity:1; transform:none; }
  /* The order stage still has to answer the question -- the unpicked case
     stays dimmed -- it just does not move to say so. */
  .buy[data-case] .pair__a,
  .buy[data-case] .pair__b{ transform:none !important; }
  .shot{ transform:none !important; }
  .shot__frame{ transform:none !important; }
  .shot__bob{ transform:none !important; animation:none !important; }
  .reel__stage, .panel{ transform:none !important; animation:none !important; }
  .shot::after{ animation:none !important; }

  /* Un-pin every scroll-driven section. Zeroing durations left the part that
     actually causes trouble untouched: viewport-heights of scroll during
     which the page does not move. This turns them into ordinary blocks --
     everything readable, in order, nothing hijacked.

     The lists have to come back to full strength as well, or the dimming and
     the one-open-answer rule would survive as a page four fifths greyed out
     with four questions that cannot be read. */
  .reel__track{ display:none; }
  .reel__track{ display:none; }
  .reel__pin{
    position:static; height:auto;
    padding-top:2rem; padding-bottom:2rem;
  }
  .reel__inner{ grid-template-columns:1fr; gap:2rem; }
  .reel__stage{ height:min(60vh, 440px); }
  .reel__caps{ gap:2rem; }
  .reel__cap{
    grid-area:auto;                 /* stop stacking them in one cell */
    opacity:1; transform:none; pointer-events:auto;
  }

  /* The setup reel needs exactly the same treatment and never got it: it kept
     three viewport-heights of dead scroll, and driveSetup() returns early
     under reduced motion, so it sat on step 01 the whole way down with the
     other two dimmed to .32 behind it. Unpinned, it is three readable steps
     under a panel. */
  .setup__track{ display:none; }
  .setup__pin{
    position:static; height:auto;
    padding-top:2rem; padding-bottom:2rem;
  }
  .setup__inner{ display:block; }
  .steps{ display:grid; gap:2.1rem; }
  .step{
    grid-area:auto;
    visibility:visible; opacity:1;
  }
}

/* The box's left column -- heading, scale drawing, size buttons, legend, fine
   print -- comes to 774px of content in a pinned box that has about 736 to
   give on a 942px screen, so the last line ("Both cases are 2 cm deep...")
   had its descenders cut off by the bottom of the viewport.

   Spacing, not scale: the drawing is a measured thing and shrinking it to buy
   room would make every dimension on it smaller than it says it is. The
   forty-odd pixels come out of the gaps instead, and only on the screens that
   need them -- above 1000px the column has room and keeps the air. */
@media (max-height: 1000px){
  .scale{ margin-block:.5rem; }
  .scale__seg{ margin-top:1rem; }
  .scale__cap{ margin-top:.8rem; }
}

/* SCROLL SNAPPING ON THE STEPPED SECTIONS
   ---------------------------------------
   `proximity`, not `mandatory`: mandatory would take hold of the scroll for
   the whole length of four sections, and a page that will not let you stop
   between two points is worse than one that occasionally shows you a caption
   arriving. Proximity only pulls when the scroll has already ended near a
   stop, which is exactly the case being fixed.

   The section's own top edge is stop 0. app.js fills each track with the
   markers for the rest. */
html{ scroll-snap-type:y proximity; }
/* NO scroll-margin on the pinned three, which [id] above would otherwise give
   them.
   -------------------------------------------------------------
   That margin exists so an anchor link lands its target below the fixed nav
   rather than under it. But scroll-margin moves an element's SNAP position by
   the same amount, and these four are their own stop 0 -- so the browser
   snapped to 94px ABOVE the section top, which is 94px above the point where
   the pin catches. Scroll a little way down from the hero and the proximity
   snap pulled you back up to it, every time, because the nearest snap point
   was behind you.

   Stop 0 being the section's own top edge is also what answers "a long scroll
   from the top should stop at the first step": `scroll-snap-stop: always`
   below will not let a fling cross this edge, so the page opens the section
   on step 1 and waits for the next scroll. No JS, and no special case for
   arriving.

   They do not need the margin in the first place: the pin carries its own
   padding-top of one nav height, so a landing on the section's exact top edge
   already clears the header. One offset, in one place, instead of two that
   disagree. */
.reel, .setup{
  position:relative;
  scroll-snap-align:start;
  scroll-snap-stop:always;
  scroll-margin-top:0;
}

/* AND OFF AGAIN THE MOMENT A SECTION STOPS BEING STEPPED.
   --------------------------------------------------------
   The markers look after themselves -- they are children of a track, so a
   display:none'd track takes them with it. Stop 0 does not: it is the section
   element, which is still there in every state, so the two rules above had to
   be switched off by hand and were not.

   Both states it survived into are the ones that exist to hand the scroll
   back. Under Reduce Motion the page is unpinned and the durations are zero,
   and yet every flick and every Page Down was still being arrested at three
   section edges -- the one thing still taking the scroll from the reader who
   had explicitly asked for none of it. And an unpinned section is an ordinary
   block whose top edge a gesture may not cross, with no second stop inside it
   to carry on to: a barrier in front of a section that no longer steps.

   scroll-margin-top goes back with it. Zeroing it is only safe because the
   pin carries a nav height of padding; both unpinned states replace that with
   2rem, so without this the "How to install" link lands the heading behind
   the sticky header. */
@media (prefers-reduced-motion: reduce){
  .reel, .setup{ scroll-snap-align:none; scroll-margin-top:calc(var(--navh, 5.5rem) + .9rem); }
}
#setup[data-unpin]{
  scroll-snap-align:none;
  scroll-margin-top:calc(var(--navh, 5.5rem) + .9rem);
}

/* Positioned against the SECTION, not against the track they live in. A
   stop is span/n, and span is the section's height less one screen -- but the
   track begins one screen (the pin) below the section's top, and only the
   reel happens to have a stop exactly one screen tall. The box steps in
   half a screen, so markers laid end to end inside their track came
   out 559px late, every one of them.

   Still CHILDREN of the track, though, which is the guard: a track that is
   display:none takes its markers with it, so a section that has stopped being
   stepped -- reduced motion, a narrow screen, a setup that did not fit --
   stops snapping too, without a rule anywhere having to say so. */
/* ONE STOP PER FLING, AND THIS IS NOW THE ONLY THING ENFORCING IT.
   ------------------------------------------------------------------
   `always` means a scroll may not pass OVER this position: a fling stops at
   the first snap point it reaches instead of carrying on to the fourth. That
   is the whole rule app.js used to try to enforce by intercepting the wheel,
   and this is the version that works, because it runs on the compositor --
   where the events cannot arrive late or go missing, and where nothing can be
   swallowed, so the page can never stop answering. See the long note in
   app.js for what was tried before this and why each attempt failed.

   Draining a fling is exactly what the property was added for, and it is
   honoured with `proximity` as well as `mandatory`. Chrome has had it since
   74; WebKit's momentum-scrolling bug (223406) was fixed in 2021.

   `proximity` still leaves a dead band mid-stop where a slow drag can rest,
   and that is fine here: a caption is fully shown for the whole of its stop,
   so resting between two snap points shows one caption rather than half of
   two. The pull only has to catch the end of a gesture, not police the
   middle of one. */
.snap{
  position:absolute; left:0; width:1px; height:1px;
  pointer-events:none; visibility:hidden;
  /* An explicit fraction rather than i/n, because not every section divides
     its track evenly -- see the stop table in app.js. */
  /* svh, matching the pins. They are sized 100svh precisely because svh does
     not change when a phone's URL bar collapses; a marker measured in vh
     does, so every stop drifted from its caption boundary by the height of
     the bar -- worst in the box, where a whole stop is only 55vh. */
  top:calc((100% - 100svh) * var(--at));
  scroll-snap-align:start;
  scroll-snap-stop:always;
}

/* The setup section when its steps do not fit the viewport (app.js measures
   and sets the attribute). Same rules the reduced-motion block applies, for
   the same reason: a pinned box cannot show a column taller than itself, and
   content nothing can scroll to is worse than a section that does not move. */
#setup[data-unpin] .setup__track{ display:none; }
#setup[data-unpin] .setup__pin{
  position:static; height:auto;
  padding-top:2rem; padding-bottom:2rem;
}
#setup[data-unpin] .setup__inner{ display:block; }
#setup[data-unpin] .steps{ display:grid; gap:2.1rem; }
#setup[data-unpin] .step{
  grid-area:auto;
  visibility:visible; opacity:1;
}


/* ── the legal page ───────────────────────────────────────── */

.doc{ background:var(--bg); }
.nav__inner--doc{ grid-template-columns:1fr auto; }

.doc__wrap{
  width:min(100% - 2.5rem, 720px); margin-inline:auto;
  padding:clamp(2rem,5vw,3.5rem) 0 clamp(3rem,6vw,5rem);
}
.doc__title{
  font-family:var(--display); font-weight:600;
  font-size:clamp(1.9rem,4vw,2.6rem); line-height:1.05;
  letter-spacing:-.025em; margin:0 0 .4rem;
}
.doc__updated{
  font-family:var(--mono); font-size:.7rem; letter-spacing:.12em;
  text-transform:uppercase; color:var(--ink-3); margin:0 0 2.6rem;
}

/* ── the launching-soon gate ──────────────────────────── */

/* The page is HIDDEN, not removed. Every other module still finds the
   elements it expects and lays out against a real viewport, so nothing throws
   behind the screen and nothing has to know the gate exists. visibility
   rather than display, for the same reason: display:none collapses the
   measurements fitSetup and the reel are built on. */
html[data-gate="locked"]{ overflow:hidden; }
html[data-gate="locked"] body > *:not(.gate){ visibility:hidden; }

.gate{
  position:fixed; inset:0; z-index:9999;
  display:grid; place-items:center;
  padding:clamp(1.5rem, 5vw, 3rem);
  background:var(--bg);
  overflow:auto;
  /* the hero's own glow, so the holding page is the same room as the site */
  background-image:radial-gradient(120% 80% at 50% 18%, rgba(232,135,58,.13), transparent 60%);
}
/* Wide enough for the hero's headline to sit on two lines the way it does on
   index.html. It was 34rem when the words came after the picture and had to
   be small; leading with them is what needs the room. */
.gate__inner{
  width:min(100%, 48rem);
  text-align:center;
  display:grid; justify-items:center; gap:0;
}

/* The brand, which the holding page was missing entirely. Bigger than the
   nav's because here it is the only thing identifying the page. */
.gate__brand{
  display:inline-flex; align-items:center; gap:.6rem;
  font-family:var(--display); font-weight:700;
  font-size:clamp(1.7rem, 3.4vw, 2.3rem); letter-spacing:-.02em; color:var(--ink);
  margin:0 0 clamp(1.5rem, 3.5vw, 2.1rem);
}
/* Sized in em so the mark tracks the word instead of staying at the 34px the
   markup asks for - the width/height attributes are there to reserve space
   before the CSS lands, not to fix the size. */
.gate__brand .brand__mark{ width:1.2em; height:auto; }

/* The figure carries .shot.shot--pair as well, so the arrangement, the bob
   and the reduced-motion switch-off all come from the hero's own rules. All
   that is left to say here is how much room it gets: the hero's own width is
   940px against a whole viewport, and this sits in a 48rem card. */
.gate__shot{
  margin:clamp(1.8rem, 4.5vw, 2.8rem) auto 0;
  width:min(100%, 30rem, 46vh);
}

/* The hero's own scale, one step down at the top end: index.html has a whole
   viewport to fill and this is a centred card, so 5.1rem would be shouting. */
.gate__h{
  font-family:var(--display); font-weight:600;
  font-size:clamp(2.3rem, 6.4vw, 4rem); line-height:.98;
  letter-spacing:-.033em; color:var(--ink);
  margin:0 0 1rem; text-wrap:balance;
}
.gate__sub{
  margin:0; max-width:54ch;
  font-size:clamp(1rem, 1.5vw, 1.13rem); line-height:1.6; color:var(--ink-2);
  text-wrap:pretty;
}

/* The only thing here that the real hero does not say. Under the shot, small
   and flat, because the page has already made the argument above it - this
   line only has to answer "can I buy it", and the answer is one word longer
   than "no". */
/* The one thing here the real hero does not say, so it is the one thing
   allowed to be loud. It was .8rem and read as a caption on a page whose
   entire message it is. */
.gate__soon{
  margin:clamp(1.6rem, 4vw, 2.4rem) 0 0;
  font-family:var(--display); font-weight:600;
  font-size:clamp(1.6rem, 4.4vw, 2.6rem); line-height:1.05;
  letter-spacing:.02em; text-transform:uppercase;
  color:var(--orange);
}

/* Folded. A launching-soon page with a login box on it invites everybody to
   wonder what they are missing; a small word does not. */
.gate__signin{ margin-top:clamp(2rem, 5vw, 3rem); width:min(100%, 19rem); }
.gate__signin summary{
  list-style:none; cursor:pointer;
  display:inline-flex; align-items:center; justify-content:center;
  min-height:44px; padding:0 .4rem;
  font-family:var(--mono); font-size:.72rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--ink-3);
}
.gate__signin summary::-webkit-details-marker{ display:none; }
.gate__signin summary::marker{ content:""; }
.gate__signin summary:hover .gate__signin-t{ color:var(--ink); }
.gate__signin[open] summary{ color:var(--ink-2); }

.gate__form{ display:grid; gap:.8rem; text-align:left; margin-top:.4rem; }
.gate__fld{ display:grid; gap:.3rem; }
.gate__lbl{
  font-family:var(--mono); font-size:.66rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3);
}
.gate__in{
  width:100%; min-height:44px;
  padding:.55rem .7rem;
  background:var(--bg-2); color:var(--ink);
  border:1px solid var(--hair-2); border-radius:9px;
  font:inherit; font-size:.95rem;
}
.gate__in:focus-visible{ border-color:var(--orange-2); }
.gate__go{ margin-top:.2rem; justify-self:stretch; }
.gate__status{
  margin:0; min-height:1.2em;
  font-size:.82rem; color:var(--orange-2);
}

@media (prefers-reduced-motion: no-preference){
  .gate__inner{ animation:gate-in .5s cubic-bezier(.2,.7,.3,1) both; }
  @keyframes gate-in{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:none; } }
}

/* ── the terms set, split across five pages ───────────── */

/* WHY THIS EXISTS. The terms used to be one long page with a table of
   contents at the top. Split into five, that table of contents becomes a row
   of sibling pages -- so it has to be on every one of them, or a reader who
   lands on Privacy from a footer link has no idea the other four exist. The
   current page is a <span>, not a link to itself. */
.docnav{
  display:flex; flex-wrap:wrap; gap:.5rem 1.4rem;
  /* No rule of its own: every h2 in .doc__wrap already draws a border-top,
     so a border here put two hairlines within fifty pixels of each other. The
     first section's own rule is the separator. */
  margin:1.6rem 0 2rem;
  font-size:.86rem;
}
.docnav a{ color:var(--ink-3); text-underline-offset:3px; }
.docnav a:hover{ color:var(--ink); }
.docnav [aria-current="page"]{ color:var(--ink); font-weight:600; }

/* The index that legal.html became. Cards rather than a list, because each
   one has to carry a sentence saying what is inside -- a reader looking for
   the returns window should not have to open three pages to find it. */
.doc__lede{
  margin:0 0 2rem; max-width:60ch;
  font-size:1.02rem; line-height:1.7; color:var(--ink-2);
}
.lcards{ list-style:none; margin:2.4rem 0 0; padding:0; display:grid; gap:0; }
.lcard{
  display:grid; gap:.35rem;
  padding:1.4rem 0;
  border-top:1px solid var(--hair-2);
  color:inherit; text-decoration:none;
}
.lcards li:last-child .lcard{ border-bottom:1px solid var(--hair-2); }
.lcard__t{
  font-family:var(--display); font-weight:600; font-size:1.1rem;
  letter-spacing:-.02em; color:var(--ink);
}
.lcard__d{ font-size:.9rem; line-height:1.6; color:var(--ink-3); max-width:64ch; }
.lcard:hover .lcard__t{ color:var(--orange-2); }
.lcard:hover .lcard__d{ color:var(--ink-2); }

.doc__wrap section{ margin-bottom:2.8rem; }
.doc__wrap h2{
  font-family:var(--display); font-weight:600; font-size:1.3rem;
  letter-spacing:-.02em; margin:0 0 .9rem;
  padding-top:1.4rem; border-top:1px solid var(--hair-2);
}
.doc__wrap h3{
  font-size:1rem; font-weight:600; margin:1.6rem 0 .5rem; color:var(--ink);
}
.doc__wrap p{ margin:0 0 .9rem; color:var(--ink-2); font-size:.95rem; }
.doc__wrap strong{ color:var(--ink); font-weight:600; }
/* `a{color:inherit}` at the top would have left the contact address the same
   grey as the sentence around it - the one thing on this page a reader is
   looking for. */
/* :not(.btn) is load-bearing. Without it this rule outranks .btn's own
   `color:#14100B` - one class beats one class, and this one comes later - so
   a button placed in a paragraph on this page rendered near-white on orange
   at 2.34:1. The cancellation button is exactly that. */
.doc__wrap p a:not(.btn){ color:var(--ink); text-underline-offset:3px; }
.doc__wrap p a:not(.btn):hover{ color:var(--orange-2); }

.doc__back{ margin-top:2.5rem; }
.doc__back a{ color:var(--ink-2); text-decoration:none; }
.doc__back a:hover{ color:var(--ink); }

/* ── the legal page, part two ─────────────────────────────
   ONE box lives on these pages now: .lawnote, the statutory floor quoted
   verbatim, marked with a quiet orange left rule.

   There were three. .doc__flag said "not legal advice, and not yet checked
   by a lawyer" and .doc__todo held notes to ourselves - both were removed on
   2026-08-30, along with these rules, because a page that opens by doubting
   itself reads as a disclaimer to a customer and as an unfinished site to a
   clearing reviewer. The open questions did not vanish with them; they moved
   to README.md, which hosting does not serve.

   The rule is the quiet one on purpose. A statutory right is not an alert -
   it is the ground the promises around it stand on, and it should read as
   something carried in from outside rather than as something shouting. */

/* Sticky nav, fifteen anchors, and every one of them arrives under the bar
   without this. */
.doc__wrap section, .doc__wrap .toc{ scroll-margin-top:5.5rem; }

.doc__lede{
  font-size:1.05rem !important; line-height:1.65;
  color:var(--ink-2); margin:0 0 1.8rem !important;
}

/* ── contents ──────────────────────────────────────────── */
/* Numbered because this genuinely is a numbered document: fifteen sections,
   and "section 6" is how anyone would refer to one of them out loud. */
/* Top rule only. The first section's h2 draws its own border-top, and two
   hairlines 3rem apart read as an empty row rather than as a boundary. */
.toc{
  margin:0 0 1.6rem; padding:1.4rem 0 0;
  border-top:1px solid var(--hair-2);
}
.toc__h{
  font-family:var(--mono) !important; font-size:.66rem !important;
  font-weight:400 !important; letter-spacing:.14em; text-transform:uppercase;
  color:var(--ink-3); margin:0 0 .9rem !important;
  padding-top:0 !important; border-top:0 !important;
}
.toc__list{
  list-style:none; margin:0; padding:0;
  display:grid; gap:.42rem .5rem;
  counter-reset:toc;
  grid-template-columns:repeat(auto-fill, minmax(15rem, 1fr));
}
.toc__list li{ counter-increment:toc; display:flex; gap:.6rem; align-items:baseline; }
.toc__list li::before{
  content:counter(toc, decimal-leading-zero);
  font-family:var(--mono); font-size:.66rem; color:var(--ink-3);
  font-variant-numeric:tabular-nums; flex:none;
}
.toc__list a{
  color:var(--ink-2); text-decoration:none; font-size:.9rem;
  border-bottom:1px solid transparent; transition:color .15s, border-color .15s;
}
.toc__list a:hover{ color:var(--ink); border-bottom-color:var(--hair); }

/* ── the seller's own details ──────────────────────────── */
.idl{
  margin:1.3rem 0 1.4rem; padding:1.15rem 0 0;
  border-top:1px solid var(--hair-2);
  display:grid; gap:.8rem; font-size:.9rem;
}
.idl__row{
  display:grid; grid-template-columns:9.5rem 1fr; gap:1rem;
  align-items:baseline;
}
.idl dt{
  font-family:var(--mono); font-size:.66rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3);
}
.idl dd{ margin:0; color:var(--ink-2); line-height:1.55; }
.idl dd a{ color:var(--ink); text-underline-offset:3px; }
/* A blank that has to look like a blank. Inventing a plausible number here
   would be worse than showing the hole. */
.idl__gap{
  color:var(--orange); font-family:var(--mono); font-size:.72rem;
  letter-spacing:.06em; border-bottom:1px dashed
    color-mix(in srgb, var(--orange) 55%, transparent);
  padding-bottom:1px;
}
@media (max-width:460px){
  .idl__row{ grid-template-columns:1fr; gap:.15rem; }
}

/* ── the statutory floor ───────────────────────────────── */
.doc__wrap .lawnote{
  margin:1.3rem 0 1.2rem;
  padding:.15rem 0 .15rem 1.15rem;
  border-left:2px solid color-mix(in srgb, var(--orange) 62%, transparent);
  font-size:.92rem; line-height:1.65; color:var(--ink-2);
}
.lawnote__t{
  display:block; font-family:var(--mono);
  font-size:.63rem !important; font-weight:400 !important;
  letter-spacing:.14em; text-transform:uppercase;
  color:var(--orange) !important; margin-bottom:.42rem;
}
.doc__wrap .lawnote strong:not(.lawnote__t){ color:var(--ink); }

/* ── lists inside the document ─────────────────────────── */
.doc__ul{
  margin:0 0 1.1rem; padding:0; list-style:none;
  display:grid; gap:.5rem;
  color:var(--ink-2); font-size:.95rem;
}
/* The bullet is positioned, not a flex item. As a flex row the <li> made
   every leading <strong> its own column -- "The courier" set in a 70px gutter
   with the sentence beside it -- because a bold run and the text after it are
   two flex items. Absolute positioning leaves the line as ordinary inline
   text, which is what a sentence with a bold opening needs to be. */
.doc__ul li{ position:relative; padding-left:1.15rem; line-height:1.6; }
.doc__ul li::before{
  content:""; position:absolute; left:0; top:.62em;
  width:4px; height:4px; border-radius:50%; background:var(--ink-3);
}
.doc__ul strong{ color:var(--ink); font-weight:600; }

/* The one action on a page otherwise made entirely of prose. */
.cta-line{ margin:1.1rem 0 1.3rem !important; }

.doc__wrap code{
  font-family:var(--mono); font-size:.84em;
  color:var(--ink); background:var(--card);
  border:1px solid var(--hair-2); border-radius:5px;
  padding:.08em .34em;
}

/* The terms box on the pay page. Wider than the receipt-address box it shares
   a class with, because this one carries three links and has to sit above the
   button without crowding it. */
.chk--accept{ margin:.2rem 0 1.1rem; align-items:flex-start; }
.chk--accept .chk__label{ font-size:.87rem; line-height:1.55; }
/* ABOVE THE INPUT, OR THEY ARE NOT LINKS AT ALL.
   .chk__box is stretched over the whole label with position:absolute;inset:0
   so that anywhere in the row toggles the tick - a good trick, and one that
   silently turns every link inside the row into part of the checkbox. The
   three links here are the only way to read the terms somebody is being
   asked to accept, and a clearing review checks that the confirmation
   carries a working link to the תקנון, so they have to sit on top.
   Everything around them still toggles. */
.chk--accept .chk__label a{
  position:relative; z-index:1;
  color:var(--ink); text-underline-offset:3px;
}
.chk--accept .chk__label a:hover{ color:var(--orange-2); }

/* The cancellation right on the receipt. Ruled off rather than boxed - it is
   part of the confirmation document, not an aside beside it. */
.done__right{
  margin:1.8rem 0 1.4rem; padding-top:1.4rem;
  border-top:1px solid var(--hair-2);
}
.done__right p{ margin:0 0 .3rem; color:var(--ink-2); font-size:.92rem; line-height:1.6; }
.done__right .cta-line{ margin:.9rem 0 0 !important; }

/* ── the currency switcher ────────────────────────────────
   Only ever drawn when two currencies actually have prices, so this is
   dormant CSS until a shekel figure is set. It sits on the same line as
   the "Pre-order price" label rather than above the number: it modifies that
   number, and putting it anywhere else makes it look like a filter on the
   page instead. */
.pre__head{
  display:flex; align-items:center; justify-content:space-between;
  gap:1rem; flex-wrap:wrap;
}
.cur{
  display:inline-flex; padding:2px;
  background:var(--card); border:1px solid var(--hair); border-radius:100px;
}
.cur__b{
  appearance:none; border:0; background:none; cursor:pointer;
  font-family:var(--mono); font-size:.66rem; letter-spacing:.08em;
  padding:.42rem .7rem; border-radius:100px;
  color:var(--ink-3); transition:background-color .15s, color .15s;
  /* A full 44px target rather than a compact pill behind a
     `@media (pointer: coarse)` rule. The media query was the nicer answer and
     it is the one that got removed: headless Chrome will not report a coarse
     pointer under device emulation, so the branch could not be tested, and an
     untested rule guarding a touch target is worse than a control that is
     simply big enough everywhere. There is room for it - the price beneath is
     three times this tall. */
  min-height:44px;
}
.cur__b:hover{ color:var(--ink); }
.cur__b.is-on{ background:var(--orange); color:#14100B; }
.cur__b:focus-visible{ outline:2.5px solid var(--orange-2); outline-offset:2px; }


/* ══════════════════════════════════════════════════════════
   THE CONSENT BANNER

   Built by consent.js, not by any page's markup, so the same question is
   asked the same way on all twelve. See the head of that file for why the
   two buttons are identical and why "unanswered" is not "no".

   A bar across the foot rather than a sheet over the middle: it has to be
   answerable without hiding the page it is asking about, and somebody who
   wants to read the privacy note before deciding must be able to reach it.
   ══════════════════════════════════════════════════════════ */

.consent{
  position:fixed; inset:auto 0 0 0; z-index:120;
  padding:0 clamp(.75rem, 3vw, 1.5rem) clamp(.75rem, 3vw, 1.5rem);
  /* Nothing catches a click except the box itself, so the page underneath
     stays usable while the question sits there. */
  pointer-events:none;
}
.consent[hidden]{ display:none; }

.consent__box{
  pointer-events:auto;
  max-width:var(--wrap); margin-inline:auto;
  display:flex; align-items:center; gap:clamp(1rem, 3vw, 2.25rem);
  flex-wrap:wrap;
  background:var(--card); border:1px solid var(--hair);
  border-radius:calc(var(--r) - 4px);
  padding:clamp(.9rem, 2.5vw, 1.25rem) clamp(1rem, 3vw, 1.6rem);
  box-shadow:0 18px 50px rgba(0,0,0,.45);
  /* The from-state. consent.js flips data-in on the second frame so this has
     something to animate out of. */
  transform:translateY(12px); opacity:0;
  transition:transform .28s cubic-bezier(.2,.7,.3,1), opacity .28s ease;
}
.consent[data-in] .consent__box{ transform:none; opacity:1; }

.consent__text{ flex:1 1 22rem; min-width:0; }
.consent__h{
  margin:0 0 .3rem; font-size:1rem; font-weight:650; letter-spacing:-.01em;
  color:var(--ink);
}
.consent__p{
  margin:0; font-size:.86rem; line-height:1.55; color:var(--ink-2);
  max-width:62ch;
}
.consent__link{ color:var(--orange); }

/* Equal weight, equal size, side by side. If this ever stops being true the
   banner stops being a question - see the note in consent.js. */
.consent__acts{ display:flex; gap:.6rem; flex:0 0 auto; }
.consent__btn{
  font:inherit; font-size:.88rem; font-weight:600; letter-spacing:-.005em;
  color:var(--ink); background:var(--card-2);
  border:1px solid var(--hair); border-radius:999px;
  padding:.62rem 1.15rem; min-height:44px; cursor:pointer;
  white-space:nowrap;
  transition:background .18s ease, border-color .18s ease, color .18s ease;
}
.consent__btn:hover{ background:var(--hair); border-color:var(--ink-3); }
.consent__btn:focus-visible{ outline:2.5px solid var(--orange-2); outline-offset:2px; }

@media (max-width: 640px){
  .consent__acts{ width:100%; }
  .consent__btn{ flex:1 1 0; }
}

@media (prefers-reduced-motion: reduce){
  .consent__box{ transition:none; transform:none; opacity:1; }
}

/* A phone number that wraps mid-number reads as two broken numbers. The
   &nbsp; between the country code and the rest was not enough - the hyphens
   inside 54-215-3777 are break opportunities too. Everywhere, not just the
   footer: any tel: link is one token. */
a[href^="tel:"]{ white-space:nowrap; }

/* The three footer links sit on one line and each is a unit: "independence
   and trademarks" split across a line break read as two half-links, which is
   what the nowrap on tel: was already there to prevent for the number. The
   line itself still wraps between links on a phone - it is the links that
   must not break, not the row. */
.foot__note a{ white-space:nowrap; }
