/* ============================================================
   KRM — brand v2.1
   Companion to css/tokens.css, which is AUTHORITATIVE and is never edited
   from here. Link order is tokens.css FIRST, then this file — head() in
   tools/gen-pages.js writes them in that order. Everything below either uses
   a v2 token or is a local value this file owns and documents.

   WHAT tokens.css ALREADY SHIPS — do not restate it here:
     body (background, color, font-family, size, line-height)
     h1 / h2 / h3 weights, tracking, text-wrap
     a (colour + transition), a:hover
     .btn-primary, .btn-secondary, .surface, .panel
     :focus-visible
     @media (prefers-reduced-motion) — a blanket transition/animation kill

   This file EXTENDS those (buttons gain a 44px floor, links gain per-context
   colour) and adds everything tokens.css does not cover: layout, components,
   and the reduced-motion cases the blanket rule does not handle.

   ------------------------------------------------------------
   COLOUR USAGE RULE — READ BEFORE ADDING A COLOUR
   ------------------------------------------------------------
   --text-mute (#8A8A83) is 3.47:1 on --paper and 3.18:1 on --panel. Both FAIL
   WCAG AA for small text. It is legal ONLY for:
       - non-text (control borders, where 3:1 applies)
       - text at 18.66px+ / 24px+ bold
   EVERY caption, label, kicker, helper line and piece of metadata uses
   --text-soft (#5A5A54 — 6.94:1 on paper, 6.36:1 on panel).
   v1 carried --ink-3-aa for exactly this job; v2 has no tertiary ink that
   clears AA, so --text-soft does double duty. Do not "soften" small text by
   reaching for --text-mute.

   Other standing rules from tokens.css, restated so they are not lost:
     - No gradients — except inside a device screen, which draws the
       customer's software (brand v2.2). No drop shadows. Hairlines and
       space do the separating.
     - Sentence case everywhere. No uppercase letterspaced labels.
     - Headings are Instrument Sans 500. Never bold, never a serif.

   ------------------------------------------------------------
   ABOVE THE PHASE 2 SECTION IS THE OLD HOMEPAGE'S CSS
   ------------------------------------------------------------
   Everything in the PHASE 2 section and below serves the pages
   tools/gen-pages.js writes, which since THE SWITCH phase 6 is every page the
   site serves. Everything above it was written for the hand-written v2.0
   homepage that phase 6 replaced. Much of it matches no element any more —
   the hero, the band, the agents grid, the services accordion, the lead
   form. The rest still paints: the base nav and footer on every page, and
   the buttons, the section heads, the FAQ rows, .field and the success mark
   where those appear. None of it reads a green any more.

   v2.1 landed beside v2.0 rather than over it. Until THE SWITCH phase 7 the
   rules the two shared were overridden behind a `v21` root class, in the
   block near the end of this file; phase 7 flattened those overrides in
   place, so each is now the SAME weight as the original it overrides up
   here and wins on source order alone. Removing the dead rules up here is
   the next task, under the same guard — tools/README.md, Routine R.
   ============================================================ */

:root{
  color-scheme:light;

  /* ---- local scale ----
     v2 tokens define colour, type, --wrap, --radius and one duration. They do
     NOT define a spacing scale, a section rhythm, a measure, or the panel
     duration the accordions are locked to. Those live here. */

  /* 8px base, same rhythm v1 used so the vertical feel is unchanged */
  --sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:16px;--sp-5:24px;
  --sp-6:32px;--sp-7:48px;--sp-8:64px;--sp-9:96px;

  --gutter:clamp(20px,5vw,28px);
  --section-y:clamp(72px,9vw,120px);

  /* MEASURE — 47ch, not the 68ch v1 used.
     Instrument Sans has a tabular-width zero: 1ch = 0.666em against an average
     prose advance of 0.451em, a ratio of 1.476. So 68ch would hold ~100
     characters and blow straight through the 70-character cap the brand sets.
     47ch resolves to ~70 characters (47 x 1.476 = 69.4), and because `ch`
     scales with the element's own font-size it stays ~70 characters at every
     step of the type scale. --measure-n is the ~59-character lead measure. */
  --measure:47ch;
  --measure-n:40ch;

  /* 260ms is LOCKED — it mirrors COLLAPSE_MS in js/main.js, which sequences
     close-then-open on the services accordion. Changing this number desyncs
     rapid clicking. */
  --dur-panel:260ms;

  --nav-h:64px;
  --z-nav:40;
}

*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
/* `clip` (not `hidden`) — hidden on the body silently kills position:sticky.
   The hidden line stays first as a fallback for engines without clip. */
body{overflow-x:hidden;overflow-x:clip}

/* .wrap — --wrap is the CONTENT width (980px), exactly as tokens.css
   describes it. v1 set max-width:1120px and let 28px of padding eat into it,
   which made the real measure 1064px and meant the token never matched the
   rendered box. min() states the intent directly: never wider than --wrap,
   never closer to the edge than --gutter. */
.wrap{width:min(100% - 2*var(--gutter),var(--wrap));margin-inline:auto;position:relative}

/* anchor targets must clear the sticky bar */
section[id],form[id]{scroll-margin-top:calc(var(--nav-h) + var(--sp-5))}

img{max-width:100%;height:auto}

/* The base ring and, in tokens.css, the base link are --orange, the light-ground
   stop — and stay this weak, so any component that states its own colour wins. */
:focus-visible{outline:2px solid var(--orange);outline-offset:3px;border-radius:2px}


/* ============================================================
   BUTTONS — additive extension of tokens.css
   ============================================================
   tokens.css sets the fill, the 500 weight, the pill radius and a 13px label
   on 24px of padding. Since v2.4b phase 4 every button is the homepage's
   instead: a 17px label on a 44px pill, 22px each side and a 1px edge — the
   size the tiles and the homepage's band always had, so the same "Learn
   more" is no longer 17px on the homepage and 13px on a lineup card
   (docs/DECISIONS.md, "v2.4b"). tokens.css is not edited; this rule wins on
   source order.

   THE EDGE IS --ink. On the filled primary it is the fill's own colour and
   does not show; "Learn more" states its orange edge at the end of this
   file. On the outline secondary it is the control's boundary, 13.62:1 on
   paper, as the homepage's light tiles draw it — tokens.css's --text-mute
   was 3.47:1, clear of the 3:1 WCAG 1.4.11 asks and no more. The
   homepage's tile and band rules state all three properties at (0,2,0), so
   nothing there moves.

   THE 44x44 TOUCH MINIMUM is the min-height floor: with no vertical padding
   the pill is exactly 44px, inline-flex centring the label in it. */
.btn-primary,.btn-secondary{
  display:inline-flex;align-items:center;justify-content:center;
  min-height:44px;padding:0 22px;font-size:17px;border:1px solid var(--ink);
  text-decoration:none;white-space:nowrap;
  transition:background var(--dur) var(--ease),border-color var(--dur) var(--ease),color var(--dur) var(--ease);
}
.btn-primary:hover,.btn-secondary:hover{text-decoration:none}

.hero-cta,.home-rec-cta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3)}


/* ============================================================
   NAV — white, hairline, sentence case
   ============================================================
   v1's dark slab is retired along with --nav-*, backdrop-filter, the @supports
   fallback and the prefers-reduced-transparency block: a white bar over a
   white page needs none of them. js/nav.js is untouched — it still toggles
   .scrolled and .menu-open, and both remain live hooks below. */
.site-nav{position:sticky;top:0;z-index:var(--z-nav);background:var(--paper);
  border-bottom:1px solid var(--hairline);transition:border-color var(--dur) var(--ease)}
/* the only thing scrolling changes now is the weight of the rule underneath */
.site-nav.scrolled{border-bottom-color:var(--text-mute)}
.nav-inner{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-6);min-height:var(--nav-h)}

.brand{display:flex;flex-direction:column;line-height:1.1;text-decoration:none;color:inherit}
.brand:hover{text-decoration:none}
.brand .name{font-weight:500;font-size:20px;letter-spacing:var(--track-section);color:var(--ink)}
/* Sentence case, no letterspacing. tokens.css allows exactly one uppercase
   letterspaced label — the logo descriptor — and only "inside the logo files".
   This is live markup, not logo artwork, so the exception does not apply.
   --text-soft, not --text-mute: 12px is small text. */
.brand .sub{font-size:var(--fs-label);color:var(--text-soft);margin-top:4px}

.nav-links{display:flex;align-items:center;gap:var(--sp-6)}
.nav-link{display:inline-flex;align-items:center;min-height:44px;
  font-size:var(--fs-ui);color:var(--text-soft);text-decoration:none;
  transition:color var(--dur) var(--ease)}
.nav-cta{display:inline-flex;align-items:center;justify-content:center;min-height:44px;
  background:var(--ink);color:var(--paper);font:500 var(--fs-ui)/1 var(--sans);
  padding:11px 24px;border:1px solid var(--ink);border-radius:var(--radius-pill);
  text-decoration:none;white-space:nowrap;cursor:pointer;
  transition:background var(--dur) var(--ease),border-color var(--dur) var(--ease)}
@media(hover:hover) and (pointer:fine){
  .nav-link:hover{color:var(--ink);text-decoration:none}
  .nav-cta:hover{text-decoration:none}
}

/* ---- mobile menu ---- */
.nav-toggle{display:none;align-items:center;justify-content:center;width:44px;height:44px;flex:none;
  padding:0;background:transparent;border:none;border-radius:var(--radius);color:var(--ink);cursor:pointer}
.nav-toggle-bars{position:relative;display:block;width:18px;height:2px;border-radius:2px;background:currentColor}
.nav-toggle-bars::before,.nav-toggle-bars::after{content:"";position:absolute;left:0;width:18px;height:2px;
  border-radius:2px;background:currentColor;transition:transform var(--dur-panel) var(--ease)}
.nav-toggle-bars::before{top:-6px}
.nav-toggle-bars::after{top:6px}
/* the bars fold into an X off aria-expanded, so open state has exactly one
   source of truth — the attribute assistive tech reads is the same one that
   drives the paint */
.nav-toggle[aria-expanded="true"] .nav-toggle-bars{background:transparent}
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::before{transform:translateY(6px) rotate(45deg)}
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::after{transform:translateY(-6px) rotate(-45deg)}

/* absolute, not in flow: opening the menu must not push the page down.
   top:100% resolves against .site-nav, which is positioned by being sticky.
   The 0fr->1fr collapse is the same mechanic as the services and FAQ panels. */
.nav-mobile{display:none;position:absolute;top:100%;left:0;right:0;grid-template-rows:0fr;
  background:var(--paper);border-bottom:1px solid transparent;
  transition:grid-template-rows var(--dur-panel) var(--ease)}
.site-nav.menu-open .nav-mobile{grid-template-rows:1fr;border-bottom-color:var(--hairline)}
/* visibility, not overflow alone: a collapsed panel whose links stay focusable
   is a tab trap into invisible content. Delayed on close so it clears after
   the collapse, immediate on open. */
.nav-mobile-inner{min-height:0;overflow:hidden;visibility:hidden;transition:visibility 0s linear var(--dur-panel)}
.site-nav.menu-open .nav-mobile-inner{visibility:visible;transition-delay:0s}
.nav-mobile-link{display:block;padding:var(--sp-4) 0;font-size:var(--fs-ui);color:var(--text-soft);
  text-decoration:none;border-bottom:1px solid var(--hairline)}
.nav-mobile .nav-cta{display:flex;margin:var(--sp-5) 0 var(--sp-6)}
@media(hover:hover) and (pointer:fine){
  .nav-mobile-link:hover{color:var(--ink);text-decoration:none}
}

/* ---- nav breakpoints ----
   MUST STAY BELOW the .nav-toggle and .nav-mobile base rules. Both of those
   declare display:none at (0,1,0), which is the same specificity as the
   overrides here, so this block only wins on SOURCE ORDER. Moving it above
   them leaves the hamburger permanently hidden at every width.

   BUG FIX carried in with the rebuild. v1 hid .nav-link at <=900px but only
   revealed .nav-toggle at <=820px, so between 821px and 900px the bar had no
   links AND no hamburger — the primary navigation was unreachable. The two
   breakpoints are now the same number. js/nav.js's matchMedia moved with it,
   from (min-width:821px) to (min-width:901px), so the menu still resets at
   exactly the width the desktop bar takes over. */
@media(max-width:900px){
  .nav-link{display:none}
  .nav-toggle{display:inline-flex}
  .nav-mobile{display:grid}
  .nav-links{gap:var(--sp-3)}
}
@media(max-width:480px){.brand .sub{display:none}}


/* ============================================================
   HERO — type on white
   ============================================================
   The video, its poster stack, the wash, the bottom fade, the source-picking
   script and the EKG wave are all gone. Nothing is layered, nothing is out of
   flow, and the section carries no background of its own: type and space do
   the work. Left-aligned rather than v1's centred block — centred display type
   over a full-bleed video was a composition for imagery that no longer
   exists. */
.hero{padding:clamp(64px,10vw,116px) 0 clamp(56px,8vw,96px)}
/* 24ch resolves to ~35 characters, so the 72-character headline balances onto
   two or three lines rather than running the full 980px measure at the 56px cap */
.hero h1{max-width:24ch;color:var(--ink)}
.hero .sub-lead{margin-top:var(--sp-5);max-width:var(--measure);
  font-size:var(--fs-section);line-height:1.5;color:var(--text-soft)}
.hero .hero-cta{margin-top:var(--sp-7)}


/* ============================================================
   BAND — three statements under the hero
   ============================================================
   Full-bleed: the section sits outside .wrap and carries its own inner .wrap,
   so it reaches both edges with no 100vw (which over-measures by the scrollbar
   width on Windows) and no negative margins.

   The hairlines are LOAD-BEARING, not decoration. --panel is 1.09:1 against
   --paper: without the top and bottom rules the band does not read as a band
   at all, it reads as a rendering artefact. */
.band{background:var(--panel);border-top:1px solid var(--hairline);border-bottom:1px solid var(--hairline);
  padding:clamp(48px,6vw,72px) 0}
/* 1.15fr in the middle: breaks the dead-even three-up, and that column carries
   the longest line, so the extra width is doing real work */
.band-grid{display:grid;grid-template-columns:1fr 1.15fr 1fr}
/* dividers come from borders + symmetric padding rather than gap, so the
   hairline sits centred in the space between columns.
   --sp-6 (32px), not v1's --sp-7 (48px): at the 980px measure the three
   columns fall to 250/288/250px, and 48px of padding on both inner sides was
   consuming 10% of the container. 32px returns the columns to 270/311/270. */
.band-item{padding:0 var(--sp-6)}
.band-item:first-child{padding-left:0}
.band-item:last-child{padding-right:0}
.band-item + .band-item{border-left:1px solid var(--hairline)}
.band-item h3{font-size:var(--fs-section);color:var(--ink);margin-bottom:var(--sp-3)}
.band-item p{color:var(--text-soft);max-width:var(--measure)}
@media(max-width:760px){
  .band-grid{grid-template-columns:1fr}
  .band-item{padding:var(--sp-6) 0}
  .band-item:first-child{padding-top:0}
  .band-item:last-child{padding-bottom:0}
  .band-item + .band-item{border-left:none;border-top:1px solid var(--hairline)}
}


/* ============================================================
   SECTION FURNITURE
   ============================================================ */
.section-head{max-width:var(--measure);margin-bottom:var(--sp-8)}
.section-title{font-size:var(--fs-section);color:var(--ink)}
.section-lead{margin-top:var(--sp-4);color:var(--text-soft);max-width:var(--measure-n)}

/* The surviving kickers. Six of v1's nine were dropped because the heading
   beneath already said it; these three carry information the heading does not.
   Sentence case, no letterspacing, no text-transform, and no "//" glyph — all
   three devices are retired. --text-soft because 12px is small text. */
.kicker{font-size:var(--fs-label);color:var(--text-soft);margin-bottom:var(--sp-3)}


/* ============================================================
   AGENTS — featured agent, then the eight that stack on
   ============================================================ */
.agents-home{padding:var(--section-y) 0 0}

/* Deck under the headline. The leading mark is injected by paint() in
   js/main.js as <span class="ic">, so it has to stay styleable. Under v2.0
   it drew a short green rule; the fill went at THE SWITCH phase 7, and the
   rule matches nothing since the old homepage went. */
.pain{margin-top:var(--sp-5);max-width:var(--measure-n);font-size:var(--fs-section);
  line-height:1.5;color:var(--text-soft)}
.pain .ic{display:block;width:34px;height:2px;border-radius:2px;
  font-size:0;line-height:0;margin-bottom:var(--sp-4)}
.pain-note{margin-top:var(--sp-4);max-width:var(--measure);color:var(--text-soft)}

/* ---- the featured agent ---- */
.home-rec{padding-top:var(--sp-2)}
.home-rec-num{font-size:var(--fs-label);font-variant-numeric:tabular-nums}
.home-rec-name{font-size:var(--fs-section);color:var(--ink);margin:var(--sp-2) 0 var(--sp-4)}
.home-rec-what{max-width:var(--measure);font-size:var(--fs-section);line-height:1.5;color:var(--text-soft)}
.home-rec-how{margin-top:var(--sp-4);max-width:var(--measure);color:var(--text-soft)}
/* the line that carries the broadened positioning — the use case is the
   variable. One step quieter than .home-rec-how so it reads as a note on it. */
.home-rec-use{margin-top:var(--sp-4);max-width:var(--measure);color:var(--text-soft)}
.home-rec-cta{gap:var(--sp-5);margin-top:var(--sp-6)}

/* 13px vertical padding + a 44px floor: at --fs-ui the link's own box is about
   18px tall and misses the touch minimum on every screen. Already inline-flex,
   so the padding counts toward the box. */
.home-rec-link,.home-agents-link,.faq-link{display:inline-flex;align-items:center;min-height:44px;
  padding:13px 0;font-size:var(--fs-ui);text-decoration:none;
  transition:color var(--dur) var(--ease)}
.home-agents-link{margin-top:var(--sp-6)}
.faq-link{margin-top:var(--sp-5)}
.arw{display:inline-block;margin-left:.4em;transition:transform var(--dur) var(--ease)}
@media(hover:hover) and (pointer:fine){
  .home-rec-link:hover,.home-agents-link:hover,.faq-link:hover{text-decoration:underline;text-underline-offset:3px}
  .home-rec-link:hover .arw,.home-agents-link:hover .arw,.faq-link:hover .arw{transform:translateX(3px)}
}

/* ---- the eight that stack on after the voice agent is running ----
   The label over a hairline is the only rule between the two groups; without
   it the eight cards read as alternatives to the featured agent rather than as
   what comes next. --sp-8 above, --sp-7 below: a slight lead, because a label
   centred in its own whitespace attaches to neither side. */
.home-stack{margin-top:var(--sp-8)}
.home-stack-label{font-size:var(--fs-label);color:var(--text-soft);
  padding-bottom:var(--sp-4);border-bottom:1px solid var(--hairline);margin-bottom:var(--sp-7)}

/* The peeking robot is gone, so this row no longer needs to be a positioning
   context for it — it is a plain block again. */
.home-agents-row{display:block}

.agents{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--sp-4)}
/* `backwards` (not `forwards`) matters twice over: it lets the JS-set
   animation-delay hold the hidden state, and it hands the element back to its
   own declared values afterwards — so :hover still works and reduced-motion
   leaves the card visible rather than stuck at opacity 0. paint() in
   js/main.js sets animation-delay inline; this rule is what makes it mean
   anything. */
.agent{background:var(--panel);border:1px solid var(--hairline);border-radius:var(--radius);
  padding:var(--sp-5);text-align:left;opacity:1;transform:none;
  animation:rise 320ms var(--ease) backwards;
  transition:border-color var(--dur) var(--ease)}
@keyframes rise{from{opacity:0;transform:translateY(8px)}}
.agent .num{font-size:var(--fs-label);font-variant-numeric:tabular-nums}
.agent h3{font-size:var(--fs-section);color:var(--ink);margin:var(--sp-2) 0 var(--sp-2)}
.agent p{color:var(--text-soft)}
/* The card is rendered as an <a> when its agent has a section on /agents (see
   AGENT_SECTIONS in js/main.js). These keep it identical to the <div> version:
   the UA underline propagates to descendants, so text-decoration has to be
   killed on the card itself, not on the children. */
a.agent{display:block;text-decoration:none;color:inherit;cursor:pointer}
a.agent:hover{text-decoration:none}
/* the global ring is border-radius:2px, which would cut the card's corners */
a.agent:focus-visible{border-radius:var(--radius)}
@media(hover:hover) and (pointer:fine){
  a.agent:hover{border-color:var(--text-mute)}
  /* A plain div.agent has nothing to click, so it must not react under the
     cursor: that covers main.js's documented div fallback. (0,2,1) against
     .agent's (0,2,0), so it wins on specificity rather than source order. */
  div.agent:hover{border-color:var(--hairline)}
}

/* 5+ cards -> a fixed 4-up grid. paint() lifts FEATURED_AGENT out of the array
   and index.html renders it as its own section, so the home grid carries
   EIGHT — and eight lands as a clean 4+4. Industries with 4 agents never reach
   the threshold and keep the auto-fit single row above.
   At the 980px measure a 4-up card is 233px, comfortably clear of the 200px
   auto-fit floor. */
.agents:has(.agent:nth-child(5)){grid-template-columns:repeat(4,1fr)}
/* A 2-up step: eight halves cleanly into 2+2+2+2 with nothing stranded.
   900px is where four tracks cross the 200px floor — below a 1036px viewport
   the container is fluid, so this crossover is exactly where it was at v1's
   1064px measure and the number did not need recomputing.
   Same specificity as the rule above (0,2,0), so it wins on SOURCE ORDER — it
   must stay below it, and so must the 600px block. */
@media(max-width:900px){.agents:has(.agent:nth-child(5)){grid-template-columns:repeat(2,1fr)}}
@media(max-width:600px){.agents,.agents:has(.agent:nth-child(5)){grid-template-columns:1fr}}

/* ---- custom agents ----
   v1 gave this a tinted green panel. v2 has no tint, so the surface is gone
   entirely: a hairline above and space around it. Everything else in this column is a panel or a card,
   so bare type IS the differentiation — this reads as a note on the whole
   agents story rather than a ninth card. */
.custom-agents{margin-top:var(--section-y);padding-top:var(--sp-7);border-top:1px solid var(--hairline)}
.custom-agents h3{font-size:var(--fs-section);color:var(--ink);margin-bottom:var(--sp-3)}
.custom-agents-body{max-width:var(--measure);color:var(--text-soft)}


/* ============================================================
   INTEGRATIONS MARQUEE
   ============================================================ */
.integrations{padding:var(--section-y) 0 0}

/* --marquee-dur is a DURATION, not a speed: the track travels one group's
   width in this time, so px/s depends on how wide the group actually is.
   Change the item count, the font, or the font-size and this needs recomputing.

   MEASURED IN THE BROWSER, not estimated. With Instrument Sans 500 loaded at
   18px the six-item group renders 1215.8px wide (track 2432px, exactly 2x):

     1215.8 / 24.3s = 50.03 px/s

   which holds v1's 50 px/s, and is why this is 24.3s rather than the 27s
   Fraunces needed at 1352px. Summing raw advances from the TTF predicted
   1207px — 0.7% under the rendered figure, the difference being kerning.
   Re-measure if the item count, the font, or --marquee-fs changes. */
.marquee{--marquee-dur:24.3s;--marquee-fs:18px;
  overflow:hidden;border-top:1px solid var(--hairline);border-bottom:1px solid var(--hairline);
  padding:var(--sp-5) 0}
/* the track holds TWO identical groups and travels exactly -50%, i.e. one
   group's width, so the loop restarts on a pixel-identical frame with no seam.
   Both <ul>s must stay identical — the second is aria-hidden so screen readers
   read the list once. */
.marquee-track{display:flex;width:max-content;will-change:transform;
  animation:marquee-scroll var(--marquee-dur) linear infinite}
@keyframes marquee-scroll{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.marquee-group{display:flex;align-items:center;list-style:none;margin:0;padding:0}
/* 18px: the marquee is neither body copy nor a heading. --fs-body (15px) would
   let the strip read as a run-on sentence; --fs-section tops out at 24px and
   would tie these third-party names with the page's own h2s. 18px sits between
   the two rungs and keeps the strip subordinate. It is also the size the
   duration above was measured at — change one and you change the other. */
.marquee-group li{display:flex;align-items:center;gap:var(--sp-6);padding-right:var(--sp-6);
  white-space:nowrap;font-weight:500;font-size:var(--marquee-fs);color:var(--text-soft)}
/* separator dot, one per item, so spacing stays even across the wrap point */
.marquee-group li::after{content:"";flex:none;width:5px;height:5px;border-radius:50%}

/* Held only while the <head> gate in index.html is waiting on
   document.fonts.ready — see the comment there. Paused, NOT `animation:none`,
   so the track keeps its keyframe and resumes from 0 once metrics are final.
   Absent the class this rule never matches and the strip runs. */
.marquee-hold .marquee-track{animation-play-state:paused}


/* ============================================================
   SERVICES ACCORDION
   ============================================================
   Rows are built by js/main.js from SERVICES in js/data.js. Every class,
   id and aria attribute below is emitted by that code — see the generated
   markup in main.js:134-147. The button must stay a DIRECT CHILD of
   .service-row: main.js reads btn.parentElement to toggle .open. */
.services{padding:var(--section-y) 0 0}
/* one place to change the row rhythm — the panel indent is derived from it, so
   the description always lines up under the title */
.services-list{display:flex;flex-direction:column;gap:var(--sp-2);
  --svc-pad:var(--sp-5);--svc-num-w:40px;--svc-gap:var(--sp-5)}

.service-row{position:relative;background:var(--paper);border:1px solid var(--hairline);
  border-radius:var(--radius);overflow:hidden;
  transition:border-color var(--dur) var(--ease),background var(--dur) var(--ease)}
.service-row.open{border-color:var(--text-mute);background:var(--panel)}
/* v1 had a cursor-follow radial-gradient spotlight here. v2 permits no
   gradients, so it is gone — and with it the --mx/--my custom properties
   js/main.js still sets. Those writes are now inert, which is harmless: they
   set properties nothing reads. main.js is not edited in this phase. */

@media(hover:hover) and (pointer:fine){
  .service-row:hover{border-color:var(--text-mute)}
}

/* staggered cascade as the section scrolls into view.
   `backwards` so the row returns to its own declared transform afterwards —
   a held end-frame would outrank :hover. Depends on .services carrying BOTH
   `services` and `reveal` on the same element, because .in is added to the
   .reveal target. */
.services.in .service-row{animation:svc-in 320ms var(--ease) backwards}
.services.in .service-row:nth-child(1){animation-delay:0ms}
.services.in .service-row:nth-child(2){animation-delay:50ms}
.services.in .service-row:nth-child(3){animation-delay:100ms}
.services.in .service-row:nth-child(4){animation-delay:150ms}
.services.in .service-row:nth-child(5){animation-delay:200ms}
@keyframes svc-in{from{opacity:0;transform:translateY(10px)}}

.service-toggle{position:relative;display:flex;align-items:center;gap:var(--svc-gap);width:100%;
  min-height:44px;background:transparent;border:none;cursor:pointer;text-align:left;
  padding:var(--sp-5) var(--svc-pad);font-family:var(--sans);color:var(--ink)}
.service-num{flex:none;min-width:var(--svc-num-w);font-size:var(--fs-label);
  font-variant-numeric:tabular-nums}
/* margin-right:auto instead of flex:1 — the title keeps its natural width */
.service-title{margin-right:auto;font-weight:500;font-size:var(--fs-section);color:var(--ink);
  transition:color var(--dur) var(--ease)}
.service-icon{flex:none;display:flex;align-items:center;justify-content:center;width:32px;height:32px;
  border-radius:50%;border:1px solid var(--text-mute);background:transparent;
  font-weight:400;font-size:19px;line-height:1;color:var(--ink);
  transition:transform var(--dur-panel) var(--ease),background var(--dur) var(--ease),
             border-color var(--dur) var(--ease),color var(--dur) var(--ease)}
/* The open row's plus turns to a cross. Under v2.0 it was also the page's one
   green fill; the fill went at THE SWITCH phase 7. */
.service-row.open .service-icon{transform:rotate(45deg);color:var(--paper)}

/* the 0fr->1fr collapse. --dur-panel is LOCKED to COLLAPSE_MS in js/main.js. */
.service-panel{display:grid;grid-template-rows:0fr;transition:grid-template-rows var(--dur-panel) var(--ease)}
.service-row.open .service-panel{grid-template-rows:1fr}
.service-panel-inner{min-height:0;overflow:hidden}
.service-desc{padding:0 var(--svc-pad) var(--sp-5) calc(var(--svc-pad) + var(--svc-num-w) + var(--svc-gap));
  max-width:var(--measure);color:var(--text-soft)}
.service-toggle:focus-visible{outline-offset:-2px}
@media(max-width:560px){
  .services-list{--svc-pad:var(--sp-4);--svc-num-w:32px;--svc-gap:var(--sp-4)}
}


/* ============================================================
   HOW IT WORKS
   ============================================================ */
.how{padding:var(--section-y) 0 0}
/* three steps, three cells — straight to one column so there is never a 2+1 orphan */
.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-4)}
@media(max-width:760px){.steps{grid-template-columns:1fr}}
.step{background:var(--panel);border:1px solid var(--hairline);border-radius:var(--radius);
  padding:var(--sp-5);transition:border-color var(--dur) var(--ease)}
/* the numeral sits over a rule: these read as a sequence, not a set, and the
   fixed block keeps all three titles on one baseline */
.step .num{display:block;font-size:var(--fs-label);font-variant-numeric:tabular-nums;
  padding-bottom:var(--sp-3);margin-bottom:var(--sp-3);border-bottom:1px solid var(--hairline)}
.step h3{font-size:var(--fs-section);color:var(--ink);margin:0 0 var(--sp-2)}
.step p{color:var(--text-soft)}
@media(hover:hover) and (pointer:fine){.step:hover{border-color:var(--text-mute)}}


/* ============================================================
   BUILT FOR A BUSY SMALL BUSINESS
   ============================================================
   4 items, 4 cells, zero empties. Three columns with two span-2 cells means
   each row totals exactly 3:  [lead lead][2] / [3][wide wide] */
.built-for{padding:var(--section-y) 0 0}
.bf-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-4)}
.bf-cell{background:var(--panel);border:1px solid var(--hairline);border-radius:var(--radius);
  padding:var(--sp-5);transition:border-color var(--dur) var(--ease)}
/* v1 tinted the lead cell green. No tint in v2 — it keeps its extra
   width and padding, and that is now the whole distinction. */
.bf-cell--lead{grid-column:span 2;padding:var(--sp-6)}
.bf-cell--wide{grid-column:span 2}
.bf-cell h3{font-size:var(--fs-section);color:var(--ink);margin-bottom:var(--sp-3)}
.bf-cell p{color:var(--text-soft);max-width:var(--measure)}
@media(hover:hover) and (pointer:fine){.bf-cell:hover{border-color:var(--text-mute)}}
/* two columns: both wide cells become full rows, the two narrow ones pair up */
@media(max-width:900px){
  .bf-grid{grid-template-columns:repeat(2,1fr)}
  .bf-cell--lead{padding:var(--sp-5)}
}
/* single column — the spans must be released or they overflow the one track */
@media(max-width:600px){
  .bf-grid{grid-template-columns:1fr}
  .bf-cell--lead,.bf-cell--wide{grid-column:auto}
}


/* ============================================================
   COMMAND CENTER — type only
   ============================================================
   v1's hand-built dark mockup (.cc-panel and its ten sibling rules) was
   already dead code — index.html had replaced it with a screenshot. The
   screenshot is out too for now: it is a v1-era capture of a dark UI carrying
   an amber accent that exists nowhere in this palette, and at the 7fr column
   it would be the largest non-white mass on the page. It returns when a
   light-theme capture exists; re-adding it is additive. */
.command-center{padding:var(--section-y) 0 0}
.cc-copy{max-width:var(--measure)}
.cc-soon{margin-top:var(--sp-5)}


/* ============================================================
   LEAD FORM
   ============================================================
   STRUCTURAL CONTRACT — js/submit.js depends on all of it:
     - every field AND .lead-success are DIRECT CHILDREN of #leadForm.
       Wrapping the fields in a layout div silently defeats the .sent swap.
     - values are read by ID (lf-name, lf-email, lf-phone, lf-business), not by
       name attribute. The one load-bearing name is input[name="botcheck"].
     - .form-msg visibility is driven by the `hidden` IDL property.
     - #ind-echo must keep its text node; `hidden` stays on the WRAPPER. */
.lead{margin:var(--section-y) auto 0;max-width:560px;background:var(--paper);
  border:1px solid var(--hairline);border-radius:var(--radius);padding:var(--sp-7) var(--sp-6)}
.lead h2{font-size:var(--fs-section);color:var(--ink);text-align:center}
.lead .lead-sub{text-align:center;color:var(--text-soft);margin:var(--sp-3) 0 var(--sp-6);
  max-width:var(--measure);margin-inline:auto}

.field{margin-bottom:var(--sp-4)}
/* .field-label is the non-control caption on the read-only industry row.
   --text-soft, sentence case: these were uppercase letterspaced mono in v1. */
.field label,.field .field-label{display:block;font-size:var(--fs-label);color:var(--text-soft);
  margin-bottom:var(--sp-2)}
/* fields sit on --panel inside the --paper card, so they read as inset.
   The border is --text-mute, not --hairline: an input is a control and its
   boundary needs 3:1. --hairline is 1.17:1 and would fail. */
.field input,.field .fake{width:100%;min-height:44px;background:var(--panel);
  border:1px solid var(--text-mute);border-radius:var(--radius);padding:12px 14px;
  color:var(--ink);font-family:var(--sans);font-size:var(--fs-body)}
.field input::placeholder{color:var(--text-soft);opacity:1}
/* The same focus pair as .intake__field:focus, the textarea beside these on
   /custom-agents: --ink border, ink halo at 12%. Neutral, not orange — see
   the note there. This was the v2.0 green until THE SWITCH C2. */
.field input:focus{outline:none;border-color:var(--ink);box-shadow:0 0 0 3px rgba(46,46,43,.12)}
/* the industry echo is a div, not an input — no fill, so it never reads as
   editable. submit.js falls back to reading its textContent, so it must stay a
   text node holder. */
.field .fake{display:flex;align-items:center;background:transparent;font-weight:500}

.submit{width:100%;margin-top:var(--sp-2)}
.submit:disabled{opacity:.5;cursor:not-allowed}
@media(hover:hover) and (pointer:fine){
  .submit:disabled:hover{background:var(--ink)}
}
.assure{text-align:center;font-size:var(--fs-label);color:var(--text-soft);margin-top:var(--sp-4);line-height:1.5}

/* honeypot — off-screen, NOT display:none, so bots still fill it */
.hp{position:absolute;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none}

/* error state. v1 used a dedicated clay hue; v2's palette has seven values and
   green is the only one, so the error reads through weight and a heavier
   border rather than a second colour. --ink on --panel is 12.48:1. */
.field input.invalid{border-color:var(--ink);box-shadow:0 0 0 3px rgba(46,46,43,.12)}
.form-msg{margin-top:var(--sp-4);padding:12px 14px;border-radius:var(--radius);
  font-size:var(--fs-body);line-height:1.5}
/* submit.js toggles the `hidden` attribute — any display value above would
   defeat it. Same guard for a hidden field row. */
.form-msg[hidden]{display:none}
.field[hidden]{display:none}
.form-msg--error{color:var(--ink);background:var(--panel);border:1px solid var(--ink)}

/* ---- success state: DO NOT change these three selectors ----
   submit.js only adds .sent to the form; this direct-child rule does the whole
   swap. Wrapping the fields in a layout div would break it silently. */
.lead-success{display:none;text-align:center;padding:var(--sp-2) 0}
.lead.sent > :not(.lead-success){display:none}
.lead.sent .lead-success{display:block}
.ls-check{width:48px;height:48px;margin:0 auto var(--sp-4);display:flex;align-items:center;justify-content:center;
  border-radius:50%;background:var(--panel);border:1px solid var(--orange);font-size:22px}
.lead-success h3{font-size:var(--fs-section);color:var(--ink);margin-bottom:var(--sp-2)}
.lead-success h3:focus-visible{outline:2px solid var(--orange);outline-offset:4px}
.lead-success p{color:var(--text-soft);max-width:38ch;margin:0 auto}


/* ============================================================
   FAQ
   ============================================================
   Static markup. Rows open INDEPENDENTLY — no close-then-open timer, nothing
   coupled to COLLAPSE_MS. js/main.js reads aria-expanded as the state store
   and toggles .open on btn.parentElement, so the button must stay a DIRECT
   CHILD of .faq-row. */
.faq{padding:var(--section-y) 0 0}
/* align-items:start is what actually lets the aside stick — a stretched grid
   item fills the row and has nowhere to travel */
.faq-split{display:grid;grid-template-columns:4fr 8fr;gap:var(--sp-8);align-items:start}
.faq-aside{position:sticky;top:calc(var(--nav-h) + var(--sp-5))}

/* hairline rows, no cards and no numerals — visually distinct from the
   services accordion even though the open/close mechanic is the same */
.faq-row{border-top:1px solid var(--hairline)}
.faq-row:last-child{border-bottom:1px solid var(--hairline)}
.faq-toggle{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--sp-5);width:100%;
  min-height:44px;padding:var(--sp-5) 0;background:transparent;border:none;cursor:pointer;text-align:left;
  font-family:var(--sans)}
.faq-q{font-weight:500;font-size:var(--fs-section);color:var(--ink);transition:color var(--dur) var(--ease)}
.faq-icon{flex:none;font-size:20px;line-height:1.4;
  transition:transform var(--dur-panel) var(--ease)}
.faq-row.open .faq-icon{transform:rotate(45deg)}
.faq-toggle:focus-visible{outline:2px solid var(--orange);outline-offset:-2px}
.faq-panel{display:grid;grid-template-rows:0fr;transition:grid-template-rows var(--dur-panel) var(--ease)}
.faq-row.open .faq-panel{grid-template-rows:1fr}
.faq-panel-inner{min-height:0;overflow:hidden}
.faq-a{padding:0 var(--sp-8) var(--sp-5) 0;max-width:var(--measure);color:var(--text-soft)}
@media(max-width:860px){
  .faq-split{grid-template-columns:1fr;gap:var(--sp-6)}
  .faq-aside{position:static}
  .faq-a{padding-right:var(--sp-5)}
}


/* ============================================================
   FOOTER — white, hairline top
   ============================================================
   v1 anchored the page with a full-bleed --ink slab. That does not survive
   the v2 palette: on #2E2E2B, --text-soft is 1.96:1, v2.0's green 1.34:1 and
   --text-mute 3.92:1 — all fail AA — leaving only --paper, --panel and
   --hairline, which sit within 1.1:1 of each other and collapse the footer's
   text hierarchy to a single flat tone. A white footer keeps the hierarchy
   (--ink headings, --text-soft body), keeps its links legible, needs no
   dark-surface focus ring, and serves the ~90% white target better than the
   largest dark mass on the page would. */
.site-footer{margin-top:var(--section-y);padding:var(--sp-8) 0 var(--sp-6);
  background:var(--paper);border-top:1px solid var(--hairline);color:var(--text-soft)}
.footer-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--sp-7)}
.footer-col-title{font-size:var(--fs-label);color:var(--text-soft);margin-bottom:var(--sp-3);font-weight:500}
.footer-col ul{list-style:none;margin:0;padding:0}
.footer-col li{line-height:1.6}
/* inline-flex + a 44px floor to clear the touch minimum — these were the
   smallest targets on the site. Inline elements ignore vertical padding for
   layout, so the display change is required for the padding to enlarge the
   hit box. */
.footer-col a,.footer-bottom-links a{display:inline-flex;align-items:center;min-height:44px;
  color:var(--text-soft);text-decoration:none;transition:color var(--dur) var(--ease)}
.footer-bottom-links a{padding:0 6px}
.footer-note{color:var(--text-soft);display:inline-flex;align-items:center;min-height:44px}
@media(hover:hover) and (pointer:fine){
  .footer-col a:hover,.footer-bottom-links a:hover{color:var(--ink);text-decoration:underline;text-underline-offset:3px}
}
.footer-bottom{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--sp-3);
  margin-top:var(--sp-7);padding-top:var(--sp-5);border-top:1px solid var(--hairline);font-size:var(--fs-label)}
.footer-mark{font-weight:500;font-size:18px;letter-spacing:var(--track-section);color:var(--ink)}
.footer-copy{color:var(--text-soft)}
@media(max-width:560px){
  .site-footer{padding:var(--sp-7) 0 var(--sp-5)}
  .footer-bottom{justify-content:center;text-align:center}
}


/* ============================================================
   REVEAL
   ============================================================
   THE TRAP, AND THE FIX.

   v1 declared .reveal{opacity:0} unconditionally and relied on js/main.js to
   add .in. That meant any failure of the script — a throw above line 125, a
   blocked file, JS disabled — left the entire page body invisible: nav, hero
   and footer rendered and everything between them did not.

   The hidden state is now gated on `html.js`, which the <head> script in
   index.html sets as its first act. No JS, no `js` class, no hidden state:
   the page renders complete and static. With JS the behaviour is exactly what
   it was.

   .in is added by the IntersectionObserver at main.js:125 and is ALSO what
   drives the services stagger via `.services.in .service-row`, so
   <section class="services reveal"> must keep both classes on one element. */
.reveal{opacity:1;transform:none}
html.js .reveal{opacity:0;transform:translateY(20px);transition:opacity .6s var(--ease),transform .6s var(--ease)}
html.js .reveal.in{opacity:1;transform:translateY(0)}


/* ============================================================
   REDUCED MOTION
   ============================================================
   tokens.css already kills every transition and animation with a blanket
   !important rule. These are the cases that rule does NOT cover — states that
   must be forced visible, and the marquee, which needs rebuilding rather than
   freezing. */
@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  /* belt and braces: this is also correct with JS off, where .in never lands */
  html.js .reveal{opacity:1;transform:none}
  /* both staggered entrances declare their own visible state and use
     `backwards`, so killing the animation leaves them shown rather than stuck
     at opacity 0 */
  .agent,.services.in .service-row{opacity:1;transform:none}
  /* Stop the scroll AND unclip it. Freezing the animation alone would leave
     the track parked at translateX(0) with everything past the container edge
     permanently hidden, so the row is rebuilt as a static wrapped list. */
  .marquee-track{width:auto;flex-wrap:wrap;justify-content:center}
  .marquee-group{flex-wrap:wrap;justify-content:center;row-gap:var(--sp-3)}
  .marquee-group[aria-hidden="true"]{display:none}
  .marquee-group li:last-child{padding-right:0}
  .marquee-group li:last-child::after{display:none}
}


/* ============================================================
   INCREASED CONTRAST
   ============================================================
   Hairlines are decorative by design — --hairline is 1.17:1 on paper. When the
   user asks for more contrast, promote every divider and control edge to
   something clearly visible. --text-mute (3.47:1) is the floor for a boundary;
   --text-soft (6.94:1) is used where the line is carrying structure. */
@media(prefers-contrast:more){
  :root{--hairline:#8A8A83}
  .site-nav{border-bottom-color:var(--text-soft)}
  .band{border-top-color:var(--text-soft);border-bottom-color:var(--text-soft)}
  .agent,.step,.bf-cell,.service-row,.lead,.field input{border-color:var(--text-soft)}
  .btn-secondary{border-color:var(--ink)}
  .faq-row,.faq-row:last-child{border-color:var(--text-soft)}
  .site-footer{border-top-color:var(--text-soft)}
}


/* ════════════════════════════════════════════════════════════
   PHASE 2 — NEW TEMPLATE COMPONENTS
   ════════════════════════════════════════════════════════════
   Everything below is ADDITIVE. No selector above this line is redefined.
   Where a new component has to change the behaviour of an existing one, it
   does so through a MODIFIER CLASS compounded onto the original — e.g.
   `.site-nav--mega .nav-links`, which is (0,2,0) against the original's
   (0,1,0) and therefore wins on specificity without editing it. A page that
   does not opt in renders exactly as it did before, which is what keeps the
   six live pages byte-identical while these styles ship unused.

   IMAGERY. No component reserves space for an image it does not have. A tile
   or card with no `data-media` sizes to its type and is finished as it
   stands. Adding `data-media` later reveals `.tile-media` / `.card-media`
   and the component grows. That growth is an accepted layout change.

   COLOUR — v2.1, and free of the v2.0 greens. Every pair used, measured:
     --ink        on --paper  13.62:1     --ink        on --panel  12.48:1
     --text-soft  on --paper   6.94:1     --text-soft  on --panel   6.36:1
     --orange     on --paper   5.42:1     --orange     on --panel   4.97:1
     --paper      on --ink    13.62:1     #FFF on --ink-press      16.51:1
     --sand       on --ink    11.98:1     --ink on --sand          11.98:1
     --orange-on-ink on --ink  5.45:1     --ink on --orange-on-ink  5.45:1
     --text-mute  on --paper   3.47:1  -> BORDERS ONLY (WCAG 1.4.11, 3:1)
     --hairline   on --paper   1.17:1  -> decorative dividers only, never
                                          the sole boundary of a control
   --text-mute never carries small text.

   THE TWO STOPS ARE NEVER SWAPPED. --orange on --ink is 2.51:1 and
   --orange-on-ink on --paper is 2.50:1: both fail, and both look plausible.
   Light ground -> --orange. Solid ink ground -> --orange-on-ink. The one
   surface that takes NEITHER is the mega nav bar, which is translucent and so
   is not reliably ink at all — its ring stays --paper, and the note at
   ".site-nav--mega :focus-visible" has the measurement.

   ORANGE IS SPENT ON: links, the outline button's hover, the chip, and at
   most one small label — and, since brand v2.2, the "Learn more" primary on
   the homepage tiles and the lineup cards, the one control orange at rest
   (the last section of this file). Not on numerals, ticks, cursors, badges
   or state marks — those are --ink or --text-soft, which is why de-greening
   this file made most things neutral rather than orange.
   ════════════════════════════════════════════════════════════ */


/* ---- the hidden guard -------------------------------------------------
   REQUIRED BY THE COMMERCE SWITCH, and a real bug if omitted. The UA sheet's
   `[hidden]{display:none}` is (0,0,1); `.btn-primary{display:inline-flex}` is
   (0,1,0) and beats it. So a Buy button written `<a class="btn-primary"
   hidden>` would render VISIBLE while commerce is off — exactly the failure
   the flag exists to prevent. The two `[hidden]` rules further up this file
   patch the same hole one component at a time; this closes it everywhere.

   It cannot change the live site: the only elements currently carrying
   `hidden` are #lf-msg and the industry .field, both of which are already
   display:none by those earlier rules. */
[hidden]{display:none!important}


/* ============================================================
   SHARED — section furniture used by several new templates
   ============================================================ */

/* A full-bleed band, same construction as .band: the section sits outside
   .wrap and carries its own inner .wrap, so it reaches both edges with no
   100vw (which over-measures by the scrollbar width on Windows). */
.bleed{background:var(--panel);border-top:1px solid var(--hairline);
  border-bottom:1px solid var(--hairline);padding:var(--section-y) 0}

/* The Book-a-call band that closes every lineup and product page. */
.cta-band{margin-top:var(--section-y);padding:var(--sp-8) 0;
  border-top:1px solid var(--hairline);text-align:center}
.cta-band__title{font-size:var(--fs-section);color:var(--ink)}
.cta-band__lead{margin:var(--sp-3) auto 0;max-width:var(--measure);color:var(--text-soft)}
.cta-band__actions{display:flex;flex-wrap:wrap;gap:var(--sp-3);justify-content:center;
  margin-top:var(--sp-6)}

/* THE HOMEPAGE'S BAND. Its one button is the last thing on a page of seven
   tiles whose buttons are 17px on 44px, so it was given that size while the
   site default was 13px — a smaller control at the end reads as a lesser
   offer. Since v2.4b phase 4 every button is that size (the buttons block
   above), so this rule restates the base declaration for declaration. It
   stays as it is with the rest of the homepage, and goes with the dead-CSS
   task. */
.cta-band--home .btn-primary{padding:0 22px;font-size:17px;
  border:1px solid var(--ink)}

/* Build status, rendered from CATALOG.buildStatusLabels. A quiet factual
   chip — never green, never a badge shouting "NEW". --text-soft on --panel
   is 6.36:1, so it clears AA as small text. */
.status{display:inline-flex;align-items:center;gap:var(--sp-2);
  padding:4px 10px;border-radius:var(--radius-pill);background:var(--panel);
  border:1px solid var(--hairline);font-size:var(--fs-label);color:var(--text-soft)}

/* A standing caveat line — A2P, "sold alongside a voice plan", and similar.
   Not an error, not a warning: a note. */
.note-line{margin-top:var(--sp-4);max-width:var(--measure);
  font-size:var(--fs-label);color:var(--text-soft)}

/* NO WORD ALONE, v2.4b phase 4. The short copy that repeats from page to
   page never leaves a word on a line of its own: the guarantee line and its
   points, the use-case price note, the /book and /search leads, the lineup
   and plan cards' lines, the notes and footnotes, the FAQ, and the two
   thread heroes' bubbles. By CSS alone and never by rewording — the owner's
   rule (docs/DECISIONS.md, "v2.4b"). Body text keeps its breaks.

   pretty, not balance: in Chrome it moves a break only where the last line
   would hold a single word, and every other line stays where it was —
   measured on all 28 pages from 320 to 1920 before it shipped, with no
   page's height changed. Safari 26 weighs the whole paragraph and may move
   more; Firefox ignores it.

   .price--contact is the block, not its .price__amount: in the pricing
   section the note is an inline span, and the break is its block's to make.
   Every FAQ answer, because CSS cannot single out "Can I cancel?"; the rest
   move only where they too ended on a word alone. The bubbles are the
   heroes' alone, so the homepage tiles keep the breaks they were approved
   with. tools/overflow-check.html holds the result against its own copy of
   this list (LONE). */
.guarantee__lead,.guarantee__list li,.price--contact,
.booking__lead,.search-lead,
.lineup-card__line,.plan-card__does,.note-line,.price__foot,
.faq-q,.faq-a,
.p-hero__media--app .scr--thread .scr-msg{text-wrap:pretty}

/* Media slots. Hidden until the component carries data-media, per the
   imagery rule — no empty boxes, no grey placeholders, no reserved space. */
.tile-media,.card-media,.rail-item__img{display:none}
[data-media] > .tile-media,
[data-media] > .card-media{display:block;margin-bottom:var(--sp-5);
  border-radius:var(--radius);overflow:hidden}
[data-media] > .rail-item__img{display:block}


/* ============================================================
   1. MEGA-MENU
   ============================================================
   Opt-in via `.site-nav--mega` on the existing <header class="site-nav">.
   js/nav.js is untouched and still owns `.scrolled` and `.menu-open`; the
   panels below are driven by `.is-open`, the flyout by its height, and the
   surface by `.mega-open` and `.mega-closing`, all set by js/mega.js. Every
   rule here is a NEW compound selector, so the live four-link bar is
   untouched.

   apple.com's DARK PATTERN. A 44px bar of ink over a 20px blur, saturated
   180%, on every page and at every scroll position: the bar never changes
   with what is under it, and no script decides its colour. --nav-h is
   scoped to the header, so everything that measures from the bar — the
   flyout, the sheet, the curtain — follows it, and every other page keeps
   its 64px.

   ONE MATERIAL, TWO DENSITIES. --mat-c carries the material, a registered
   colour, and every surface reads it, so bar, flyout and sheet cannot
   disagree:
     rest   grey glass, rgba(71,71,69,.8) — the bar alone
     open   ink at 96% — the bar AND the flyout, or the bar and the sheet,
            as one surface while a panel or the sheet is open
   THE REST IS GLASS, apple.com's dark nav (homepage v2.4a): Apple's own
   stylesheet gives rgba(22,22,23,.8) with the same saturate(180%) blur(20px),
   .88 only where the browser cannot blur, and no contrast override. Until
   v2.4a the rest was ink at 70%, and over the homepage's ink tiles — the
   first tile starts directly under the bar — ink at 70% over ink composites
   to exactly ink: the bar read as solid. This grey is the one whose
   composite over paper is exactly #6C6C6A, what the bar showed before, so
   a white page does not change by a pixel; over an ink tile it is #424240,
   glass and not ink. (theme-color carried #6C6C6A too, until 5 October
   2026; it is the page's white now — see "THE EDGE MARKER" below.)
   Near-opaque while open because the flyout drops over whatever the page
   has under it — the homepage's first tile is a dark ground with white
   type and a white phone screen — and at rest density that reads through as
   smears behind the links. @property makes --mat-c animatable, so the two
   densities fade into each other rather than jumping; js/mega.js leaves the
   open density only once the flyout has finished collapsing, never
   mid-close.

   THE MATERIAL IS A LAYER, NOT THE HEADER. backdrop-filter makes its element
   the containing block for position:fixed descendants, and the mobile sheet
   and the curtain are both fixed to the viewport. On the header it would
   pin both inside a 44px box. So the header stays transparent and ::before
   carries the glass and the blur behind the bar's content; the flyout and
   the sheet carry their own. It is glass for everyone and never solid:
   denser where backdrop-filter is unsupported or the reader asks for less
   transparency or more contrast — see the end of this section.

   SPREAD EVENLY, apple's way: the wordmark, the seven labels and the search
   icon sit at equal intervals across the 980px container. .nav-links takes
   the width beside the wordmark and spaces its items apart, and its empty
   ::before is the first of them — so the gap from the wordmark to the first
   label is the same as every gap after it.

   12px LABELS, WHITE AT 90%. The bar's worst backdrop is white paper, and
   that is also its usual one: every product and lineup page is white under
   it. The glass over paper is #6C6C6A, where white at 80% would be 4.05:1 —
   under AA. At 90% it is 4.64:1 over paper and 4.77:1 over --panel, 8.53:1
   with an ink tile under the bar, 8.40:1 over the green and 9.75:1 over the
   black bezel of a device plate. Full white on hover, and while the label's
   panel is open — no underline, no rule. */
@property --mat-c{syntax:"<color>";inherits:true;initial-value:rgba(71,71,69,.8)}
.site-nav--mega{--nav-h:44px;--mat-c:rgba(71,71,69,.8);background:transparent;border-bottom:0;
  transition:--mat-c 200ms var(--ease)}
.site-nav--mega.mega-open,.site-nav--mega.menu-open{--mat-c:rgba(46,46,43,.96)}
.site-nav--mega::before{content:"";position:absolute;inset:0;z-index:-1;
  background:var(--mat-c);
  -webkit-backdrop-filter:saturate(180%) blur(20px);backdrop-filter:saturate(180%) blur(20px)}

/* THE EDGE MARKER — the white strip above the bar on iOS 26 (5 October
   2026). The strip behind the clock is the page's white above the dark
   bar, as on apple.com; theme-color says so to iOS 15–18 and Android, and
   iOS 26 Safari ignores it. Safari 26 hit-tests 4px in from the middle of
   the top edge, walks up to the first fixed or sticky box — this header —
   and paints the first plain background-color it meets on the way into the
   strip. The header's own background is transparent, and the glass is on
   ::before, which a hit test reports as the header, so Safari found no
   colour, sampled the glass's pixels instead and kept them: #6C6C6A.

   This box gives it the page's white to read, and paints nothing:
     12px tall           WebKit skips a box 10px or less as a thin border
     height 0            and the background is clipped to the content box,
                         so not one pixel of it is drawn
     full width          WebKit counts a box only at 90% of the viewport
     z-index 1           the hit lands on it, not on a label below it
     pointer-events none taps and hover pass through; Safari's sampling
                         ignores pointer-events
   The header's last child, empty and aria-hidden. tools/audit.js check 21
   holds this rule byte for byte, beside theme-color. */
.site-nav--mega .nav-edge{position:absolute;inset:0 0 auto;z-index:1;height:0;padding-top:12px;
  background:var(--paper) content-box;pointer-events:none}
.site-nav--mega .nav-inner{gap:0}
.site-nav--mega .nav-links{flex:1;justify-content:space-between;gap:0}
.site-nav--mega .nav-links::before{content:""}
.site-nav--mega .nav-link{font-size:12px;color:rgba(255,255,255,.9)}
.site-nav--mega .nav-utility{margin-left:0}
.site-nav--mega .nav-icon-btn,.site-nav--mega .nav-toggle{color:rgba(255,255,255,.9)}
.site-nav--mega .nav-icon-btn svg{width:15px;height:15px}
@media(hover:hover) and (pointer:fine){
  .site-nav--mega .nav-link:hover,.site-nav--mega .nav-icon-btn:hover{color:var(--paper);background:transparent}
}
@media(max-width:900px){
  .site-nav--mega .nav-links{justify-content:flex-end;gap:var(--sp-2)}
  .site-nav--mega .nav-links::before{content:none}
}

/* ---- focus, and only from the keyboard ----
   THE RING IN THE NAV IS PAPER, AND STAYS PAPER UNDER v2.1. Everywhere else
   on an ink ground the ring becomes --orange-on-ink, but not here, and the
   reason is that this bar is TRANSLUCENT. At rest it is glass — grey at 80%
   since homepage v2.4a, ink at 70% until then — so the bar's real colour is
   whatever is under it: over paper it composites to #6C6C6A, and
   --orange-on-ink on that is 2.11:1 — under the 3:1 a focus indicator needs.
   Paper on the same composite is 5.28:1, and 12.06:1 on the open density.
   An ink-ground token is the wrong tool for a surface that is not reliably
   ink.

   (Under v2.0 the reason was different and simpler: its green was 1.34:1 on ink.
   The conclusion is the same and the rule does not move.)

   It is inset, not offset: a 44px label fills the 44px bar, and an offset ring
   would have its top edge off the screen and its bottom under the flyout —
   which is what the two thin lines beside a label were. :focus-visible only,
   and js/mega.js keeps a mouse press from focusing anything here, so a ring
   means the keyboard and nothing else. */
.site-nav--mega :focus-visible,
.site-nav--mega .nav-acc-toggle:focus-visible,
.site-nav--mega .nav-acc-link:focus-visible{outline-color:var(--paper)}
.site-nav--mega .nav-link:focus-visible,
.site-nav--mega .nav-icon-btn:focus-visible,
.site-nav--mega .brand:focus-visible{outline-offset:-2px}
.site-nav--mega :focus:not(:focus-visible){outline:none}

/* ---- the wordmark ----
   Replaces the typeset "KRM" with the supplied artwork. This is a NEW class,
   so index.html — which still renders <span class="name">KRM</span> — is
   untouched and keeps its type until the Batch 8 nav switch.

   THE BAKED-IN BORDER, MEASURED.
     image box     2198 x 1036
     letters       1708 x  546, at offset 245,245
     padding       245px on all four sides, uniform
   So the letters are 546/1036 = 0.5270 of the image height, and the padding
   is 245/1036 = 0.2365 of it on every side once scaled uniformly.

   The image file is NOT edited and NOT cropped. Instead --wordmark-box is
   set to the height the IMAGE must be for the LETTERS to land at the height
   we want:  box = letters / 0.5270.
     nav and footer   letters 18px  ->  box 34px
   width follows from the aspect ratio (2198/1036) and needs no declaration.

   The transparent border IS the guide's clear space — 8px at this size — so
   nothing extra is added around it. The negative inline-start margin cancels
   only the LEFT band, so the letters align optically with the content edge
   below rather than sitting 8px indented. It is derived from the same ratio,
   so changing --wordmark-box keeps the alignment correct. */
/* flex:none is the SECOND half of the same fix. .brand is itself a flex item
   of .nav-inner and shrinks by default; once it is narrower than the mark,
   `img{max-width:100%}` clamps the width while height stays fixed, squashing
   the letters again (aspect 1.885 against the true 2.121). Both declarations
   are needed — one stops the stretch, one stops the squash. */
/* --wordmark-box and flex:none live on the MARK and on any .brand that
   contains one, not on .site-nav--mega — so the mark works in the current
   four-link nav too, which is what the unlinked Batch 3+ pages paste in
   while waiting for the Batch 8 switch. Neither selector can match a live
   page: none of them contains a .brand-mark. */
.brand:has(.brand-mark){flex:none}
/* align-self is LOAD-BEARING, not tidiness. .brand is a flex COLUMN, whose
   default align-items:stretch makes the image fill the column width — which
   is set by the descriptor beneath it. That stretched the mark to 170x34
   against its true 72x34, distorting the letters by 2.4x horizontally.
   flex-start lets width:auto resolve from the aspect ratio instead.

   max-width:none is the THIRD part, and the least obvious. The negative
   start margin reduces the mark's OUTER width, so a .brand sized to its
   content lands at 72.1 - 8.04 = 64.06px. The file-wide `img{max-width:100%}`
   then resolves 100% against that 64.06px and clamps the image, while the
   explicit height holds — squashing the letters to aspect 1.885 again. The
   desktop nav hid this because the descriptor made .brand wider than the
   mark; the mobile sheet, which has no descriptor, showed it. */
.brand-mark{--wordmark-box:34px;
  display:block;align-self:flex-start;flex:none;max-width:none;
  height:var(--wordmark-box);width:auto;
  margin-inline-start:calc(var(--wordmark-box) * -0.2365)}
/* NO DESCRIPTOR UNDER THE MARK. The brand guide treats "Karame Resource
   Management" as part of the artwork, and below 120px the wordmark is used
   alone — the nav mark is 72px. So the new nav, mobile sheet and footer
   carry the wordmark by itself, and there is no .sub to position here.
   (The v1 `.brand .sub` rules further up this file still serve index.html
   until the Batch 8 switch; they are not touched.) */
@media(max-width:480px){.brand-mark{--wordmark-box:30px}}

/* footer mark — same artwork, same letter height, no descriptor beneath */
.footer-mark-img{--wordmark-box:34px;display:block;flex:none;align-self:center;max-width:none;
  height:var(--wordmark-box);width:auto;
  margin-inline-start:calc(var(--wordmark-box) * -0.2365)}

/* The trigger stays an <a href="/voice-agents"> so it works with JS off and
   is a real link for crawlers. aria-expanded is added by js/mega.js.
   No underline and no rule when its panel is open, as on apple.com: the
   label goes to full ink, and the open panel under it is the cue that
   cannot be missed. */
.nav-link--has-mega{position:relative}
.nav-link--has-mega[aria-expanded="true"]{color:var(--ink)}

/* ---- search + cart controls ---- */
.nav-utility{display:flex;align-items:center;gap:var(--sp-2);margin-left:var(--sp-2)}
.nav-icon-btn{position:relative;display:inline-flex;align-items:center;
  justify-content:center;width:44px;height:44px;flex:none;padding:0;
  background:transparent;border:none;border-radius:var(--radius);
  color:var(--text-soft);cursor:pointer;
  transition:color var(--dur) var(--ease),background var(--dur) var(--ease)}
.nav-icon-btn svg{width:18px;height:18px;display:block}
.nav-icon-btn:hover{text-decoration:none}
@media(hover:hover) and (pointer:fine){
  .nav-icon-btn:hover{color:var(--ink);background:var(--panel)}
}
/* Cart count. It sits ON THE INK BAR, so it takes the ink ground's values:
   a --sand fill with --ink numerals, 11.98:1 both ways. It is not orange —
   v2.1 spends orange on links, the outline button's hover and the chip, and a
   count badge is none of those. (It was a green fill under v2.0.) */
.nav-icon-btn__badge{position:absolute;top:4px;right:4px;min-width:16px;height:16px;
  display:flex;align-items:center;justify-content:center;padding:0 4px;
  border-radius:var(--radius-pill);background:var(--sand);color:var(--ink);
  font-size:10px;font-variant-numeric:tabular-nums;line-height:1}

/* ---- the desktop panel: one flyout, seven panels ----
   THE FLYOUT IS THE SURFACE, and there is only one of it: the same ink and
   blur as the bar, at the same density, from the bar's bottom edge down and
   as tall as the open panel. The seven panels sit inside it, stacked at its
   top, and only fade. So the bar and the panel are one material at every
   moment, and switching category keeps the surface — it moves to the next
   panel's height while the links cross-fade.

   NOTHING IS EVER HALF-CUT. The flyout clips, but the links inside are
   faded out before it collapses and faded in only once it has grown past
   them — the --in delay below, which js/mega.js sets to 200ms while the
   flyout grows (all that is still hidden by then is the panel's own 84px
   bottom margin) and to 120ms when it shrinks or stays, which is exactly
   when the outgoing panel's links have gone.

     open     .mega-open — bar and flyout to the open density together, the
              flyout grows, the curtain fades in, then the links
     close    .mega-closing — the links fade (120ms), THEN the flyout
              collapses (--dur-panel) with the curtain fading in step;
              js/mega.js drops .mega-open only when that has finished, so
              the bar never returns to rest mid-close

   Absolute, not in flow: opening must not push the page down. top:100%
   resolves against .site-nav, which is positioned by being sticky. */
.nav-flyout{position:absolute;top:100%;left:0;right:0;z-index:var(--z-nav);
  height:0;overflow:hidden;
  background:var(--mat-c);
  -webkit-backdrop-filter:saturate(180%) blur(20px);backdrop-filter:saturate(180%) blur(20px);
  transition:height var(--dur-panel) var(--ease)}
.site-nav--mega.mega-closing .nav-flyout{transition-delay:120ms}
/* visibility, not opacity alone: a faded panel whose links stay focusable
   is a tab trap into invisible content. Hidden once the fade has run,
   visible at once on open, so the keyboard can move straight in. */
.nav-mega{position:absolute;top:0;left:0;right:0;
  opacity:0;visibility:hidden;
  transition:opacity 120ms var(--ease),visibility 0s linear 120ms}
.nav-mega.is-open{opacity:1;visibility:visible;
  transition:opacity 120ms var(--ease) var(--in,200ms),visibility 0s}
.nav-mega-pad{padding:40px 0 84px}

/* ---- the panel's two columns, apple's flyout ----
   A 12px label, paper at 72%, over each list: "Explore <category>" over the
   product names at 24px semibold paper, then "Quick links" at 12px semibold.
   No descriptions, no taglines, no prices.

   The product names keep the 44px floor everywhere. The quick links sit at
   apple's tighter pitch only where the pointer is fine and can hover — a
   mouse; the brand's 44px floor is a TOUCH target, and on anything else
   they keep it.

   The links only ever show on the open density, so their contrast is read
   against that: paper 12.06:1, the 12px labels at 72% 7.11:1, and the
   hover, which dims to 80% — under v2.0, because its green was 1.34:1 on
   ink — 8.36:1. */
.nav-mega-cols{display:flex;flex-wrap:wrap;gap:0 88px}
.nav-mega-label{margin-bottom:12px;font-size:12px;color:rgba(255,255,255,.72)}
.nav-mega-items,.nav-mega-quick{list-style:none;margin:0;padding:0}
.nav-mega-item{display:flex;align-items:center;min-height:44px;
  font-size:24px;font-weight:600;line-height:1.17;color:var(--paper);text-decoration:none}
.nav-mega-quick a{display:flex;align-items:center;min-height:44px;
  font-size:12px;font-weight:600;color:var(--paper);text-decoration:none}
.nav-mega-item:hover,.nav-mega-quick a:hover{text-decoration:none}
@media(hover:hover) and (pointer:fine){
  .nav-mega-item:hover,.nav-mega-quick a:hover{color:rgba(255,255,255,.8)}
  .nav-mega-quick a{min-height:28px}
}

/* ---- the curtain ----
   While a panel is open the page under it is blurred and lightly dimmed,
   and a click on it shuts the panel (js/mega.js). Fixed from the bar down,
   inside the header's stacking context at z-index -1: under the bar and the
   flyout, over the page. It is decorative — aria-hidden, never focusable —
   and invisible until .mega-open lands on the header.

   It fades in as the flyout grows, and out IN STEP WITH THE COLLAPSE: the
   same 120ms wait for the links to go, then the same --dur-panel. Its
   visibility follows at the end of that, so a shut curtain never eats a
   click meant for the page. */
.nav-curtain{position:fixed;top:var(--nav-h);left:0;right:0;bottom:0;z-index:-1;
  background:rgba(46,46,43,.12);
  -webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);
  opacity:0;visibility:hidden;
  transition:opacity var(--dur-panel) var(--ease),visibility 0s linear var(--dur-panel)}
.site-nav--mega.mega-open .nav-curtain{opacity:1;visibility:visible;transition-delay:0s}
.site-nav--mega.mega-open.mega-closing .nav-curtain{opacity:0;visibility:hidden;
  transition:opacity var(--dur-panel) var(--ease) 120ms,
             visibility 0s linear calc(120ms + var(--dur-panel))}

/* ---- the Batch 2 panel, for the styleguide ----
   tools/styleguide.html still draws the first version of the panel — a
   title, a grid of names with a line and a price each, and a foot — so
   these rules stay for it. The generated pages do not use them. */
.nav-mega-title{font-size:var(--fs-label);color:var(--text-soft);margin-bottom:var(--sp-5)}
/* The GRID GOES ON THE LIST, not on a wrapper around it. With
   grid-template-columns on .nav-mega-grid, the <ul> is a single grid item —
   auto-fit fills one track, every product stacks in one narrow column, and
   two thirds of the panel sits empty. Putting the tracks on the <ul> makes
   each <li> a grid item, which is what auto-fit needs to distribute. */
.nav-mega-grid{display:block}
/* 200px + a --sp-6 column gap so FOUR tracks fit the 980px measure:
   4x200 + 3x32 = 896. At 230px and --sp-7 only three fitted
   (4x230 + 3x48 = 1064), which left Voice Agents — four products, the lead
   category and the most-opened menu — as 3 + 1 with an empty second row.
   Categories with 2, 3 or 5 products sit ragged in the same tracks, which is
   normal for a link list and not the same problem as a stranded card. */
.nav-mega-list{list-style:none;margin:0;padding:0;
  display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
  gap:var(--sp-5) var(--sp-6)}

.nav-mega-link{display:flex;flex-direction:column;gap:2px;min-height:44px;
  justify-content:center;padding:var(--sp-2) 0;text-decoration:none;color:inherit;
  border-radius:var(--radius)}
.nav-mega-link:hover{text-decoration:none}
.nav-mega-link__name{font-size:var(--fs-body);font-weight:500;color:var(--ink);
  transition:color var(--dur) var(--ease)}
.nav-mega-link__line{font-size:var(--fs-label);color:var(--text-soft);
  max-width:34ch;line-height:1.5}
.nav-mega-link__price{font-size:var(--fs-label);color:var(--text-soft);
  font-variant-numeric:tabular-nums}
/* This panel is the styleguide's light one, so both of these take --orange,
   the light-ground stop: 5.42:1 on paper. */
@media(hover:hover) and (pointer:fine){
  .nav-mega-link:hover .nav-mega-link__name{color:var(--orange)}
}
.nav-mega-foot{margin-top:var(--sp-6);padding-top:var(--sp-5);
  border-top:1px solid var(--hairline);display:flex;flex-wrap:wrap;gap:var(--sp-5)}
.nav-mega-foot a{display:inline-flex;align-items:center;min-height:44px;
  font-size:var(--fs-ui);color:var(--orange)}

/* ---- the mobile panel ----
   Full-screen from the bar down, per Apple, in the bar's own material at
   the open density — one surface with the bar above it. This REPLACES the
   height collapse for the mega variant: a fixed, full-height sheet has no
   height to animate, so it fades instead. The accordion rows inside it keep
   the 0fr->1fr mechanic. */
@media(max-width:900px){
  .site-nav--mega .nav-mobile{display:block;position:fixed;
    top:var(--nav-h);left:0;right:0;bottom:0;
    grid-template-rows:none;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;
    background:var(--mat-c);
    -webkit-backdrop-filter:saturate(180%) blur(20px);backdrop-filter:saturate(180%) blur(20px);
    border-bottom:none;
    opacity:0;visibility:hidden;
    transition:opacity var(--dur-panel) var(--ease),visibility 0s linear var(--dur-panel)}
  .site-nav--mega.menu-open .nav-mobile{opacity:1;visibility:visible;transition-delay:0s}
  /* the inner wrapper's clip and hidden visibility are for the height
     collapse, which this variant does not use.
     INHERIT, NOT VISIBLE. An explicit `visible` on a child overrides a
     hidden parent, so the shut sheet — opacity 0, but still laid over the
     page from the bar down — caught every tap on the page beneath it and
     took Tab into links nobody could see. Inheriting lets .nav-mobile's own
     visibility decide, shut and open. */
  .site-nav--mega .nav-mobile-inner{overflow:visible;visibility:inherit;transition:none}
  .site-nav--mega .nav-mobile-pad{padding:var(--sp-5) 0 var(--sp-8)}
}

/* search field at the top of the mobile sheet */
.nav-mobile-search{display:flex;align-items:center;gap:var(--sp-3);width:100%;
  min-height:44px;margin-bottom:var(--sp-5);padding:0 14px;
  background:var(--panel);border:1px solid var(--text-mute);border-radius:var(--radius);
  color:var(--text-soft);font-family:var(--sans);font-size:var(--fs-body);
  text-align:left;cursor:pointer}
.nav-mobile-search svg{width:16px;height:16px;flex:none}

/* accordion category rows */
.nav-acc{border-bottom:1px solid var(--hairline)}
.nav-acc-toggle{display:flex;align-items:center;justify-content:space-between;
  gap:var(--sp-4);width:100%;min-height:48px;padding:var(--sp-4) 0;
  background:transparent;border:none;cursor:pointer;text-align:left;
  font-family:var(--sans);font-size:var(--fs-section);font-weight:500;color:var(--ink)}
/* The light accordion's plus, which only tools/styleguide.html still draws —
   the generated pages use .nav-acc-chevron on the dark sheet. --orange, with
   everything else on a light ground: 5.42:1. */
.nav-acc-icon{flex:none;font-size:18px;line-height:1;color:var(--orange);
  transition:transform var(--dur-panel) var(--ease)}
.nav-acc-toggle[aria-expanded="true"] .nav-acc-icon{transform:rotate(45deg)}
/* The mega sheet, apple's light list: the seven category names at 28px
   semibold, no rules between the rows, and a chevron in place of the plus —
   down while a row is shut, turned up once it opens. The links under an
   open row are 17px. */
.site-nav--mega .nav-acc{border-bottom:none}
.site-nav--mega .nav-acc-toggle{min-height:52px;padding:var(--sp-2) 0;
  font-size:28px;font-weight:600;line-height:1.14}
.nav-acc-chevron{flex:none;width:20px;height:20px;color:var(--text-soft);
  transition:transform var(--dur-panel) var(--ease)}
.nav-acc-toggle[aria-expanded="true"] .nav-acc-chevron{transform:rotate(180deg)}
.site-nav--mega .nav-acc-link{font-size:17px;color:var(--paper)}
/* the sheet's own colours on the dark material: rows and links paper
   (12.06:1), chevrons and the search row's text paper at 72% (7.11:1 and
   5.81:1 on the row's fill), its edge paper at 50% (3.71:1, where a control
   boundary needs 3:1), and the Book a call pill inverted, as on a dark
   tile */
.site-nav--mega .nav-acc-toggle{color:var(--paper)}
.site-nav--mega .nav-acc-chevron{color:rgba(255,255,255,.72)}
.site-nav--mega .nav-mobile-search{background:rgba(255,255,255,.08);
  border-color:rgba(255,255,255,.5);color:rgba(255,255,255,.72)}
.site-nav--mega .nav-mobile .nav-cta{background:var(--paper);border-color:var(--paper);color:var(--ink)}
@media(hover:hover) and (pointer:fine){
  .site-nav--mega .nav-mobile .nav-cta:hover{background:var(--panel);border-color:var(--panel);color:var(--ink)}
}
.nav-acc-panel{display:grid;grid-template-rows:0fr;
  transition:grid-template-rows var(--dur-panel) var(--ease)}
.nav-acc-toggle[aria-expanded="true"] + .nav-acc-panel{grid-template-rows:1fr}
.nav-acc-panel-inner{min-height:0;overflow:hidden}
/* A shut row's links must not take focus. The 0fr collapse hides them but
   leaves them tabbable — the trap .nav-mega-inner and .nav-mobile-inner
   already guard against with visibility. Same fix, same timing: hidden once
   the collapse has run, shown at once on open. `inherit` when open, never
   `visible`, so a row left open inside a shut sheet stays hidden with it. */
.nav-acc-panel-inner{visibility:hidden;transition:visibility 0s linear var(--dur-panel)}
.nav-acc-toggle[aria-expanded="true"] + .nav-acc-panel .nav-acc-panel-inner{
  visibility:inherit;transition-delay:0s}
.nav-acc-link{display:flex;flex-direction:column;gap:2px;justify-content:center;
  min-height:44px;padding:var(--sp-3) 0 var(--sp-3) var(--sp-4);
  text-decoration:none;color:inherit}
.nav-acc-link:last-child{margin-bottom:var(--sp-4)}
.nav-acc-link__name{font-size:var(--fs-body);color:var(--ink)}
.nav-acc-link__price{font-size:var(--fs-label);color:var(--text-soft);
  font-variant-numeric:tabular-nums}
/* --orange, the LIGHT-ground stop, because this base rule only ever paints on
   a light ground: wherever these rows sit on the dark sheet they are inside
   .site-nav--mega, whose own ring rule above is (0,3,0) and takes the ring to
   --paper. See the note there for why the nav's ring must stay paper. */
.nav-acc-toggle:focus-visible,.nav-acc-link:focus-visible{outline:2px solid var(--orange);
  outline-offset:-2px;border-radius:2px}
/* The sheet's search row is a link to /search on a real page, not the button
   the styleguide draws, and its rows are links too. Neither takes the
   underline every other link gets on hover. */
.nav-mobile-search:hover,.nav-acc-link:hover{text-decoration:none}
/* Below the bar's breakpoint the sheet replaces the panels. They are shut
   there already; this stops one — or its curtain — showing if the window
   narrows while it is open, before js/mega.js hears the breakpoint pass. */
@media(max-width:900px){.nav-flyout,.nav-mega,.nav-curtain{display:none}}

/* ---- glass for everyone, and never solid (homepage v2.4a) ----
   apple.com's dark nav keeps its glass for every reader — it has no
   contrast override and no reduced-transparency one — and so does this one.
   Until v2.4a all three cases below went solid ink, and iOS reported
   Increase Contrast at the September review: on that phone the bar was a
   slab on every page. Now each case only DENSIFIES the rest. The open
   density stays ink at 96% in every case (the open rule above outranks
   these), and the blur is the same:
     more contrast          grey at 85%, the labels full white: 6.06:1 at
                            worst, over paper
     reduced transparency   grey at 85%, the labels at 90%: 5.28:1
     no backdrop-filter     grey at 90%, Apple's .88 rounded up: with
                            nothing blurred behind it, the page must not
                            read through. The labels 6.04:1
   No backdrop-filter comes last, so it wins where it meets either
   preference. Only Chrome and Edge report reduced transparency today;
   Safari reports Increase Contrast. The curtain keeps its blur in every
   case. */
@media(prefers-reduced-transparency:reduce){
  .site-nav--mega{--mat-c:rgba(71,71,69,.85)}
}
@media(prefers-contrast:more){
  .site-nav--mega{--mat-c:rgba(71,71,69,.85)}
  .site-nav--mega .nav-link,.site-nav--mega .nav-icon-btn,.site-nav--mega .nav-toggle,
  .nav-mega-label,.site-nav--mega .nav-acc-chevron,
  .site-nav--mega .nav-mobile-search{color:var(--paper)}
}
@supports not ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))){
  .site-nav--mega{--mat-c:rgba(71,71,69,.9)}
}

/* ============================================================
   2. HOMEPAGE TILES
   ============================================================
   apple.com's homepage geometry, as closely as the brand allows.

     - The page is paper; the tiles sit on it with a 12px white gap.
     - Full-width tiles run edge to edge. Half-width tiles keep 12px from the
       page edges and 12px between them, and stack below 761px, where they
       run edge to edge too (13 October 2026).
     - The grounds alternate in apple's rhythm (homepage v2.3): four dark
       tiles, --ink with white type, one of them on the green feature
       ground, and three --panel — chosen by tools/gen-pages.js (HOME_DARK,
       HOME_FEATURE). White on ink is 13.62:1, ink on --panel 12.48:1.
     - The text block sits at the top, centred, 52px below the tile's edge:
       the name, the line, two buttons.
     - Below it, the tile's device — a phone or laptop plate, see "the
       device plates" at the end of this section — or, on a tile without
       one, the image slot, which fills the rest of the tile and is cropped
       by its bottom edge.

   HEIGHT ONLY WITH MEDIA. A tile takes apple's fixed height — 724px full
   width and 612px half at desktop, 532px and 512px on a phone — only when
   it carries data-media: a device, or a filled image slot. Each is apple's
   original plus the 32px the caption band takes back. An empty slot is
   display:none and takes no height, so a tile with neither ends 52px under
   its buttons.

   TYPE. Names semibold at -0.015em: 56px full and 40px half, 40px and 32px
   on a phone. Lines 28px and 21px, 21px and 19px. The line-heights are
   apple's ratios, so a one-line name and line leave the image a known
   height (tools/README.md, "Homepage image slots").

   The paper ground and .tile-price stay for tools/styleguide.html, which
   still draws the first version of these tiles; the homepage prints no
   price. */
.tiles{display:grid;gap:12px;background:var(--paper)}
.tiles-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:0 12px}
/* Websites, Systems and Apps sit three across in the styleguide's version,
   so the row takes a count modifier rather than being locked to pairs. */
.tiles-row--three{grid-template-columns:repeat(3,1fr)}
@media(max-width:900px){.tiles-row--three{grid-template-columns:1fr}}

.tile{position:relative;container-type:inline-size;container-name:tile;
  display:flex;flex-direction:column;overflow:hidden;
  padding:52px 0;background:var(--panel);color:var(--ink);text-align:center}
.tile--paper{background:var(--paper)}
.tile--panel{background:var(--panel)}
.tile--dark{background:var(--ink);color:var(--paper)}
/* THE ONE FEATURE GROUND, brand v2.2: on --mark's green, the Custom Agents
   tile since homepage v2.3 (the Voice tile until then). It adds the ground
   and nothing else — `tile--dark` still carries the white type and the dark
   stops — and it is the only rule in any stylesheet that may read --mark or
   its value, which tools/audit.js check 16 holds. The caption band's
   background:inherit takes it too. Measured on it: paper 12.37:1, the
   caption at paper 72% 7.2:1, the --orange-on-ink fill and the focus ring
   4.95:1. Every other dark area stays ink. */
.tile--feature{background:var(--mark)}

.tile-inner{width:100%;max-width:calc(var(--wrap) + 2 * var(--gutter));
  margin-inline:auto;padding:0 var(--gutter)}
.tile-name{font-size:56px;font-weight:600;letter-spacing:-0.015em;
  line-height:1.07143;color:inherit}
.tile-line{margin-top:6px;font-size:28px;line-height:1.14286;color:inherit}
.tile--half .tile-name{font-size:40px;line-height:1.1}
.tile--half .tile-line{font-size:21px;line-height:1.19048}
.tile-price{margin-top:var(--sp-4);font-size:var(--fs-body);color:var(--text-soft);
  font-variant-numeric:tabular-nums}
.tile-price__from{color:var(--text-soft)}
.tile-cta{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;
  gap:16px;margin-top:18px}

/* ---- the buttons ----
   apple's: a 17px label on a 44px pill, the SAME size on a full tile and a
   half one, 16px apart. On a light tile the first is ink-filled and the
   second an ink outline; on a dark tile the first is a white fill with ink
   type and the second a white outline. Ink on paper and paper on ink are
   13.62:1, ink on --panel 12.48:1, so every label clears AA — and a white
   outline on ink clears the 3:1 a control's edge needs many times over.

   SINCE BRAND v2.2 THE FIRST IS ORANGE — "Learn more", the one control
   orange at rest — by the section "v2.2 — THE ORANGE PRIMARY" at the end of
   this file, which overrides the two primary rules here and their hovers.
   The dark tile's white-fill primary below is therefore dead on every dark
   tile, and is left for the dead-CSS task. The secondaries are exactly as
   described. */
.tile .btn-primary,.tile .btn-secondary{min-height:44px;padding:0 22px;
  font-size:17px;border:1px solid}
.tile .btn-primary{background:var(--ink);border-color:var(--ink);color:var(--paper)}
.tile .btn-secondary{background:transparent;border-color:var(--ink);color:var(--ink)}
.tile--dark .btn-primary{background:var(--paper);border-color:var(--paper);color:var(--ink)}
.tile--dark .btn-secondary{background:transparent;border-color:var(--paper);color:var(--paper)}
/* The solid primary presses to --ink-press — one step darker than --ink, no
   hue, white on it 16.51:1. v2.1 bans green from buttons and spends orange on
   the OUTLINE button's hover, so the filled one needs a neutral of its own.
   A light tile's outline secondary has NO hover rule here, deliberately: it
   takes the shared .btn-secondary:hover at the end of this file, orange with
   a paper label. A `.tile .btn-secondary:hover` would be (0,3,0) against
   that rule's (0,2,0) and turn the hover ink on every light tile. */
@media(hover:hover) and (pointer:fine){
  .tile .btn-primary:hover{background:var(--ink-press);border-color:var(--ink-press);color:var(--paper)}
  .tile--dark .btn-primary:hover{background:var(--panel);border-color:var(--panel);color:var(--ink)}
  .tile--dark .btn-secondary:hover{background:var(--paper);border-color:var(--paper);color:var(--ink)}
}
/* A REAL DARK GROUND, so here the ring takes the ink stop: --orange-on-ink
   on --ink is 5.45:1, and on the Custom Agents tile's green (tile--feature)
   4.95:1. Unlike the nav bar this tile is a solid ground and not a
   translucent material, so a dark-ground token is exactly right for it —
   and for the Voice tile's play button, which takes the same ring.
   (--paper under v2.0, because its green was 1.34:1 on --ink.) */
.tile--dark :focus-visible{outline-color:var(--orange-on-ink)}

/* ---- the image slot ----
   For a tile WITHOUT a device, which is none of the seven today: a device
   replaces the slot outright, so the slot rules never reach a plate. The
   two height rules are the exception. They key on data-media, and a device
   tile takes them too.

   Below the text, never above it, and never a reserved box: display:none
   (the shared media rule above) until the tile carries data-media. Filled,
   it takes the rest of the tile — out to both edges, down to the bottom
   edge, which crops it. The image covers the slot from its top, so it loses
   its bottom first when there is too little room, and its sides on a
   narrow screen. */
/* EVERY TILE GREW BY THE BAND'S 32px: 692 to 724 full width, 580 to 612
   half. The band takes 32px off the bottom of every tile, so every tile
   gives back 32px and nothing above it moves.

   The half tiles needed it as much as the phone tiles did, for a different
   reason. A phone is cut by the band, so without the extra 32 it would
   simply show 32px less of itself. A laptop stands clear of the band
   instead, which means it moved 24px UP the tile — and at 580 that came
   straight out of the gap between the buttons and the laptop's lid, which
   fell from 35.7px to 11.7px at 1440. At 612 it is 43.7px. */
.tile[data-media]{height:724px;padding-bottom:0}
.tile--half[data-media]{height:612px}
.tile[data-media] > .tile-media{position:relative;flex:1 1 auto;min-height:0;
  margin:20px 0 0;border-radius:0}
.tile-media img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:50% 0}

/* STACKED, THE HALF TILES RUN EDGE TO EDGE (the owner, 13 October 2026).
   At 760px and under a pair stacks, and the row's 12px side padding would
   leave its two tiles as boxes between full tiles that run to both edges.
   So a stacked row of the homepage's half tiles drops its padding and runs
   full width like them. The 12px gaps stay: the row's own gap between its
   two tiles, and .tiles' gap between rows. The homepage's rows only
   ([data-media]): the styleguide draws its own rows of half tiles, and they
   keep theirs. Without :has(), today's padded rows. Over 760px the pairs
   sit side by side, padded, as before. */
@media(max-width:760px){
  .tiles-row{grid-template-columns:1fr}
  .tiles-row:has(> .tile--half[data-media]){padding:0}
  .tile-name{font-size:40px;line-height:1.1}
  .tile-line{font-size:21px;line-height:1.19048}
  .tile--half .tile-name{font-size:32px;line-height:1.125}
  .tile--half .tile-line{font-size:19px;line-height:1.21053}
  .tile[data-media]{height:532px}
  .tile--half[data-media]{height:512px}
}

/* ---- level buttons in the half-tile pairs (homepage v2.4a) ----
   Two half tiles side by side keep their names, their lines and their
   buttons on the same rows, as apple.com's pairs do: where one tile's line
   wraps and its neighbour's does not, the neighbour's buttons drop to the
   same height rather than sitting higher. Each half tile is a SUBGRID of
   its row — name, line and buttons share the row's first three tracks, and
   the taller tile sets each — and the laptop takes the fourth, which is
   the rest of the fixed 612px, so it still stands 48px above the bottom
   edge.

   A SIZE CONTAINER CANNOT BE A SUBGRID, and every .tile is one: its
   container-type is what the "tile" queries and the screens' cqi read.
   Chrome drops the subgrid outright and the text rows measure 0px. So at
   desktop widths a half tile hands that role to its .tile-devices — the same
   width, and every "tile" query in a half tile already sits inside it —
   and the row, not the subgrid, carries the 612px: a subgrid's own height
   does not size the row it spans.

   Desktop only, where the pair sits side by side; on a phone the tiles
   stack and each keeps its own rows. Without subgrid, or without :has(),
   which places the row's half, today's layout — never one half of the rule
   without the other. The homepage's tiles only ([data-media]): the
   styleguide draws its own rows of half tiles, and they keep theirs.
   Nothing moves at 1440, where every name and line fits one row; the
   harness holds the two button rows of every pair level, at every width
   from 761 to 1920. */
@media(min-width:761px){
  @supports (grid-template-rows:subgrid) and selector(:has(a)){
    .tiles-row:has(> .tile--half[data-media]){grid-template-rows:auto auto auto minmax(0,1fr);row-gap:0;height:612px}
    .tiles-row > .tile--half[data-media]{container-type:normal;display:grid;grid-row:span 4;
      grid-template-rows:subgrid;row-gap:0}
    .tiles-row > .tile--half[data-media] > .tile-inner{grid-row:1 / span 3;display:grid;grid-template-rows:subgrid;row-gap:0}
    .tiles-row > .tile--half[data-media] > .tile-devices{grid-row:4;container:tile / inline-size}
  }
}

/* ---- the device plates ----
   A phone or laptop plate on each of the seven tiles, in place of the image
   slot: tools/gen-pages.js (HOME_DEVICES) writes .tile-devices where the
   slot would be, and no slot. The tile carries data-media, so it keeps the
   fixed heights above and clips what falls outside them.

   NEVER COVER-CROPPED. The slot's image is cropped to fill its area; a plate
   must not be. The screen is laid over the plate's own display in percent
   of the plate, and a cropped plate would move the display out from under
   it. So the device sizes itself, and only the tile's edge cuts it.

   PHONES hang 40px under the buttons, 300px wide, and the tile's bottom edge
   crops them; two sit 40px apart, centred as a pair. On a phone a single one
   is 200px and a pair 160px each, 16px apart — narrower still in a tile
   under 360px, so the pair keeps 12px from each edge. (No tile has held a
   pair since homepage v2.3; the pair's rules stay while the generator can
   still write one.)
   THE HAND, homepage v2.3, is the Voice tile's one device: a photograph of
   a hand holding a phone. Its phone hangs 40px under the buttons as a
   phone does, and the hand runs off the tile's bottom edge. Below.
   LAPTOPS are shown whole, base 24px above the bottom edge: 560px, and never
   wider than 88% of the tile. Below a ~1309px layout the half tile is too
   narrow for 560 to keep that margin, and below ~1156px it would be cut. On
   a phone, 88% to a 340px cap.

   CLIP, NOT HIDDEN. A tile with media is overflow:clip, with the .tile rule's
   overflow:hidden as the fallback. A hidden box is still a scroll container:
   selection-drag or find-in-page can scroll the ~180px of phone under the
   bottom edge into view and drag the tile's text up with it. A clipped box
   cannot scroll at all. Its minimum width becomes its content's, which is
   at most the 640px desktop pair (the percentage widths below count as
   zero), and no tile at desktop is under 746px.

   THE SCREEN is --paper over the plate's display. The phone's camera sits
   under it in the photo, so it is redrawn on top: #0a0a0a is that piece of
   the artwork, not a colour of the palette. <picture> is inline by default;
   as a block, the device box is exactly the plate, which every percentage
   here is taken of. */
.tile[data-media]{overflow:clip}
.tile-devices{display:flex;justify-content:center;gap:40px;flex:none}
.tile-devices--phone{margin-top:40px}
/* THE PHONE'S WRAPPER IS THE PART OF THE PHONE THE TILE SHOWS (homepage
   v2.4a). It fills the tile from 40px under the buttons to the tile's
   bottom edge, so its height is the phone's visible window plus the band,
   whatever the tile's words and buttons wrap to: 472px at desktop, 278 on
   a 390 layout, 218 at 320 with the buttons wrapped. As a size container
   it hands that height to the screen inside it as cqh, which is how the
   messaging app's input bar finds the foot of the window (.scr-th-input)
   at every width, with no script. Its phone hangs from its top and
   overflows it, and the tile clips that, as before. Size containment
   takes its width too, and that is the tile's own — the wrapper is
   stretched across the tile's column — so every cqi inside it resolves as
   it did against the tile, and the "tile" queries still read the tile. */
.tile-devices--phone{flex:1 1 0%;min-height:0;align-items:flex-start;container:window / size}
/* 48, NOT 24: the caption band is 32px tall and the laptop stands 16px
   clear above it. Phones are cut by the band instead, which is why their
   tiles grew by its height and these did not. */
.tile-devices--laptop{margin-top:auto;margin-bottom:48px}
.device{position:relative;flex:none}
.device picture{display:block}
.device img{display:block;width:100%;height:auto}
.device .screen{position:absolute;overflow:hidden;background:var(--paper);
  container-type:inline-size;container-name:screen}
.device--phone{width:300px}
.device--laptop{width:min(560px,88%)}
.device--phone .screen{left:4.903%;top:2.375%;width:90.194%;height:95.188%;
  border-radius:10.59% / 4.86%}
.device--phone .screen::after{content:"";position:absolute;
  left:50.36%;top:2.56%;width:4.29%;aspect-ratio:1;
  transform:translate(-50%,-50%);border-radius:50%;background:#0a0a0a}
.device--laptop .screen{left:11.333%;top:3.662%;width:77.278%;height:84.507%;
  border-radius:0.65% 0.65% 0 0 / 1% 1% 0 0}

/* THE HAND, homepage v2.3: the Voice tile's one device, a photograph of a
   hand holding a phone (tools/hand-plate.py), in place of the two phones.
   The PHONE in it is what the tile lays out by: --phone-w is its outer
   frame's width, 250px (190 on a phone), and every number below is that
   width over the photo's own geometry, measured by the script and kept in
   docs/devices/hand-screen-rect.json:
     - the frame is 49.554% of the photo's width and centred in it, so the
       photo is --phone-w / .49554 wide, and centring it centres the phone;
     - the frame's top is 1.811% down a photo 1038/1009 as tall as it is
       wide, which is .0376 of --phone-w: taken off the 40px a phone hangs
       under the buttons, so the phone's own top hangs at exactly 40.
   The ground round the hand is transparent, so the tile's own ink is what
   shows there, exact to the level. The hand runs off the tile's bottom
   edge, under the caption band; the photo's own edges lie on that ground
   and never cut the hand inside the tile (the harness checks the bottom). */
.tile-devices--hand{--phone-w:250px;margin-top:calc(40px - var(--phone-w) * .0376)}
/* Homepage v2.4a: the hand's wrapper, too, fills the tile to its bottom
   edge, so the Voice column beside the phone can end at a fixed distance
   over the caption band (.float, below). The photo hangs from its top. */
.tile-devices--hand{flex:1 1 0%;min-height:0;align-items:flex-start}
.device--hand{width:calc(var(--phone-w) / .49554)}
/* THE SCREEN MEETS THE SILVER FRAME, with no black between. Its outline
   is a clip-path, not a radius: the phone's corner is a continuous curve
   that no circle follows — the nearest circle leaves black at some angles
   and covers silver at others. Since homepage v2.4a the polygon is the
   frame's inside edge TRACED all the way round by tools/hand-plate.py, one
   sub-pixel crossing per pixel of its length, so the sides and the corners
   are one curve with no joins; each point is pushed out to where the
   silver begins (at most 0.6 CSS px at 250px), so the frame's darker inner
   bevel never shows; and where a fingertip covers the frame the white
   meets the skin at the skin's own edge, so the fingertips stay in front
   of it with no step and no shadowed rim. The rectangle is the outline's
   box. tools/audit.js check 12 holds this rule to
   docs/devices/hand-screen-rect.json, number for number. The border-radius
   under it is the fallback shape. No camera is drawn: the photographed
   phone has none. */
.device--hand .screen{left:25.904%;top:2.404%;width:48.21%;height:96.001%;
  border-radius:16.541% 15.454% 15.656% 15.857% / 8.078% 7.547% 7.645% 7.744%;
  clip-path:polygon(16.533% 0.056%,19.03% 0.018%,66.722% 0.018%,76.147% 0%,79.37% 0.018%,82.189% 0.008%,84.123% 0.044%,85.728% 0.105%,87% 0.186%,88.417% 0.323%,89.354% 0.437%,90.125% 0.554%,91.105% 0.74%,92.059% 0.959%,92.982% 1.211%,93.793% 1.477%,94.679% 1.827%,95.453% 2.18%,96.049% 2.506%,96.747% 2.945%,97.365% 3.41%,97.833% 3.825%,98.261% 4.248%,98.65% 4.723%,98.894% 5.097%,99.215% 5.702%,99.548% 6.583%,99.668% 7.048%,99.807% 8.027%,99.839% 8.538%,99.819% 10.307%,99.879% 10.937%,99.839% 28.981%,99.839% 49.306%,99.879% 56.029%,99.899% 76.315%,99.94% 77.376%,99.92% 80.128%,99.98% 82.369%,100% 87.44%,100% 89.956%,99.902% 92.276%,99.763% 92.984%,99.652% 93.374%,99.423% 93.992%,99.122% 94.603%,98.704% 95.24%,98.242% 95.788%,97.651% 96.351%,97.098% 96.79%,96.579% 97.146%,95.84% 97.568%,95.039% 97.963%,94.244% 98.304%,93.047% 98.714%,92.212% 98.958%,91.049% 99.236%,90.149% 99.411%,89.227% 99.553%,88.218% 99.686%,87.117% 99.793%,86.244% 99.857%,85.366% 99.897%,83.761% 99.935%,80.862% 99.971%,66.048% 99.999%,22.088% 100%,19.834% 99.98%,17.58% 99.99%,16.05% 99.965%,14.926% 99.92%,12.858% 99.793%,11.759% 99.678%,10.443% 99.497%,9.459% 99.32%,8.422% 99.096%,7.495% 98.851%,6.74% 98.618%,6.013% 98.363%,5.209% 98.028%,4.439% 97.673%,3.804% 97.31%,3.117% 96.867%,2.657% 96.54%,2.091% 96.059%,1.498% 95.451%,1.206% 95.21%,1.149% 95.135%,0.772% 94.527%,0.482% 93.868%,0.288% 93.242%,0.115% 92.315%,0.108% 92.237%,0.187% 92.099%,0.161% 91.981%,0.193% 91.863%,0.233% 90.821%,0.274% 90.644%,0.274% 90.114%,0.173% 89.898%,0.13% 89.681%,0.034% 89.544%,0.013% 88.227%,0.045% 86.929%,0.05% 86.89%,0.222% 86.752%,0.256% 86.693%,0.314% 86.418%,0.314% 86.241%,0.388% 86.222%,0.382% 85.848%,0.339% 85.652%,0.369% 85.573%,0.379% 85.298%,1.577% 84.138%,2.005% 83.529%,2.367% 82.742%,2.441% 82.467%,2.56% 81.583%,2.542% 81.013%,2.463% 80.462%,2.335% 80.049%,2.183% 79.794%,1.945% 79.283%,1.354% 78.595%,0.918% 78.202%,0.424% 77.868%,0.346% 77.769%,0.253% 77.75%,0.216% 77.533%,0.046% 77.396%,0.03% 77.278%,0.048% 77.199%,0.193% 77.042%,0.23% 76.197%,0.193% 74.388%,0.038% 74.251%,0.091% 74.133%,0.05% 73.995%,0.095% 73.897%,0.04% 73.759%,0.03% 73.563%,0.034% 72.226%,0.21% 72.089%,0.288% 71.833%,0.282% 71.754%,0.366% 71.735%,0.411% 71.42%,0.356% 71.263%,0.392% 70.968%,0.368% 70.811%,0.493% 70.536%,0.763% 70.162%,1.091% 69.572%,1.352% 68.786%,1.437% 68.354%,1.515% 67.685%,1.561% 66.801%,1.422% 65.857%,1.237% 65.307%,1.042% 64.933%,0.79% 64.56%,0.307% 64.01%,0.285% 63.793%,0.213% 63.774%,0.19% 63.42%,0.113% 63.125%,0.173% 62.948%,0.138% 62.712%,0.173% 62.575%,0.153% 62.064%,0.193% 61.828%,0.193% 59.98%,0.173% 59.606%,0.113% 59.253%,0.006% 59.115%,0% 59.036%,0% 58.565%,0.113% 58.427%,0.02% 58.309%,0.113% 58.172%,0.003% 57.995%,0.003% 57.818%,0.141% 57.68%,0.193% 57.346%,0.233% 55.538%,0.274% 54.968%,0.326% 54.948%,0.332% 54.633%,0.307% 52.864%,0.233% 52.845%,0.193% 50.545%,0.193% 40.579%,0.153% 39.144%,0.173% 28.942%,0.133% 26.917%,0.153% 22.455%,0.113% 16.755%,0.113% 10.622%,0.213% 10.17%,0.207% 9.934%,0.133% 9.718%,0.113% 8.931%,0.153% 8.008%,0.293% 6.921%,0.527% 6.093%,0.676% 5.699%,0.868% 5.308%,1.346% 4.577%,1.806% 4.055%,2.53% 3.412%,3.393% 2.811%,4.216% 2.349%,5.496% 1.762%,6.493% 1.392%,7.628% 1.051%,8.756% 0.792%,10.232% 0.533%,11.574% 0.355%,12.689% 0.245%,14.928% 0.106%)}

@media(max-width:760px){
  .tile-devices--pair{gap:16px}
  .device--phone{width:200px}
  .tile-devices--pair .device--phone{width:min(160px,calc(50% - 20px))}
  .device--laptop{width:min(340px,88%)}
  /* 190, not a phone's 200: beside a hand there is less room, and the play
     button has to fit left of the fingertips. The screen is still 184px
     wide, and its smallest type 8.2px at a true 390 layout — over the 8px
     floor. */
  .tile-devices--hand{--phone-w:190px}
}

/* ---- the caption band ----
   32px along the bottom of every tile that shows a screen, in the tile's
   own ground, over the devices. Every screen on this page is a drawing —
   of a business called "Your business", or, on the Voice call, of KRM's own
   line taking a sample caller — and the copy rules ban invented results:
   a screenful of sample conversation reads AS a result unless it is
   labelled, so the label is not decoration and is not optional.

   background:inherit, not a named colour, so the band is the tile's ground
   whichever ground that is — it has to hide the phone behind it, and a
   wrong guess would show as a stripe. On the Custom Agents tile it inherits
   the green of tile--feature.

   TEXT-SOFT ON PANEL IS 6.36:1. On a dark tile it would be 1.96:1 on ink
   (1.78:1 on the green), so there the caption is paper at 72%, which is
   7.84:1 on ink and 7.24:1 on the green. */
.tile-caption{position:absolute;left:0;right:0;bottom:0;z-index:1;
  display:flex;align-items:center;justify-content:center;
  height:32px;padding:0 var(--gutter);background:inherit;
  font-size:12px;line-height:16px;color:var(--text-soft)}
.tile--dark .tile-caption{color:rgba(255,255,255,.72)}

/* ---- the banners, beside a phone ----
   Homepage v2.4a, in place of v2.3's notes. On the Voice, Messaging and
   Follow-up tiles a notification sits beside the phone (`screens.banner` in
   js/catalog.js): on Messaging and Follow-up the customer's own reply
   arriving on the business's phone, on Voice the email with the new lead.
   It is a SCREEN — the phone's notification, lifted out of it — so it
   carries `.screen`, which is check 16's boundary, and its `.scr` brings
   the screen-only colours with it; .scr's own layout is for a display
   overlay and is undone here. Flat, with no shadow, per the brand: white
   with a hairline on a light tile, a lift of the ink on a dark one.

   PLACED FROM THE TILE'S CENTRE LINE, so a banner keeps its place beside
   its phone at any tile width, and narrows with the tile rather than
   running off it: 16px outside a 300px phone's frame, near enough to read
   as arriving on that phone, up to 300px wide, and never nearer than 24px
   to the tile's side. The owner's spacing, 4 October 2026: until then a
   banner stood 56px out and might come to 12px from the side, so on a
   tile under ~1060px it hugged the window's edge, far from its phone.
   (cqi is 1% of the tile, the width these sit in.)

   THE VOICE TILE'S COLUMN, the owner's pick, Voice C · Panel. It stands 24px
   outside the fingertips, which reach .66915 of --phone-w left of the
   phone's centre (docs/devices/hand-screen-rect.json), 34px down the
   devices, and never nearer than 24px to the tile's side: the email banner,
   the live captions under it, and the play button under them,
   right-aligned, so the button sits just left of the phone in its lower
   half. The button stands at the column's foot, and never moves: at rest
   the cards head the column, and while the call plays the banner's slot is
   shut and the captions stand on the button, growing up the column as the
   words arrive, the seek bar at their foot; when the call ends the slot
   opens and the banner drops in at the top (the real call, October 2026;
   until then the captions headed the column and the button followed them
   down). The column may come no nearer than 8px to the caption band — the
   wrapper reaches the tile's bottom edge, so that is `bottom:40px` — and
   the captions' window is what gives way to keep it there: it shows the
   newest lines and fades the older ones out at its top.
   THE EMAIL BANNER REACHES ON PAST THE COLUMN, to 16px from the phone's
   frame like the other banners. It can: it heads the column, above the
   fingers, which start 34.453% down the photo, so only the captions and the
   button have to stand clear of the fingertips. Its slot keeps the
   column's left edge and runs --reach further right: the fingertips'
   .66915 of --phone-w less the frame's .5, plus the column's 24px, less the
   16. The banner ends at least 9px above the fingers at every desktop
   width, however its lines wrap — 9.8px at a 780px tile, where its title
   first sets at 15px — and the harness holds it clear of them. */
.tile-devices{position:relative}
.float{position:absolute;z-index:1;top:var(--float-top,112px);width:min(300px, calc(50cqi - 190px));
  display:flex;flex-direction:column;align-items:flex-end;gap:24px}
.float--right{align-items:flex-start;left:calc(50% + 150px + 16px)}
.float--left{right:calc(50% + 150px + 16px)}
.tile-devices--hand .float--left{--float-top:34px;bottom:40px;gap:16px;
  right:calc(50% + var(--phone-w) * .66915 + 24px);
  width:min(300px, calc(50cqi - var(--phone-w) * .66915 - 48px))}
.float-card{width:100%}
.float .scr,.p-hero__float .scr{position:static;inset:auto;display:block;font-size:inherit}
/* the banner's slot heads the Voice column: one row, which opens from 0fr
   when the banner drops in (the screens move, below), and never gives way */
.float-slot{flex:none;width:100%;display:grid;grid-template-rows:1fr}
.float-slot > .float-card{min-height:0}
@media(min-width:761px){
  .tile-devices--hand .float-slot{--reach:calc(var(--phone-w) * .16915 + 8px);
    width:calc(100% + var(--reach));margin-right:calc(-1 * var(--reach))}
}

.scr--banner{padding:13px 15px 14px;border-radius:18px}
.tile--panel .scr--banner{background:var(--paper);border:1px solid var(--hairline);color:var(--ink)}
/* #3E3E3B is white at 7.5% over --ink, flattened, and OPAQUE: on a phone
   the banner overlaps the phone's frame, and a translucent one would let
   the rim show through. Paper on it is 10.72:1, paper at 72% 6.47:1. Its
   edge is --hairline-ink, decorative. */
.tile--dark .scr--banner{background:#3E3E3B;border:1px solid var(--hairline-ink);color:var(--paper)}
.scr-bn-top{display:flex;align-items:center;gap:8px;font-size:13px;line-height:18px;color:var(--text-soft)}
/* the icon: a generic speech bubble on a text, an envelope on the email,
   white on the messaging blue (5.33:1) — the phone's own artwork, not a KRM
   mark, and not a platform's icon either. The sender's name carries the
   meaning; the time sits at the far end. */
.scr-bn-icon{flex:none;display:inline-flex;align-items:center;justify-content:center;
  width:20px;height:20px;border-radius:5px;background:var(--screen-blue);color:#FFFFFF}
.scr-bn-icon svg{display:block;width:12px;height:12px}
.scr-bn-from{font-weight:600;color:var(--ink)}
.scr-bn-time{margin-left:auto}
.scr-bn-title,.scr-bn-text{margin-top:6px;font-size:15px;line-height:21px}
/* pretty, as the owner approved it in round 2: it never leaves the last
   word alone — see the lone-word note below */
.scr-bn-title{font-weight:500;text-wrap:pretty}
.scr-bn-body{margin-top:2px;font-size:13px;line-height:18px;color:var(--text-soft)}
/* a text's preview is two lines at most, as a phone's banner shows it */
.scr-bn-text{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
.tile--dark .scr-bn-top,.tile--dark .scr-bn-body{color:rgba(255,255,255,.72)}
.tile--dark .scr-bn-from{color:var(--paper)}

/* THE LIVE CAPTIONS, the Voice column's second card: a dark card on the
   tile, its title beside the phone's own live dot (--screen-go, 4.83:1 on
   the card, decoration), then the lines in a window of their own. The
   card gives way when the column is full: every level of it may shrink,
   down to the window, whose newest lines stay and whose older ones leave
   through the fade at its top. */
.float-card--captions{flex:0 1 auto;min-height:0;display:flex;flex-direction:column}
.float-card--captions > .screen{flex:0 1 auto;min-height:0;display:flex;flex-direction:column}
.float .scr--captions{flex:0 1 auto;min-height:0;display:flex;flex-direction:column;
  padding:14px 16px 15px;border-radius:18px;background:#3E3E3B;border:1px solid var(--hairline-ink);
  --say-fs:15px;--who-fs:12px}
.scr-cc-title{flex:none;display:flex;align-items:center;gap:8px;
  font-size:12px;line-height:16px;color:rgba(255,255,255,.72)}
.scr-cc-title::before{content:"";flex:none;width:7px;height:7px;border-radius:50%;background:var(--screen-go)}
/* THE WINDOW. Bottom-anchored, so it keeps the newest line; 250px at most.
   Its top 14px is a fade: a line that scrolls off leaves through it and is
   never cut hard. While the lines fit, that 14px is just the gap under the
   title. */
.scr-cc-scroll{flex:0 1 auto;min-height:0;max-height:250px;overflow:hidden;
  display:flex;flex-direction:column;justify-content:flex-end;padding-top:14px;
  -webkit-mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.6) 7px,#000 14px);
  mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.6) 7px,#000 14px)}
/* THE SIZES LIVE ON THE WORDS. A paragraph of speech is set at font-size
   0, so a word not said yet — font-size 0 while the call plays (the
   screens move, below) — has no strut and takes no room, and the card
   grows as the call goes on; each word and each speaker's name sets its
   own size from --say-fs and --who-fs, and every word carries its own
   trailing space (tools/gen-pages.js, captions()). A word is `scr-cc-w`,
   the captions' own, so the tiles' typing rules for `scr-w` never reach
   it. Said words rest in sand, 9.44:1 on the card; the speakers are white
   at 72%, 6.47:1. An entry's own line breaks are paragraphs, with no space
   between them: the transcript's. */
.scr-cc-say{font-size:0;line-height:1.4;color:#FFFFFF}
.scr-cc-say .scr-cc-w{font-size:var(--say-fs);color:var(--sand)}
.scr-cc-who{font-size:var(--who-fs);line-height:1.34;padding-bottom:.15em;color:rgba(255,255,255,.72)}
.scr-cc-line + .scr-cc-line .scr-cc-who{padding-top:.75em}

/* ON A PHONE there is no room BESIDE a 200px phone in a 390px tile, so the
   phone steps aside by --shift and its banner overlaps the phone's frame —
   never a line of its screen, which the harness checks. --shift is in cqi
   and clamped, so a phone's frame keeps 12px from the tile's side down to
   320; a banner fills the space from 14px over the frame to 12px from the
   tile's side, up to 220px.

   THE VOICE TILE SPLITS ITS COLUMN. Its cards need width, which only the
   phone's upper left offers — over the frame, above the fingers — and its
   button belongs in the phone's lower half, where the fingers reach out to
   the left. So on a phone they are placed apart, all from the photo's own
   geometry: the banner high on the frame, 6px over it, short of the
   screen's first letter; the button 12px left of the fingertips and 140px
   down the photo, which lands it in the phone's lower half whether the
   tile's words take one line or two and its buttons one row or two. The
   live captions share the banner's place and show only while the call
   plays: then the banner drops in where they were (Voice C on a phone). At
   rest, with no script and under reduced motion, the phone shows the
   finished call and the banner. Under a 390px tile the button is its ring
   alone (the label stays its accessible name), and the banner drops its
   second line and sets its first at 13px, so it still ends above the
   fingers. */
@media(max-width:760px){
  .tile-devices:has(> .float--right){--shift:max(-72px, calc(112px - 50cqi))}
  .tile-devices:has(> .float--left){--shift:min(72px, calc(50cqi - 112px))}
  .tile-devices.tile-devices--hand{--shift:min(88px, calc(50cqi - 107px))}
  .tile-devices:has(> .float) .device{transform:translateX(var(--shift))}
  .float{--float-top:96px}
  .float--right{left:calc(50% + var(--shift) + 86px);width:min(220px, calc(50cqi - var(--shift) - 98px))}
  .float--left{right:calc(50% - var(--shift) + 86px);width:min(220px, calc(50cqi + var(--shift) - 98px))}
  .tile-devices--hand .float--left{inset:0;width:auto;display:block;pointer-events:none}
  .float-slot{display:contents}
  .tile-devices--hand .float-card{position:absolute;top:-4px;
    right:calc(50% - var(--shift) + var(--phone-w) / 2 - 6px);
    width:min(240px, calc(50cqi + var(--shift) - var(--phone-w) / 2 - 6px))}
  .tile-devices--hand .float-slot > .float-card{z-index:1}
  .tile-devices--hand .float-card--captions{opacity:0}
  .tile-devices--hand .tile-play{position:absolute;top:140px;pointer-events:auto;
    right:calc(50% - var(--shift) + var(--phone-w) * .66915 + 12px)}
  .scr--banner{padding:10px 12px 11px;border-radius:14px}
  .scr-bn-top{font-size:12px;line-height:16px;gap:6px}
  .tile-devices--phone .scr-bn-icon{width:17px;height:17px;border-radius:4px}
  .tile-devices--phone .scr-bn-icon svg{width:10px;height:10px}
  .scr-bn-title{margin-top:5px;font-size:14px;line-height:19px}
  .scr-bn-body{font-size:12px;line-height:16px}
  .scr-bn-text{margin-top:5px;font-size:13px;line-height:17px}
  /* the phone's captions: about four lines show, the older ones fading up
     through a fade a full line deep, so a line leaving the top fades out
     whole rather than reading as cut. The speakers stay at 12px, the
     floor every card here keeps (the round-2 mockup set them at 11). */
  .float .scr--captions{padding:10px 12px 11px;border-radius:14px;--say-fs:13px;--who-fs:12px}
  .scr-cc-scroll{max-height:100px;padding-top:24px;
    -webkit-mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.12) 7px,rgba(0,0,0,.55) 15px,#000 24px);
    mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.12) 7px,rgba(0,0,0,.55) 15px,#000 24px)}
}
@container tile (max-width:389px){
  .tile-devices--hand .scr-bn-title{font-size:13px;line-height:17px}
  .tile-devices--hand .scr-bn-body{display:none}
}
/* THE VOICE BANNER'S TITLE LEAVES NO WORD ALONE — the owner's rule, 1
   October 2026. Pretty keeps the last line from holding one word; since
   the real call the title is "New lead: plumbing company, demo booked", and
   where it needs two lines it breaks "New lead: plumbing / company, demo
   booked", or "New lead: plumbing company, / demo booked" on a wider card.
   Under a 780px tile the title sets at 14px, the phone layout's size: the
   desktop column was too narrow there for "New lead: plumbing" at 15px — a
   window just past 760 with a classic scrollbar beside it — until the
   banner reached on past the column (4 October 2026), and the size stays as
   the owner approved it.
   Under a 354px tile one word may stay alone — the known exception: the
   banner cannot widen there without covering the screen. With the real
   call's title that is under a 318px tile only (balance, below). */
@media(min-width:761px){
  @container tile (max-width:779px){
    .tile-devices--hand .scr-bn-title{font-size:14px;line-height:19px}
  }
}
/* THE REAL CALL'S TITLE, "New lead: plumbing company, demo booked" (October
   2026), breaks differently: under a 371px tile, where it needs three
   lines or four, pretty leaves "booked" alone ("…company, demo / booked")
   from a 331px tile up. Balance keeps it with "demo" there — "New lead:
   plumbing / company, / demo booked" — and every wider tile keeps pretty's
   breaks. Under a 318px tile it takes five lines and "booked" stays alone,
   inside the known exception under a 354px tile, below. */
@container tile (max-width:370px){
  .tile-devices--hand .scr-bn-title{text-wrap:balance}
}
/* at a 340px tile and under, where the screens step down to 0.8, a phone's
   banner is at most 126px wide: it rides higher, laps the frame by 10px
   rather than 14 — clear of the bubbles, which sit nearer the edge once the
   screen has stepped down — and drops its time, so the sender keeps the
   line; the Voice captions show three lines, and end above the play
   button */
@container tile (max-width:340px){
  .float{--float-top:20px}
  .float--right{left:calc(50% + var(--shift) + 90px);width:min(220px, calc(50cqi - var(--shift) - 102px))}
  .float--left{right:calc(50% - var(--shift) + 90px);width:min(220px, calc(50cqi + var(--shift) - 102px))}
  .tile-devices--phone .scr-bn-time{display:none}
  .tile-devices--hand .scr-cc-scroll{max-height:82px}
}
/* and under a 315px tile — a 320 window with a classic scrollbar — the
   Voice banner is under 100px wide, and its time goes too */
@container tile (max-width:315px){
  .tile-devices--hand .scr-bn-time{display:none}
}

/* ---- the play button ----
   Homepage v2.3, the Voice tile — and since v2.4b phase 3 the AI
   receptionist's hero, on paper, in the values §4 gives it. "Play a sample
   call" plays the call from the start (js/voice-play.js) — since October
   2026 the real call, with sound. Pressed again, "Pause the call", it
   pauses the call where it is; "Resume the call" plays on from there; only
   once the call has ended does a press start it from the top (the owner, 8
   October 2026). At rest the tile shows it finished. A real <button>,
   written `hidden` and shown by the script for everyone, reduced motion
   included: sound is not motion. With no script there is no button that
   does nothing. It sits in the captions' column, at its foot: just left of
   the phone, in its lower half (homepage v2.4a), and it never moves while
   the call plays or is paused.

   A 44px ring with the label beside it; the whole row is one button, so its
   accessible name is the visible label and its target is 44px tall. The
   ring's edge is --sand-soft, 5.48:1 on ink — a control's edge needs 3:1.
   Neutral at rest; under the pointer the ring fills with the ink ground's
   own stop, --orange-on-ink, with an ink glyph (5.45:1), as a dark tile's
   outline button does, and it presses to #F5A468 like that tile's Learn
   more. The focus ring is the dark tile's, --orange-on-ink, 5.45:1 on ink.
   While the call plays the ring fills with its progress: js/voice-play.js
   sets the progress circle's own stroke-dashoffset each frame, from the
   clip's length — on the circle itself, so nothing else restyles — holds
   it while the call is paused, and clears it when the call ends.
   131.95 is the circle's circumference, 2π·21. The play glyph turns to a
   pause glyph while the call plays, and back while it is paused
   (.is-paused); both are .tile-play__glyph, so every colour rule here and
   the hero's reach both. The label swaps with them (the script), so the
   button's accessible name is always what it shows. */
.tile-play{display:inline-flex;align-items:center;gap:12px;min-height:44px;padding:0 16px 0 0;
  background:transparent;border:0;border-radius:999px;cursor:pointer;
  font:500 17px/1.2 var(--sans);color:var(--paper);text-align:left}
.tile-play[hidden]{display:none}
.tile-play__icon{flex:none;width:44px;height:44px}
.tile-play__icon svg{display:block;width:44px;height:44px;overflow:visible}
.tile-play__track{fill:rgba(255,255,255,.08);stroke:var(--sand-soft);stroke-width:1.5}
.tile-play__prog{fill:none;stroke:var(--paper);stroke-width:2;
  stroke-dasharray:131.95;stroke-dashoffset:131.95;
  transform:rotate(-90deg);transform-origin:22px 22px}
.tile-play__glyph{fill:var(--paper)}
.tile-play__pause{display:none}
[data-play].is-playing:not(.is-ended):not(.is-paused) .tile-play__start{display:none}
[data-play].is-playing:not(.is-ended):not(.is-paused) .tile-play__pause{display:inline}
/* AT THE COLUMN'S FOOT, in both desktop columns — the tile's over 760px,
   the hero's over 900px — so it never moves under the pointer: at rest the
   cards take the top of the column and the free space falls above the
   button; while the call plays the captions take it instead (the screens
   move, below), and the button stays put. On a phone, and in the hero at
   900 and under, the button is placed absolutely and this is inert. */
.float .tile-play,.p-hero__float .tile-play{margin-top:auto}
@media(hover:hover) and (pointer:fine){
  .tile-play:hover .tile-play__track{fill:var(--orange-on-ink);stroke:var(--orange-on-ink)}
  .tile-play:hover .tile-play__glyph{fill:var(--ink)}
}
.tile-play:active .tile-play__track{fill:#F5A468;stroke:#F5A468}
.tile-play:active .tile-play__glyph{fill:var(--ink)}
@media(max-width:760px){
  .tile-play{font-size:14px;gap:8px;padding-right:4px}
  .tile-play__label{max-width:76px;text-wrap:balance}
}
/* its ring alone under a 390px tile: the label is still its name, read by
   a screen reader and not drawn (the .visually-hidden pattern) */
@container tile (max-width:389px){
  .tile-play{padding:0}
  .tile-play__label{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
}

/* ---- the seek bar ----
   The real call, October 2026. While the call plays or is paused, a slim
   bar under the captions — a native <input type="range"> over the clip,
   labelled "Position in the call" — jumps back or ahead: a tap or a drag,
   and the arrow keys 5 seconds. A finger is js/voice-play.js's, anywhere
   on the bar: iOS's own range drags only from its thumb. Written `hidden`,
   so at rest and with no script it is not there. It sits in the captions
   card, after the card's screen and its sentence: under the captions on
   every layout, and before the play button in the keyboard's order.
   Besides the play button it is the only control beside the phone, and
   nothing may lie over it ("the shut slot takes no touch", below).

   A 44px box, the site's touch target, around a 4px track and a 16px
   thumb in the button ring's own values: on the tile a --sand-soft track
   (5.48:1 on ink, a control's edge) and a paper thumb; on the hero's paper
   a --text-soft track (6.94:1) and an ink thumb. No fill, no gradient and
   no shadow: the thumb is where the call is, and the ring beside it fills.
   Its focus ring is its ground's, as every control's is. touch-action:none
   lets a finger drag it on a phone without scrolling the page. */
.tile-seek{display:block;flex:none;align-self:stretch;width:100%;height:44px;margin:0;padding:0;
  -webkit-appearance:none;appearance:none;background:transparent;cursor:pointer;
  touch-action:none;pointer-events:auto}
.tile-seek[hidden]{display:none}
.tile-seek::-webkit-slider-runnable-track{height:4px;border:0;border-radius:2px;background:var(--sand-soft)}
.tile-seek::-moz-range-track{height:4px;border:0;border-radius:2px;background:var(--sand-soft)}
.tile-seek::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;margin-top:-6px;
  border:0;border-radius:50%;background:var(--paper)}
.tile-seek::-moz-range-thumb{width:16px;height:16px;border:0;border-radius:50%;background:var(--paper)}
.p-hero__float .tile-seek::-webkit-slider-runnable-track{background:var(--text-soft)}
.p-hero__float .tile-seek::-moz-range-track{background:var(--text-soft)}
.p-hero__float .tile-seek::-webkit-slider-thumb{background:var(--ink)}
.p-hero__float .tile-seek::-moz-range-thumb{background:var(--ink)}
/* on a phone the tile's captions card laps the phone's frame by 6px, and
   the bar stops 6px short of the frame */
@media(max-width:760px){
  .tile-devices--hand .tile-seek{width:calc(100% - 12px)}
}

/* ---- the screens ----
   WHAT IS ON THE DISPLAY. Real text, laid out in one drawing per kind,
   written by tools/gen-pages.js from js/catalog.js.

   ONE DRAWING THAT SCALES. Each .screen is a size container, so .scr sets
   its font-size from the screen's own width — 1cqi is 1% of it — and every
   length inside is in em. A screen is therefore the same picture at 1440
   and at 320: the same line breaks, the same proportions, just smaller.
   Nothing here is a media query on the viewport, because the screen's
   width is what the drawing depends on and the viewport is only one of the
   things that decides it.

   --s IS THE ONE EXCEPTION, and it exists because a phone's WIDTH does not
   fall at the same rate as the space around it. Two steps, both on the
   TILE's width rather than the viewport's:
     - the Voice pair narrows to 160px against a single phone's 200px at
       ≤760, so 1.25 puts its type back at the same pixel size;
     - under a 380px tile the pair's two buttons wrap to a second row and
       take 60px of phone with them, so it steps back to 1.1;
     - under a 340px tile a single phone's buttons wrap too, so it steps to
       0.8.
   The tile's width, not the viewport's, because the harness measures in a
   390px iframe that lays out at 375 — a viewport query would read 390 and
   miss the wrap that has already happened.

   EM OF WHAT. An element that sets its own font-size states its padding,
   margin and radius in em of THAT size; a wrapper that sets none states
   them in em of .scr's. Both scale with the device either way.

   THE FALLBACK, for an engine with no container units: a flat px size
   equal to 1cqi at a 390 layout, which draws every screen at its 390 sizes
   whatever the width. It fits, and at worst it clips. */
/* ────────────────────────────────────────────────────────────
   SCREEN-ONLY COLOURS — NOT BRAND TOKENS, NOT PALETTE
   ────────────────────────────────────────────────────────────
   These values exist ONLY inside a device screen, and a device screen is a
   drawing of THE CUSTOMER'S OWN SOFTWARE — their phone's call screen and
   messages app, their mail app, their website's chat widget, their
   quoting tool. It is not KRM's interface, so it is not held to KRM's
   palette: a text thread whose bubbles were brand orange would read as a
   KRM product, which is exactly the wrong story.

   BRAND v2.2 MADE THEM VIVID (docs/DECISIONS.md, "Brand — v2.2"): real app
   colour in the bubbles, the time chips and the status pill, and a
   colourful Mail icon. A lock wallpaper added five more values until
   homepage v2.3 retired the lock screen; they went with it, and sixteen
   became eleven.

   HOMEPAGE v2.4a DRAWS THE PHONE ITSELF on the Voice, Messaging and
   Follow-up tiles: the in-call screen's dark ground and red end-call, a
   contact's avatar, and the messaging app's own greys — its header rule,
   its input field and edge, its hint text and its plus glyph. The banners
   replaced the notes, and their icons sit on the messaging blue, so the
   Mail icon's two stops are gone. Eleven became twenty. The call's ground
   and the avatar are GRADIENTS — a screen is the one place one may appear,
   because it is the customer's phone and not KRM's interface.

   TWO SCREENS DRAW KRM'S OWN PRODUCT, not the customer's software: the
   Quotes screen (KRM's quote product) and the Custom Agents intake (KRM's
   own form). They are the only screens where KRM's orange may appear — the
   quote's Send, --orange with a white label — and tools/audit.js check 16
   fails an orange anywhere else inside a screen. The intake's Send stays
   ink.

   KRM'S OWN INTERFACE DOES NOT USE ANY OF THESE. Not one. If you find one of
   these values on a button, a link, a label, a border or a ground anywhere
   outside `.screen`, it is a bug — tools/audit.js check 16 fails on it, and
   lists them all, so a new one has to be added there deliberately too. It
   also holds this block to that list, value for value, and fails a value
   declared here that no rule reads.

   THEY ARE DECLARED ON `.scr`, WHICH IS THE ENFORCEMENT. A custom property
   is only visible to the subtree it is declared on, so `var(--screen-blue)`
   resolves to nothing anywhere but inside a screen. You cannot reach for one
   of these from a component even by accident.

   Measured, every pair (WCAG 2.x):
     #FFFFFF on --screen-blue #0B63E6        5.33:1  the business's bubbles in a thread
     --screen-blue on the white screen       5.33:1  the call's header; the bubble's edge
     --screen-bubble-ink on --screen-bubble 14.03:1  the other side's bubbles
     #FFFFFF on --screen-teal #0F766E        5.47:1  the chat widget's bubble
     --screen-teal on the white screen       5.47:1  "Book a time", the widget's outline
                                                     button: its label and its edge
     --screen-go-ink on --screen-go          5.58:1  the "Ready to send" pill
     --screen-go on the white screen         2.22:1  the call's voice level — NON-TEXT
                                                     decoration in an aria-hidden screen
     --screen-green on the white screen      5.02:1  the online dot (non-text)
     --screen-slot-ink on --screen-slot      7.42:1  the time chips, all one colour
     --screen-slot on the white screen       1.19:1  the chip's fill — the words carry it
     #FFFFFF glyph on --screen-blue          5.33:1  a banner's icon tile; the messaging
                                                     app's send button
     on the call's ground, #2B2F37 → #16181D at 48% → #0B0C0F, where each sits:
       #FFFFFF, the status bar and the timer 13.67:1 at its top, 15.85:1 at the timer
       #FFFFFF at 90%, KRM's line             12.53:1
       #FFFFFF at 62%, the number, "Call ended" 6.78:1 and 7.00:1
       the control labels, #FFFFFF at 78% on the panel (#FFFFFF at 8%): 9.98:1
         while the call plays; 3.49:1 at rest, where the panel is dimmed to
         half — the drawn state of controls that are inactive once the call
         has ended, inside an aria-hidden screen
       --screen-end, the red end-call         4.89:1 on the ground; its white
                                                     handset 3.91:1 — non-text
     #FFFFFF on --screen-av-top → --screen-av-foot   2.72:1 → 4.89:1 — a contact's
                                             initial or glyph on its avatar, decoration
     --screen-bubble-ink on the white screen 17.01:1 the messaging app's contact name
     --screen-hint on the white screen       4.78:1  the timestamp, the input's hint
     --screen-glyph on --screen-field        5.77:1  the input bar's plus — non-text
     --screen-rule, --screen-field-edge      1.24:1, 1.41:1 on the white screen —
                                             the header's hairline and the field's
                                             edge, decorative
     --screen-field on the white screen      1.12:1  the plus button's fill
   ──────────────────────────────────────────────────────────── */
.scr{
  --screen-blue:#0B63E6;        /* messaging blue: the business's bubbles, the call's header,
                                   a banner's icon, the messaging app's back and send */
  --screen-bubble:#E9E9EB;      /* the other side's bubble, in a thread and in the chat widget */
  --screen-bubble-ink:#1C1C1E;  /* its text; the messaging app's contact name and status bar */
  --screen-green:#188038;       /* the chat assistant's "online" dot */
  --screen-go:#34C759;          /* the system green: the call's voice level, the quote's
                                   status pill, the live captions' dot */
  --screen-go-ink:#0B3D1A;      /* the status pill's text */
  --screen-teal:#0F766E;        /* the website chat widget: its bubble, and its button's edge and label */
  --screen-slot:#E2ECFC;        /* the time chips: the bubbles' blue as a 12% tint, */
  --screen-slot-ink:#0845A1;    /*   and the same blue at 70% for their text */
  --screen-call-top:#2B2F37;    /* the in-call screen's ground, top, */
  --screen-call-mid:#16181D;    /*   at 48%, */
  --screen-call-foot:#0B0C0F;   /*   and foot (tools/hand-plate.py's CALL_GROUND draws the same) */
  --screen-end:#E5484D;         /* the end-call button */
  --screen-av-top:#9A9DA4;      /* a contact's avatar, top to foot */
  --screen-av-foot:#6E7178;
  --screen-rule:#E6E6EB;        /* the messaging app's header hairline */
  --screen-field:#F2F2F5;       /* its plus button */
  --screen-field-edge:#D9D9DE;  /* its input field's edge */
  --screen-hint:#727277;        /* its timestamp and the field's hint */
  --screen-glyph:#5E5E63;       /* its plus glyph */
}

.scr{position:absolute;inset:0;display:flex;flex-direction:column;
  text-align:left;color:var(--ink);font-size:1.8px;letter-spacing:0}
.device--laptop .scr{font-size:2.5px}
@supports (font-size:1cqi){
  .device .scr{font-size:calc(var(--s,1) * 1cqi)}
}
@media(max-width:760px){
  .tile-devices--pair .scr{--s:1.25}
}
/* After the media query above, so it wins where both apply. */
@container tile (max-width:380px){
  .tile-devices--pair .scr{--s:1.1}
}
@container tile (max-width:340px){
  .tile-devices--phone:not(.tile-devices--pair) .scr{--s:0.8}
}

/* -- a call, with both sides captioned --
   The AI receptionist's hero from homepage v2.4a until v2.4b phase 3. NO
   PAGE DRAWS IT NOW: the homepage tile and the hero both draw the phone's
   in-call screen ("the tiles' own screens", below). These rules, the voice
   meter's and the captions' with them, are left for the dead-CSS task. The
   header takes the phone's own blue (brand v2.2): 5.33:1 on the white
   screen. */
.scr--call{padding:9em 4.5em 0;align-items:center}
.scr-head{font-size:4.45em;line-height:1.35;color:var(--screen-blue);text-align:center}
.scr-num{margin-top:.5em;font-size:7.4em;line-height:1.2;font-weight:500}
/* 1.5em, NOT the 5em this rule used to carry. The voice meter below sits in
   that gap, and the gap is now 1.5 + 9 + 1.5 = 12em — see .scr-vu. The extra
   7em IS a deliberate downward move of everything below it, not an accident:
   at 5em the meter measured 24x6px at 1440, too small to read as a waveform
   or to show that it was moving. The clearances that move with it are
   measured, not assumed — see .scr-vu. */
.scr-caps{margin-top:1.5em;display:flex;flex-direction:column;gap:4em;
  width:100%;text-align:left}
.scr-who{font-size:4.45em;line-height:1.3;color:var(--text-soft)}
.scr-say{margin-top:.3em;font-size:5em;line-height:1.45}

/* ---- the voice meter, between the number and the first caption ----
   UNUSED SINCE v2.4b PHASE 3, with the white call above: left for the
   dead-CSS task. Five bars, on the AI receptionist's hero, standing still,
   until the hero drew the tile's call, which has none. Written by
   tools/gen-pages.js, screenBody() case "call", as five bare spans;
   everything about them is here. (On the homepage Voice tile they moved,
   while Ava spoke and in an idle loop after; homepage v2.4a took the call
   off that tile's screen, and its motion rules went with it.)

   IT MOVES EVERYTHING BELOW IT DOWN BY 7em, DELIBERATELY. The gap between
   .scr-num and .scr-caps was one value, margin-top:5em. It is now

       1.5em  (this rule's margin-top)
     + 9em    (this rule's height)
     + 1.5em  (.scr-caps margin-top, above)
     = 12em

   The first version fitted inside the old 5em and cost nothing, and that was
   the mistake: at 1440 the meter came out 24x6px, which is too small to read
   as a waveform at all, let alone to show that it is moving. Three times the
   size needs the room, so the captions move down 7em.

   THAT MOVE IS MEASURED, NOT ASSUMED. A call screen's content has to clear
   the hero's crop box, which shows 60% of the phone — and until v2.4a the
   tile's caption band too — and nothing here would catch it: the bars
   carry no text, so the overflow
   harness skips them (it walks only elements with a direct text node) and
   audit.js check 12 never sees them (it sweeps strip()ped text). What the
   harness DOES see is the caller's line, which is text, so a meter that
   pushes it too far fails as "a line spills off the screen" with no mention
   of the meter. Clearances were measured at four sizes before this landed;
   tools/README.md carries the numbers.

   .scr--call is align-items:center, so the meter centres itself — the same
   default .scr-caps opts out of with width:100%.

   THE COLOUR IS THE PHONE'S OWN GREEN, --screen-go (brand v2.2): a live
   call's level drawn in the system green of the phone it is on, and so a
   screen-only colour, declared on .scr and never KRM's. It was --text-mute,
   a neutral, until v2.2 made the screens vivid. It is NON-TEXT and
   decorative: 2.22:1 on the white screen, inside an aria-hidden screen
   whose one sentence says what the screen shows, so it carries nothing a
   reader needs — which is why it may sit under the 3:1 a meaningful graphic
   would need. Never orange: a screen draws the CUSTOMER'S software, and
   KRM's interface colour inside it would tell the wrong story. Nothing
   enforces that — the two-stop probe skips screens — so it is discipline,
   not a check.

   --r IS THE BAR'S REST HEIGHT, and the settled shape is the waveform
   .3 / .5 / .7 / .5 / .3 — ılılı. The product hero never animates (it is not
   a .tile and js/screens.js is not loaded there), so this still frame is
   what every reader of that page sees. It has to read as a voice on its
   own, which a row of equal stubs does not. */
.scr--call .scr-vu{margin-top:1.5em;height:9em;display:flex;gap:1.8em}
.scr--call .scr-vu span{--r:.3;width:2.4em;height:100%;border-radius:1.2em;
  background:var(--screen-go);
  transform:scaleY(var(--r));transform-origin:50% 100%}
.scr--call .scr-vu span:nth-child(2),
.scr--call .scr-vu span:nth-child(4){--r:.5}
.scr--call .scr-vu span:nth-child(3){--r:.7}

/* -- a text thread, from the BUSINESS's side: its messages right, the
      customer's left, and the grey line only its own phone shows -- */
.scr--thread{padding:9em 4em 0;gap:3em}
.scr-sys{align-self:center;font-size:4.45em;line-height:1.3;color:var(--text-soft);
  text-align:center}
.scr-msg{max-width:80%;padding:.55em .75em;border-radius:.9em;
  font-size:5em;line-height:1.4}
.scr-msg--business{align-self:flex-end;background:var(--ink);color:var(--paper)}
/* The other side's bubble is the grey every phone draws it in (brand v2.2),
   in a thread and in the chat widget alike: 14.03:1. */
.scr-msg--customer{align-self:flex-start;background:var(--screen-bubble);color:var(--screen-bubble-ink)}
/* IN A TEXT THREAD ONLY, the business's own bubbles take the messaging blue
   every phone draws them in — white on it is 5.33:1, and the bubble reads off
   the white screen at the same 5.33:1. Scoped to .scr--thread on purpose:
   the assistant on the customer's WEBSITE (.scr--site-chat) is a chat
   widget, not a text message, and takes the widget's own teal instead. */
.scr--thread .scr-msg--business{background:var(--screen-blue);color:#FFFFFF}
/* THE OPENINGS, brand v2.2: the times a booking link offers, drawn as the
   chips a calendar draws them in, right-aligned under the message that
   offers them. THE TYPE IS 4.45em, the smallest on a phone screen and the
   same as .scr-sys, so it sits on the 8px floor at a true 390 layout and
   never under it — the card variant B drew them in set them at 3.2em, 5.8px,
   and ran the Messaging thread under the caption band. Messaging's three
   fit one row; Follow-up's longer ones wrap to two, and it has the room. A
   chip is a label, never a control: a <p>, like every "button" on a
   screen. ONE COLOUR FOR EVERY CHIP (owner's review of v2.2): the bubbles'
   own blue as a soft tint, its text the same blue deepened, 7.42:1 — the
   chips read as part of the thread that offers them. v2.2 first drew them
   in three colours, green, blue and pink; they are gone. */
.scr-slots{align-self:flex-end;display:flex;flex-wrap:wrap;justify-content:flex-end;gap:1.2em}
.scr-slot{padding:.45em .8em;border-radius:.6em;font-size:4.45em;line-height:1.3;font-weight:500;
  background:var(--screen-slot);color:var(--screen-slot-ink)}

/* -- the customer's own site, with the assistant open over it -- */
.scr--site-chat{padding:0}
.scr-site{padding:3em 3.5em;border-bottom:1px solid var(--hairline)}
.scr-site-addr{display:inline-block;padding:.35em .9em;border-radius:1em;
  background:var(--panel);font-size:2.8em;line-height:1.4;color:var(--text-soft)}
.scr-site-name{margin-top:.35em;font-size:4.6em;line-height:1.2;font-weight:500}
.scr-chat{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;
  align-items:flex-start;gap:2.2em;padding:3em 3.5em}
.scr-chat-head{font-size:2.8em;line-height:1.35;color:var(--text-soft)}
/* the assistant's "online" dot — the convention every chat widget uses, and
   the customer's widget, not KRM's. Non-text, 5.02:1 on the white screen. */
.scr-chat-head::before{content:"";display:inline-block;width:.58em;height:.58em;
  margin-right:.5em;border-radius:50%;background:var(--screen-green);
  vertical-align:.04em}
.scr--site-chat .scr-msg{max-width:72%;font-size:3.4em}
/* the widget's own colour, brand v2.2: its bubble in teal, white on it
   5.47:1. Its "Book a time" is the outline button below, .scr-pill. */
.scr--site-chat .scr-msg--business{background:var(--screen-teal);color:#FFFFFF}

/* -- a quote, in KRM's own quote product, whose price column is bars
      because the homepage prints no amount and an invented one would break
      two copy rules at once. The status sits top right, on the title's first
      line; the action sits bottom right, under the lines. -- */
.scr--quote{padding:4em 4em 0}
.scr-doc-head{display:flex;align-items:baseline;justify-content:space-between;gap:2.4em}
.scr-doc-head .scr-chip{flex:none}
/* Beside the pill the title takes two lines at every size (the screen is
   one drawing at every width); balanced, so the break falls between the
   business and the quote rather than leaving "R." alone. The catalog's
   title holds "Dana R." together with a no-break space for an engine that
   does not balance. */
.scr-doc-head .scr-doc-title{text-wrap:balance}
.scr-doc-title{font-size:4.6em;line-height:1.25;font-weight:500}
.scr-rows{margin-top:3.4em;display:flex;flex-direction:column;gap:2.2em}
.scr-row{display:flex;align-items:center;justify-content:space-between;gap:2em;
  padding-bottom:2em;border-bottom:1px solid var(--hairline)}
.scr-row-text{font-size:3.4em;line-height:1.35}
.scr-bar{flex:none;width:15%;height:1em;border-radius:.5em;
  background:rgba(46,46,43,.14)}
.scr-foot{margin-top:3.4em;display:flex;justify-content:flex-end}
.scr-chip{padding:.4em .9em;border-radius:1em;background:var(--panel);
  font-size:2.8em;line-height:1.4;color:var(--text-soft)}
/* "Ready to send" — the quote's status pill, in the system green it took
   at brand v2.2, with dark text on it at 5.58:1. The owner's review moved
   it beside the title and kept its colour. Not to be confused with the
   BRAND chip, which is orange, lives on the launch line, and is a
   different component entirely. */
.scr--quote .scr-chip{background:var(--screen-go);color:var(--screen-go-ink)}

/* -- the intake box, typing itself in -- */
.scr--intake{padding:4em 4em 0;align-items:flex-start}
.scr-box{margin-top:2.8em;width:100%;padding:2.6em 3em;
  border:1px solid var(--hairline);border-radius:1.2em;background:var(--panel)}
.scr-box-text{font-size:3.4em;line-height:1.5}
.scr--intake .scr-pill{margin-top:3em}

/* -- a real photograph, the one screen that is not a drawing -- */
.scr--shot{padding:0}
.scr--shot picture{display:block;width:100%;height:100%}
.scr--shot img{display:block;width:100%;height:100%;
  object-fit:cover;object-position:50% 0}

/* A button inside a screen is a <p>, never a control: see
   tools/gen-pages.js, "the device screens". Ink by default — which is what
   the Custom Agents intake keeps: that screen draws KRM's OWN form.

   THE CHAT WIDGET'S "Book a time" IS AN OUTLINE BUTTON (owner's review of
   v2.2): white, with the widget's teal as its edge and its label, 5.47:1 on
   the white screen. Filled teal it read as one more chat bubble. The
   padding gives back the 1px edge, so the button is the size it was.

   THE QUOTE'S "Send to customer" IS KRM's ORANGE, --orange with a white
   label, 5.42:1 — the light stop, because a screen is white. The Quotes
   screen draws KRM's own quote product, like the intake draws KRM's own
   form, so KRM's interface colour is right there and nowhere else inside a
   screen: tools/audit.js check 16 (e). */
.scr-pill{align-self:flex-start;padding:.5em 1.1em;border-radius:1.4em;
  background:var(--ink);color:var(--paper);font-size:3.4em;line-height:1.35}
.scr--site-chat .scr-pill{margin-top:.4em;padding:calc(.5em - 1px) calc(1.1em - 1px);
  border:1px solid var(--screen-teal);background:#FFFFFF;color:var(--screen-teal)}
.scr--quote .scr-pill{background:var(--orange);color:#FFFFFF}

/* ---- the tiles' own screens, homepage v2.4a ----
   The Voice, Messaging and Follow-up tiles draw the phone itself, as the
   owner chose from the v2.4 mockups and refined in round 2: the Voice call
   as the phone's in-call screen (Voice C · Panel), and each thread as the
   business's messaging app (M-A · Clean). tools/gen-pages.js writes them on
   a tile — inCall() and messagingApp() — and on the heroes that draw their
   tile's app (.p-hero__media--app): the messaging app on the two thread
   heroes since v2.4b phase 2, and the call on the AI receptionist's since
   phase 3, in its own sizes (.p-hero__media--call, in §4). Every rule here
   is scoped to them, by classes only they carry or by .tile,
   .p-hero__media--app and .device--hand.

   A PHONE'S STATUS BAR heads both: the time, signal and battery — no
   percentage, no carrier — centred on the line a phone's camera sits on
   (the plain plate draws its camera 2.56% down the screen), inside each
   screen's rounded top corners. White on the call, the app's own ink on a
   thread. */
.scr-sb{position:absolute;left:0;right:0;top:0;z-index:2;height:11.2em;
  display:flex;align-items:center;justify-content:space-between;pointer-events:none}
.scr-sb-time{font-size:4.45em;line-height:1;font-weight:600;letter-spacing:.01em;font-variant-numeric:tabular-nums}
.scr-sb-icons{display:flex;align-items:center;gap:1.5em}
.scr-sb svg{display:block;width:auto;height:3em}
.device--hand .scr-sb{padding:0 10em 0 11em}
.device--phone .scr-sb{padding:0 7.5em 0 8.5em}
.scr--call .scr-sb{color:#FFFFFF}
.scr--thread .scr-sb{color:var(--screen-bubble-ink)}

/* THE CALL, on the hand's phone. Its own dark ground, the gradient the
   phone draws, top to foot — the same three stops tools/hand-plate.py's
   fit sheet draws the traced outline on. The screen under it takes the
   foot, so no white shows at the outline's edge: a background is the one
   thing a second rule may set on the hand's screen (tools/audit.js check
   12 holds the rest to docs/devices/hand-screen-rect.json).

   Top to foot: the round "K" avatar, KRM's line in white at 90% with its
   number at 62% (check 12 holds the two words exactly), the large timer
   with the call's status under it, and at the foot a translucent panel
   with three round controls and the red end-call. Everything a phone
   needs to show sits in the part of the screen the tile shows: on desktop
   the panel stands at the foot of the visible window, over the band; on a
   phone the call is compact, the panel right under the status, and under
   a 390px tile the controls lose their labels.

   AT REST THE CALL HAS ENDED — the markup's frame: the timer at the call's
   real length, 1:31, "Call ended", the panel dimmed to half, as a phone
   shows controls that have nothing left to do. While the call plays they
   are bright and the timer counts on the call's own clock (the real call
   plays, below). */
.tile .device--hand .screen{background:#0B0C0F}
.device--hand .scr--call{padding:16em 5em 27em;align-items:center;text-align:center;color:#FFFFFF;
  background:linear-gradient(180deg,var(--screen-call-top) 0%,var(--screen-call-mid) 48%,var(--screen-call-foot) 100%)}
.scr-av{flex:none;display:flex;align-items:center;justify-content:center;width:20em;height:20em;
  border-radius:50%;background:linear-gradient(180deg,var(--screen-av-top),var(--screen-av-foot));color:#FFFFFF}
.scr-av span{font-size:9em;font-weight:500;line-height:1}
.device--hand .scr-head{margin-top:.75em;font-size:5.6em;line-height:1.2;font-weight:500;color:rgba(255,255,255,.9)}
.device--hand .scr-num{margin-top:.3em;font-size:4.45em;line-height:1.3;font-weight:400;color:rgba(255,255,255,.62)}
.scr-status{margin-top:2em;display:flex;flex-direction:column;align-items:center;gap:.25em;
  font-size:4.45em;line-height:1.3;color:rgba(255,255,255,.62);font-variant-numeric:tabular-nums}
.scr-timer{position:relative;display:inline-block;font-size:3.6em;font-weight:400;letter-spacing:-.01em;
  line-height:1.05;color:#FFFFFF}
.scr-ended{white-space:nowrap}
.scr-panel{margin-top:auto;align-self:stretch;display:flex;flex-direction:column;align-items:center;
  padding:8em 5em 7em;border-radius:9em;background:rgba(255,255,255,.08);opacity:.5}
.scr-ctls{align-self:stretch;display:flex;justify-content:space-around}
.scr-ctl{display:flex;flex-direction:column;align-items:center;gap:1.8em}
.scr-btn{display:flex;align-items:center;justify-content:center;width:17em;height:17em;
  border-radius:50%;background:rgba(255,255,255,.15)}
.scr-btn svg{display:block;width:46%;height:46%}
.scr-lbl{font-size:4.45em;line-height:1.3;color:rgba(255,255,255,.78)}
.scr-end{margin-top:6em;display:flex;justify-content:center}
.scr-end-btn{display:flex;align-items:center;justify-content:center;width:19em;height:19em;
  border-radius:999em;background:var(--screen-end)}
.scr-end-btn svg{display:block;width:50%;height:50%}
@media(max-width:760px){
  .device--hand .scr--call{padding:12.5em 5em 0}
  .scr-av{width:10.5em;height:10.5em}
  .scr-av span{font-size:4.9em}
  .device--hand .scr-head{margin-top:.45em}
  .scr-status{margin-top:.5em}
  .scr-timer{font-size:2em}
  .scr-panel{margin-top:3em;padding:3em 4em;border-radius:7em}
  .scr-btn{width:13em;height:13em}
  .scr-end{margin-top:3em}
  .scr-end-btn{width:13em;height:13em}
}
@container tile (max-width:389px){
  .scr-lbl{display:none}
}

/* THE MESSAGING APP, on a thread's phone. The contact header — a back
   chevron in the messaging blue, a round avatar with the contact's initial
   or a generic person (never a face), the name — sits a little way under
   the status bar, over a hairline. The thread starts DIRECTLY UNDER IT, at
   the top, as a short conversation does in a real app: the timestamp, if
   the thread has one ("Today 10:05 AM"; on Messaging the system line
   "Missed call · 2:14 PM" is the one timestamp), just under the hairline,
   and the messages straight after it. The owner asked for both in round 2's
   review: the header further down from the status bar, and no empty gap
   above the timestamp.

   THE BUBBLES ARE THE THREAD'S OWN, a little tighter, with a tail on the
   last of each run: a square of the bubble's colour past its bottom
   corner, its far edge carved by a white curve — the classic hook. The
   bubble is its own stacking context, and the tail's two pieces sit behind
   its text: the colour reaches inside the bubble, and painted over the
   text it would hide a letter.

   THE INPUT BAR sits at the foot of the part of the phone the tile shows,
   not of the screen: the caption band cuts a phone at ~71% of its height
   on desktop and ~60% on a phone, and how much shows depends on how the
   tile's words and buttons wrap. So it is placed from the window itself.
   100cqh is the wrapper's height, which is the phone's visible window
   plus the 32px band (.tile-devices--phone); 5.4363cqi is how far the
   screen starts below the phone's top (2.375% of a plate 1600/775 as tall
   as it is wide, over a screen 90.194% of its width). The bar's foot is
   4px over the band at every width; the harness holds it within 6px, and
   holds every line of the thread clear of it. The em value before it is
   for an engine without container units: the bar where a 390 layout puts
   it. */
.tile .scr--thread,.p-hero__media--app .scr--thread{padding:0;gap:0}
.scr-th-head{position:relative;z-index:1;flex:none;display:flex;align-items:center;gap:2.6em;
  padding:12.6em 5em 2em;border-bottom:1px solid var(--screen-rule);background:#FFFFFF}
.scr-th-back{flex:none;width:3.6em;height:6.4em;color:var(--screen-blue)}
.scr-th-back svg{display:block;width:100%;height:100%}
.scr-th-av{flex:none;display:flex;align-items:center;justify-content:center;width:10em;height:10em;
  border-radius:50%;background:linear-gradient(180deg,var(--screen-av-top),var(--screen-av-foot));color:#FFFFFF}
.scr-th-av svg{display:block;width:62%;height:62%}
.scr-th-av span{font-size:4.8em;font-weight:500}
.scr-th-name{font-size:5.2em;line-height:1.2;font-weight:600;color:var(--screen-bubble-ink)}
.scr-th-body{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:1.6em;padding:1.4em 4em 0}
.scr-th-stamp{align-self:center;font-size:4.45em;line-height:1.3;color:var(--screen-hint)}
.tile .scr--thread .scr-msg,.p-hero__media--app .scr--thread .scr-msg{position:relative;z-index:0;
  max-width:86%;padding:.45em .74em;font-size:4.6em;line-height:1.3}
.tile .scr--thread .scr-slots,.p-hero__media--app .scr--thread .scr-slots{gap:1.1em;margin-top:-.7em}
.scr-tail::before,.scr-tail::after{content:"";position:absolute;bottom:0;z-index:-1}
.scr-msg--business.scr-tail{border-bottom-right-radius:.2em}
.scr-msg--business.scr-tail::before{right:-.9em;width:1.9em;height:1.6em;
  background:var(--screen-blue);border-bottom-left-radius:.6em}
.scr-msg--business.scr-tail::after{right:-1.5em;width:1.5em;height:1.7em;
  background:#FFFFFF;border-bottom-left-radius:1.5em 1.3em}
.scr-msg--customer.scr-tail{border-bottom-left-radius:.2em}
.scr-msg--customer.scr-tail::before{left:-.9em;width:1.9em;height:1.6em;
  background:var(--screen-bubble);border-bottom-right-radius:.6em}
.scr-msg--customer.scr-tail::after{left:-1.5em;width:1.5em;height:1.7em;
  background:#FFFFFF;border-bottom-right-radius:1.5em 1.3em}
.scr-th-input{position:absolute;left:3.6em;right:3.6em;top:117em;top:calc(100cqh - 36px - 5.4363cqi - 8.6em);
  display:flex;align-items:center;gap:2.2em}
.scr-th-plus,.scr-th-send{flex:none;display:flex;align-items:center;justify-content:center;
  width:8.6em;height:8.6em;border-radius:50%}
.scr-th-plus svg,.scr-th-send svg{display:block;width:50%;height:50%}
.scr-th-plus{background:var(--screen-field);color:var(--screen-glyph)}
.scr-th-send{background:var(--screen-blue);color:#FFFFFF}
.scr-th-field{flex:1;min-width:0;height:8.6em;display:flex;align-items:center;padding:0 1.6em 0 3.4em;
  border:1px solid var(--screen-field-edge);border-radius:4.3em;background:#FFFFFF}
.scr-th-field span{flex:1;font-size:4.45em;line-height:1;color:var(--screen-hint)}

/* ---- the screens move ----
   A tile plays its screen when it reaches 40% in view, and again each time
   it comes back — js/screens.js moves .is-armed and .is-playing. Every
   timing is in the markup, as --at on the part and --w on the word, so the
   sequence is readable in the page rather than hidden in a script.

   REPLAY IS A RESET, NOT A LOOP. Dropping .is-playing takes the animation
   off the element; adding it again is a fresh start. screens.js only does
   that once the tile is FULLY out of view, so a screen never restarts
   under the reader's eye.

   THE FINAL FRAME IS THE DEFAULT MARKUP. Motion only hides and reveals:
   nothing here is the only way a word gets on the screen. So with
   JavaScript off no tile is ever armed and every screen is simply
   finished, which is also what the generated HTML says with no CSS at all.

   backwards FILL is what holds a part hidden until its turn: the element
   takes the keyframes' from-state through the whole delay, then animates
   to its own ordinary style. No second class, no per-step selector.

   UNDER REDUCED MOTION js/screens.js exits before arming anything, and
   tokens.css kills the animations outright. The rule at the end is for the
   gap between those two: a tile armed a frame before the preference is
   read would otherwise keep opacity:0 with no animation left to clear it. */
@keyframes scr-fade{from{opacity:0}to{opacity:1}}
@keyframes scr-rise{from{opacity:0;transform:translateY(.8em)}to{opacity:1;transform:none}}
@keyframes scr-drop{from{opacity:0;transform:translateY(-16px) scale(.97)}to{opacity:1;transform:none}}

.tile.is-armed:not(.is-playing) .scr-step,
.tile.is-armed:not(.is-playing) .scr-w{opacity:0}

.tile.is-playing .scr-step{animation:scr-rise 320ms var(--ease) backwards;
  animation-delay:var(--at,0ms)}
.tile.is-playing .scr-w{animation:scr-fade 90ms linear backwards;
  animation-delay:calc(var(--at,0ms) + var(--w,0) * 55ms)}
/* A banner comes DOWN, as a phone's notification does, and settles: on
   Messaging and Follow-up when the reply it announces lands in the thread,
   at 900ms. (The Voice players' email drops the same way when the call
   ends, below.) */
.tile.is-playing .float-card.scr-step{animation:scr-drop 450ms var(--ease) backwards;
  animation-delay:var(--at,0ms)}

/* ---- the real call plays (October 2026) ----
   "Play a sample call" plays a real call: its recording, 89 seconds, on
   the homepage Voice tile and the AI receptionist's hero alike. Audio
   buffers, pauses and seeks, so no CSS delay can follow it: the clock is
   js/voice-play.js's — the clip's own time — and the script moves classes,
   never timings: the player's .is-playing and .is-ended, and on the
   captions a word's .is-said and .is-now and a line's .is-on. These rules
   style those classes, both players named by [data-play], and nothing
   else. They replace the 14-second sample's keyframes, whose every moment
   was an --at in the markup.

   ONLY THE UNSAID STATE IS STYLED. While the call plays a word not said yet
   is font-size 0 — no room, no colour — and so is a speaker's name until
   the first word of its line. A said word falls back on its own rest
   style: sand on the tile, ink on the hero. So the frame the call ends on
   is the rest frame by construction, and seeking back un-says words simply
   by taking .is-said off them.

   THE WORD BEING SAID is .is-now: full white with a thin stroke in its own
   white on the tile, a thin ink stroke on the hero — weight without width,
   so the line never reflows. The script holds it on the latest word for
   half a second at most, or until the next one starts.

   WHILE IT PLAYS, AND UNTIL IT ENDS (.is-playing:not(.is-ended), paused
   or not): the email banner's slot is shut and the email hidden, "Call
   ended" hidden, and the panel bright; on a phone, and in the hero at
   900px and under, the captions show in the email's place, where they
   rest hidden. The captions stand on the play button in the desktop
   columns, so their seek bar never moves under the pointer either.

   THE SHUT SLOT TAKES NO TOUCH. In the hero at 900px and under it keeps
   the email's box, over the captions' foot, and it is positioned to sit
   above their card — so on an iPhone it lay over the seek bar and took
   every tap and drag meant for it (8 October 2026). While the call is on
   it lets every pointer through; the email inside it is hidden, so
   nothing that could be pressed is lost.

   WHEN IT ENDS (.is-ended), for the second before the player rests: the
   slot opens and the email drops in, and "Call ended" fades in, the panel
   dimmed. Every other part is already at rest, so when the classes go
   nothing jumps. Under reduced motion tokens.css stops the animations and
   the end is the rest frame outright; the call still plays, with sound,
   its words arriving and its timer counting — state, not motion. */
[data-play].is-playing .scr-cc-w:not(.is-said),
[data-play].is-playing .scr-cc-line:not(.is-on) .scr-cc-who{font-size:0}
.tile.is-playing .scr-cc-w.is-now{color:#FFFFFF;-webkit-text-stroke:.05em #FFFFFF}
.p-hero__media.is-playing .scr-cc-w.is-now{-webkit-text-stroke:.05em var(--ink)}

[data-play].is-playing:not(.is-ended) .float-slot{grid-template-rows:0fr;pointer-events:none}
[data-play].is-playing:not(.is-ended) .float-slot > .float-card{opacity:0;visibility:hidden}
[data-play].is-playing:not(.is-ended) .scr-ended{visibility:hidden}
[data-play].is-playing:not(.is-ended) .scr-panel{opacity:1}
@media(min-width:761px){
  .tile.is-playing:not(.is-ended) .float-card--captions{margin-top:auto}
  .tile.is-playing:not(.is-ended) .float .tile-play{margin-top:0}
}
@media(min-width:901px){
  .p-hero__media.is-playing:not(.is-ended) .float-card--captions{margin-top:auto}
  .p-hero__media.is-playing:not(.is-ended) .p-hero__float .tile-play{margin-top:0}
}
/* ON A PHONE the tile's captions take the email's place over the frame for
   the call. Their box keeps its rest size from the press — the window is
   a fixed 56px, so the seek bar under it never moves as the words come —
   and the bar's 44px comes out of the window: about three lines show, the
   oldest fading at its top through a fade 14px deep, as on desktop; at a
   340px tile and under, two. The card never reaches the play button below
   it. */
@media(max-width:760px){
  .tile.is-playing:not(.is-ended) .tile-devices--hand .float-card--captions{opacity:1}
  .tile.is-playing:not(.is-ended) .tile-devices--hand .scr-cc-scroll{height:56px;max-height:none;padding-top:0;
    -webkit-mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.6) 7px,#000 14px);
    mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.6) 7px,#000 14px)}
}
@container tile (max-width:340px){
  .tile.is-playing:not(.is-ended) .tile-devices--hand .scr-cc-scroll{height:38px}
}
/* IN THE HERO AT 900px AND UNDER the captions take the email's box over
   the phone's top, which has room for two lines and no more: so for the
   call the card reaches 44px further UP, into the 64px the figure keeps
   above itself, and the seek bar takes the box's foot. The card fills its
   box from the press, the words arriving at the foot of its window, so the
   bar never moves. It never reaches down over the screen, and nothing on
   the page moves: the card is placed absolutely. */
@media(max-width:900px){
  .p-hero__media--call.is-playing:not(.is-ended) .float-card--captions{opacity:1;top:-44px}
  .p-hero__media--call.is-playing:not(.is-ended) .float-card--captions > .screen,
  .p-hero__media--call.is-playing:not(.is-ended) .scr--captions,
  .p-hero__media--call.is-playing:not(.is-ended) .scr-cc-scroll{flex:1 1 auto}
}

@keyframes scr-open{from{grid-template-rows:0fr}to{grid-template-rows:1fr}}
[data-play].is-ended .float-slot{animation:scr-open 420ms var(--ease) backwards}
[data-play].is-ended .float-slot > .float-card{animation:scr-drop 450ms var(--ease) backwards}
[data-play].is-ended .scr-ended{animation:scr-fade 240ms linear backwards}

@media(prefers-reduced-motion:reduce){
  .tile.is-armed .scr-step,.tile.is-armed .scr-w{opacity:1}
}

/* Read by a screen reader, drawn by nothing. One sentence per device,
   because the screen it describes is aria-hidden: its text is a picture of
   a conversation, and reading it aloud word by word would be worse than
   useless. clip-path rather than clip, which is deprecated, and
   white-space:nowrap so the sentence cannot be collapsed to one character
   per line by the 1px box. */
.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}


/* ============================================================
   3. CATEGORY LINEUP
   ============================================================ */

/* ---- the top of a lineup page, apple.com/mac's ----
   The category's name at apple's "Mac" scale, then the product rail, and
   the page as it was underneath. The name is the page's h1 — it is what
   the page is — so the tagline below it is an h2 that keeps the h1's
   looks, and the kicker that used to repeat the category name is gone.

   600 IS APPLE'S WEIGHT FOR THIS, and the brand's one exception to
   "headings are 500", shared with the homepage tile names. See CLAUDE.md. */
.lineup-title{padding-top:64px;font-size:80px;font-weight:600;
  letter-spacing:-0.015em;line-height:1.05;color:var(--ink)}
.lineup-hero__title{max-width:20ch;font-size:var(--fs-page);
  letter-spacing:var(--track-page);line-height:1.13;color:var(--ink)}
@media(max-width:760px){
  .lineup-title{padding-top:40px;font-size:48px;line-height:1.08333}
}

/* ---- the product rail ----
   apple.com/mac's chapter nav: the product names, evenly spaced, the row
   centred in the 980px container with a hairline under it. No price — see
   tools/gen-pages.js, "THE RAIL".

   NAMES ONLY, FOR NOW. Each item carried the category's device plate above
   its name, and a plate is a device with a blank screen: at 56px every
   item drew the same small black rectangle. The thumbnails come back after
   Part C on each product's own filled screen, and the item is still a
   column, so they return above the name without moving anything else.

   EVENLY SPACED IS AN EVEN GAP, NOT AN EVEN WIDTH. A name sets its own
   width and never wraps, so the distance between any two items is the
   same whether they read "Apps" or "Sales and lead qualifying". Fixed
   120px items were sized for a thumbnail, and a 17px name does not fit
   one.

   16px OF GAP, 4px EACH SIDE OF A NAME — 24px between one name and the
   next. Measured, not chosen: Follow-up's five names are 847px of type at
   17px, and the container is 980px, so 133px is all the spacing there is
   to spend before the widest rail stops fitting on a 1440 screen. This
   spends 119 and leaves 18px either side. The 4px is what keeps the inset
   focus ring off the first and last letter.

   CENTRED WHEN IT FITS, SCROLLING WHEN IT DOES NOT. The track is
   max-content wide with auto inline margins inside the scroller: the
   margins centre it while there is room and resolve to zero when there is
   not, so it scrolls from its first item. justify-content:center would
   instead push the first item out of reach on the left. Snap points make
   the sideways scroll land on an item, and it happens inside .rail-scroll
   — the page body never scrolls sideways.

   THE RING IS INSET. A scroller clips whatever its children paint outside
   it, so an offset ring would be cut at the top and bottom edges. */
.rail{margin-top:32px;border-bottom:1px solid var(--hairline)}
.rail-scroll{overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:thin;
  scroll-snap-type:x mandatory}
.rail-track{display:flex;gap:16px;width:max-content;margin-inline:auto;
  padding:6px 0 14px;list-style:none}
.rail-item{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:8px;min-height:44px;padding:0 var(--sp-1);scroll-snap-align:start;
  white-space:nowrap;text-align:center;text-decoration:none;color:inherit;
  border-radius:var(--radius)}
.rail-item:hover{text-decoration:none}
.rail-item:focus-visible{outline-offset:-2px}
.rail-item__name{font-size:17px;line-height:1.29412;color:var(--ink)}
/* Current page is a STATE, not a link, so it stays neutral and says "you are
   here" with weight of colour rather than hue: --ink, 13.62:1. Hover is the
   link-like one and takes --orange, 5.42:1. The two rules are the same
   specificity (0,3,0), so hover must stay BELOW to win — which is also what
   the reader expects, since pointing at the current item should still react. */
.rail-item[aria-current="page"] .rail-item__name{color:var(--ink)}
@media(hover:hover) and (pointer:fine){
  .rail-item:hover .rail-item__name{color:var(--orange)}
}
/* the first rail, for the styleguide: an image slot and a price under the
   name. The generated pages carry neither — the thumbnail above is a device
   plate, and a rail shows no price. */
.rail-item__img{width:56px;height:56px;object-fit:contain}
.rail-item__price{font-size:var(--fs-label);color:var(--text-soft);
  font-variant-numeric:tabular-nums}

/* ---- lineup hero ---- */
.lineup-hero{padding:var(--section-y) 0 0}
.lineup-hero h1{max-width:20ch;color:var(--ink)}
.lineup-hero__lead{margin-top:var(--sp-5);max-width:var(--measure);
  font-size:var(--fs-section);line-height:1.5;color:var(--text-soft)}

/* ---- lineup cards ---- */
.lineup{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
  gap:var(--sp-4);margin-top:var(--sp-8)}
/* EXACTLY FOUR -> 2x2, never 3+1.
   auto-fit resolves to three tracks at the 980px measure, which strands the
   fourth card alone on its own row. Voice Agents is the only four-product
   category, and 2x2 also gives each card a comfortable 482px rather than the
   245px a forced 4-up would leave. The :not() bound keeps this off the
   five-product Follow-up lineup, which falls to a clean 3+2.
   Same specificity as the rule above, so it must stay BELOW it. */
.lineup:has(.lineup-card:nth-child(4)):not(:has(.lineup-card:nth-child(5))){
  grid-template-columns:repeat(2,1fr)}
@media(max-width:700px){
  .lineup,.lineup:has(.lineup-card:nth-child(4)):not(:has(.lineup-card:nth-child(5))){
    grid-template-columns:1fr}
}
.lineup-card{display:flex;flex-direction:column;background:var(--paper);
  border:1px solid var(--hairline);border-radius:var(--radius);padding:var(--sp-6);
  transition:border-color var(--dur) var(--ease)}
.lineup-card__name{font-size:var(--fs-section);color:var(--ink)}
.lineup-card__line{margin-top:var(--sp-3);color:var(--text-soft);flex:1 0 auto}
.lineup-card__cta{display:flex;flex-wrap:wrap;gap:var(--sp-3);margin-top:var(--sp-6)}
@media(hover:hover) and (pointer:fine){.lineup-card:hover{border-color:var(--text-mute)}}

/* ---- essentials ---- */
.essentials{padding:var(--section-y) 0 0}
.essentials-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));
  gap:var(--sp-7) var(--sp-6);margin-top:var(--sp-7)}
.essential__title{font-size:var(--fs-body);font-weight:500;color:var(--ink);
  padding-bottom:var(--sp-3);margin-bottom:var(--sp-3);
  border-bottom:1px solid var(--hairline)}
.essential__body{color:var(--text-soft);max-width:var(--measure)}

/* ---- comparison table ----
   Wide content scrolls inside its own container, never the page body. */
.compare{padding:var(--section-y) 0 0}
.compare-scroll{overflow-x:auto;margin-top:var(--sp-7);
  border:1px solid var(--hairline);border-radius:var(--radius)}
.compare-table{width:100%;min-width:560px;border-collapse:collapse;
  font-size:var(--fs-body)}
.compare-table th,.compare-table td{padding:var(--sp-4) var(--sp-5);text-align:left;
  border-bottom:1px solid var(--hairline);vertical-align:top}
.compare-table tr:last-child th,.compare-table tr:last-child td{border-bottom:none}
.compare-table thead th{background:var(--panel);font-weight:500;
  font-size:var(--fs-label);color:var(--text-soft);white-space:nowrap}
.compare-table tbody th{font-weight:500;color:var(--ink);white-space:nowrap}
/* The one row label that wraps: /voice-agents' text-back row, whose label
   carries the registration clause (Pricing v2). Kept on one line it took
   ~470px at 1440, crushing the plan columns, and at 390 it filled the
   screen and pushed every price out of view. Balanced, as the plan cards'
   text-back line is: pretty left "approved" alone on its last line at 390. */
.compare-table tbody th.compare-table__wrap{white-space:normal;text-wrap:balance}
.compare-table td{color:var(--text-soft);font-variant-numeric:tabular-nums}
.compare-table .is-key{color:var(--ink)}


/* ============================================================
   4. PRODUCT PAGE
   ============================================================
   Section order is LOCKED and documented in CLAUDE.md:
   hero · highlights · how it works · choose your plan · guarantee · FAQ ·
   Book a call. These classes appear in that order. */

/* ---- hero, apple.com's product-page opening ----
   The product NAME is the eyebrow and the TAGLINE is the headline, which is
   the inversion the pattern is built on: a lineup page's h1 is what the page
   is ("Voice Agents"), and a product page's headline is what the product
   DOES. The name still opens the page, one step up, in the class that was
   shipped waiting for it — see "the eyebrow" in §10.

   THEN THE CTA ROW, THEN THE PRICE, THEN THE PICTURE. The price moved up out
   of the pricing section, where it used to be three sections below the fold.

   24ch, NOT THE 20ch A LINEUP TITLE TAKES. A category tagline is 37–46
   characters and a product's is 38–82, so at 20ch the longest — missed-call
   text-back's 82 — wraps to four lines at --fs-page. 24ch brings it to three
   and leaves the shortest unchanged. */
.p-hero{padding:var(--section-y) 0 0}
.p-hero .eyebrow{margin-bottom:var(--sp-3)}
.p-hero__title{max-width:24ch;color:var(--ink)}
.p-hero__cta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-5);
  margin-top:var(--sp-7)}
/* The actions block keeps its own class and its single button: tools/audit.js
   check 7 reads <div class="p-hero__actions"> and counts the anchors inside
   it, and one hero CTA is Price Book v1.2 §A. The survey link sits BESIDE it,
   not inside it — in the row to the eye, a sibling to the audit. */
.p-hero__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3)}
/* a real link, so --orange — 5.42:1 on paper */
.p-hero__survey{display:inline-flex;align-items:center;min-height:44px;
  padding:13px 0;font-size:var(--fs-ui);color:var(--orange);text-decoration:none}
@media(hover:hover) and (pointer:fine){
  .p-hero__survey:hover{text-decoration:underline;text-underline-offset:3px}
  .p-hero__survey:hover .arw{transform:translateX(3px)}
}

/* ---- the hero price ----
   The amount, the launch chip, the build-status chip and the route to the
   footnote, in that order. The status chip lives here rather than above the
   headline because Price Book v1.2 §A keeps the price, the launch line and
   the chip together. */
.p-hero__price{margin-top:var(--sp-7)}
/* The amount takes a line of its own. .price__amount is an inline span and
   the launch line is an inline-block chip, so in the pricing section it is
   the block .price__setup between them that breaks the line — and the hero
   carries no setup figure, which left the chip sitting beside the amount on
   one line. */
.p-hero__price .price__amount{display:block}
.p-hero__price .price__launch{margin-top:var(--sp-4)}
.p-hero__status{margin-top:var(--sp-4)}
/* The setup-fee footnote is required beside every price and is printed once,
   in the pricing section; this is the route to it rather than a second copy
   of the sentence. A link, so --orange, 5.42:1 on paper, on the 44px floor
   every other inline link holds. */
.p-hero__price .price__more{margin-top:var(--sp-4);font-size:var(--fs-ui)}
.p-hero__price .price__more a{display:inline-flex;align-items:center;
  min-height:44px;color:var(--orange);text-decoration:none}
@media(hover:hover) and (pointer:fine){
  .p-hero__price .price__more a:hover{text-decoration:underline;
    text-underline-offset:3px}
  .p-hero__price .price__more a:hover .arw{transform:translateY(2px)}
}
/* A use case's route to its plans. On the four use-case pages the pricing
   section holds no amount — the note says which of the AI receptionist's
   plans the use case runs on — and this link goes to them, on the
   receptionist's page. The hero's link restated for the plans section: a
   real link, so --orange, 5.42:1 on paper, on the 44px floor. Its arrow
   points right and moves right, as .p-hero__survey's does, because it
   leaves the page rather than dropping down it. */
.plans .price__more{margin-top:var(--sp-4);font-size:var(--fs-ui)}
.plans .price__more a{display:inline-flex;align-items:center;
  min-height:44px;color:var(--orange);text-decoration:none}
@media(hover:hover) and (pointer:fine){
  .plans .price__more a:hover{text-decoration:underline;
    text-underline-offset:3px}
  .plans .price__more a:hover .arw{transform:translateX(3px)}
}

/* ---- the hero picture ----
   The category's plate, whole and uncropped, on the seven pages whose screen
   genuinely draws that product — tools/gen-pages.js, HERO_DEVICE. The ten
   without one end at the price line.

   NOT .tile-devices. The overflow harness's device probe walks
   .tile-devices and calls closest(".tile") on each one, so reusing that class
   here would throw rather than measure. The sizing below is the only
   homepage-tile machinery this needed: .device, its .screen overlay and the
   .scr size container are all keyed on .device and carry over untouched.

   EVERY SIZE IS AT OR ABOVE THE HOMEPAGE'S, MEASURED. A screen's type is
   1cqi of the screen's own width, so a device that is never smaller than the
   homepage's can never print smaller type than the homepage's measured worst
   case — which is the only reason the 8px floor carries over to a page the
   floor was not measured on.

   THE LAPTOP TAKES THE WHOLE COLUMN, not 88% of it. A tile is full-bleed and
   a hero sits inside the 980px column, so the tile's own 88% left the hero
   laptop ~14px NARROWER than the homepage's at every phone width, and its
   screen type 0.3px smaller with it — 5.3px against 5.6px at a 320 layout.
   Measured 23 September 2026: at 100% it is 280 / 320 / 335 / 340 against the
   tile's 260.5 / 295.7 / 308.9 / 322.1 at 320, 360, 375 and 390.

   THE PHONE IS CUT, AS A TILE CUTS IT. A phone's screen content fills the top
   of the display and the rest of the display is empty, so a whole 660px phone
   is half white. The homepage never shows that: the tile's fixed height cuts
   the phone and the caption band paints over what is left. Here the crop box
   does it and the caption sits directly under the cut.

   THE WIDTH IS ON THE CROP BOX, NOT THE DEVICE, and the device fills it. The
   cut cannot be made by shortening .device itself: .device .screen is placed
   in PERCENT of .device, so a shorter device would move and squash the screen
   instead of cropping it. The device keeps its whole box; only the wrapper
   clips.

   775 / 960 IS THE CUT: the plate is 775 x 1600, so a box 960 plate-pixels
   tall shows 60% of the phone. The top of the 55-60% band the brief asked
   for, and it is the LONGEST screen that sets it, not the average. Measured
   23 September 2026, as a fraction of each display:

     receptionist, the call        content fills 39.8%
     repeat-visit, the thread      content fills 46.6%
     missed-call, the thread       content fills 57.0%   <- the binding one

   At 57.5% the missed-call thread cleared the cut by 0.9 percentage points,
   about 5px at 1440 — it passed, and a font fallback or one more word would
   have put the last bubble through the crop line. 60% leaves 3 points, ~20px
   at 1440 and ~16px at 390.

   The harness measures the fraction, and passes the crop line to the screen
   probe as the line no text may cross — the same test a tile's caption band
   gets. So if a screen ever grows past the cut, it fails there rather than
   shipping a sentence chopped in half. */
.p-hero__media{margin:var(--sp-8) 0 0;display:flex;flex-direction:column;
  align-items:center;gap:var(--sp-2)}
/* min-height:0 is what makes the crop possible at all. The plate is a flex
   item in a column, where min-height:auto resolves to the CONTENT's
   min-content height — the whole 660px phone — so aspect-ratio was computing
   the right box and the automatic minimum was immediately overriding it. The
   harness measured the phone showing 100% of itself and failed, which is what
   it is for. */
.p-hero__plate{position:relative;flex:none;min-height:0}
.p-hero__media .device{width:100%}
.p-hero__media--phone .p-hero__plate{width:320px;aspect-ratio:775/960;
  overflow:clip}
/* THE LAPTOP IS NOT CUT, and does not want to be. Measured the same way, its
   screens fill 76% (the quote), 79.7% (the intake), 98.3% (the site chat)
   and 100% (Build's photograph) of their display — there is no empty half to
   remove. A laptop plate also carries its base below the display, so a crop
   would cut through the machine rather than through blank screen. */
.p-hero__media--laptop .p-hero__plate{width:min(760px,100%)}
/* --text-soft, not --text-mute: this is small text and --text-mute is 3.47:1.
   6.94:1 on paper. */
.p-hero__caption{font-size:var(--fs-label);line-height:1.4;color:var(--text-soft);
  text-align:center}
/* Percentages of the container, never vw: vw includes the scrollbar, so a
   vw-sized device is a different size on Windows than it is anywhere else
   and the harness cannot state one expected width. */
@media(max-width:760px){
  .p-hero__media--phone .p-hero__plate{width:min(260px,80%)}
  .p-hero__media--laptop .p-hero__plate{width:min(340px,100%)}
}

/* ---- the tile's own drawing on the hero — v2.4b phase 2 ----
   Missed-call text-back and repeat-visit reminders draw what their homepage
   tiles draw: the business's messaging app ("the tiles' own screens",
   below) and, beside the phone, the tile's banner — the customer's reply
   arriving on the business's phone. tools/gen-pages.js marks the figure
   .p-hero__media--app, and every rule here is scoped by it or by
   .p-hero__float, so no tile and no other hero moves. Nothing animates: a
   product page loads no js/screens.js, so the banner is simply there.

   THE CROP BOX IS THE WINDOW. On a tile the input bar finds the foot of the
   part of the phone it shows through the phone's wrapper, a size container
   whose height it reads as cqh. The hero's crop box has a definite size —
   its width and the 775/960 cut — so here it is that container, and the bar
   sits 4px over the crop line instead of over the caption band: 100cqh is
   the crop box, 5.4363cqi how far the screen starts below the phone's top,
   8.6em the bar. The em value before it is for an engine without container
   units: the bar where the 60% cut puts it. Size containment asks nothing of
   the box's content, and a phone's crop box takes nothing from it; the
   laptop's takes its height from its plate, which is why this is the app's
   figure only. */
.p-hero__media--app{position:relative}
.p-hero__media--app .p-hero__plate{container-type:size}
.p-hero__media--app .scr-th-input{top:122em;top:calc(100cqh - 4px - 5.4363cqi - 8.6em)}

/* THE BANNER BESIDE THE PHONE, over 900px, as on the tile: 16px outside the
   320px phone's frame — 176px from the column's centre line, which is the
   phone's — up to 300px wide and never past the column (246px at 901, where
   the reply still takes two lines), and 120px down the
   phone, the tile's 112 on its 300px one. On --paper the card is --panel
   with a --hairline edge: the tile's paper card on its panel ground,
   inverted, as a card on a .bleed band is. Ink on it is 12.48:1,
   --text-soft 6.36:1. */
.p-hero__float{position:absolute;z-index:1;top:120px;width:min(300px, calc(50% - 176px))}
.p-hero__float--right{left:calc(50% + 176px)}
.p-hero__float--left{right:calc(50% + 176px)}
.p-hero__float .scr--banner{background:var(--panel);border:1px solid var(--hairline);color:var(--ink)}

/* AT 900px AND UNDER THERE IS NO ROOM BESIDE IT. The homepage steps its
   200px phone aside so the banner can lap the frame's side, but a tile is
   full bleed and the hero sits in the column: at 390 a 260px phone in a
   350px column could step 45px, leaving the banner 104px; at 768 the 320px
   phone leaves 180px, where the sender wraps and the reply is cut after
   "Can". So the banner comes down OVER THE PHONE'S TOP, as a notification
   arrives: in flow, ahead of the crop box, lapping the frame's top edge by
   10px — the bezel, never a line of the screen, which starts 12.7px down a
   260px phone and 15.7px down a 320px one — and moved toward the side it
   takes beside the phone, so that it runs 30px past the frame, but never
   past the column. 240px wide, so the reply takes two lines and is not
   cut. The type steps down with the tile's at 760px. */
@media(max-width:900px){
  .p-hero__float{position:relative;top:auto;order:-1;width:min(240px,100%);
    margin-bottom:calc(-10px - var(--sp-2))}
  .p-hero__float--right{left:clamp(0px, calc(50% - 120px), 70px)}
  .p-hero__float--left{right:clamp(0px, calc(50% - 120px), 70px)}
}
@media(max-width:760px){
  .p-hero__float--right{left:clamp(0px, calc(50% - 120px), 40px)}
  .p-hero__float--left{right:clamp(0px, calc(50% - 120px), 40px)}
  .p-hero__float .scr-bn-icon{width:17px;height:17px;border-radius:4px}
  .p-hero__float .scr-bn-icon svg{width:10px;height:10px}
}

/* ---- the tile's call on the AI receptionist's hero — v2.4b phase 3 ----
   The AI receptionist's hero draws what the homepage Voice tile draws: the
   phone's own in-call screen ("the tiles' own screens", above) on the plain
   phone plate — the hand, cut at 60%, would lose the palm — and beside it
   the tile's column: the email banner, the live captions and "Play a
   sample call". tools/gen-pages.js marks the figure .p-hero__media--call
   as well as --app, and every rule here is scoped by it or by
   .p-hero__float, so no tile and no other hero moves. The call plays on
   its button as the tile's does: js/voice-play.js, and the motion rules in
   "the screens move", which name both players.

   THE CALL IN THE TILE'S PHONE SIZES. The crop shows 131.9em of the screen
   at every width — the screen's own em, 1% of its width — and the Voice
   tile on a phone shows 131.8em at a true 390 layout. So the hero draws the
   call in the values the tile uses on a phone (the 760px block of "the
   tiles' own screens"), restated here so they hold at every width. The
   tile's desktop values need about 151em, and would run under the crop.

   THE PANEL AT THE FOOT OF THE WINDOW, as the tile's stands at the foot of
   its own on desktop. The call's box ends 8px over the crop line — the crop
   box is a size container (the app's figure, phase 2), so 100cqh is its
   height, and 5.4363cqi is where the screen starts below the phone's top —
   and the panel sits at that box's foot. The em height before it is for an
   engine without container units. The phone's own screen takes the
   ground's foot colour, so no white shows at its rounded edge: the device's
   screen only, since the cards beside it are screens too. */
.p-hero__media--call .device .screen{background:#0B0C0F}
.p-hero__media--call .scr--call{bottom:auto;height:129em;height:calc(100cqh - 5.4363cqi - 8px);
  padding:12.5em 5em 0;align-items:center;text-align:center;color:#FFFFFF;
  background:linear-gradient(180deg,var(--screen-call-top) 0%,var(--screen-call-mid) 48%,var(--screen-call-foot) 100%)}
.p-hero__media--call .scr-av{width:10.5em;height:10.5em}
.p-hero__media--call .scr-av span{font-size:4.9em}
.p-hero__media--call .scr-head{margin-top:.45em;font-size:5.6em;line-height:1.2;font-weight:500;color:rgba(255,255,255,.9)}
.p-hero__media--call .scr-num{margin-top:.3em;font-size:4.45em;line-height:1.3;font-weight:400;color:rgba(255,255,255,.62)}
.p-hero__media--call .scr-status{margin-top:.5em}
.p-hero__media--call .scr-timer{font-size:2em}
.p-hero__media--call .scr-panel{margin-top:auto;padding:3em 4em;border-radius:7em}
.p-hero__media--call .scr-btn,.p-hero__media--call .scr-end-btn{width:13em;height:13em}
.p-hero__media--call .scr-end{margin-top:3em}

/* THE COLUMN BESIDE THE PHONE, over 900px — the tile's: the email in its
   slot, the live captions under it and the play button under them,
   right-aligned. It stands on the tile's side 16px from the frame, as
   phase 2's banners do (a plain phone has no fingertips to stand clear
   of), and runs from the phone's top to 8px over the crop line; the
   captions' window gives way to keep it there, as on the tile, the older
   lines leaving through its fade. 320 x 960 / 775 is the crop box's height
   over 760px. */
@media(min-width:901px){
  .p-hero__media--call .p-hero__float{top:0;height:calc(320px * 960 / 775 - 8px);
    display:flex;flex-direction:column;align-items:flex-end;gap:16px}
}

/* THE LIVE CAPTIONS ON THE WHITE PAGE: the banner's card — --panel with a
   --hairline edge, the tile's paper card on its panel ground inside out, as
   phase 2 set it. Ink words, 12.48:1 on --panel; the title and the
   speakers --text-soft, 6.36:1; the live dot the phone's own green,
   decoration. The word being said takes the tile's thin stroke in ink —
   weight without width — and settles to plain ink: .is-now, in "the real
   call plays". */
.p-hero__float .scr--captions{flex:0 1 auto;min-height:0;display:flex;flex-direction:column;
  padding:14px 16px 15px;border-radius:18px;background:var(--panel);border:1px solid var(--hairline);
  --say-fs:15px;--who-fs:12px}
.p-hero__float .scr-cc-title,.p-hero__float .scr-cc-who{color:var(--text-soft)}
.p-hero__float .scr-cc-say,.p-hero__float .scr-cc-say .scr-cc-w{color:var(--ink)}
/* The email's second line leaves no word alone, as its title does (the
   owner's rule for the Voice card, 1 October 2026): in the hero's 240px
   box, under 900, "jobs." would otherwise sit on a line of its own. Only
   the email has a second line; the threads' replies are .scr-bn-text. */
.p-hero__float .scr-bn-body{text-wrap:pretty}

/* THE PLAY BUTTON ON PAPER: the tile's button, in the light ground's own
   values as an outline button has them. Neutral at rest: its label and
   glyph ink (13.62:1), its ring --text-soft (6.94:1, the light twin of the
   tile's --sand-soft). Under the pointer the ring fills --orange with a
   paper glyph (5.42:1), and presses to --orange-hover (7.33:1). Its focus
   ring is the base one, --orange on paper, 5.42:1; its progress ring ink. */
.p-hero__media .tile-play{color:var(--ink)}
.p-hero__media .tile-play__track{fill:transparent;stroke:var(--text-soft)}
.p-hero__media .tile-play__prog{stroke:var(--ink)}
.p-hero__media .tile-play__glyph{fill:var(--ink)}
@media(hover:hover) and (pointer:fine){
  .p-hero__media .tile-play:hover .tile-play__track{fill:var(--orange);stroke:var(--orange)}
  .p-hero__media .tile-play:hover .tile-play__glyph{fill:var(--paper)}
}
.p-hero__media .tile-play:active .tile-play__track{fill:var(--orange-hover);stroke:var(--orange-hover)}
.p-hero__media .tile-play:active .tile-play__glyph{fill:var(--paper)}

/* AT 900px AND UNDER, THE EMAIL'S PLACE OVER THE PHONE'S TOP, phase 2's.
   The captions take its box while the call plays — reaching 44px further
   up for the seek bar at their foot, since the real call ("the screens
   move") — and rest hidden, as the tile's do on a phone; the email drops
   back in when the call ends. The email's box never changes size, so
   nothing on the page moves, and it laps only the bezel, never a line of
   the screen. The button sits on its right, absolutely placed so
   nothing moves when the script shows it: ring and label from 761px, its
   ring alone at 760 and under, where its label stays its name, as the
   tile's does under a 390px tile. The box narrows to leave the button its
   56px at 320, and its shift keeps the two inside the column. The button
   takes its own content's width: placed past its box's right edge, it
   would otherwise shrink to nothing and wrap its label. */
@media(max-width:900px){
  .p-hero__media--call .p-hero__float{width:min(240px, 100% - 56px)}
  .p-hero__media--call .float-slot{display:block;position:relative;z-index:1}
  .p-hero__media--call .float-card--captions{position:absolute;inset:0;opacity:0}
  .p-hero__media--call .tile-play{position:absolute;top:calc(50% - 22px);left:calc(100% + 12px);width:max-content}
}
@media(max-width:760px){
  .p-hero__media--call .p-hero__float--left{right:clamp(28px, calc(50% - 120px), 40px)}
  .p-hero__float .scr--captions{padding:10px 12px 11px;border-radius:14px;--say-fs:13px}
  .p-hero__media--call .tile-play{padding:0}
  .p-hero__media--call .tile-play__label{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
}

/* ---- highlights, apple's "Get the highlights" ----
   The same three cards, in a row that scrolls sideways instead of a grid that
   reflows. It is the lineup rail's construction — the scroll happens inside
   .highlights-scroll and the page body never moves sideways — with one
   difference: the row is FULL BLEED, so the section steps outside .wrap and
   carries its own inner .wrap for the heading. That is the .bleed band in
   §1, which is why the cards invert to --paper: panel cards on a panel band
   would disappear.

   THE TRACK STARTS AT THE CONTENT COLUMN. padding-inline resolves to the
   column's left margin where there is one (230px at 1440) and to the gutter
   where there is not (20px at 390), so the first card lines up with every
   other section's text at any width, and scroll-padding makes a snap land it
   there rather than against the screen edge.

   Three cards is what the copy gives — what / how / who — so at 1440 the row
   is 992px against a 980px column and barely scrolls. That is correct for
   three items and the same thing Messaging's two-item rail does. */
.highlights{padding:var(--section-y) 0 0}
/* .bleed is earlier in this file and would otherwise lose its bottom padding
   to the rule above, which sets all four. */
.highlights.bleed{padding:var(--section-y) 0}
/* THE HERO KEEPS ITS OWN WHITE BEFORE THE BAND — v2.4b phase 1, 5 October
   2026. .p-hero pads only its top and the band adds no margin, so the band
   sat flush on the hero's last box: its hairline 11px under "Pricing
   details ↓" on the ten pages with no picture, 0–4px under the laptop's
   caption on the four with one. /about met the same flush band and answered
   it with .about.bleed's margin-top; this is that answer, --section-y,
   120px at 1440 and 72 on a phone.

   A PHONE HERO TAKES IT WITH ITS NEW COMPOSITION, the tile's own drawing
   (.p-hero__media--app): missed-call text-back and repeat-visit reminders
   since v2.4b phase 2, and the AI receptionist since phase 3, so every
   product hero has it now. Keyed on what the hero holds, so no page is
   regenerated for it; a browser without :has() drops the rule and shows
   the old layout. */
.wrap:not(:has(.p-hero__media--phone:not(.p-hero__media--app))) + .highlights.bleed{margin-top:var(--section-y)}
.highlights-scroll{margin-top:var(--sp-7);overflow-x:auto;
  overscroll-behavior-x:contain;scrollbar-width:thin;
  scroll-snap-type:x mandatory;
  scroll-padding-inline-start:max(var(--gutter),(100% - var(--wrap))/2)}
.highlights-track{display:flex;align-items:stretch;gap:var(--sp-4);
  margin:0;list-style:none;
  padding-block:6px 14px;
  padding-inline:max(var(--gutter),(100% - var(--wrap))/2)}
.highlight{flex:0 0 320px;background:var(--panel);border:1px solid var(--hairline);
  border-radius:var(--radius);padding:var(--sp-5);scroll-snap-align:start;
  transition:border-color var(--dur) var(--ease)}
.bleed .highlight{background:var(--paper)}
.highlight__title{font-size:var(--fs-body);font-weight:500;color:var(--ink);
  margin-bottom:var(--sp-2)}
.highlight__body{color:var(--text-soft)}
/* A scroller clips whatever its children paint outside it, so the ring is
   inset — the same reason .rail-item's is. */
.highlight:focus-visible{outline-offset:-2px}
@media(hover:hover) and (pointer:fine){.highlight:hover{border-color:var(--text-mute)}}

/* ---- how it works ----
   A vertical numbered flow, not the three-up .steps grid: these run to six
   items and read as a sequence, so the rule down the left side does more
   work than three boxes would. */
.flow{padding:var(--section-y) 0 0}
/* margin-left is 13px because the numerals STRADDLE this border: each circle
   is 26px, positioned at left:0 and pulled back by translateX(-50%), so it
   paints 13px to the left of the list's content edge. With the list flush to
   the column, that put the circles 12px OUTSIDE it at every width — 8px from
   the screen edge at 390, inside a 20px gutter. Indenting the list by half a
   circle moves the rule and the numerals together, so the straddle is intact
   and the circle's left edge lands on the column instead of past it.
   Not a media query: the breach is the same 12px at every width, and below
   about 1036px — not just at phone widths — that 12px is gutter. */
.flow-list{list-style:none;margin:var(--sp-7) 0 0 13px;padding:0;
  border-left:1px solid var(--hairline)}
.flow-step{position:relative;padding:0 0 var(--sp-6) var(--sp-6)}
.flow-step:last-child{padding-bottom:0}
/* The numerals are metadata, not links: --text-soft, 6.94:1 on paper, which
   clears AA at 12px. Six orange numerals down the left of a page would be six
   of the "one thing in an eyeful" the brand allows. */
.flow-step__num{position:absolute;left:0;top:0;transform:translateX(-50%);
  width:26px;height:26px;display:flex;align-items:center;justify-content:center;
  border-radius:50%;background:var(--paper);border:1px solid var(--text-mute);
  font-size:var(--fs-label);font-variant-numeric:tabular-nums;color:var(--text-soft)}
.flow-step__body{padding-top:3px;color:var(--text-soft);max-width:var(--measure)}

/* ---- choose your plan ----
   The cards ARE the picker: each is a <label> wrapping a visually-hidden
   radio, so the whole card is the hit target and the control keeps native
   keyboard and screen-reader behaviour. :has() is already a dependency of
   this file (see .agents:has(...)). `.is-selected` is the JS-set fallback. */
.plans{padding:var(--section-y) 0 0}
.plans-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(215px,1fr));
  gap:var(--sp-4);margin-top:var(--sp-7)}
.plan-card{position:relative;display:flex;flex-direction:column;
  background:var(--paper);border:1px solid var(--hairline);border-radius:var(--radius);
  padding:var(--sp-5);cursor:pointer;
  transition:border-color var(--dur) var(--ease),background var(--dur) var(--ease)}
.plan-card__input{position:absolute;opacity:0;width:1px;height:1px;
  margin:0;pointer-events:none}
.plan-card__name{font-size:var(--fs-section);color:var(--ink)}
.plan-card__minutes{margin-top:var(--sp-2);font-size:var(--fs-label);
  color:var(--text-soft);font-variant-numeric:tabular-nums}
.plan-card__does{margin-top:var(--sp-3);color:var(--text-soft);flex:1 0 auto}
/* MISSED-CALL TEXT-BACK, INCLUDED (Pricing v2, 14 October 2026): its own
   line inside .plan-card__does, so it sits under what the plan does and the
   card's foot stays level. BALANCE, NOT PRETTY, here alone: pretty, which
   .plan-card__does carries, left "approved." alone in the 761–768px band,
   and balance leaves no word alone at any width the harness measures — at
   1440 it keeps pretty's three lines. The line is new, so no approved break
   moves; everywhere else the rule stays pretty (v2.4b phase 4). */
.plan-card__incl{display:block;margin-top:var(--sp-2);text-wrap:balance}
.plan-card__foot{display:block;margin-top:var(--sp-5);padding-top:var(--sp-4);
  border-top:1px solid var(--hairline)}
/* Each plan carries its own CTA, because the Buy gate is per plan: Book and
   Book Max show Contact us even on the built receptionist. Full width so the
   cards' buttons line up across the row regardless of copy length. */
.plan-card__cta{display:flex;width:100%;margin-top:var(--sp-4)}
/* The plan's own build status, beside its price — the hero keeps the
   product's chip beside its price for the same reason (Price Book v1.2 §A).
   The product's chip cannot speak for all four plans: capture is built and
   booking is in build. The same quiet .status chip, on its own line. */
.plan-card__status{display:block;margin-top:var(--sp-3)}
/* selected: a heavier edge and the panel tint. Two cues, not colour alone.
   Only once a visitor chooses: no plan is checked at rest until checkout is
   live (docs/DECISIONS.md, "Brand — v2.2"). */
.plan-card:has(.plan-card__input:checked),
.plan-card.is-selected{border-color:var(--ink);background:var(--panel)}
/* The card's own ring, because the real radio is visually hidden. --orange,
   5.42:1 on paper — the light-ground stop, and the same ring the rest of the
   page gets from :focus-visible once that moves in the next phase. */
.plan-card:has(.plan-card__input:focus-visible){outline:2px solid var(--orange);
  outline-offset:3px}
@media(hover:hover) and (pointer:fine){
  .plan-card:hover{border-color:var(--text-mute)}
}
/* The price-less enterprise tier that closes the voice plan row.
   Full width, on its own row. Receptionist offers four plans and the grid
   resolves to four tracks, so a fifth equal card would strand alone; and the
   other voice jobs offer two, where a third equal card is just as odd. It is
   also not the same KIND of thing as a priced plan — spanning the row says
   "or, above these" instead of pretending to be a fifth option. */
.plan-card--contact{grid-column:1 / -1}
.plan-card--contact .plan-card__does{flex:0 1 auto;max-width:var(--measure)}
.plan-card--contact .plan-card__foot{color:var(--text-soft)}

/* ---- guarantee ---- */
.guarantee{margin-top:var(--section-y);padding-top:var(--sp-7);
  border-top:1px solid var(--hairline)}
.guarantee__title{font-size:var(--fs-section);color:var(--ink)}
.guarantee__lead{margin-top:var(--sp-3);max-width:var(--measure);color:var(--text-soft)}
.guarantee__list{list-style:none;margin:var(--sp-5) 0 0;padding:0;
  display:grid;gap:var(--sp-3);max-width:var(--measure)}
.guarantee__list li{position:relative;padding-left:var(--sp-5);color:var(--text-soft)}
/* a hairline tick, drawn in CSS so no icon font or asset is needed.
   --text-soft, 6.94:1 on paper: a list of ticks is punctuation, not a set of
   accents, and there are four of them in one eyeful. */
.guarantee__list li::before{content:"";position:absolute;left:0;top:8px;
  width:8px;height:5px;border-left:1.5px solid var(--text-soft);
  border-bottom:1.5px solid var(--text-soft);transform:rotate(-45deg)}
/* a real link, so --orange */
.guarantee__link{display:inline-flex;align-items:center;min-height:44px;
  padding:13px 0;font-size:var(--fs-ui);color:var(--orange)}

/* ---- FAQ, stacked ----
   The homepage FAQ is a 4fr/8fr split with a sticky aside. A product page
   FAQ has no aside, so this variant drops the grid. Every other .faq-*
   rule above is reused unchanged, including the open/close mechanic. */
.faq--stack .faq-split{grid-template-columns:1fr;gap:var(--sp-6)}
.faq--stack .faq-aside{position:static}
/* ONE RULE ABOVE THE CLOSING BAND, NOT TWO. .faq-row:last-child draws a
   border-bottom and .cta-band draws a border-top, and on a product page the
   band follows the FAQ directly — so the page ended with two hairlines
   separated by the band's margin-top, 72px of white at phone widths and
   120px at 1440.

   The FAQ's line is the one to drop. The band's top border is the only rule
   closing the section above it on a lineup page (it follows .compare) and on
   home-preview (it follows the tiles), so removing that would fix 17 pages
   by breaking 6. Scoped to .faq--stack because index.html has seven
   .faq-row elements and no .faq--stack: this selector cannot reach the live
   homepage, whose own last row doubles the same way against .site-footer and
   is deliberately left alone. (0,3,0) beats the (0,2,0) rule above, so no
   !important; and border-bottom:none survives increased contrast, where
   line 788 restates only border-color. */
.faq--stack .faq-row:last-child{border-bottom:none}

/* The generated pages' rows are native <details>, so they open and shut
   with no script — js/main.js, which drives the homepage's <button> rows,
   is not loaded there. The row keeps the .faq-* look: the summary takes
   .faq-toggle's layout, the browser's disclosure marker goes, and the +
   turns to × off [open] as it did off .open. The browser shows and hides
   the answer itself, so the panel is simply always its full height. The
   selectors name the element, so the homepage's <div> rows cannot match.

   line-height:normal is what the old <button> had — a button does not
   inherit the body's 1.65 — and without it every question line is 10px
   taller than it was. */
details.faq-row > summary{list-style:none;line-height:normal}
details.faq-row > summary::-webkit-details-marker{display:none}
details.faq-row[open] .faq-icon{transform:rotate(45deg)}
details.faq-row .faq-panel{grid-template-rows:1fr}


/* ============================================================
   5. PRICE DISPLAY
   ============================================================
   The amount, the setup line, and the note that qualifies it.

   THE NOTE IS LOAD-BEARING. Three Follow-up products display $999 setup —
   what a first-time buyer actually pays — and the $299 figure applies only
   if another follow-up agent is already on the account. That qualifier is
   not a footnote: detached from the price it reads as fine print, and the
   displayed number becomes misleading. So it is bound to the amount by a
   2px rule and sits immediately beneath, inside the same block.

   THE RULE IS --ink, NOT ORANGE. Its job is to tie the note to the number
   above it, which is a structural job and not an accent — and the page it
   sits on already spends its orange on the launch chip and the links. Under
   v2.0 this was green.

   Never render .price__amount for a shared-connection product without
   .price__note. PRICING.connectionNoteFor() returns the text. */
.price{margin-top:var(--sp-5)}
.price__amount{font-size:var(--fs-section);color:var(--ink);
  font-variant-numeric:tabular-nums;line-height:1.2}
.price__period{color:var(--text-soft)}
.price__from{font-size:var(--fs-label);color:var(--text-soft);
  display:block;margin-bottom:2px}
.price__setup{margin-top:var(--sp-2);font-size:var(--fs-label);color:var(--text-soft);
  font-variant-numeric:tabular-nums}
.price__note{margin-top:var(--sp-3);padding-left:10px;
  border-left:2px solid var(--ink);
  font-size:var(--fs-label);line-height:1.5;color:var(--text-soft);
  max-width:38ch}
/* THE LAUNCH LINE — "Launch price. Confirmed when it goes live."
   Price Book v1.2 §A: every price on the site is a launch price until the
   product goes live, and it is never shown without this sentence. It sits
   with the amount, not under the section, for the same reason .price__note
   does: a qualifier that floats away from the number it qualifies stops
   qualifying it. Quieter than .price__setup and carrying no rule of its own,
   so the ink bar stays reserved for the $299 connection note.
   (This line becomes the orange chip in a later phase — one per page, and the
   only orange fill on a light ground.) */
.price__launch{margin-top:var(--sp-3);font-size:var(--fs-label);
  color:var(--text-soft);max-width:38ch}

/* the required footnote under a voice price — a note on the section, not on
   one card, so it is quieter and detached rather than bound */
.price__foot{margin-top:var(--sp-5);font-size:var(--fs-label);color:var(--text-soft);
  max-width:var(--measure)}
/* no published price */
.price--contact .price__amount{font-size:var(--fs-body);color:var(--text-soft)}


/* ============================================================
   6. CUSTOM AGENTS — INTAKE
   ============================================================
   Replaces "choose your plan" in the locked section order. The textarea is
   the whole control, sized large enough to invite a real description rather
   than a sentence.

   BUILT TO BE REPLACED. The AI intake will swap the <textarea> for a chat
   surface. .intake__field sets only the box — size, border, radius, padding —
   so the replacement inherits the same footprint and nothing around it
   moves. Do not attach layout to the textarea element itself. */
.intake{padding:var(--section-y) 0 0}
.intake__form{margin-top:var(--sp-7);max-width:640px}
.intake__label{display:block;font-size:var(--fs-label);color:var(--text-soft);
  margin-bottom:var(--sp-2)}
.intake__field{display:block;width:100%;min-height:200px;resize:vertical;
  background:var(--panel);border:1px solid var(--text-mute);border-radius:var(--radius);
  padding:14px;color:var(--ink);font-family:var(--sans);font-size:var(--fs-body);
  line-height:var(--lh-body)}
.intake__field::placeholder{color:var(--text-soft);opacity:1}
/* The border IS the focus indicator here, because this rule kills the outline:
   --text-mute (3.47:1) goes to --ink (13.62:1), with an ink halo at 12% behind
   it — the same pair .field input.invalid uses. Neutral, not orange: a focus
   state is not a link, and the :focus-visible ring is the orange affordance.
   The old rgba(36,72,59,.16) was the v2.0 green written out by hand, which is
   why it would not have followed a token change. */
.intake__field:focus{outline:none;border-color:var(--ink);
  box-shadow:0 0 0 3px rgba(46,46,43,.12)}
.intake__help{margin-top:var(--sp-3);font-size:var(--fs-label);color:var(--text-soft)}
.intake__actions{display:flex;flex-wrap:wrap;gap:var(--sp-3);margin-top:var(--sp-5)}
/* ---- success state: the same swap the lead form uses ----
   js/intake.js only adds .sent to #customForm; this direct-child rule does
   the whole swap, exactly as .lead.sent does for #leadForm. THE SAME CAVEAT
   APPLIES: wrap the fields in a layout div and they stop being direct
   children, and the swap fails silently. The .lead rules above are not
   touched and not reused — the two forms share the .lead-success markup and
   nothing else, so neither can break the other. */
.intake__form.sent > :not(.lead-success){display:none}
.intake__form.sent .lead-success{display:block}
/* ---- the same swap without script: Web3Forms redirects to #cf-sent ----
   With JS off the form posts to Web3Forms, which sends the visitor back to
   /custom-agents#cf-sent. #cf-sent is an empty span and the form's FIRST
   child, so `~` reaches every field after it: the same direct-child swap as
   .sent, driven by :target. Empty and display:block, it adds no height; the
   scroll margin is form[id]'s, so the thanks lands below the sticky nav.
   tools/gen-pages.js intakeSection() writes the span and audit.js check 31
   holds both rules. */
.intake__form > #cf-sent{display:block;scroll-margin-top:calc(var(--nav-h) + var(--sp-5))}
#cf-sent:target ~ :not(.lead-success){display:none}
#cf-sent:target ~ .lead-success{display:block}


/* ============================================================
   7. SEARCH OVERLAY
   ============================================================
   Drops from the nav like the mega panel and reuses its collapse, so the
   two never look like different mechanisms. */
.search-overlay{position:fixed;top:var(--nav-h);left:0;right:0;bottom:0;
  z-index:calc(var(--z-nav) + 1);background:rgba(46,46,43,.28);
  opacity:0;visibility:hidden;
  transition:opacity var(--dur-panel) var(--ease),visibility 0s linear var(--dur-panel)}
.search-overlay.is-open{opacity:1;visibility:visible;transition-delay:0s}
.search-panel{background:var(--paper);border-bottom:1px solid var(--hairline);
  padding:var(--sp-6) 0 var(--sp-7);max-height:100%;overflow-y:auto}

.search-field{display:flex;align-items:center;gap:var(--sp-3);
  border-bottom:1px solid var(--text-mute);padding-bottom:var(--sp-3)}
.search-field svg{width:18px;height:18px;flex:none;color:var(--text-soft)}
.search-input{flex:1;min-height:44px;background:transparent;border:none;
  color:var(--ink);font-family:var(--sans);font-size:var(--fs-section);
  letter-spacing:var(--track-section)}
.search-input::placeholder{color:var(--text-soft);opacity:1}
.search-input:focus{outline:none}
.search-field:focus-within{border-bottom-color:var(--ink)}
.search-clear{display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;flex:none;padding:0;background:transparent;border:none;
  border-radius:var(--radius);color:var(--text-soft);cursor:pointer;font-size:18px}
@media(hover:hover) and (pointer:fine){
  .search-clear:hover{color:var(--ink);background:var(--panel)}
}

.search-results{margin-top:var(--sp-6)}
.search-group + .search-group{margin-top:var(--sp-6)}
.search-group__title{font-size:var(--fs-label);color:var(--text-soft);
  padding-bottom:var(--sp-2);border-bottom:1px solid var(--hairline);
  margin-bottom:var(--sp-2)}
.search-result{display:flex;flex-direction:column;gap:2px;justify-content:center;
  min-height:44px;padding:var(--sp-2) var(--sp-3);margin:0 calc(var(--sp-3) * -1);
  border-radius:var(--radius);text-decoration:none;color:inherit}
.search-result:hover{text-decoration:none}
.search-result__name{font-size:var(--fs-body);color:var(--ink)}
.search-result__meta{font-size:var(--fs-label);color:var(--text-soft)}
/* keyboard cursor. Background + an ink edge, so the active row is not
   signalled by colour alone — and so the keyboard cursor and the mouse hover,
   which share the --panel fill, stay tellable apart. Neutral rather than
   orange: this is a cursor, and a results list would otherwise put an orange
   mark in the same eyeful as every result's own text. */
.search-result.is-active,
.search-result:hover{background:var(--panel)}
.search-result.is-active{box-shadow:inset 2px 0 0 var(--ink)}
.search-empty{padding:var(--sp-6) 0;color:var(--text-soft)}

/* ---- the /search page ----
   Everything above is the overlay that drops out of the nav in Batch 9.
   These rules are the standalone page at /search, which reuses every
   component above from .search-field down and needs only its own section
   rhythm, a field that does not run the full 980px measure, a visible
   label — the overlay has none, because a field inside a panel that opened
   on a search button does not need one, and a field on a page does — and
   its lead.

   ADDITIVE, and it cannot touch anything that already ships: no selector
   above is redefined, .search-page, .search-label and .search-lead are new
   class names, and the scoped rule cannot reach the overlay's own
   .search-field.

   BOTH ENDS since v2.4b phase 1. The page ends on this section, with no
   closing band, and the mega footer has no margin of its own, so the last
   result sat 11px over the footer's panel — the 404's case, answered the
   same way (.not-found, below). */
.search-page{padding:var(--section-y) 0}
.search-label{display:block;font-size:var(--fs-label);color:var(--text-soft);
  margin-bottom:var(--sp-2)}
.search-page .search-field{max-width:var(--measure)}
/* The same declarations as .about__lead and .lineup-hero__lead — a
   page-level lead at section scale. Until v2.4b phase 1 it was
   .p-hero__line, which has no rule, so it was body text in ink flush under
   the h1. --text-soft is 6.94:1 on paper. */
.search-lead{margin-top:var(--sp-5);max-width:var(--measure);
  font-size:var(--fs-section);line-height:1.5;color:var(--text-soft)}


/* ============================================================
   8. CART AND CHECKOUT RETURN
   ============================================================
   Unreachable while js/config.js has commerce:false. Styled now so the
   switch is a one-line change and not a design exercise. */
.cart{padding:var(--section-y) 0 0}
.cart-list{list-style:none;margin:var(--sp-7) 0 0;padding:0;
  border-top:1px solid var(--hairline)}
.cart-row{display:grid;grid-template-columns:1fr auto auto;align-items:start;
  gap:var(--sp-4);padding:var(--sp-5) 0;border-bottom:1px solid var(--hairline)}
.cart-row__name{font-size:var(--fs-body);font-weight:500;color:var(--ink)}
.cart-row__meta{margin-top:var(--sp-2);font-size:var(--fs-label);color:var(--text-soft)}
.cart-row__price{text-align:right;color:var(--ink);font-variant-numeric:tabular-nums}
.cart-row__sub{display:block;margin-top:2px;font-size:var(--fs-label);
  color:var(--text-soft)}
.cart-row__remove{display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;flex:none;padding:0;background:transparent;border:none;
  border-radius:var(--radius);color:var(--text-soft);cursor:pointer;font-size:16px}
@media(hover:hover) and (pointer:fine){
  .cart-row__remove:hover{color:var(--ink);background:var(--panel)}
}

.cart-summary{margin-top:var(--sp-6);margin-left:auto;max-width:360px}
.cart-summary__line{display:flex;justify-content:space-between;gap:var(--sp-5);
  padding:var(--sp-2) 0;color:var(--text-soft);font-variant-numeric:tabular-nums}
.cart-summary__total{display:flex;justify-content:space-between;gap:var(--sp-5);
  margin-top:var(--sp-3);padding-top:var(--sp-4);
  border-top:1px solid var(--hairline);color:var(--ink);font-weight:500;
  font-variant-numeric:tabular-nums}
.cart-summary__note{margin-top:var(--sp-4);font-size:var(--fs-label);
  color:var(--text-soft)}
.cart-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--sp-3);
  margin-top:var(--sp-6)}
.cart-empty{padding:var(--sp-8) 0;color:var(--text-soft)}
@media(max-width:560px){
  .cart-row{grid-template-columns:1fr auto}
  .cart-row__remove{grid-row:1;grid-column:2}
  .cart-row__price{grid-column:1 / -1;text-align:left}
  .cart-summary{margin-left:0;max-width:none}
}

/* checkout return pages — success and cancel share one shape */
.receipt{padding:var(--section-y) 0;max-width:var(--measure);margin-inline:auto;
  text-align:center}
/* --ink on --panel, 12.48:1. Unreachable while commerce is off; neutral for
   the same reason the search cursor is — a confirmation mark is a state, and
   the page's orange belongs to its links. */
.receipt__mark{width:48px;height:48px;margin:0 auto var(--sp-5);display:flex;
  align-items:center;justify-content:center;border-radius:50%;
  background:var(--panel);border:1px solid var(--ink);color:var(--ink);
  font-size:22px}
.receipt__title{font-size:var(--fs-section);color:var(--ink)}
.receipt__body{margin-top:var(--sp-4);color:var(--text-soft)}
.receipt__actions{display:flex;flex-wrap:wrap;gap:var(--sp-3);justify-content:center;
  margin-top:var(--sp-7)}


/* ============================================================
   9. FOOTER, apple.com PATTERN — the homepage preview
   ============================================================
   A --panel ground, 12px --text-soft type, tight lines. Two columns — the
   seven categories; About, Book a call and the address — over one row with
   the copyright, then Privacy and Terms, once each. A NEW MODIFIER on
   .site-footer, so the footer every other page carries is untouched.

   TIGHT ROWS AND THE 44px RULE. The brand's 44px floor is a TOUCH target.
   Where the pointer is fine and can hover — a mouse — the links sit at
   apple's 24px pitch; on a touchscreen, or a device that does not say,
   every link keeps the full 44px. --text-soft on --panel is 6.36:1, and the
   ink column titles 12.48:1.

   The rules between the rows are ink at 15% and 30% — ink at an alpha,
   like the overlays above, not a new colour. --hairline is 1.05:1 on
   --panel and does not show. */
.site-footer--mega{margin-top:0;padding:40px 0 24px;background:var(--panel);
  border-top:none;font-size:12px;line-height:1.33337;color:var(--text-soft)}
.footer-dir{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:24px}
.footer-dir-title{margin-bottom:6px;font-weight:600;color:var(--ink)}
.footer-dir ul,.footer-legal-links{list-style:none;margin:0;padding:0}
.site-footer--mega a{display:inline-flex;align-items:center;min-height:44px;color:var(--text-soft)}
.site-footer--mega a:hover{color:var(--ink)}
@media(hover:hover) and (pointer:fine){
  .site-footer--mega a{min-height:24px}
}
.footer-legal{display:flex;flex-wrap:wrap;align-items:center;gap:0 24px;
  margin-top:24px;padding-top:12px;border-top:1px solid rgba(46,46,43,.15)}
.footer-legal-links{display:flex;flex-wrap:wrap;align-items:center}
.footer-legal-links li{display:flex;align-items:center}
.footer-legal-links li + li::before{content:"";width:1px;height:12px;margin:0 10px;
  background:rgba(46,46,43,.3)}


/* ============================================================
   NEW COMPONENTS — FOCUS
   ============================================================
   tokens.css sets a 2px --orange ring at 3px offset, and the file-wide rule
   near the top adds border-radius:2px. These restore each component's own
   corner so the ring does not clip a rounded card. */
.tile:focus-visible,.lineup-card:focus-visible,.plan-card:focus-visible,
.highlight:focus-visible,.nav-mega-link:focus-visible,.search-result:focus-visible,
.rail-item:focus-visible,.nav-icon-btn:focus-visible{border-radius:var(--radius)}


/* ============================================================
   NEW COMPONENTS — REDUCED MOTION
   ============================================================
   tokens.css kills every transition and animation with a blanket !important
   rule. These are the cases it does NOT cover: states held open by a
   transition-delay, which freeze mid-way rather than resolving. Each is
   forced to its resolved value. */
@media(prefers-reduced-motion:reduce){
  .nav-mega,.search-overlay,.site-nav--mega .nav-mobile,.nav-curtain{transition-delay:0s}
  .nav-mega.is-open,
  .search-overlay.is-open,
  .site-nav--mega.menu-open .nav-mobile,
  .site-nav--mega.mega-open:not(.mega-closing) .nav-curtain{visibility:visible}
  .nav-mega:not(.is-open),
  .search-overlay:not(.is-open),
  .site-nav--mega:not(.menu-open) .nav-mobile,
  .site-nav--mega:not(.mega-open) .nav-curtain,
  .site-nav--mega.mega-closing .nav-curtain{visibility:hidden}
}


/* ============================================================
   NEW COMPONENTS — INCREASED CONTRAST
   ============================================================
   Same policy as the block above this section: every divider and control
   edge is promoted from the decorative --hairline (1.17:1) to something
   clearly visible. --text-mute (3.47:1) is the floor for a boundary;
   --text-soft (6.94:1) where the line carries structure. */
@media(prefers-contrast:more){
  .tile,.lineup-card,.plan-card,.highlight,.compare-scroll,
  .nav-mobile-search,.intake__field{border-color:var(--text-soft)}
  .rail,.cta-band,.guarantee,.essential__title,.flow-list,
  .cart-list,.cart-row,.cart-summary__total,.search-group__title,
  .nav-acc,.plan-card__foot,.nav-mega-foot{border-color:var(--text-soft)}
  .search-panel{border-bottom-color:var(--text-soft)}
  .compare-table th,.compare-table td{border-bottom-color:var(--text-soft)}
  .status{border-color:var(--text-soft)}
  .flow-step__num{border-color:var(--text-soft)}
  .search-overlay{background:rgba(46,46,43,.5)}
  /* The mega bar, flyout and sheet lose their translucency and the labels
     go to full white — in §1, "where the material cannot be translucent",
     with the rest of that case. */
}


/* ════════════════════════════════════════════════════════════
   v2.1 — PAGE COMPONENTS FOR THE HAND-WRITTEN PAGES
   ════════════════════════════════════════════════════════════
   The legal pages (/privacy, /terms), the booking page (/book) and /about.
   These LANDED A PHASE AHEAD OF THE PAGES THAT USE THEM: when this block was
   written those pages were still on css/legacy.css and its v1 type scale, and
   nothing rendered a line of it. They converted in phase 5 and render it now.
   The split was the point — "the CSS is right" and "the pages moved" were two
   commits with a verification run between them, and the pixel gate on the
   first one proved the new rules could not reach the live homepage before a
   single page depended on them. It is the same order `.eyebrow` shipped in.

   None of these class names exists anywhere else in this file, so nothing
   here has a specificity fight to win. Until THE SWITCH phase 7 each rule
   carried the `v21` root-class prefix all the same, which kept it off the
   hand-written v2.0 homepage structurally rather than carefully — the pixel
   gate on the phase that landed this was meaningful because of it. Phase 7
   flattened them with the rest.

   THE NAMES ARE v2.1's, NOT THE LEGACY ONES. css/legacy.css — deleted at
   phase 5 — spelled these `.legal-title`, `.booking-head`; this file's
   convention for a component's parts is `.block__element` (.guarantee__lead,
   .cta-band__title, .p-hero__price). The pages were REGENERATED rather than
   edited, so their markup was new either way and there was no migration cost
   to paying the convention. On /privacy and /terms those four class names
   are the ONLY thing that changed: the legal text moved byte for byte, and
   the phase proved it with a rendered-text diff.

   WHAT CHANGED FROM THE LEGACY RULES, beyond the type scale: the green is
   gone. Legacy drew the list markers green, and built the SMS callout from a
   green tint with a green left edge. v2.1 gives green to the mark alone, so
   the markers are neutral and the callout is --panel behind an --ink edge. It is still the highest-contrast block on the page, which is
   the job: a carrier reviewer has to find STOP/HELP and the consent language
   without hunting.

   MEASURED, every text pair below:
     --text on --paper            13.62:1   body, headings
     --text on --panel            12.48:1   the callout's body
     --text-soft on --paper        6.94:1   the updated date, the closing note
     --orange on --paper           5.42:1   links, underlined
   ════════════════════════════════════════════════════════════ */

/* ---- the legal pages ----
   62ch, not --measure's 47ch. 47 is a LEAD measure — one paragraph beside
   other content, as .guarantee__lead and .cta-band__lead use it. A legal
   clause is read end to end and the document IS the page, where 60-70ch is
   the comfortable range; at 47ch these clauses would run to eight lines and
   the page would be twice as long to scroll for no gain in readability. */
.legal{max-width:62ch;margin-inline:auto;
  padding:clamp(48px,7vw,88px) 0 var(--section-y)}
.legal__title{font-size:var(--fs-page);letter-spacing:var(--track-page);
  line-height:1.13;color:var(--text)}
/* metadata, so --text-soft rather than --text: 6.94:1, and it is not the
   document. --text-mute would be 3.47:1 and is illegal for text. */
.legal__updated{margin-top:var(--sp-4);font-size:var(--fs-ui);
  color:var(--text-soft)}

/* A hairline above each clause. Legacy's reason still holds and is a good
   one: it gives a reviewer something to scan by, and a numbered clause with
   no rule above it reads as a continuation of the one before. */
.legal section{margin-top:var(--sp-8);padding-top:var(--sp-6);
  border-top:1px solid var(--hairline)}
.legal section:first-of-type{margin-top:var(--sp-7);border-top:none}
.legal h2{font-size:var(--fs-section);color:var(--text);
  margin-bottom:var(--sp-4)}
.legal p,.legal li{color:var(--text);line-height:var(--lh-body)}
.legal p+p{margin-top:var(--sp-4)}
.legal ul{margin:var(--sp-4) 0 0;padding-left:1.35em}
.legal li{margin-bottom:var(--sp-3)}
/* neutral, and --text-soft rather than --text-mute. Legacy drew these green;
   v2.1 has no green to spend. A marker is punctuation, not an accent
   — the same reasoning as .guarantee__list's tick, which takes --text-soft
   for exactly this — and it is a rendered GLYPH, so the "--text-mute is legal
   only for non-text" rule applies to it and rules --text-mute out at 3.47:1. */
.legal li::marker{color:var(--text-soft)}
/* 500, not 600. Headings on this site are 500 and "never bold"; a run of
   emphasis inside body copy is distinguished by weight AND by taking --text
   where the surrounding sentence already does, so the step is small on
   purpose. */
.legal strong{font-weight:500;color:var(--text)}
/* UNDERLINED, deliberately. Orange alone marks a link everywhere else on the
   site; in a document somebody may be reading for what it commits KRM to, a
   link must not depend on colour to be seen at all. */
.legal a{color:var(--orange);text-decoration:underline;
  text-underline-offset:2px}

/* The SMS and carrier-compliance callouts. The highest-contrast block on the
   page, built from the neutrals: --panel behind a 3px --ink edge, with --text
   on it at 12.48:1. No hue — orange is spent on links, the outline button's
   hover, the chip and the eyebrow, and a block fill is none of those. */
.legal__callout{margin:var(--sp-5) 0;padding:var(--sp-5);
  background:var(--panel);border:1px solid var(--hairline);
  border-left:3px solid var(--ink);border-radius:var(--radius)}
.legal__callout p{color:var(--text)}
.legal__callout p+p{margin-top:var(--sp-4)}

.legal__note{margin-top:var(--sp-8);padding-top:var(--sp-5);
  border-top:1px solid var(--hairline);
  font-size:var(--fs-ui);font-style:italic;line-height:1.6;
  color:var(--text-soft)}

/* ---- the booking page ----
   Centred rather than left-aligned like the editorial sections: it is a
   single-purpose conversion page with one thing on it, which is the
   .cta-band's shape, not .section-head's.

   NO EYEBROW. Legacy opened this page with a `// Book a call` mono kicker.
   `.eyebrow` is v2.1's one small orange label and it has exactly one call
   site — the product's name on the 17 product pages, once each — and is not
   to acquire another. The h1 says "Book a call"; a label above it saying the
   same thing twice was a v1 habit. */
.booking{padding:clamp(48px,7vw,88px) 0 var(--section-y)}
.booking__head{max-width:var(--measure);margin-inline:auto;
  text-align:center}
.booking__title{font-size:var(--fs-page);letter-spacing:var(--track-page);
  line-height:1.13;color:var(--text)}
.booking__lead{margin:var(--sp-4) auto 0;max-width:var(--measure-n);
  font-size:var(--fs-section);line-height:1.45;color:var(--text-soft)}

/* The mat under the third-party booker. Cal.com paints its own WHITE surface,
   so the mat is --panel: on --paper the frame would be a hairline round white
   on white and read as a seam rather than as something holding the booker.

   NO BORDER ON IT, and the styleguide is why. A --hairline edge on --panel
   measures 1.07:1 — fainter than the 1.17:1 the token is specified at on
   paper, and fainter than the 1.09:1 the panel fill already has against the
   page. The fill IS the separation here, so the border was a line nobody can
   see doing a job already done. This follows .panel in css/tokens.css, which
   is a fill and a radius and no border, against .surface, which takes a
   border precisely because its fill matches the page.

   Full column width — .wrap is 980px and the month view needs the room; below
   about 560px that padding is width the calendar needs more than the mat
   does, so it goes. */
.booking__embed{max-width:var(--wrap);margin:var(--sp-8) auto 0;
  padding:var(--sp-5);background:var(--panel);border-radius:var(--radius)}
@media(max-width:560px){
  .booking__embed{padding:var(--sp-3)}
}

/* ---- /about ----
   LANDED WITH THE PAGE, not ahead of it like the two blocks above. /about was
   the one page of the four whose body is positioning copy rather than legal
   text or a booking widget, so its conversion was gated on the owner
   approving new copy. The copy arrived at the gate; this is the CSS for it.

   SEVEN RULES, because almost nothing here is new. The hero is .p-hero, the
   label is .kicker, the h1 takes --fs-page from tokens.css on its own, every
   heading is .section-head + .section-title, the numbered steps are the
   .flow-list/.flow-step the 17 product pages use for "How it works", and the
   close is ctaBand(). What was actually missing was a page rhythm, a lead and
   a bulleted list.

   --measure (47ch), NOT .legal's 62ch. The comment on .legal above argues
   that a document read end to end wants 60-70ch; the inverse of that argument
   applies here. /about is marketing copy read section by section, with a
   heading every few lines, and it sits at the same measure as .section-lead
   and .flow-step__body — which are its actual neighbours on the page. */
.about{padding:var(--section-y) 0 0}
/* Top padding only, as .highlights, .flow, .plans and .p-hero all do, so
   consecutive sections stack on one gap instead of two. */

/* THE ONE BANDED SECTION, on exactly the construction the product pages use:
   .highlights{padding:var(--section-y) 0 0} with
   .highlights.bleed{padding:var(--section-y) 0} over it, so a section that
   normally pads only at the top pads BOTH ends once it becomes a band and its
   content sits centred in the panel rather than crowding the bottom rule.
   This is the same two-line pair, one specificity step up (0,3,0 over the
   (0,2,0) above), so it wins wherever it is written.

   It needs BOTH class names, and .about is on one page, so nothing else can
   match it. .bleed itself is untouched — the band takes --panel and two
   --hairline rules from the existing rule at :856 and introduces no colour.

   THE MARGIN IS NOT DECORATION, IT IS THE GAP THE BAND WOULD OTHERWISE EAT.
   Measured: a .bleed sits flush against the previous element's box on the
   product pages too — `prev box bottom -> band top` is 0px on both. It was
   thought to work there because .p-hero ends in a device plate; the v2.4b
   audit measured it flush there as well, and phase 1 gave that band this
   same margin (the highlights block above). .about ends in a PARAGRAPH and pads
   only at the top, so with no margin the panel's edge is shaved flush against
   the last line of "What we sell": the 120px that used to separate the two
   sections does not move into the band, it disappears, and the band gains
   120px of white below it from the next section while having none above.
   Lopsided, and it reads as a mistake rather than as a band.

   So the white either side is --section-y, symmetrically: the margin above,
   the next section's own top padding below. The band's panel padding is then
   its own internal space, centring the list, rather than doubling as the gap
   between two sections. */
.about.bleed{padding:var(--section-y) 0;margin-top:var(--section-y)}

/* ---- the heading sits CLOSE to what it introduces ----
   Measured on the built page: 64px from heading to content, against a 120px
   gap between sections at 1440. Two gaps that close to equal stop reading as
   a hierarchy — the heading floats between the section above and the one it
   belongs to, and at a glance it is not obvious which. 24px against 120px is
   unambiguous.

   .section-head's --sp-8 is right where it is used elsewhere: on a product or
   lineup page it introduces a card grid, a rail or a compare table — a block
   with its own edges and fill that needs air to sit in. Here it introduces a
   single paragraph or a short list, which is just more text, and text under a
   heading wants to be near it.

   BOTH RULES ARE NEEDED, because the gap is a COLLAPSED margin and the larger
   side wins. styles.css:92 resets every element to margin:0, so .about__body
   contributes nothing and the first rule alone sets its gap. The two lists
   bring their own margin-top: .about__list is already --sp-5 and lands on 24
   either way, but .flow-list is --sp-7 (48px) and would keep 48 while its
   neighbours moved to 24. Scoped under .about, so the .flow-list on all
   17 product pages is untouched. */
.about .section-head{margin-bottom:var(--sp-5)}
.about .flow-list{margin-top:var(--sp-5)}

/* The same declarations as .lineup-hero__lead (:2101) — a page-level lead at
   section scale. */
.about__lead{margin-top:var(--sp-5);max-width:var(--measure);
  font-size:var(--fs-section);line-height:1.5;color:var(--text-soft)}
/* No margin-top: .section-head's --sp-8 margin-bottom does the spacing, and a
   margin here would collapse against it rather than add to it. */
.about__body{max-width:var(--measure);color:var(--text);
  line-height:var(--lh-body)}

/* The four below mirror .legal ul / li / li::marker / strong at :2850-2862
   exactly, and for the same reasons stated there: a marker is punctuation
   rather than an accent, so it is --text-soft and not --orange; it is a
   rendered GLYPH, so --text-mute's 3.47:1 is illegal for it; and emphasis
   inside body copy steps by weight alone, at 500, because headings on this
   site are 500 and never bold. Kept as their own selectors rather than
   grouped with .legal's: the two lists differ in measure (47ch against 62ch)
   and in what they hold, and grouping them would mean every future change to
   one silently changing the other. */
.about__list{margin:var(--sp-5) 0 0;padding-left:1.35em;
  max-width:var(--measure)}
.about__list li{margin-bottom:var(--sp-3);color:var(--text);
  line-height:var(--lh-body)}
.about__list li::marker{color:var(--text-soft)}
.about__list strong{font-weight:500;color:var(--text)}

/* /404 — SEO phase D. The only page with nothing after its hero: every other
   page closes on a .cta-band, whose padding is what separates it from
   .site-footer--mega (margin-top:0, so the band is the whole gap). Without
   this the button row sits flush on the footer. --section-y below matches
   .p-hero's --section-y above, so the block sits centred in its white. */
.not-found{padding-bottom:var(--section-y)}


/* ════════════════════════════════════════════════════════════
   v2.1 — THE SHARED RULES
   ════════════════════════════════════════════════════════════
   v2.1's side of the components the old v2.0 homepage shared with the
   generated pages: the button presses, the FAQ row, the success mark. Each
   overrides an original further up this file.

   THEY WIN ON SOURCE ORDER, SO THEY STAY AT THE END. Until THE SWITCH phase 7
   each carried the `v21` root-class prefix, one class heavier than its
   original, and won wherever it sat. Phase 7 flattened them in place: each
   is now the SAME weight as the rule it overrides and wins only by coming
   after it. Moved above §2, a tile's outline button would lose its orange
   hover. The originals were cleaned in the same commit — their greens
   deleted, and the rules that could only ever lose removed.

   The base link and ring are not here: they are --orange in tokens.css
   itself, and any component that states its own colour beats them.
   ════════════════════════════════════════════════════════════ */

/* ---- buttons ----
   Neutral at rest — with one exception since brand v2.2, the "Learn more"
   primary, in the section "v2.2 — THE ORANGE PRIMARY" at the end of this
   file. What changes is the touch: the SOLID primary presses to --ink-press,
   a neutral one step darker than --ink (white on it 16.51:1), and the
   OUTLINE secondary fills --orange with a paper label (5.42:1), which is the
   one place v2.1 spent orange on a control. */
@media(hover:hover) and (pointer:fine){
  .btn-primary:hover{background:var(--ink-press)}
  .btn-secondary:hover{background:var(--orange);border-color:var(--orange);color:var(--paper)}
  .nav-cta:hover{background:var(--ink-press);border-color:var(--ink-press)}

  /* THE DARK TILE'S PRIMARY WAS A LIGHT FILL, so the generic press above is
     exactly wrong for it: --ink-press under the --ink label the dark tile's
     primary carries is 1.06:1, an invisible button. This keeps the light
     press it has always had, --ink on --panel, 12.48:1. At (0,3,0) it
     outranks the generic (0,2,0) wherever it sits, and it restates the dark
     tile's own hover in §2, which is the same weight and paints the same.
     Since brand v2.2 the one dark tile's primary is the orange "Learn more",
     whose own hover at the end of this file outranks this one at (0,4,0);
     this rule is dead there, and left for the dead-CSS task. */
  .tile--dark .btn-primary:hover{background:var(--panel);border-color:var(--panel);color:var(--ink)}

  /* The ink ground's stop, and the label goes INK, not white: white on
     --orange-on-ink is 2.50:1, ink on it is 5.45:1. The same (0,3,0) weight
     as the dark tile's own hover in §2, which paints paper: this wins only
     by coming after it. */
  .tile--dark .btn-secondary:hover{background:var(--orange-on-ink);border-color:var(--orange-on-ink);color:var(--ink)}
}
/* Pressed. Outside the hover guard on purpose — a touch device has no hover
   but does fire :active, and this is the only feedback it gets. */
.btn-secondary:active{background:var(--orange-hover);border-color:var(--orange-hover);color:var(--paper)}
.tile--dark .btn-secondary:active{background:var(--orange-on-ink);border-color:var(--orange-on-ink);color:var(--ink)}

/* ---- FAQ ----
   The generated pages' <details> rows. The icon is the one orange mark in
   this component; the question turns orange under the pointer because the
   whole row is the control. */
.faq-icon{color:var(--orange)}
.faq-toggle:focus-visible{outline-color:var(--orange)}
@media(hover:hover) and (pointer:fine){
  .faq-toggle:hover .faq-q{color:var(--orange)}
}

/* ---- the success mark ----
   custom-agents' intake uses .lead-success and its tick — the one thing it
   shared with the old homepage's lead form, which went at phase 6. */
.ls-check{border-color:var(--orange);color:var(--orange)}
.lead-success h3:focus-visible{outline-color:var(--orange)}

/* ---- ink-ground companion ----
   A link inside a dark tile takes the ink stop. :where() keeps it at (0,0,1),
   the weight of the base `a` in tokens.css: later, so it wins the two-stop
   swap, and weaker than anything that states its own colour. Nothing renders
   one today — the tile's buttons carry their own colour — so this is the rule
   written down where it will be needed, not a live override. */
:where(.tile--dark) a{color:var(--orange-on-ink)}


/* ════════════════════════════════════════════════════════════
   v2.1 — THE INK GROUND, THE CHIP, THE EYEBROW
   ════════════════════════════════════════════════════════════
   The components docs/brand/tokens.css introduces, adopted here rather than
   copied into css/tokens.css, so nothing above this line is redefined.

   TWO CONTRAST DEFECTS IN THE BRAND FILE ARE FIXED ON THE WAY IN. Neither is
   a judgement call; both are measured:

     C9  .btn-secondary's border ships as a pale grey, 1.42:1 on white —
         under the 3:1 WCAG 1.4.11 wants for a control's boundary. The light
         ground takes --text-mute (3.47:1), set in css/tokens.css itself since
         THE SWITCH phase 7; the ink ground needed the same treatment and is
         below.
     C10 .on-ink .btn-secondary's border ships as #6B6B65 — 2.54:1 on --ink,
         which also fails 3:1. It takes --sand-soft here instead: 5.48:1.
         --hairline-ink (#4A4A45, 1.53:1) would have been worse; that token
         is a DIVIDER and must never become a control edge.
   ════════════════════════════════════════════════════════════ */

/* ---- the ink ground ----
   A section that flips every colour to its dark-ground value. Nothing on the
   site carries .on-ink yet — the homepage's dark tile has its own system in
   §2 — so this is the component ready for the first section that needs it. */
.on-ink{background:var(--ink);color:var(--sand)}
.on-ink h1,.on-ink h2,.on-ink h3{color:var(--sand)}
.on-ink .secondary{color:var(--sand-soft)}
.on-ink a,
.on-ink .eyebrow{color:var(--orange-on-ink)}
.on-ink .surface{background:transparent;border-color:var(--hairline-ink)}
.on-ink .btn-primary{background:var(--sand);color:var(--ink)}
/* C10, fixed: --sand-soft is 5.48:1 on --ink where the brand file's #6B6B65
   is 2.54:1 and fails the 3:1 a control boundary needs. */
.on-ink .btn-secondary{color:var(--sand);border-color:var(--sand-soft)}
.on-ink :focus-visible{outline-color:var(--orange-on-ink)}
@media(hover:hover) and (pointer:fine){
  /* the label takes INK here — white on --orange-on-ink is 2.50:1 */
  .on-ink .btn-secondary:hover{background:var(--orange-on-ink);
    border-color:var(--orange-on-ink);color:var(--ink)}
  .on-ink .btn-primary:hover{background:var(--paper);color:var(--ink)}
}

/* ---- the eyebrow ----
   IN USE, ON THE 17 PRODUCT PAGES, ONE PER PAGE: the product's name above
   the headline, which is the apple.com product-page pattern this class was
   shipped waiting for. That is its only call site. Do not sprinkle it
   elsewhere — one per page at most, and it is one of the few places v2.1
   spends orange. Spacing is set with the hero, in §4.
   See docs/DECISIONS.md. */
.eyebrow{font-size:var(--fs-label);font-weight:500;color:var(--orange)}

/* ---- the chip ----
   Solid orange with ink text, 5.45:1. ONE PER PAGE AT MOST, and never a
   background for anything else. (The dark tile's "Learn more" shares its
   --orange-on-ink as a fill since brand v2.2; that is a button with its own
   rule at the end of this file, not the chip lent out.)

   Its edge is 2.50:1 on paper and 2.29:1 on panel, which is below the 3:1 a
   CONTROL's boundary needs — and that is fine, because a chip is a label and
   not a control. Its text carries the meaning and clears AA on the fill. Do
   not turn it into a button. */
.chip{display:inline-block;background:var(--orange-on-ink);color:var(--ink);
  font:500 var(--fs-label)/1.4 var(--sans);padding:5px 11px;
  border-radius:var(--radius-chip)}

/* ---- the chip's one job: the launch line ----
   "Launch price. Confirmed when it goes live." — Price Book v1.2 §A, the
   line that ships beside every price. It is the page's single most important
   caveat, it appears once on a product page, and it is the one orange fill
   on a product page's light ground. (Since brand v2.2 the homepage tiles and
   the lineup cards carry orange fills too — their "Learn more" buttons —
   but no product page does: its CTAs stay ink.)

   NOT ON A LINEUP PAGE. A lineup card carries its own launch line, so those
   pages hold three or four of them, and chipping all of them would break the
   one-per-page rule the chip is defined under. There the line stays quiet
   text, which is what it has always been — a caveat, not a control — beside
   the card's orange "Learn more". Product pages have exactly one, and that
   one becomes the chip. The homepage has none at all: docs/DECISIONS.md keeps every price
   and its launch line off it, and tools/audit.js check 11 enforces that.

   A PRODUCT PAGE NOW CARRIES THE LINE TWICE, AND ONE CHIP. The apple-pattern
   hero prints the price at the top and the pricing section prints it again
   below with its setup figure and its footnote, so the launch line has to
   travel with both — tools/audit.js check 3 requires it inside every price
   block that shows an amount. The HERO's is the chip, because that is where
   the caveat is read; the one in the pricing section goes back to the quiet
   text it has always been, exactly as a lineup card's does. Still one orange
   fill per page. */
.price__launch{display:inline-block;background:var(--orange-on-ink);
  color:var(--ink);font-weight:500;padding:5px 11px;
  border-radius:var(--radius-chip);max-width:38ch}
.lineup-card .price__launch,
.plans .price__launch{display:block;background:none;
  color:var(--text-soft);font-weight:400;padding:0;border-radius:0}

/* ---- the homepage band's second path ----
   A link under the Book-a-call button for a reader who is still deciding.
   Until brand v2.2 it was the only resting orange on the homepage; the
   seven "Learn more" buttons above it are orange at rest now, and it is the
   only resting orange LINK. The homepage carries no price and therefore no
   launch chip. --orange on --paper, 5.42:1. The 44px floor applies as it
   does to every other inline link on the site.

   `a.cta-band__link`, NOT `.cta-band__link`, AND THE `a` IS LOAD-BEARING. It
   makes the rule (0,1,1), the weight of tokens.css's `a:hover`, which it must
   beat: that rule underlines every hovered link and sits behind no hover
   guard, so on a touch screen — where the guarded rule below never applies —
   a tap would leave this link underlined. The same weight and later, so it
   wins. */
.cta-band__aside{margin-top:var(--sp-5);font-size:var(--fs-ui);color:var(--text-soft)}
a.cta-band__link{display:inline-flex;align-items:center;min-height:44px;
  color:var(--orange);text-decoration:none}
@media(hover:hover) and (pointer:fine){
  .cta-band__link:hover{text-decoration:underline;text-underline-offset:3px}
  .cta-band__link:hover .arw{transform:translateX(3px)}
}


/* ════════════════════════════════════════════════════════════
   v2.2 — THE ORANGE PRIMARY
   ════════════════════════════════════════════════════════════
   Brand v2.2, 29 September 2026 (docs/DECISIONS.md, "Brand — v2.2"). ONE
   control takes orange AT REST: "Learn more" — the first button on each of
   the seven homepage tiles, and the one button on every lineup card.
   Every other button keeps v2.1: the solid primary is ink and presses to
   --ink-press, and the outline secondary is neutral at rest and fills
   orange on hover. "Book a call", "Book a free demo" and "Contact us" are
   exactly as they were.

   THE TWO STOPS, AS EVERYWHERE, NEVER SWAPPED. The ground picks the stop,
   and the stop picks the label:
     light grounds   --orange #AD4E0C, white label 5.42:1. The fill reads
                     off --panel at 4.97:1 and off --paper at 5.42:1.
     the dark tiles  --orange-on-ink #F08A3C, INK label 5.45:1 (white on it
                     is 2.50:1). The fill reads off ink at 5.45:1, and off
                     the Custom Agents tile's green at 4.95:1.
   Pressed, the light one goes to --orange-hover #8E3F09 (white 7.33:1). A
   dark tile's goes LIGHTER, to #F5A468 (ink 6.74:1, and 6.12:1 off the
   green), because its ink label needs the fill to stay light.
   tools/overflow-check.html's two-stop probe measures every orange fill on
   a control against the ground under it.

   AT THE END, AND EVERY RULE STATES ALL THREE COLOURS. The tile's own rules
   in §2 set background, border-color and color at (0,2,0), and hover at
   (0,3,0). These match that weight and win on source order, or outrank it
   on the dark tile. A rule that left border-color out would let §2's ink or
   paper edge show round an orange pill.

   :active IS OUTSIDE THE HOVER GUARD, as the secondary's press is: a touch
   screen has no hover but does fire :active. Without the dark tile's own
   :active it would take the light-ground press, a white label on #8E3F09,
   which is the wrong stop on that ground. */
.tile-cta .btn-primary,
.lineup-card__cta .btn-primary{background:var(--orange);border-color:var(--orange);color:var(--paper)}
.tile--dark .tile-cta .btn-primary{background:var(--orange-on-ink);border-color:var(--orange-on-ink);color:var(--ink)}
@media(hover:hover) and (pointer:fine){
  .tile-cta .btn-primary:hover,
  .lineup-card__cta .btn-primary:hover{background:var(--orange-hover);border-color:var(--orange-hover);color:var(--paper)}
  .tile--dark .tile-cta .btn-primary:hover{background:#F5A468;border-color:#F5A468;color:var(--ink)}
}
.tile-cta .btn-primary:active,
.lineup-card__cta .btn-primary:active{background:var(--orange-hover);border-color:var(--orange-hover);color:var(--paper)}
.tile--dark .tile-cta .btn-primary:active{background:#F5A468;border-color:#F5A468;color:var(--ink)}
