/* ============================================================================
   CrewBase marketing site — the system
   ----------------------------------------------------------------------------
   Every rule here exists to enforce one of the eight mechanisms in
   design/bar.md, torn down from Stripe, QuickBooks AU and Supabase.

   The four that matter most, because they are what made the old build read
   as amateur:

     · Colour is an EVENT, not a wash. Max 3 coloured elements per screen.
       Links are plain. Icons are grey. Violet is the badge, not the paint.
       Violet reads LOUDER on white than it did on near-black — the budget is
       tighter here than it looks.
     · Buttons are SMALL. Never a full-width slab. One primary per screen.
       Cap is 220px above 520px wide, and 280px at or below it — the phone
       exception, settled 2026-08-16 in LD#26 and written up in
       design-system.md. Those are the only two values.
     · Things OVERLAP. At least two places per page where an element crosses
       its container's edge. This is what creates depth.
     · There is AIR. 140px above a headline, minimum.

   THE GROUND. The page is WHITE, broken up by two or three full-width GRAPHITE
   bands. Nothing in this file names a light or a dark colour to do that: every
   colour is read through var(--cb-*), and tokens.css re-declares that whole set
   on .band. So a band inverts its own contents and this file never has to know
   which ground it is standing on. If you add a rule here, read the colour from
   a token or it will be right on one ground and wrong on the other.
   ========================================================================== */

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

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

body {
  margin: 0;
  background: var(--cb-ground);
  color: var(--cb-ink);
  font-family: var(--cb-font);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

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

::selection { background: var(--cb-violet); color: var(--cb-white); }

/* --cb-violet-ink, not --cb-violet-lift: the lift only reads on a dark ground.
   Scoped, so the ring is #6D28D9 on the white page and #B29BFF inside a band. */
:focus-visible {
  outline: 2px solid var(--cb-violet-ink);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Skip link — visible only on keyboard focus. */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--cb-violet); color: var(--cb-white); padding: 12px 18px;
  border-radius: 0 0 var(--cb-r-sm) 0; font-weight: 700;
}
.skip:focus { left: 0; }

/* ============================================================================
   TYPE
   Mechanism 6: headline line-height <= 1.1, max two lines at desktop,
   sentence case. One thought, not three stacked slogans.
   ========================================================================== */

h1, h2, h3, h4 { margin: 0; font-weight: 800; letter-spacing: -0.03em; }

.display {
  font-size: clamp(2.55rem, 6.2vw, 4.4rem);
  line-height: 1.04;
  letter-spacing: -0.035em;
  max-width: 19ch;              /* forces the two-line discipline */
  margin-bottom: clamp(20px, 2.4vw, 30px);
  text-wrap: balance;           /* stops "for." orphaning on a phone */
}

h2, .h2 {
  font-size: clamp(1.95rem, 3.6vw, 2.9rem);
  line-height: 1.1;
  max-width: 20ch;
}

h3, .h3 { font-size: 1.2rem; line-height: 1.3; letter-spacing: -0.02em; }

p { margin: 0 0 1.1em; color: var(--cb-ink-2); }
p.lead {
  font-size: clamp(1.05rem, 1.6vw, 1.25rem);
  color: var(--cb-ink-2);
  max-width: 46ch;
  line-height: 1.6;
}

/* The small grey label above a heading. Greyscale — NOT violet.
   This is one of the places the old build spent colour it did not need to. */
.eyebrow {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--cb-ink-3);
  margin: 0 0 var(--cb-gap-2);
}

/* .accent used to paint a headline phrase violet. Adam killed it on 2026-08-15,
   when the page went light.

   Why, so nobody "restores" it: at display size on white, #6D28D9 is a bigger
   and louder mass than the primary button sitting under it, so the eye lands on
   the claim instead of the thing you want pressed. It read as a soft highlight
   on near-black; it does not read that way on white. Violet now lives only on
   things you press, which is BRAND.md rule 3 — the badge, not the paint.

   The class is kept, colourless, so the markup does not need touching if a
   future page wants to emphasise a phrase some other way (weight, a rule under
   it). If violet text is ever genuinely needed inside a graphite band, the
   #B29BFF lift still exists as --cb-violet-ink. */
.accent { color: inherit; }

a { color: inherit; text-decoration: none; }
.link {
  color: var(--cb-ink);
  border-bottom: 1px solid var(--cb-line);
  padding-bottom: 1px;
  transition: border-color var(--cb-fast) var(--cb-ease), color var(--cb-fast);
}
.link:hover { border-color: var(--cb-violet-ink); color: var(--cb-ink); }

/* ============================================================================
   LAYOUT
   ========================================================================== */

.wrap { width: 100%; max-width: var(--cb-wrap); margin-inline: auto; padding-inline: 24px; }

section { position: relative; }

/* Mechanism 7: real air. 140px above a headline at desktop. */
.section      { padding-block: clamp(72px, 11vw, var(--cb-gap-7)); }
.section-tight{ padding-block: clamp(48px, 7vw, 88px); }

/* Full-width graphite band. BRAND.md: graphite is the structural dark in BOTH
   modes. On a light page these are what stop it going flat. Two or three per
   page, no more — index.html runs three.

   tokens.css re-declares the whole dark palette on .band, so this element and
   everything under it resolves ground/surface/line/ink/violet-ink/shadow to
   their dark values. Two things this rule still has to do by hand:

     · `color`. It is INHERITED, so descendants that set no colour of their own
       would otherwise inherit the value <body> already computed from the LIGHT
       --cb-ink. Re-stating it here makes the band the new inheritance root.
       Without this line every unstyled h2, h3 and <b> inside a band goes
       graphite-on-graphite and disappears. It is the single load-bearing
       declaration in the whole flip.
     · `background`. Graphite is a brand constant, not a ground token, so it
       does not come along with the inversion. */
.band {
  background: var(--cb-graphite);
  color: var(--cb-ink);
  border-block: 1px solid var(--cb-line-soft);
}

/* ============================================================================
   THE SECTION WASH  (.section:not(.band)::before)  — new 2026-08-18
   ----------------------------------------------------------------------------
   WHY THIS EXISTS. Adam, looking at the hero: *"when it goes to the right side
   of the screen and has the purple shading through it, it looks really nice…
   can we add more purple to all the white sections, the soft purple / white mix
   looks nice."* This is that, taken off the hero and given to every white
   section on the site.

   ⛔ READ THIS BEFORE DECIDING IT BREAKS THE BRAND RULE, BECAUSE IT LOOKS LIKE
   IT DOES AND IT DOES NOT. BRAND.md rule 3 is "purple is the badge, not the
   paint — never tint a whole screen violet again", written after the old app
   ran about 70% purple. The distinction that rule turns on is already written
   on .hero-paths, and it is the reason the hero's glow was allowed violet in
   the first place:

       A wash with NO EDGE reads as atmosphere.  A shape with an edge reads as
       an object, and objects are what mechanism 2's colour budget counts.

   Everything here is a radial gradient fading to transparent. There is no edge
   anywhere on it, at any width. It is the glow, which Adam approved and has now
   asked for more of, applied consistently instead of only on the one section
   that happened to have it.

   ⚠️ THE CEILING IS .085 ALPHA AND IT IS THE WHOLE SAFETY MARGIN. That is
   BELOW the hero glow's own .13, on purpose: the hero is the one place the wash
   is meant to be noticed. ⛔ If a future session is asked for "more purple", the
   answer is NOT to raise this number. Past roughly .12 the ground stops reading
   as white and starts reading as lilac, the graphite bands stop being the thing
   that breaks the page up, and rule 3 is genuinely broken. Adam should see a
   frame before it moves.

   ⛔ NEVER ON A .band. A band is graphite and is the structural dark; a violet
   wash on it would muddy the one element holding the page's rhythm. The
   :not(.band) is load-bearing, not defensive.

   HOW IT IS BUILT, and each choice is here because the obvious version is worse:

     · z-index: -1, NOT a raised .wrap. Raising the content instead would mean
       putting position/z-index on .wrap, which creates a stacking context on
       every section on the site — and mechanism 5's overhanging artefacts
       (.hero-float, .float, the cropped cards) all depend on the current
       stacking. Painting the wash BEHIND touches none of it. `section` is
       already position: relative, so it is contained without a new rule.
     · IT ALTERNATES SIDES. Every even section mirrors the gradient. Twenty
       identical washes down a page is wallpaper, and the eye finds a repeat
       within two scrolls. Same reasoning as the two crossing fans in the hero.
     · No blur(), no filter, no animation. It is two static gradients. MEASURED
       at 0.0ms cost — the page holds the same 60fps with it as without it.
     · pointer-events: none, so it can never take a tap from a control under it.
   ========================================================================== */
.section:not(.band)::before,
.section-tight:not(.band)::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(48% 58% at 86% 14%, rgba(109,40,217,.085) 0%, transparent 72%),
    radial-gradient(40% 50% at 10% 86%, rgba(109,40,217,.05)  0%, transparent 76%);
}

/* The mirror. nth-of-type over the real <section> elements, so it alternates by
   position on the page rather than by anything the markup has to remember. */
.section:not(.band):nth-of-type(even)::before,
.section-tight:not(.band):nth-of-type(even)::before {
  background:
    radial-gradient(48% 58% at 14% 14%, rgba(109,40,217,.085) 0%, transparent 72%),
    radial-gradient(40% 50% at 90% 86%, rgba(109,40,217,.05)  0%, transparent 76%);
}

/* The 404's body is a .wrap rather than a .section — it is the one page with no
   section element at all — so it takes the wash directly or it would be the
   single flat-white page on the site. */
.lost::before {
  content: "";
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(48% 58% at 86% 14%, rgba(109,40,217,.085) 0%, transparent 72%),
    radial-gradient(40% 50% at 10% 86%, rgba(109,40,217,.05)  0%, transparent 76%);
}

.grid { display: grid; gap: var(--cb-gap-3); }
.grid-2 { grid-template-columns: 1fr; }
.grid-3 { grid-template-columns: 1fr; }
@media (min-width: 760px)  { .grid-2 { grid-template-columns: 1fr 1fr; } }
@media (min-width: 980px)  { .grid-3 { grid-template-columns: repeat(3, 1fr); } }

/* ============================================================================
   NAV
   Mechanism 8: quiet. Small type, no heavy bar, no borders, one accent button
   top right, everything else plain text.
   Fixed chrome must stay under 12% of a 390px viewport.

   THE PROBLEM THIS BLOCK SOLVES. The nav is sticky and the page it scrolls over
   is white in some places and graphite in others. A permanently white bar would
   cut a visible strip out of every band; a permanently dark one would sit on the
   white hero like a toolbar. Neither is quiet.

   SO THE NAV CHANGES GROUND WITH THE PAGE. site.js watches what is behind the
   bar and adds .is-dark whenever a band is passing under it. tokens.css gives
   .nav.is-dark the dark palette, so the type, the ghost button's outline and the
   hairline all invert with it, and the bar's own tint switches from white to
   graphite below. Over white it is white; over a band it is the band. In both
   cases you see type on a ground, never a bar.

   The 320ms cross-fade is what stops that reading as a flicker at the seam.

   With JS off, .is-dark is never added: the nav stays white-tinted and its
   graphite type stays readable everywhere, because the tint is opaque enough to
   carry it. Quieter with JS, correct without. */

.nav {
  --nav-fade: 460ms;             /* the ground cross-fade. over 400ms, per bar.md. */
  position: sticky; top: 0; z-index: 100;
  color: var(--cb-ink);          /* inheritance root, same reason as .band */
  /* 92%, not 82%. Over its OWN ground the tint is the ground, so opacity costs
     nothing — you never see a bar. It only matters in the ~60px of scroll where
     the bar has not swapped yet and the other ground is showing through behind
     it: at 82% that read as a muddy grey strip, at 92% it stays clean. */
  background: color-mix(in srgb, var(--cb-ground) 92%, transparent);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--nav-fade) var(--cb-ease),
              background var(--nav-fade) var(--cb-ease),
              color var(--nav-fade) var(--cb-ease);
}

/* Graphite, not --cb-ground: the band itself is graphite, so tinting with
   graphite makes the bar vanish into it. --cb-ground would be #0F1115 and would
   show as a darker strip laid over the band. */
.nav.is-dark { background: color-mix(in srgb, var(--cb-graphite) 92%, transparent); }

/* The hairline only exists once you have scrolled. --cb-line, not --cb-line-soft:
   soft is invisible on white. Inside .is-dark it resolves to #262B36, which is
   one step off graphite — present, not a border. */
.nav.is-stuck { border-bottom-color: var(--cb-line); }

/* The ghost button and the sign-in link carry token colours too, so they have to
   cross-fade on the same clock as the bar or they snap while it drifts. */
.nav .btn,
.nav .nav-signin {
  transition: transform var(--cb-fast) var(--cb-ease),
              background var(--nav-fade) var(--cb-ease),
              border-color var(--nav-fade) var(--cb-ease),
              color var(--nav-fade) var(--cb-ease);
}

.nav-in {
  display: flex; align-items: center; gap: var(--cb-gap-3);
  height: 62px;
}

/* min-height, not height: the bar is 62px and .nav-in centres it, so this buys
   the logo a 44px hit box without moving a pixel of what you can see. */
.logo { display: flex; align-items: center; min-height: var(--cb-tap); gap: 10px; font-weight: 800; letter-spacing: -.03em; font-size: 1.06rem; }
.logo svg { width: 26px; height: 26px; color: var(--cb-ink); transition: color var(--nav-fade) var(--cb-ease); }

/* gap-1, not gap-3, because each link now carries 8px of its own side padding
   for the tap box. 8 + 8 + 8 = the 24px rhythm the gap used to hold on its own,
   so the bar looks identical and the hit boxes no longer touch. Change the
   padding below and this gap has to move with it. */
.nav-links { display: none; gap: var(--cb-gap-1); margin-left: var(--cb-gap-2); }
/* --nav-fade, not --cb-fast: this same `color` carries BOTH hover and the
   light/graphite ground change, and the ground change must not be under 400ms.
   One property cannot hold two durations, so the slower need wins. */
.nav-links a {
  display: inline-flex; align-items: center;
  min-height: var(--cb-tap); padding-inline: 8px;
  font-size: 14.5px; font-weight: 600; color: var(--cb-ink-2);
  transition: color var(--nav-fade) var(--cb-ease);
}
.nav-links a:hover { color: var(--cb-ink); }
.nav-links a[aria-current="page"] { color: var(--cb-ink); }

/* 6px, not 14px, because .nav-signin now carries 8px of side padding for its
   tap box. 6 + 8 = the 14px this row has always shown. Same reason as
   .nav-links above: the padding took over part of the gap's old job. */
.nav-right { margin-left: auto; display: flex; align-items: center; gap: 6px; }

.nav-burger {
  width: 44px; height: 44px; display: grid; place-items: center;
  background: none; border: 0; color: var(--cb-ink); cursor: pointer;
  transition: color var(--nav-fade) var(--cb-ease);
}

/* Must stay AFTER the .nav-burger base rule. Same specificity, so source order
   decides — with the block above it, `display: grid` won and the burger showed
   at every width, sitting beside the full desktop nav. */
@media (min-width: 900px) {
  .nav-links { display: flex; }
  .nav-burger { display: none; }
}

/* Mobile menu. Finding 10 in the usability report: the old one would not close
   on outside tap or Escape, and the page scrolled behind it. Both fixed. */
.menu {
  position: fixed; inset: 0; z-index: 150;
  display: grid; grid-template-rows: auto 1fr;
  background: color-mix(in srgb, var(--cb-ground) 96%, transparent);
  backdrop-filter: blur(20px);
  opacity: 0; pointer-events: none;
  transition: opacity var(--cb-mid) var(--cb-ease);
}
.menu[data-open="true"] { opacity: 1; pointer-events: auto; }
.menu-panel { padding: 0 24px 40px; display: flex; flex-direction: column; gap: 4px; }
.menu-panel a {
  font-size: 1.5rem; font-weight: 700; letter-spacing: -.03em;
  padding: 14px 0; border-bottom: 1px solid var(--cb-line-soft);
}
body.menu-open { overflow: hidden; }
/* Was 20px inline, which is not on the rhythm. --cb-gap-3 is the nearest step
   and the .menu-panel's own gap is 4px, so the buttons still read as a separate
   group below the links. */
.menu-cta { margin-top: var(--cb-gap-3); }

/* ============================================================================
   BUTTONS
   Mechanism 3. THE most important rule on this page.
   Small. Capped. One primary per screen. Never a full-width slab.
   Two caps and only two: 220px above 520, 280px at or below. LD#26.
   ========================================================================== */

.btn {
  --btn-bg: var(--cb-violet);
  --btn-fg: var(--cb-white);   /* white on violet. Correct on either ground. */
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 46px;
  padding: 0 22px;
  max-width: 220px;                 /* the cap above 520px. do not remove. */
  background: var(--btn-bg); color: var(--btn-fg);
  border: 1px solid transparent; border-radius: var(--cb-r-sm);
  font: inherit; font-size: 15px; font-weight: 700; letter-spacing: -.01em;
  cursor: pointer; white-space: nowrap;
  transition: transform var(--cb-fast) var(--cb-ease),
              background var(--cb-fast), box-shadow var(--cb-fast);
}
.btn:hover { --btn-bg: var(--cb-violet-hover); transform: translateY(-1px); }
.btn:active { transform: translateY(0); }

/* Secondary. Outline, never a second colour. BRAND.md. */
.btn-ghost {
  --btn-bg: transparent;
  --btn-fg: var(--cb-ink);
  border-color: var(--cb-line);
}
.btn-ghost:hover { --btn-bg: var(--cb-surface); border-color: var(--cb-ink-3); }

.btn-lg { min-height: 52px; padding: 0 26px; font-size: 16px; }
/* 44, not 38. A "small" button is still a button and still gets pressed with a
   glove on. Small now means quieter type and tighter sides, never a shorter
   box. --cb-tap is the floor for every real control on the site. */
.btn-sm { min-height: var(--cb-tap); padding: 0 15px; font-size: 14px; }

.cta-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }

/* Even on a phone a button is not a slab. It gets wider, not edge to edge.
   THE PHONE CAP — 280px, deliberate, not a drift. Settled 2026-08-16 (LD#26)
   after the Brief critic read it as a full-width slab. Measured at 390px it
   renders 280 inside a 342px column, so 62px of ground still shows on the
   right and it reads as an object. This is the second of the only two caps.
   NOTE: this block must stay AFTER the .cta-row base rule above. Same
   specificity, so source order decides, and flipping them silently centres
   every button on a phone. */
@media (max-width: 520px) {
  .btn { max-width: 280px; width: 100%; }
  .cta-row { flex-direction: column; align-items: flex-start; }
  /* .cta-centre sits under centred copy, so it must NOT pick up flex-start when
     the row stacks — that leaves two buttons hard left under a centred
     headline. Inside the same block, after the rule it corrects. */
  .cta-centre { align-items: center; }
}

/* A .cta-row under centred copy. Kept as a modifier rather than a .centre
   descendant selector so the footer and any other centred block can use
   .centre without their rows being re-laid out. */
.cta-centre { justify-content: center; }

/* The quiet text link that sits under a CTA. Carries price, proof, or a
   second route without spending another button. */
.cta-note { font-size: 14px; color: var(--cb-ink-3); margin: 14px 0 0; }

/* A section-head note that disclaims every mockup in the section at once —
   design-system.md non-negotiable 4. It replaced four per-card captions in
   #what on 2026-08-16. Reuse it on the other 19 pages: one of these under the
   h2 beats a caption on every artefact.
   Colour: --cb-ink-3 pulled a quarter toward --cb-ink. That was the old
   .wt-cap's value, measured at 6.6:1 on --cb-surface, and both ends are tokens
   so it inverts correctly inside a band. 50ch is a CEILING, not a target. */
.mock-note {
  font-size: 14px; max-width: 50ch; margin: var(--cb-gap-2) 0 0;
  color: color-mix(in srgb, var(--cb-ink-3) 72%, var(--cb-ink));
}
/* `a, button` and not just `a`. LD#62, 2026-08-18. "Forgot your password?" fires
   the app's inline reset, it does not route to a page, so it is a <button>. The
   reset below is what makes a button indistinguishable from the link beside it:
   the browser's own background, border, padding and font are the only things
   that would give it away. Widen BOTH selectors or neither. */
.cta-note button {
  background: none; border: 0; padding: 0; font: inherit;
  text-align: left; cursor: pointer;
}
.cta-note a, .cta-note button { color: var(--cb-ink-2); border-bottom: 1px solid var(--cb-line); }
.cta-note a:hover, .cta-note button:hover { color: var(--cb-ink); border-color: var(--cb-ink-3); }

/* When the link IS the whole note, it is a call to action wearing a link's
   clothes, so it gets the full tap floor. LD#25, 2026-08-16.
   A link inside a running sentence does NOT get this and must not: padding it
   out to 44px would put its hit box over the line of text above, and stealing
   a tap from body copy is worse than a small target. That is the documented
   exception in design-system.md, and it is what WCAG 2.2 SC 2.5.8 exempts.
   `align-self: start` keeps the flex box hugging the words rather than
   stretching across the column and making the whole line pressable. */
.cta-note.is-link { display: flex; }
.cta-note.is-link a, .cta-note.is-link button {
  display: inline-flex; align-items: center; align-self: start;
  min-height: var(--cb-tap);
}

/* ============================================================================
   CARDS AND DEPTH
   Mechanism 5: content overlaps its container. At least two per page.
   ========================================================================== */

.card {
  background: var(--cb-surface);
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-lg);
  padding: var(--cb-gap-3);
  position: relative;
  transition: border-color var(--cb-mid) var(--cb-ease),
              transform var(--cb-mid) var(--cb-ease);
}
.card:hover { border-color: var(--cb-line); transform: translateY(-2px); }

/* h1-h4 are margin: 0 globally, so a card heading sat flush against its own
   paragraph. Barely visible on near-black, obvious on white. */
.card h3 { margin-bottom: var(--cb-gap-1); }

/* Card icons are GREY. Not violet. This was a big part of the old colour wash. */
.card-icon {
  width: 38px; height: 38px; border-radius: 10px;
  display: grid; place-items: center;
  background: var(--cb-surface-2); color: var(--cb-ink-3);
  border: 1px solid var(--cb-line);
  margin-bottom: var(--cb-gap-2);
}

/* A card whose content is allowed to run past the edge and crop.
   This single class does most of the depth work on the page. */
.card-bleed { overflow: hidden; padding-bottom: 0; }
.card-bleed .bleed-media {
  margin: var(--cb-gap-3) calc(var(--cb-gap-3) * -1) 0 0;   /* runs off the right */
  border-top-left-radius: var(--cb-r-md);
  border-left: 1px solid var(--cb-line);
  border-top: 1px solid var(--cb-line);
  box-shadow: var(--cb-shadow-3);
}

/* Something floating over something else. QuickBooks does this over a photo. */
.float {
  position: absolute; z-index: 3;
  background: var(--cb-surface-2);
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-md);
  box-shadow: var(--cb-shadow-3);
  padding: 12px 14px;
}

/* Hi-vis chip. SURFACE, never ink. Live status and warnings only. */
.chip-live {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--cb-hivis); color: var(--cb-hivis-ink);
  border-radius: var(--cb-r-pill); padding: 4px 11px;
  font-size: 12.5px; font-weight: 800; letter-spacing: -.01em;
}
.chip-live::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--cb-hivis-ink);
  animation: cb-pulse 2.4s var(--cb-ease) infinite;
}

/* Neutral chip. Greyscale. Used for everything that is not live status. */
.chip {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--cb-surface); color: var(--cb-ink-2);
  border: 1px solid var(--cb-line); border-radius: var(--cb-r-pill);
  padding: 5px 13px; font-size: 12.5px; font-weight: 700;
}

/* ============================================================================
   FORMS
   Findings 7 and 19: correct input types, and the submit button is not a
   different species from every other button on the site.
   ========================================================================== */

.field { display: grid; gap: 7px; margin-bottom: var(--cb-gap-2); }
.label { font-size: 13.5px; font-weight: 700; color: var(--cb-ink-2); }
.input, .textarea, .select {
  width: 100%; min-height: 48px; padding: 12px 14px;
  background: var(--cb-surface); color: var(--cb-ink);
  border: 1px solid var(--cb-line); border-radius: var(--cb-r-sm);
  font: inherit; font-size: 16px;   /* 16px stops iOS zooming on focus */
  transition: border-color var(--cb-fast), background var(--cb-fast);
}
.input::placeholder, .textarea::placeholder { color: var(--cb-ink-3); }
.input:focus, .textarea:focus, .select:focus {
  border-color: var(--cb-violet-ink); background: var(--cb-surface-2); outline: none;
}
.textarea { min-height: 120px; resize: vertical; line-height: 1.55; }
.hint { font-size: 13px; color: var(--cb-ink-3); }

/* ============================================================================
   FOOTER
   ========================================================================== */

/* The footer is LIGHT. It used to be graphite, which on a dark page was just
   more page. On a white page a graphite footer would be a fourth band, and
   BRAND.md line 57 says two or three. index.html already runs three, so the
   footer stays on the ground and separates with a hairline instead.
   If you ever need a dark footer back, add .band to it — it will invert itself
   and nothing else has to change. */
.foot { background: var(--cb-ground); border-top: 1px solid var(--cb-line); }
.foot-in { padding-block: var(--cb-gap-5) var(--cb-gap-4); display: grid; gap: var(--cb-gap-4); }
.foot-cols { display: grid; gap: var(--cb-gap-4); grid-template-columns: 1fr; }
@media (min-width: 760px) { .foot-cols { grid-template-columns: 1.4fr 1fr 1fr 1fr; } }
.foot h4 { font-size: 12px; letter-spacing: .14em; text-transform: uppercase; color: var(--cb-ink-3); margin-bottom: 14px; font-weight: 700; }
/* 11px, not 6px. A footer column is a stack of real controls, so each one gets
   the tap floor: 23.9px of line + 22px of padding clears 44. The column grows
   about 10px per link, which a footer can afford and a thumb cannot do without. */
.foot a { display: block; padding: 11px 0; font-size: 14.5px; color: var(--cb-ink-2); }
.foot a:hover { color: var(--cb-ink); }
.foot-base { border-top: 1px solid var(--cb-line); padding-top: var(--cb-gap-3); font-size: 13.5px; color: var(--cb-ink-3); display: flex; flex-wrap: wrap; align-items: center; gap: var(--cb-gap-2) var(--cb-gap-3); }
/* Privacy and Terms are the two smallest real controls on the site. "Terms" is
   only 40.8px of glyphs, so it needs the side padding as well as the height. */
.foot-base a {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--cb-tap); padding-inline: 6px; margin-inline: -6px;
}
/* 30ch is a ceiling. Both margins were 14px inline, which is not on the
   8/16/24/40/64/96/140 rhythm; --cb-gap-2 is the nearest step. */
.foot-blurb { margin-top: var(--cb-gap-2); max-width: 30ch; font-size: 14.5px; }
/* .foot-phone was here and went with the phone placeholders on 2026-08-17
   (LD#40). It was one line: margin-top: var(--cb-gap-2). Put it back verbatim
   the day CrewBase has a number, and put the number in all seven footers at
   once — a phone on one page and not the next is worse than none. */

/* ============================================================================
   MOTION
   Adam's rule: motion runs continuously and subtly, it never fires once and
   stops. A one-shot gets missed and reads as "no effect".
   Reveals are the exception: they are a state change, so they fire once.
   ========================================================================== */

@keyframes cb-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .35; transform: scale(.82); }
}

/* The slow drift on the hero glow. Continuous, never resolves abruptly. */
@keyframes cb-drift {
  0%   { transform: translate3d(0,0,0)      scale(1); }
  33%  { transform: translate3d(3%, -2%, 0) scale(1.06); }
  66%  { transform: translate3d(-2%, 2%, 0) scale(.97); }
  100% { transform: translate3d(0,0,0)      scale(1); }
}

/* The counter-drift, on the hero glow's second layer. Runs the other way on a
   28s clock against the first layer's 22s. Coprime-ish on purpose: they never
   repeat the same overlap, so the wash keeps changing density instead of just
   sliding about. */
@keyframes cb-drift-2 {
  0%   { transform: translate3d(0,0,0)        scale(1); }
  33%  { transform: translate3d(-4%, 3%, 0)   scale(.94); }
  66%  { transform: translate3d(3%, -3.5%, 0) scale(1.08); }
  100% { transform: translate3d(0,0,0)        scale(1); }
}

/* Reveal on scroll. 400ms minimum, per bar.md — nothing animates under 400ms. */
.reveal {
  opacity: 0; transform: translateY(18px);
  transition: opacity var(--cb-slow) var(--cb-ease),
              transform var(--cb-slow) var(--cb-ease);
}
.reveal.is-in { opacity: 1; transform: none; }
.reveal[data-delay="1"] { transition-delay: 90ms; }
.reveal[data-delay="2"] { transition-delay: 180ms; }
.reveal[data-delay="3"] { transition-delay: 270ms; }

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

/* ============================================================================
   HERO
   ========================================================================== */

.hero { position: relative; overflow: hidden; padding-block: clamp(64px, 9vw, 118px) clamp(56px, 8vw, 104px); }

/* The one continuous piece of motion on the page. Adam's rule: motion runs
   continuously, it does not fire once and stop.

   REBUILT FOR A WHITE GROUND. The old version was two violet radials at .55 and
   .24 alpha, tuned to bloom OUT of near-black. Additive light on near-black is
   a glow; the same paint laid on white is a bruise. So the light treatment is
   built the other way round:

     · Alpha drops by roughly a factor of five, to .12 and .07. On white you are
       not adding light, you are staining paper. It has to sit under the
       threshold where you would call it "a purple shape".
     · The blur goes UP to 64px and the whole thing gets bigger and pushed
       further off the corner, so there is no readable edge anywhere. An edge is
       what turns a wash into an object, and an object costs a slot in the
       three-colour budget. This has to stay atmosphere.
     · Both layers are the SAME violet, #6D28D9. The lift (#B29BFF) is for small
       text on dark and would go chalky here.

   TWO LOOPS, NOT ONE, and this is the part that earns its place. A single
   translating blob on white is nearly invisible — there is no edge to notice
   moving. So a second, counter-drifting layer runs on ::after at 28s against
   the parent's 22s. 22 and 28 do not divide, so the two never line up the same
   way twice and the density where they cross keeps breathing. That reads as
   alive at an alpha where movement alone would not.

   PHONE FIRST, and this is where the light version nearly failed. The dark
   version could sit almost entirely off the top-right corner and still read,
   because a bloom on near-black carries a long way past its own edge. At .12 on
   white it does not: parked off-corner at 390px it was invisible, which is a
   whole hero with nothing alive in it on the device most of these get opened on.
   So the phone placement is bigger (120vw) and pulled further onto the screen,
   and the desktop placement is the one that gets to hang off the corner.
   ------------------------------------------------------------------------ */
/* ⛔ THE blur() IS GONE, AND IT IS A PERFORMANCE FIX, NOT A LOOK CHANGE.
   Removed 2026-08-18, in the same pass as LD#57.

   It used to carry filter: blur(64px). A blurred filter re-rasterises the whole
   layer every frame the element drifts, and this element is up to 1100px square.
   MEASURED at 1440x900, best of three: with the blur the hero sits at 33.3ms
   (30fps, 79 janky frames in 115); with it removed, 16.7ms — a clean 60fps and
   ZERO janky frames. On a phone it made no difference either way, because the
   element is capped smaller there.

   ⚠️ IT COSTS NOTHING VISUALLY BECAUSE A RADIAL GRADIENT IS ALREADY SOFT. The
   blur was smoothing something that has no edge to smooth. The stops are widened
   slightly below to give back the few pixels of spread the blur was adding, so
   the wash reads the same size as it did.

   ⛔ DO NOT PUT A blur() BACK ON A DRIFTING ELEMENT. If a future effect needs
   one, it has to be on something that does not animate. -------------------- */
.hero-glow {
  position: absolute; z-index: 0; pointer-events: none;
  top: -26%; right: -26%; width: 120vw; height: 120vw; max-width: 1100px; max-height: 1100px;
  background:
    radial-gradient(46% 46% at 60% 42%, rgba(109,40,217,.13) 0%, transparent 76%),
    radial-gradient(52% 52% at 34% 62%, rgba(109,40,217,.08) 0%, transparent 80%);
  animation: cb-drift 22s var(--cb-ease) infinite;
}

@media (min-width: 760px) {
  .hero-glow { top: -34%; right: -20%; width: 86vw; height: 86vw; }
}

/* The counter-drift. Same hue, weaker still, its own clock. */
.hero-glow::after {
  content: ""; position: absolute; inset: 6% -4% -6% 4%;
  background: radial-gradient(44% 44% at 46% 50%, rgba(109,40,217,.09) 0%, transparent 72%);
  animation: cb-drift-2 28s var(--cb-ease) infinite;
}

/* The dark treatment, kept and still correct. It is what runs if anyone sets
   data-theme="dark" on <html>, and it is the version to restore if the ground
   decision is ever reversed again. On near-black the alphas go back up and the
   second layer uses the lift, because there it reads as light, not as paint. */
/* ⚠️ NO blur() HERE EITHER, for the reason written on .hero-glow above. This
   block is DORMANT on the marketing site — light is :root and the bands
   re-declare their own palette, so nothing here ever sets data-theme="dark" —
   but it is kept in step deliberately. It carried filter: blur(46px) until
   2026-08-18, which would have re-armed the exact 30fps trap the moment the
   ground decision was reversed or this file went into the app, where the theme
   IS switchable. Stops widened to match. */
[data-theme="dark"] .hero-glow {
  background:
    radial-gradient(44% 44% at 62% 40%, rgba(109,40,217,.55) 0%, transparent 76%),
    radial-gradient(48% 48% at 38% 62%, rgba(178,155,255,.24) 0%, transparent 78%);
}
[data-theme="dark"] .hero-glow::after {
  background: radial-gradient(44% 44% at 46% 50%, rgba(178,155,255,.10) 0%, transparent 72%);
}

/* ---------------------------------------------------------------------------
   THE FLOWING LINES  (.hero-paths)  — new 2026-08-18
   ----------------------------------------------------------------------------
   WHY THIS EXISTS. Adam looked at the built pages and said "there is no
   movement". He was reading it correctly. The glow above IS continuous motion,
   but it is a blurred wash on a 22 second clock, and a blurred wash with no
   edge moving over 22 seconds is not something an eye reports as movement. The
   status chips pulse across about 8px. So the hero had motion on paper and
   none in practice.

   WHAT IT IS. A set of long curved lines, drawn as SVG, with a dash sliding
   along each one forever. It is the effect Adam brought in as a React
   component. Rebuilt in plain SVG because this site has no framework and no
   build step, and neither does the app it plugs into.

   ⛔ IT IS GREYSCALE AND IT MUST STAY GREYSCALE. bar.md mechanism 2 allows two
   or three coloured things on a screen and the hero already spends them on the
   primary button and the hi-vis chip. The glow gets away with violet because it
   has no edge anywhere, so it reads as atmosphere rather than as an object.
   A line HAS an edge. Give these lines a colour and the hero is over budget.

   HOW THE LOOP IS SEAMLESS. Each path carries pathLength="1", which tells the
   browser to treat that curve as exactly one unit long no matter what its real
   geometry is. So a dash pattern of .34/.66 is "a third on, two thirds off" on
   every line, and sliding stroke-dashoffset from 0 to -1 travels exactly one
   full pattern. It lands where it started, so there is no jump and no restart.
   This is Adam's standing rule in code: motion loops, it never fires once.

   TWO CLOCKS THAT DO NOT DIVIDE, same trick as the glow above. The slide and
   the opacity breathe run on separate durations that never line up, so the
   picture does not repeat itself on a countable beat.

   ⚠️ THE DURATIONS ARE DERIVED FROM THE PATH INDEX, NOT RANDOM. The reference
   component used Math.random(). It cannot be used here: checks/prestart.py pins
   animations to one frame and screenshots, and a rig that renders something
   different every run cannot be checked. Index-derived spreads them just as
   well and gives the same picture every time.
   -------------------------------------------------------------------------- */
.hero-paths {
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

.hero-paths svg { display: block; width: 100%; height: 100%; }

/* ⛔⛔ THE BREATHE IS ON THE CONTAINER, NOT ON THE PATHS. DO NOT MOVE IT BACK.
   ⛔⛔ Fixed 2026-08-18, hours after this component shipped. LD#57.

   Adam looked at the site and said it was "flickering and carrying on like
   shit". He was right, and it was this. The first version put BOTH animations on
   every path, so 44 SVG paths each animated their own `opacity` under a large
   blurred .hero-glow. Animated opacity on an SVG path is not composited: every
   frame re-rasterises the path AND forces the blurred layer stacked over it to
   re-rasterise with it. The result was a hero that could not hold a frame.

   MEASURED, headless at 1440x900 and 390x844, best of three runs each:

                                   desktop              phone
     no hero effects at all        16.7ms  60fps        16.7ms  60fps
     glow only (the old normal)    33.3ms  30fps        16.7ms  60fps   0 jank
     ⛔ as first shipped          116.1ms   8.6fps      33.3ms  30fps  101/115 jank
     ✅ breathe on the container   33.3ms  30fps        16.7ms  60fps   0 jank

   ⛔ THE PATH COUNT IS NOT THE LEVER AND CUTTING IT IS THE WRONG FIX. Twelve
   paths WITH the per-path breathe measured 100ms / 10fps, which is WORSE than
   forty-four without it. Forty-four with the slide alone costs nothing on top of
   the glow. It was never how many lines there were, it was what was being
   animated on each one.

   ⚠️ WHAT CHANGED VISUALLY, so nobody "restores" the old behaviour thinking it
   was better: the fan now breathes as ONE, instead of every line breathing on
   its own clock. The per-line depth is still there — stroke-opacity still steps
   .08 to .52 across the fan in site.js — it is just static now instead of
   animated. The slide is untouched and it is what carries the movement.

   ⚠️ AND THE SLIDE IS STILL PER-PATH, which is correct: stroke-dashoffset is
   the effect. It is the one thing here worth paying for, and measured on its own
   it costs nothing.
   -------------------------------------------------------------------------- */
.hero-paths {
  animation: cb-flow-breathe 13s var(--cb-ease) infinite;
}

.hero-paths path {
  fill: none;
  stroke: var(--cb-ink-3);
  stroke-dasharray: .34 .66;
  animation: cb-flow var(--cb-flow-dur, 22s) linear infinite;
}

/* One full dash pattern, so the end frame IS the start frame. */
@keyframes cb-flow {
  to { stroke-dashoffset: -1; }
}

/* The breathe. Kept shallow — this is meant to be felt, not watched. */
@keyframes cb-flow-breathe {
  0%, 100% { opacity: .62; }
  50%      { opacity: 1; }
}

/* On near-black the same graphite disappears, so the dark treatment lifts the
   stroke to the line colour. Kept in step with the glow's dark block above:
   if the ground decision is ever reversed, both are already written. */
[data-theme="dark"] .hero-paths path { stroke: var(--cb-ink-3); }

.hero-in { position: relative; z-index: 1; display: grid; gap: clamp(48px, 6vw, 72px); align-items: center; }
@media (min-width: 980px) { .hero-in { grid-template-columns: 1.02fr .98fr; } }

/* Mechanism 7 — real air above the headline. */
.hero-copy { padding-top: clamp(24px, 6vw, 96px); }

/* Coded product UI. Mechanism 4. */
.hero-art { margin: 0; position: relative; }
.ui-panel {
  background: var(--cb-surface);
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-lg);
  box-shadow: var(--cb-shadow-3), var(--cb-glow);
  overflow: hidden;
}
.ui-bar { display: flex; gap: 6px; padding: 13px 16px; border-bottom: 1px solid var(--cb-line-soft); }
.ui-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--cb-line); }
.ui-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 17px 18px; border-bottom: 1px solid var(--cb-line-soft); font-size: 15px; }
.ui-rows { list-style: none; margin: 0; padding: 0; }
.ui-rows li { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 15px 18px; border-bottom: 1px solid var(--cb-line-soft); }
.ui-rows li:last-child { border-bottom: 0; }
.ui-rows b { display: block; font-size: 14.5px; font-weight: 700; }
.ui-rows em { display: block; font-style: normal; font-size: 12.5px; color: var(--cb-ink-3); margin-top: 2px; }
.ui-time { font-size: 13px; font-weight: 700; color: var(--cb-ink-2); white-space: nowrap; }

/* Mechanism 5 — this crosses the panel's edge. It is what makes the page
   read as layered rather than as a template. */
.hero-float {
  right: -14px; bottom: 14px;
  display: grid; gap: 2px; min-width: 176px;
}
/* On a phone it crosses the right edge only. With room, it hangs below too. */
@media (min-width: 620px) { .hero-float { right: -34px; bottom: -14px; } }
.float-label { font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--cb-ink-3); }
.float-main  { font-size: 15.5px; font-weight: 700; }
.float-sub   { font-size: 12.5px; color: var(--cb-ink-3); }

/* Finding 15 — mockups must be captioned so nobody taps them expecting a button. */
.hero-art figcaption,
.phone-art figcaption {
  font-size: 12.5px; color: var(--cb-ink-3); text-align: center;
}
/* 44px because the hero's float hangs BELOW the panel and the caption has to
   clear it. This is a clearance, not a spacing choice. */
.hero-art figcaption { margin-top: 44px; }
/* The phone's caption is DERIVED, not measured. See --phone-float-hang on
   .phone-art: this is the hang plus the air we want under the card, so moving
   the float moves the caption with it and cannot leave it stale.

   It was a hand-measured constant twice, and it went wrong both times. LD#20
   typed 40px to clear a 22px hang; LD#24 moved the float to a 75px hang and the
   40px would have run the caption straight through the card. Two sessions, same
   number, measured by hand, wrong the moment anything above it moved. */
.phone-art figcaption {
  margin-top: calc(var(--phone-float-hang) + var(--phone-float-air));
}

/* ============================================================================
   CARDS THAT ARE LINKS
   Finding 3 — every card link says where it goes, and no two go to the
   same page.
   ========================================================================== */

.card-link { display: block; }
.card-link:hover { border-color: var(--cb-ink-3); }
.card-go {
  display: inline-flex; align-items: center; gap: 7px;
  margin-top: 4px; font-size: 14px; font-weight: 700; color: var(--cb-ink);
}
.card-go span { transition: transform var(--cb-fast) var(--cb-ease); }
.card-link:hover .card-go span { transform: translateX(4px); }

.split { align-items: center; gap: clamp(40px, 6vw, 72px); }

/* ============================================================================
   PHONE MOCKUP
   ========================================================================== */

/* ── The phone artefact's three coupled numbers, declared ONCE ──────────────
   These three are not independent. The float hangs below the handset, and the
   caption underneath has to clear whatever the float does. Written as separate
   constants they went out of step twice (LD#20, then LD#24), because nothing in
   the file said they were related.

   Change --phone-float-hang and the caption follows it on its own. Nothing else
   needs touching, and nothing can be left stale.

     --phone-float-hang  how far .phone-float drops below the handset's bottom.
                         MEASURED, not a rhythm step: the float is 94.3px tall
                         and the third mock button's bottom sits 41.0px above the
                         handset's, so 75px puts the float's top 12px clear of
                         that button — the same 12px .phone-in gives between its
                         own children. Below this the button gets clipped (LD#24);
                         far above it the float stops crossing the handset at all
                         and bar.md mechanism 5 is lost. Re-measure if the mock's
                         copy ever changes height.
     --phone-float-air   the gap left between the float's bottom edge and the
                         caption. Close enough that the caption still reads as
                         belonging to the artefact.
     --phone-float-cross how far the float crosses the handset's left edge. It
                         tightens on a phone because the handset only leaves
                         ~42px each side inside the wrap, and nothing on this
                         page may make a phone pan sideways.
   ────────────────────────────────────────────────────────────────────────── */
.phone-art {
  margin: 0;
  --phone-float-hang: 75px;
  --phone-float-air: 21px;
  --phone-float-cross: 40px;
}
@media (max-width: 430px) { .phone-art { --phone-float-cross: 16px; } }
.phone {
  width: 258px; margin-inline: auto;
  background: var(--cb-surface-2);
  border: 1px solid var(--cb-line);
  border-radius: 30px; padding: 12px;
  box-shadow: var(--cb-shadow-3);
}
.phone-in { background: var(--cb-ground); border-radius: 20px; padding: 18px 16px; display: grid; gap: 12px; }
.phone-top { display: flex; align-items: center; gap: 9px; font-size: 14.5px; }
.logo-mini svg { width: 17px; height: 17px; color: var(--cb-ink-2); }
.phone-status { margin-bottom: 2px; }

/* Finding 15 — mock buttons are outlines, not solid violet, so they cannot be
   mistaken for the real primary button 200px up the page. */
.phone-btn {
  min-height: 46px; display: grid; place-items: center;
  border-radius: var(--cb-r-sm); font-size: 15px; font-weight: 700;
  border: 1px dashed var(--cb-line); color: var(--cb-ink-2); background: transparent;
}
.phone-btn.is-mock { cursor: default; }
.phone-btn:not(.ghost) { border-style: solid; border-color: var(--cb-violet); color: var(--cb-violet-ink); }

/* The holder is only as wide as the handset, so the float can be positioned
   off the PHONE's edge rather than off the column's. width: max-content, not
   258px: if the handset is ever resized the float follows it. */
.phone-holder { position: relative; width: max-content; margin-inline: auto; }
/* Crosses the handset's bottom-LEFT CORNER, hanging below it, which is the same
   move .hero-float makes on the panel above. It sat over the middle of the
   screen first and covered two of the three mock buttons — the section's
   headline promises "four taps", so the taps are the one thing on this artefact
   that may not be obscured. Crossing a corner costs no content.

   THE -22px VERSION DID NOT DO WHAT THIS COMMENT SAID. Measured 2026-08-16:
   it covered 74.8% of the third button at 1440 and 85.5% at 390, leaving a
   9.6px scrap of its right end poking out beside the card. Two critics found
   it independently. A crop pays at ZERO or at a confident inset; a scrap is
   the ambiguous middle, and it read as a rendering fault rather than depth.

   Both numbers below are DERIVED from the constants declared on .phone-art.
   Change them there, not here — the caption underneath reads the same hang, so
   editing it in one place keeps the two in step. That is the whole point. */
.phone-float {
  left: calc(var(--phone-float-cross) * -1);
  bottom: calc(var(--phone-float-hang) * -1);
  display: grid; gap: 2px; min-width: 176px;
}

/* ============================================================================
   PRICING TIERS
   ========================================================================== */

.tiers { margin-top: var(--cb-gap-4); }
/* The tag crosses the top border, so the card must not clip it. .card sets no
   overflow of its own; this states the dependency rather than relying on it. */
.tier { position: relative; overflow: visible; }
.tier-price { font-size: 2.2rem; font-weight: 800; letter-spacing: -.04em; color: var(--cb-ink); margin: 10px 0 6px; }
.tier-price span { font-size: 1rem; font-weight: 700; color: var(--cb-ink-3); letter-spacing: 0; }
.tier-note { font-size: 14.5px; margin: 0; }

/* The one tier that steps forward. It is a MATERIAL step, not a colour one:
   one surface up, a real shadow, and a graphite hairline instead of the soft
   one. Nothing here spends from the three-colour budget. */
.tier-common {
  background: var(--cb-surface-2);
  border-color: var(--cb-line);
  box-shadow: var(--cb-shadow-2);
}
/* Graphite, not violet — violet is things you press and this is a label.
   --cb-ground on --cb-ink is the highest-contrast pair in the palette, and both
   ends are tokens, so it survives a move into a band. */
.tier-tag {
  position: absolute; top: -11px; left: var(--cb-gap-3);
  background: var(--cb-ink); color: var(--cb-ground);
  font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  padding: 5px 10px; border-radius: var(--cb-r-pill);
  white-space: nowrap;
}
.tier-cta { margin-top: var(--cb-gap-4); }

/* ============================================================================
   FAQ
   ========================================================================== */

.faq { margin-top: var(--cb-gap-4); }
.faq-wrap { max-width: 780px; }
.faq-item { border-bottom: 1px solid var(--cb-line); }
.faq-q {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 18px;
  background: none; border: 0; padding: 22px 0; cursor: pointer;
  font: inherit; font-size: 1.05rem; font-weight: 700; letter-spacing: -.02em;
  color: var(--cb-ink); text-align: left;
}
.faq-chev {
  flex: none; width: 11px; height: 11px; border-right: 2px solid var(--cb-ink-3); border-bottom: 2px solid var(--cb-ink-3);
  transform: rotate(45deg) translate(-2px,-2px);
  transition: transform var(--cb-mid) var(--cb-ease);
}
.faq-item[data-open="true"] .faq-chev { transform: rotate(-135deg) translate(-3px,-3px); }

/* ⚠️ This accordion is FINE. It was rewritten and reverted on 2026-08-16, and
   the reason is worth more than the code: measured in a Chrome tab whose window
   was behind the terminal, every answer appeared to open to 0px. It looked like
   a dead accordion and six invisible answers. It was not.
   `document.timeline.currentTime` was **0 and never advanced** — an occluded
   window freezes the animation clock, so a CSSTransition reports playState
   "running" while stuck permanently on its first frame, and the computed height
   stays at the FROM value. That beats an inline style, which is what made it
   look like an override.
   To measure anything transitioned from a script, either finish the transition
   first — `el.getAnimations().forEach(a => a.finish())` — or read it with
   transitions off. Both give the true end state with a frozen clock. Done that
   way, all six answers open to their exact content height and close to 0. */
.faq-a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--cb-mid) var(--cb-ease); }
.faq-a > p { overflow: hidden; margin: 0; }
.faq-item[data-open="true"] .faq-a { grid-template-rows: 1fr; }
.faq-item[data-open="true"] .faq-a > p { padding-bottom: 22px; }

/* ============================================================================
   MISC NAV / CLOSER
   ========================================================================== */

.nav-signin {
  display: inline-flex; align-items: center;
  min-height: var(--cb-tap); padding-inline: 8px;
  font-size: 14.5px; font-weight: 700; color: var(--cb-ink-2);
}
.nav-signin:hover { color: var(--cb-ink); }
@media (max-width: 560px) { .hide-sm { display: none; } }

/* 20ch, not 22. design-system.md sets the h2 ceiling at 20ch and says plainly
   that narrower is fine and wider is the violation. The closer's headline is
   short enough that neither figure ever wraps it, so this changes nothing on
   screen — it stops the one rule on the page that contradicted the system. */
.closer h2   { max-width: 20ch; margin-inline: auto; }
.closer .lead { margin: var(--cb-gap-3) auto 0; }
.closer .cta-centre { margin-top: var(--cb-gap-4); }

/* Placeholders. Visible on purpose — these are things the team must fill in.
   Hi-vis was the INK here, which breaks BRAND.md rule 2 and, now that the page
   is white, is also barely readable: #F5A524 on #FFFFFF is about 2:1. So hi-vis
   goes back to being a SURFACE — a tint of it over whatever ground it lands on —
   and the text is ordinary ink. Both sides are tokens, so this reads correctly
   on the white page and inside a graphite band without a second rule. */
[data-placeholder] {
  font-size: 13px;
  color: var(--cb-ink-2);
  background: color-mix(in srgb, var(--cb-hivis) 16%, var(--cb-ground));
  border: 1px dashed color-mix(in srgb, var(--cb-hivis) 55%, transparent);
  border-radius: var(--cb-r-sm); padding: 8px 11px; display: inline-block;
}

/* ============================================================================
   UTILITIES
   ========================================================================== */

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

/* ============================================================================
   THE PROBLEM SECTION  —  index.html, "Right now the job lives in six places"
   ----------------------------------------------------------------------------
   Everything here is prefixed .pb- so it cannot collide with any other
   section's work. It touches no shared class; it only USES .card as it stands.

   Every colour below resolves from a --cb-* token, mostly through color-mix.
   There is not one raw hex or rgba() in this block. That matters more than it
   used to: this section lives inside a .band, and .band re-points the whole
   palette, so a hardcoded value would be right here and wrong the day the
   section moves onto the white page.

   The shape of it, and why:

     · THE ARTEFACT IS THE CARD. On the reference sites the coded visual owns
       roughly three quarters of a card and the words read as its caption. So
       each card here is a heading plus ONE short paragraph sitting on top of
       an artefact that is taller than both put together.

     · THE ARTEFACT IS BIGGER THAN THE CARD. .pb-art carries negative margins
       on the left, right and bottom, and the card does NOT clip it. The
       artefact's own frame does the clipping, one layer further in. So the
       card's outline is broken on two edges and you see a fragment of a real
       screen rather than a diagram sitting politely in a box. This is bar.md
       mechanism 5, and it is the reason nothing here uses .card-bleed —
       .card-bleed clips at the card, which is exactly what we do not want.

     · THREE MATERIALS, NOT ONE. The three artefacts are made of different
       stuff and lit from the same direction, which is what stops a scene
       reading as line-work:
          A  matte utility screen — cool, sharp, a specular sheen falling
             across the top left, falloff into the bottom right corner.
          B  manila PAPER — warm, off-white, square corners, printed rules, a
             hand-drawn tick, an ink stamp. Not a UI card, on purpose.
          C  lit glass — a rounded slab with a bright rim on its top edge and
             its own light bleeding onto the card behind it.
       Each carries ONE bright dense focal mass: the selected cell, the sheet
       itself, the message you sent two days late.

     · COLOUR: zero coloured elements. The hi-vis chip that used to sit on the
       pre-start was a SaaS badge pattern and it made the paper read as one of
       our screens, which inverts the meaning of the whole section. It is now
       an ink stamp. Three coloured elements is a ceiling, not a target.

     · MOTION: two loops, continuous, no step under 400ms.
   ========================================================================== */

/* The artefacts are wider than their cards on purpose, and at 390px the fanned
   pre-start sheets ran about 23px past the viewport — enough for a phone to pan
   sideways. Clipping the x axis here kills that for good, and has the bonus
   that on a phone the artefacts now crop against the screen edge as well.
   `clip` not `hidden`: hidden would make this a scroll container and break the
   sticky nav's ancestry. */
.pb-section { overflow-x: clip; overflow-y: visible; }

/* -- Header: h2 left, lead right, so the block spans the page instead of
      leaving half the width empty. Stacks on a phone. -------------------- */
.pb-head { display: grid; gap: var(--cb-gap-3); }
.pb-head h2 { max-width: 17ch; }
.pb-head .lead { max-width: 44ch; margin: 0; }
@media (min-width: 900px) {
  .pb-head {
    grid-template-columns: 1.05fr .95fr;
    gap: clamp(32px, 5vw, 72px);
    align-items: end;
  }
  .pb-head .lead { padding-bottom: 6px; }
}

/* -- The composition -------------------------------------------------------
   Phone and tablet: stacked, with a big row gap because every artefact hangs
   past the bottom of its card and needs somewhere to hang.
   Desktop: two across, then one wide. */
.pb-cards {
  display: grid;
  gap: 56px 24px;
  margin-top: clamp(36px, 4vw, 56px);
}
@media (min-width: 980px) {
  .pb-cards { grid-template-columns: repeat(12, 1fr); gap: 54px 24px; align-items: start; }
  .pb-a { grid-column: 1 / 8; }
  /* Dropped 44px so the two cards do not share a top line or a bottom line.
     Two aligned rectangles is only a smaller version of the three-in-a-row
     fault this section exists to break. */
  .pb-b { grid-column: 8 / 13; margin-top: 36px; }
  .pb-c { grid-column: 1 / 13; }
}

/* z-order runs left to right and top to bottom, so the spreadsheet lies over
   the pre-start card next to it rather than the other way round. */
.pb-a { z-index: 3; }
.pb-b { z-index: 2; }
.pb-c { z-index: 1; }

.pb-card {
  padding-bottom: 0;
  overflow: visible;          /* the artefact must be allowed OUT of the card */
}
.pb-card:hover { transform: none; }   /* nothing to lift; the layers do the work */
.pb-card h3 { margin-bottom: 6px; }
.pb-card p  { font-size: 15px; margin-bottom: var(--cb-gap-3); max-width: 46ch; }

/* The one class that breaks the card open. The card's padding is 24px, so
   these numbers are measured from the CONTENT box: -25 puts the artefact's
   left edge exactly on the card's left border, -44 runs it 20px past the right
   border, -34 hangs it 34px below the bottom border. */
.pb-art {
  position: relative;
  margin: 0 -44px -34px -25px;
}

.pb-frame {
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-md);
  box-shadow: var(--cb-shadow-3);
  overflow: hidden;                    /* the clipping happens HERE, not at the card */
}

/* 34px of this clears the last artefact's overhang, which hangs below its
   card's bottom border; the rest is the actual gap. Do not trim it back to a
   plain 24px — the chat panel lands on top of this line if you do. */
.pb-note {
  margin: calc(34px + var(--cb-gap-3)) 0 0;
  font-size: 12.5px; color: var(--cb-ink-3);
}

/* ---------------------------------------------------------------------------
   CARD A — the spreadsheet.  MATERIAL: matte utility screen.
   Lit from the top left: the ground lifts a few percent up there, falls to
   near graphite in the bottom right corner, and a single specular band runs
   across the top left over the top of everything. Fixed column widths totalling
   ~700px, wider than the frame at every breakpoint, so the week always runs off
   the right and TOTAL is always cut in half.
   ------------------------------------------------------------------------ */
.pb-xl {
  position: relative;
  height: 306px;
  display: grid; grid-template-rows: auto 1fr auto;
  background:
    linear-gradient(157deg,
      color-mix(in srgb, var(--cb-ink) 7%, var(--cb-ground)) 0%,
      var(--cb-ground) 44%,
      color-mix(in srgb, var(--cb-graphite) 62%, var(--cb-ground)) 100%);
}
/* The sheen. This one element is most of what separates "a lit screen" from
   "a rectangle with a border". */
.pb-xl::after {
  content: ""; position: absolute; inset: 0; z-index: 4; pointer-events: none;
  background: linear-gradient(114deg,
    color-mix(in srgb, var(--cb-ink) 16%, transparent) 0%,
    color-mix(in srgb, var(--cb-ink) 6%, transparent) 15%,
    color-mix(in srgb, var(--cb-ink) 2%, transparent) 30%,
    transparent 44%);
}

.pb-xl-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 0 12px; height: 38px;
  border-bottom: 1px solid var(--cb-line);
  background: color-mix(in srgb, var(--cb-ink) 5%, var(--cb-ground));
  font-size: 11.5px; color: var(--cb-ink-3);
}
.pb-xl-file {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10.5px; letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 46%;
}
.pb-xl-ref {
  flex: none; min-width: 42px; text-align: center;
  border: 1px solid var(--cb-line); border-radius: 4px;
  padding: 3px 6px; font-size: 10.5px; font-weight: 700; color: var(--cb-ink-2);
}
.pb-xl-fx { flex: none; font-style: italic; font-weight: 700; color: var(--cb-ink-3); }
/* The formula bar is EMPTY for the selected cell. That is the joke. */
.pb-xl-val {
  flex: 1; min-width: 60px; height: 22px;
  border-left: 1px solid var(--cb-line-soft);
}

.pb-grid {
  display: grid;
  grid-template-columns: 28px 150px repeat(6, 72px) 96px;
  align-content: start;
  overflow: hidden;
  font-size: 12.5px;
}
.pb-grid > span {
  height: 31px;
  display: flex; align-items: center;
  padding: 0 10px;
  color: var(--cb-ink-2);
  border-right: 1px solid var(--cb-line-soft);
  border-bottom: 1px solid var(--cb-line-soft);
  white-space: nowrap; overflow: hidden;
}
/* The column-letter and row-number gutters. Half of what makes a spreadsheet
   read as a spreadsheet at a glance. */
.pb-grid .col, .pb-grid .cnr, .pb-grid .rn {
  height: 22px;
  justify-content: center; padding: 0;
  background: color-mix(in srgb, var(--cb-ink) 6%, var(--cb-ground));
  font-size: 10px; font-weight: 700; color: var(--cb-ink-3);
}
.pb-grid .rn { height: 31px; }
.pb-grid .hd {
  font-size: 10px; font-weight: 800; letter-spacing: .09em;
  color: var(--cb-ink-3);
  background: var(--cb-wash);
}
.pb-grid .nm { color: var(--cb-ink); font-weight: 600; }
.pb-grid .n  { justify-content: flex-end; font-variant-numeric: tabular-nums; }

/* THE FOCAL MASS of card A. The selected cell is not an outline, it is a solid
   lit block with dark ink in it, and it throws a little light onto the cells
   around it. It is the brightest thing in this artefact and it is also the
   thing the whole card is about. */
.pb-guess {
  position: relative; z-index: 2;
  background: var(--cb-ink);
  color: var(--cb-ground);
  font-weight: 800;
  box-shadow:
    0 0 0 1px var(--cb-ink),
    0 0 26px -2px color-mix(in srgb, var(--cb-ink) 42%, transparent);
}
.pb-guess i {
  position: absolute; inset: 0 10px 0 0;
  display: flex; align-items: center; justify-content: flex-end;
  font-style: normal; opacity: 0; color: var(--cb-ground);
  animation: cb-pb-guess 7.2s var(--cb-ease) infinite;
}
.pb-guess i:nth-of-type(2) { animation-delay: 2.4s; }
.pb-guess i:nth-of-type(3) { animation-delay: 4.8s; }

/* 7% of 7.2s = ~500ms per fade. Nothing under 400ms, per bar.md. */
@keyframes cb-pb-guess {
  0%        { opacity: 0; }
  7%, 26%   { opacity: 1; }
  33%, 100% { opacity: 0; }
}

.pb-xl-tabs {
  display: flex; align-items: center; gap: 4px;
  height: 32px; padding: 0 8px;
  border-top: 1px solid var(--cb-line);
  background: color-mix(in srgb, var(--cb-graphite) 45%, var(--cb-ground));
}
.pb-xl-tabs span {
  padding: 4px 10px; border-radius: 5px 5px 0 0;
  font-size: 10.5px; font-weight: 700; color: var(--cb-ink-3);
  white-space: nowrap;
}
.pb-xl-tabs .on {
  background: var(--cb-ground); color: var(--cb-ink-2);
  box-shadow: inset 0 0 0 1px var(--cb-line);
}

/* ---------------------------------------------------------------------------
   CARD B — the pre-start.  MATERIAL: manila paper.
   Square corners, warm off-white stock, printed rules instead of UI borders,
   a hand-drawn tick instead of a vector one, and an ink stamp instead of a
   status pill. It is also the single brightest mass in the whole section,
   which is what gives the composition a centre of gravity.
   ------------------------------------------------------------------------ */
.pb-paper {
  height: 336px;
  /* One local palette, all of it mixed from brand tokens so it survives a
     change of ground. Hi-vis at 7% is what makes the stock read manila rather
     than printer white — it is a material, not a coloured element. */
  --pb-stock:  color-mix(in srgb, var(--cb-hivis) 12%, var(--cb-white));
  --pb-lit:    color-mix(in srgb, var(--cb-white) 62%, var(--pb-stock));
  --pb-shade:  color-mix(in srgb, var(--cb-graphite) 13%, var(--pb-stock));
  --pb-ink:    var(--cb-graphite);
  --pb-rule:   color-mix(in srgb, var(--cb-graphite) 26%, transparent);
  --pb-faint:  color-mix(in srgb, var(--cb-graphite) 58%, transparent);
}
.pb-leaf {
  position: absolute;
  border-radius: 2px;                    /* paper, not a card */
  background: linear-gradient(158deg, var(--pb-lit) 0%, var(--pb-stock) 52%, var(--pb-shade) 100%);
  box-shadow: var(--cb-shadow-2);
}
.pb-leaf-back  { top: 20px; left: 46px; right: -16px; bottom: -12px; transform: rotate(4.5deg); filter: brightness(.82); }
.pb-leaf-mid   { top: 10px; left: 26px; right: 2px;   bottom: -4px;  transform: rotate(-3.2deg); filter: brightness(.91); }
.pb-leaf-front {
  top: 0; left: 0; right: 34px; bottom: 16px;
  padding: 15px 16px;
  display: flex; flex-direction: column;
  transform: rotate(-.7deg);             /* nothing on a dash is square to a grid */
  box-shadow: var(--cb-shadow-3);
  color: var(--pb-ink);
  z-index: 2;
}

.pb-doc-head {
  display: grid; gap: 1px;
  padding-bottom: 9px; margin-bottom: 11px;
  border-bottom: 2px solid var(--pb-rule);   /* a printed form rule, not a hairline */
}
.pb-doc-kicker {
  font-size: 9px; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--pb-faint);
}
.pb-doc-head b { font-size: 13.5px; font-weight: 800; letter-spacing: -.01em; }
.pb-doc-meta  { font-size: 10.5px; color: var(--pb-faint); }

.pb-checks { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.pb-checks li {
  position: relative;
  display: flex; align-items: center; gap: 11px;
  font-size: 12.5px; color: var(--pb-ink);
}
/* The printed box. A 1px rule on paper, square, no radius. */
.pb-checks li::before {
  content: ""; flex: none; width: 13px; height: 13px;
  border: 1px solid var(--pb-rule);
}
/* The tick, drawn by hand. Two strokes at an angle, a rounded turn where the
   pen changes direction, and NOTHING repeated between one line and the next:
   angle, size, stroke weight, skew and ink density all differ, because a biro
   run down a list four times never puts down the same mark twice. A vector tick
   from an icon set is the thing that made this read as an app. */
.pb-checks .is-ticked::after {
  content: ""; position: absolute; left: 1px; top: 50%;
  width: 6px; height: 12px;
  border-right: 2px solid var(--pb-ink);
  border-bottom: 2px solid var(--pb-ink);
  border-bottom-right-radius: 3px;
  transform: translateY(-62%) rotate(42deg) skewX(-6deg);
  opacity: .74;
}
.pb-checks li:nth-child(2)::after {
  left: 3px; width: 8px; height: 14px;
  border-right-width: 2.4px; border-bottom-width: 1.7px;
  border-bottom-right-radius: 2px;
  transform: translateY(-56%) rotate(36deg) skewX(4deg);
  opacity: .86;
}
.pb-checks li:nth-child(3)::after {
  left: 0; width: 6px; height: 12px;
  border-right-width: 1.6px; border-bottom-width: 2.2px;
  border-bottom-right-radius: 4px;
  transform: translateY(-68%) rotate(48deg) skewX(-9deg);
  opacity: .63;
}
.pb-checks li:nth-child(4)::after {
  left: 2px; width: 8px; height: 13px;
  border-right-width: 2.6px; border-bottom-width: 2px;
  border-bottom-right-radius: 3px;
  transform: translateY(-58%) rotate(39deg) skewX(7deg);
  opacity: .8;
}

/* The empty signature block. This is what the card is actually about. */
.pb-sign { margin-top: auto; padding-top: 12px; display: grid; gap: 4px; }
.pb-sign-label { font-size: 9px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; color: var(--pb-faint); }
.pb-sign-line  { height: 20px; border-bottom: 1px solid var(--pb-rule); }

/* The stamp. Ink on paper, off square, bleeding into the stock underneath it
   via multiply. This replaces a rounded hi-vis pill with a status dot, which
   was a dashboard component sitting on a piece of paper. */
.pb-stamp {
  position: absolute; right: 13px; bottom: 15px; z-index: 3;
  padding: 5px 12px 4px;
  border: 2px solid var(--pb-ink);
  border-radius: 2px;
  font-size: 12.5px; font-weight: 800; letter-spacing: .13em;
  text-transform: uppercase; color: var(--pb-ink);
  transform: rotate(-9deg);
  opacity: .56;
  mix-blend-mode: multiply;
}

/* ---------------------------------------------------------------------------
   CARD C — the group chat.  MATERIAL: lit glass, and it is a WINDOW, not a
   stretched thread.

   Round 3 left this card as a hole: the slab was painted from --cb-ground
   (#0F1115) while the card under it is --cb-surface (#161920), so the artefact
   was literally DARKER than its own card and had no material at all. Three
   things fix it, and each one answers a measured fault:

     · MATERIAL. The slab is now built on --cb-surface-2 (#1D212B) — one step
       ABOVE the card, not below it — with the rail lifted again on top of that.
       Card #161920 → pane #1D212B → rail ~#2E323B. Three tones stepping up, so
       the object separates from the card by value, not by a 1px rim.

     · THE RAIL kills the dead width. A thread whose bubbles are 174–274px wide
       inside a 766px pane is 500px of nothing on every single row. A real chat
       client at that width has a conversation list, so it has one — and the
       list is the point of the card as well: the job is one row among fourteen
       unread.

     · THE NOTIFICATIONS kill the dead height. The caption column used to be
       four lines of type centred in ~280px of void. The Tuesday alert that went
       nowhere and the Thursday alert that finally told you now sit under the
       caption, cross the card's left border, and overlap the slab. They are
       also the card's FOCAL MASS: the lit Thursday card is the densest bright
       object here, sized to hold its own beside the manila sheet next door.

   Zero coloured elements. Every value mixes from a --cb-* token.
   ------------------------------------------------------------------------ */
.pb-c-copy { margin-bottom: var(--cb-gap-3); }
.pb-c-copy p:last-child { margin-bottom: 0; }

/* The light this screen throws. Sits behind the slab, not on it. */
.pb-c .pb-art::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  inset: -30px -24px -34px -24px;
  background: radial-gradient(58% 58% at 46% 42%,
    color-mix(in srgb, var(--cb-ink) 14%, transparent) 0%, transparent 72%);
  filter: blur(26px);
}
.pb-c .pb-frame { position: relative; z-index: 1; }

.pb-thread {
  /* Below 760px Tuesday's three lines are gone from the thread (see
     .pb-msg-wide), so the pane is sized for what is actually in it. Do not
     restore 386px without putting those lines back — an over-tall pane just
     reintroduces the empty ground this card spent three rounds losing. */
  height: 254px;
  display: grid; grid-template-columns: minmax(0, 1fr);
  border-radius: var(--cb-r-lg);
  border-color: color-mix(in srgb, var(--cb-ink) 17%, transparent);
  /* Lit from the top left like the spreadsheet and the paper, falling away
     into the bottom right — but the whole slab stays above the card's value. */
  background: linear-gradient(166deg,
    color-mix(in srgb, var(--cb-ink) 9%, var(--cb-surface-2)) 0%,
    var(--cb-surface-2) 52%,
    color-mix(in srgb, var(--cb-graphite) 30%, var(--cb-surface-2)) 100%);
  /* specular rim on the top and left edges — the "glass" cue */
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--cb-ink) 40%, transparent),
    inset 1px 0 0 color-mix(in srgb, var(--cb-ink) 13%, transparent),
    var(--cb-shadow-3);
}
.pb-pane { display: grid; grid-template-rows: auto 1fr; min-width: 0; }

/* -- The conversation rail. Desktop only: below 1000px the pane needs every
      pixel it has, and on a phone a real chat app hides the list too. ----- */
.pb-rail { display: none; }
.pb-rail-top {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  height: 44px; padding: 0 13px;
  border-bottom: 1px solid color-mix(in srgb, var(--cb-ink) 15%, transparent);
}
.pb-rail-top b { font-size: 12.5px; font-weight: 800; color: var(--cb-ink); }
.pb-rail-top span { font-size: 9.5px; font-weight: 700; letter-spacing: .04em; color: var(--cb-ink-3); }
.pb-rail-list { list-style: none; margin: 0; padding: 0; }
.pb-rail-list li {
  display: flex; align-items: center; gap: 9px;
  padding: 10px 11px;
  border-bottom: 1px solid color-mix(in srgb, var(--cb-ink) 8%, transparent);
}
/* The open conversation. A step lighter again, with a lit edge on the left. */
.pb-rail-list .on {
  background: color-mix(in srgb, var(--cb-ink) 8%, transparent);
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--cb-ink) 62%, transparent);
}
.pb-rw { min-width: 0; flex: 1; display: grid; gap: 1px; }
.pb-rw b {
  font-size: 11px; font-weight: 700; color: var(--cb-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pb-rw span {
  font-size: 10px; color: var(--cb-ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pb-rail-meta { flex: none; display: grid; gap: 4px; justify-items: end; }
.pb-rail-meta em { font-style: normal; font-size: 9px; color: var(--cb-ink-3); }
.pb-unread {
  min-width: 16px; height: 16px; padding: 0 5px;
  border-radius: var(--cb-r-pill);
  background: color-mix(in srgb, var(--cb-ink) 82%, transparent);
  color: var(--cb-ground);
  font-size: 9px; font-weight: 800; font-style: normal;
  display: grid; place-items: center;
}

.pb-thread-top {
  display: flex; align-items: center; gap: 10px;
  position: relative; z-index: 2;
  height: 44px; padding: 0 15px;
  border-bottom: 1px solid color-mix(in srgb, var(--cb-ink) 15%, transparent);
  background: color-mix(in srgb, var(--cb-ink) 6%, var(--cb-surface-2));
}
.pb-thread-top b { font-size: 13px; font-weight: 700; color: var(--cb-ink); }
.pb-thread-sub { font-size: 11px; color: var(--cb-ink-3); }

.pb-av {
  flex: none; width: 23px; height: 23px; border-radius: 50%;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--cb-ink) 15%, var(--cb-surface-2));
  border: 1px solid color-mix(in srgb, var(--cb-ink) 15%, transparent);
  font-size: 8.5px; font-weight: 800; color: var(--cb-ink-2);
}
.pb-av-grp { width: 27px; height: 27px; font-size: 9.5px; }

/* The negative top margin slides the thread up under the header, so the oldest
   message on screen is cut in half. Mid-thread, like the real thing. */
.pb-msgs { padding: 13px 15px 0; margin-top: -28px; overflow: hidden; }

/* Day dividers. These carry the whole card: the Tuesday problem is discussed
   on Thursday. */
.pb-day { display: flex; align-items: center; gap: 12px; margin: 2px 0 9px; }
.pb-day::before, .pb-day::after {
  content: ""; flex: 1; height: 1px;
  background: color-mix(in srgb, var(--cb-ink) 13%, transparent);
}
.pb-day span {
  font-size: 9.5px; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase; color: var(--cb-ink-3);
}

.pb-msg { display: flex; align-items: flex-end; gap: 9px; margin-bottom: 7px; }
/* MUST stay after .pb-msg — same specificity, so source order decides.
   The three messages and the day divider carrying this class are DESKTOP ONLY.
   Below 760px the notification stack above the pane already says Tuesday, so
   showing Tuesday in the thread as well made card C repeat itself word for word
   and cost it a third scroll beat that cards A and B do not have. Turned back
   on in the breakpoint block. */
.pb-msg-wide,
.pb-day-wide { display: none; }
.pb-bub {
  max-width: 78%;
  background: color-mix(in srgb, var(--cb-ink) 11%, var(--cb-surface-2));
  border: 1px solid color-mix(in srgb, var(--cb-ink) 13%, transparent);
  border-radius: 4px 14px 14px 14px;
  padding: 6px 11px 6px;
  font-size: 12.5px; line-height: 1.42;
  color: color-mix(in srgb, var(--cb-ink) 86%, transparent);
}
.pb-bub b {
  display: block; margin-bottom: 1px;
  font-size: 9px; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase; color: var(--cb-ink-3);
}
.pb-bub em { font-style: normal; font-size: 10.5px; color: var(--cb-ink-3); margin-left: 9px; }

/* THE FOCAL MASS of card C. Your own message is a solid lit block with dark
   ink in it, the way every messaging app renders the one you sent — and it is
   the question that arrived two days too late. */
.pb-me { flex-direction: row-reverse; }
.pb-me .pb-bub {
  background: var(--cb-ink);
  border-color: var(--cb-ink);
  border-radius: 14px 4px 14px 14px;
  color: var(--cb-ground);
  box-shadow: 0 8px 22px -10px color-mix(in srgb, var(--cb-ink) 55%, transparent);
}
/* Same declaration, same 55%, same fault as the lit notification's timestamp:
   ground at 55% ALPHA over near-white composited to 4.07:1. Opaque and darker,
   which is the one other lit surface in this card. Measured 5.13:1. */
.pb-me .pb-bub b,
.pb-me .pb-bub em { color: color-mix(in srgb, var(--cb-ground) 62%, var(--cb-ink)); }

/* Bazza is STILL typing about it on Thursday. 2.4s loop, ~530ms per step. */
.pb-typing { display: flex; align-items: center; gap: 6px; padding: 12px 13px; }
.pb-typing i {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--cb-ink-3); opacity: .35;
  animation: cb-pb-type 2.4s var(--cb-ease) infinite;
}
.pb-typing i:nth-child(2) { animation-delay: .4s; }
.pb-typing i:nth-child(3) { animation-delay: .8s; }
@keyframes cb-pb-type {
  0%, 100% { opacity: .35; transform: translateY(0); }
  22%      { opacity: 1;   transform: translateY(-3px); }
  44%      { opacity: .35; transform: translateY(0); }
}

/* -- The lock-screen notifications ----------------------------------------
   These run at EVERY width. The lit Thursday card is card C's focal mass, and
   at phone width it is the ONLY one — the rail is gone and the sent bubble is
   too small to carry a card on its own. Hiding these on a phone is what left
   card three as a flat dark rectangle at the end of a band whose first two
   cards each land a big bright artefact.

   Phone: the stack sits between the caption and the pane, crosses the card's
   left border by the card's own 25px of padding, and runs 20px past the right
   one, so it breaks the card outline on both sides the way the artefacts in
   cards A and B do. The middle notification drops below 760px so the stack
   says exactly what the phone thread says: Tue 06:20 and Thu 10:52. */
.pb-alerts {
  display: grid; gap: 13px;
  position: relative; z-index: 4;
  margin: 4px -44px 26px -25px;   /* the same -25 / -44 as .pb-art */
}
.pb-alerts .pb-alert:nth-child(2) { display: none; }
.pb-alert {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 13px 15px;
  border-radius: 18px;
}
.pb-alert-icon {
  flex: none; width: 30px; height: 30px; border-radius: 9px;
  display: grid; place-items: center;
  font-size: 9.5px; font-weight: 800; letter-spacing: .02em;
}
.pb-alert-body { min-width: 0; flex: 1; display: grid; gap: 4px; }
.pb-alert-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.pb-alert-head b { font-size: 10.5px; font-weight: 800; letter-spacing: .03em; }
.pb-alert-head em { flex: none; font-style: normal; font-size: 9.5px; font-weight: 700; }
.pb-alert-msg { font-size: 12.5px; line-height: 1.35; }

/* Tuesday. It came in, it went nowhere. Dimmer, and it sits behind.

   CONTRAST, and why this is built the way it is. Round 5 measured this card's
   timestamps at 2.28:1 and its message text and avatar at 4.24:1, all under the
   4.5:1 floor. The obvious fix — brighten everything until it passes — would
   have destroyed the one thing the brief critic called the fastest read in the
   card: Tuesday faded, Thursday lit.

   So the DIMNESS MOVED FROM THE INK TO THE SURFACE. The card is darker than it
   was (ink 13% → 8% into surface-2), which makes it recede further from the lit
   Thursday card than it did before, and every piece of type on it is then free
   to be bright enough to read. It is quieter AND legible, because what makes it
   quiet is now the paper it is printed on, not the ink.

   Every colour here is OPAQUE — mixed with a token, never with `transparent` —
   so the contrast a critic measures is the contrast this file declares. A
   half-alpha ink over a half-alpha plate is a number nobody can check by
   reading the CSS. */
.pb-alert-old {
  background: color-mix(in srgb, var(--cb-ink) 8%, var(--cb-surface-2));
  border: 1px solid color-mix(in srgb, var(--cb-ink) 15%, transparent);
  box-shadow: var(--cb-shadow-2);
  transform: rotate(-1.3deg);
  /* the message line. 8.2:1 */
  color: color-mix(in srgb, var(--cb-ink) 90%, var(--cb-surface-2));
}
/* Nothing on a lock screen lines up either. */
.pb-alert-old + .pb-alert-old { transform: rotate(.9deg); }
/* A dark app-icon plate, the same shape the lit card carries, so the two read
   as the same object twice. 6.0:1 for the initials on it. */
.pb-alert-old .pb-alert-icon {
  background: color-mix(in srgb, var(--cb-graphite) 55%, var(--cb-surface-2));
  color: var(--cb-ink-2);
}
.pb-alert-old .pb-alert-head b  { color: var(--cb-ink); }              /* 12.4:1 */
.pb-alert-old .pb-alert-head em { color: var(--cb-ink-2); }            /* 5.0:1  */

/* THE FOCAL MASS of card C. Thursday, lit — a phone screen at full brightness
   in a dark card. Same recipe as the E2 cell and the sent message: solid ink
   surface, ground-coloured type on it. It is the biggest bright mass on this
   card and it is the moment the card is about. */
.pb-alert-now {
  background: var(--cb-ink);
  color: color-mix(in srgb, var(--cb-ground) 88%, var(--cb-ink));
  transform: rotate(.7deg);
  box-shadow:
    0 20px 44px -16px color-mix(in srgb, var(--cb-graphite) 92%, transparent),
    0 0 34px -8px color-mix(in srgb, var(--cb-ink) 30%, transparent);
}
.pb-alert-now .pb-alert-icon {
  background: color-mix(in srgb, var(--cb-ground) 88%, var(--cb-ink));
  color: var(--cb-ink);
}
.pb-alert-now .pb-alert-head b  { color: var(--cb-ground); }                               /* 17.4:1 */
/* Was ground at 55% ALPHA, which composited to 4.05:1. Opaque, and darker. */
.pb-alert-now .pb-alert-head em { color: color-mix(in srgb, var(--cb-ground) 62%, var(--cb-ink)); }
.pb-alert-now .pb-alert-msg     { color: color-mix(in srgb, var(--cb-ground) 88%, var(--cb-ink)); }

/* ---------------------------------------------------------------------------
   BREAKPOINTS
   Phone first. At 390px the artefacts keep their real size and simply crop
   harder — shrinking them to fit is what turns a screen into a decoration.
   ------------------------------------------------------------------------ */

/* Card C goes sideways once there is width for it: caption top left, the two
   notifications under it, thread on the right still running past the card's
   right and bottom borders.

   The copy is TOP aligned, not centred. Centring it was what produced the void
   the round-3 critic measured — 136px of nothing above the heading and 112px
   below the paragraph — because a 130px caption was being centred against a
   352px artefact. The caption now starts on the artefact's top line and the
   notifications occupy the rest of the column. */
@media (min-width: 760px) {
  .pb-c {
    display: grid;
    grid-template-columns: minmax(0, .32fr) minmax(0, .68fr);
    grid-template-rows: auto 1fr;
    column-gap: clamp(28px, 3.4vw, 48px);
    align-items: start;
  }
  .pb-c-copy { grid-column: 1; grid-row: 1; margin-bottom: 0; }
  .pb-c-copy p { max-width: 29ch; }
  .pb-c .pb-art { grid-column: 2; grid-row: 1 / 3; margin-left: 0; }

  /* Sits on the card's LEFT border (-25px, the card's own padding) and runs
     right, under the slab. A fourth place where something crosses a container
     edge, and the only one on this card's left side. */
  .pb-alerts {
    display: grid; gap: 18px;
    grid-column: 1; grid-row: 2;
    position: relative; z-index: 4;
    /* -25px is the card's own padding, so the stack starts exactly on the
       card's left border. The right side is the column gap plus a fixed 16px,
       so the overlap onto the slab stays 16px at every width instead of
       growing as the gap shrinks. */
    margin: 24px calc(-16px - clamp(28px, 3.4vw, 48px)) 0 -25px;
  }
  /* The middle notification comes back once the column is tall enough for it. */
  .pb-alerts .pb-alert:nth-child(2) { display: flex; }

  /* Tuesday comes back into the thread, and needs somewhere to go. */
  .pb-thread { height: 440px; }
  .pb-msg-wide { display: flex; }
  .pb-day-wide { display: flex; }
}

/* The rail only appears once the pane can spare 216px and still keep every
   message on one line. If you lengthen any message in here, re-check this
   width: a wrapped bubble pushes the typing dots off the bottom of the slab,
   and they are card C's only motion. Below this the thread uses the full slab,
   exactly as it did before. */
@media (min-width: 1000px) {
  .pb-thread { grid-template-columns: 216px minmax(0, 1fr); }
  .pb-rail {
    display: block;
    overflow: hidden;
    border-right: 1px solid color-mix(in srgb, var(--cb-ink) 15%, transparent);
    background: linear-gradient(180deg,
      color-mix(in srgb, var(--cb-ink) 9%, var(--cb-surface-2)) 0%,
      color-mix(in srgb, var(--cb-ink) 5%, var(--cb-surface-2)) 100%);
  }
}

/* Cards A and B go sideways too in the band where .pb-cards is still one
   column but the page is wide. Without this the artefact sits in a puddle. */
@media (min-width: 760px) and (max-width: 979px) {
  .pb-a, .pb-b {
    display: grid;
    grid-template-columns: minmax(0, .42fr) minmax(0, .58fr);
    column-gap: clamp(32px, 4vw, 56px);
    align-items: center;
  }
  .pb-a h3, .pb-b h3 { grid-column: 1; align-self: end; }
  .pb-a > p, .pb-b > p { grid-column: 1; align-self: start; margin-bottom: 0; }
  .pb-a .pb-art, .pb-b .pb-art { grid-column: 2; grid-row: 1 / 3; margin-left: 0; }
}

@media (max-width: 520px) {
  /* Narrower columns so THURSDAY — the cell that carries the whole animation —
     still lands on screen before the sheet runs off the edge. Friday is the one
     that crops. Phone first means the point has to survive here first. */
  .pb-grid { grid-template-columns: 24px 100px repeat(6, 54px) 76px; }
  .pb-xl { height: 292px; }
  .pb-paper { height: 344px; }
  .pb-thread { height: 260px; }   /* the typing dots must not crop; they are card C's only motion */
  .pb-art { margin: 0 -44px -30px -25px; }
  .pb-bub { max-width: 84%; }
  .pb-leaf-front { right: 26px; }
  .pb-stamp { right: 8px; }
}

/* Reduced motion. The global block kills the animations, but both loops START
   at or near opacity 0, so without this the E2 cell would come out EMPTY and
   the typing dots nearly invisible. Freeze them on the frame that carries the
   meaning: the "?" and three visible dots. */
@media (prefers-reduced-motion: reduce) {
  .pb-guess i { opacity: 0; }
  .pb-guess i:nth-of-type(3) { opacity: 1; }
  .pb-typing i { opacity: .8; }
}

/* ============================================================================
   WHAT IT DOES  —  index.html, "Four things, done properly"
   ----------------------------------------------------------------------------
   Everything here is prefixed .wt- so it cannot collide with .pb- above it or
   with anything else. It touches no shared class; it only USES .card.

   THIS SECTION IS THE OPPOSITE OF THE ONE ABOVE IT. The band above is the mess
   BEFORE CrewBase, so its artefacts are deliberately not product UI. This is
   what CrewBase DOES, so every artefact here IS product UI — bar.md mechanism
   4, and what Stripe does below its own fold. Each is marked SWAP POINT in the
   markup with the real screenshot that replaces it.

   AND IT IS ON WHITE, WHICH CHANGES THE RULES. Everything the band above
   taught had to be re-thought:

     · DEPTH ON WHITE is a soft shadow, a hairline, and a real step in value
       between surfaces. Heavy shadow looks cheap here. So each frame carries
       --cb-shadow-1 (the hairline-tight one) plus --cb-shadow-3 (long, diffuse,
       -20px spread) and nothing in between.
     · THE SURFACES MUST STEP THE RIGHT WAY. The card is --cb-surface #F7F8FA
       and the artefact frame inside it is --cb-surface-2 #FFFFFF, so the card
       recedes and the artefact is the lit thing. Getting this backwards is what
       cost the section above two rounds: it painted an artefact from
       --cb-ground, which inside a band is DARKER than --cb-surface, so the
       object was a hole and every highlight added to it did nothing.
     · THE COLOUR BUDGET IS TIGHTER. #6D28D9 on #FFFFFF is a solid saturated
       mass where the same idea on near-black read as a soft highlight. So this
       section spends ONE, against a budget of three: the --cb-hivis-deep edge on
       card 2's unsigned row (see .wt-brow.is-out). No violet anywhere.
       ⚠️ THIS SAID "ZERO" UNTIL ROUND 8 and was wrong from the round the amber
       edge went in. The round-7 System critic caught the drift while passing the
       section. A comment that overstates the budget is worse than no comment: it
       reads as headroom, and the next person spends against it.

   FOUR MATERIALS, because four things made of the same flat grey read as coded:

     1  MATTE UTILITY SCREEN. Cool, hairline-ruled, an inset white lip on the
        top edge and a falloff into a graphite-tinted bottom right corner.
     2  DEVICE GLASS. A graphite handset with a bright screen in it and a
        specular streak laid over the whole slab in `screen` blend, so it
        lightens the bezel and does nothing at all to the white screen — which
        is what light actually does.
     3  RECESSED INSTRUMENT. A light panel with a dark hour-meter well PRESSED
        INTO it: inner shadow above, a 1px white lip below. It is the only
        surface on the page that reads as pushed in rather than raised.
     4  BACKLIT BOARD. The live view as it sits on an office wall. It carries
        its OWN local dark palette (--wt-b-*), all of it mixed from brand
        tokens, exactly the way .pb-paper carries its manila one. It is NOT a
        .band and must never be given that class: a fourth full-width band
        breaks BRAND.md line 57.

   ONE DENSE FOCAL MASS EACH: the lit "on the clock" row, the signature and the
   graphite Signed bar under it, the hour-meter well, the floating event feed.

   MOTION: three loops, continuous, nothing under 400ms.
   ========================================================================== */

/* Same reason as .pb-section: the artefacts are wider than their cards on
   purpose, and a phone must never be able to pan sideways. `clip`, not
   `hidden` — hidden would make this a scroll container and break the sticky
   nav's ancestry. */
.wt-section { overflow-x: clip; overflow-y: visible; }

/* THE LABEL INK, AND WHY THIS SECTION NEEDS ITS OWN.
   --cb-ink-3 is #6B7284, which measures 4.81:1 on straight white and passes.
   But the labels inside these two frames do not sit on straight white: the
   timesheet and the plant record are both painted with a gradient that falls
   off into a graphite tint, and their bars sit on `--cb-ink 4%`. Measured on the
   live page, seven labels landed between 4.15:1 and 4.47:1 — under AA, all of
   them, and the darkest were the three service-history rows sitting deepest in
   the plant frame's falloff.

   AC#2 in findings/accessibility-contrast.md exempts text inside artefacts, and
   this section may NOT use it. That exemption is written for pictures of OTHER
   PEOPLE'S software — the spreadsheet and the chat app in the band above, which
   are depictions of a mess. These artefacts are pictures of CrewBase's own
   product. A supervisor reading this section is reading our UI, so it is held to
   the same bar as the page around it.

   The fix is 14% of --cb-ink mixed into --cb-ink-3, for these labels only:

       --cb-ink-3   #6B7284   4.15 - 4.47:1   FAILS
       --wt-label   #5F6576   5.00 - 5.45:1   passes, measured at 390px and at
                                              desktop, worst case being the
                                              third history row sitting deepest
                                              in the plant gradient's falloff

   Re-measured every text node inside #what, most recently after round 4's
   composition work: 150 at desktop and 127 at 390px, ZERO below AA, worst case
   4.53:1. The section does not rely on AC#2 at all. The measurement that matters
   samples the GRADIENT at each label's own position — reading the panel's base
   colour instead gives numbers that are wrong by half a point.

   Deliberately NOT done two other ways. Changing --cb-ink-3 itself would move
   every muted caption on every page to fix seven labels in one section, and that
   token was already given a second value last session for a different reason.
   Lifting the frames' gradients would flatten the falloff that makes them read
   as a lit material rather than flat grey. Both ends of this mix are tokens, so
   it still inverts correctly if this section ever moves into a band. */
.wt-section { --wt-label: color-mix(in srgb, var(--cb-ink-3) 86%, var(--cb-ink)); }

/* -- The composition --------------------------------------------------------
   Phone: stacked, big row gap because every artefact hangs past the bottom of
   its card and needs somewhere to hang.
   Desktop: WIDE / narrow, then narrow / WIDE, with the offset on the right in
   row one and on the left in row two. Four equal boxes in a 2x2 is the single
   loudest template tell on a page, and simply copying the band above's "two
   across then one wide" would have been the second loudest. */
/* PACE. Round 4: the section ran 1779px at 1440 and 2861px at 390 — 2.3 desktop
   screens and 3.2 phone screens for four features, against roughly 345px per
   feature on supabase.com. Every number below came down, and each one is air
   that was carrying nothing:

       row gap        76 -> 48      the artefacts hang 30 into it, so the real
                                    clear space was 46px, not 76
       margin-top     56 -> 36      at desktop; 36 -> 24 at 390
       padding-bottom 88 -> 44      measured: the deepest hang below the grid's
                                    own bottom line is card three's artefact at
                                    33px and card four's feed at 27px

   The artefacts themselves came down separately, each one noted on its own rule.
   What did NOT come out: the copy, the captions, the four materials, and the
   four focal masses. Height is not density and four cramped cards would be a
   worse section than four tall ones. */
.wt-cards {
  display: grid;
  gap: 48px 24px;
  margin-top: clamp(24px, 3vw, 36px);
  padding-bottom: 44px;
}
@media (min-width: 980px) {
  .wt-cards { grid-template-columns: repeat(12, 1fr); gap: 52px 24px; align-items: start; }
  .wt-1 { grid-column: 1 / 8;  grid-row: 1; }
  .wt-2 { grid-column: 8 / 13; grid-row: 1; margin-top: 36px; }
  .wt-3 { grid-column: 1 / 6;  grid-row: 2; margin-top: 36px; }
  .wt-4 { grid-column: 6 / 13; grid-row: 2; }

  /* Card two's office list has to reach further left than it used to. Card one's
     timesheet no longer hangs 19px past its own right border — it is CROPPED at
     that border now (see .wt-1's clip-path below) — so at the old left:-16px the
     list stopped 4px short of card one and the two cards no longer touched. The
     overlap defended in the z-index note below is a real piece of the
     composition, so the list moves out to meet it: measured 16px of overlap at
     1440 and 12px at 980. Scoped to >=980 on purpose. Below that the cards are
     full width and -36px would put the list off the left of the screen. */
  .wt-2 .wt-behind { left: -36px; }

  /* ══ THE WEEK COLUMN IS CUT BY CARD TWO, NOT BY CARD ONE ═══════════════════
     Round 5. Round 4 believed card one's own clip severed each weekly total to
     a clean "31:". It does not, and the belief cost a round: two critics read
     these same pixels and drew opposite conclusions. Walking every pixel of
     each total cell with elementFromPoint settled it —

         cell    width   visible   what covers the rest
         31:01    84px    44.7px   .wt-behind   (card two)
         31:57    84px    45.7px   .wt-behind
         29:14    84px    45.7px   .wt-behind
         32:57    84px    45.7px   .wt-brow
         29:58    84px    45.7px   .wt-brow
         29:07    84px    46.7px   .wt-brow.is-out

     Card one's clip lands at 59px into the cell, which WOULD read "31:". Card
     two's office list starts 15.7px earlier and gets there first. The digits are
     right-aligned at 39.3-73.0, so the ~45px that survived held the "3" and
     nothing else: a stray, meaningless digit under a header showing a 2.8px stub
     of its "W".

     THE INTERLOCK IS NOT THE FAULT AND IS NOT TOUCHED. Craft rated card two's
     list lying over card one the best crossing on the page, and moving
     .wt-behind back to clear the totals would cost the whole 16px overlap at
     1440 and turn it into a gap at 980. So the CUT LINE STAYS EXACTLY WHERE IT
     IS and the TYPE MOVES to meet it: the numerals slide left until the cut
     lands just past the colon, and each total reads as a severed "31:" flush
     against the edge of the thing severing it.

     The cell keeps its full 84px, so .wt-tot's wash plate and the lit row's
     graphite still run under card two's list and are still cut by it. Only the
     glyphs move.

         .wt-tot   27px, and it was measured against the COLON'S INK, not
                   against its advance box. Those are not the same and the
                   difference decided the value: the colon's advance is 4.90px
                   but its two dots only occupy 1.0-4.0 of it, so a cut inside
                   the advance can still leave the glyph whole. The list's
                   -1.4deg tilt staggers the cut down the six rows, and the cut
                   also moves with the viewport, so both were walked at 0.05px:

                       row, at 1440      980-1179   1180+
                       31:01 (the lit)     42.80     43.85   <- earliest cut
                       31:57               42.80     44.75
                       29:14               42.80     44.75
                       32:57               42.80     44.75
                       29:58               43.10     45.55
                       29:07               43.10     46.40

                   At 24px the colon's ink ran 41.70-44.70 and the LIT row cut
                   at 43.85 — 0.85px into the dots, on the one row that is the
                   card's focal mass. 27px puts the ink at 38.70-41.70, which
                   clears the WORST cut in either band (42.80, at 980) by 1.10px,
                   so every colon is whole from 980 all the way up. The cost is
                   at the other end: the latest-cut row shows a couple of px of
                   the following digit. That is the crop working, not a fault —
                   a fragment says the value carries on under the list, which is
                   the whole point. A stray "3" said nothing.

                   ONE VALUE COVERS BOTH BANDS ONLY BECAUSE IT WAS CHECKED IN
                   BOTH. The cut is ~1px later at 980-1179 than at 1440 because
                   the overlap is 12px there against 16px at 1440. Re-measure in
                   both if .wt-behind's offset ever moves.
         .wt-wk    NOT CROPPED AT ALL — it is kept WHOLE and pushed left of
                   whatever the cut is. See the block by .wt-ts-grid for why
                   cropping the head was abandoned.

     SCOPED TO >=980 DELIBERATELY. Below it the cards are full width, card two's
     list is nowhere near card one, and card one's OWN clip does the severing at
     the card border — measured at 390 it already lands 0.4px past the colon and
     renders "31:" with no help. Applying this padding there would shove the
     numerals left of a cut that is not there. */
  /* ══ LD#8 + LD#9, ROUND 7 — THE WEEK TOTALS ARE NO LONGER CROPPED AT ALL ══════
     Adam's call, 2026-08-16. Everything above this describes what 27px USED to do
     and WHY, and it is kept deliberately: this reverses it, and the next person
     needs to know it was a decision and not an oversight.

     WHY IT WAS REVERSED. 27px was measured against the COLON'S INK so the cut
     landed just past it and each row read as a severed "31:" plus a fragment of
     the next digit. That was sound while a fragment was read as "the value
     carries on under the list". Two rounds of critics did not read it that way:

       round 5 Craft   the week totals are illegible at phone width
       round 6 Brief   same finding, independently, one round later
       round 6 Craft   LD#8 — row 2's fragment is a 7 cut at its diagonal and the
                       remnant reads as a SLASH. "31:5/" is not a number carrying
                       on under an edge, it is a rendering fault.
       round 6 Craft   LD#9 — row 4's cut numeral terminates at the same pixel as
                       the amber TP09 accent, so the section's ONE colour event
                       and a truncation share an edge.

     Three separate findings, one cause. The card's paragraph promises "timesheets
     build themselves as the week goes" and this column is the proof of it, so it
     is the wrong thing in the section to make the reader decode. bar.md's finding
     on Supabase applies exactly: a crop may land on a surface, never on a letter.

     43px, and it is NOT a new measurement — it is .wt-wk's existing value. Round 5
     already pushed the WEEK head fully clear of this same cut and landed on 43px.
     Matching it does three things at once: the totals clear the cut, they align
     with their own column head again, and the column stops carrying two different
     ideas about where its right edge is.

     Verified at 1440 against card two's chip edge, which staggers 0.88px per row
     from the list's -1.4deg tilt (789.45, 790.45, 791.33, 792.21, 793.09, 793.97):

         all six text runs end at 786.33
         tightest clearance 3.12px, on row 1, which is the round-6 house minimum
         loosest 7.64px, on row 6
         text left edge 752.63 against a cell starting at 745.33 — 7.3px spare,
         so the fixed 84px column cannot be widened by this and nothing reflows

     THE CROP MECHANISM IS NOT LOST, only moved off the type. The cell keeps its
     full 84px, so .wt-tot's wash plate and the lit row's graphite still run under
     card two's list and are still severed by it, and card one's own clip still
     cuts the panel at 804.33. What changed is that the cut now lands on surfaces
     and never on a numeral.

     ⚠️ ROUND 8 — 43px BECOMES 49px, AND THIS IS THE ONE PLACE THE +8 DOES NOT FIT.
     Every other value took LD#16's full +8 to reach an 11px gutter. Here the
     track is a fixed 84px and the totals measure 33.70px, so the ceiling is
     84 - 33.70 = 50.30px of padding before the text runs out the LEFT side of its
     own border box and gets clipped there — a brand new severance at the opposite
     end, which is exactly the trap this round exists to close. 49px takes the
     gutter to ~9.1px and leaves 1.3px of headroom for font variation.
     ⛔ Do not "finish the job" by setting 51px here. Anything above 50 must widen
     the track first, and the track is load-bearing: `repeat(4, 64px) 84px` is what
     keeps the wash plate running under card two. Measure, do not assume. */
  .wt-1 .wt-tot { padding-right: 49px; }

}

/* ══ CARDS 1 AND 3 ARE CROPPED, NOT OVERHUNG ═══════════════════════════════════
   Round 4, and this is the change that buys the most depth for the least ink.

   Until now every card in this section did the same thing: the artefact ran ~20px
   past one border and stopped. Nothing was ever severed, so each panel read as
   slightly too wide for its card rather than as a window onto something larger.
   Supabase's Authentication card is the counter-example — its email chips are cut
   mid-string by the card's own border, so you read a fragment and your eye
   supplies the rest of the table.

   THE CROP HAPPENS AT THE OUTER CARD BORDER. That is the only place it counts; a
   crop inside .wt-frame is the artefact clipping its own contents and cost this
   project a whole round on the section above. .wt-card has to keep
   `overflow: visible` — cards 2 and 4 need their handset and their feed to get
   OUT — so the clip is a one-sided clip-path, and the artefact is given enough
   extra margin to have something to lose:

       card 1   frame runs 26px past the card's RIGHT border, clipped there.
                The WEEK column is right-aligned, so every weekly total is cut
                mid-value: 31:01 renders as "31:", and the column head as "W".
                The lit row's total goes with them, which is what makes the
                focal mass read as continuing off the card.
       card 3   frame runs 40px past the card's LEFT border, clipped there.
                "Plant register" loses its first six characters, the EX12 plate
                loses its front half, and the recessed hour-meter well loses its
                left corner — so the instrument reads as set into a panel that
                carries on past the card.

   Both crops are the same at 390px as at 1440px, because the clip line is the
   card's own border box and the artefact's overshoot is a fixed margin. Neither
   can produce horizontal scroll: the clip removes the overshoot entirely.

   COMPREHENSIBLE IS THE LIMIT. 40px on card 3 was measured against the machine
   name: "Excavator 12 — CAT 320" starts 68px into the frame, so it survives whole
   and the reader always knows which machine they are looking at. Push either
   number further and the artefact stops being readable, which is a worse fault
   than a shallow crop.

   THE CLIP GOES ON .wt-art, NOT ON THE CARD, and that is not a style preference.
   The card is an <a>, and :focus-visible draws a 2px ring at 3px offset OUTSIDE
   its border box. A clip-path on the card cuts that ring off on the cropped side
   and a keyboard user loses a third of their focus indicator. Clipping the
   artefact instead lands the same line in the same place — .wt-art's own edge is
   exactly the card border plus the overshoot, so the inset is that overshoot —
   and the card keeps no clip at all, so the ring is whole. */
.wt-1 .wt-art { margin-right: -51px; clip-path: inset(-600px 26px -600px -600px); }
.wt-3 .wt-art { margin-right: -25px; margin-left: -65px; clip-path: inset(-600px -600px -600px 40px); }

/* ══ CARD 4 IS CROPPED TOO, AND THE CLIP GOES ON THE BOARD ═════════════════════
   Round 5. Card four was the last artefact on the page that was never severed by
   anything: at desktop the board ran 19px past the card's right border and just
   STOPPED, rounded corner intact, so it read as a panel slightly too wide for its
   card rather than as a window onto a bigger board. Craft named it; the round-4
   builder had already named it as its own least-confident call.

   THE CLIP IS ON .wt-board, NOT ON .wt-art, AND THAT IS THE WHOLE TRICK HERE.
   Cards 1 and 3 clip .wt-art because everything inside them is meant to be cut on
   that side. Card four is different: .wt-feed is an absolutely-positioned SIBLING
   of the board inside .wt-art, it is the card's focal mass, and it is supposed to
   float out past the card's border uncut. A clip on .wt-art would take 37px off
   its right-hand end at desktop and kill the one crossing this card has.

   Clipping the board alone gives the card BOTH moves at once, which no other card
   on the page does: a hard crop where the board meets the card's border, and a
   white panel lying across that same border in front of it. The severed board is
   what the feed now reads as floating over.

   The card's :focus-visible ring is untouched either way — it is drawn on the <a>,
   and this clips a grandchild div.

   THE INSET IS THE OVERSHOOT: .wt-art's margin-right minus .wt-card's 25px of
   padding, so the clip line lands exactly on the card's border box.
       44 - 25 = 19px   at every width down to 521
       74 - 25 = 49px   at <=520, where .wt-4 > .wt-art is pushed out to -74px
   Verify after any change to either number by asserting the board's rendered
   right edge equals the card's, not by eye. */
.wt-4 .wt-board { clip-path: inset(-600px 19px -600px -600px); }

/* ══ ROUND 6 — THE CROP RULE, STATED ONCE ══════════════════════════════════════
   ⛔ A CROP MAY SEVER GRAPHIC MATTER, SURFACES AND NUMERALS.
      IT MAY NOT SEVER RUNNING TEXT.

   This is the rule rounds 2 through 5 kept rediscovering one card at a time and
   never wrote down. Round 2 fixed `OUR METER` and `0 h service` on card 3. Round 5
   fixed `34 on sit` on card 4 and held the WEEK head clear on card 1. Each was
   treated as that card's own bug. They are one mechanism landing wrong wherever it
   meets type, which is also why LD#1 read as a fault in the 521-979px band when it
   was never about the band at all.

   WHY, so nobody softens it back: Supabase — the site this whole crop move is
   borrowed from — never lets a crop land on a letter. Its fragments cut through
   table rows, chart bodies, chrome and gutter, all matter with no correct ending,
   so the eye reads a plane continuing behind an edge. A severed word is different
   because the eye completes it automatically and then registers the missing letter
   as damage. `Export to payrol` is not depth. It is a typo.

   NUMERALS ARE STILL FAIR GAME, WITH ONE EXCEPTION: a severed value must keep at
   least one digit after its separator. `31:0` reads as a number continuing under
   the edge. `31:` reads as missing data, because a trailing separator promises a
   value and then does not deliver one.

   HOW IT IS ENFORCED: the cut lines do not move. Every one of them was measured
   and argued for in the blocks above and they are load-bearing. The TYPE moves to
   clear them, which is exactly what round 5 did to the WEEK head, and that retreat
   was the correct instinct. Each value below is the measured overshoot past its
   own cut plus a gutter.

   ⚠️ ROUND 8 — THE GUTTER WENT FROM 3px TO 11px. EVERY VALUE BELOW GAINED 8px.
   Round 6 set the gutter to 3px, which was the "house minimum", and round 7 hit
   it exactly everywhere. The round-7 Craft critic failed the section anyway and
   was right: 3px is the ambiguous middle. It is too much to read as a deliberate
   severance and too little to read as room, so a cropped panel reads as a box
   too small for its contents. 11px is not a bigger minimum — it is the SAME
   gutter the timesheet grid already gives every cell on its left, so the type
   now sits on the card's own rhythm instead of grazing a cut.
   ⛔ Do not restate this as a minimum. A minimum is satisfied by landing on it,
   and landing on it is the failure.

   ⚠️ MEASURED AT BOTH JUDGED WIDTHS, AND THE OVERSHOOT IS THE SAME AT BOTH — the
   artefacts are cut by their own clip-path at a fixed inset from the card border,
   so 390 and 1440 overshoot identically. That is why these are NOT scoped to a
   breakpoint. Re-measure at both if any inset changes.

       element        text                 over cut   shift (r6 → r8)
       .wt-ts-live    "On the clock"          11px     14px → 22px
       .wt-ts-act     "Export to payroll"      2px      5px → 13px  ⬅ NOT set here
       .wt-b-tot      "34 on site"            21px     24px → 32px
       .wt-b-sub      "N machines out"      2-4px       7px → 15px  (4 at 390, 2 at 1440)

   These are padding-right, not margin-right, on purpose: every one of these
   elements is either a flex item pushed to the end of its row or a fixed-width
   right-aligned cell, so padding moves the glyphs inward while the box, its
   background and its borders stay exactly where they were. The surfaces still run
   to the cut and are still severed by it. Only the letters moved.

   ⚠️ `.wt-ts-act` IS DELIBERATELY NOT IN THIS BLOCK. It already carries
   `padding: 3px 8px` in its own rule further down the file, and a `padding-right`
   here would lose to it on source order — and if it did win it would REPLACE the
   8px with 5px and shove the text 3px the wrong way. Its 5px is folded into that
   rule as `3px 13px 3px 8px` instead. Any element that already has padding must be
   handled the same way. Check before adding to this list. */
.wt-ts-live { padding-right: 22px; }
.wt-b-tot   { padding-right: 32px; }
.wt-b-sub   { padding-right: 15px; }

/* Card two sits ABOVE card one on purpose. Card one's timesheet runs 20px past
   its own right border and card two's pre-start list runs 16px past its left
   one, so on a wide screen the two overlap by about 12px. Measured: with card
   one on top, that overlap cut the word "Pre-starts" off the list's header and
   read as a clipping bug. With card two on top the same 12px reads as one
   surface lying on another, which is what the band above does with the
   spreadsheet and the pre-start card. */
.wt-2 { z-index: 5; }
.wt-1 { z-index: 4; }
.wt-3 { z-index: 2; }
.wt-4 { z-index: 1; }

.wt-card {
  padding-bottom: 0;
  overflow: visible;          /* the artefact must be allowed OUT of the card */
}
.wt-card:hover { transform: none; }   /* nothing to lift; the layers do the work */
.wt-card h3 { margin-bottom: 6px; }
/* 17px is the documented body size in design-system.md, and unlike the `ch`
   figures in that file it is a value, not a ceiling. This ran at 15px.

   Measured before and after, at both widths:

       desktop 1423px   every card +10px,  section 1759 -> 1779  (+20)
       phone   390px    cards +13 to +41,  section 2725 -> 2862  (+137)

   Nothing broke. No crossing moved by a pixel — every artefact overhangs by a
   MARGIN off the card's content box, which does not care how tall the copy above
   it is — and scrollWidth still equals innerWidth at 390, 430, 768, 980, 1024,
   1180, 1228, 1280 and 1440. The extra height lands in the 76px row gap, so no
   artefact reaches the card below it. The section is taller and nothing is
   worse, so there is no exception to write into design-system.md. */
.wt-card p  { font-size: 17px; margin-bottom: 10px; max-width: 46ch; }

/* 12px, not --cb-gap-3's 24. The gap between this line and the artefact was the
   widest piece of empty space inside a card and it was doing nothing: the line
   reads AS the artefact's label, so it belongs closer to it than to the
   paragraph above it. Round 4, -12px x 4 cards. */
/* This used to hold the link AND a per-card "Not live, it ticks" caption, kept
   on one shared line so rule 4 cost the section no height. The caption moved to
   .mock-note at the section head on 2026-08-16 (Adam's call: say it once, not
   four times), so only the link is left. justify-content stays: with one child
   it is a no-op, and it is correct again the day a second child returns. */
.wt-foot {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  margin-bottom: 12px;
}

/* The one class that breaks the card open, same geometry as .pb-art. The
   card's padding is 24px, so these are measured from the CONTENT box: -25 puts
   the artefact's left edge exactly on the card's left border, -44 runs it 20px
   PAST the right border, -34 hangs it 34px below the bottom one. The crossing
   happens at the OUTER card border. A crop inside the artefact's own frame is
   not depth — that failed a whole round on the section above. */
.wt-art { position: relative; margin: 0 -44px -34px -25px; }

.wt-frame {
  position: relative;
  background: var(--cb-surface-2);       /* one step UP from the card. */
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-md);
  overflow: hidden;                      /* clipping happens HERE, not at the card */
  box-shadow:
    inset 0 1px 0 var(--cb-white),       /* the lit top lip */
    var(--cb-shadow-1),
    var(--cb-shadow-3);
}

/* ---------------------------------------------------------------------------
   CARD 1 — the timesheet.  MATERIAL: matte utility screen.
   ------------------------------------------------------------------------ */
/* 250px, down from 328. The crew list is unchanged in the markup — all six
   rows are still there and "6 on the clock" in the footer is still true — the
   frame simply stops showing them all. At 328 the table ended on a border and
   read as the whole of a six-person crew; at 250 the bar, the column heads, the
   lit row and two full rows fit, and the third is cut mid-row by the frame's
   own overflow. A table that runs out of frame is what a real week view looks
   like, and it costs 78px. */
.wt-ts {
  height: 250px;
  display: grid; grid-template-rows: auto 1fr auto;
  background: linear-gradient(158deg,
    var(--cb-surface-2) 0%,
    var(--cb-surface-2) 48%,
    color-mix(in srgb, var(--cb-graphite) 9%, var(--cb-surface-2)) 100%);
}

.wt-ts-bar {
  display: flex; align-items: center; gap: 10px;
  height: 42px; padding: 0 14px;
  border-bottom: 1px solid var(--cb-line);
  background: color-mix(in srgb, var(--cb-ink) 4%, var(--cb-surface-2));
}
.wt-ts-bar b { font-size: 12.5px; font-weight: 800; color: var(--cb-ink); letter-spacing: -.01em; }
.wt-ts-tab {
  font-size: 10.5px; font-weight: 700; color: var(--cb-ink-2);
  border: 1px solid var(--cb-line); border-radius: var(--cb-r-pill);
  padding: 3px 9px; background: var(--cb-surface-2); white-space: nowrap;
}
.wt-ts-live {
  margin-left: auto; display: flex; align-items: center; gap: 6px;
  font-size: 9.5px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase;
  color: var(--wt-label); white-space: nowrap;
}
.wt-ts-live i {
  width: 6px; height: 6px; border-radius: 50%; background: var(--cb-ink-2);
  animation: cb-pulse 2.4s var(--cb-ease) infinite;
}

.wt-ts-grid {
  display: grid;
  grid-template-columns: minmax(118px, 1fr) repeat(4, 64px) 84px;
  align-content: start;
  overflow: hidden;
  font-size: 12px;
}
.wt-ts-grid > span {
  position: relative;
  display: flex; align-items: center; height: 36px; padding: 0 11px;
  border-bottom: 1px solid var(--cb-line-soft);
  color: var(--cb-ink-2);
  white-space: nowrap; overflow: hidden;
}
.wt-ts-grid .n   { justify-content: flex-end; font-variant-numeric: tabular-nums; }
.wt-ts-grid .wt-nm  { color: var(--cb-ink); font-weight: 700; }
.wt-ts-grid .wt-tot { font-weight: 800; color: var(--cb-ink); background: var(--cb-wash); }
.wt-ts-grid .wt-hd {
  height: 26px;
  font-size: 9.5px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase;
  color: var(--wt-label);
  background: var(--cb-wash);
  border-bottom: 1px solid var(--cb-line);
}
.wt-ts-grid .wt-hd.wt-now { color: var(--cb-ink); }

/* ══ THE WEEK HEAD IS NOT CROPPED. IT IS KEPT WHOLE, LEFT OF THE CUT ═══════════
   Round 5. Cropping this head was tried first, to match the totals under it, and
   abandoned after measuring — write down why, because it looks like an
   inconsistency until you see the numbers.

   To render a clean "W" the cut has to fall in the gap between the W's ink and
   the next letter's. In this face that gap is about ONE PIXEL wide: the W has
   0.94px of right side bearing and the uppercase E has NO left side bearing at
   all (its ink starts at 0.00 of its advance). Miss the window and you do not
   get a rougher crop, you get "We" or "Wee" — and a severed label that forms
   real English reads as WRONG rather than as cropped, which is the exact fault
   this round fixed on card 3 ("OUR METER", "0 h service").

   And the window MOVES. Card one's WEEK column is severed by three different
   things depending on width:

       <=520        by card one's own clip, at 61.00 in an 86px cell
       521-1179     by card one's own clip, at 59.00 in an 84px cell
       >=1180       by CARD TWO's .wt-behind-top, at 42.95

   Three cut positions, each needing its own sub-pixel value, all of them one
   font tweak away from spelling a word. That is a bad thing to own. So the head
   is simply held clear of the cut instead and always reads "Week": one target,
   clearance to spare, and no width anywhere in the range can turn it into English.

   ⚠️ THE LAST PARAGRAPH OF THIS COMMENT USED TO SAY the column still reads as
   cropped because the six totals under it still are, and that the head sits
   flush against the same cut they are flush against. ROUND 7 ENDED THAT: the
   totals are no longer severed at any width (Adam's call, 2026-08-16 — see the
   LD#8 + LD#9 block and the two sub-980 blocks). The column is now right-aligned
   to a single edge that the head and all six totals share, and the CROP still
   reads because the panel's surfaces either side of it are still cut.

   28px, not 27px, so the head lands on the same edge the totals now use. The cut
   sits 25.00px in from the cell's right edge in both sub-980 bands, so this
   clears "Week" by 3.00px — the round-6 house minimum, and 1px more than the 27px
   it replaces. */
.wt-ts-grid .wt-wk { padding-right: 28px; }
@media (min-width: 980px) {
  /* ROUND 7 MOVED THIS BREAKPOINT DOWN FROM 1180 TO 980, and the reason is a
     correction to the paragraph above.

     THE TABLE ABOVE IS WRONG ABOUT ONE BAND. It says 521-1179 is severed by card
     one's own clip and that card two's list only arrives at 1180. Swept at 980,
     1024, 1100, 1179, 1180, 1280 and 1440, the chips cut the TOTALS at 44.1-44.4
     in the cell at EVERY width from 980 up — the list is already there at 980, it
     does not arrive at 1180. The 1180 breakpoint was reading a change in the cut's
     size, not its cause.

     The head itself is a separate case and the paragraph above is right about it:
     it sits ABOVE the topmost chip, so it is only ever cut by card one's clip, at
     59.00 in the cell. Measured head clearance is 2.00px at 27px padding and
     18.00px at 43px, at every width in the range.

     So why move it. Round 7 pushed .wt-1 .wt-tot to 43px so the six totals clear
     the chips (see the LD#8 + LD#9 block). That left the head at 27px and the
     totals at 43px — a 16px misalignment in a column whose whole job is to be
     read as one right-aligned stack, and it showed at 980-1179 only, which is the
     worst kind of bug to leave behind. Matching the head to the totals costs
     nothing (18px of clearance instead of 2px, still whole, still "Week") and the
     column reads as one edge again at every width from 980 to 1440. Verified:
     misalignment 0.00px at 980, 1100, 1180 and 1440.
     ROUND 8 — tracks .wt-1 .wt-tot's 43 → 49 for the same reason it was matched
     in the first place. These two numbers must stay equal or the head and the
     stack stop reading as one edge. "Week" is 4 glyphs and has room to spare, so
     it is the totals' ceiling that governs both. */
  .wt-ts-grid .wt-wk { padding-right: 49px; }
}

/* THE FOCAL MASS of card 1. The crew member still on the clock is not a tinted
   row, it is a solid graphite band with ground-coloured numerals in it — the
   same recipe as the E2 cell and the lit notification in the band above, which
   is what keeps the whole page lit from one direction. It is also taller than
   the rows around it, because a real timesheet expands the row you are
   watching. */
.wt-ts-grid .lit {
  height: 46px;
  background: var(--cb-ink);
  border-bottom-color: var(--cb-ink);
  color: color-mix(in srgb, var(--cb-ground) 88%, var(--cb-ink));
  z-index: 2;
  box-shadow: 0 14px 30px -18px color-mix(in srgb, var(--cb-graphite) 70%, transparent);
}
.wt-ts-grid .lit.wt-nm  { color: var(--cb-ground); font-weight: 800; }
/* background must be re-stated, not removed: `background: none` here let the
   frame's white show through and punched a hole in the right-hand end of the
   focal mass. */
.wt-ts-grid .lit.wt-tot { color: var(--cb-ground); background: var(--cb-ink); }

/* The running cell. 13.2s loop, 4.4s a step, ~530ms of fade — nothing under
   400ms, per bar.md, and it runs forward the way a clock does. */
.wt-run { padding-left: 24px !important; }
.wt-run::before {
  content: ""; position: absolute; left: 10px; top: 50%; margin-top: -3px;
  width: 6px; height: 6px; border-radius: 50%;
  background: color-mix(in srgb, var(--cb-ground) 82%, var(--cb-ink));
  animation: cb-pulse 2.4s var(--cb-ease) infinite;
}
.wt-run i {
  position: absolute; inset: 0 11px 0 0;
  display: flex; align-items: center; justify-content: flex-end;
  font-style: normal; opacity: 0;
  animation: cb-wt-tick 13.2s var(--cb-ease) infinite;
}
.wt-run i:nth-of-type(2) { animation-delay: 4.4s; }
.wt-run i:nth-of-type(3) { animation-delay: 8.8s; }
@keyframes cb-wt-tick {
  0%        { opacity: 0; }
  4%, 29%   { opacity: 1; }
  33%, 100% { opacity: 0; }
}

/* LD#15, ROUND 8 — THE WEEK TOTAL HAS TO TICK TOO.
   The running cell advanced 3:53 -> 3:54 -> 3:55 while this row's total sat on
   31:01, so for two thirds of the loop the row's own columns did not sum to its
   own total. Every other row in the grid sums exactly, which is what made the
   one that did not stand out.

   It reuses cb-wt-tick and the SAME 4.4s delays, so the two advance on the same
   frame. If you ever retime the running cell, retime this with it or the card
   goes back to lying about its own arithmetic.

   Only the last digit is swapped, inside an inline-block. That is deliberate:
   the cell keeps ONE text flow, so it inherits this column's per-breakpoint
   padding-right and its flex-end alignment instead of restating them — the
   mistake that would have coupled this fix to LD#16's geometry.

   ⚠️ THE FIRST FRAME STAYS IN NORMAL FLOW AND SIZES THE BOX. Frames 2 and 3 are
   absolute on top of it. Do not "simplify" this to `width: 1ch` — that was the
   first attempt and `1ch` measures 3.50px in this face at 12px, roughly half a
   digit, because `ch` is the advance of "0" and this font's zero is narrow. The
   digits would have overflowed a box half their width. Sizing the box with a
   real glyph cannot be wrong in any font, and tabular-nums on .wt-ts-grid .n
   guarantees frames 2 and 3 have the identical advance to frame 1. */
.wt-tot-run { position: relative; display: inline-block; }
.wt-tot-run i {
  font-style: normal; opacity: 0;
  animation: cb-wt-tick 13.2s var(--cb-ease) infinite;
}
.wt-tot-run i:nth-of-type(2),
.wt-tot-run i:nth-of-type(3) { position: absolute; inset: 0; }
.wt-tot-run i:nth-of-type(2) { animation-delay: 4.4s; }
.wt-tot-run i:nth-of-type(3) { animation-delay: 8.8s; }

.wt-ts-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  height: 34px; padding: 0 14px;
  border-top: 1px solid var(--cb-line);
  background: color-mix(in srgb, var(--cb-ink) 4%, var(--cb-surface-2));
  font-size: 10.5px; font-weight: 700; color: var(--wt-label); white-space: nowrap;
}
.wt-ts-act {
  border: 1px solid var(--cb-line); border-radius: 5px;
  /* Round 6, the crop rule. 8px was symmetric; the right side carries the shift
     that holds "Export to payroll" clear of card 1's clip (2px of overshoot plus
     the gutter). It lives HERE rather than in the round-6 block because that
     block is earlier in the file and this shorthand would overwrite it.
     ROUND 8 — 13px BECOMES 21px, the same +8 every value in that block took.
     This one was the worst instance of the 3px fault and the reason LD#16 was
     the round's biggest gap: at 390 the pill kept its left rounded corner and
     both long borders and then STOPPED WITH NO RIGHT CAP. A severed numeral
     still reads as a number continuing. A capless pill reads as a broken
     control, not a cropped one, which is the line the crop device cannot cross.
     The chip's border and background still run to the cut and are still cut. */
  padding: 3px 21px 3px 8px;
  background: var(--cb-surface-2); color: var(--cb-ink-2);
}
/* ⚠️ AT >=980 THE CUT IS NOT THE CLIP, IT IS CARD TWO LYING ON TOP.
   Round 6 nearly missed this. The 13px above holds the chip clear of card one's
   own clip-path at 804.33, which is the only thing cutting it below 980 — and it
   measured clear by 3px, so every automated check passed. But from 980 up, card
   two's .wt-behind is z-index 5 over card one's 4 and its rotated left edge
   arrives at x=792.75 at this chip's y, 11.6px earlier than the clip. The chip
   still rendered "Export to payr".

   AN OCCLUSION IS A CUT AND THE RULE APPLIES TO IT IDENTICALLY. A clip-path probe
   will never find one, because nothing is clipped — a sibling is simply drawn on
   top. Test occlusion with elementFromPoint along the element's own centre line,
   not by walking ancestors for overflow and clip-path.

   26px = 13 + 13, landing the ink at 788.75 against a 792.75 cover, clear by 4px.
   ROUND 8 — 26px BECOMES 33px, for the 11px gutter LD#16 set everywhere else.
   4px was the same ambiguous middle as the 3px elsewhere: enough to prove an
   automated "not covered" check, not enough to read as clear of the card lying
   on top of it. The pill's BOX is pinned to the footer's content-right by
   `space-between`, so extra padding grows the box leftward and moves only the
   ink — the border still runs under card two and is still cropped by it, which
   at this width is a genuine overlap between two cards and is the point.
   Scoped to >=980 because below it the cards are stacked, card two is nowhere near
   card one, and the extra 13px would just push the chip off its own right edge for
   no reason. The WEEK head already clears this same cover by 6.42px and the week
   totals are cut by it ON PURPOSE — that is round 5's "31:0", and it is a severed
   numeral, which the rule allows. */
@media (min-width: 980px) {
  .wt-ts-act { padding-right: 33px; }
}

/* ---------------------------------------------------------------------------
   CARD 2 — the pre-start.  MATERIAL: device glass in a graphite handset.
   The one near-black object in the section, and the biggest value step
   available on a white page. The office list runs out from underneath it and
   crosses the card's LEFT border; the handset crosses the right and the bottom.
   ------------------------------------------------------------------------ */
/* 322px, down from 384. The handset is the material and the signature well is
   the focal mass, so neither shrank — the 42px signature keeps every pixel it
   had. What came out was one check row (five to four; a real pre-start has more
   than either number and this list was always a sample) and 12px of padding
   spread across the body, the check gaps and the signed bar.

   MEASURED, and measured the hard way after 314 was tried and clipped: the
   screen needs 34 (top bar) + 209 (body: head, four checks, the signature well)
   + 53 (the signed bar, two lines) = 296, plus the handset's own 10px bezel top
   and bottom = 316. At 322 the body row keeps 6px of slack. The check is
   .wt-screen's scrollHeight against its offsetHeight — they must be equal. */
.wt-art-glass { height: 322px; }

.wt-behind {
  position: absolute; z-index: 1;
  left: -16px; top: 16px; width: 300px;
  display: grid; gap: 7px;
  transform: rotate(-1.4deg);
}
/* flex-start, not space-between: pushed to the right-hand end of a 300px row
   the count landed under the handset and never rendered. Same rule as the rows
   below — everything readable lives in the left 200px.

   It carries its own opaque plate for one measured reason: this block starts
   16px left of card two's border, which is 13px back inside card one's
   timesheet frame. The ROWS crossing there is the whole point and they cross
   fine, because they are opaque panels and read as one surface lying on
   another. Naked 9.5px type over a ruled grid does not — it read as a clipped
   word. So the header is a small tab instead. */
.wt-behind-top {
  display: inline-flex; width: max-content; align-items: baseline; gap: 9px;
  padding: 5px 11px;
  background: var(--cb-surface-2);
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-sm);
  box-shadow: var(--cb-shadow-1);
  font-size: 9.5px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase;
  color: var(--cb-ink-3);
}
.wt-behind-top b { font-weight: 800; color: var(--cb-ink-2); letter-spacing: .12em; }
.wt-brow {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 11px;
  background: var(--cb-surface-2);
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-sm);
  box-shadow: var(--cb-shadow-1);
  font-size: 11.5px; color: var(--cb-ink-2);
}
/* Everything readable is packed into the LEFT 200px of the row. The rows are
   300px wide so they run on under the handset, which is the whole depth cue —
   but a right-aligned time would then be sitting under the phone where nobody
   can read it. Measured: at 1383px the handset covers the row from 205px in.
   So the name gets a fixed measure and the status follows it. */
/* SIX ROWS, NOT SEVEN, NOW THE HANDSET IS 62px SHORTER. The list is absolutely
   positioned and nothing clips it, so its length has to be fitted to the zone by
   hand or it hangs out of the bottom of the card and into the row below. From
   top:16 to the artefact's new 322 there is 306px; six one-line rows measure
   28 + 7 + 6x40 + 5x7 = 310, seven measure 357. Six it is. The unsigned TP09 row
   is third in the markup, so it is never one of the ones that goes. */
.wt-behind .wt-brow:last-of-type { display: none; }
.wt-brow b { flex: none; min-width: 84px; font-weight: 700; color: var(--cb-ink); white-space: nowrap; }
.wt-brow em { font-style: normal; font-size: 10.5px; color: var(--cb-ink-3); white-space: nowrap; }
/* THE ONE COLOURED ELEMENT IN THIS ENTIRE SECTION, and the only one it gets.
   Everything else from the eyebrow to the feed is greyscale or graphite.

   This row used to be greyscale too, on the grounds that a hi-vis mark would
   repeat the pill-and-dot badge the band above spent a round removing. Half of
   that was right and half was wrong. The pattern IS banned — hence no pill, no
   dot, no badge, and nothing that could be mistaken for a status chip. But the
   reason it was banned up there does not reach down here: a badge made that
   artefact look like an app, and it was a picture of the mess BEFORE CrewBase.
   This section is meant to look like the app. And "TP09 Tipper 9 — not signed"
   is the one state anywhere on this page that means STOP: a machine that is
   about to move without a signed pre-start. Rendering the only genuine warning
   on the page in the same grey as the four rows that are fine is the section
   under-reading its own content.

   So it is a solid edge, not a badge. An inset shadow rather than a border-left,
   for two reasons: border-left would have to fight `border-style: dashed` on the
   same box, and an inset shadow follows --cb-r-sm exactly, so the mark takes the
   row's own rounded corner instead of sitting on it as a rectangle.

   --cb-hivis-deep, not --cb-hivis. See the long note in tokens.css: the plain
   amber measures 1.92:1 on this card and would read as decoration. The deep one
   measures 3.44:1 against --cb-surface #F7F8FA, the row's own plate.
   VERIFY IF YOU TOUCH IT: 3:1 minimum against the surface it sits on.

   THE DASHED OUTLINE STAYS. The meaning must never depend on the colour alone —
   colour-blind, greyscale print, or a forced-colours mode all still get "this
   one is different" from the dashed rule and from the words "not signed". The
   edge makes it FASTER to find; it is not what makes it legible. */
.wt-brow.is-out {
  border-style: dashed;
  background: var(--cb-surface);
  box-shadow: inset 4px 0 0 var(--cb-hivis-deep), var(--cb-shadow-1);
}
/* Hi-vis is a SURFACE, never ink — tokens.css rule 2. The row's own text does
   not change colour. */
.wt-brow.is-out b { color: var(--cb-ink-2); }

.wt-plate {
  flex: none;
  display: grid; place-items: center;
  min-width: 34px; height: 20px; padding: 0 5px;
  border-radius: 4px;
  background: color-mix(in srgb, var(--cb-ink) 8%, var(--cb-surface-2));
  border: 1px solid var(--cb-line);
  font-size: 9px; font-weight: 800; letter-spacing: .06em;
  color: var(--cb-ink-2);
}

.wt-phone {
  position: absolute; z-index: 2;
  right: 0; top: 0; bottom: 0; width: 272px;
  border-radius: 36px; padding: 10px;
  transform: rotate(-1.8deg);
  background: linear-gradient(152deg,
    color-mix(in srgb, var(--cb-white) 30%, var(--cb-graphite)) 0%,
    var(--cb-graphite) 40%,
    color-mix(in srgb, var(--cb-white) 12%, var(--cb-graphite)) 78%,
    color-mix(in srgb, var(--cb-white) 26%, var(--cb-graphite)) 100%);
  box-shadow:
    0 34px 60px -26px color-mix(in srgb, var(--cb-graphite) 55%, transparent),
    0 6px 16px -8px color-mix(in srgb, var(--cb-graphite) 35%, transparent);
}
/* The specular streak. `screen` blend means it lightens the graphite bezel and
   does nothing at all where it crosses the white screen — which is what light
   on glass actually does, and it is why the bezel reads as a hard material
   rather than a dark rectangle. */
.wt-phone::after {
  content: ""; position: absolute; inset: 0; z-index: 6; pointer-events: none;
  border-radius: 36px;
  background: linear-gradient(112deg,
    color-mix(in srgb, var(--cb-white) 34%, transparent) 0%,
    color-mix(in srgb, var(--cb-white) 10%, transparent) 13%,
    transparent 27%);
  mix-blend-mode: screen;
}

.wt-screen {
  position: absolute; inset: 10px;
  border-radius: 27px; overflow: hidden;
  background: var(--cb-surface-2);
  display: grid; grid-template-rows: auto 1fr auto;
}
.wt-scr-bar {
  display: flex; align-items: center; gap: 8px;
  height: 34px; padding: 0 15px;
  border-bottom: 1px solid var(--cb-line-soft);
  font-size: 11.5px; color: var(--cb-ink);
}
.wt-scr-bar b { font-weight: 800; letter-spacing: -.01em; }
.wt-scr-bar em { margin-left: auto; font-style: normal; font-size: 10.5px; color: var(--cb-ink-3); }
.wt-scr-logo svg { width: 15px; height: 15px; color: var(--cb-ink); }

.wt-scr-body { padding: 10px 15px 0; }
.wt-scr-head { display: grid; gap: 2px; margin-bottom: 9px; }
.wt-scr-head b  { font-size: 12.5px; font-weight: 800; letter-spacing: -.01em; color: var(--cb-ink); }
.wt-scr-head em { font-style: normal; font-size: 10px; color: var(--cb-ink-3); }

/* Vector ticks are CORRECT here and wrong in the band above. Up there the sheet
   is paper and a crisp vector mark made it read as one of our screens. Down
   here it IS one of our screens. */
.wt-scr-checks { list-style: none; margin: 0 0 10px; padding: 0; display: grid; gap: 6px; }
.wt-scr-checks li {
  position: relative; padding-left: 24px;
  font-size: 11.5px; line-height: 15px; color: var(--cb-ink-2);
}
/* A solid ink box with a ground-coloured tick cut across it. Both halves read
   from a token, and unlike an SVG mask there is no colour literal anywhere in
   it. Every one of these is IDENTICAL on purpose — the paper sheet in the band
   above needs four hand-drawn ticks that never repeat, and this needs the
   opposite: a machine drew these, all at once, the same way every time. */
.wt-scr-checks li::before {
  content: ""; position: absolute; left: 0; top: 0;
  width: 15px; height: 15px; border-radius: 5px;
  background: var(--cb-ink);
}
.wt-scr-checks li::after {
  content: ""; position: absolute; left: 4.6px; top: 3.4px;
  width: 4px; height: 8px;
  border-right: 2px solid var(--cb-ground);
  border-bottom: 2px solid var(--cb-ground);
  transform: rotate(42deg);
}

/* The signature well, and the stroke in it. THE FOCAL MASS of card 2: the one
   piece of handwriting on a page that is otherwise entirely set type. */
.wt-scr-sig {
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-sm);
  background: color-mix(in srgb, var(--cb-ink) 3%, var(--cb-surface-2));
  padding: 6px 10px 2px;
}
.wt-scr-siglab {
  display: block;
  font-size: 8.5px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase;
  color: var(--cb-ink-3);
}
.wt-sig { width: 100%; height: 42px; color: var(--cb-ink); }

/* CARD 2's LOOP — the stroke draws itself, 17s, continuous.
   Cards 1 and 4 were live and cards 2 and 3 were screenshots. This is card 2's
   answer, and it is the card's own subject rather than decoration on top of it:
   the sentence above the artefact is "the sheet gets signed before the machine
   moves", so the thing that moves is the signing.

   The path carries pathLength="100" (see index.html), so the dash figures below
   are plain percent and do not have to be re-measured if the stroke is ever
   redrawn.

   ROUND 4 FIXED THE CONTRADICTION, and it was bigger than the previous round
   thought. Round 3 flagged 2.4s per cycle where the well was clearing or empty
   while the bar underneath still read "Signed 06:04". Re-measured, it was ~7s of
   every 17: the whole 4.6s draw is also a frame where the sheet is not signed
   yet and the bar was claiming it was.

   The fix is the one the artefact's own logic asks for: the stroke and the bar
   are two views of one fact, so they run off ONE clock and the bar has two
   states. The bar only says "Signed 06:04" between the moment the pen finishes
   and the moment the well clears. Contradicted frames: zero.

   The clock, spelled out, because "nothing under 400ms" is checkable:
       0 - 4%     0.68s   empty well, waiting. Bar reads "Not signed yet".
       4 - 30%    4.42s   the pen draws, eased out, quick then trailing.
                          The bar is still "Not signed yet" — true while it draws.
       30 - 33.5% 0.60s   the bar cross-fades to "Signed 06:04", which is the
                          instant the stroke completed.
       33.5 - 88% 9.27s   signed, and still. Most of the loop is this.
       88 - 92%   0.68s   the well fades out for the next machine AND the bar
                          fades back to "Not signed yet" over the same window.
       92 - 100%  1.36s   empty well again.
   Nothing in that is a flash, nothing repeats faster than once every 17s, and
   the two elements never disagree for a single frame. */
.wt-sig path {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: cb-wt-sign 17s var(--cb-ease) infinite;
}
@keyframes cb-wt-sign {
  0%      { stroke-dashoffset: 100; opacity: 1; }
  4%      { stroke-dashoffset: 100; opacity: 1; }
  30%     { stroke-dashoffset: 0;   opacity: 1; }
  88%     { stroke-dashoffset: 0;   opacity: 1; }
  92%     { stroke-dashoffset: 0;   opacity: 0; }
  92.01%  { stroke-dashoffset: 100; opacity: 0; }
  96%     { stroke-dashoffset: 100; opacity: 1; }
  100%    { stroke-dashoffset: 100; opacity: 1; }
}

/* The signed bar. Solid graphite, ground-coloured type — the same lit-mass
   recipe as the clocked-on row and the hour meter. The two states are stacked
   in one grid cell so the bar's height is the taller of them and cannot move
   as they cross over. */
.wt-scr-signed {
  display: grid;
  padding: 8px 15px 9px;
  background: var(--cb-ink);
}
.wt-sg { grid-area: 1 / 1; display: grid; gap: 1px; }
.wt-scr-signed b  { font-size: 11.5px; font-weight: 800; color: var(--cb-ground); }
.wt-scr-signed em { font-style: normal; font-size: 9.5px; color: color-mix(in srgb, var(--cb-ground) 72%, var(--cb-ink)); }
.wt-sg-un { animation: cb-wt-unsigned 17s var(--cb-ease) infinite; }
.wt-sg-ok { opacity: 0; animation: cb-wt-signed 17s var(--cb-ease) infinite; }
@keyframes cb-wt-signed {
  0%, 30%     { opacity: 0; }
  33.5%, 88%  { opacity: 1; }
  92%, 100%   { opacity: 0; }
}
@keyframes cb-wt-unsigned {
  0%, 30%     { opacity: 1; }
  33.5%, 88%  { opacity: 0; }
  92%, 100%   { opacity: 1; }
}

/* ---------------------------------------------------------------------------
   CARD 3 — the plant record.  MATERIAL: light panel with a RECESSED
   instrument well pressed into it. Every other surface in this section is
   raised; this one is the only thing pushed in, which is what makes it read as
   a different material rather than a different layout.
   ------------------------------------------------------------------------ */
.wt-plant {
  /* 292px, down from 352, and the rule about the history rows still holds: you
     must see enough of the cut row to read it as a row, not as an empty strip.
     23px of the header bar, the machine head, the meter, the service line and
     the rail came out as padding (each noted on its own rule below), which left
     the history rows sitting 23px higher. Measured at 292: the first history row
     runs 220-262 and is whole, the second runs 262-304 and shows 30px of its 42
     — its title line in full, cut 3px under the baseline and before its bottom
     rule — so the list still reads as carrying on past the frame rather than
     ending on a border. */
  height: 292px;
  display: grid; grid-template-rows: auto auto auto auto auto 1fr;
  background: linear-gradient(160deg,
    var(--cb-surface-2) 0%,
    var(--cb-surface-2) 46%,
    color-mix(in srgb, var(--cb-graphite) 8%, var(--cb-surface-2)) 100%);
}
.wt-p-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  height: 36px; padding: 0 14px;
  border-bottom: 1px solid var(--cb-line);
  background: color-mix(in srgb, var(--cb-ink) 4%, var(--cb-surface-2));
}
.wt-p-bar b { font-size: 12.5px; font-weight: 800; color: var(--cb-ink); }
.wt-p-bar span { font-size: 10.5px; font-weight: 700; color: var(--wt-label); }

.wt-p-head { display: flex; align-items: center; gap: 10px; padding: 10px 14px 9px; }
.wt-plate-lg { min-width: 44px; height: 26px; font-size: 10px; border-radius: 5px; }
.wt-p-name { display: grid; gap: 1px; min-width: 0; }
.wt-p-name b  { font-size: 12.5px; font-weight: 800; letter-spacing: -.01em; color: var(--cb-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wt-p-name em { font-style: normal; font-size: 10.5px; color: var(--cb-ink-3); }

/* THE FOCAL MASS of card 3. A machine's hour meter is a dark readout set into
   the dash, so this is a dark readout set into the panel: inner shadow along
   the top, a 1px white lip along the bottom. The lip is what sells "pressed
   in" — without it this is just a dark rectangle lying on top. */
.wt-meter {
  margin: 0 14px;
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 9px 14px 10px;
  border-radius: 9px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--cb-white) 7%, var(--cb-graphite)) 0%,
    var(--cb-graphite) 100%);
  box-shadow:
    inset 0 3px 7px -1px color-mix(in srgb, var(--cb-graphite) 85%, transparent),
    inset 0 0 0 1px color-mix(in srgb, var(--cb-graphite) 60%, transparent),
    0 1px 0 var(--cb-white);
}
.wt-meter-lab {
  font-size: 9px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase;
  color: color-mix(in srgb, var(--cb-white) 58%, var(--cb-graphite));
}
.wt-meter-val {
  font-size: 27px; font-weight: 800; letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
  color: var(--cb-white);
}
.wt-meter-val i { font-style: normal; font-size: 16px; color: color-mix(in srgb, var(--cb-white) 62%, var(--cb-graphite)); }

/* CARD 3's LOOP — the hour meter counts, 15s, continuous.
   An hour meter on a machine that is out on a job genuinely climbs, and the
   tenths digit is the only part of it that moves at a speed a person can see.
   So the tenths cell holds three digits stacked on top of each other and
   cross-fades them, which is exactly the mechanism card 1's running cell uses,
   already proven on this page and already handled for reduced motion.

   ::before sizes the cell with a hidden "8" rather than a `ch` guess, so the
   digits cannot shuffle the decimal point sideways as they swap.

   15s / three digits = 5s a step, and cb-wt-tick spends 4% of the loop fading,
   which is 600ms. Over the 400ms floor. Nothing here is a flash and the number
   next to it — "Next service at 4 250 h · 67 h to go" — stays true across all
   three digits (67.4, 67.3, 67.2 all round to 67). */
.wt-tenth { position: relative; display: inline-block; }
.wt-tenth::before { content: "8"; visibility: hidden; }
.wt-tenth b {
  position: absolute; left: 0; top: 0;
  font-weight: inherit; opacity: 0;
  animation: cb-wt-tick 15s var(--cb-ease) infinite;
}
.wt-tenth b:nth-of-type(2) { animation-delay: 5s; }
.wt-tenth b:nth-of-type(3) { animation-delay: 10s; }

.wt-p-svc {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 11px 14px 7px;
  font-size: 11px; color: var(--cb-ink-2);
}
.wt-chip {
  border: 1px solid var(--cb-line); border-radius: var(--cb-r-pill);
  padding: 3px 10px; background: var(--cb-surface-2);
  font-size: 10px; font-weight: 800; color: var(--cb-ink); white-space: nowrap;
}
.wt-rail {
  margin: 0 14px 11px; height: 7px; border-radius: 4px;
  background: var(--cb-wash);
  box-shadow: inset 0 0 0 1px var(--cb-line-soft);
  overflow: hidden;
}
.wt-rail i { display: block; height: 100%; width: 73%; border-radius: 4px; background: color-mix(in srgb, var(--cb-ink) 52%, var(--cb-surface-2)); }

.wt-p-hist { border-top: 1px solid var(--cb-line); }
.wt-p-hist div {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--cb-line-soft);
  font-size: 11.5px;
}
.wt-p-hist b  { font-weight: 700; color: var(--cb-ink-2); }
.wt-p-hist em { font-style: normal; font-size: 10.5px; color: var(--wt-label); white-space: nowrap; }

/* ══ THE CROP EATS SURFACE, NEVER WORDS ═══════════════════════════════════════
   Round 5, and this is the rule that was missing when the 40px crop went in.

   A severed NUMBER is obviously severed — your eye supplies the rest. A severed
   LABEL that happens to form real English does not read as cropped, it reads as
   a typo. Card three's left crop was landing straight on the label column, 25px
   into every one of them, and the register was quietly telling lies:

       "Plant register"   ->  "t register"
       "Hour meter"       ->  "OUR METER"
       "1000 h service"   ->  "0 h service"      (a different service interval)
       "Next service..."  ->  "t service at 4 250 h"

   "OUR METER" and "0 h service" are the bad ones: both are real, readable, and
   wrong. A dev team screenshotting this section would ship a machine showing the
   wrong service interval.

   THE CROP DEPTH IS NOT REDUCED. Cutting less would have cost the depth Craft
   bought with it, and the depth was never coming from the words — it comes from
   SURFACES running out of the frame, and every one of those is untouched here:

       .wt-plant     the panel itself, severed 40px with its gradient
       .wt-p-bar     the header band and its bottom rule, severed 40px
       .wt-meter     THE FOCAL MASS. Keeps margin-left: 14px, so the well's
                     rounded left corner is still cut 25px in and the instrument
                     still reads as set into a panel carrying on past the card.
       .wt-rail      the service bar, severed
       .wt-p-hist    every row separator, severed

   So only the TYPE moves right, out of the cut, and only far enough to clear it
   by 2px. The cut is at the card's left border, 40px into .wt-art; type sat at
   15px (1px frame border + 14px padding) and now sits at 42px.

   .wt-meter is the exception because it is inset 14px already: its label sat at
   29px, so it needs 27px of padding rather than 41 to land on the same line as
   everything else.

   MEASURED FOR FIT AT BOTH WIDTHS BEFORE IT WENT IN — 27px of content width is
   real money on a phone. At 390 the frame is 382px, which leaves 325px of
   content, against a widest row of 242px (history title + its date + the gap).
   The head row is 196px, the service line 195px, the bar 154px. Nothing wraps,
   nothing ellipsises, and .wt-p-name b keeps its ellipsis as the backstop.

   Not scoped to a breakpoint on purpose: the crop is on at every width except
   760-979px, where .wt-3 > .wt-art turns clip-path off because the artefact sits
   in the right-hand column. The extra padding is harmless there — it reads as a
   slightly deeper left gutter on a panel that is not being cut at all. */
/* ══ LD#10, ROUND 7 — 2px OF CLEARANCE BECOMES 3px, ON ALL OF CARD 3's TYPE ════
   The round-6 Craft critic reported the hour meter's label as SEVERED at 390 and
   read it as "-IOUR METER". Measured before acting, that was wrong: card 3's clip
   sits at x=24 and the "H" runs 26.00-34.03, so it clears by 2.00px and a 2x zoom
   shows the stem whole. Filed cosmetic for that reason.

   BUT THE CRITIC'S EYE WAS NOT WRONG ABOUT THE READING. 2px against a hard dark
   slab edge is not clearance anyone can see, and at screenshot resolution it
   looks cut. Round 6 gave every other label in this section its measured overshoot
   PLUS 3px; card 3 was built earlier and never went through that pass, so it kept
   the 2px this comment describes above.

   ⚠️ ALL FIVE VALUES MOVE, NOT JUST THE METER. OPEN_DEFECTS filed LD#10 as "one
   declaration" — bump the meter alone. That would have been wrong: the whole point
   of the block above is that 41px and 27px put five different rows on ONE left
   line (the meter differs only because it is already inset 14px). Moving the meter
   by itself would fix a 1px clearance complaint by introducing a 1px misalignment
   in the card's main type edge, which is a worse fault than the one being fixed.
   41 -> 42 and 27 -> 28 keeps the line and gives the whole card the 3px minimum.

   The width cost was re-checked against the paragraph above: at 390 this leaves
   324px of content against a widest row of 242px. Nothing wraps or ellipsises. */
.wt-p-bar    { padding-left: 42px; }
.wt-p-head   { padding-left: 42px; }
.wt-p-svc    { padding-left: 42px; }
.wt-p-hist div { padding-left: 42px; }
.wt-meter    { padding-left: 28px; }

/* ---------------------------------------------------------------------------
   CARD 4 — the live view.  MATERIAL: a backlit board.
   It carries its own local dark palette, all of it mixed from brand tokens, so
   it survives a change of ground exactly the way the manila stock in the band
   above does. It is NOT a .band and must never be given that class.
   ------------------------------------------------------------------------ */
/* 284px, down from 320, and the four site rows are all still in the markup so
   "34 on site" (12 + 9 + 6 + 7) is still the sum of the list. The board is a
   grid now — auto / 1fr / auto — for one reason: at a shorter height a plain
   block would have pushed the footer strip out of the frame's overflow and the
   feed would have been floating over nothing. As a grid the LIST is the row
   that gives, so the fourth job is cut in half by the frame and the footer
   stays put. A wall board with more on it than fits is also what a wall board
   actually looks like. */
.wt-board {
  height: 284px;
  display: grid; grid-template-rows: auto 1fr auto;
  --wt-b-bg:    var(--cb-graphite);
  --wt-b-2:     color-mix(in srgb, var(--cb-white) 7%, var(--cb-graphite));
  --wt-b-line:  color-mix(in srgb, var(--cb-white) 13%, var(--cb-graphite));
  --wt-b-ink:   color-mix(in srgb, var(--cb-white) 95%, var(--cb-graphite));
  --wt-b-ink-2: color-mix(in srgb, var(--cb-white) 62%, var(--cb-graphite));
  background: linear-gradient(163deg,
    var(--wt-b-2) 0%, var(--wt-b-bg) 54%,
    color-mix(in srgb, var(--cb-graphite) 88%, var(--cb-white)) 100%);
  border-color: color-mix(in srgb, var(--cb-white) 18%, var(--cb-graphite));
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--cb-white) 22%, transparent),
    var(--cb-shadow-1),
    var(--cb-shadow-3);
}
.wt-b-bar {
  display: flex; align-items: center; gap: 11px;
  height: 46px; padding: 0 16px;
  border-bottom: 1px solid var(--wt-b-line);
}
.wt-b-bar b { font-size: 12.5px; font-weight: 800; color: var(--wt-b-ink); letter-spacing: -.01em; }
.wt-b-time { display: flex; align-items: center; gap: 6px; font-size: 10.5px; font-weight: 700; color: var(--wt-b-ink-2); }
.wt-b-time i {
  width: 6px; height: 6px; border-radius: 50%;
  background: color-mix(in srgb, var(--cb-white) 78%, var(--cb-graphite));
  animation: cb-pulse 2.4s var(--cb-ease) infinite;
}
/* The -19px is the SAME RULE card 3 is built on: the crop may eat surface and it
   may eat a fragment, but it must never leave a real English word standing.
   Once the board started being cut at the card's border (round 5), this label sat
   in exactly the wrong place — the border landed between the "t" and the "e" and
   the headcount rendered "34 ON SIT". "sit" is a word, so it reads as a typo in
   the product rather than as a panel continuing past the card.
   Measured at the card border, glyph right edges relative to it:
       "3" -54.17   "4" -46.55   "o" -33.98   "n" -25.55
       "s" -15.28   "i" -11.41   "t"  -4.89   "e"  +2.00
   -19px slides the label out until the border falls halfway through the "s", so
   it renders "34 ON" with a severed letter after it and no word is formed. The
   count itself — the only part that carries meaning — stays whole and so does
   "On site now" at the left-hand end of this same bar, which is why the crop
   costs the reader nothing: the words it eats are already on screen.
   The tail now runs 3px past the board's own right edge and .wt-board clips it,
   which is invisible: that is 22px outside the card border that already cut it.
   RE-MEASURE THIS IF THE BOARD'S WIDTH, THE BAR'S PADDING OR THE CLIP INSET
   CHANGES. It is a glyph-level fit, not a round number. */
.wt-b-tot {
  margin-left: auto; margin-right: -19px;
  font-size: 10px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: var(--wt-b-ink-2); white-space: nowrap;
}

.wt-b-list { list-style: none; margin: 0; padding: 0; overflow: hidden; }
/* padding-left leaves a permanent 16px lane for the ping, so the three rows
   without one still line up with the row that has it. */
/* LD#6, round 6. 56 → 47, the same value the phone band uses, so all FOUR job
   rows fit the board at both judged widths and none of them is severed.

   .wt-b-list is `overflow: hidden` and the board is a flex column, so the list
   gets whatever height is left and clips its rows inside it — the footer strip is
   static and never moves. At 56px the four rows measured 224px into a 191px list,
   so row 4 showed 23px and the clip landed straight through "Yard", its "7" and
   "4 machines in". Four rows at 47 measure 188 into that same 191, so the list
   ends 3px of bare ground short of the strip and nothing is cut.

   ⚠️ Do not "fix" this by growing the board instead. The 23px numeral is
   baseline-aligned and its ink starts ~4px below the row's top edge, so ANY
   partial row wide enough to notice severs it — there is no gap to aim a cut at.
   The choice is all four rows or a severed one. */
.wt-b-list li {
  position: relative;
  display: flex; align-items: baseline; gap: 12px;
  height: 47px; padding: 0 16px 0 34px;
  border-bottom: 1px solid var(--wt-b-line);
}
.wt-b-site { flex: 1; min-width: 0; font-size: 14px; font-weight: 700; color: var(--wt-b-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wt-b-num  { font-size: 23px; font-weight: 800; letter-spacing: -.03em; color: var(--wt-b-ink); font-variant-numeric: tabular-nums; }
.wt-b-sub  { width: 116px; text-align: right; font-size: 11px; color: var(--wt-b-ink-2); white-space: nowrap; }

/* The rule on top is not decoration. Now that the list is cut by the frame, the
   fourth job's row runs out mid-type directly above this strip, and with nothing
   between them the two read as one jammed line. The hairline makes the strip a
   summary bar lying over a list that continues underneath it, which is what a
   wall board actually does. Same --wt-b-line as every other rule on the board. */
.wt-b-foot {
  display: flex; align-items: baseline; gap: 12px;
  height: 46px; padding: 0 16px 0 34px;
  border-top: 1px solid var(--wt-b-line);
  font-size: 10.5px; color: var(--wt-b-ink-2);
}
.wt-b-foot b { font-weight: 800; color: var(--wt-b-ink); }

/* The one job with something happening on it. A radar ping, 3.6s, continuous.
   left:18px, not left:6px — at 6px the expanding ring was cut in half by the
   frame's own overflow:hidden and read as a grey smudge on the edge. */
.wt-b-ping {
  position: absolute; left: 18px; top: 50%; margin-top: -3px;
  width: 6px; height: 6px; border-radius: 50%;
  background: color-mix(in srgb, var(--cb-white) 82%, var(--cb-graphite));
  animation: cb-wt-ping 3.6s var(--cb-ease) infinite;
}
@keyframes cb-wt-ping {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--cb-white) 30%, transparent); }
  72%  { box-shadow: 0 0 0 17px color-mix(in srgb, var(--cb-white) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--cb-white) 0%, transparent); }
}

/* THE FOCAL MASS of card 4, and the brightest thing on the darkest artefact.
   It floats over the board's bottom corner and hangs past the card's border,
   which is the QuickBooks move: a light UI card laid over a dark surface.

   bottom:-84px is measured, not chosen. The board is 46 + 4x56 + 46 = 316px,
   so the last site row ends at 270. At -24px the panel's top landed at 210 and
   sat straight across the Yard row's headcount, which read as a broken render
   rather than a floating card. At -84 its top lands on 268, so it covers the
   footer strip and nothing else. If you change the board's height or its row
   height, re-measure this. */
.wt-feed {
  position: absolute; z-index: 4;
  right: -18px; bottom: -84px; width: 258px;
  padding: 12px 14px 11px;
  background: var(--cb-surface-2);
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-md);
  box-shadow: 0 26px 48px -18px color-mix(in srgb, var(--cb-graphite) 45%, transparent);
}
.wt-feed-lab {
  display: block; margin-bottom: 7px;
  font-size: 8.5px; font-weight: 800; letter-spacing: .18em; text-transform: uppercase;
  color: var(--cb-ink-3);
}
.wt-fr {
  display: flex; align-items: baseline; gap: 8px;
  padding: 5px 0;
  font-size: 11px; color: var(--cb-ink-2);
  border-top: 1px solid var(--cb-line-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wt-fr:first-of-type { border-top: 0; }
.wt-fr em { flex: none; font-style: normal; font-size: 10px; font-weight: 700; color: var(--cb-ink-3); font-variant-numeric: tabular-nums; }
.wt-fr b  { font-weight: 700; color: var(--cb-ink); }

/* ---------------------------------------------------------------------------
   BREAKPOINTS — phone is its own composition, not a stacked desktop.
   Twice on the section above, a fix landed at desktop and vanished behind a
   breakpoint at 390px. Every focal mass below still renders at 390.
   ------------------------------------------------------------------------ */

/* Cards go sideways in the range where .wt-cards is still one column but the
   page is wide, so an artefact never sits in a puddle. */
@media (min-width: 760px) and (max-width: 979px) {
  .wt-1, .wt-3, .wt-4 {
    display: grid;
    grid-template-columns: minmax(0, .42fr) minmax(0, .58fr);
    grid-template-rows: auto auto 1fr;
    column-gap: clamp(32px, 4vw, 56px);
    align-items: start;
  }
  .wt-1 > h3, .wt-3 > h3, .wt-4 > h3 { grid-column: 1; }
  .wt-1 > p,  .wt-3 > p,  .wt-4 > p  { grid-column: 1; }
  /* align-self: end, NOT align-items: center on the card. Centring the copy is
     exactly what produced the void the band above spent two rounds measuring.
     The heading stays on the artefact's top line and the link/caption line
     drops to its bottom line, so the column reads as a full column instead of
     a short block with 120px of nothing under it. */
  .wt-1 > .wt-foot, .wt-3 > .wt-foot, .wt-4 > .wt-foot {
    grid-column: 1; grid-row: 3; align-self: end; margin-bottom: 0;
  }
  .wt-1 > .wt-art,  .wt-3 > .wt-art,  .wt-4 > .wt-art  { grid-column: 2; grid-row: 1 / 4; margin-left: 0; }
  /* Card 3's left crop is switched OFF in this band and only here. The artefact
     sits in the right-hand column, so its left edge is nowhere near the card's
     left border — the 40px inset would be a crop inside the panel, cutting the
     register's own labels for no reason. Card 1 keeps its crop: its artefact
     still runs 26px past the card's right border in this layout. */
  .wt-3 > .wt-art { clip-path: none; }
  .wt-2 .wt-phone { right: 6%; }
  .wt-2 .wt-behind { width: 340px; }
}

/* THE COUNT GETS ITS OWN LINE BELOW 1180px, INSTEAD OF BEING DELETED.
   The tab used to run PRE-STARTS and the count side by side on one line: 60px +
   9 + 92 + 22 of padding = 183px wide. Below 1180 there is only ~159px of list
   left of the handset at 980 and ~132px at 390, so the right-hand end of that
   tab — which is the count — was landing under the phone. The old fix was
   `display: none`, which threw away the one number the office list exists to
   report.

   Stacked, the tab is as wide as its widest LINE, not the sum of both: 95px as
   rendered. Measured clearance from the count's right edge to the handset —
   26.1px at 390, 74.4px at 980 — so it reads at every width below 1180, and the
   label-over-value shape is what a real stat tile does anyway. The count drops
   its uppercase and most of its tracking on the second line — 92px to 72px —
   because at 9.5px a tracked-out all-caps count is the least legible way to set
   a number, and it is now a value rather than a label. */
@media (max-width: 1179px) {
  .wt-behind-top {
    display: inline-grid; justify-items: start; gap: 1px;
    line-height: 1.3;
  }
  .wt-behind-top span { text-transform: none; letter-spacing: .04em; }
}

/* The narrow end of the desktop grid, and the tightest place on the page above
   the phone breakpoint. Card two's artefact is only 393px wide at a 980px
   viewport, and the handset takes 262 of it. Measured there: with the plate, the
   name and the status all on one line the row ran to 171px and the handset
   started at 137, so "not signed" — the one thing that list is in the card for —
   was rendering underneath the phone and could not be read.

   THE PLATE IS WHAT GOES, NOT THE NAME. This used to be the other way round, on
   the reasoning that the plate carries the same identity in 34px. It does not:
   a reader who has never seen this fleet gets nothing from "EX12", and with the
   name hidden every row rendered as a code and a time with no subject in it.

   Measured at a 980px viewport, which is the tightest point of this range. The
   row's readable zone runs from -5px to the handset's left edge at 143px, so
   148px, and it has to hold a name, a gap and a status:

       plate 34 + gap 9 + name lane 84 + gap 9 + status 53   =  189   overflowed
       name lane 84 + gap 9 + status 53                      =  146   1.4px clear
       name lane 76 + gap 8 + status 53                      =  137   10.4px clear

   10.4px is the measured worst case across all seven rows at 980, and it is the
   "not signed" row. Every other row clears by 25px or more, and by 28.8px at
   1024. Re-measure THIS row if you touch the row's padding, gap or type size.

   So the plate goes AND the name lane comes in from 84px to 76px. 76 is not a
   guess either: the longest name in the list, "Water Truck 2", measures 75.1px,
   so the lane still fits every name and the times still line up in a column
   instead of going ragged. */
@media (min-width: 980px) and (max-width: 1179px) {
  .wt-phone { width: 250px; }
  .wt-brow { gap: 8px; }
  .wt-brow .wt-plate { display: none; }
  .wt-brow b { min-width: 76px; }
}

/* THE TABLET BAND KEEPS THE OLD ROW GAP. Between 521 and 979 the cards are still
   one column but card two's office list is six rows deep, and at the section's
   new 48px gap the last row finished 2.8px INSIDE card three's top border —
   a stray row touching a card it has nothing to do with. 56px puts 8px of clear
   air back. This range is not the judged one and 8px here costs the phone
   nothing, because the phone is under 521. */
@media (min-width: 521px) and (max-width: 979px) {
  .wt-cards { row-gap: 56px; padding-bottom: 64px; }

  /* ══ LD#1, CLOSED IN ROUND 6 ════════════════════════════════════════════════
     The office list had no `left` of its own in this band and fell through to the
     base -16px, which put its left edge at x=4.1 against the section's content
     line at x=24 — 19.9px adrift, at every width from 521 to 979.

     There is nothing to cross into here. The cards are full width in this band,
     so card 1 is not beside card 2, and the -16px exists only to interlock with
     it at >=980. An offset with nothing behind it is not a bleed, it is a slip.

     4px puts the list's own left edge on the grid line at 24. It was filed as a
     tidiness defect and it was not: at 640 and 860 the overhang was severing the
     header to `re-starts`, a real word, which is the same fault as LD#3 and the
     reason LD#1 was reclassified. Verified after the change at 521, 560, 640,
     760, 860 and 979 — the header reads "Pre-starts" whole at all six. */
  .wt-2 .wt-behind { left: 4px; }

  /* Same 11 + 9 as the phone band, and for the same reason: card 1's own clip
     does the severing here too, and it was landing 0.4px past the colon so every
     row read a bare "31:". The >=980 rule does not reach this band and the <=520
     one stops below it, so the column fell through to the base padding and lost
     its digit. Measured at 521, 560, 600, 640, 680, 700, 720, 860, 920 and 979:
     the shift needed is 8.8px at every one of them, because the cut tracks the
     card's right border and the column is flex-end aligned against it.

     ROUND 7 — 20px BECOMES 28px, so the totals are not severed here either.
     Adam's call, 2026-08-16, and it is the same decision as the >=980 block: the
     week column is the card's proof and it is not the thing to make the reader
     decode. 20px kept a digit after the colon, which satisfied the round-6 rule;
     28px keeps the whole value. The cut sits 25.00px in from the cell's right
     edge in BOTH sub-980 bands (61.00 in the 86px cell at <=520, 59.00 in the
     84px cell here), so 28px is one value covering both and it clears by 3.00px,
     the round-6 house minimum. */
  .wt-1 .wt-tot { padding-right: 28px; }
}

/* ══ LD#13, ROUND 7 — THE 772-836px HOLE, AND WHY NO SWEEP HAD SEEN IT ═════════
   FOUND BY ROUND 7, PRE-EXISTING, NOT INTRODUCED BY IT. Verified by running the
   band at both the old 20px and the new 28px: at 20px the totals were severed
   from ~775 all the way past 845; at 28px the band is 772-836. The 8px shift
   narrowed the hole. It did not open it.

   WHY SIX ROUNDS OF SWEEPS MISSED IT. Round 6's nineteen widths include 760 and
   860 and nothing between them. Both are clean, for two different reasons — 760
   has the whole column hidden past the clip, 860 has it comfortably inside — so
   the sweep stepped over a 64px hole in the middle. A width list is only as good
   as its worst gap, and this is the argument for walking a band rather than
   sampling it whenever a layout breakpoint changes the geometry.

   THE CAUSE. At 760 card one's artefact moves into the right-hand column and the
   timesheet grid's track widths are fixed, so the grid stops scaling with the
   artefact that clips it. The WEEK column then drifts across that clip as the
   viewport grows: fully past it at 760-770 (hidden, allowed), straddling it from
   772, and fully inside it from 837 where it finally clears by the 3.00px
   minimum. 832-836 is "not severed" but clears by only 0.41-2.64px, which is
   under the house minimum, so the guard runs to 836 rather than to 831.

   THE FIX IS THE SAME ONE AS LD#11, and deliberately so: where the column cannot
   be shown whole, show no glyphs rather than part of them. The cells keep their
   wash plates, so the column still reads as a cropped SURFACE through the band.
   Result across 760-979: hidden from 760 to 836, then whole with >=3.00px of
   clearance from 837 up. One transition instead of three.

   ⚠️ Specificity (0,3,0) to beat `.wt-ts-grid .lit.wt-tot`, same trap as the
   LD#11 guard — a plain `.wt-1 .wt-tot` is (0,2,0) and the lit row would have
   kept painting while the other five went blank. */
@media (min-width: 760px) and (max-width: 836px) {
  .wt-1 .wt-ts-grid .wt-tot,
  .wt-1 .wt-ts-grid .wt-wk { color: transparent; }
}

@media (max-width: 759px) {
  /* Same corner, but flush with the artefact instead of 18px past it. At 390px
     the artefact already ends 20px past the card's right border at x=386, and
     another 18 would put the panel off the side of the phone. Measured: 0px of
     horizontal scroll at 390.
     -52 rather than -80 for the same reason as the -50 at 520 and below: the
     board is 36px shorter and the section's trailing padding 44px tighter, and
     at -80 the feed's bottom edge finished 3px above the graphite band. At -52
     it clears by 31px and its top still lands below the last legible row. */
  .wt-feed { right: 0; bottom: -52px; width: 244px; }
  /* The feed lands on the right-hand end of the footer strip below 760px, so
     the second stat there would be sitting under it. */
  .wt-b-foot span { display: none; }
}

@media (max-width: 520px) {
  /* Monday and Tuesday leave the timesheet. At 390px the artefact is 362px
     wide, and six columns would push the WEEK total — which is the whole point
     of the card — off the right-hand edge. So the phone shows the two most
     recent days, the running one, and the total. Same artefact, composed for
     the width it is actually being read at. */
  .wt-ts-grid { grid-template-columns: minmax(96px, 1fr) repeat(2, 72px) 86px; }
  /* MUST be written as a child selector. `.wt-d1 { display: none }` on its own
     loses to `.wt-ts-grid > span { display: flex }` on specificity, and the
     render proved it: the columns dropped to four while all six cells stayed in
     the flow, so every row wrapped and the table turned to mush. */
  .wt-ts-grid > .wt-d1,
  .wt-ts-grid > .wt-d2 { display: none; }

  /* The WEEK head needs no override here. Its unscoped 27px puts "Week" whole at
     26.9-59.0 against this width's 61.00 cut, clearing by 2px. The totals below
     it are untouched too: card one's own clip severs them to "31:" at 61.00
     without help, which is why .wt-tot's padding is scoped to >=980. */

  .wt-art { margin: 0 -44px -30px -25px; }
  /* Same value as desktop. The handset is 244px wide here instead of 272 and
     nothing inside it wraps differently at that width, so the screen needs the
     same 296px and the frame the same 322. 314 was tried first and MEASURED
     WRONG: the screen's scrollHeight came back 311 against an offsetHeight of
     294 and the signed bar's second line was cut off at the bottom of the glass
     — the same fault 356px used to produce before round 3. If you change any
     padding inside .wt-screen, re-read scrollHeight against offsetHeight here
     before you trust the render. */
  .wt-art-glass { height: 322px; }
  .wt-phone { width: 244px; border-radius: 32px; }
  .wt-phone::after { border-radius: 32px; }
  /* THE LIST SITS ON THE GRID AT PHONE WIDTH. IT DOES NOT CROSS ANYTHING HERE.
     At desktop this block starts 16px left of the card's border, which is a real
     crossing because there is a whole other card behind it to cross into. At
     390px there is nothing to the left but page margin, and -16px plus the 4.4px
     the -1.4deg tilt takes off the top corner put the tab at x=3.7 while every
     other element in the section — eyebrow, heading, card, copy — starts at
     x=26. Three pixels off the screen edge is not a bleed, it is a slip, and it
     made the whole artefact read as a layout that had overflowed.

     +6px is measured to cancel the tilt: 24 (artefact) + 6 - 4.4 = 25.7 as
     rendered, so the tab's top-left corner lands on the section's own content
     line at x=26 instead of 3.7. Re-measured after the change: 25.7. The list
     still crosses the handset, and still runs past the card's BOTTOM border, so
     the card keeps its depth without faking one on the left. */
  .wt-behind { width: 250px; top: 12px; left: 6px; }

  /* THE ROW GOES TO TWO LINES, AND THE PLATE GOES.
     There is ~132px of list left of the handset at 390px. A plate, a name and a
     time on one line need 190. The previous round's answer was to hide the NAME,
     which left every row reading "EX12  06:04" — a code and a time with no
     subject in it, seven times over, while the count that would have explained
     them was hidden too. That is what made this card fail.

     So the row is re-composed for the width it is actually read at, which is the
     rule this whole breakpoint block exists for. The plate goes, because the
     name is the part a reader can actually use and the plate is the part that
     duplicates it. The time drops to its own line under the name, which costs
     nothing horizontally and buys 43px:

         widest name   Water Truck 2   75px
         widest time   not signed      53px
         row needs     9 + 75          84px   against 132px available

     Measured at 390px, worst case of all six rows: the name ends 25.6px short of
     the handset ("Water Truck 2") and the status ends 47.2px short ("not
     signed"). Nothing sits under the phone. Every row carries its machine. */
  .wt-brow {
    display: grid; gap: 1px;
    padding: 8px 10px; line-height: 1.25;
  }
  .wt-brow .wt-plate { display: none; }
  .wt-brow b  { min-width: 0; }
  .wt-brow em { font-size: 10.5px; }
  /* Six rows, not seven. Two-line rows measure 46.5px against the one-line 40,
     and the stacked tab is 34, so the zone from top:12 to the artefact's 374
     bottom — 362px — holds six rows (34 + 7 + 6x46.5 + 5x7 = 355) and not seven
     (408). Measured after the change: the list's bottom edge lands at 364.7 of
     374, so it still fills the zone the way seven one-line rows did, and it
     still runs past the card's bottom border.

     Written from the END of the list because every child of .wt-behind is a div —
     an :nth-of-type number counted forwards would silently move if a row were
     ever added. A 16-machine fleet showing 5 is no less true than one showing 7,
     and the unsigned TP09 row is third, so it is never one of the ones dropped. */
  .wt-behind .wt-brow:nth-last-of-type(-n+2) { display: none; }
  /* 278px, down from 306. 46 (bar) + three full job rows at 54 + 26px of the
     fourth + 44 (footer strip) — the fourth row is cut through its own type by
     the frame, which is the same "there is more of this" cue the timesheet and
     the service history now carry. */
  .wt-board { height: 278px; }
  /* LD#6, round 6. 54 → 47, and the board's 278px height does NOT change, so
     .wt-feed's measured position is untouched.

     Round 5 sized this so the footer strip cuts the fourth job row as a "there is
     more of this" cue. The cut landed 10.5px into the word "Yard" — measured: row
     4 ran 5631.3-5685.3, its type 5644.8-5662.3, and the strip's top edge sat at
     5655.3, straight through the x-height. A word halved horizontally is the same
     fault as a word shaved vertically. It survived five rounds because every check
     this loop has run looked at type meeting a LEFT or RIGHT cut, and nothing ever
     looked at a horizontal one.

     ⚠️ MOVING THE CUT INTO THE GAP ABOVE ROW 4'S TYPE DOES NOT WORK, and the
     first attempt at this proves it. `.wt-b-list li` is `align-items: baseline`
     and `.wt-b-num` is 23px against the site name's 14px, so the numeral's ink
     starts only 4px below the row's top edge — measured at 57px rows, the strip
     was 15px and still cut 11px into the "7". Any strip wide enough to see is wide
     enough to sever that numeral. There is no gap to aim at.

     So the cut goes BETWEEN rows instead, which is the honest version of the same
     move: 46 (bar) + 4 rows at 47 (188) + 44 (footer) = 278 exactly, and the strip
     lands flush on row 4's bottom border. Nothing is severed and all four sites
     read, which also returns the count that row 4 was carrying.

     The list has exactly four items, so nothing is being hidden by this — the cue
     it used to give was pointing at a fifth row that does not exist. Card 4 still
     crops: the board is severed at the card's right border by .wt-4 .wt-board's
     clip, which is untouched, and the feed still crosses that border in front of
     it. This card loses none of its crop, only the severed letterform.

     ⚠️ AT 390 THE FEED STILL COVERS ROW 4's "7" AND "4 machines in", and that is
     NOT a regression introduced here — check it before you go moving the feed.
     .wt-feed is anchored to .wt-art's bottom, so its top always sits ~37.5px above
     the footer strip no matter what the rows do. Row 4 lives in the 47px directly
     above that strip, so the feed has always reached into it; at 54px rows it
     covered the whole 26px sliver that showed. The number was unreadable before
     this change and is unreadable after it. What changed is that "Yard" is no
     longer cut in half.

     An opaque card lying over a list is a LAYER, which is the mechanism working —
     it is not the same fault as a severed word, and it is what QuickBooks does
     with its floating UI cards. Moving the feed down far enough to clear the row
     needs 29.5px, which puts its bottom 10.5px past the section edge with a 48px
     shadow landing in the graphite band below. Round 5 measured that and rejected
     it, and that judgement still holds. */
  .wt-b-list li { height: 47px; padding: 0 14px 0 30px; }
  .wt-b-ping { left: 15px; }
  .wt-b-foot { height: 44px; padding: 0 14px 0 30px; }
  .wt-b-sub { width: 92px; font-size: 10px; }

  /* LD#3, round 6 — the totals must keep a digit after the colon at this width.
     At >=980 card two's list is what severs these and .wt-1 .wt-tot's 27px was
     measured against that. Below 520 the list is nowhere near card one and card
     one's OWN clip does the cutting at x=366, where the numerals ended at 380 —
     the colon's ink finished at 365.59, 0.41px inside the cut, so every row
     rendered a bare "31:". A trailing separator promises a value and does not
     deliver one, which reads as broken data rather than as a number carrying on
     under the edge.

     ⚠️ 20px, and it is NOT the 9px of shift this needs. These cells already carry
     a symmetric `0 11px` from the base rule, so the value here REPLACES that 11
     rather than adding to it — a naive `padding-right: 9px` moves the numerals
     2px the WRONG way, which is exactly what happened on the first attempt. The
     shift is `11 + 9`. The same trap as `.wt-ts-act` in the round-6 block, one
     rule down.

     Safe to grow because the column is a fixed `86px` in this width's
     `grid-template-columns`, not an auto track, so the padding cannot widen it and
     push the row around. All six rows are flex-end aligned with the same character
     count, so one value covers the column.

     ROUND 7 — 20px BECOMES 28px, AND THE PARAGRAPH BELOW IS WHAT IT REVERSED.
     Adam's call, 2026-08-16. 20px left the "0" ending at 363.8, clear of the 366
     cut by 2.2px, with ~2.2px of the final "1" still showing past it. That
     fragment was argued as the crop working — a severed numeral is allowed by the
     round-6 rule and says the value carries on.

     Two critics in two consecutive rounds read it the other way (round 5 Craft,
     round 6 Brief): at phone width the week column, which is the card's proof of
     "timesheets build themselves as the week goes", renders 31:0 31:5 29:1 32:5
     and cannot be read. That is a pattern, not noise. 28px puts the text's right
     edge at 363.00 against the same 366 cut — 3.00px of clearance, the round-6
     house minimum, and the whole value legible. THE CARD STILL CROPS: .wt-art is
     still clipped flush at the card's right border and the panel's surfaces are
     still severed there. Only the numerals stepped out from under it. */
  /* ══ LD#16, ROUND 8 — 28px BECOMES 36px, AND WHY "3.00px MINIMUM" WAS THE BUG ══
     Round 7 hit its target exactly: 3.00px of clearance, nothing severed. The
     round-7 CRAFT critic failed it anyway, and it was right. THREE PIXELS IS THE
     AMBIGUOUS MIDDLE — too much to read as a deliberate severance, too little to
     read as room, so the card's proof number reads as a box too small for its
     contents. Meanwhile every cell in this grid carries an 11px gutter on its
     LEFT, so the payoff column was the only thing in the card with no gutter.

     A MINIMUM IS THE WRONG SHAPE OF TARGET. A minimum is satisfied by landing on
     it, and landing on it is precisely the failure. So this is not a bigger
     minimum: 36px gives the WEEK column the SAME 11px gutter every other cell
     already has, measured from the same cut at x=366. Ink lands at 355.

     36px IS ALSO THE CEILING, WHICH IS WHY IT IS NOT 40. The track is a fixed
     84px and padding-left is 11, so the content box is 391 - 11 - 36 = 33px wide
     against text that measures ~33px. At 40px the text would overflow a cell
     that is `overflow: hidden` + `flex-end`, and clip on its LEFT — a brand new
     severance at the other end, reading "1:01". Anything above 36 must widen the
     track first. Adam's call, 2026-08-16: pull the numbers in, do not push the
     crop out.

     THE CARD STILL CROPS. .wt-art is still clipped flush at the card's right
     border and the panel's surfaces are still severed there. Only the numerals
     stepped out from under it, and now they step clear rather than graze it. */
  .wt-1 .wt-tot,
  .wt-1 .wt-ts-grid .wt-wk { padding-right: 36px; }

  /* ══ THE PILL IS THE ONE THING PADDING CANNOT FIX ════════════════════════════
     Everything else in LD#16 is TYPE grazing a cut, and the shared block above
     moves type inward with padding while the box, its background and its borders
     stay put and stay cropped. That is the whole crop mechanism and it is right.

     "Export to payroll" is different because it HAS A BORDER. Padding moved its
     letters 8px inward and left the border exactly where it was, so the pill
     still kept its left rounded corner and both long edges and still stopped
     dead with NO RIGHT CAP. A severed numeral reads as a number continuing. A
     capless pill reads as a broken control — that is the line the crop device
     cannot cross, and it is why LD#16 was the round's biggest gap.

     So at phone width the BOX moves, not the type: margin-right pulls the whole
     pill inside the card to the same 11px gutter, and padding goes back to
     symmetric 8px because a pill that is fully inside has no cut left to clear.
     Keeping the 21px here would have double-shifted it to a 32px gutter.
     ⚠️ At >=980 the pill is deliberately left cropped by card two's list, which
     is a real overlap between two cards rather than a card's own edge. Do not
     "fix" it there. */
  .wt-1 .wt-ts-act { margin-right: 22px; padding-right: 8px; }

  /* ══ LD#11 GUARD, ROUND 7 — BELOW 350 THE COLUMN IS HIDDEN, NOT SEVERED ══════
     Round 6 accepted LD#11: at 320 the whole WEEK column falls past card one's
     clip and renders nothing. Hidden is allowed; severed is not. That state was
     an accident of geometry rather than a rule, and moving the totals 8px left
     broke it — round 7 measured a 93%-visible "3" appearing at 320, which is a
     stray severed numeral and exactly what the round-6 rule forbids.

     WHY NO PADDING VALUE FIXES IT. The card stops shrinking at ~350: the cell's
     right edge is pinned at 351 from 350 all the way down to 320, while the clip
     keeps tracking the viewport left. So the gap the column has to clear grows as
     the screen narrows, and the two requirements pull apart:

         width   padding to show it whole   max padding that still hides it
         320             58.00                        21.30
         330             48.00                        11.30
         340             38.00                         1.30
         350             28.00                        none — cannot be hidden

     No single value satisfies either column across 320-349, and the "show it
     whole" values push the numerals out of their own cell into the Thu column.
     So below 350 the text is simply not painted.

     colour, NOT visibility or display. The cells keep their wash plates and the
     lit row keeps its graphite, so the column still renders as a cropped SURFACE
     at these widths — which is LD#11's accepted state exactly, and keeps the
     crop mechanism doing its job. Only the glyphs go. 350 is the breakpoint
     because that is the measured width at which 28px starts clearing the cut.

     ⚠️ THE SELECTOR CARRIES .wt-ts-grid ON PURPOSE. The lit row is styled by
     `.wt-ts-grid .lit.wt-tot`, which is (0,3,0); a plain `.wt-1 .wt-tot` is
     (0,2,0) and LOSES to it, so row one would have kept painting its fragment
     while the other five went blank. Matching (0,3,0) and sitting later in the
     file is what makes this apply to all six. */
  @media (max-width: 349px) {
    .wt-1 .wt-ts-grid .wt-tot,
    .wt-1 .wt-ts-grid .wt-wk { color: transparent; }
  }

  /* -50, not -78. The board lost 28px and the section lost 44px of trailing
     padding at the same time, and at -78 the feed's bottom edge finished 9px
     above the graphite band with its 48px shadow landing inside it. Re-measured
     at 390: the feed's top lands at 195.5 in the board's own coordinates, which
     is below the last readable row's type (that row ends at 184) and above the
     job row the frame already cuts, so it still covers nothing legible. Its
     bottom now clears the section's edge by 37px. It still hangs 79px past the
     card's bottom border and still crosses its right one. */
  .wt-feed { bottom: -50px; }

  /* ══ THE PHONE IS ITS OWN COMPOSITION ═════════════════════════════════════
     Round 4. Until now all four cards used one construction at 390 — light text
     box, product panel below it, bleeding right — stamped out four times. That
     is the desktop composition unstacked, not a phone layout. Three of the four
     now build differently, and the fourth is the one round 3 measured and won:

       card 1  panel CROPPED flush at the card's right border. The only card
               here whose artefact does not cross anything: it is cut instead.
       card 2  unchanged, and deliberately. The handset crosses the right border
               and hangs below, the office list sits on the section's own grid
               line at x=26, every row carries its machine name and the count is
               visible. That was round 3's whole job and it is not being reopened
               to make the phone view look varied.
       card 3  ARTEFACT FIRST. The register leads the card, flush on its top
               border, cropped at its left border, and the heading and copy read
               underneath it. Nothing else on the page puts the picture first.
       card 4  the only card that CROPS AND CROSSES ON THE SAME EDGE. The board
               is severed at the card's right border — the headcount total and
               each row's "N machines out" are cut mid-string — and the feed
               floats across that same border in front of it, uncut. Card 1
               crops the same edge but has nothing crossing it.

     So the four differ in what leads, which edge is broken, whether the edge is
     a crop or a crossing, and how much of the card the artefact takes. */
  /* ══ LD#5, ROUND 6 — CARD 3 NO LONGER LEADS WITH ITS ARTEFACT ════════════════
     Adam's call, 2026-08-16. The paragraph above describes what this USED to do:
     `order: -1` put the register above the heading so the four cards would not
     read as one phone layout stamped out four times. That reasoning was sound and
     it is kept above deliberately, because this reverses it and the next person
     needs to know it was a decision and not an oversight.

     WHY IT WAS REVERSED: the round-5 Craft critic, with no access to any of that
     reasoning, read the same pixels as a fault — the visitor scrolls into an
     unexplained hour meter, a service interval and a history, and only afterwards
     reaches "Plant and maintenance" and the sentence explaining what any of it
     was. Measured at 390: the artefact started 308px ABOVE its own heading, while
     cards 1, 2 and 4 all led with theirs, and card 3 itself leads with its heading
     at 1440. It contradicted the other three cards AND its own desktop order.

     This is a marketing page whose job is to make a buyer understand four features.
     Comprehension beats variety when the two pull apart. The other three cards
     still differ in what they crop, which edge breaks, and whether that edge is a
     crop or a crossing — the band does not go flat for losing this one difference.

     The card keeps its LEFT crop, which is the thing that actually made it
     distinct: clip-path still takes 40px off .wt-art's left edge, so the register
     is still severed at the card's left border. Only the reading order changed.

         order          removed, so the artefact falls back to DOM order (last)
         margin top     -25px → 0     it no longer needs to sit flush to the card's
                                      top border, because it is not at the top
         margin bottom  16px → -30px  matches the base .wt-art at this width, so it
                                      runs past the card's bottom border like the
                                      other three instead of stopping short
         padding-bottom 24px → 0      matches .wt-card's base. The 24px existed to
                                      give the trailing copy room; the copy no
                                      longer trails. */
  .wt-3 {
    display: grid;
    padding-bottom: 0;
  }
  .wt-3 > .wt-art { margin: 0 -25px -30px -65px; }

  /* -74px keeps the board wide at this width — it is what stops the four job
     rows from squeezing — and the board is still severed. What CHANGED in round
     5 is WHAT severs it.

     It used to be cut by the screen: at 390 the board reached x=415 and
     .wt-section's overflow-x: clip took the last 25px off, which rendered the
     bar's headcount as "34 on sit". That was defended here as the Supabase move,
     and it was wrong on this card. Supabase cuts type on a container's own
     border, where the border is visibly the thing doing the cutting. Card four
     has a 24px page margin, so the board crossed its card's border, crossed the
     page margin, and then vanished at the screen edge with NOTHING visible
     cutting it — which reads as a panel that overflowed, not as a crop. Two
     independent reads landed on it: the round-4 builder flagged it against
     itself, and the Brief critic found it cold.

     So the board is now clipped at the card's border at this width too, by the
     49px inset declared with the other crops further up. The headcount is still
     severed, but by a line you can see, and the number survives whole — "34 on"
     rather than "34 on sit", which cut the label and kept a fragment of it.

     THE PHONE COMPOSITION STILL DIFFERS FOUR WAYS, which was the point of the
     note above: card 1 crops right and nothing crosses; card 3 leads with the
     artefact and crops left; card 4 crops right AND has the feed crossing that
     same border in front of it; card 2 crosses without cropping at all.

     Nothing scrolls, and this is the assertion to re-run if you touch either
     number: scrollWidth still equals innerWidth at 320, 360, 390, 430 and 520.
     The feed goes out to 34px so it still ends 15px past the card's border and
     still sits 9px inside the screen instead of being clipped with the board. */
  .wt-4 > .wt-art { margin-right: -44px; }
  .wt-4 .wt-feed { right: 4px; }
}

/* Reduced motion. The global block kills the animations, but it does not put
   anything back: every one of the three loops below is declared with its
   RESTING state hidden, so without this section the timesheet shows an empty
   cell, the hour meter shows a missing digit, and the signature well is blank
   with a "Signed 06:04" bar under it. Each one is frozen on a real, complete,
   internally consistent frame instead.

   If you add a fourth loop here, add its static state at the same time. This is
   the third round in a row a loop has been declared from a hidden start. */
@media (prefers-reduced-motion: reduce) {
  /* Card 1 — the running cell, frozen on the middle frame, AND the week total
     frozen on the frame that MATCHES it. The cell rests on 3:54, so the total
     must rest on 31:02. Freezing them apart would hand a reduced-motion reader
     the exact arithmetic fault LD#15 exists to remove — the same "two halves of
     one fact" trap already written up for card 2's signature below. */
  .wt-run i { opacity: 0; }
  .wt-run i:nth-of-type(2) { opacity: 1; }
  .wt-tot-run i { opacity: 0; }
  .wt-tot-run i:nth-of-type(2) { opacity: 1; }

  /* Card 2 — the signature, fully drawn, AND the bar that reports it frozen on
     its signed state. These two are one fact; freezing them apart is exactly the
     contradiction round 4 removed from the running loop. */
  .wt-sig path { stroke-dashoffset: 0; opacity: 1; }
  .wt-sg-un { opacity: 0; }
  .wt-sg-ok { opacity: 1; }

  /* Card 3 — the hour meter, frozen on .6, which is the reading the "67 h to
     go" chip beside it is derived from. */
  .wt-tenth b { opacity: 0; }
  .wt-tenth b:nth-of-type(1) { opacity: 1; }
}

/* ============================================================================
   SHARED PAGE COMPONENTS
   Added 2026-08-16, building timesheets.html — the first page stamped off
   index.html.

   These three are here because the HOME PAGE happened not to need them, not
   because they belong to timesheets. Each one is wanted by at least two more of
   the nineteen: .steps by pre-starts, plant and live view; .points by security,
   about and pricing; .tick anywhere a number has to prove it is live.

   ⚠️ THE RULE THAT KEEPS THIS SECTION HONEST, and it is the whole reason page
   one was built alone. A page may NOT invent a local class for one of these
   shapes. If a page needs a variant, add a modifier here so every later page
   inherits it. Nineteen pages each rolling their own numbered-steps block is
   the single most likely way this stamping run ends up with twenty dialects.
   ========================================================================== */

/* -- Numbered steps: the "how it works" row ---------------------------------
   Deliberately NOT three more .cards. A page that spends one row of bordered
   cards on the problem and a second identical row on the answer reads as a
   template rather than a composition, and every one of the nineteen has both.
   Steps are open instead: a hairline above each, a numeral, no box.

   The numeral is a GREYSCALE outline, not a filled violet disc. Violet is
   things you press (BRAND.md rule 3) and a step number is a label, same call
   already settled for .tier-tag. It also keeps bar.md mechanism 2's budget
   free for the one coloured element the section actually needs.

   Geometry is lifted from .card-icon on purpose — same 38px, same border, same
   surface — so a step number and a card icon are visibly the same family.
   -------------------------------------------------------------------------- */
.steps { display: grid; gap: var(--cb-gap-4); margin-top: var(--cb-gap-4); }
@media (min-width: 900px) {
  .steps { grid-template-columns: repeat(3, 1fr); gap: var(--cb-gap-3); }
}

.step { border-top: 1px solid var(--cb-line); padding-top: var(--cb-gap-3); }
.step h3 { margin-bottom: var(--cb-gap-1); }
.step p  { font-size: 15.5px; margin: 0; max-width: 42ch; color: var(--cb-ink-2); }

.step-num {
  display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: 50%;
  background: var(--cb-surface-2); color: var(--cb-ink-2);
  border: 1px solid var(--cb-line);
  margin-bottom: var(--cb-gap-2);
  font-size: 14px; font-weight: 800;
  font-variant-numeric: tabular-nums;
}

/* -- .hero-sub: the hero for every page that is NOT the landing page ---------
   Adam's call, 2026-08-16, from the first look at a finished sub-page:
   *"at first when it opens I thought I was still on the landing page."*

   He was right, and the reason is worth writing down because it is not about
   the words. The two heroes shared a COMPOSITION — big headline left, panel
   right, two buttons, and the identical "7 days free. No card, no contract.
   From $99/month. See pricing" line in the identical spot. The eye reads
   layout before it reads text, so the page announced itself as the same page
   for the first second. The eyebrow was the only thing saying otherwise and it
   was 12px of grey.

   ⚠️ THIS IS NOT A TIMESHEETS FIX. Put .hero-sub on all 18 remaining marketing
   pages. Eighteen pages that each open looking like the landing page is one
   fault repeated eighteen times, and it is invisible on any single page — you
   only see it by arriving from somewhere else, which is how every real visitor
   arrives.

   Three changes, and none of them is the artefact:
     1. the eyebrow becomes a real section LABEL instead of a whisper
     2. the big landing-page air above the headline goes, so the page's own
        content is on screen sooner
     3. the trial note is deleted — the landing page owns that line, and the
        closer at the bottom of every page still carries "7 days free"

   The headline also steps down one size. The landing page keeps the loudest
   voice on the site; a section page is not competing with it.
   -------------------------------------------------------------------------- */
.hero-sub { padding-block: clamp(40px, 5.5vw, 78px) clamp(48px, 7vw, 92px); }

/* The landing page spends up to 96px here on top of the section padding, which
   is bar.md mechanism 7's "140px of air above the headline". A section page has
   not earned that and does not want it: it wants the reader past the hero. */
.hero-sub .hero-copy { padding-top: 0; }

/* One step down from .display's clamp(2.55rem, 6.2vw, 4.4rem). Still a display
   size, still the biggest thing on its own page, and visibly not the home
   page's headline when you land on it. */
.hero-sub .display { font-size: clamp(2.15rem, 4.6vw, 3.4rem); }

/* The section label. Same greyscale recipe as .chip — surface, hairline, pill —
   so it costs NOTHING from bar.md mechanism 2's three-colour budget, which the
   hero already spends on the primary button and the hi-vis chip.

   Deliberately NOT violet and deliberately not a filled block: violet is things
   you press (BRAND.md rule 3) and this is a label you cannot press. Same call
   already settled for .tier-tag on the pricing card.

   Keep the text SHORT — the nav's own word for the section is the right one
   ("Timesheets", "Pre-starts", "Plant"). A sentence in here is an eyebrow again
   and the label stops working. */
.hero-sub .eyebrow {
  display: inline-flex; align-items: center;
  background: var(--cb-surface); color: var(--cb-ink-2);
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-pill);
  padding: 7px 14px;
  font-size: 12px; font-weight: 800; letter-spacing: .12em;
  margin-bottom: var(--cb-gap-3);
}

/* -- .hero-holder: anchor the hero float to the PANEL, not to the figure -----
   ⚠️ THIS FIXES A REAL DEFECT AND IT IS DELIBERATELY OPT-IN. See LD#32.

   .hero-float is positioned against .hero-art, which is the <figure> — and a
   figure's box INCLUDES its figcaption. So the float is pinned 14px off the
   bottom of the CAPTION, not off the bottom of the panel, and it lands on the
   caption's words at every width. It is invisible on index.html only because
   that caption is 40 characters and centred, so at 760 and 1440 the ink happens
   to stop short of the float. Measured on index.html: the caption's ink is
   struck through by 104px at 390, 39px at 520 and 36px at 1000.

   This is the SAME defect the phone artefact had twice, LD#20 and LD#24, and
   .phone-holder is how it was cured there: wrap the artefact and its float in
   their own positioned box so the float is measured off the artefact, then
   DERIVE the caption's clearance from the hang instead of typing a constant.
   That constant was hand-measured twice on the phone and was wrong both times.

   ⚠️ WHY IT IS SCOPED TO .hero-holder RATHER THAN JUST FIXING .hero-float.
   index.html's hero is BANKED work (Adam's standing decision, 2026-08-16) and
   changing .hero-float outright would move it without him asking. Every rule
   below needs a .hero-holder in the markup, so index.html renders byte for byte
   as it did. Adopting the fix there is one wrapper div and nothing else, and it
   is written up as LD#32 for Adam to call.

   The two constants are declared on .hero-art so the caption can read them.
   Change them THERE, never here.
   -------------------------------------------------------------------------- */
.hero-art { --hero-float-hang: 78px; --hero-float-air: 18px; }

.hero-holder { position: relative; }

/* The float now hangs off the PANEL. The hang is what is left of the float's
   own height after it has crossed the panel's bottom edge — mechanism 5 still
   wants that crossing, so the overlap is kept to about 16px, which sits under
   the last row's baseline rather than over its value. Covering a figure in the
   artefact is LD#24 and it is the fault this component must not reintroduce. */
.hero-holder .hero-float {
  right: -14px;
  bottom: calc(var(--hero-float-hang) * -1);
}
@media (min-width: 620px) { .hero-holder .hero-float { right: -34px; } }

/* DERIVED, not measured. Same rule as .phone-art figcaption: the hang plus the
   air we want under it. Move the float and the caption follows, so the two can
   never go stale against each other. */
.hero-holder + figcaption {
  margin-top: calc(var(--hero-float-hang) + var(--hero-float-air));
}

/* -- A row of cards under a section head ------------------------------------
   One property, and it earns its own name anyway. Every one of the nineteen has
   a row of cards under an h2, and index.html had no class for it because its
   only card row is the pricing tiers. Without this, page after page reaches for
   `.tiers` — which is margin-top and nothing else, but is named for pricing —
   and the class list stops describing the page.
   -------------------------------------------------------------------------- */
.card-row { margin-top: var(--cb-gap-4); }

/* -- Points: a stack of heading + paragraph, split by hairlines --------------
   The thing a split column wants when three .cards inside one half of a
   two-column grid would be three boxes inside a box. No border, no fill, no
   padding to speak of: the hairline is the whole component.
   -------------------------------------------------------------------------- */
.points { display: grid; }
.point  { border-top: 1px solid var(--cb-line); padding: var(--cb-gap-3) 0; }
.point:last-child { padding-bottom: 0; }
.point h3 { margin-bottom: 6px; }
.point p  { font-size: 15.5px; margin: 0; max-width: 46ch; color: var(--cb-ink-2); }

/* -- .tick: a number that advances on its own -------------------------------
   The generic version of .wt-tot-run, which was written for one cell on the
   home page and is welded to that grid's padding. This one is welded to
   nothing, so any page can put a live figure in an artefact.

   It reuses cb-wt-tick and the SAME 4.4s delays as the home page's running
   cells, so two artefacts on two pages advance on the same beat.

   ⚠️ THREE THINGS IT WILL NOT FORGIVE, all learned the expensive way on the
   home page:

   1. FRAME 1 STAYS IN NORMAL FLOW AND SIZES THE BOX. Frames 2 and 3 are
      absolute on top of it. Do not "simplify" this to a width in `ch` — `1ch`
      is the advance of the "0" glyph, which in this face at 12px measures
      3.50px against a real digit's 7.20px, so the digits would overflow a box
      half their width. A real glyph cannot size wrong in any font.
   2. EVERY FRAME MUST BE THE SAME NUMBER OF CHARACTERS. Frames 2 and 3 are
      positioned with `inset: 0`, so a shorter frame does not re-centre, it
      sits left and the number appears to jump sideways.
   3. IF A TOTAL IS DERIVED FROM A TICKING FIGURE, TICK THE TOTAL TOO, on these
      same delays. That is LD#15: for two thirds of the loop the home page's
      week row did not add up to its own total, on the card whose promise is
      that timesheets build themselves.
   -------------------------------------------------------------------------- */
.tick { position: relative; display: inline-block; font-variant-numeric: tabular-nums; }
.tick i {
  font-style: normal; opacity: 0;
  animation: cb-wt-tick 13.2s var(--cb-ease) infinite;
}
.tick i:nth-of-type(2),
.tick i:nth-of-type(3) { position: absolute; inset: 0; }
.tick i:nth-of-type(2) { animation-delay: 4.4s; }
.tick i:nth-of-type(3) { animation-delay: 8.8s; }

/* Frozen on the MIDDLE frame, matching how the home page's running cells rest.
   Same rule as the block above: this loop is declared from a hidden start, so
   without this the cell would simply be empty for a reduced-motion reader. */
@media (prefers-reduced-motion: reduce) {
  .tick i { opacity: 0; }
  .tick i:nth-of-type(2) { opacity: 1; }
}

/* ============================================================================
   SHARED PAGE COMPONENTS — the PAGE TWO additions
   Added 2026-08-16, building prestarts.html.

   Four, against page one's six, and every one is a shape the home page and
   timesheets between them had no reason to want:

     .ui-rows li.is-flag   a row in a shared panel that needs attention
     .ui-body              non-row content inside a .ui-panel
     .checks               a completed checklist, machine-drawn
     .sig                  a signature well with a real stroke in it

   ⚠️ The rule above still holds and it is the point of this file. A page may
   NOT invent a local class for one of these shapes. Plant wants .checks for a
   service sign-off; live view wants .is-flag for the machine nobody has
   claimed; every page with a signed artefact wants .sig.
   ========================================================================== */

/* -- .ui-rows li.is-flag: the one row that needs someone to do something -----
   The generic version of .wt-brow.is-out, which is welded to the home page's
   miniature office list. Same recipe, same reasoning, and the reasoning is the
   part worth re-reading — the long note on .wt-brow.is-out has it in full:

     1. --cb-hivis-deep, NOT --cb-hivis. The plain amber measures 1.92:1 on a
        white card and reads as decoration. The deep one measures 3.44:1
        against --cb-surface #F7F8FA, which is exactly the surface .ui-panel
        paints, so that measurement carries over to this component unchanged.
     2. ⛔ NEVER INSIDE A BAND. --cb-hivis-deep is a light-ground value. On
        graphite it measures 4.85:1 against the plain token's 8.70:1, so in a
        band it is strictly the worse mark. A .ui-panel in a band wanting a
        flagged row needs --cb-hivis, which is a different rule and does not
        exist yet because no page has needed it.
     3. THE COLOUR IS NEVER THE MEANING. The row must also SAY what is wrong in
        words, in its own .ui-time, and prestarts.html says "Not signed".
        Colour-blind, greyscale print and forced-colours all still get it. The
        edge makes the row faster to find; it is not what makes it legible.

   An inset shadow rather than a border-left, because .ui-rows li already
   carries a bottom hairline and a border-left would fight it at the corner.
   -------------------------------------------------------------------------- */
.ui-rows li.is-flag { box-shadow: inset 4px 0 0 var(--cb-hivis-deep); }

/* -- .ui-body: content inside a .ui-panel that is not a list of rows ---------
   .ui-panel had exactly two children until now, .ui-head and .ui-rows, because
   both artefacts that used it were lists. A pre-start sheet is not a list: it
   is a checklist and a signature. One property, and it is here rather than in
   the page so that the padding matches .ui-head's 18px on every later page
   that puts a form, a chart or a document inside the same panel.
   -------------------------------------------------------------------------- */
.ui-body { padding: 18px; }

/* -- .checks: a checklist that has been completed ---------------------------
   The generic version of .wt-scr-checks, scaled up off the home page's 11.5px
   miniature to something readable at full size.

   ⚠️ EVERY TICK IS IDENTICAL AND THAT IS THE POINT. There are two checklists
   on this site and they are opposites on purpose. The paper sheet in the home
   page's band uses .pb-checks, where no two ticks repeat — angle, weight, skew
   and ink density all differ, because a biro run down a list four times never
   puts down the same mark twice. This one is the software: a machine drew
   these, all at once, the same way every time. Do not "improve" this by
   varying them. The difference between the two is the argument.

   The mark is a solid ink box with a ground-coloured tick cut across it. Both
   halves read from a token, so there is no colour literal anywhere in it and
   it inverts correctly inside a .band without a second rule.
   -------------------------------------------------------------------------- */
.checks { list-style: none; margin: 0; padding: 0; display: grid; gap: 11px; }
.checks li {
  position: relative; padding-left: 30px;
  font-size: 14px; line-height: 19px; color: var(--cb-ink-2);
}
.checks li::before {
  content: ""; position: absolute; left: 0; top: 1px;
  width: 19px; height: 19px; border-radius: 6px;
  background: var(--cb-ink);
}
.checks li::after {
  content: ""; position: absolute; left: 6.1px; top: 5.3px;
  width: 5px; height: 10px;
  border-right: 2.4px solid var(--cb-ground);
  border-bottom: 2.4px solid var(--cb-ground);
  transform: rotate(42deg);
}

/* -- .sig: the signature well -----------------------------------------------
   The generic version of .wt-scr-sig. The one piece of handwriting on a page
   that is otherwise entirely set type, which is what makes it the focal mass
   of any artefact it lands in.

   ⚠️ THE STROKE IS A SHARED ASSET, NOT DECORATION. The path in
   prestarts.html is byte for byte the path in index.html's card 2, because it
   is the same person signing the same machine: B. Kelly, Excavator 12, 06:04.
   Two different squiggles for one signature is the sort of fault that costs
   nothing to avoid now and cannot be found later without opening both pages
   side by side. If the signature is ever redrawn, redraw it in BOTH.

   It does NOT draw itself here, and that is deliberate rather than lazy. On
   the home page the stroke animates because that card's sentence is "the sheet
   gets signed before the machine moves", so the signing is the subject. Here
   the sheet is a FILED RECORD being produced months later. A record that is
   still signing itself is not a record.
   -------------------------------------------------------------------------- */
.sig {
  margin-top: var(--cb-gap-3);
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-sm);
  background: color-mix(in srgb, var(--cb-ink) 3%, var(--cb-surface-2));
  padding: 9px 12px 3px;
}
.sig-label {
  display: block;
  font-size: 9.5px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase;
  color: var(--cb-ink-3);
}
/* height:auto, NOT a fixed height. .wt-sig on the home page can afford
   `height: 42px` because it sits in a ~150px phone screen and is width-bound
   there anyway. In a full-width panel a fixed height makes an SVG with a
   viewBox scale to FIT that height and then sit centred in a sea of empty
   well — the stroke ends up smaller in the bigger box. Width-bound with a cap
   is the version that survives both a narrow split column and a wide one.

   The cap is 320px because a signature should fill roughly half its line, not
   all of it. Measured: at 390 the well is ~300px and the stroke takes the lot,
   which is right on a phone; at 1440 the well is ~660px and 320 puts the
   stroke at a bit over half, which is where a real one sits. Below 240 it
   started reading as a squiggle in a big empty box. */
.sig-mark { display: block; width: 100%; max-width: 320px; height: auto; color: var(--cb-ink); }

/* ============================================================================
   SHARED PAGE COMPONENTS — the PAGE FIVE additions
   Added 2026-08-17, building live.html.

   Two, against page one's six and page two's four. Page three added none. Both
   of these are the same shape said twice, and it is a shape no page before this
   one had a reason to want: a row that ANSWERS WITH A COUNT.

     .ui-rows.is-board   a row that reads name, number, then a note
     .ui-body.is-foot    a summing strip closing off a .ui-panel

   ⚠️ The rule still holds. A page may NOT invent a local class for one of
   these shapes. Pricing wants .is-board for a plan's limits; the component
   sheet wants .is-foot under anything that totals.
   ========================================================================== */

/* -- .ui-rows.is-board: a row that answers with a count ----------------------
   The generic version of .wt-b-list, which is welded to the home page's
   miniature wall board — that one carries its own local dark palette and a
   clip, and neither travels. This is the same reading order at full size on
   the light ground: WHO, HOW MANY, and then the qualifier.

   ⛔ THE ORDER IS NOT COSMETIC. Site, then count, then the machines note.
   checks/figures.py sweeps every page for
       (site) (crew) (machines) machines (in|out)
   and it reads the RENDERED text in DOM order. Reorder these three and the
   sweep stops matching — and a rule that matches nothing is SILENT, so the
   page would quietly stop being checked rather than fail. That is the worst of
   the three outcomes, which is why it is written here rather than in the page.

   ⚠️ min-width in px, NOT in ch. `1ch` is the advance of the "0" glyph, and in
   this face it is nowhere near one digit — at 12px it computes to 3.50px
   against a real digit width of 7.20px. That trap already cost the home page a
   numeral box. 30px is two tabular digits at 22px plus a hair, measured.

   The note is a fixed width so the counts stay in a true column. Without it a
   longer note on one row pushes that row's numeral left and the column reads
   as a mistake. 96px holds "4 machines in" at 12.5px with room to spare, and
   it is the figure that keeps "Ironbark St" off the ellipsis at 320.
   -------------------------------------------------------------------------- */
.ui-rows.is-board li { gap: 10px; }
.ui-rows.is-board li > b {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ui-count {
  min-width: 30px; text-align: right;
  font-size: 22px; font-weight: 800; letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
}
.ui-note {
  width: 96px; text-align: right;
  font-size: 12.5px; color: var(--cb-ink-3); white-space: nowrap;
}

/* -- .ui-body.is-foot: the strip that sums the rows above it -----------------
   .ui-rows li:last-child drops its bottom hairline, so a plain .ui-body under
   a list runs straight into it with no seam and reads as a fifth row that
   forgot its columns. The hairline is what makes it a FOOT.

   A real board's foot is the only line on it that is about the whole board
   rather than one site, so the bold half leads and the rest is muted. Both
   halves are sums of the rows above — nothing new may be stated here, or the
   foot becomes a second source for a figure the list already gives.

   ⛔ THE BOTTOM PADDING IS LOAD-BEARING. THIS IS THE ONLY COMPONENT ON THE SITE
   THAT ENDS A PANEL WITH A SINGLE LINE OF TEXT, AND THE PANEL'S BOTTOM-RIGHT
   CORNER IS WHERE .hero-float LANDS.

   Every other hero panel ends in a .ui-rows row, and a row is two lines tall
   with its right-hand .ui-time centred in it — so on plant.html the last
   readable text clears the float by 12.2px at every width WITHOUT ANYONE
   HAVING DECIDED THAT. It is a by-product of the row being tall.

   A one-line foot has no such by-product: measured flush, its text overlapped
   the float's box at all 58 widths, by 0.9px. The words were still legible in
   a screenshot, because a line box is taller than its ink — which is exactly
   how this ships broken. A figure one character longer, a fallback font, or a
   fourth line in the float, and it is on the words.

   So the air is DECLARED here rather than inherited from luck, and it reuses
   the token .hero-art already publishes for the gap under a float. The
   fallback is 0px: outside a hero nothing hangs off the panel, and the strip
   should be symmetrical there. Measured after: 17.1px of clearance at every
   width, against the 12.2px the banked pages get by accident.
   -------------------------------------------------------------------------- */
.ui-body.is-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  padding-top: 14px;
  padding-bottom: calc(14px + var(--hero-float-air, 0px));
  border-top: 1px solid var(--cb-line-soft);
  font-size: 12.5px; color: var(--cb-ink-3);
}
.ui-body.is-foot b { font-size: 13px; font-weight: 800; color: var(--cb-ink-2); }

/* ============================================================================
   SHARED PAGE COMPONENTS — the PAGE SIX additions
   Added 2026-08-17, building pricing.html.

   Two, and the running count is now 6 / 4 / 0 / 2 / 2. The jig is still still.

     .ui-val        a row's right-hand value, when that value is not a time
     .tier .checks  a tier card that lists what is in the plan

   ⚠️ The rule still holds. A page may NOT invent a local class for one of
   these shapes. Every page that ever shows an amount, a total or a count in a
   panel row wants .ui-val, and the component sheet wants both.
   ========================================================================== */

/* -- .ui-val: a row's right-hand value, when it is not a time ----------------
   .ui-time is the same slot at the same weight, and it is named for what the
   feed rows put in it. A bill puts money there. Reusing .ui-time for a dollar
   figure would make the class list lie on this page and on every later one
   that shows an amount or a count in that position, and the class list is the
   only thing a dev team reads before the CSS.

   The one real difference is tabular-nums, and it is not cosmetic. A feed's
   times are all four characters and never have to line up as a column. A
   column of money does: with proportional digits $99.00 and $219.00 sit off
   each other by about a digit's width, and a bill whose decimal points wander
   reads as a mock-up of a bill rather than a bill.
   -------------------------------------------------------------------------- */
.ui-val {
  font-size: 13px; font-weight: 700; color: var(--cb-ink-2);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}

/* -- A tier card that lists what is in the plan ------------------------------
   .checks sets `margin: 0` so it can sit flush inside a .ui-panel, which is
   what page two wanted it for. In a tier card it follows .tier-note, which
   also sets `margin: 0`, so without this the first tick sits hard against the
   sentence above it and the card reads as one run-on block.

   A scoping rule rather than a new class, deliberately: the component is
   unchanged, only where it sits has. --cb-gap-3 is the same step .tier-cta and
   .card h3 use, so the card keeps one internal rhythm.
   -------------------------------------------------------------------------- */
.tier .checks { margin-top: var(--cb-gap-3); }

.stack   { display: grid; gap: var(--cb-gap-2); }
.muted   { color: var(--cb-ink-3); }
.centre  { text-align: center; }
.rule    { height: 1px; background: var(--cb-line-soft); border: 0; margin: 0; }

/* ============================================================================
   SHARED PAGE COMPONENTS — the PAGE TEN additions, and they are GROUP B's JIG
   Added 2026-08-17, building signin.html — the first of the seven "ways in".

   Three, and the running count is now 6 / 4 / 0 / 2 / 2 / 0 / 0 / 0 / 3.

     .wayin       the narrow utility column every way-in page sits in
     .notice      a message the APP will put on the page at runtime
     .pw          a password field with a reveal toggle in it

   ⚠️ THESE ARE DELIBERATELY BUILT ON PAGE ONE OF SEVEN, NOT ON PAGE FOUR. Every
   one of them is wanted by at least four of the remaining ways in:

     .wayin   — all seven. It IS the shape of the group.
     .notice  — sign in (session expired, KI#233), worker sign in, check email,
                forgot password, reset password, accept invite. Six.
     .pw      — sign in, worker sign in, sign up, reset password, accept invite.
                Five.

   This is Adam's own call of 2026-08-16 applied to a second family: fix the
   shared jig before running parts off it. Seven pages each rolling their own
   password toggle is the same twenty-dialects failure the note at the top of
   this file exists to prevent, and it is cheaper to stop now than at page four.

   ⛔ A WAY-IN PAGE IS NOT A MARKETING PAGE AND MUST NOT BORROW .hero-sub. It has
   no artefact, no float and no closer. It is a door: one thing to do, and the
   ways out for whoever is at the wrong one. If a later way-in page finds itself
   wanting a hero, the question to ask is what it is selling, and the answer on
   a login screen is nothing.
   ========================================================================== */

/* -- .wayin: the narrow column -----------------------------------------------
   Sits on .wrap and tightens it, the same idiom .faq-wrap already uses. 940px
   rather than the full --cb-wrap because two columns of a marketing width put
   the password field about 600px from the notices beside it, and a form nobody
   can hold in one glance is the one thing a login page cannot afford.

   The heading steps down again, one below h2. A sign-in page is not competing
   with anything: it is the quietest page on the site and the visitor is already
   sold. .hero-sub dropped the display size for a section page; this drops it
   again for a utility page, so the three tiers read landing > section > door.
   -------------------------------------------------------------------------- */
.wayin { max-width: 940px; }
.wayin h1 {
  font-size: clamp(1.7rem, 3vw, 2.25rem);
  line-height: 1.12;
  max-width: 18ch;
  margin-bottom: var(--cb-gap-2);
}
.wayin .lead { margin-bottom: var(--cb-gap-4); }

/* The form is the thing to do, so it leads at every width. .grid-2 stacks in
   DOM order on a phone, which puts the form first and the notices under it
   without a second rule — the reason the markup is ordered form, then notices.

   ⚠️ align-items on .split is `center`, and it is WRONG for this pair. The form
   card is roughly twice the height of the notices beside it, so centring floats
   the notices in the middle of the card with air above and below, and the page
   reads as two things that missed each other. They start on the same line
   instead. -------------------------------------------------------------------- */
.wayin-cols { align-items: start; }

/* -- .notice: a message the APP puts here at runtime -------------------------
   ⛔ THIS IS NOT [data-placeholder] AND THE DIFFERENCE MATTERS. An amber dashed
   placeholder means "the dev team must fill this in before handover". A .notice
   means "the running app writes into this, in front of a customer". They look
   different on purpose, because the first must never ship and the second must.

   ✅ IT EXISTS BECAUSE THE APP ALREADY NEEDS IT. src/pages/AdminLogin.js takes
   an `opts.notice` and renders a session-expiry line into its own banner
   (KI#233), and the forgot-password flow renders a second, neutral one. A
   static page handed over WITHOUT a home for those two messages is a page the
   dev team has to reopen and cut a hole in, which is how a plugged-in design
   starts drifting from the one that was approved.

   HIDDEN BY DEFAULT, and `hidden` is load-bearing rather than tidy: the contrast
   sweep and the 44px sweep both skip what is not shown, so an empty notice costs
   the checks nothing and cannot fail a page for a message that is not there.

   ⛔ .is-bad IS NOT RED, AND DO NOT ADD RED. This palette has no danger colour
   and BRAND.md's budget has no room for a fourth. It takes the same inset edge
   .ui-rows li.is-flag uses, for the same three reasons written out in full up
   there — the deep amber is the only warning mark that measures on the light
   ground, it must NEVER go inside a band, and THE COLOUR IS NEVER THE MEANING.
   A failed sign-in says so in words. The edge only makes it faster to find.

   ⚠️ Ways-in pages are never banded, so the light-ground constraint above is
   satisfied by the group's shape rather than by a rule. If a way-in page ever
   grows a band, this component does not travel into it.
   -------------------------------------------------------------------------- */
.notice {
  background: var(--cb-surface);
  border: 1px solid var(--cb-line);
  border-radius: var(--cb-r-sm);
  padding: 13px 15px;
  margin-bottom: var(--cb-gap-3);
  font-size: 14.5px;
  color: var(--cb-ink-2);
}
.notice.is-bad { box-shadow: inset 4px 0 0 var(--cb-hivis-deep); }

/* -- .pw: a password field with a reveal toggle ------------------------------
   Phone first, and this is the phone-first control on the page. A password typed
   into a job-site phone in sunlight, one-handed, in gloves, is the single most
   likely thing on this site to be typed wrong — and the app's own login already
   carries the toggle, so leaving it off here would hand the dev team a page that
   is a step BACKWARDS from what they are replacing.

   .input already reserves nothing on its right, so the field gets padding-right
   for the toggle. 52px = the 44px control plus the field's own 8px of breathing
   room, measured rather than picked: at 44px exactly, a long password's last
   character sits under the icon.

   ⛔ THE TOGGLE IS 44px AND IT IS NOT NEGOTIABLE. --cb-tap is the site's tap
   floor and checks/prestart.py fails any new control under it. It reads the
   token rather than a number so it cannot drift from the rest of the site.

   ⚠️ IT SITS INSIDE A 48px FIELD, so it is centred with top/transform rather
   than stretched: a 44px button in a 48px box leaves 2px each side, and any
   `inset: 0` version of this would be a 48px target that LOOKS like a 44px one.

   ⚠️ The button is `type="button"`. Inside a <form> the default is `submit`, so
   without it the first tap on the eye posts the sign-in form. That is not a
   theory, it is what the default does.
   -------------------------------------------------------------------------- */
.pw { position: relative; }
.pw .input { padding-right: 52px; }
.pw-toggle {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: var(--cb-tap); height: var(--cb-tap);
  display: grid; place-items: center;
  background: none; border: 0; border-radius: var(--cb-r-sm);
  color: var(--cb-ink-3); cursor: pointer;
  transition: color var(--cb-fast);
}
.pw-toggle:hover { color: var(--cb-ink-2); }
.pw-toggle svg { width: 19px; height: 19px; }
/* The struck-through eye. One SVG in the markup, one line hidden or shown here,
   so the two states cannot get out of step in six pages' worth of copies. */
.pw-toggle .pw-slash { display: none; }
.pw-toggle[aria-pressed="true"] .pw-slash { display: block; }

/* -- .wayin-help: the "if it went wrong" half of a way-in page ---------------
   ⛔ ADDED FOR PAGE THIRTEEN (check-email.html), AND IT IS THE GROUP'S JIG
   RATHER THAN THAT PAGE'S. It is one rule because it needs to be one rule.

   The two doors put their reassurance BESIDE the form, in a .grid-2. The
   pages after them have no form to sit beside: check-email has nothing to
   press at all, and accept-invite and reset-password are each one field and a
   button. On those, the help belongs UNDER the main event, full width, as a
   second movement — which needs a gap big enough to read as a change of
   subject rather than as more of the same paragraph.

   ⚠️ WHY IT IS NOT JUST margin-top ON THE h2. The block is a heading plus a
   .points stack, and .points already owns the hairline above its first child.
   Hanging the space off the wrapper keeps the two components independent: the
   heading is optional, and a page that drops it still gets the same break.

   ⛔ AND WHY IT IS NOT A SECOND .section. A second section would take
   .section-tight's full block padding again and re-open the fold problem LD#47
   closed. This is one section with two movements, not two sections.
   -------------------------------------------------------------------------- */
.wayin-help { margin-top: var(--cb-gap-5); }

/* ⛔ THE SIZE OVERRIDE IS THE WHOLE REASON THIS h2 RULE EXISTS, and the fault it
   fixes is one a way-in page creates for itself. .wayin deliberately SHRINKS its
   h1 to clamp(1.7rem, 3vw, 2.25rem) — a door is the quietest page on the site,
   the tier below a section page. But the site's plain h2 is
   clamp(1.95rem, 3.6vw, 2.9rem), which is UNCHANGED and therefore LARGER.

   Measured at 1440: the h1 caps at 36px and the h2 at 46.4px, so a secondary
   heading out-shouted the page's own title by ten pixels and "If it has not
   turned up" read as the headline. No check can see this — both clear contrast,
   both meet every rule. It was found by looking at the frame, which is LD#34's
   standing argument all over again.

   ⚠️ ANY way-in page adding a second heading inherits this. Do not fix it by
   growing .wayin h1 back: the small h1 is the deliberate part. -------------- */
.wayin-help h2 {
  font-size: clamp(1.3rem, 1.8vw, 1.55rem);
  line-height: 1.25;
  margin-bottom: var(--cb-gap-2);
}

/* ============================================================================
   SHARED PAGE COMPONENTS — the PAGE FOURTEEN additions, and they are THE
   DOCUMENT FAMILY'S JIG
   Added 2026-08-18, building privacy.html — the first of the two legal pages.

   Six, and the running count is now 6 / 4 / 0 / 2 / 2 / 0 / 0 / 0 / 3 / 0 / 0 /
   1 / 6.

     .doc          the reading column a legal document sits in
     .doc-head     the title block, and the last-updated line under it
     .doc-updated  the date itself
     .doc-toc      the contents list at the top of a long document
     .doc h2 / h3  the document heading scale
     .doc ul       a list inside running legal copy

   ⛔ THIS IS A THIRD PAGE FAMILY, NOT A VARIANT OF THE OTHER TWO. The site now
   has three shapes and picking the wrong one is the mistake this block exists to
   stop:

     marketing  index.html + the eight beside it.  Hero, artefact, bands, closer.
                It is SELLING. .hero-sub on a section page.
     way in     signin.html + the three beside it. No hero, one thing to do.
                It is a DOOR. .wayin.
     document   privacy.html, terms.html.          No hero, nothing to press,
                nothing to do. It is a REFERENCE. .doc.

   A document page is the only one on this site a person arrives at already
   suspicious, and usually looking for one specific sentence rather than reading
   it through. Everything below follows from that: a narrow measure, a heading
   scale you can scan, and a contents list that gets someone to the clause they
   came for without a scroll hunt.

   ⛔ AND IT IS DELIBERATELY BUILT ON PAGE ONE OF TWO. Adam's jig rule of
   2026-08-16, applied to a third family. terms.html is the same shape as
   privacy.html with different words in it, so every rule here is wanted twice
   before it is written once. If a third document page ever appears (a DPA, a
   sub-processor list), it costs zero lines.
   ========================================================================== */

/* -- .doc: the reading column ------------------------------------------------
   720px, and it is a MEASURE rather than a layout choice. At the body's 17px
   this lands around 68 characters a line, which is the band long prose is
   comfortable in. .wayin's 940px is a two-column form width and is far too wide
   for unbroken paragraphs — at 940 the eye loses the line it is on somewhere
   around the fourth clause of a liability section.

   ⚠️ IT SITS ON .wrap AND TIGHTENS IT, the same idiom .faq-wrap and .wayin both
   already use. Do not give it its own padding: .wrap owns the 24px gutter, and a
   second one here would put a document 48px off a phone's edge.
   -------------------------------------------------------------------------- */
.doc { max-width: 720px; }

/* -- .doc-head: the title block ---------------------------------------------
   The h1 steps down for the same reason .wayin's does, and one step further.
   The three tiers now read landing > section > door, and a document sits at the
   door's level: it is not competing with anything and nobody arrives here to be
   impressed. "Privacy" at 4.4rem is a document shouting its own filename.
   -------------------------------------------------------------------------- */
.doc-head { margin-bottom: var(--cb-gap-4); }
.doc h1 {
  font-size: clamp(1.7rem, 3vw, 2.25rem);
  line-height: 1.12;
  margin-bottom: var(--cb-gap-2);
}

/* The last-updated line. --cb-ink-2, NOT --cb-ink-3: this is the first thing a
   careful reader checks on a policy, so it is quiet, not faint. ink-3 is for
   labels nobody needs to read. */
.doc-updated { font-size: 15px; color: var(--cb-ink-2); margin: 0; }

/* -- .doc-toc: the contents list --------------------------------------------
   ⛔ THIS IS THE ONE COMPONENT HERE THAT IS A USABILITY FIX RATHER THAN A TYPE
   CHOICE, and it is Nielsen 6 — recognition over recall. Nobody reads a privacy
   policy top to bottom. They arrive wanting one answer ("do you sell it", "where
   does it live", "what happens when I leave") and a wall of h2s with no index
   makes them scroll-hunt for it. The old August 6 page had thirteen sections and
   no way in but the scrollbar.

   ⛔ EVERY ROW IS 44px AND THAT IS NOT DECORATION. checks/prestart.py fails any
   new control under the tap floor, and a contents entry is a CONTROL, not a
   sentence link — the sentence-link exemption is for a link inside running body
   copy and it deliberately does not stretch to cover a list of navigation. It
   reads --cb-tap rather than a number so it cannot drift from the rest of the
   site.

   The rows are a column at every width on purpose. A two-up grid of thirteen
   short entries reads as a phrase list rather than a contents page, and the
   scan order stops being obvious the moment two columns are uneven.
   -------------------------------------------------------------------------- */
.doc-toc {
  list-style: none;
  margin: 0 0 var(--cb-gap-4);
  padding: var(--cb-gap-2) 0 0;
  border-top: 1px solid var(--cb-line);
  display: grid;
}
.doc-toc li { border-bottom: 1px solid var(--cb-line-soft); }
.doc-toc a {
  display: flex;
  align-items: center;
  min-height: var(--cb-tap);
  font-size: 15.5px;
  color: var(--cb-ink-2);
  transition: color var(--cb-fast);
}
.doc-toc a:hover { color: var(--cb-ink); }

/* -- the document heading scale ---------------------------------------------
   ⛔ THE OVERRIDE IS THE WHOLE POINT, and it is the same fault LD#51 caught on
   .wayin-help. The site's plain h2 is clamp(1.95rem, 3.6vw, 2.9rem) and its h1
   here is capped at 2.25rem, so WITHOUT this rule every one of a policy's
   thirteen section headings would out-shout the page's own title — at 1440 that
   is 46.4px of "Cookies" over 36px of "Privacy".

   It is also a scanning rule, not just a size one. A document is read by
   jumping between headings, so the headings have to be obviously headings and
   obviously smaller than the title. h3 keeps the site's own 1.2rem, which
   already sits correctly under this.

   max-width is cleared because the site's h2 caps at 20ch for display headlines.
   A section heading like "Where it is stored and how it is protected" is longer
   than that and would wrap into three words a line inside a 720px column.
   -------------------------------------------------------------------------- */
.doc h2 {
  font-size: clamp(1.25rem, 1.7vw, 1.45rem);
  line-height: 1.25;
  max-width: none;
  margin: var(--cb-gap-4) 0 var(--cb-gap-2);
}
.doc h2:first-of-type { margin-top: 0; }
.doc h3 { margin: var(--cb-gap-3) 0 6px; }

/* Running copy. 16.5px rather than the body's 17px: a legal page is long, and
   the half-pixel down buys about three words a line back without the text
   reading as small print. It is still well above anything anyone would call
   fine print, which is the line this must not cross. */
.doc p { font-size: 16.5px; }
.doc p:last-child { margin-bottom: 0; }

/* -- .doc ul: a list in running legal copy -----------------------------------
   The site has no global list reset, so this is the first real ul on it. Kept as
   a marked list rather than restyled into .points or .checks, and that is a
   deliberate difference from every other list on the site: .checks says "you get
   these" and .points says "here is a thing worth knowing". A list inside a
   policy is neither. It is an enumeration, and a bullet is the mark that says
   "these are the items, and this is all of them" — which on a "who we share it
   with" list is the entire meaning.
   -------------------------------------------------------------------------- */
.doc ul { margin: 0 0 1.1em; padding-left: 1.15em; }
.doc li { font-size: 16.5px; color: var(--cb-ink-2); margin-bottom: .4em; }
.doc li:last-child { margin-bottom: 0; }

/* A [data-placeholder] inside a document is a whole paragraph, not an inline
   chip, so it needs to break the line. The base rule is display:inline-block for
   the short ones sitting in a footer. */
.doc p[data-placeholder],
.doc div[data-placeholder] { display: block; margin-bottom: 1.1em; }

/* ============================================================================
   .lost — the 404 body.  PAGE SIXTEEN.
   Added 2026-08-18. One component, and it is the only thing 404.html needs.

   ⛔ IT IS NOT A HERO AND MUST NOT BORROW ONE. A hero sells; this page is an
   apology with a way out. It gets the door's type size, one primary button, and
   nothing else — no artefact, no glow, no flowing lines, no closer.

   min-height keeps the footer off the fold on a tall desktop window, which is
   the one thing a short page gets wrong by default: without it a 404 leaves a
   band of empty ground under the footer and reads as a broken page rather than
   a handled one. 62vh is measured, not picked — it is what puts the button at
   roughly the same height on the screen as a hero's button on every other page,
   so a visitor who lands here from the nav does not feel the page jump.
   ========================================================================== */
.lost {
  position: relative;      /* contains the section wash's ::before. */
  min-height: 62vh;
  display: flex;
  align-items: center;
  padding-block: var(--cb-gap-5);
}
.lost h1 {
  font-size: clamp(1.7rem, 3vw, 2.25rem);
  line-height: 1.12;
  margin-bottom: var(--cb-gap-2);
}
.lost p { max-width: 46ch; }
.lost .cta-row { margin-top: var(--cb-gap-3); }

/* ══ The product-visuals audit, 2026-08-28 ══════════════════════════════════
   Adam's go-live ruling: a product visual either shows the design CrewBase is
   actually building toward, or it comes off. Two patterns fell out of it. */

/* ── Hero without a product panel ──────────────────────────────────────────
   The admin-side hero panels came off (no approved admin design exists yet),
   so these heroes run one centred column instead of the two-column grid. The
   panel slot returns with an approved admin design, not before. */
.hero-in.hero-solo { grid-template-columns: none; justify-items: center; text-align: center; }
.hero-in.hero-solo .hero-copy { max-width: 760px; }
.hero-in.hero-solo .cta-row { justify-content: center; align-items: center; }

/* ── The product-preview handset ───────────────────────────────────────────
   The phone chrome now carries a render of the APPROVED worker app design
   (design/mockups, signed off), not a coded abstraction. The PNGs are
   GENERATED by checks/phone-shots.py at ONE fixed handset viewport — after a
   mockup change, re-run that script, never edit a PNG and never copy the
   content-height renders from design/state-renders (four different aspects
   in one fixed frame is the misfit Adam caught on 2026-08-28). The aspect
   here matches the script's pinned output — change one, change both. */
.phone-shot { display: block; width: 100%; aspect-ratio: 430 / 760; object-fit: cover; object-position: top; border-radius: 20px; border: 1px solid var(--cb-line); background: var(--cb-ground); }

/* ── Points without their panel ────────────────────────────────────────────
   Five sections lost their coded admin panel to the same ruling. A lone
   .points stack runs narrow-left with the right half empty (the exact shape
   the security page's card-row note warns about), so the survivors run
   two-up instead. Reverts to the split grid the day an approved admin render
   fills the slot. */
@media (min-width: 880px) {
  .points.points-wide { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 56px; align-items: start; }
}
