/* CentreOS marketing site: WOW variant (entropy -> order)
   ponytail: hand-rolled, no reset library, no build step beyond the vendored Lenis UMD.
   Fresh Act I (hero/week/turn) built to the WOW concept; Act II lifts balnea's already-
   calm/airy component language near-verbatim (same paper palette, same tokens, one
   register throughout -- density/alignment is the contrast axis, not a second theme). */

/* ---------- reset (verbatim from balnea) ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; }
img, svg { display: block; max-width: 100%; height: auto; }
button, input, textarea { font: inherit; color: inherit; }
ul { list-style: none; padding: 0; }
a { color: inherit; }
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- design tokens (verbatim from balnea -- one paper palette, no night register) ---------- */
:root {
  /* w85: same name and same value as the hub's token (tenant-hub/src/index.css:56), so the
     product and its depiction name one curve instead of repeating the literal seven times. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --paper: #FAF8F4;
  --paper-white: #FFFFFF;
  --stone-white: #FDFCF8;
  --stone-100: #F1EEE7;
  --stone-200: #E8E4DB;
  --ink: #1F2A22;
  --eucalyptus: #2D5A42;
  --eucalyptus-dark: #14422C;
  --eucalyptus-light: #6FA98A;
  --ink-green: #1C2416;
  --hairline: #D1CEC4;
  --hairline-soft: rgba(31, 42, 34, 0.08);
  --amber: #8A5A1E;
  --turn-bg: #1A1A1A; /* the one dark section (proof band, carried from balnea's "turn") */

  --font-display: "Abril Fatface", Georgia, serif;
  --font-display-quiet: "Fraunces", Georgia, serif;
  --font-body: "Archivo", Arial, sans-serif;
  --font-product-title: "Fraunces", Georgia, serif;
  --font-product-meta: "Source Sans 3", Arial, sans-serif;

  --radius-soft: 9px;
  --radius-card: 16px;

  --chip-new-bg: #EEECE7; --chip-new-fg: #5C574A;
  --chip-seen-bg: #E4E9EE; --chip-seen-fg: #3C5468;
  --chip-tan-bg: #F1E6D2; --chip-tan-fg: #7E6339; /* NS-272: darkened from #8A6F45, 3.83:1 -> 4.55:1 vs bg, only failing chip pair */
  --chip-amber-bg: #FBEDD0; --chip-amber-fg: #8A5A10;

  --content-max: 1280px;
  --space-s: clamp(0.75rem, 0.6rem + 0.6vw, 1.25rem);
  --space-m: clamp(1.5rem, 1.1rem + 1.5vw, 2.75rem);
  --space-l: clamp(2.5rem, 1.6rem + 3.5vw, 5.5rem);
  --space-xl: clamp(4rem, 2.4rem + 6vw, 7rem);

  --measure-narrow: 34em;
  --measure-air: 52em; /* w3 tune: Max verdict "still too squished" -- widened from 42em to the
    practical max that still reads as a measure (not a full-bleed line) at 1280 wrap */

  --focus-ring: 3px solid var(--eucalyptus);

  /* w66: BEZEL, one source of truth. Ratios measured live off .turn-phone (the Monday phone,
     the reference Max wants matched) at 1366x768: 7.2px bezel / 212.9px width = 0.0338,
     35.2px outer radius / 212.9px width = 0.1653. Every phone mockup derives its chrome from
     its own --phone-w times these two ratios instead of carrying its own literal px/rem. */
  --phone-bezel-ratio: 0.0338;
  --phone-radius-ratio: 0.1653;
}

.font-quiet { --font-display: var(--font-display-quiet); }
.font-quiet .display { font-weight: 600; }

html { scroll-behavior: auto; }
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(17px, 0.9rem + 0.25vw, 20px);
  line-height: 1.6;
  overflow-x: hidden;
}

.wrap { max-width: var(--content-max); margin-inline: auto; padding-inline: var(--space-m); }
.display { font-family: var(--font-display); font-weight: 400; line-height: 1.12; text-transform: none; }
.label { font-family: var(--font-body); font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; font-size: 0.8rem; }
h1, h2, h3 { font-weight: 400; }

/* ---------- skip link / focus (verbatim) ---------- */
.skip-link { position: absolute; left: -9999px; top: 0; background: var(--ink); color: var(--paper); padding: 0.75em 1.25em; z-index: 100; }
.skip-link:focus { left: var(--space-s); top: var(--space-s); }
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, summary:focus-visible, .contact-question:focus-visible {
  outline: var(--focus-ring); outline-offset: 2px;
}

/* ---------- header (verbatim) ---------- */
.site-header { border-bottom: 1px solid var(--hairline); background: var(--paper); position: sticky; top: 0; z-index: 30; }
.header-inner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-m); padding-block: var(--space-s); }
.wordmark { font-family: var(--font-body); font-weight: 600; letter-spacing: 0.08em; font-size: 1.1rem; text-decoration: none; min-height: 44px; display: inline-flex; align-items: center; }
.nav-links { display: flex; gap: var(--space-m); }
.nav-links a { text-decoration: none; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.82rem; min-height: 44px; display: inline-flex; align-items: center; }
.nav-links a:hover { color: var(--eucalyptus); }
@media (max-width: 720px) { .nav-links { display: none; } }

/* ---------- buttons (verbatim) ---------- */
.btn { display: inline-flex; align-items: center; justify-content: center; min-height: 48px; padding-inline: 1.5em; font-family: var(--font-body); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.85rem; text-decoration: none; border-radius: var(--radius-soft); cursor: pointer; border: 1px solid transparent; }
.btn-small { min-height: 44px; padding-inline: 1.1em; }
.btn-primary { background: var(--eucalyptus); color: var(--paper-white); border-color: var(--eucalyptus); }
.btn-primary:hover, .btn-primary:focus-visible { background: var(--eucalyptus-dark); border-color: var(--eucalyptus-dark); }
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; } /* w21: honest in-flight state for the contact form's "Sending..." */
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--ink); }
.btn-ghost:hover, .btn-ghost:focus-visible { background: var(--ink); color: var(--paper); }
.btn-row { display: flex; flex-wrap: wrap; gap: var(--space-s); }

/* ---------- clock device: persistent corner, jumps erratically through Act I ---------- */
.clock-device {
  position: fixed;
  top: calc(64px + var(--space-s));
  right: var(--space-s);
  z-index: 25;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-product-meta);
  font-weight: 600;
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  padding: 0.5em 0.8em;
  border-radius: 4px;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.22);
  pointer-events: none;
}
.clock-device.clock-final { background: var(--eucalyptus-dark); }
@media (prefers-reduced-motion: reduce), (max-width: 600px) {
  .clock-device { display: none; }
}

/* =========================================================================
   ACT I: NOISE
   ========================================================================= */

/* ---------- 1. hero: pure paper-and-ink typographic chaos, no photo ---------- */
.hero {
  position: relative;
  background: var(--paper);
  border-bottom: 1px solid var(--hairline-soft);
}
/* unarmed (no-JS / reduced-motion / <=600px): plain readable block, no pin, no scrub
   range -- scraps stay display:none by default CSS (see .scrap below). */
.hero .scroll-scene-sticky {
  min-height: 100vh;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
}
[data-scroll-scene="wow-hero"].scene-armed {
  height: 200vh;
  min-height: 0;
  display: block;
  overflow: visible; /* an overflow:hidden ancestor would become the sticky child's own
                         scrolling container and it would never reach its stuck state */
}
[data-scroll-scene="wow-hero"].scene-armed .scroll-scene-sticky {
  position: sticky;
  top: 0;
  min-height: 100vh;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
}
.hero-headline {
  display: block;
  max-width: none;
  margin-bottom: var(--space-m); /* w25 (Max: "move [the headline block] down slightly...
    awkward gap" - was space-l): pulls the h1 (incl. its Daily Grind / 9:47pm meta labels,
    which live inside it) down toward the body copy. */
}
.hero-obj { display: block; margin-bottom: 0.15em; }
.hero-obj-text {
  display: block;
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.04;
  font-size: clamp(2.4rem, 1.2rem + 5.6vw, 5.7rem); /* w1 tune: 6.4rem cap clipped "in" at the viewport edge */
}
.hero-obj-meta {
  display: block;
  font-family: var(--font-body);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.68rem;
  color: var(--eucalyptus);
  margin-bottom: 0.15em;
}
.hero-obj-sms { color: var(--eucalyptus-dark); }
.hero-obj-note .hero-obj-text { font-style: italic; font-size: clamp(1.9rem, 0.9rem + 4.4vw, 4.5rem); } /* w2 tune: the long line gets its own step so it never clips; the size drop reads as
  hierarchy. w25: cap RESTORED to 4.5rem - the w23 trim existed only for the wide
  "...in the / car park." break, which Max reverted ("trial moving the in the to the other
  line with the car park"); with the short break back the wide-line collision is gone. */

.hero-inner {
  position: relative;
  z-index: 5; /* w1 tune: headline above the mid-pile (z<=4); only z6-7 scraps may crowd it */
  margin-inline-start: 17%; /* w22 (Max: "more to the right and more to the center... doesn't look
    as cramped on the left"). Slack measured before the bump: widest glyph x=953 vs pile at
    x=1071 @1366 under 13%, so +4% (~55px) keeps ~60px clear of the gate margin; re-gated live
    after deploy. Prior: w20 (Max, 26 Aug: "more space on the left... blank spot in middle
    right column" - the widest glyph edge sat at x=899 vs the right pile at x=1071 @1366). 13%
    closes that band; glyph gate passes clean at 1280x800, 1366x768 AND 1600x900. History: 7% (w17
    first cut) left the h1 edge at
    x=125 vs left-pile scrap rights of 108-131 (measured live, 1280x800): 11 glyph-gate hits. 9%
    puts the edge at x=150/184 (1280/1600) and the gate passes clean at both. margin-right
    stays 0/auto so the box's own right edge is unchanged (width:auto shrinks by the same 7%) --
    combined with the line-3 <br> above, this pulls the h1's widest rendered line well clear of
    its old ~899px/941px right extent, freeing the right margin the scrap pile already uses. */
}
@media (max-width: 1280px) {
  /* w23: 17% + the car-park break overflow into the right pile at 1280 (measured, 2 scrap hits
     even after the italic cap cut); one step down keeps the glyph gate clean there. */
  .hero-inner { margin-inline-start: 13%; }
}
/* paints above the position:absolute scraps regardless of DOM order (a static element
   with no stacking context would otherwise paint BEHIND any positioned sibling). */
.hero-cta-panel {
  position: relative;
  z-index: 8; /* w1 tune: copy + CTAs above every scrap; the chaos shows through the translucent panel instead of over it */
  max-width: 44em; /* w4: hugs the copy instead of spanning the full .wrap column -- the panel
    used to be as wide as the h1, which trapped a scrap underneath it (Max flagged); narrower
    panel + the A2 scrap repositioning below fix the same complaint from both directions */
  background: rgba(250, 248, 244, 0.86);
  backdrop-filter: blur(2px);
  padding: var(--space-m) var(--space-m) var(--space-l);
  margin-top: var(--space-m);
}
.hero-body { position: relative; font-size: clamp(1.05rem, 1rem + 0.4vw, 1.3rem); max-width: 36em; margin-bottom: var(--space-s); }
.hero-sub { opacity: 0.75; margin-bottom: var(--space-m); }
.hero .btn-row { margin-top: 0; }

/* w82: on short viewports the hero's own vertical rhythm pushed .btn-row past the fold --
   measured 65px below at a 724px-tall viewport, so the primary CTA was cut off on any
   laptop. Only the hero's internal gaps tighten; nothing moves, resizes or restyles, and
   above 820px tall the spacing is byte-identical to w81. ponytail: one media query on the
   existing space tokens, no new values and no layout change. Same 820px breakpoint the
   w68 phone fix already uses, so there is one short-viewport threshold on this page. */
/* w82 ROOT CAUSE of the clipped CTA. .site-header is `position:sticky` and sits IN FLOW, so at
   scroll 0 the hero's min-height:100vh sticky runs from y=80 to y=(80+100vh) -- a full header's
   height past the fold. The sticky is align-items:flex-end, so whatever is last in .hero-inner
   lands in exactly that hidden band: the primary CTA was cut off on first paint at every
   viewport (measured 2px over at 1026 tall, 63px at 856). Reserving the header's height at the
   bottom of the content box lifts the whole stack clear of it. Scraps are positioned against
   the sticky, not .hero-inner, so their verified layout is untouched. */
.hero-inner { padding-bottom: 5rem; } /* = the 80px header; keep in step if the header resizes */

@media (max-height: 900px) {
  .hero-headline { margin-bottom: var(--space-s); }
  .hero-cta-panel { margin-top: var(--space-s); padding: var(--space-s) var(--space-m) var(--space-m); }
  .hero-sub { margin-bottom: var(--space-s); }
  /* The sticky is min-height:100vh but starts BELOW the in-flow sticky header, so at scroll 0
     it runs 80px past the fold -- and align-items:flex-end puts the CTA in exactly that hidden
     band. Trimming gaps alone could not recover it (measured: 686px of content into a 644px
     visible band), so the headline gives ~11% on short viewports. Both clamps keep their
     shape; only the coefficients scale, so the type still fluid-scales with width. */
  .hero-obj-text { font-size: clamp(2.1rem, 1.05rem + 4.9vw, 5rem); }
  .hero-obj-note .hero-obj-text { font-size: clamp(1.7rem, 0.8rem + 3.9vw, 4rem); }
}

/* w82: below ~700px tall (a 1280x800 laptop lands here once browser chrome is counted) the
   visible band is only ~566px and the pile physically cannot fit. The three lowest pieces
   stand down rather than hang past the fold, and the headline takes one more step. This is a
   fit decision at a viewport where they were never legible, NOT the density cut -- all 15
   still show at every normal height. ponytail: one class, one query, no JS. */
@media (max-height: 700px) {
  /* .scrap.scrap-low, not .scrap-low: the base `.scene-armed .scrap { display:block }` sits
     LATER in this file at equal specificity and was winning on source order. */
  [data-scroll-scene="wow-hero"].scene-armed .scrap.scrap-low { display: none; }
  .hero-obj-text { font-size: clamp(1.9rem, 0.95rem + 4.3vw, 4.4rem); }
  .hero-obj-note .hero-obj-text { font-size: clamp(1.55rem, 0.72rem + 3.5vw, 3.6rem); }
}

/* ---------- w82: display-face round (?font=<slug>), see main.js ----------
   Six Fontshare faces, every one with a real weight axis (Abril has ONE weight, which is why
   the page needed 19 sizes to make hierarchy). Only --font-display is swapped: --font-body stays
   Archivo, and the product-mockup faces (Fraunces / Source Sans 3) are deliberately untouched so
   the phone keeps depicting the real app. Constraint from style.css:~1487 -- the marketing face
   must never become Fraunces. No ?font= param means Abril, unchanged.
   Axes: Zodiak 100-900, Boska 200-900, Bespoke Serif 300-800, Melodrama/Erode/Gambetta 300-700. */
:root[data-font="zodiak"]        { --font-display: "Zodiak", Georgia, serif; }
:root[data-font="boska"]         { --font-display: "Boska", Georgia, serif; }
:root[data-font="bespoke-serif"] { --font-display: "Bespoke Serif", Georgia, serif; }
:root[data-font="melodrama"]     { --font-display: "Melodrama", Georgia, serif; }
:root[data-font="erode"]         { --font-display: "Erode", Georgia, serif; }
:root[data-font="gambetta"]      { --font-display: "Gambetta", Georgia, serif; }
/* Abril is a single-weight face, so .display hard-codes font-weight:400. With a real axis the
   headline can carry weight instead of size -- this is what makes the sprawl fixable later. */
:root[data-font] .display,
:root[data-font] .hero-obj-text { font-weight: 600; }

/* manager's pen: hand-drawn annotation, redrawn as a calm single-pass ellipse (F3) */
.annotation-svg { position: absolute; overflow: visible; fill: none; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
.annotation-svg path { stroke-dasharray: 1; stroke-dashoffset: 1; }
@media (prefers-reduced-motion: no-preference) {
  .annotation-svg path { transition: stroke-dashoffset 600ms cubic-bezier(0.23, 1, 0.32, 1); }
  .reveal.js-hidden .annotation-svg path { stroke-dashoffset: 1; }
  .reveal:not(.js-hidden) .annotation-svg path { stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) { .annotation-svg path { stroke-dashoffset: 0; } }
.circled { position: relative; white-space: nowrap; }
/* w22: JS-generated turn fill dups (see generateTurnFill in main.js) - width cap so pieces
   landing near the right edge never wrap one-word-per-line (the w20 94% lesson). */
.turn-artifact-gen { max-width: 11em; }
/* w23 perf: at 122 pieces the un-composited per-frame opacity/transform writes repainted the
   scene (measured p95 36ms, 52/150 frames >20ms). Compositor-promote only while armed. */
[data-scroll-scene="wow-turn"].scene-armed .turn-artifact { will-change: transform, opacity; }
/* w32 interactivity (Max: hover moves + draggable notes). Outer transform/scale are JS-owned,
   so the hover lift lives on .scrap-inner. Pieces stay aria-hidden decoration; drag is a
   mouse/pen bonus (touch scrolls, and the scene never arms <=600px). */
[data-scroll-scene="wow-turn"].scene-armed .turn-artifact { cursor: grab; user-select: none; }
[data-scroll-scene="wow-turn"].scene-armed .turn-artifact.turn-dragging { cursor: grabbing; }
[data-scroll-scene="wow-turn"].scene-armed .turn-artifact .scrap-inner { transition: transform 180ms ease, box-shadow 180ms ease; }
[data-scroll-scene="wow-turn"].scene-armed .turn-artifact:hover .scrap-inner {
  transform: scale(1.07);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2);
}
.annotation-circle {
  stroke: var(--eucalyptus-light);
  stroke-width: 3px;
  /* w23 (Max: "the oval is a bit too short horizontal wise", root-caused): an SVG is a REPLACED
     element, so `inset` + width:auto never stretched it - it kept an intrinsic-ish width hung
     off the left inset only (measured live: svg 357..514 vs glyphs 380..535, offset AND short).
     Replaced elements need explicit width/height; insets replaced with left/top + calc(). */
  left: -1.05em;
  top: -0.45em;
  width: calc(100% + 2.1em);
  max-width: none; /* the reset's `svg { max-width:100% }` was clamping the calc back to span width */
  height: calc(100% + 0.9em);
  transform: rotate(-2deg);
}

/* ---------- hero scraps: dense, tilted, balanced, deep field (no convergence in W1) ---------- */
.scrap {
  display: none;
  position: absolute;
  max-width: 15em;
  transform: translate(var(--pose-x, 0px), var(--pose-y, 0px)) rotate(var(--pose-r, 0deg));
}
[data-scroll-scene="wow-hero"].scene-armed .scrap { display: block; cursor: pointer; }
/* click-to-organise (w59, relayfi's own tidy-up toggle): while organised (or riding back
   out of it) the scraps' transform is a one-shot target, animated by CSS with a per-piece
   stagger (--scrap-i stamped by main.js) -- the drift writer yields, so this transition
   never fights per-frame scroll writes. Reduced motion: instant state change, no tween. */
@media (prefers-reduced-motion: no-preference) {
  [data-scroll-scene="wow-hero"].hero-organised .scrap,
  [data-scroll-scene="wow-hero"].hero-detidying .scrap {
    transition: transform 600ms cubic-bezier(0.23, 1, 0.32, 1);
    transition-delay: calc(var(--scrap-i, 0) * 25ms);
  }
  /* hover hint that the mess is touchable (relay: whileHover scale 1.02) */
  [data-scroll-scene="wow-hero"].scene-armed .scrap .scrap-inner { transition: transform 180ms ease; }
  [data-scroll-scene="wow-hero"].scene-armed .scrap:hover .scrap-inner { transform: scale(1.02); }
}
.scrap-inner { display: block; position: relative; }
.scrap-text { display: block; font-family: var(--font-body); font-weight: 600; font-size: 0.85rem; line-height: 1.32; }

.scrap-sms .scrap-inner { background: var(--eucalyptus); color: var(--paper-white); padding: 0.75em 1.05em; border-radius: 16px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18); }
.scrap-sms .scrap-text { color: var(--paper-white); }

.scrap-email .scrap-inner { background: var(--paper-white); color: var(--ink); border: 1px solid var(--hairline); padding: 0.8em 1.05em; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.16); }
.scrap-email .scrap-text { color: var(--ink); }

/* w32 (Max: "proper sticky notes like a square and the size and handwritten stuff"):
   square aspect, real post-it proportions, handwriting face (Caveat, loaded in both pages'
   font links), slight bottom-corner curl shadow. Applies everywhere postit chrome appears
   (hero, week, turn) - one sticky-note language site-wide. */
.scrap-postit .scrap-inner {
  background: var(--chip-tan-bg);
  color: var(--chip-tan-fg);
  aspect-ratio: 1;
  width: 7.5em;
  display: flex;
  align-items: center;
  padding: 0.8em 0.9em;
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16), 0 14px 10px -10px rgba(0, 0, 0, 0.12);
}
.scrap-postit .scrap-text {
  color: var(--ink);
  font-family: "Caveat", "Segoe Script", cursive;
  font-size: 1.45em;
  line-height: 1.08;
  font-weight: 500;
}
.scrap-note .scrap-text, .turn-artifact-taped .scrap-text {
  font-family: "Caveat", "Segoe Script", cursive;
  font-size: 1.35em;
  line-height: 1.1;
  font-style: normal;
}

.scrap-note .scrap-inner {
  background: var(--paper-white);
  color: var(--ink);
  padding: 0.95em 1.05em 1.3em;
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16);
  clip-path: polygon(2% 8%, 18% 0%, 34% 6%, 50% 1%, 66% 7%, 82% 0%, 98% 5%, 100% 96%, 84% 100%, 68% 94%, 52% 100%, 36% 95%, 20% 100%, 0% 93%);
}
.scrap-note .scrap-text { color: var(--ink); font-style: italic; }
.scrap-note .scrap-tape { position: absolute; top: -0.55em; left: 30%; width: 2.6em; height: 0.95em; background: rgba(255, 255, 255, 0.55); border: 1px solid rgba(255, 255, 255, 0.7); transform: rotate(-3deg); }

.scrap-notif .scrap-inner { background: var(--ink); color: var(--paper); padding: 0.65em 0.95em; border-radius: var(--radius-soft); box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22); }
.scrap-notif .scrap-text { color: var(--paper); font-size: 0.78rem; }

.scrap-photo { width: 7em; }
.scrap-photo .scrap-inner img { display: block; width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border: 1px solid var(--hairline); border-radius: var(--radius-card); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18); }

/* F1/staging: one scrap runs oversized -- the leak text can be big */
.scrap-big { max-width: 16em; } /* w4: 20em->16em so it still fits the viewport at its new right-column position (A2) */
.scrap-big .scrap-text { font-size: 1.15rem; }

/* w17 (Max, 26 Aug: "dense on BOTH sides like relayfi"): left-margin pile. hero-inner now
   starts ~10% in at 1280px (3.4% wrap padding + the 7% shift above) -- .scrap-left caps width
   well under that gap so the box's right edge stays clear of the h1/panel's left-aligned glyphs
   even with the +/-14px jitter and rotation. ponytail: a width cap, not a second positioning
   system. UNVERIFIED BY RENDER -- see report, orchestrator should measure at 1280x800/1600x900. */
.scrap-left { max-width: 8.5em; } /* w25 (Max: "the left one, we can widen slightly") - was 6em;
   the 17% text shift opened the corridor, gate re-run after the bump */
/* duplicate scraps (reuse existing 10 strings, zero new copy): fainter/smaller via CSS `scale`
   (not `transform`, so it composes for free with main.js's per-frame translate/rotate jitter,
   same trick as .week-artifact-dup) -- included in the generic .scrap selector so they drift
   like every other scrap (no JS changes needed). */
.scrap-dup { scale: 0.78; opacity: 0.5; }

@media (max-width: 600px) {
  .hero-obj-text { font-size: 2.4rem; }
}

/* =========================================================================
   ACT I: THE WEEK (pinned scene, crossfading captions + accumulating clutter)
   ========================================================================= */
.week-scene { position: relative; background: var(--paper); }
/* w3 tune (Max: "devices must have PURPOSE" / "if they were actual more problems that
   combine into the dashboard after"): the soft-bar skeletons are gone. Six real,
   scrap-styled notifications carrying the six titles that already exist on the page
   (ticker + what-you-see board) accumulate here, 2 per beat, then converge into the
   phone dashboard in THE TURN -- position/pose set inline per item (see index.html),
   same donor chrome (.scrap-email/.scrap-sms/.scrap-notif/.scrap-postit) as the hero. */
.week-artifact {
  display: none;
  position: absolute;
  z-index: 4;
  max-width: 15em;
  transform: translate(var(--pose-x, 0px), var(--pose-y, 0px)) rotate(var(--pose-r, 0deg));
}
[data-scroll-scene="wow-week"].scene-armed .week-artifact { display: block; }
/* w5 "more mess": static clutter layer, excluded from main.js's weekArtifacts query (class
   selector there is now ".week-artifact:not(.week-artifact-dup)") so it never receives the
   per-frame opacity/filter fade-in the six originals get -- these just sit at a fixed dimmed
   scale once the scene arms, ambient rather than choreographed. */
.week-artifact-dup { scale: 0.8; opacity: 0.55; }
/* the dups are static clutter with no JS opacity drive, so they were the worst seam
   amputees (63px past the sticky's overflow:hidden bottom edge, 28 Aug) -- same 8% tail
   fade as the JS-driven originals, computed from the scene's own --scene-p. */
[data-scroll-scene="wow-week"].scene-armed .week-artifact-dup {
  opacity: calc(0.55 * (1 - clamp(0, (var(--scene-p, 0) - 0.92) / 0.08, 1)));
}

/* week captions: same cascade shape as balnea, F4 air -- wider measure, more line-height/padding */
.cascade { position: relative; min-height: 65vh; display: flex; align-items: center; }
.cascade-inner { position: relative; padding-block: var(--space-xl); }
.quote-mark { font-family: var(--font-display); color: var(--ink); opacity: 0.28; font-size: clamp(4rem, 2.5rem + 8vw, 9rem); line-height: 0.7; margin-bottom: var(--space-m); }
.cascade-headline { font-size: clamp(2.1rem, 1.5rem + 3vw, 3.8rem); max-width: 22em; margin-bottom: var(--space-xl); line-height: 1.3; } /* w3 tune: line-height 1.18->1.3, margin space-l->space-xl (breathing before the body copy) */
.beat-three .cascade-headline-large { font-size: clamp(3.5rem, 10vw, 8.5rem); max-width: none; line-height: 1.35;
  /* w88 (Max, 4 Sep: the space under this headline "takes up too much space"). Overrides
     .cascade-headline's --space-xl (measured 112px at 1440, 64px at 390) for beat three ONLY --
     beats one and two were not complained about and keep their breathing room. Pulling the
     paragraphs up also increases clearance to the .scrap-postit / .scrap-sms pieces below. */
  margin-bottom: var(--space-m); }
.giant-type .word { display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  .giant-type .word { transition: opacity 0.5s ease, transform 0.5s ease; }
  .cascade-inner.js-hidden .giant-type .word { opacity: 0; transform: translateY(0.3em); }
  .giant-type .word:nth-child(1) { transition-delay: 0ms; }
  .giant-type .word:nth-child(2) { transition-delay: 70ms; }
  .giant-type .word:nth-child(3) { transition-delay: 140ms; }
  .giant-type .word:nth-child(4) { transition-delay: 210ms; }
  .giant-type .word:nth-child(5) { transition-delay: 280ms; }
  .giant-type .word:nth-child(6) { transition-delay: 350ms; }
  .giant-type .word:nth-child(7) { transition-delay: 420ms; }
}
.cascade-large { font-size: clamp(1.25rem, 1.1rem + 0.6vw, 1.65rem); font-weight: 600; max-width: var(--measure-air); margin-bottom: var(--space-m); line-height: 1.55; }
.cascade-small { position: relative; font-size: clamp(1.05rem, 1rem + 0.3vw, 1.2rem); max-width: var(--measure-air); color: #3a3a3a; line-height: 1.65; }
.underlined { position: relative; white-space: nowrap; }
.annotation-underline { stroke: var(--eucalyptus); stroke-width: 3px; left: 0; right: 0; width: auto; height: 0.55em; bottom: -0.5em; } /* w7 tune: anchored to the phrase span, spans exactly its width */

[data-scroll-scene="wow-week"].scene-armed { height: 260vh; min-height: 0; }
[data-scroll-scene="wow-week"].scene-armed .scroll-scene-sticky { position: sticky; top: 0; min-height: 100vh; display: flex; align-items: center; overflow: hidden; }
[data-scroll-scene="wow-week"].scene-armed .week-inner { width: 100%; } /* w6 fix: a flex item whose only content is absolutely positioned shrink-wraps to its padding (measured 83px live); captions rendered one word per line */
[data-scroll-scene="wow-week"].scene-armed .week-captions { position: relative; min-height: 62vh; width: 100%; }
[data-scroll-scene="wow-week"].scene-armed .cascade { min-height: 0; }
/* w3 tune: re-fit for air, not just no-clip. Worst case 1600px viewport (container still
   capped at --content-max 1280px): cap trimmed 5.5rem->5rem so the wrap never maxes out
   the 1192px column edge-to-edge -- "When does the place / actually get better?" (19/21
   chars) at ~80px (~0.58em/char estimate) needs ~1035px, leaving visible margin either
   side. At 1280px the vw-preferred value (~4rem) is already below the cap, same margin. */
[data-scroll-scene="wow-week"].scene-armed .cascade .giant-type { font-size: clamp(2.4rem, 5vw, 5rem); word-spacing: 0.06em; }
/* beat-three nudge (Max, 28 Aug: "bring the When does the place actually get better down
   slightly - overlap with the notifications above and a gap with the text below"): measured
   at 1366x768, the headline topped out at 130px (into the artifact band) with a 96px dead
   gap to its own paragraph. +48px closes both at once. */
[data-scroll-scene="wow-week"].scene-armed .beat-three .giant-type { margin-top: 48px; }
[data-scroll-scene="wow-week"].scene-armed .cascade {
  position: absolute;
  inset: 0;
  border-bottom: none;
  will-change: transform, opacity;
}
[data-scroll-scene="wow-week"].scene-armed .cascade-inner.js-hidden .giant-type .word { opacity: 1; transform: none; }

/* w17: the 1440px override block that used to live here is DELETED, not re-derived. Its root
   cause -- "the long h1 line's glyphs reach x~1062" -- was specifically the unbroken 38-char
   line-3 ("One someone told you in the car park."), which task 1a above splits into two much
   shorter lines. Its selectors were also fragile against the new dup scraps: .scrap-note moved
   into the left pile entirely (the old override would have yanked it back to left:80%), and
   :nth-of-type(5) counts by sibling POSITION among all <span> scraps, which shifts every time a
   scrap is inserted/reordered -- exactly what tasks 1c/3 do. UNVERIFIED BY RENDER: orchestrator
   should re-check 1280x800 for any residual line-3/scrap collision and re-add a targeted rule
   (class-based, not nth-of-type) only if one is found. */

/* =========================================================================
   ACT I: THE TURN (the 10-metre moment -- clutter aligns, converges, holds)
   ========================================================================= */
.the-turn { position: relative; background: var(--paper); }
[data-scroll-scene="wow-turn"].scene-armed { height: 280vh; min-height: 0; }
[data-scroll-scene="wow-turn"].scene-armed .scroll-scene-sticky { position: sticky; top: 0; min-height: 100vh; display: flex; align-items: center; overflow: hidden; }

/* w85: inert by DEFAULT. The JS flips this to auto on the frame a piece becomes visible and
   back to none when it fades, but a piece that has never been visible yet would otherwise
   sit hit-testable at full CSS opacity over the phone. Safe state is the default. */
.turn-artifact { pointer-events: none; display: none; position: absolute; z-index: 4; max-width: 15em; transform: translate(var(--pose-x, 0px), var(--pose-y, 0px)) rotate(var(--pose-r, 0deg)); }
[data-scroll-scene="wow-turn"].scene-armed .turn-artifact { display: block; }

/* ---------- checkpoint B: solid-colour chip variants + 3 new "manager artifact" chrome
   types, all EXISTING tokens only. Applied as a modifier class alongside the existing
   scrap-email/sms/notif/postit/note chrome; placed after that chrome block above so a
   modifier always wins the cascade (same specificity, later source order) regardless of
   which base chrome a clone inherited from its source -- one override mechanism for both
   hand-authored spans (class added directly in index.html) and generated clones (class
   added by generateTurnFill's own hash in main.js). Contrast pairs computed via the WCAG
   relative-luminance formula (not eyeballed) -- ratio noted per rule, all >=4.5:1. */
.turn-chip--euca .scrap-inner { background: var(--eucalyptus); box-shadow: 0 8px 18px rgba(0, 0, 0, 0.18); }
.turn-chip--euca .scrap-text { color: var(--paper-white); } /* 7.9:1 */
.turn-chip--euca-dark .scrap-inner { background: var(--eucalyptus-dark); box-shadow: 0 8px 18px rgba(0, 0, 0, 0.2); }
.turn-chip--euca-dark .scrap-text { color: var(--paper-white); } /* 11.4:1 */
.turn-chip--ink .scrap-inner { background: var(--ink); box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22); }
.turn-chip--ink .scrap-text { color: var(--paper); } /* 14.0:1 */
.turn-chip--euca-light .scrap-inner { background: var(--eucalyptus-light); box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16); }
.turn-chip--euca-light .scrap-text { color: var(--ink); } /* 5.5:1 */
.turn-chip--slate .scrap-inner { background: var(--chip-seen-fg); box-shadow: 0 8px 18px rgba(0, 0, 0, 0.18); }
.turn-chip--slate .scrap-text { color: var(--paper-white); } /* 7.9:1 */
.turn-chip--amber .scrap-inner { background: var(--amber); box-shadow: 0 8px 18px rgba(0, 0, 0, 0.18); }
.turn-chip--amber .scrap-text { color: var(--paper-white); } /* 5.9:1 */
.turn-chip--stone .scrap-inner { background: var(--stone-200); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.14); }
.turn-chip--stone .scrap-text { color: var(--ink); } /* 11.7:1 */
.turn-chip--euca .scrap-inner, .turn-chip--euca-dark .scrap-inner, .turn-chip--ink .scrap-inner,
.turn-chip--euca-light .scrap-inner, .turn-chip--slate .scrap-inner, .turn-chip--amber .scrap-inner,
.turn-chip--stone .scrap-inner {
  padding: 0.7em 1em;
  border-radius: 14px;
  border: none;
  clip-path: none; /* clears a note-chrome torn-edge clip if a chip overrides a scrap-note source */
}

/* new artifact types: chrome only, zero new copy -- each overrides whatever base scrap-*
   chrome a clone inherited, same mechanism as the chips above. No extra HTML needed (pure
   ::before decoration), so a generated clone gets full chrome from the class alone. */
.turn-artifact-taped .scrap-inner {
  background: var(--paper-white);
  border: none;
  padding: 0.9em 1.05em 1.15em;
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16);
  clip-path: polygon(1% 5%, 24% 0%, 55% 4%, 100% 0%, 98% 96%, 68% 100%, 32% 95%, 0% 100%);
  position: relative;
}
.turn-artifact-taped .scrap-inner::before {
  content: "";
  position: absolute;
  top: -0.55em;
  left: 30%;
  width: 2.6em;
  height: 0.95em;
  background: rgba(255, 255, 255, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.7);
  transform: rotate(-3deg);
}
.turn-artifact-taped .scrap-text { color: var(--ink); font-style: italic; }

.turn-artifact-voicemail .scrap-inner {
  background: var(--eucalyptus-dark);
  border: none;
  border-radius: var(--radius-soft);
  padding: 0.65em 0.95em 0.65em 0.8em;
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22);
  clip-path: none;
  display: flex;
  align-items: center;
  gap: 0.55em;
}
.turn-artifact-voicemail .scrap-inner::before {
  content: "";
  width: 0.55em;
  height: 0.55em;
  border-radius: 50%;
  background: var(--amber);
  flex-shrink: 0;
}
.turn-artifact-voicemail .scrap-text { color: var(--paper-white); } /* 11.4:1, same pair as turn-chip--euca-dark */

.turn-artifact-docket { max-width: 9em; }
.turn-artifact-docket .scrap-inner {
  background: var(--stone-white);
  border: 1px solid var(--hairline);
  border-top: 2px dashed var(--hairline);
  border-radius: 2px;
  padding: 0.75em 0.8em;
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.14);
  clip-path: none;
}
.turn-artifact-docket .scrap-text {
  color: var(--ink);
  font-family: var(--font-product-meta);
  letter-spacing: 0.03em;
  font-size: 0.76rem;
}

/* w3->w4: the converge TARGET is now a phone showing the real dashboard, not a banner (Max:
   "have like a phone cutout and show the dashboard from there" / "you can make the phone
   way bigger"). Pure CSS, no image -- aspect-ratio holds a phone proportion. w4: reworked
   to a side-by-side stage (phone left, Monday card right, vertically centred) so the phone
   can grow much larger without colliding with the card -- see .turn-stage below. */
.turn-phone {
  display: none;
  /* w85, Max: "just make the Monday one not react because we already have the interactive
     one below". This phone is aria-hidden="true" -- it is a depiction, not a control -- so
     it should never have been hit-testable. One rule here instead of neutralising each
     hover inside it: the shared .report-card:hover (scale 1.03 + heavy shadow) was firing
     on its open card, exactly as it once did on the interactive phone before ~992 fixed
     that one. The interactive phone in what-it-is is the only phone that responds. */
  pointer-events: none;
  position: relative; /* containing block for .turn-phone-notch (was implicit via position:absolute before w4) */
  /* w66: --phone-w replaces the old aspect-ratio:390/844 (0.4621 IS that ratio) as the single
     source of truth -- width is now computed explicitly so the bezel/radius below can derive
     off it. Rendered px at 1366x768 is unchanged (was aspect-ratio-derived 212.9px, still
     ~212.9px): this is a refactor, not a resize. */
  /* w75c -- 60vh -> 72vh. Max ruled the phone up to ~255px wide so its chip text reaches 8px,
     the size below which hinting and antialiasing stop preserving proportion: at 213px the
     chips rendered at 6.69px and read heavier, blurrier and vertically off against the demo's
     9.26px even though every ratio matched. The 390/844 RATIO is untouched (0.4621 is that
     ratio) -- only the size changes. At 768-high, 72vh = 553px tall -> 255.5px wide; the
     clamp max of 640px still caps it below the demo, which sits at 294.8px there.
     HONEST LIMIT: the 8px floor only holds at >=768 viewport height. Shorter viewports fall
     back down the vh curve, because forcing a hard minimum would overflow the scene there. */
  --phone-w: calc(clamp(420px, 72vh, 640px) * 0.4621);
  width: var(--phone-w);
  /* w75c: MUST use the same clamp as --phone-w above. Changing only the width broke the
     390/844 aspect ratio (measured 0.5545 against the correct 0.4621) -- the phone got wider
     without getting taller. Max's standing instruction is that this ratio is never touched,
     so the two expressions are kept identical on purpose. */
  height: clamp(420px, 72vh, 640px); /* w4: was a fixed 44vh */
  flex-shrink: 0;
  background: var(--ink);
  /* w66: bezel/radius now derive from --phone-w times the shared root ratios (concentric by
     construction: inner radius = outer radius - bezel, same rule as before, just computed
     instead of two independently-tuned literals). Verified unchanged at 1366x768: 7.2 / 35.2. */
  border-radius: calc(var(--phone-w) * var(--phone-radius-ratio));
  padding: calc(var(--phone-w) * var(--phone-bezel-ratio));
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.18);
  opacity: 0;
}
[data-scroll-scene="wow-turn"].scene-armed .turn-phone { display: block; }
.turn-phone .turn-phone-screen .board-row { padding-block: 0.3em; border-bottom-width: 1px; } /* w10: fit six rows; extra specificity because the shared .board-row rule comes later in the file and was winning */
/* w63: Dynamic Island, not a notch (Max: "pick one design. probably the dynamic island").
   Checked against the current iPhone 17 Pro: a DETACHED floating pill, cutout ~20.8mm of a
   71.9mm body (~29% width), fully rounded, clear screen above and around it. The old rule
   was a hybrid (32% wide, glued to the top edge, bottom-only rounding). Shared by all three
   phone mockups (turn scene, what-you-see, thanks). */
.turn-phone-notch {
  position: absolute;
  /* w66: was a flat 1rem (16px on both phones regardless of size); now scales off the host
     phone's own --phone-w so it keeps floating proportionally on the bigger wys-phone too.
     0.0751 reproduces the same 16px on .turn-phone at 1366x768 (16 / 212.9px), so the turn
     scene is unchanged; on .wys-phone (wider) it grows with the now-bigger bezel. */
  top: calc(var(--phone-w) * 0.0751);
  left: 50%;
  transform: translateX(-50%);
  width: 29%;
  aspect-ratio: 126 / 37; /* the DI pill proportion (126x37.33pt) */
  height: auto;
  background: var(--ink);
  border-radius: 999px;
  z-index: 2;
}
/* real board UI, measured hub spec (site\references\hub-ui-2026-08-25): paper-white
   screen, rows + pills only -- no invented header copy, reuses .board-row/.board-title/
   .board-pill verbatim, scaled down proportionally via the screen's own font-size. */
.turn-phone-screen {
  position: relative;
  width: 100%;
  height: 100%;
  background: var(--paper-white);
  /* w66: concentric with the shell, computed instead of a literal (shell radius - bezel, same
     rule as before, now off --phone-w so it tracks whichever phone this screen sits inside) */
  border-radius: calc(var(--phone-w) * (var(--phone-radius-ratio) - var(--phone-bezel-ratio)));
  overflow: hidden;
  padding: 2.4em 1em 0.8em; /* w63: top raised to clear the floating Dynamic Island pill (was 1.7em for the flush notch) */
  font-size: clamp(12px, 2.8vh, 15px); /* w4: raised proportionally with the ~1.36x taller phone (was clamp(9px,2.05vh,12px)) */
}
/* w63: the six rows total ~497px against a ~441px screen at 768-tall viewports, so row 6 was
   amputated at the bezel (pre-existing at w60, measured 46px over before the island padding).
   Same tail-fade recipe as the week field: rows melt into the paper before the bezel, reads
   as "the board continues" instead of a hard cut. */
.turn-phone-screen::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 3em;
  background: linear-gradient(to bottom, transparent, var(--paper-white) 78%);
  pointer-events: none;
  z-index: 1;
}
.turn-phone-screen .board-row { padding: 0.5em 0.1em; }
.turn-phone-screen .board-title { font-size: 1em; }
.turn-phone-screen .board-pill { font-size: 0.72em; padding: 0.3em 0.6em; }

/* w4: side-by-side stage -- phone and Monday card as flex siblings, vertically centred,
   padding-top clears the sticky header (64px, the same constant .clock-device already
   uses). Hand-computed clearance at 1280/1600 wide x 800/900 tall: phone width never
   exceeds ~250px (aspect-ratio off a <=640px height cap), gap caps at --space-xl (112px),
   monday-card caps at 26em (~468px) -- worst-case row width ~830px against >=1139px
   available at every one of the four combos. Full numbers in the w4 handoff/report. */
.turn-stage { display: flex; justify-content: center; }
[data-scroll-scene="wow-turn"].scene-armed .turn-stage {
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  gap: var(--space-xl);
  padding-inline: var(--space-l);
  padding-top: calc(64px + var(--space-m));
}
.turn-monday { position: relative; z-index: 7; width: 100%; display: flex; justify-content: center; }
[data-scroll-scene="wow-turn"].scene-armed .turn-monday { width: auto; }
.monday-card { position: relative; background: var(--stone-white); border: 1px solid var(--hairline); border-radius: var(--radius-soft); padding: var(--space-m); max-width: 26em; } /* w4: 44em->26em, sits beside the bigger phone instead of spanning full width */
.monday-headline { font-size: clamp(1.8rem, 1.5rem + 1.6vw, 2.6rem); margin-bottom: var(--space-s); }
.monday-body { max-width: var(--measure-narrow); }

/* ---------- .board: shared component (verbatim from balnea) ---------- */
.board { background: var(--paper-white); border: 1px solid rgba(31, 42, 34, 0.06); border-radius: var(--radius-card); box-shadow: 0 1px 2px rgba(18, 18, 18, 0.06), 0 8px 24px rgba(18, 18, 18, 0.08); }
.board--card { padding: var(--space-l); }
.board-title { display: block; font-family: var(--font-product-title); font-weight: 600; line-height: 1.25; color: var(--ink); }
.board-pill { display: inline-flex; align-items: center; font-family: var(--font-product-meta); font-weight: 600; letter-spacing: 0; text-transform: none; border-radius: 999px; white-space: nowrap; border: none; }
.mini-board-pill-new { background: var(--chip-new-bg); color: var(--chip-new-fg); }
.mini-board-pill-seen { background: var(--chip-seen-bg); color: var(--chip-seen-fg); }
.board-row { padding: 0.55em 0.2em; border-bottom: 1px solid var(--hairline-soft); display: flex; flex-direction: column; gap: 0.4em; align-items: flex-start; }
.board-row:last-child { border-bottom: none; }

/* =========================================================================
   ACT II: ORDER (calm, airy, aligned -- same tokens, near-verbatim from balnea)
   ========================================================================= */

/* ---------- ghost numerals ---------- */
.ghost-section { position: relative; overflow: clip; } /* w15 fix: `hidden` made every ghost section a scroll container, silently killing position:sticky inside (the wys phone never actually stuck); `clip` crops the numeral identically without becoming one */
.ghost-numeral { position: absolute; top: -0.15em; left: calc(var(--space-m) * -0.4); font-family: "Abril Fatface", Georgia, serif; font-size: clamp(5rem, 18vw, 20rem); line-height: 1; color: var(--ink); opacity: 0.05; z-index: 0; pointer-events: none; user-select: none; }
.ghost-section .section-heading { position: relative; z-index: 1; }
.contact-band .ghost-numeral { color: var(--paper); opacity: 0.08; }

/* ---------- compare table ---------- */
.compare { position: relative; padding-block: var(--space-xl); border-bottom: 1px solid var(--hairline-soft); }
/* S3 scale contrast: steps up toward giant-type scale. Hand-computed fit check (Abril
   Fatface ~0.58em/char estimate, --content-max caps the container at 1280px even at a
   1600px viewport so the vw-preferred value is the only thing that grows): worst case
   is the 1600px viewport, where the clamp resolves to ~80px (never reaches its 5.2rem/
   83.2px cap) -- "The same week, twice." (22 chars) needs ~1020px at that size, fits
   the ~1192px content column on one line, no clipping at 1280 or 1600. */
.compare .section-heading { font-size: clamp(2.6rem, 1.8rem + 3.2vw, 5.2rem); max-width: none; }
/* F5: small warm supporting plate, positioned clear of the table -- never touches its
   copy (hidden under 900px so it can never crowd the table on a narrow column). */
.compare-photo-plate { position: absolute; top: var(--space-l); right: var(--space-m); width: min(14em, 22vw); z-index: 1; transform: rotate(3deg); }
.compare-photo-plate img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border: 1px solid var(--hairline); border-radius: var(--radius-soft); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.16); }
@media (max-width: 900px) { .compare-photo-plate { display: none; } }
.compare-table { border: 1px solid var(--hairline); border-radius: var(--radius-soft); overflow: hidden; }
.compare-row { display: grid; grid-template-columns: 1fr 1fr; border-bottom: 1px solid var(--hairline); }
.compare-row:last-child { border-bottom: none; }
.compare-row-head { background: var(--stone-white); }
.compare-cell { padding: var(--space-m); }
.compare-row > .compare-cell:first-child { border-right: 1px solid var(--hairline); }
.compare-cell-now { color: #686868; }
.compare-cell-with { color: var(--ink); }
.compare-tick { display: inline-block; width: 1em; height: 0.7em; margin-right: 0.5em; vertical-align: baseline; fill: none; stroke: var(--eucalyptus); stroke-width: 2.4px; stroke-linecap: round; stroke-linejoin: round; }
.compare-tick use { stroke-dasharray: 1; stroke-dashoffset: 1; }
@media (prefers-reduced-motion: no-preference) { .compare-tick use { transition: stroke-dashoffset 600ms cubic-bezier(0.23, 1, 0.32, 1); } .reveal:not(.js-hidden) .compare-tick use { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) { .compare-tick use { stroke-dashoffset: 0; } }
@media (max-width: 600px) { .compare-row { grid-template-columns: 1fr; } .compare-row > .compare-cell:first-child { border-right: none; border-bottom: 1px solid var(--hairline); } }

/* ---------- monday proof: ticker only (v5-journey removed w4, see what-you-see below) ---------- */
.monday-proof { padding-block: 0; }
.ticker { overflow: hidden; border-bottom: 1px solid var(--hairline-soft); background: var(--stone-white); }
.ticker-track { display: flex; width: max-content; white-space: nowrap; padding-block: 0.65em; }
.ticker-item { font-family: var(--font-product-meta); font-size: 0.85rem; font-weight: 600; color: #3a3a3a; }
.ticker-sep { color: var(--eucalyptus); opacity: 0.6; margin-inline: 0.9em; }
@media (prefers-reduced-motion: no-preference) { .ticker-track { animation: ticker-scroll 70s linear infinite; } } /* w85: 70s is the CONSTANT speed (Max). Was 40s here and overwritten every frame by a JS velocity loop that ran 70s at rest to 38s scrolling, so 40s was never actually seen. That loop is deleted; this value is now the whole truth. */
@keyframes ticker-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (hover: hover) and (pointer: fine) { .ticker:hover .ticker-track { animation-play-state: paused; } }
@media (prefers-reduced-motion: reduce) { .ticker { display: none; } }

/* ---------- report card component (reused verbatim by the merged what-you-see phone below;
   .journey-counter/.journey-caption also reused there as the narration beats' own copy).
   The old crossfading chip-span device and step-tinted wash are gone with v5-journey -- the
   card's status is now ONE real button (.report-status-btn, defined under what-you-see). ---------- */
.journey-counter { color: var(--eucalyptus); margin-bottom: 0.5em; }
.journey-caption { color: var(--ink); max-width: var(--measure-narrow); font-size: clamp(1.05rem, 1rem + 0.3vw, 1.25rem); }
.report-card { position: relative; transform: rotate(-1.5deg); display: block; transform-origin: center; }
@media (hover: hover) and (pointer: fine) {
  .report-card { transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1), box-shadow 160ms cubic-bezier(0.23, 1, 0.32, 1); }
  .report-card:hover { transform: rotate(-1.5deg) scale(1.03); box-shadow: 0 1px 2px rgba(18, 18, 18, 0.06), 0 14px 30px rgba(18, 18, 18, 0.14); }
}
.report-photo {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3; /* w5: naturally <=34% of screen height at every width in the 300-370px
    range (hand-computed: card width ~cardW, photo height 0.75*cardW; screen height per the
    390/844 aspect-ratio; ratio holds 31.4-32.1% across 300-370px -- no extra cap needed) */
  border-radius: calc(var(--radius-card) - 4px);
  background: var(--stone-200) url("assets/wet-floor.jpg?v=w84") center/cover; /* w84: this bust
     sat at ?v=w15 for ~68 versions. The HTML busts get bumped every release but this CSS one was
     never in that sweep, so a returning visitor kept being served the ORIGINAL leak photo no matter
     what we wrote to disk. Bump it with the release like everything else. */
  margin-bottom: 0.6em; /* w5: was var(--space-s) (rem-clamp) -- now em, scales with the screen */
}
.report-title {
  font-size: 1.15em; /* w5: was fixed 18px */
  margin-bottom: 0.5em;
  overflow: hidden;
  display: -webkit-box; /* w5: title capped at 2 lines max at this scale, per the plan spec.
    Overrides .board-title's display:block -- safe: same specificity, this rule is later in
    source order (verified against style.css). */
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.report-location, .report-submitted {
  display: block;
  font-family: var(--font-product-meta);
  font-size: 0.72em; /* w5: was 0.85rem -- now em */
  color: #5c574a;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; /* w5: one line each, per spec */
}
.report-submitted { margin-top: 0.15em; color: #8a8a8a; }

/* ---------- shared scroll-scene engine plumbing ---------- */
[data-scroll-scene] { position: relative; }
.scroll-scene-sticky { position: static; }
[data-scroll-scene].scene-armed { padding-block: 0; }

/* ---------- proof band (carried verbatim from balnea's "turn" -- the one dark section) ---------- */
.grain { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
.proof { position: relative; background: var(--turn-bg); color: var(--paper); padding-block: var(--space-xl); overflow: hidden; }
.proof::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  /* S3 (w2): scrim eased ~15% relative (0.9/0.8/0.6 -> 0.77/0.68/0.51) so the plate reads
     full-bleed and image-forward instead of a flat dark texture. Contrast re-verified
     below against the lightened backdrop. */
  background-image: linear-gradient(0deg, rgba(15,15,13,0.77) 0%, rgba(15,15,13,0.68) 45%, rgba(15,15,13,0.51) 100%), url("assets/card-scan5.jpg?v=w87");
  background-size: cover;
  background-position: center;
}
.proof-inner { position: relative; z-index: 2; max-width: 44em; }
.proof-headline { font-size: clamp(2rem, 1.5rem + 2.2vw, 3.2rem); margin-bottom: var(--space-m); }
/* w88 (Max, 4 Sep: the paragraph block "is too tall, making it look like an essay ... make it
   wider"). It was --measure-narrow (34em = 612px) inside a 44em .proof-inner offering 713.6px,
   so it sat 101.6px narrower than its own box for no reason. 100% fills that and matches the
   headline's width exactly. Measured cost, accepted by Max: ~66 -> ~77 chars/line, slightly
   past the 45-75 ideal. The TOKEN is deliberately not touched -- 6 other rules use it. */
.proof-body { max-width: 100%; opacity: 0.92; }
/* w87: the proof story is five short paragraphs now, not one block (Max's own rewrite, 4 Sep:
   before, the realisation, then the change). Spacing between them, and the closing three-beat
   line sits slightly brighter because it is the payoff. */
.proof-body + .proof-body { margin-top: var(--space-s); }
.proof-body-close { opacity: 1; font-weight: 600; }
.proof-stat-wrap { position: relative; z-index: 2; padding-top: var(--space-l); }
.stat-tiles { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: var(--space-s); }
@media (max-width: 900px) { .stat-tiles { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px) { .stat-tiles { grid-template-columns: 1fr; } }
.stat-tile { background: rgba(249, 247, 242, 0.06); border: 1px solid rgba(249, 247, 242, 0.16); border-radius: var(--radius-soft); padding: var(--space-m); }
.stat-tile-primary { display: flex; flex-direction: column; justify-content: center; }
/* w88: second line inside the security tile (Max: "we can do both ... put both in"). Quieter
   than the claim above it, because it is the evidence for that claim rather than a new one. */
.stat-tile-sub { margin-top: 0.5em; opacity: 0.7; font-size: 0.85em; }
.kpi-number-wrap { line-height: 1; }
.kpi-number { color: var(--paper); font-size: clamp(3.5rem, 2.5rem + 6vw, 6.5rem); line-height: 0.9; }
.kpi-sub { color: var(--paper); opacity: 0.85; max-width: var(--measure-narrow); margin-top: var(--space-s); }
.stat-tile-label { color: var(--paper); opacity: 0.85; align-self: center; }
.centre-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-s); margin-top: var(--space-s); }
@media (max-width: 600px) { .centre-tiles { grid-template-columns: 1fr; } }
.centre-name { font-family: var(--font-display-quiet); font-size: 1.3rem; color: var(--paper); margin-bottom: 0.4em; }
.centre-sub { color: var(--paper); opacity: 0.85; }

/* ---------- photo breaks (courtyard deleted w5 -- purposeless per Max's own test; walkway
   stays, its caption gives it a job) ---------- */
.photo-break { position: relative; overflow: hidden; display: flex; align-items: flex-end; }
.photo-break-walkway { min-height: 60vh; }
.photo-break-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
.photo-break-scrim { position: absolute; inset: 0; z-index: 1; background-image: linear-gradient(0deg, rgba(20,20,18,0.78) 0%, rgba(20,20,18,0.4) 32%, rgba(20,20,18,0) 58%); }
.photo-break-caption { position: relative; z-index: 2; color: var(--paper); padding: var(--space-m); max-width: 22em; font-size: 0.95rem; }

/* ---------- what it is ---------- */
.what-it-is { padding-block: var(--space-xl); border-bottom: 1px solid var(--hairline-soft); }
/* w27 proximity fix (the alignment reel's spacing rule, Max: "spacing and sizing to communicate"):
   heading-to-first-item gap measured 92px vs 93px BETWEEN items - uniform spacing groups nothing.
   Halving the heading gap makes the heading visibly own its list. */
.what-it-is .section-heading { margin-bottom: calc(var(--space-l) / 2); }
.section-heading { font-size: clamp(1.9rem, 1.5rem + 1.8vw, 2.8rem); max-width: 24em; margin-bottom: var(--space-l); }
.three-col { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-l); margin-bottom: var(--space-l); }
.three-col h3 { font-family: var(--font-body); font-weight: 600; font-size: 1.15rem; margin-bottom: var(--space-s); }
@media (max-width: 860px) { .three-col { grid-template-columns: 1fr; } }
.footer-line { border-top: 1px solid var(--hairline); padding-top: var(--space-m); }

/* ---------- what you see: MERGED with the old v5-journey (w4, Max's design) ----------
   Narration column (normal document flow, .reveal paper-lift, NO scrub scene -- interaction
   is the point) beside a sticky phone holding ONE live, tappable report card. The old
   .board--full 5-row mockup and its two-column .wys-text/.wys-plate layout are both gone. */
.what-you-see { padding-block: var(--space-xl); border-bottom: 1px solid var(--hairline-soft); background: var(--stone-white); }
.board-pill-amber { background: var(--chip-amber-bg); color: var(--chip-amber-fg); } /* still used by the untouched turn-phone-screen rows */

.wys-merged-inner { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-l); align-items: start; }
@media (max-width: 860px) { .wys-merged-inner { grid-template-columns: 1fr; } }
.wys-narration { display: flex; flex-direction: column; gap: var(--space-l); }
/* tail room so the sticky phone stays fully pinned THROUGH the 05 DONE beat -- without it
   the column ends with the last caption and the release drags the phone's top off screen
   exactly as the reader gets there (Max, 28 Aug). Scoped: thanks.html's shorter, non-sticky
   narration must not inherit the tail. */
.what-you-see .wys-narration { padding-bottom: 22vh; }
.wys-beat p { margin-bottom: var(--space-s); max-width: var(--measure-narrow); }
.wys-beat .journey-caption { margin-bottom: 0; }

.wys-phone-col {
  --phone-w: min(clamp(300px, 23vw, 370px), calc((100vh - 130px) * 0.4621));
  position: sticky; top: 110px; align-self: start; } /* w5: literal ~110px per spec, was calc(64px+space-l) */
@media (max-width: 600px), (prefers-reduced-motion: reduce) {
  .wys-merged-inner { grid-template-columns: 1fr; }
  .wys-phone-col { position: static; margin-top: var(--space-l); }
}
/* w5 REBUILD (root cause per the plan: the desktop .report-card/.board--card component was
   reused verbatim inside a ~276px screen -- desktop --space-l padding and fixed-px type
   overflowed it, clipping the card top/bottom and wrapping the pill into a blob). Sized by
   WIDTH now (was height-based off vh, which could starve the screen at short viewports):
   same visual recipe as .turn-phone (ink bezel, rounded, aspect-ratio), independent,
   always-visible (no scroll-scene gating -- interaction-driven, must render under reduced
   motion too). On short viewports the bottom may crop below the fold; content is anchored
   to the TOP of the screen (flex-direction:column, no vertical centering) so nothing
   meaningful is ever the part that's cut. */
.wys-phone {
  position: relative;
  /* height-capped (Max, 28 Aug: "at 05 done the top part of the phone is gone... should we
     make the phone smaller?"): 23vw resolved to a 680px-tall phone at 1366x768 -- 22px past
     the fold from its 110px sticky top, so it could NEVER be fully on screen. The second
     term caps width so height (aspect 390/844, factor 0.4621) fits under the header with
     slack. */
  /* w66: same width expression as before, just named -- --phone-w now also drives the bezel
     and radius below (proportional match to .turn-phone, Max's brief: this phone's chrome was
     2.44%/11.94% of its width against the reference's 3.38%/16.53%, i.e. visibly thinner). */
  /* w79c: --phone-w HOISTED to .wys-phone-col (below) so the Manager/Tenant pill, which is a
     SIBLING of this phone, can size itself to exactly the phone's width. Declaring it in two
     places would let them drift; the col is the nearest common ancestor, so one source feeds
     both. The @media (max-width:600px) override at ~1338 targets .turn-phone, not this phone,
     so this was the only declaration and nothing else has to move. */
  width: var(--phone-w);
  height: auto;
  aspect-ratio: 390 / 844;
  margin-inline: auto;
  background: var(--ink);
  border-radius: calc(var(--phone-w) * var(--phone-radius-ratio));
  padding: calc(var(--phone-w) * var(--phone-bezel-ratio));
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.18);
}
.wys-phone-screen {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  background: var(--paper-white);
  border-radius: calc(var(--phone-w) * (var(--phone-radius-ratio) - var(--phone-bezel-ratio))); /* w66: concentric, computed off --phone-w */
  overflow: hidden; /* short-viewport crop happens here, by design -- content above stays anchored top */
  padding: 3.3em 0.9em 0.9em; /* w63: top clears the floating Dynamic Island (screen-em: 40-50px, island bottom lands 36-42px in); was 1.1em when the notch was flush */
  /* w5: screen-width-relative type scale -- a calc-based em base (ponytail: no container
     queries needed). Mirrors .wys-phone's own width clamp exactly, scaled by 16/390 (the
     phone's logical-point base), so 1em here is always the same fraction of the phone's
     rendered width regardless of viewport: 300px->12.3px, 23vw->0.94vw, 370px->15.18px. */
  font-size: clamp(12.3px, 0.94vw, 15.18px);
}
/* THE root-cause fix: kills .board--card's desktop var(--space-l) padding (40-88px) that
   was being applied verbatim inside this ~300-370px screen -- replaced with a small em
   value that scales with the screen's own type. Specificity (0,3,0) beats .board--card's
   (0,1,0) regardless of source order. */
.wys-phone-screen .report-card.board--card { padding: 0.8em; }
/* the card renders straight inside the phone (a real app screen, not a tilted desk photo --
   the -1.5deg tilt + hover-flutter above belongs to the old floating-card context only).
   w86: the MONDAY phone is joined to this rule. It is the same context and the comment above
   already said so, but its screen was never listed, so its card sat at -1.5deg -- which tilted
   the leak photo, the status pill and the "Outside Shop 12 / Submitted Sunday, 7:03pm" lines
   off level inside a phone screen (Max: "i want it to be flat horizontally"). Measured before:
   Monday card -1.5deg, interactive card 0deg. */
.wys-phone-screen .report-card,
.wys-phone-screen .report-card:hover,
.turn-phone .turn-phone-screen .report-card,
.turn-phone .turn-phone-screen .report-card:hover { width: 100%; transform: none; box-shadow: none; }
.wys-phone-hint { margin-top: var(--space-s); color: #8a8a8a; text-align: center; }

/* w86: the brand claim under the hero paragraph (Max, 3 Sep: "One central channel, for the whole
   centre"). Deliberately NOT the .hero-sub label: merged into that small-caps line it read as an
   uppercase run-on of three unrelated sentences. Its own line, body size, weight for emphasis,
   sitting between the paragraph and the no-app label. No new type scale, no new colour. */
.hero-claim {
  font-family: var(--font-body);
  font-weight: 600;
  color: var(--ink);
  margin-top: var(--space-s);
}
/* quiet board context under the interactive card (Max, 28 Aug aesthetic pass; w61: rows are
   now real, tappable -- reads as the board with one card open, tap a row to open it). Same
   shared .board-row chrome as the turn phone. Sits directly under the hint (w61: killed the
   old margin-top:auto bottom-push, the phone-screen flow just ends here now). */
.wys-context-rows { width: 100%; margin-top: var(--space-m); }
/* w66: one .wys-row wrapper per REPORTS entry, fixed order, never reordered -- header (the
   old .board-row button, class kept for the chrome below) plus its own collapsible panel
   right underneath it, instead of the old flat list of 3 collapsed rows beside a card pinned
   separately above them. */
.wys-row { width: 100%; }
.wys-context-rows .board-row {
  display: flex; flex-direction: row; align-items: center; justify-content: space-between; gap: 0.6em;
  padding: 0.55em 0.2em; border-bottom: 1px solid var(--hairline); font-size: 0.85em; /* w73 NOTE: this declaration is DEAD for the w66+ card layout -- .wys-context-rows .board-row is re-declared at the same specificity further down (see "separation is the gap between cards now"), and later wins. Change the spacing THERE, not here. Three edits were made here before that was noticed. */
  width: 100%; min-height: 44px; margin: 0; background: transparent; border-radius: 0;
  border-left: none; border-right: none; border-top: none; font: inherit; text-align: left;
  cursor: pointer; opacity: 0.8; /* real buttons now, w61 */
}
/* flex-direction: row matters: the shared .board-row (631) is a COLUMN and this rule
   originally didn't override that property, so titles stacked centred over their pills */
/* w66: was .wys-context-rows .board-row:last-child -- the header is no longer a direct child
   of #wys-context-rows (each sits inside its own .wys-row wrapper alongside its panel), so
   :last-child on the header itself never matched its row position any more. Select by the
   wrapper's position instead. */
.wys-row:last-child .board-row { border-bottom: none; }
/* w66 defect fix: was ungated, so a tap on a touch device could leave a row "stuck" highlighted
   (touch fires :hover with no matching pointerup to clear it). Lift is a hover-capable-device
   affordance only; :focus-visible (keyboard) stays on unconditionally below. */
@media (hover: hover) and (pointer: fine) {
  .wys-context-rows .board-row { transition: opacity 120ms ease; }
  .wys-context-rows .board-row:hover { opacity: 1; }
}
.wys-context-rows .board-row:focus-visible { opacity: 1; }
/* w61: title takes the flexible space and ellipsizes, time/pill stay fixed -- 3 items in the
   row now (was 2), needed so the longest title ("Car park lights out near trolley bay") can't
   push the time/pill off the edge of a 300px-wide phone screen. */
.wys-context-rows .board-title { font-size: 1em; flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wys-context-rows .board-pill { flex: 0 0 auto; font-size: 0.72em; padding: 0.3em 0.6em; }
.wys-context-rows .board-time { flex: 0 0 auto; color: #8a8a8a; font-size: 0.78em; white-space: nowrap; } /* w61: muted, reuses .report-submitted's colour */
.mini-board-pill-done { background: rgba(45, 90, 66, 0.14); color: var(--eucalyptus-dark); } /* w61: matches .report-status-btn-done's recipe */

/* w66: the accordion panel itself -- height set from JS (scrollHeight), never to `auto` mid-
   transition (main.js pins an explicit px value, animates to/from it, only settles on 0 at
   rest). overflow:hidden is what actually makes height:0 read as "closed". */
.wys-row-panel { height: 0; overflow: hidden; opacity: 0; }
@media (prefers-reduced-motion: no-preference) {
  .wys-row-panel { transition: height 220ms var(--ease-out), opacity 220ms var(--ease-out); }
}
@media (prefers-reduced-motion: reduce) {
  /* expand/collapse itself is instant (no height transition); a short opacity crossfade is
     kept so content doesn't hard-cut in and out (site rule: reduced motion is gentler, not
     zero -- keep opacity, drop movement). */
  .wys-row-panel { transition: opacity 120ms ease; }
}
.wys-row-panel .report-card.board--card { margin-top: 0.2em; } /* small clearance under the header it now sits inside */
.wys-panel-meta { display: block; } /* transform (the stagger below) has no effect on an inline box */
/* w85 (Max: make this phone's animation match the Kingscliff hub). The hub reveals an opened
   row's contents as ONE opacity fade over the same 220ms and the same curve as the row expand
   itself (tenant-hub/src/index.css .ui-a .board-row-a-panel-inner), with no transform and no
   stagger. This phone was running a three-part sequence instead -- photo / status / meta,
   contact-step-in 300ms from translateY(6px), 40ms apart -- which is a different recipe from the
   product it depicts. Now the same single fade, so the two read identically side by side.
   Superseded: the w66 stagger and w67's 0/40/80 re-tightening. */
@media (prefers-reduced-motion: no-preference) {
  .wys-row-panel .report-photo,
  .wys-row-panel .report-status-btn,
  .wys-row-panel .wys-panel-meta { animation: wys-panel-fade 220ms var(--ease-out) both; }
}
@media (prefers-reduced-motion: reduce) {
  /* no stagger, no translate -- just the opacity half of the same reveal, all at once */
  .wys-row-panel .report-photo,
  .wys-row-panel .report-status-btn,
  .wys-row-panel .wys-panel-meta { animation: wys-panel-fade 120ms ease both; }
}
@keyframes wys-panel-fade { from { opacity: 0; } to { opacity: 1; } }

/* w61: explicit two-segment view control, top of the phone screen (replaces the old bottom
   "From their phone" toggle button). Hairline-bordered rounded group, active segment filled --
   same --ink/--paper/--hairline tokens and aria-pressed pattern as the rest of the page. */
.wys-view-switch { display: flex; width: 100%; gap: 0.2em; margin-bottom: var(--space-s); padding: 0.2em; border: 1px solid var(--hairline); border-radius: 999px; }
.wys-view-btn {
  flex: 1;
  min-height: 44px; /* fixed, not em -- same reasoning as .report-status-btn: a real tap target */
  padding-inline: 0.4em;
  background: transparent;
  border: none;
  border-radius: 999px;
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.78em;
  cursor: pointer;
}
.wys-view-btn[aria-pressed="true"] { background: var(--ink); color: var(--paper-white); }

/* the status pill is a real button now (was 4 crossfading spans under v5-journey) -- one
   label at a time, swapped by main.js on tap or as a narration beat scrolls into view.
   w5: control affordance added (ink-tinted border, :active press, CSS-only cycle glyph) so
   it reads as a control, not a static chip. min-height:44px is a hard tap-target floor and
   deliberately NOT em-scaled -- a real phone UI keeps a constant tap target regardless of
   type scale (matches platform HIG/Material guidance), so this is correct, not an
   inconsistency with the rest of the em-scaled card. */
.report-status-btn {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: 0.85em;
  margin-block: 0.5em;
  border: 1px solid rgba(31, 42, 34, 0.22); /* w5: ink-tinted */
  border-radius: 999px;
  font-family: var(--font-product-meta);
  font-weight: 600;
  font-size: 0.95em; /* w5: was fixed 13px */
  cursor: pointer;
  transition: background-color 160ms ease, color 160ms ease, transform 80ms ease;
}
.report-status-btn::after { content: "\21BB"; margin-left: 0.45em; font-size: 0.9em; } /* w5: CSS-only cycle glyph, not new copy */
.report-status-btn:active { transform: translateY(1px) scale(0.98); } /* w5: press affordance */
.report-status-btn-new { background: var(--chip-new-bg); color: var(--chip-new-fg); }
.report-status-btn-seen { background: var(--chip-seen-bg); color: var(--chip-seen-fg); }
.report-status-btn-progress { background: var(--chip-amber-bg); color: var(--chip-amber-fg); }
.report-status-btn-done { background: rgba(45, 90, 66, 0.14); color: var(--eucalyptus-dark); }
/* w61: tenant view is read-only (main.js sets disabled instead of toggling a class) -- strips
   the control affordance so it reads as a plain status pill, no cycle glyph, no press. Keyed
   off :disabled directly so both index.html and thanks.html get it for free. */
.report-status-btn:disabled { cursor: default; border-color: transparent; }
.report-status-btn:disabled::after { content: none; }

/* w61: tenant view keeps location + submitted visible (Max's spec) -- the old
   display:none rule for them is gone; only the status control changes between views. */

/* ---------- how it starts (4-step process, four photo tile slots) ---------- */
.how-it-starts { padding-block: var(--space-xl); border-bottom: 1px solid var(--hairline-soft); }
.steps-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-s); }
@media (max-width: 900px) { .steps-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px) { .steps-grid { grid-template-columns: 1fr; } }
.step-tile { background: var(--stone-100); border: 1px solid var(--hairline); border-radius: var(--radius-soft); overflow: hidden; }
.step-tile-photo { aspect-ratio: 4 / 3; background: var(--stone-200); }
.step-tile-photo img { width: 100%; height: 100%; object-fit: cover; }
.step-tile-body { padding: var(--space-m); }
.step-body { color: var(--ink); }

/* ---------- not-do ---------- */
.not-do { padding-block: var(--space-xl); border-bottom: 1px solid var(--hairline-soft); }
.grid-2x2 { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid var(--hairline); border-radius: var(--radius-soft); overflow: hidden; margin-bottom: var(--space-l); }
.cell { padding: var(--space-l); border-right: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline); }
.cell:nth-child(2n) { border-right: none; }
.cell:nth-child(3), .cell:nth-child(4) { border-bottom: none; }
.cell h3 { font-family: var(--font-body); font-weight: 600; font-size: 1.1rem; margin-bottom: var(--space-s); }
@media (max-width: 720px) { .grid-2x2 { grid-template-columns: 1fr; } .cell { border-right: none; } .cell:nth-child(4) { border-bottom: none; } }
.admission { font-size: clamp(1.15rem, 1.05rem + 0.4vw, 1.4rem); max-width: var(--measure-narrow); font-weight: 600; }
/* w5: not-do photo band deleted -- failed Max's own purpose test ("cut off, idk what they
   are for"), same as the courtyard break above. */

/* ---------- pricing ---------- */
.pricing { padding-block: var(--space-xl); border-bottom: 1px solid var(--hairline-soft); background: var(--stone-100); }
/* S3 scale contrast: a smaller giant-step than compare's -- "The price is on the page.
   That is deliberate." is 47 chars, too long for one line at any giant size, so this is
   tuned to wrap to exactly 2 lines instead of 4-5. Worst case 1600px viewport: clamp
   resolves to ~64px (below its 4.4rem/70.4px cap) -> ~32-char capacity per line in the
   ~1192px column; natural break "The price is on the page." (26) / "That is deliberate."
   (20) both clear it with margin, no clipping at 1280 or 1600. */
.pricing .section-heading { font-size: clamp(2.3rem, 1.6rem + 2.4vw, 4.4rem); max-width: none; line-height: 1.15; }
.pricing-sub { max-width: var(--measure-narrow); margin-bottom: var(--space-l); color: #3a3a3a; }
.price-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-m); margin-bottom: var(--space-l); }
.price-card { border: 1px solid var(--hairline); border-radius: var(--radius-soft); padding: var(--space-l) var(--space-m); background: var(--stone-white); }
.price-card h3 { font-family: var(--font-body); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.9rem; margin-bottom: var(--space-s); }
.price { font-family: var(--font-display); font-size: clamp(2rem, 1.6rem + 1.6vw, 2.8rem); margin-bottom: var(--space-s); }
.price span { font-family: var(--font-body); font-size: 1rem; font-weight: 400; }
.price-tag { font-weight: 600; margin-bottom: var(--space-s); }
@media (max-width: 900px) { .price-cards { grid-template-columns: 1fr; } }
.price-card--pro { background: var(--stone-200); border-color: var(--hairline); }
.price-card--ops { background: var(--ink-green); border-color: var(--ink-green); }
.price-card--ops h3, .price-card--ops .price, .price-card--ops .price-tag, .price-card--ops .price-body { color: var(--paper); }
.price-card--ops .price span { color: var(--paper); opacity: 0.75; }
.pricing-fine p { max-width: var(--measure-narrow); margin-bottom: var(--space-s); color: #3a3a3a; }

/* ---------- FAQ ---------- */
.faq { padding-block: var(--space-xl); border-bottom: 1px solid var(--hairline-soft); }
.faq-list { display: flex; flex-direction: column; gap: var(--space-s); }
.faq-item { border: 1px solid var(--hairline); border-radius: var(--radius-soft); padding-inline: var(--space-m); background: var(--stone-white); }
.faq-item summary { min-height: 44px; display: flex; align-items: center; justify-content: space-between; gap: var(--space-s); padding-block: var(--space-s); cursor: pointer; font-family: var(--font-body); font-weight: 600; font-size: 1.05rem; list-style: none; }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after { content: "+"; font-family: var(--font-body); font-weight: 600; color: var(--ink); font-size: 1.4rem; line-height: 1; flex-shrink: 0; }
.faq-item[open] summary::after { content: "\2212"; }
.faq-item summary:hover { color: var(--eucalyptus); }
.faq-item p { padding-bottom: var(--space-m); max-width: var(--measure-narrow); color: #3a3a3a; }

/* ---------- contact ---------- */
.contact { padding-block: 0; }
.contact-band { background: var(--eucalyptus-dark); color: var(--paper); padding-block: var(--space-xl) var(--space-l); }
.contact-band-inner .section-heading { color: var(--paper); }
.contact-band .contact-body { color: var(--paper); opacity: 0.88; }
/* w27 proximity fix: the band's heading literally introduces step 1, but 234px (largest gap on
   the page) separated them - the form now reads as the band's continuation, not a new topic.
   w82 (Max: the questions "kinda look a bit disconnected" from the 08 above): proximity was
   only half of it, and closing the gap could never finish the job. The real separator is a hard
   DARK-TO-LIGHT COLOUR EDGE - the band is --eucalyptus-dark and the form sat on bare paper, so
   they read as two sections however close together they were. The form now TUCKS UP into the
   band on its own paper-coloured card: invisible against the page below, but it interrupts the
   green edge above, which is what ties the two into one block. */
.contact-form-wrap {
  padding-block: var(--space-m) var(--space-xl);
  position: relative;
  z-index: 2;
  margin-top: calc(var(--space-l) * -1);
  background: var(--paper);
  border-top-left-radius: var(--radius-card);
  border-top-right-radius: var(--radius-card);
}
.contact-body { max-width: var(--measure-narrow); margin-bottom: var(--space-l); color: #3a3a3a; }
#contact-form { max-width: 40em; }
.field { margin-bottom: var(--space-m); }
.field label { display: block; font-weight: 600; margin-bottom: 0.4em; }
.field input { width: 100%; border: 1px solid var(--ink); background: var(--paper-white); padding: 0.75em 0.9em; border-radius: var(--radius-soft); min-height: 44px; }
.field input:focus-visible { outline: none; border: 2px solid var(--eucalyptus); }

/* ---------- contact: one-question-at-a-time flow (w19) ---------- */
.contact-progress { display: flex; align-items: center; justify-content: space-between; gap: var(--space-s); margin-bottom: var(--space-m); }
.contact-progress-label { color: var(--ink); margin: 0; }
.contact-progress-dots { display: flex; gap: 0.5em; }
.contact-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--hairline); }
.contact-dot.is-current { background: var(--eucalyptus); box-shadow: 0 0 0 3px rgba(45, 90, 66, 0.18); }
.contact-dot.is-done { background: var(--eucalyptus-light); }

.contact-step { border: 0; padding: 0; margin: 0 0 var(--space-m); min-width: 0; }
@media (prefers-reduced-motion: no-preference) {
  .contact-step:not([hidden]) { animation: contact-step-in 220ms ease both; }
}
@keyframes contact-step-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

.contact-question { display: block; width: 100%; padding: 0; border: 0; font-family: var(--font-display); font-weight: 400; font-size: clamp(1.4rem, 1.2rem + 1vw, 2rem); line-height: 1.18; color: var(--ink); margin-bottom: var(--space-m); }

.contact-pills { display: flex; flex-direction: column; gap: var(--space-s); max-width: var(--measure-narrow); }
.contact-pill { display: flex; align-items: center; min-height: 48px; width: 100%; padding-inline: 1.1em; border: 1px solid var(--hairline); border-radius: 999px; background: var(--stone-white); color: var(--ink); font-family: var(--font-body); font-weight: 600; font-size: 0.95rem; text-align: left; cursor: pointer; transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease; }
.contact-pill:hover { border-color: var(--eucalyptus); color: var(--eucalyptus-dark); }
.contact-pill:active { background: rgba(45, 90, 66, 0.14); }

.contact-other-field { margin-top: var(--space-m); max-width: var(--measure-narrow); }
.contact-other-field label { display: block; font-weight: 600; margin-bottom: 0.4em; }
.contact-other-field input { width: 100%; border: 1px solid var(--ink); background: var(--paper-white); padding: 0.75em 0.9em; border-radius: var(--radius-soft); min-height: 44px; margin-bottom: var(--space-s); }
.contact-other-field input:focus-visible { outline: none; border: 2px solid var(--eucalyptus); }

.contact-step-actions { display: flex; align-items: center; gap: var(--space-m); margin-top: var(--space-s); }
.contact-back { min-height: 44px; padding-inline: 0.25em; background: none; border: 0; text-decoration: underline; font-family: var(--font-body); font-weight: 600; font-size: 0.9rem; color: var(--ink); cursor: pointer; }
.contact-back:hover { color: var(--eucalyptus-dark); }

/* w21: retry line replaces the old mailto fallback/explainer -- shown only on a failed
   submission, contains no success claim (5.56:1 contrast, --amber on --paper, checked). */
.contact-error { margin-top: var(--space-s); color: var(--amber); font-weight: 600; }

/* ---------- footer (giant wordmark -- kept) ---------- */
.site-footer { border-top: 1px solid var(--hairline); padding-block: var(--space-l) 0; background: var(--ink); color: var(--paper); overflow: hidden; }
.footer-brand { font-weight: 600; margin-bottom: 0.4em; }
.footer-line-2 { opacity: 0.75; margin-bottom: var(--space-s); }
.footer-links a { text-decoration: underline; }
.footer-links a:hover { color: var(--eucalyptus-light); }
.footer-giant-mark { margin-top: var(--space-l); height: clamp(3rem, 11vw, 8rem); overflow: hidden; line-height: 1; }
.giant-text { display: block; font-family: var(--font-display); font-size: clamp(4.5rem, 17vw, 13rem); line-height: 1; white-space: nowrap; color: rgba(249, 247, 242, 0.14); }
.giant-dot { color: var(--eucalyptus); }

/* ---------- scroll reveal: paper-lift signature (verbatim) ---------- */
@media (prefers-reduced-motion: no-preference) {
  .reveal.js-hidden { opacity: 0; filter: blur(10px); transform: translateY(16px) rotate(1.5deg); }
  .reveal.js-hidden.reveal-alt { transform: translateY(16px) rotate(-1.5deg); }
  .reveal { transition: opacity 0.55s ease, transform 0.55s ease, filter 0.55s ease; }
}

/* ---------- S2 iris reveals (compare/proof wraps -- courtyard deleted w5): CSS-transition, NOT scrub.
   Composes with the existing paper-lift above (same .reveal/.js-hidden toggle, no new
   JS) rather than replacing it -- the wrap fades/lifts in AND its clip-path opens at
   the same time. Placed after .reveal above so this rule's combined transition list
   (same specificity, later in source) wins over the shorthand there. Reduced motion:
   this whole block is gated, so clip-path is simply never set (initial value = no
   clip) and .js-hidden itself is never added by the observer under reduced motion --
   "none" by construction, no explicit override needed. Ghost numerals are siblings of
   these wraps, never children, so they are never clipped. ---------- */
@media (prefers-reduced-motion: no-preference) {
  .iris-reveal {
    clip-path: inset(0);
    transition: opacity 0.55s ease, transform 0.55s ease, filter 0.55s ease, clip-path 0.9s ease-out;
  }
  .iris-reveal.js-hidden { clip-path: inset(12%); }
}

/* ---------- UC cursor progress ring: desktop >991px, pointer:fine only (main.js gates
   creation; this media query is the CSS-side kill for touch/coarse/narrow/reduced-
   motion). Native cursor stays visible -- this is a small trailing dot + ring, not a
   replacement cursor. Self-stopping rAF loop lives in main.js (initCursorRing). ---------- */
.cursor-ring {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 40;
  width: 32px;
  height: 32px;
  pointer-events: none;
  transform: translate(-9999px, -9999px);
  will-change: transform;
}
.cursor-ring svg { width: 32px; height: 32px; overflow: visible; }
.cursor-ring-track { fill: none; stroke: rgba(31, 42, 34, 0.15); stroke-width: 2; }
.cursor-ring-fill {
  fill: none;
  stroke: var(--eucalyptus);
  stroke-width: 2;
  stroke-linecap: round;
  transform-origin: 16px 16px;
  transform: rotate(-90deg);
}
/* w81: the dot left the ring (see index.html) so it can track the pointer 1:1 while the ring eases
   behind it. It is its own fixed element now, offset by half its size in the JS transform. */
.cursor-dot {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 41;              /* above the ring, so it is never hidden behind the stroke */
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ink);
  pointer-events: none;
  transform: translate(-9999px, -9999px);
  will-change: transform;
}
@media (hover: none), (pointer: coarse), (max-width: 991px), (prefers-reduced-motion: reduce) {
  .cursor-ring, .cursor-dot { display: none; }
}

/* w81 -- hide the NATIVE cursor, but only once main.js has confirmed the custom one is running.
   The .custom-cursor class is added at the end of initCursorRing's guard chain, so touch, coarse
   pointers, narrow viewports, reduced motion and any JS failure all keep a real pointer. Hiding
   this from CSS alone would strand those users with no cursor at all.
   Interactive elements set their own cursor (pointer / text), so they need it too or the native
   one reappears the moment you touch a link. */
.custom-cursor,
.custom-cursor a, .custom-cursor button, .custom-cursor summary,
.custom-cursor input, .custom-cursor textarea, .custom-cursor select,
.custom-cursor [role="button"], .custom-cursor label {
  cursor: none;
}

/* ---------- thanks.html (w22): calm static page, no scroll scenes/Lenis. Reuses the
   .wys-merged-inner grid + .wys-phone component verbatim (see what-you-see above) for its
   tap-only demo; these rules style the page's own copy blocks. ---------- */
.thanks-hero { padding-block: var(--space-xl) var(--space-l); border-bottom: 1px solid var(--hairline-soft); }
/* the page's one hero -- bigger than the standard section-heading scale (2.8rem cap). Reuses
   the exact clamp already proven for .compare's own hero-scale heading elsewhere in this file,
   not a new size invented for this page. */
.thanks-hero .section-heading { font-size: clamp(2.6rem, 1.8rem + 3.2vw, 5.2rem); }
.thanks-lede { max-width: var(--measure-narrow); margin-top: var(--space-s); color: #3a3a3a; font-size: clamp(1.05rem, 1rem + 0.3vw, 1.25rem); }
.thanks-demo { padding-block: var(--space-l) var(--space-xl); border-bottom: 1px solid var(--hairline-soft); background: var(--stone-white); }
.thanks-demo-inner .wys-narration { justify-content: center; }
.thanks-demo-line { max-width: var(--measure-narrow); margin-top: 0.5em; font-size: clamp(1.05rem, 1rem + 0.3vw, 1.25rem); }
/* w22 fix (defect 1): .wys-phone's aspect-ratio:390/844 is sized for index.html's taller
   sticky narration column. This page's narration is shorter, so the phone frame (built to that
   fixed shape) left a tall empty void below the toggle button. Scoped to this page only: the
   frame now hugs its content -- no aspect-ratio, height:auto -- with a floor so it never reads
   thinner than a phone at short content lengths. */
.thanks-demo-inner .wys-phone { aspect-ratio: auto; height: auto; }
.thanks-demo-inner .wys-phone-screen { height: auto; min-height: 400px; }
.thanks-next-list { display: flex; flex-direction: column; gap: var(--space-s); max-width: var(--measure-narrow); margin-top: var(--space-m); }
.thanks-next-list li { position: relative; padding-left: 1.3em; }
.thanks-next-list li::before { content: "\2022"; position: absolute; left: 0; color: var(--eucalyptus); font-weight: 700; }
.thanks-back-link { padding-block: var(--space-l) var(--space-xl); }
.thanks-back-link a { min-height: 44px; display: inline-flex; align-items: center; font-weight: 600; text-decoration: underline; }
.thanks-back-link a:hover { color: var(--eucalyptus-dark); }

/* =========================================================================
   MOBILE STATIC TREATMENT (<=600px): the three scroll scenes never arm below
   600px (mediaOK in main.js requires innerWidth > 600), so without this the
   hero/week/turn sections show zero notification pieces there -- just the
   plain stacked copy. initMobileStatic() in main.js poses a curated subset of
   the SAME existing .scrap/.week-artifact/.turn-artifact spans with inline
   styles (no new DOM) and adds class "mobile-static" to each section + class
   "mob-piece" to every curated piece. Everything below is keyed off those two
   classes; the armed/.scene-armed path above is untouched. ---------- */
@media (max-width: 600px) {
  /* one rule shows every curated piece across all three zones + kills their
     pointer-events (decorative, must never intercept a tap) -- the base
     .scrap/.week-artifact/.turn-artifact rules default to display:none, this
     wins on specificity (2 classes vs 1) with no !important needed. */
  /* :not(.turn-artifact-gen) matters: generateTurnFill() clones the hand-authored turn
     pieces AFTER initMobileStatic() has posed them, so clones of posed pieces inherit the
     mob-piece class -- without the exclusion ~51 clones render at their desktop grid poses
     all over the mobile turn section (live-caught 27 Aug, w34 gate). */
  .mobile-static .mob-piece:not(.turn-artifact-gen) { display: block; pointer-events: none; }

  /* width caps (round 2, Max: cut-off pieces + hero "on the sides"): uncapped 15em chips span
     62% of a 390px screen -- hero pieces met in the middle instead of reading as two side
     piles, and turn pieces overhung the viewport edge and were clipped. Caps make the poses
     in main.js hold at 360px, the narrowest real target. */
  .hero.mobile-static .mob-piece { max-width: 9.5em; }
  .the-turn.mobile-static .mob-piece { max-width: 12em; }

  /* ZONE 1 -- hero (round 3, Max: "creeping out on the side next to the text"):
     no reserved band; pieces are posed by JS with NEGATIVE insets so they peek
     in from the screen edges beside the text. The clip crops that bleed at the
     hero's box -- the pieces' containing block is .hero (position:relative),
     so the sticky's own overflow:hidden cannot clip them, and unclipped bleed
     would open horizontal page scroll. clip, not hidden: pure crop, never a
     scroll container (the overflow:hidden-kills-sticky lesson). */
  .hero.mobile-static { overflow: hidden; overflow: clip; }

  /* ZONE 2 -- week: the beats' min-height:65vh "empty top margin" does NOT exist at 390px
     (content is taller than the min-height, live-measured 27 Aug), so space for the two
     artifacts per beat is RESERVED here instead of assumed -- same by-construction trick
     as the hero band above. initMobileStatic poses into the first ~2-14vh of this pad. */
  .week-scene.mobile-static .cascade { padding-top: 20vh; }

  /* ZONE 3 -- turn: same band trick, taller (scatter band + phone + Monday
     card all need to fit before the sticky's content ends). turn-stage flips
     to a column (base rule is a row, for the armed side-by-side layout) so
     DOM order alone gives scatter-band-space -> phone -> Monday card; the
     phone and its board rows are display:none/opacity:0 by default (only
     shown via .scene-armed, which never applies here) so both get switched
     on directly. .turn-monday is already width:100% in the unarmed base
     rule, nothing to override there. */
  .the-turn.mobile-static .scroll-scene-sticky { padding-top: 68vh; }
  .the-turn.mobile-static .turn-stage { flex-direction: column; align-items: center; gap: var(--space-l); }
  .the-turn.mobile-static .turn-phone { display: block; opacity: 1; }
  /* Note: the three .the-turn.mobile-static rules above are the REDUCED-MOTION mobile path
     only -- with motion allowed the turn scene ARMS at every width (allowNarrow, w46) and
     initMobileStatic never adds mobile-static to .the-turn, so they cannot fight the armed
     rules below. */

  /* ---------- ARMED turn at <=600px (w46, Max: the notifications must fly into the phone
     on portrait too). The engine is width-agnostic (percent lefts + px --pose offsets,
     live-measured targets); only the LAYOUT needs overriding: the desktop armed stage is a
     phone-beside-card ROW (~487px minimum) that cannot fit under 600px. ---------- */
  [data-scroll-scene="wow-turn"].scene-armed .turn-stage {
    flex-direction: column;
    gap: var(--space-m);
    padding-inline: var(--space-s);
    padding-top: 72px; /* the desktop calc(64px + --space-m) is budgeted for a row layout */
  }
  /* The Monday card CANNOT share the 100vh pinned stage with the phone on a phone: its
     body copy alone measures ~276px at readable sizes (live, 360x640), so any budget that
     fits both squeezes the type below the a11y floor. Mobile shape instead: the pin stars
     the PHONE alone, and the card sits BELOW the sticky box, scrolling in as the pin
     releases -- still animated by the same JS (its opacity is 0 until p=0.55, so it never
     flashes early). */
  /* w66: --phone-w must be re-derived here too -- it is a literal computed value, not a live
     binding to `height`, so overriding height alone would leave the desktop --phone-w in
     place and the chrome/aspect would go stale (too wide for the shorter mobile height). */
  [data-scroll-scene="wow-turn"].scene-armed .turn-phone {
    --phone-w: calc(clamp(280px, 56vh, 520px) * 0.4621);
    height: clamp(280px, 56vh, 520px);
  }
  [data-scroll-scene="wow-turn"].scene-armed .turn-monday {
    position: absolute;
    top: calc(100% + var(--space-l));
    left: 0;
    right: 0;
  }
  [data-scroll-scene="wow-turn"].scene-armed .turn-monday .monday-card { max-width: min(26em, 88vw); }
  /* the desktop screen font cap (15px) pushes row 6's pill past the bezel at a 56vh
     mobile phone -- one notch down fits all six rows (eyeballed w50: pill half-clipped) */
  [data-scroll-scene="wow-turn"].scene-armed .turn-phone-screen { font-size: clamp(11px, 2.2vh, 14px); }
  /* the card lives past the sticky's box, so the sticky must not clip vertically; x stays
     clipped (mid-flight pieces bleed the edges). clip, not hidden: hidden+visible computes
     to hidden+auto = a scroll container (the overflow-kills-sticky family of traps). */
  [data-scroll-scene="wow-turn"].scene-armed .scroll-scene-sticky { overflow-x: clip; overflow-y: visible; }
  /* room for the out-of-flow card between the scene and the next section -- sized for the
     tallest case, the 295px-wide card at 360w. MARGIN, never padding: padding inflates the
     section rect that the scrub's p is computed over, so the sticky released at p~0.74 and
     the converge end (0.85) played after the pin let go (live-caught: phone top -182px at
     p 0.92). Margin is outside the rect; the p mapping and release stay at p=1.
     240vh, not the desktop 280vh: no drag/hover on touch, so the standing mood-board phase
     is dead time -- a shorter pin plus the compressed appear schedule (main.js) closes the
     empty-entry stretch Max hit on-device (28 Aug). */
  [data-scroll-scene="wow-turn"].scene-armed { height: 240vh; margin-bottom: 540px; }

  /* the Monday card carries .reveal, and the generic observer churns js-hidden on it because
     on mobile it hovers exactly at the fold for the whole pin (never 20% visible). The scene's
     JS overrides the hidden state's opacity and transform INLINE every frame -- but never
     writes filter, so .reveal.js-hidden's blur(10px) leaked through and the card rendered as
     a ghost smudge on-device (Max, 28 Aug: "Monday 8:40am disappears mid scroll"). While the
     armed scene owns the card, neutralize the whole hidden state; desktop is untouched (this
     lives in the <=600px block) and reduced-motion never arms. */
  [data-scroll-scene="wow-turn"].scene-armed .monday-card.js-hidden { opacity: 1; filter: none; transform: none; }
  /* mid-flight 15em chips span 62% of a 390px screen */
  [data-scroll-scene="wow-turn"].scene-armed .turn-artifact { max-width: 11em; }
  /* phones converge the 30 hand-authored pieces only -- 130 will-change-promoted layers is
     desktop budget, not phone budget. Same specificity as the armed display:block rule,
     later in source, so this wins; the converge loop still writes transforms to the hidden
     clones, which is style-write-only on display:none elements (no layout, no paint). */
  [data-scroll-scene="wow-turn"].scene-armed .turn-artifact-gen { display: none; }
}

/* ============================================================================
   w68 -- THE INTERACTIVE PHONE'S BOARD, rebuilt to match the real product.
   Max, 30-31 Aug. Three complaints drove this, in order:
     1. "too much space between the leak and the different view selection"
     2. "it kinda looks too similar now between problem lines"
     3. "why does it not look like the dashboard kingscliff you just screenshotted?"
   Ground truth for every value below: site\references\hub-ui-2026-08-25\board-mobile-390.png
   plus the hub's own source (constants.js, StatusBadge.jsx). Nothing here was invented.
   The phone's 390/844 aspect ratio is UNTOUCHED -- Max: "why does the phone change shape?
   don't do that." Every rule below fits inside the existing frame.
   ============================================================================ */

/* --- 1. the top gap. Was 57.4px: .wys-view-switch's margin-bottom (--space-s, 18.24px)
       PLUS .wys-context-rows' margin-top (--space-m, 39.2px) -- two PAGE-level tokens spent
       inside a ~331px phone. Same bug class as the fix at line 904. Now ONE owner, in em, so
       it scales with the phone's own type. Apple: the control switches the list, so they are
       one group, and proximity is how you say that. 57.4px -> 10.1px. --- */
.wys-view-switch { margin-bottom: 0.75em; }
.wys-context-rows { margin-top: 0; }

/* --- 2. white cards on warm paper, as the real board has. Was white-on-white with hairline
       dividers, which is what made four rows read as one undifferentiated block. --- */
.wys-phone-screen { background: var(--paper); }
.wys-phone-screen > .wys-context-rows { flex: 1 1 auto; display: flex; flex-direction: column; gap: 0.95em; } /* w73: 0.5em -> 0.8em -> 0.95em; the gap BETWEEN cards carries more of the crowding than padding inside them */
.wys-row {
  display: flex; flex-direction: column; flex: 1 1 auto;
  background: var(--paper-white, #fff);
  border-radius: 0.9em;
  box-shadow: 0 1px 2px rgba(31, 42, 34, 0.05), 0 4px 12px rgba(31, 42, 34, 0.06);
  overflow: hidden;
  border-left: 5px solid transparent; /* priority edge, coloured below */
}
.wys-row .wys-row-panel { flex: 0 0 auto; }
/* separation is the gap between cards now, so the divider goes */
.wys-context-rows .board-row {
  flex: 1 1 auto; align-items: flex-start; gap: 0.6em;
  /* w73: 0.7em -> 1.05em. Max, three times: the reports read squished. This IS the live
     declaration for the card layout; the one near .wys-row's first definition is dead. */
  padding: 1.05em 0.75em; border-bottom: none; opacity: 1;
}
.wys-row:last-child .board-row { border-bottom: none; }

/* --- 3. the two-line row: title, then muted location + time + priority. --- */
.board-main { display: flex; flex-direction: column; gap: 0.2em; flex: 1 1 auto; min-width: 0; }
.wys-context-rows .board-title {
  font-size: 1.02em; line-height: 1.22; white-space: normal;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* reuses .report-submitted's grey -- no new colour enters the system (a11y.md: one hue at
   varying brightness, never a new hue) */
.board-sub {
  font-size: 0.76em; line-height: 1.2; color: #8a8a8a;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* --- 3b. collapsed "me too" pill (4 Sep, port from BoardRow.jsx). Read-only outline pill,
       left, under the meta line: hairline border, no fill, meta-grey text (reuses .board-sub's
       #8a8a8a -- no new colour). Hidden while the row is open (data-count="0" and
       aria-expanded="true" both hide it) so the count never shows twice -- the open panel's
       .board-metoo-btn below carries it instead. --- */
.board-metoo {
  display: inline-flex; align-items: center;
  align-self: flex-start; margin-top: 0.3em; margin-left: 0.35rem;
  min-height: 28px; padding: 0.3em 0.7em;
  border: 1px solid #d1cec4; border-radius: 999px; background: transparent; color: #8a8a8a;
  font-family: var(--font-product-meta); font-weight: 600; font-size: 0.7em;
  line-height: 1; white-space: nowrap;
}
.board-metoo[data-count="0"] { display: none; }
.board-row[aria-expanded="true"] .board-metoo { display: none; }
.wys-context-rows .board-pill { flex: 0 0 auto; margin-top: 0.1em; }
.wys-row-panel .report-card.board--card {
  margin-top: 0; box-shadow: none; background: transparent; padding: 0 0.75em 0.7em;
}

/* --- 4. the CATEGORY chip: in the DOM always, revealed only on the expanded row. Chips on
       every card overflowed the phone by 52px (measured), which is why this is gated.
       Reuses the existing --chip-tan-* tokens (line 55), the same tan the real board uses. --- */
.board-chips { display: none; }
.board-row[aria-expanded="true"] .board-chips { display: flex; flex-wrap: wrap; gap: 0.3em; margin-top: 0.28em; }
.board-chip {
  display: inline-flex; align-items: center;
  background: var(--chip-tan-bg); color: var(--chip-tan-fg);
  font-family: var(--font-product-meta); font-weight: 600; font-size: 0.68em;
  line-height: 1; padding: 0.34em 0.6em; border-radius: 999px; white-space: nowrap;
}

/* --- 4b. the open panel's foot row (4 Sep, port from BoardRow.jsx): the submitted date sits
       left, the tappable "me too" button sits bottom-RIGHT (reach rule -- right-handed reach,
       CLAUDE.md). Reuses .wys-filter-chip's outline shape; filled eucalyptus when tapped, same
       fill treatment as an active filter chip. --- */
.board-foot { display: flex; align-items: center; justify-content: space-between; gap: 0.5em; margin-top: 0.4em; }
.board-metoo-btn {
  display: inline-flex; align-items: center; gap: 0.3em; flex: 0 0 auto;
  min-height: 32px; padding: 0.3em 0.75em;
  border: 1px solid var(--hairline); border-radius: 999px;
  background: var(--paper-white, #fff); color: #6b6b6b;
  font-family: var(--font-product-meta); font-weight: 600; font-size: 0.72em;
  line-height: 1; cursor: pointer;
  transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
}
.board-metoo-btn.is-tapped { background: var(--eucalyptus); border-color: var(--eucalyptus); color: var(--paper-white, #fff); }
.board-metoo-btn:active { transform: translateY(1px) scale(0.98); } /* w5's press affordance, reused */
.board-metoo-thumb { font-size: 1.05em; line-height: 1; }

/* --- 4c. "What was done" resolution block (4 Sep, port from BoardRow.jsx). Only rendered
       when a report carries an outcome -- today, the "done" report only. Reuses .chip-tan's
       warm neutral so it reads as a quiet closing note, not another alert. --- */
.board-resolution {
  margin-top: 0.5em; padding: 0.6em 0.7em;
  background: var(--chip-tan-bg); border-radius: calc(var(--radius-card) - 6px);
}
.board-resolution-title {
  font-family: var(--font-product-meta); font-weight: 700; font-size: 0.62em;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--chip-tan-fg); margin: 0;
}
.board-resolution-outcome { font-size: 0.78em; font-weight: 600; color: var(--ink); margin: 0.25em 0 0; }
.board-resolution-note { font-size: 0.76em; color: #5c574a; margin: 0.2em 0 0; }

/* --- 5. PRIORITY: a coloured left edge, always visible, plus the word in .board-sub.
       THREE levels, not the hub's four. "Urgent" was dropped on evidence:
         data/issues.json -- Medium 3, empty 3, Low 1, High 1, URGENT 0
         classify.js:79   -- "Urgent is never assigned by machine, that judgement stays human"
       and Max confirmed 31 Aug that no real tenant has used it. Four levels also forced
       orange and red adjacent, which WAS the legibility problem; removing the level dissolved
       it rather than managing it. High is the ceiling now.
       Tones are the SITE's palette, not web primaries -- Max: "they don't really match our
       kind of colour theme colour palette." Low is a lift of --eucalyptus #2D5A42, the page's
       own accent.
       ⚠ THE HUB MUST FOLLOW: URGENCIES in constants.js and URGENCY_STYLES in StatusBadge.jsx
       still carry four levels in brown/amber/orange/red. Until that ships, this mockup shows a
       board the product does not have. Verify against the LIVE database first (both hubs were
       503 when the counts above were read from a local file, n=8). --- */
.wys-row.prio-low    { border-left-color: #3A7355; } /* eucalyptus, lifted to read at 5px */
.wys-row.prio-medium { border-left-color: #E0A800; } /* w83b YELLOW, Max: "yellow instead of amber". Previous value was C2912B warm ochre.
       Measured before switching: the OLD ochre was already 2.85:1 against a white card,
       UNDER WCAG 1.4.11's 3:1, so this change does not break a bar that was being met.
       The exemption holds either way - the WORD ("Medium") is printed beside the strip in
       .board-sub, so colour is redundant reinforcement, never the sole carrier of state.
       This yellow measures 2.15:1. ⚠ THE HUB MUST FOLLOW (URGENCY_STYLES in StatusBadge.jsx). */
.wys-row.prio-high   { border-left-color: #A8392B; } /* warm brick, not fire-engine */

/* --- w68 defect fix, caught by measurement at 1366x768 (a laptop viewport Max has hit
       before): --phone-w is height-capped, so a 768px-tall viewport gives a 638px phone
       against 716px at 1440x900. The card padding and gaps that fit the taller frame
       overflowed the shorter one by 19.7px, clipping the last card.
       Tightened ONLY where it is short, rather than making every viewport denser to serve
       the worst one. Threshold picked from the measurements: 900px fits at 0px, 768px was
       19.7px over. --- */
@media (max-height: 820px) {
  .wys-phone-screen > .wys-context-rows { gap: 0.76em; }   /* w73: 0.3em -> 0.55em -> 0.76em. Max: drop the sliver of the 6th card and let 5 fill the screen evenly. Measured: at 0.55em card6's top sat 10px inside the screen edge; +3px per gap clears it while card5 still ends ~7px clear of the bottom. */
  .wys-context-rows .board-row { padding: 0.78em 0.7em; }  /* w73: 0.5em, lifted with the base */
  .wys-row-panel .report-card.board--card { padding: 0 0.7em 0.5em; }
}

/* ============================================================================
   w69 -- the view control leaves the phone, and the screen gets its real title.
   Max, 31 Aug: "the manager view tenant view is not an app feature". Correct -- the two
   VIEWS are real (the hub gives tenants a read-only board), but no real user toggles
   between them; they are one or the other. It is a preview device for a visitor, so it
   must not wear app chrome.
   ============================================================================ */

/* --- the screen's title, matching both Codex renders and the live hub. Serif, quiet,
       left-aligned, sitting where the switch used to. --- */
.wys-board-title {
  /* w69b: was var(--font-display-alt, ...) -- a token that DOES NOT EXIST, so it silently fell
     back to --font-body and rendered in Archivo while every card title beside it was Fraunces.
     That mismatch, not the size, is why Max read it as "so small".
     --font-product-title is the PRODUCT's heading font, the same one .board-title uses (line
     681) and the same one the live hub renders. Deliberately NOT --font-display (Abril
     Fatface, the CentreOS wordmark face): this phone depicts the product, and the marketing
     display font inside the product's own chrome would make the mockup lie about the product.
     Abril is also a heavy display cut that turns to mud at UI size. */
  font-family: var(--font-product-title);
  font-weight: 600; font-size: 1.5em; line-height: 1.15;
  margin: 0 0 0.5em; padding-inline: 0.1em; color: var(--ink);
  /* LEFT, not centred. The first render came back centred and every measurement still passed
     -- caught only by looking at it. Both Codex renders and the live hub left-align this
     title; a centred one reads as a splash screen, not a board.
     text-align alone was NOT enough: .wys-phone-screen is a flex column with
     align-items:center, so this <p> shrank to its 98px text width and got centred as a flex
     ITEM. align-self:stretch is what actually fixes it -- the alignment bug was in the box,
     not the text. */
  align-self: stretch;
  text-align: left;
}

/* --- the switch, restyled from segmented-pill app control to a page control. The pill,
       border and filled-ink active state all read as UI; out here they would read as a
       misplaced app feature, which is the exact thing being fixed. Now: page type, a hairline
       under the active one, nothing that looks tappable-as-a-setting.
       NOTE this OVERRIDES the w68 rule above (margin-bottom: 0.75em) -- that value existed to
       tighten the gap INSIDE the phone, and the element is no longer in there. --- */
.wys-view-switch {
  width: auto; gap: var(--space-s); margin: var(--space-s) 0 0;
  padding: 0; border: 0; border-radius: 0; justify-content: center;
}
.wys-view-btn {
  flex: 0 0 auto; min-height: 44px; padding-inline: 0.2em;
  background: none; border: 0; border-bottom: 2px solid transparent; border-radius: 0;
  font-family: var(--font-body); font-weight: 600; font-size: 0.9rem;
  letter-spacing: 0; text-transform: none; color: #8a8a8a; cursor: pointer;
}
.wys-view-btn[aria-pressed="true"] {
  background: none; color: var(--ink); border-bottom-color: var(--eucalyptus);
}
@media (hover: hover) and (pointer: fine) {
  .wys-view-btn:hover { color: var(--ink); }
}
.wys-view-btn:focus-visible { outline: 2px solid var(--eucalyptus); outline-offset: 3px; }
/* the hint follows the switch now, so it needs less of its own top space */
.wys-phone-hint { margin-top: 0.35em; }

/* ============================================================================
   w70 -- ported FROM the Kingscliff dashboard, not designed here.
   Max, 31 Aug: "i don't know why we are re-engineering this ... reverse engineer from
   dashboard ... like from our kingscliff one". Sources, read directly:
     tenant-hub\src\pages\Board.jsx            filter chips + their shape (lines 57-78)
     tenant-hub\src\components\IssueCard.jsx   TYPE_LABELS / TYPE_STYLES (lines 3-13)
   ============================================================================ */

/* --- category filter chips. Board.jsx builds ["All", ...categories present], filters on
       exact match, and does NOT sort (it sorts newest-first, always). Chip shape from
       Board.jsx:67-71: full-round, bordered, active = filled primary. --- */
.wys-filters {
  display: flex; flex-wrap: nowrap; gap: 0.3em;
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
  align-self: stretch;
  /* w74 BUG FIX. This row is a flex item of .wys-phone-screen and was shrinkable, so when the
     leak report (the only one with a photo) opened under the All filter, the list demanded
     176px it could not have and flexbox took 5.9px out of THIS row -- exactly the
     padding-block below. The chips lost their breathing room and their round caps in the same
     moment. Measured: chip bar 40.6px with a short row open vs 34.7px with the leak open; the
     title was untouched. It only reproduced under All because any category filter hides the
     leak and the overflow pressure vanishes with it. The list is flex:1 1 auto with
     overflow-y:auto, so the LIST is what should absorb pressure -- never the header. */
  flex-shrink: 0;
  /* w73 BUG FIX (Max spotted it on the share card): every chip's bottom border and radius were
     being shaved flat. Cause is not the chips, it is this container -- `overflow-x: auto` makes
     the used value of overflow-y compute to `auto` too (CSS Overflow 3: if one axis is not
     `visible`, the other resolves to `auto`), so the row became a scroll container that clips
     vertically at exactly content height. Setting `overflow-y: visible` cannot work for the same
     reason. Padding is the fix: it gives the clip box room to contain the full chip. */
  padding-block: 4px 10px;
  /* w73: the padding above exists ONLY to stop the scroll container shaving the chips'
     round caps -- it must not also cost 14px of vertical space. These negative margins
     cancel it, so the gap the eye sees is the same as before the fix (Max: the space
     above and below the pills was too much). Net visual gap below the chips ~6px. */
  /* w73: chips CENTRED in the gap between the board title and the first card, not pinned to
     the card (measured before: 18px above, 4px below). Asymmetric margins do it without
     changing the total space, and the padding above still keeps the round caps intact. */
  /* w75g, Max: "move the filter pills on both like just the tiniest bit down". Done on the
     MARGINS, not the padding -- the padding above is load-bearing (it is the only thing
     stopping the scroll container shaving the round caps) and the w73 note above already
     established that asymmetric margins move this row without changing the total space.
     -10/+1 -> -8/-1: the pair still sums to -9px, so the first card does not move; the row
     itself sits 2px lower. */
  margin-top: -8px; margin-bottom: -1px;
}
.wys-filters::-webkit-scrollbar { display: none; }
.wys-filter-chip {
  /* w73 BUG (Max spotted it): the label sat high in the pill. min-height makes the box taller
     than its line box, and in normal block flow the text stays at the top of that box -- the
     extra height all fell below the text. inline-flex + centring puts the label on the pill's
     optical centre instead. */
  /* Padding stays SYMMETRIC and the inline-flex centring below does the work. A w73 pass
     added an asymmetric optical nudge (0.4em top / 0.16em bottom) because a pixel scan
     showed more space below each label than above -- Max ruled the symmetric version sat
     perfectly and the nudge was over-correction. DO NOT re-add it off a measurement; his
     eye is the acceptance test for this. */
  /* w88: sized from the PRODUCT, not by eye. Real board at 390px: chip height 38, font 14,
     padding 8/16, and the sort chip is the SAME 38 height. Demo screen is 280, so scale
     280/390 = 0.718 -> height 28, font ~10px, horizontal padding ~11.5px. Getting this right is
     also what makes the strip scroll: at the old 8.85px font the three chips totalled 216px
     inside a 229px strip and never overflowed, while the real board's chips DO overflow and
     scroll. Max, 4 Sep: "the filter pills should be scrollable". */
  flex: 0 0 auto; min-height: 28px; padding: 0 1.14em;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--hairline); border-radius: 999px;
  background: var(--paper-white, #fff); color: #6b6b6b;
  font-family: var(--font-product-meta); font-weight: 600; font-size: 0.82em;
  line-height: 1; white-space: nowrap; cursor: pointer;
  transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
}
.wys-filter-chip[aria-pressed="true"] {
  background: var(--eucalyptus); border-color: var(--eucalyptus); color: var(--paper-white, #fff);
}
@media (hover: hover) and (pointer: fine) {
  .wys-filter-chip:not([aria-pressed="true"]):hover { border-color: var(--eucalyptus); color: var(--ink); }
}
/* the category chips' own wrapper (4 Sep, main.js buildFiltersAndSort). It scopes applyFilter's
   aria-pressed loop away from the sort chip.
   w88: this was display:contents, which put the sort chip and the category chips in ONE flex row.
   Measured on the 280px phone screen: the row ran 68px past the screen's right edge. .wys-filters
   is overflow-x:auto so it technically scrolled rather than clipped, but a marketing mockup is
   read, never dragged, so it simply looked cut off. Sort now sits on its own line above the
   filters, and dropping the "All" chip (which the product does not have) gave the categories
   enough width to fit outright. Keeping overflow-x here so a centre with more categories still
   degrades to a scroll instead of overflowing. */
/* w88, SECOND correction (Max: "it should be next to the filter pills. and the filter pills
   should be scrollable"). Structure copied from the product, Board.jsx:147-151 + index.css
   :446-461. The hub's own comment says why it is built this way: "sort chip sits OUTSIDE the
   scrolling chip strip so its menu is never clipped". So the OUTER bar is one nowrap flex row
   that does NOT scroll (the sort menu anchors to it and stays visible), and only the INNER
   category strip scrolls, bleeding past the right edge the way it does on the phone.
   My first pass put sort on its own line, which is not what the product does. */
.wys-filters {
  flex-wrap: nowrap; align-items: flex-start; gap: 5.75px;
  overflow: visible; margin-bottom: 0.6rem;
}
.wys-filter-chips {
  display: flex; flex-wrap: nowrap; gap: 5.75px; min-width: 0;
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
  /* .board-a-filters' bleed, scaled: the strip runs under the screen's right edge so a
     half-visible chip is the scroll affordance, exactly as on the real board. */
  margin-right: -11.5px; padding-right: 11.5px;
}
.wys-filter-chips::-webkit-scrollbar { display: none; }
/* --- sort control (w88, ported from the PRODUCT: tenant-hub SortMenu.jsx + index.css:886-990).
       Max, 4 Sep: "the sort by on our kingscliff does not look like that" -- the first pass was a
       text chip that cycled, which the product does not have. The real control is an ICON-ONLY
       round button that opens an anchored listbox. Geometry scaled to this phone: the hub renders
       at ~350px of usable width, the demo screen is 280px, so 38px -> 30px and 220px -> 190px.
       Everything else (radius, shadow, the 0fr->1fr grow, the 220ms curve, the 44px option rows,
       the tick on the selected row) is the product's own. --- */
.wys-sort { --sort-ease: cubic-bezier(0.32, 0.72, 0, 1); position: relative; flex: none; }
.wys-sort-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0;  /* == .wys-filter-chip min-height, as on the real board */
  border: 1px solid #d1cec4; border-radius: 999px;
  background: #fff; color: #6b6b6b; cursor: pointer;
  transition: border-color 150ms ease, background-color 150ms ease;
}
.wys-sort-chip svg { width: 14px; height: 14px; }
.wys-sort.is-open .wys-sort-chip { border-color: var(--eucalyptus); }
.wys-sort.is-active .wys-sort-chip { background: var(--eucalyptus); border-color: var(--eucalyptus); color: #fff; }
.wys-sort-chip:focus-visible { outline: 2px solid var(--eucalyptus); outline-offset: 2px; }
/* absolute so the filter row keeps its height; visibility flips AFTER the collapse so a closed
   menu casts no shadow line (the product's own comment, and the reason for the 0s 220ms delay) */
.wys-sort-menu {
  position: absolute; left: 0; top: calc(100% + 6px); z-index: 20;
  min-width: 190px; display: grid; grid-template-rows: 0fr; visibility: hidden;
  background: #fff; border-radius: 14px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.12), 0 0 0 1px #e6e2d9;
  transition: grid-template-rows 220ms var(--sort-ease), visibility 0s 220ms;
}
.wys-sort.is-open .wys-sort-menu { grid-template-rows: 1fr; visibility: visible; transition-delay: 0s; }
.wys-sort-options {
  display: flex; flex-direction: column; overflow: hidden; min-height: 0;
  border-radius: 14px; opacity: 0; transition: opacity 160ms ease;
}
.wys-sort.is-open .wys-sort-options { opacity: 1; transition-delay: 80ms; }
.wys-sort-option {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  min-height: 36px; padding: 0 0.7rem; border: 0; background: transparent;
  text-align: left; font-family: var(--font-body); font-size: 0.72rem; font-weight: 600;
  color: #6b6b6b; cursor: pointer;
}
.wys-sort-option + .wys-sort-option { border-top: 1px solid #e6e2d9; }
.wys-sort-option.is-selected { color: var(--eucalyptus); }
.wys-sort-tick { display: none; }
.wys-sort-option.is-selected .wys-sort-tick { display: inline-flex; }
@media (prefers-reduced-motion: reduce) {
  .wys-sort-menu, .wys-sort-options { transition: none; }
}

/* --- type badges. The PRODUCT ships promotion=amber, idea=teal (IssueCard.jsx:10-12), but
       Max changed both 31 Aug: "promotion should be light blue idea maybe white".
       Light blue was rejected in review -- it collides with the "Seen by management" status
       pill (--chip-seen-bg #E4E9EE, a blue-grey) which sits on the SAME card. Teal was moved
       onto promotion instead, and Idea took the outlined treatment Max asked for.
       ⚠ Plain white was also rejected as-is: a white badge on a white card is invisible.
       "White with a hairline border" is what he approved, and it reads as the lightest weight
       of the three, which suits an idea.
       ⚠ THE HUB STILL SHIPS AMBER/TEAL. Until TYPE_STYLES is updated to match, this mockup
       and the product disagree. Note also a PRE-EXISTING defect found while reading it: the
       hub gives Promotion the same amber as the "In progress" STATUS -- two meanings, one
       colour. --- */
.board-chip-type-promotion { background: #DCEDE6; color: #1F5D4C; }        /* teal */
.board-chip-type-idea {
  background: var(--paper-white, #fff); color: #5c574a;
  box-shadow: inset 0 0 0 1px var(--hairline);                             /* hairline, so it is visible on a white card */
}

/* the filter bar takes ~30px, and the phone was at exactly 0px spare. Trimming it from the
   open card's photo, which is the largest single element and the one nobody measures. */
.wys-row-panel .report-photo { max-height: 148px; }
/* 4 Sep: main.js buildRow() no longer emits .board-type -- the type badge moved inside
   .board-chips so it only shows once the row is open, same as category (hub's 4 Sep round).
   This rule stays because index.html's static "Monday phone" snapshot (~line 492, an
   aria-hidden extract of an OLDER render, out of this port's file scope) still uses the class;
   deleting the rule would silently unstyle that markup instead of just leaving it stale. That
   snapshot still shows the pre-4-Sep chip/metoo/resolution layout and needs its own re-extract
   pass -- not done here, main.js/style.css only. */
.board-type { display: flex; margin-top: 0.28em; }
/* w70 defect: [hidden] is a UA rule (display:none) with specificity 0,0,1 -- .wys-row's own
   display:flex (0,1,0) beat it, so filtered-out rows kept rendering. The filter was setting
   state correctly and NOTHING was disappearing. Any class that sets display must restate this. */
.wys-row[hidden] { display: none; }

/* w70 defect: with the filter down to 1-2 cards, flex:1 grew each row to fill the phone --
   a collapsed card became a huge empty box and the open card showed a void where the photo
   would be. Growth is right for a FULL list (it is what closes the bottom gap) and wrong for
   a short one, so it gets a ceiling rather than being removed. 5.5em ~= 70px at the phone's
   own type scale, which is the height a two-line row wants anyway. */
.wys-context-rows .board-row { max-height: 5.5em; }
/* the open row's panel is height-driven by JS from its content, so the ROW must not stretch
   past what its own content needs either */
.wys-row { justify-content: flex-start; }
.wys-phone-screen > .wys-context-rows { justify-content: flex-start; }

/* w70, second pass. Capping the HEADER was not enough: .wys-row itself still grew, so a
   filtered-down card became a 228px box with a 70px header and 150px of nothing.
   Decision: COLLAPSED cards never stretch. A two-item board SHOULD have space beneath it --
   that is what a short list looks like. Only the open card, whose height is its real content,
   takes the room it needs. The cost is a bottom gap in filtered states, which is honest, and
   the full "All" view still fills because four cards plus an open photo panel genuinely do. */
.wys-row:not(:has(.board-row[aria-expanded="true"])) { flex: 0 0 auto; }
.wys-context-rows .board-row { max-height: none; }   /* undo the first attempt */

/* w70, third pass and the right answer: NO card stretches, open or collapsed.
   The previous passes kept trying to make content fill the frame, and every one of them broke
   somewhere -- collapsed cards ballooned into empty boxes, then the OPEN card did the same
   whenever its report had no photo (the trolley-bay light), leaving ~350px of nothing between
   the chips and the status button.
   Cards are their content's height. When the list is short the space falls to the BOTTOM,
   which is what a short board actually looks like. The full view still reads full because
   four cards plus an open photo panel genuinely fill it. */
.wys-row { flex: 0 0 auto !important; }

/* ============================================================================
   w71 -- Max, 1 Sep: "make it scroll ... are you sure the size of Centre Board is the same
   as 04? i feel like it should be bigger. and it looks a bit crammed next to the dynamic
   island."
   ============================================================================ */

/* --- 1. THE LIST SCROLLS. This is what a real board does, and it dissolves the fill problem
       rather than managing it. Three passes were spent trying to make cards stretch to fill a
       fixed frame; every one broke somewhere (collapsed cards ballooned, then the open card
       did whenever its report had no photo). Content is its natural height and overflow
       scrolls. min-height:0 is the part that actually makes a flex child scrollable -- without
       it the item refuses to shrink below its content and the overflow escapes instead.
       No overscroll-behavior: when the list reaches its end the PAGE should keep scrolling,
       so a visitor is never trapped inside a mockup. With four cards it does not overflow at
       all, so no scrollbar appears in the default view. --- */
.wys-phone-screen > .wys-context-rows {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  scrollbar-width: none; -ms-overflow-style: none;
}
.wys-phone-screen > .wys-context-rows::-webkit-scrollbar { width: 0; height: 0; }

/* --- 2. more clearance under the Dynamic Island. Was 3.3em (~44.6px) with the island's
       bottom landing 36-42px in, leaving 3-8px -- which is the "crammed" Max saw. --- */
.wys-phone-screen { padding-top: 3.6em; } /* w73: 4.15em -> 3.6em. Max: "make sure there's not too much space between the centre board and the dynamic island". w71 raised it from 3.3em (which left only 3-8px and read crammed); 3.6em keeps real clearance without the gap reading as dead space. */

/* --- 3. the title, bigger. 1.5em read as a label rather than the proud heading step 04
       carries. 1.85em is ~25px at the phone's own type scale. --- */
.wys-board-title { font-size: 1.85em; margin-bottom: 0.33em; flex-shrink: 0; } /* w74: flex-shrink guard, same cause as .wys-filters. It measured unaffected only because the chip row yielded first. */ /* w73: briefly 2.2em, REVERTED at Max's word back to the w71 size. margin 0.45em -> 0.33em so the chips stay centred in the gap below it (they are not, at 0.45em). */

/* --- w71b: the promo's edge. Max, 1 Sep: "i still dont like how there is no edge for the
       promo. can we just have a black like to show a white edge for the promo?"
       A promotion genuinely has no priority (the hub only sets urgency on the issue branch),
       so it must not borrow a priority colour -- that would show a state the product cannot
       produce. But a missing edge read as broken rather than as absent.
       So: a WHITE edge, made visible by a 1px ink line. Same logic as the Idea badge, which
       is also white-with-a-hairline: white on white is invisible, white outlined is the
       lightest weight in the set.
       The inset shadow does it in one value -- offset-x 5px with -4px spread paints a 1px
       vertical line just inside the 5px white border, so the card reads white edge, thin dark
       rule. The two outer shadows are restated because box-shadow does not merge. --- */
.wys-row:not([class*="prio-"]) {
  border-left-color: var(--paper-white, #fff);
  box-shadow: inset 5px 0 0 -4px var(--ink),
              0 1px 2px rgba(31, 42, 34, 0.05),
              0 4px 12px rgba(31, 42, 34, 0.06);
}

/* w71b fix: the inset-shadow version did not render. An inset box-shadow is painted inside the
   PADDING box, which begins after the 5px border, so the rule landed past the edge instead of
   on it. Painting the edge with a background gradient works because background-clip defaults
   to border-box -- the background extends UNDER a transparent border, which is exactly the
   5px strip we want to fill: 1px ink, then 4px white. */
.wys-row:not([class*="prio-"]) {
  border-left-color: transparent;
  box-shadow: 0 1px 2px rgba(31, 42, 34, 0.05), 0 4px 12px rgba(31, 42, 34, 0.06);
  background-image: linear-gradient(to right, var(--ink) 0 1px, var(--paper-white, #fff) 1px 5px);
  background-repeat: no-repeat;
  background-size: 5px 100%;
  background-position: left top;
}

/* w71c fix: Max, 1 Sep -- "it should match like the shape of the edges next to them".
   The previous gradient put the ink at 0-1px and white at 1-5px, so it read as a thin dark
   hairline, not as a bar. Flipped: white fills the full 5px like every coloured edge, and the
   1px ink rule sits on the INNER side, separating the bar from the card body. Same shape and
   same width as the priority edges, just unfilled -- which is the honest reading, because a
   promotion has no priority to colour. The bar still follows the card's corner radius because
   backgrounds are clipped to the border box. */
.wys-row:not([class*="prio-"]) {
  background-image: linear-gradient(to right, var(--paper-white, #fff) 0 4px, var(--ink) 4px 5px);
}

/* w71d fix: one rule on the inner side was not enough -- a white bar on a white card has no
   outer boundary either, so it still read as a hairline rather than a bar. Ruled on BOTH sides
   now: 1px ink, 3px white, 1px ink. That is a white bar of the same 5px width and shape as the
   coloured priority edges, just outlined instead of filled -- which is the honest reading for a
   promotion, since it has no priority to colour. */
.wys-row:not([class*="prio-"]) {
  background-image: linear-gradient(to right,
    var(--ink) 0 1px,
    var(--paper-white, #fff) 1px 4px,
    var(--ink) 4px 5px);
}


/* w71f -- Max, 1 Sep: "they dont follow the shape of the colour edges that we have originally".
   Correct, and it was the mechanism, not the colour. The priority edges are a real border-left,
   so they taper into the card's 0.9em corner radius. My promo bar was a painted background
   gradient, which fills that corner area differently -- so it could never match the shape no
   matter what colour it was.
   Fix: use border-left for the promo too. Identical property, identical geometry, guaranteed.
   That rules out white (invisible on a white card), so the fill is a warm neutral instead:
   clearly present, clearly NOT one of the three priority colours, which is the honest reading
   for a card that has no priority. */
.wys-row:not([class*="prio-"]) {
  background-image: none;
  border-left-color: #C9C6BF;
}

/* w71i -- THE PROMO EDGE, settled. Max picked warm grey from a four-way render comparison
   (stone #5c574a / warm grey #B5AFA3 / pearl #E4DFD5 / off-white #F2EEE6, judged with the
   green and amber cards either side of it as a constant).
   Two things were wrong before this and both are recorded because both were non-obvious:
     1. MECHANISM. The priority edges are a real border-left, so they taper into the card's
        12.18px corner radius. The promo's bar was a painted background gradient, which fills
        that corner differently -- it could never match the shape at ANY colour. Using
        border-left made the geometry identical by construction.
     2. OPTICAL WEIGHT. Even at identical geometry, a light edge reads thinner, because its
        antialiased fringe blends toward the paper ground. Shape and perceived shape are not
        the same thing.
   Verified in pixels, not by eye: edge width 4px at quarter/half/three-quarter height on all
   four cards, and the promo's top-corner profile deviates from the red card's by at most 1px,
   which is the same deviation the AMBER card has from it (sub-pixel antialiasing off
   fractional y-positions).
   #B5AFA3 over the darker stone deliberately: stone was the heaviest edge on the board, so a
   promotion out-pulled a High-priority leak and inverted the hierarchy. Priority must out-rank
   type. This is present, and subordinate. */
.wys-row:not([class*="prio-"]) { border-left-color: #B5AFA3; }

/* ============================================================================
   w74 -- THE MONDAY 8:40 PHONE NOW SHOWS THE SAME BOARD AS THE INTERACTIVE ONE.
   Max: "we need to update the monday 840 phone to reflect the dashboard phone we have
   just spent so long changing."

   It was six flat title+pill rows on a white screen with hairline dividers -- a different
   product from the one the page's own interactive phone depicts. The markup now reuses the
   REAL components (.wys-row for the card + priority edge, .board-main/.board-title/
   .board-sub for the two-line row), so this phone cannot drift from the other one: they
   share the same classes.

   SPECIFICITY NOTE, and it matters: .turn-phone .turn-phone-screen .board-row (0,3,0) at
   ~line 602 already exists and beats a plain .turn-phone-screen .board-row (0,2,0). Three
   w73 edits were lost to exactly this class of cascade trap, so every override below is
   written at (0,3,0) or higher AND sits at the end of the file.
   ============================================================================ */

.turn-phone .turn-phone-screen { background: var(--paper); }

/* the card itself: same shape language as .wys-row, scaled down for a phone that is
   roughly half the interactive one's rendered width */
.turn-phone .turn-phone-screen .wys-row {
  display: block;
  background: var(--paper-white, #fff);
  border-radius: 0.7em;
  box-shadow: 0 1px 2px rgba(31, 42, 34, 0.05), 0 3px 9px rgba(31, 42, 34, 0.06);
  /* width+style only. Declaring the SHORTHAND here reset border-left-color to transparent and
     silently killed the priority edges, because this selector (0,3,0) outranks .wys-row.prio-*
     (0,2,0). The three colours are therefore re-declared at THIS specificity just below. */
  border-left-width: 4px; border-left-style: solid; border-left-color: transparent;
  overflow: hidden;
  margin-bottom: 0.44em;
  flex: 0 0 auto;
}
.turn-phone .turn-phone-screen .wys-row:last-child { margin-bottom: 0; }

/* kill the flat-row chrome the old markup relied on: no dividers between cards now that
   separation is the gap, same decision the interactive phone made at w66 */
.turn-phone .turn-phone-screen .board-row {
  display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between;
  gap: 0.5em; padding: 0.5em 0.6em; border-bottom: none; margin: 0;
}
.turn-phone .turn-phone-screen .board-title {
  font-size: 1em; line-height: 1.25; white-space: normal;
}
.turn-phone .turn-phone-screen .board-sub { font-size: 0.76em; margin-top: 0.1em; }
.turn-phone .turn-phone-screen .board-pill { flex: 0 0 auto; font-size: 0.66em; padding: 0.28em 0.55em; }

/* priority edge colours, re-declared at this selector's specificity so the base rule above
   cannot outrank them. Same three values as the interactive phone (style.css ~1453). */
.turn-phone .turn-phone-screen .wys-row.prio-low    { border-left-color: #3A7355; }
.turn-phone .turn-phone-screen .wys-row.prio-medium { border-left-color: #E0A800; }
.turn-phone .turn-phone-screen .wys-row.prio-high   { border-left-color: #A8392B; }

/* the meta line WRAPS here rather than ellipsing. The interactive phone truncates it, which is
   honest at 331px because that is what the real screen does -- but this phone is roughly half
   that width, where truncation ate the line entirely ("Th...", "Sat..."), taking the priority
   word with it. Six cards still fit with room to spare, so the height is affordable. */
.turn-phone .turn-phone-screen .board-sub {
  white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.3;
}

/* w75 -- three defects Max found by LOOKING at the Monday phone on the live w74 build.
   All three are cases where this phone silently disagreed with the interactive one beside it. */

/* w75/1 -- THE PROMO EDGE, missing on this phone only.
   w74 wrote `border-left-color: transparent` on .turn-phone .turn-phone-screen .wys-row at
   (0,3,0), then re-declared the THREE prio-* colours at that specificity so they could win.
   The fourth case -- a row with NO priority, i.e. the promotion -- was never re-declared, so
   .wys-row:not([class*="prio-"]) at (0,2,0) lost to the transparent base and the promo card
   rendered with no edge at all. Measured: rgba(0,0,0,0) here vs rgb(181,175,163) on the
   interactive phone.
   #B5AFA3 is NOT a new choice -- Max picked it at w71i from a four-way render, specifically
   because a heavier edge made a promotion out-rank a High-priority leak. Priority must
   out-rank type. Reused verbatim, not re-derived. */
.turn-phone .turn-phone-screen .wys-row:not([class*="prio-"]) { border-left-color: #B5AFA3; }

/* w75/2 -- THE TAIL FADE was melting to the wrong colour.
   The w63 fade exists so the last visible card melts into the screen instead of being amputated
   at the bezel. It fades to --paper-white (#FFF), which was right when the screen WAS white --
   but w74 changed this phone's screen to --paper (#FAF8F4). So it painted a white band on warm
   paper: more visible than the hard cut it was hiding. Fades to the actual ground now.
   (0,2,1) beats the base (0,1,1). */
.turn-phone .turn-phone-screen::after {
  background: linear-gradient(to bottom, transparent, var(--paper) 78%);
}

/* w75/3 -- PROPORTIONS. Max: "you can tighten card padding and make the words smaller to match
   the proportions of our interactive phone."
   ROOT CAUSE, and it is ONE number: this phone's base font-size was never scaled down when the
   phone itself got smaller. Measured on the live w74 build --
       interactive: base 12.84px on a 274.9px screen  -> ratio 0.0467
       Monday:      base 15.00px on a 198.6px screen  -> ratio 0.0755   (1.6x too big)
   Every child here is em-based, so that one wrong number inflated the title, the meta line, the
   pill and the header together, forced the titles to wrap to 3-4 lines, and THAT is what pushed
   the cards past the bezel. Fixing the base fixes all of them at once.
   Expressed off --phone-w (not vh) so it tracks the phone at every viewport instead of being a
   clamp that happens to be right at one height. 0.0436 = the interactive phone's base/screen
   ratio (0.0467) x its screen/phone ratio (0.9325). At 212.9px wide that is 9.28px, and the
   already-correct 1.85em header then lands at 17.17px against a 17.16px target. */
.turn-phone .turn-phone-screen { font-size: calc(var(--phone-w) * 0.0436); }

/* Screen padding is deliberately NOT in em any more. It has to clear the Dynamic Island, whose
   bottom sits 0.1265 x phone-width below the screen top -- a font-relative padding would have
   collided with the notch the moment the font shrank. Ratios preserved from the w63 values. */
.turn-phone .turn-phone-screen {
  padding: calc(var(--phone-w) * 0.17) calc(var(--phone-w) * 0.07) calc(var(--phone-w) * 0.056);
}

/* Compensation: these four were sized against the OLD 15px base, so they must grow in em terms
   to hold their measured ratio against the interactive phone rather than shrinking with it. */
.turn-phone .turn-phone-screen .board-row { padding: 0.78em 0.70em; }  /* ratio 0.0364 / 0.0327 */
.turn-phone .turn-phone-screen .board-pill { font-size: 0.72em; padding: 0.3em 0.6em; } /* 0.0336 */
.turn-phone .turn-phone-screen .wys-row {
  border-radius: 0.9em;      /* interactive radius/screen = 0.042 -> 8.35px here */
  margin-bottom: 0.76em;     /* the interactive list uses gap 9.76px, ratio 0.0355 -> 7.05px */
}

/* w75/4 -- the Monday phone's open card. .wys-row-panel defaults to height:0/opacity:0 because
   main.js animates it open on the interactive phone; this one is static markup with nobody to
   drive it, so it needs an explicit open state. Scoped to .turn-panel-open so the interactive
   phone's collapse/expand is untouched. */
.turn-phone .turn-phone-screen .wys-row-panel.turn-panel-open { height: auto; opacity: 1; }

/* The chips row is a flex ITEM of .wys-phone-screen on the interactive phone (which is display:flex)
   and carries align-self:stretch for that. This screen is display:block, where align-self is inert,
   so the row needs its own bottom gap instead of inheriting the flex layout's spacing. */
.turn-phone .turn-phone-screen .wys-filters { margin-bottom: 0.55em; }

/* w75/5 -- THE PX TAP TARGETS, rescaled for this phone ONLY.
   Three shared values are deliberately absolute px because the interactive phone is genuinely
   interactive: .wys-filter-chip min-height 28px, .wys-filters padding-block 4/10px, and
   .report-status-btn min-height 44px ("fixed, not em -- a real tap target", ~line 994).
   They are right there and MUST NOT change globally.
   But this phone is a static depiction: aria-hidden, every control tabindex="-1", nothing to
   tap. Absolute px meant the chips row rendered at 42px on BOTH phones -- identical height on a
   phone 100px narrower -- so it read as 0.2164 of phone width against the demo's 0.1425.
   Each value below is the demo's own px divided by the demo's phone width, re-applied to this
   one, so the two phones match by construction at any viewport. */
.turn-phone .turn-phone-screen .wys-filter-chip { min-height: calc(var(--phone-w) * 0.095); }   /* 28/294.8 */
.turn-phone .turn-phone-screen .wys-filters {
  padding-block: calc(var(--phone-w) * 0.0136) calc(var(--phone-w) * 0.0339);                    /* 4 and 10 / 294.8 */
}
.turn-phone .turn-phone-screen .report-status-btn { min-height: calc(var(--phone-w) * 0.1493); } /* 44/294.8 */

/* w75b -- MEASURED DIFF against the demo, live on w75. Max: "keep fixing the proportions until
   they match. that includes spacing". Every value below is the demo's own measured ratio to its
   phone width, re-applied here, so the two phones are identical by construction at any viewport.
   Base font stays at 0.0436 -- it is already correct and every other em value hangs off it. */

/* screen padding: sides were 0.0700 vs the demo's 0.0392 (79% too wide), which is also what was
   squeezing the chip row into overflow. Top 0.1700 -> 0.1568; still clears the Dynamic Island,
   whose bottom sits 0.1265 x phone-width below the screen top. */
.turn-phone .turn-phone-screen {
  padding: calc(var(--phone-w) * 0.1568) calc(var(--phone-w) * 0.0392) calc(var(--phone-w) * 0.0392);
}

/* the chips' gap to the first card comes from .wys-filters' OWN padding-bottom (0.0339), not
   from a margin -- the demo's margin is 0.0034, i.e. ~1px. Mine added 0.55em on top and
   double-counted the gap. */
/* w75g: BOTH margins scaled off phone width. margin-top was inherited unscaled at -10px,
   which on this narrower phone is a ratio of -0.0391 against the demo's -0.0339 -- the chips
   sat proportionally 15% higher here. Ratios are the demo's own new values / 294.8. */
.turn-phone .turn-phone-screen .wys-filters {
  margin-top: calc(var(--phone-w) * -0.02714);   /* -8px  / 294.8 */
  margin-bottom: calc(var(--phone-w) * -0.00339); /* -1px  / 294.8 */
}

/* priority edge was 0.0188 vs 0.0158 -- 19% too heavy, so every coloured edge read bolder here
   than on the demo. It is a signal, and a signal that shouts louder on the smaller phone is wrong. */
.turn-phone .turn-phone-screen .wys-row { border-left-width: calc(var(--phone-w) * 0.0158); }

/* card height was 0.1775 vs 0.1705. Not the font sizes -- the LINE HEIGHTS. w74 set 1.25/1.3
   here while the demo computes 1.22/1.20, and two lines of each compound into ~4% of card height.
   Title also lands 1.8% short of the demo's 0.0444, hence the 1.018em. */
.turn-phone .turn-phone-screen .board-title { font-size: 1.018em; line-height: 1.22; }
.turn-phone .turn-phone-screen .board-sub { line-height: 1.20; }

/* w78 -- the Manager/Tenant switch A/B (?switch=inside). Scoped so the default arm is untouched.
   Inside the phone the control is app chrome, not a page control, so it takes the phone's own
   font scale and loses the 44px tap-target minimum -- that minimum is right on a real page
   control but on a 275px-wide depiction it renders as a slab. Ratios follow the same
   phone-width derivation as the chips (see w75/5). */
.wys-view-switch--inside {
  width: 100%;
  margin: 0 0 calc(var(--phone-w) * 0.026);
  padding: calc(var(--phone-w) * 0.006);
  border-radius: 999px;
}
.wys-view-switch--inside .wys-view-btn {
  min-height: calc(var(--phone-w) * 0.095);   /* same 28/294.8 ratio the filter chips use */
  font-size: 0.72em;
  padding-inline: 0.5em;
}

/* w79 -- THE SWITCH GOES BACK TO THE BLACK PILL, IN ITS CURRENT PLACE.
   Max, 2 Sep: "like a black switch that we had originally that was in the phone thing. keep the
   switch in the next place."
   This deliberately reverses the w69 restyle (~line 1523), which replaced the pill with page type
   plus a hairline underline, on the reasoning that a pill out here would read as misplaced app
   chrome. That reasoning was mine; Max has now looked at both arms and prefers the pill, and his
   eye is the acceptance test for this. The POSITION is untouched -- still below the phone, which
   is the part he confirmed he wants kept.
   Width is capped rather than the original 100%: inside the phone the screen constrained it, out
   here a full-width pill would stretch the whole content column. */
[data-switchstyle="pill"] .wys-view-switch {
  display: flex;
  /* w79c, Max: "can we make the width the same width as the phone". Reads the SAME
     --phone-w the phone sizes off, hoisted to their shared parent, so they cannot drift. */
  width: var(--phone-w);
  max-width: 100%;
  margin: var(--space-s) auto 0;
  gap: 0.2em;
  padding: 0.25em;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  justify-content: center;
}
[data-switchstyle="pill"] .wys-view-btn {
  flex: 1 1 0;
  min-height: 44px;              /* a real tap target -- this one IS a page control */
  padding-inline: 1.1em;
  background: transparent;
  border: 0;
  border-bottom: 0;              /* clears w69's 2px underline mechanism */
  border-radius: 999px;
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.85rem;
  letter-spacing: 0;
  cursor: pointer;
}
[data-switchstyle="pill"] .wys-view-btn[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper-white, #fff);
  border-bottom-color: transparent;
}
@media (hover: hover) and (pointer: fine) {
  [data-switchstyle="pill"] .wys-view-btn:not([aria-pressed="true"]):hover { color: var(--ink); background: rgba(31,42,34,0.06); }
}

/* ============================================================================
   w80 -- THE LEGAL / UTILITY PAGES JOIN THE SITE.
   Max, 2 Sep: "can we make the privacy policy actually matching our site? it kinda feels like
   it's in it's own unmaintained basement rn."
   Cause was structural: privacy / terms / accessibility / 404 never loaded this stylesheet. Each
   hand-carried a ~46-line copy of the tokens "rather than importing the 86KB sheet" (measured:
   127KB raw, 40KB gzipped -- and already cached for anyone arriving from the site). Those four
   copies had drifted: body 16px against the site's 18px, section headings in Archivo 16.8px
   instead of the display face, a hard-coded 38em column instead of --measure, and a header with
   no nav at all. The copies also broke a real thing: .skip-link was added to those pages and had
   NO rules there, so it rendered as a visible link at (0,0) until Max spotted it.
   These rules now live here ONCE. The pages carry no <style> block at all.
   ============================================================================ */

.legal-wrap {
  max-width: var(--measure-narrow);
  margin-inline: auto;
  padding: var(--space-l) 1.5rem var(--space-xl);
}
.legal-wrap h1 {
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(1.8rem, 5vw, 2.6rem); line-height: 1.15; margin: 0 0 0.5rem;
}
/* the display face, so it reads as a heading in the site's voice -- but NOT at the section-heading
   scale (41.6-72px). That size is for one heading per screen, not fifteen in one document. */
.legal-wrap h2 {
  font-family: var(--font-display); font-weight: 400;
  font-size: 1.35em; line-height: 1.2; margin: 2.2rem 0 0.6rem; color: var(--ink);
}
.legal-updated { font-size: 0.9em; color: var(--ink-soft); margin: 0 0 2rem; }
.legal-intro { color: var(--ink-soft); }
.legal-wrap p, .legal-wrap li { line-height: 1.65; margin: 0 0 0.9rem; }  /* size INHERITS body's fluid clamp -- never hard-code a second number */
/* style.css:13 resets `ul { list-style: none; padding: 0 }` for the site's nav and chip rows.
   A policy document needs its bullets back -- the list structure is part of the meaning, and
   without markers these read as stray indented sentences. Scoped so the reset still holds
   everywhere else. */
.legal-wrap ul { list-style: disc; padding-left: 1.4em; margin: 0 0 0.9rem; }
.legal-wrap li { margin-bottom: 0.45em; }
.legal-wrap a { color: var(--eucalyptus-dark); }

/* 404 -- same treatment, its own shape */
.notfound {
  max-width: var(--measure-narrow); margin-inline: auto;
  padding: var(--space-xl) 1.5rem; text-align: center;
}
.notfound-numeral {
  font-family: var(--font-display); font-size: clamp(4rem, 18vw, 9rem);
  line-height: 1; color: var(--hairline); margin: 0 0 0.5rem;
}
.notfound h1 { font-family: var(--font-display); font-weight: 400; font-size: clamp(1.8rem, 5vw, 2.6rem); margin: 0 0 0.75rem; }
.notfound p { line-height: 1.65; margin: 0 0 1.5rem; }
