/* ==========================================================================
   Frand — Portfolio home
   Mobile-first layout. Breakpoints: tablet >= 768px, desktop >= 1200px.
   ========================================================================== */

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

/* The page colour on the root too, not only on <body>: it is what shows
   under Safari's translucent bars and in the overscroll. */
/* The ground the page's cards float on (see --frame-gap). */
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  background-color: var(--color-frame);
}

/* Clears the floating top bar (66–74px with its gaps, below 1200px); reset
   to --space-8 at >= 1200px, where the bar is gone. */
section[id] {
  scroll-margin-top: calc(var(--space-8) + var(--space-5));
}

body,
h1, h2, h3, p, figure, ul, ol {
  margin: 0;
}

ul, ol {
  padding: 0;
  list-style: none;
}

a {
  color: inherit;
  text-decoration: none;
}

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

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
}

/* One focus ring for every control. */
:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}

/* ---- Accent presets ----
   Each preset carries everything it needs on one line; the two theme blocks
   below pick the active text color. Adding a sixth accent is a single line
   here plus an entry in the ACCENTS table in theme.js.

   --accent-fill is the color as picked, at full brightness: every filled
   surface (bands, pills, bars, cursors) wears it in both themes, with
   --accent-on-fill on top. Text and hairlines can't: lime and amber land at
   1.3–1.7:1 on the ivory page. So --accent-l is the same hue taken down to
   ≥5:1 for text on the light page, and --accent-d the one that reads on the
   night palette. */
:root                       { --accent-fill: #b6f22e; --accent-on-fill: #0b0b0d; --accent-l: #3a7404; --accent-d: #b6f22e; } /* lime (default) */
:root[data-accent="cobalt"] { --accent-fill: #2f6bff; --accent-on-fill: #ffffff; --accent-l: #1f55e6; --accent-d: #6b9bff; }
:root[data-accent="violet"] { --accent-fill: #7c4dff; --accent-on-fill: #ffffff; --accent-l: #6d3df0; --accent-d: #b197ff; }
:root[data-accent="coral"]  { --accent-fill: #ff4f6e; --accent-on-fill: #ffffff; --accent-l: #d4183f; --accent-d: #ff7a90; }
:root[data-accent="amber"]  { --accent-fill: #ffb300; --accent-on-fill: #0b0b0d; --accent-l: #8f5a00; --accent-d: #ffc233; }

/* Wide-gamut screens (most Macs and iPhones) get the fills past what sRGB
   can show. Text keeps its hex, whose contrast is the one checked. */
@media (color-gamut: p3) {
  :root                       { --accent-fill: oklch(0.9 0.24 128); }
  :root[data-accent="cobalt"] { --accent-fill: oklch(0.58 0.25 262); }
  :root[data-accent="violet"] { --accent-fill: oklch(0.56 0.27 290); }
  :root[data-accent="coral"]  { --accent-fill: oklch(0.68 0.24 12); }
  :root[data-accent="amber"]  { --accent-fill: oklch(0.82 0.19 75); }
}

/* ---- Design tokens ---- */
:root {
  color-scheme: light;

  /* color
     A barely-there ivory, warm without going cream or khaki, and the same
     whatever the accent: mixing the accent into the surfaces is what made
     them read muddy. The accent shows in its own marks and fills instead. */
  --color-bg: #fbf9f6;
  --color-bg-card-header: #f3f0eb;
  --color-bg-menu: #ffffff; /* sidebar + theme strip: a white panel by day */
  --color-bg-card-media: #ffffff;
  /* the two glass surfaces are the same colors again, at their old alphas */
  --color-bg-card-header-glass: color-mix(in srgb, var(--color-bg-card-header) 60%, transparent);
  --color-bg-card-media-glass: color-mix(in srgb, var(--color-bg-card-media) 50%, transparent);
  --color-text: #141210;
  --color-text-secondary: #66625c;
  --color-accent: var(--accent-l);
  --color-border: #ebe7e1;
  --color-placeholder-bg: #efebe5;
  --color-placeholder-border: #cfc9c0;
  --color-placeholder-text: #7f7a72;
  /* Deliberately NOT derived from --color-accent: the accent is visitor-selectable,
     and a green or blue "error" would read as the wrong signal. Fixed red, picked
     for contrast (the accent itself sits at ~3.0:1 on white, too low for body text). */
  --color-error: #b23c16;
  --color-cursor-you: #557495;
  /* Fran's cursor is a fill (arrow and name tag), so it wears the bright form;
     its tag text is --accent-on-fill, set on .cursor--fran. */
  --color-cursor-fran: var(--accent-fill);
  --color-cursor-ai: #7c4dff;
  --color-cursor-you-projects: #8e6843;

  /* Foregrounds that sit ON a filled surface, so they flip with the theme
     rather than inverting with it: both are white on light, near-black on dark. */
  --color-on-accent: #ffffff;   /* text on an accent / cursor-colored fill */
  --color-on-error: #ffffff;    /* text on a solid --color-error fill */
  /* Large filled surfaces (bands, pills, bars): the accent in its bright
     form, in both themes, with the preset's own ink on top. */
  --color-accent-fill: var(--accent-fill);
  --color-on-accent-fill: var(--accent-on-fill);
  /* A pale wash of the accent, for a highlighted row or column. Mixed in
     sRGB with white so the hue stays clean instead of drifting muddy. */
  --color-accent-tint: color-mix(in srgb, var(--color-accent-fill) 18%, #ffffff);
  --color-on-inverse: #ffffff;  /* text on a --color-text fill */
  /* The agent's pill and the hero's agent bar: a solid surface a step off
     the page, white by day. */
  --agent-pill: #ffffff;

  /* Channel triples, for the places that need the same color at an alpha */
  --shadow-rgb: 20 18 16;             /* --color-text, for every box-shadow */
  --color-cursor-you-rgb: 85 116 149;
  --color-cursor-ai-rgb: 124 77 255;

  /* type */
  --font-family: "Rethink Sans", -apple-system, "Helvetica Neue", Arial, sans-serif;
  --fs-h1: clamp(1.75rem, 5vw, 2.5rem);
  --fs-h2: 1.375rem;
  --fs-h3: 1rem;
  --fs-body: 0.9375rem;
  --fs-small: 0.8125rem;

  /* spacing */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;
  --space-9: 6rem;

  /* layout */
  --radius: 6px;
  --radius-lg: 24px;
  --radius-agent: 8px; /* the agent's bubbles and cards, a step under the input's */
  --sidebar-width: 208px;

  /* The frame: the page is cards floating on --color-frame, --frame-gap
     apart. Below 1200px the top bar and the content's cards (Frand Agent
     too, from 1024px); from there the menu, the content and the agent. */
  --frame-gap: 8px;
  --frame-radius: 12px;
  --color-frame: var(--color-bg-card-header);   /* the ground, a step under the page */
  --color-panel: var(--color-bg-menu);          /* menu + agent, a step over it */
  --shadow-panel: 0 1px 2px rgb(var(--shadow-rgb) / 0.06), 0 8px 24px rgb(var(--shadow-rgb) / 0.08);

  /* Text input: one standard for every field you type into (the agent's
     bar, pill and column, the password modal). Rounded and on its own
     surface with a firm edge, so it reads as a place to type. */
  --radius-input: 10px;
  --input-bg: #ffffff;
  --input-border: color-mix(in oklab, var(--color-text) 18%, var(--color-border));
  --input-border-hover: color-mix(in oklab, var(--color-text) 35%, var(--color-border));
  --input-ring: color-mix(in srgb, var(--color-accent-fill) 28%, transparent);
  --content-max-width: 760px;
  --page-padding: var(--space-5);
  /* What sticky things inside the page clear: the floating top bar (--bar
     plus its two hairlines) with a --frame-gap above and below it. 0 from
     1200px, where the bar is gone. */
  --sticky-top: calc(2 * var(--frame-gap) + 48px + 2px);
}

/* ---- Night theme ----
   theme-boot.js / theme.js set this attribute from the stored light|dark
   pick, or from prefers-color-scheme when there is none, so the attribute is
   the single switch — no @media duplicate of the palette to keep in sync.
   Warm near-black rather than neutral gray, to keep the ivory's warmth. */
:root[data-theme="dark"] {
  color-scheme: dark;

  /* Fixed, like the light palette: no accent mixed into the surfaces. */
  --color-bg: #0e0d0c;
  --color-bg-card-header: #171614;
  --color-bg-menu: var(--color-bg-card-header); /* a step lighter than the page */
  --color-bg-card-media: #1c1b18;
  --color-bg-card-header-glass: color-mix(in srgb, var(--color-bg-card-header) 60%, transparent);
  --color-bg-card-media-glass: color-mix(in srgb, var(--color-bg-card-media) 50%, transparent);
  --color-text: #ece9e4;
  --color-text-secondary: #a39f98;
  --color-accent: var(--accent-d);
  --color-border: #2a2825;
  --color-placeholder-bg: #1f1e1b;
  --color-placeholder-border: #3d3a35;
  --color-placeholder-text: #8a857d;
  --color-error: #ff9578;
  --color-cursor-you: #8fb4dc;
  --color-cursor-ai: #b197ff;
  --color-cursor-you-projects: #c9a075;

  /* The fills these sit on are lighter here, so white would fail; they go dark. */
  --color-on-accent: #141210;
  --color-on-error: #141210;
  --color-on-inverse: #0e0d0c;
  --agent-pill: color-mix(in oklab, var(--color-bg-card-media), var(--color-text) 12%);
  --color-frame: color-mix(in oklab, var(--color-bg) 55%, #000);
  --input-bg: color-mix(in oklab, var(--color-bg-card-media), var(--color-text) 8%);

  /* On near-black a wash of a bright accent reads as mud, so the highlight
     is a neutral lift instead; the accent shows in its marks and edges. */
  --color-accent-tint: color-mix(in oklab, var(--color-text) 7%, var(--color-bg-card-media));

  --shadow-rgb: 0 0 0;   /* a tinted shadow is invisible on dark; use true black */
  --color-cursor-you-rgb: 143 180 220;
  --color-cursor-ai-rgb: 177 151 255;
}

body {
  font-family: var(--font-family);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.55;
  color: var(--color-text);
  background: var(--color-frame);
}

h1, h2, h3 {
  font-weight: 600;
  line-height: 1.15;
}

/* The hero title is the only h1: full 2.5rem from mobile up, rather than the
   fluid --fs-h1 (which the modal titles keep). */
h1 {
  font-size: 2.5rem;
}

h2 {
  font-size: var(--fs-h2);
}

h3 {
  font-size: var(--fs-h3);
}

p {
  color: var(--color-text);
}

.accent {
  color: var(--color-accent);
}

/* ---- Utility: image / media placeholder ---- */
.placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  background: var(--color-placeholder-bg);
  border: 1px dashed var(--color-placeholder-border);
  border-radius: var(--radius);
  color: var(--color-placeholder-text);
  font-size: var(--fs-small);
  padding: var(--space-4);
}

/* ---- Checkbox-hack for mobile menu (no JS) ---- */
.menu-toggle-input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

/* ---- Fade-in on scroll into view ----
   Only hidden when JS is on (.js is set by theme-boot.js before first paint), so
   a failed or slow script never leaves the page blank. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

.js [data-reveal="down"] {
  transform: translateY(-24px);
}

/* Failsafe: if reveal.js hasn't started within 2s (slow host, blocked request),
   show everything anyway. Once it runs it sets .reveal-ready and takes over. */
.js:not(.reveal-ready) [data-reveal] {
  animation: reveal-failsafe 0.4s ease 2s forwards;
}

@keyframes reveal-failsafe {
  to { opacity: 1; transform: none; }
}

[data-reveal].is-visible {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ==========================================================================
   Layout shell
   ========================================================================== */

.page {
  display: flex;
  flex-direction: column;
  gap: var(--frame-gap);
  min-height: 100vh;
  padding: var(--frame-gap);
}

/* The content column: the A/B banner as a box of its own on the frame
   (index only), the content as cards, and the footer under them on the
   frame, out of any card. */
.content {
  display: flex;
  flex-direction: column;
}

/* A case: the whole main is one card. Its sections' hairlines run to its
   edge and stop there; clip rather than hidden, so the case tabs still
   stick. */
.main {
  flex: 1;
  border: 1px solid var(--color-border);
  border-radius: var(--frame-radius);
  background: var(--color-bg);
  overflow: clip;
}

/* The home: a column of cards, --frame-gap apart (two columns from
   1200px) — the hero with the cases it opens onto, then Fit check and
   Method. The hero and the cases don't clip (dragged words and the sphere
   may reach past them); their corners are rounded on their own surface. */
.main:has(> .hero) {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--frame-gap);
  border: none;
  border-radius: 0;
  background: none;
  overflow: visible;
}

.main:has(> .hero) > :is(.hero, .cases, .principles, .method) {
  border: 1px solid var(--color-border);
  background: var(--color-bg);
}

.main:has(> .hero) > :is(.hero, .cases) {
  grid-column: 1 / -1;
}

.main:has(> .hero) > .hero {
  border-bottom: none;
  border-radius: var(--frame-radius) var(--frame-radius) 0 0;
}

/* …flush to the hero: one card, so the column's gap is taken back. */
.main:has(> .hero) > .cases {
  margin-top: calc(-1 * var(--frame-gap));
  border-top: none;
  border-radius: 0 0 var(--frame-radius) var(--frame-radius);
}

.main:has(> .hero) > :is(.principles, .method) {
  border-radius: var(--frame-radius);
  overflow: clip;
}

.section-inner {
  width: 100%;
  padding: 0 var(--page-padding);
}

.site-footer {
  padding: var(--space-6) var(--page-padding) var(--space-8);
}

/* ==========================================================================
   Header — desktop sidebar (hidden below 1200px)
   ========================================================================== */

.nav-sidebar {
  display: none;
}

/* ==========================================================================
   Header — tablet / mobile top navigation
   ========================================================================== */

/* The bar can only stick inside its own parent, and <header> is exactly as tall
   as the bar — so the sticky has to sit on the header, whose containing block is
   .page and therefore spans the whole document. It sticks a --frame-gap down. */
.site-header {
  position: sticky;
  top: var(--frame-gap);
  z-index: 50;
}

/* A card floating on the frame, like the menu that replaces it at 1200px:
   the menu's surface as glass, its edge and shadow. Its blocks' hairlines
   run to the card's edge and stop there, clipped to its corners; the
   mobile menu opens inside it. */
.nav-topbar {
  border: 1px solid var(--color-border);
  border-radius: var(--frame-radius);
  background: color-mix(in srgb, var(--color-panel) 75%, transparent);
  box-shadow: var(--shadow-panel);
  overflow: clip;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

/* Edge to edge, in blocks: photo · name · (CV/LinkedIn on tablet) · dark
   mode · color · language · menu, each split from the next by a full-height
   hairline. --bar is the row height and the side of every square block. */
.nav-topbar__row {
  --bar: 48px;
  display: flex;
  flex-wrap: nowrap;
  align-items: stretch;
  justify-content: flex-start;
  gap: 0;
  min-height: var(--bar);
  padding: 0;
  overflow-x: auto;
  scrollbar-width: none;
}

.nav-topbar__row > * {
  border-right: 1px solid var(--color-border);
}

.nav-topbar__row > :last-child {
  border-right: none;
}

/* Photo and name are one link, split into two blocks. */
.nav-topbar .brand--compact {
  flex: 1;
  min-width: 0;
  gap: 0;
  align-items: stretch;
  border-right: 1px solid var(--color-border);
}

/* A square block holding the round photo, with air around it. */
.nav-topbar .brand__photo {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--bar);
  border-right: 1px solid var(--color-border);
}

.nav-topbar .brand__photo .avatar {
  width: calc(var(--bar) - 16px);
  height: calc(var(--bar) - 16px);
}

.nav-topbar .brand__text {
  justify-content: center;
  min-width: 0;
  padding: var(--space-2) var(--space-4);
}

.nav-topbar .brand__name,
.nav-topbar .brand__role {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The bar's first block is the card's head, as the menu's is (.brand--head):
   the content cards' warm ground, the name in ink and the role in grey —
   the photo and name on the home, the "Frand / Nomo" breadcrumb on a case. */
.nav-topbar :is(.brand--compact, .case-crumbs) {
  background: var(--color-bg);
}

.nav-topbar .case-crumbs .brand--compact {
  background: none;
}

.nav-topbar .case-crumbs a {
  color: var(--color-text);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 3px;
  transition: text-decoration-color 0.2s ease;
}

.nav-topbar .case-crumbs a:hover {
  color: var(--color-text);
  text-decoration-color: currentColor;
}

.nav-topbar .case-crumbs li + li,
.nav-topbar .case-crumbs [aria-current="page"] {
  color: var(--color-text-secondary);
  font-weight: 400;
}

/* The mount steps out of the layout, so dark mode and language are
   blocks of the bar in their own right: each a --bar square, divided by the
   same hairline instead of drawing its own box. */
.nav-topbar [data-theme-controls="topbar"] {
  display: contents;
}

[data-theme-controls="topbar"] .mode-trigger,
[data-theme-controls="topbar"] .lang-trigger {
  flex-shrink: 0;
  width: var(--bar);
  height: auto;
  padding: 0;
  margin-left: 0;
  border: none;
  border-right: 1px solid var(--color-border);
  background: transparent;
}

/* No box to recolor here, so hover fills the block instead. */
[data-theme-controls="topbar"] .mode-trigger:hover,
[data-theme-controls="topbar"] .lang-trigger:hover {
  border-color: var(--color-border);
  background: var(--color-bg-card-header);
}

.nav-topbar__row::-webkit-scrollbar {
  display: none;
}

.brand {
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* The menu card's head: the name and role get the top of the card to
   themselves, edge to edge, on the content cards' warm ground (--color-bg)
   and closed by a hairline. A case keeps the same two lines: the breadcrumb
   breaks after "Frand", which takes the name's place, and "/ Nomo" the
   role's. */
.brand--head {
  margin: calc(-1 * var(--space-6)) calc(-1 * var(--space-5)) 0;
  padding: var(--space-5);
  border-bottom: 1px solid var(--color-border);
  border-radius: calc(var(--frame-radius) - 1px) calc(var(--frame-radius) - 1px) 0 0;
  background: var(--color-bg);
}

.brand--head .case-crumbs ol {
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
}

.brand--head .case-crumbs li {
  gap: var(--space-1);
}

.brand--head .case-crumbs a {
  color: var(--color-text);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 3px;
  transition: text-decoration-color 0.2s ease;
}

.brand--head .case-crumbs a:hover {
  color: var(--color-text);
  text-decoration-color: currentColor;
}

/* The role's line: its size and grey, slash and case name alike. */
.brand--head .case-crumbs li + li,
.brand--head .case-crumbs [aria-current="page"] {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
  font-weight: 400;
}

.brand--compact {
  flex-direction: row;
  align-items: center;
  gap: var(--space-3);
}

/* Only ever rendered inside .brand--compact, which lives in the topbar — and
   the topbar is display:none from 1200px up. So this is the tablet/mobile brand. */
.brand__text {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.brand__name {
  font-weight: 600;
  font-size: var(--fs-body);
}

/* Text (™, "Agent"), so the accent in its readable form: the bright
   fill is too light to read as letters on the light surfaces. */
.brand__tm {
  color: var(--color-accent);
  font-size: 0.6em;
  vertical-align: super;
}

.brand__role {
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

.avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--color-placeholder-bg);
  border: 1px solid var(--color-border);
  flex-shrink: 0;
}

.avatar--sm {
  width: 32px;
  height: 32px;
}

/* below 768px these two links live inside the hamburger menu instead
   (.nav-topbar__mobile-actions), so the row only carries brand + toggle */
.nav-topbar__actions {
  display: none;
  align-items: center;
  gap: var(--space-4);
  padding: 0 var(--space-4);
}

.nav-link {
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
  white-space: nowrap;
  transition: color 0.25s ease;
}

.nav-link.is-active {
  color: var(--color-text);
  font-weight: 600;
}

.nav-link--soon {
  text-decoration: line-through;
  text-decoration-color: var(--color-text-secondary);
  cursor: not-allowed;
}

.tag {
  font-size: 0.6875rem;
  font-weight: 400;
  color: var(--color-text-secondary);
  vertical-align: super;
  text-decoration: none;
  margin-left: var(--space-1);
}

.text-link {
  font-size: var(--fs-small);
  text-decoration: underline;
  text-underline-offset: 2px;
  color: var(--color-text);
  white-space: nowrap;
}

/* mobile menu toggle (hamburger) — the last square block of the bar */
.menu-toggle-label {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: var(--bar, 48px);
  cursor: pointer;
  font-size: var(--fs-small);
  flex-shrink: 0;
}

.menu-toggle-label__icon {
  display: inline-flex;
  flex-direction: column;
  gap: 4px;
}

.menu-toggle-label__icon span {
  width: 18px;
  height: 2px;
  background: var(--color-text);
  display: block;
}

/* mobile dropdown nav, toggled via checkbox. Boxed like the bar above it:
   every link and action is a full-width row, split by the same hairline. */
.nav-topbar__mobile-nav {
  display: none;
  flex-direction: column;
  border-top: 1px solid var(--color-border);
}

.nav-topbar__mobile-nav ul {
  display: flex;
  flex-direction: column;
}

.menu-toggle-input:checked ~ .page .nav-topbar__mobile-nav {
  display: flex;
}

.nav-topbar__mobile-nav li + li,
.nav-topbar__mobile-actions {
  border-top: 1px solid var(--color-border);
}

.nav-topbar__mobile-nav .nav-link,
.nav-topbar__mobile-actions > .text-link {
  display: block;
  width: 100%;
  padding: var(--space-4) var(--page-padding);
  text-align: left;
  transition: background-color 0.25s ease;
}

.nav-topbar__mobile-nav .nav-link:hover,
.nav-topbar__mobile-actions > :hover {
  background: var(--color-bg-card-header);
}

.nav-topbar__mobile-actions {
  display: flex;
  flex-direction: column;
}

.nav-topbar__mobile-actions > * + * {
  border-top: 1px solid var(--color-border);
}

/* ==========================================================================
   Theme switch + primary-color dropdown

   Markup is injected by theme.js into the [data-theme-controls] mounts, so the
   controls never exist as dead UI when JS is off. Two mounts, never visible at
   the same time: the topbar row below 1200px, and a pinned top-right widget
   above it, where the topbar is display:none and the nav is a left sidebar.
   ========================================================================== */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Theme, color and language sit in one strip of square tags drawn with the
   section hairline. Each one after the first pulls back 1px so neighbours
   share a single edge; a hovered or focused tag rises above its neighbours
   so its own border wins that shared edge. */
[data-theme-controls] {
  display: flex;
  align-items: center;
  gap: 0;
  flex-shrink: 0;
  margin-left: auto;
}

.lang-trigger {
  margin-left: -1px;
}

.mode-trigger,
.lang-trigger {
  position: relative;
}

.mode-trigger:hover,
.mode-trigger:focus-visible,
.lang-trigger:hover,
.lang-trigger:focus-visible {
  z-index: 1;
}

/* …and once these are in the row, the hamburger's own margin-left:auto would
   fight them for the same space. */
[data-theme-controls] ~ .menu-toggle-label {
  margin-left: 0;
}

/* The desktop widget only exists above 1200px; see the media query at the foot
   of this file. Hidden by default so it never doubles up with the topbar one. */
[data-theme-controls="desktop"] {
  display: none;
}

/* ---- Light / dark tag: only the active icon; both modes drop down ---- */
.mode-trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid var(--color-border);
  background: var(--color-bg-menu);
  color: var(--color-text);
  cursor: pointer;
  transition: border-color 0.25s ease;
}

.mode-trigger:hover {
  border-color: var(--color-accent);
}

.mode-trigger__icon {
  display: flex;
}

:root[data-theme="dark"] .mode-trigger__icon--light,
:root:not([data-theme="dark"]) .mode-trigger__icon--dark {
  display: none;
}

.mode-trigger svg,
.mode-menu__option svg {
  width: 16px;
  height: 16px;
}

/* The tag always shows the active mode, so its icon is always the bold one. */
.mode-trigger svg,
.mode-menu__option[aria-pressed="true"] svg {
  stroke-width: 2;
}

/* A pill-organiser frame (.theme-palette): one compartment per mode. */
.mode-menu__option {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 32px;
  border-top: 1px solid var(--color-border);
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: color 0.25s ease;
}

.mode-menu__option:hover,
.mode-menu__option[aria-pressed="true"] {
  color: var(--color-text);
}

/* ---- Language tag: shows the current code; the menu drops down below ---- */
.lang-trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px; /* same square as the mode tag */
  height: 32px;
  padding: 0;
  border: 1px solid var(--color-border);
  background: var(--color-bg-menu);
  font-size: var(--fs-small);
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: border-color 0.25s ease;
}

.lang-trigger:hover {
  border-color: var(--color-accent);
}

/* The same pill-organiser frame as the modes (.theme-palette): one
   compartment per language, the current one in bold. */
.lang-menu__option {
  display: block;
  line-height: 1;
  width: 100%;
  height: 32px;
  border-top: 1px solid var(--color-border);
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: color 0.25s ease;
}

.lang-menu__option:hover {
  color: var(--color-text);
}

.lang-menu__option[aria-pressed="true"] {
  color: var(--color-text);
  font-weight: 700;
}

/* Only capitals here ("EN", "ES"), so the descender space the line box keeps
   below them makes the code sit high. Trim to the cap height where supported. */
.lang-trigger [data-lang-current],
.lang-menu__option {
  text-box: trim-both cap alphabetic;
}

/* ---- Dropdown ---- */
/* Top layer, so light-dismiss and Escape come from the platform rather than
   from outside-click bookkeeping in theme.js. Anchor positioning is not
   broadly supported yet, so this is pinned to the viewport corner instead —
   which is where both triggers already are. */
/* Hangs straight down from its tag like a pill organiser: same width,
   same hairline, sharing the tag's bottom edge. theme.js measures the trigger
   on open and feeds --palette-top / --palette-left / --palette-width. */
.theme-palette {
  position: fixed;
  inset: auto;
  top: var(--palette-top, 32px);
  left: var(--palette-left, auto);
  width: var(--palette-width, 32px);
  margin: 0;
  padding: 0;
  border: 1px solid var(--color-border);
  border-top: none;
  background: var(--color-bg-menu);
  color: var(--color-text);
  overflow: visible;
}

.theme-palette::backdrop {
  background: transparent;
}

/* Engines without the Popover API get the same box via the [hidden] fallback
   in theme.js, which needs a stacking context of its own. */
.theme-palette:not([popover]) {
  z-index: 60;
}


@media (prefers-reduced-motion: reduce) {
  .lang-trigger,
  .lang-menu__option,
  .mode-trigger,
  .mode-menu__option {
    transition: none;
  }
}

/* ==========================================================================
   Hero
   ========================================================================== */

/* One scene: the sentence with its three word boxes, the Fran and AI cursors
   resting on them, the bio and, below 1024px, the agent bar. hero.js runs the
   drag, the FLIP and the cursors. Never a full screen: the next section's
   label and a good part of its first cards always show below it, so it reads
   as a page to scroll rather than a cover. The top padding leaves room for
   the drag tooltip over the first line. */
.hero {
  --hero-peek: 300px;
  padding: var(--space-7) 0 var(--space-6);
  /* A box dragged past the window's edge mustn't open a sideways scroll. */
  overflow-x: clip;
}

.hero__inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

/* The "hero area": the cursors live and stay inside it. Sized by its
   column, so the title can scale with the room it actually has (the nav and
   chat columns both eat into the window from 1024px). */
.hero__scene {
  position: relative;
  container-type: inline-size;
}

.hero__title {
  position: relative; /* the dragged box is laid out against it */
  font-family: "Space Grotesk", var(--font-family);
  font-size: clamp(40px, 7vw, 104px);
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: -0.03em;
}

@supports (font-size: 1cqi) {
  .hero__title {
    font-size: clamp(40px, 10.5cqi, 104px);
  }
}

/* Every piece of the sentence is its own box, so hero.js can FLIP it when
   the words around it change width. */
.hero__word,
.hero__punct,
.hero__slot {
  display: inline-block;
}

/* A slot never wraps away from its comma. */
.hero__group {
  white-space: nowrap;
}

/* A hair of room, so the box's corner handles don't sit on the comma (or,
   in B, the full stop). */
.hero__punct--comma,
.hero__punct--period {
  margin-left: 0.08em;
}

/* The A/B test (experiment.js): only the drawn version's sentence
   is there at all. A is the fallback when nothing was drawn. */
:root:not([data-hero-variant="b"]) [data-hero-only="b"],
:root[data-hero-variant="b"] [data-hero-only="a"] {
  display: none;
}

/* ---- Word box: a Figma-style selection frame ----
   The frame and the handles are absolutely positioned, so the box is exactly
   as tall as the line and never changes the sentence's line height. --frame
   carries the state: neutral, a cursor's colour, or ink. */
.word-box {
  --frame: var(--color-border);
  position: relative;
  display: inline-block;
  padding: 0 0.2em;
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.word-box::before {
  content: "";
  position: absolute;
  inset: 0;
  border: 1.5px solid var(--frame);
  pointer-events: none;
  transition: border-color 0.2s ease;
}

.word-box__handle {
  position: absolute;
  width: 9px;
  height: 9px;
  border: 1.5px solid var(--frame);
  background: var(--color-bg);
  pointer-events: none;
  transition: border-color 0.2s ease;
}

.word-box__handle--tl { top: -4.5px; left: -4.5px; }
.word-box__handle--tr { top: -4.5px; right: -4.5px; }
.word-box__handle--bl { bottom: -4.5px; left: -4.5px; }
.word-box__handle--br { bottom: -4.5px; right: -4.5px; }

/* A cursor resting on the box lends it its colour… */
.word-box[data-presence="fran"] {
  --frame: var(--color-cursor-fran);
}

.word-box[data-presence="ai"] {
  --frame: var(--color-cursor-ai);
}

/* …but the visitor's own hover, focus or drag wins. */
@media (hover: hover) {
  .word-box:hover {
    --frame: var(--color-text);
  }
}

.word-box:focus-visible {
  outline: none;
  --frame: var(--color-text);
}

/* Lifted out of the sentence while dragged; hero.js moves it with
   `translate`, so the lift can use `scale` and `rotate` alongside it. */
.word-box.is-dragging,
.word-box.is-settling {
  --frame: var(--color-text);
  position: absolute;
  top: 0;
  left: 0;
  z-index: 4;
  background: var(--color-bg);
  transition: scale 0.2s ease-out, rotate 0.2s ease-out, box-shadow 0.2s ease-out;
}

.word-box.is-dragging {
  cursor: grabbing;
  scale: 1.03;
  rotate: -2deg;
  box-shadow: 0 14px 32px rgb(var(--shadow-rgb) / 0.3);
}

/* The drag hint (hero.js): the first box, lifted for a moment as if picked
   up. A softer shadow than a real drag; the motion is a WAAPI animation. */
.word-box.is-nudged {
  --frame: var(--color-text);
  z-index: 1;
  background: var(--color-bg);
  box-shadow: 0 8px 20px rgb(var(--shadow-rgb) / 0.18);
}

/* Holds the dragged box's place: the same word, invisible, in the same
   padding, so it is exactly the box's size and sits on the same baseline. */
.word-box--placeholder {
  --frame: var(--hatch-line);
  cursor: default;
  pointer-events: none;
}

/* Dashed, and hatched like an empty frame in Figma (--hatch is set with the
   intro's frames, below). */
.word-box--placeholder::before {
  border-style: dashed;
  background: var(--hatch);
}

.word-box--placeholder .word-box__label {
  visibility: hidden;
}

/* ---- B's "no" (hero.js, locked) ----
   The swap is wrong the moment it happens: every frame in the sentence turns
   the error colour inside a faint red halo, and the word goes red with it,
   from the moment Fran notices until the box is back home (Fran's comment,
   on solid red, is .cursor__say). */
.word-box {
  transition: box-shadow 0.25s ease, color 0.25s ease;
}

.hero__title.is-wrong .word-box {
  --frame: var(--color-error);
  color: var(--color-error);
  box-shadow:
    0 0 0 3px color-mix(in oklab, var(--color-error) 14%, transparent),
    0 6px 28px color-mix(in oklab, var(--color-error) 26%, transparent);
}

/* ---- The A/B test's strip, at the top of the page (ab.js) ----
   Small and quiet, left-aligned: one short sentence with the versions in
   ink, and the way into the results right after it. Full width over the
   hero; from 1200px a box of its own above the content card (see the
   full-screen block). It waits for the intro. */
.ab-chip {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 32px;
  margin-bottom: var(--frame-gap);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--frame-radius);
  background: var(--color-panel);
  color: var(--color-text-secondary);
  font-size: 0.75rem;
  line-height: 1.3;
  text-align: left;
  cursor: pointer;
  transition: opacity 0.4s ease, color 0.2s ease, border-color 0.25s ease;
}

.ab-chip[hidden] {
  display: none;
}

:root.intro-pending .ab-chip {
  opacity: 0;
}

.ab-chart__you {
  padding: 1px 7px;
  border: 1px solid var(--color-border);
  color: var(--color-text-secondary);
  font-size: 0.625rem;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.ab-chip__text {
  min-width: 0;
}

.ab-chip__cta {
  flex-shrink: 0;
  white-space: nowrap;
  color: var(--color-text);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-color 0.2s ease;
}

.ab-chip:hover .ab-chip__cta {
  text-decoration-color: var(--color-accent);
}

.hero__bio {
  max-width: 34em;
  margin-top: var(--space-6);
  color: var(--color-text-secondary);
  font-size: clamp(16px, 1.25vw, 18px);
  line-height: 1.5;
}

.hero__bio-lead {
  color: var(--color-text);
}

/* ---- Cursors ----
   The shared look (also the Method loop's): an arrow in the collaborator's
   colour with a thin dark edge, and a name tag below-right. */
.cursor {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  pointer-events: none;
}

.cursor--fran {
  --cursor-color: var(--color-cursor-fran);
}

.cursor--fran .cursor__label {
  color: var(--accent-on-fill);
}

.cursor--ai {
  --cursor-color: var(--color-cursor-ai);
}

.cursor__arrow {
  width: 18px;
  height: 18px;
  overflow: visible;
  fill: var(--cursor-color);
  stroke: rgb(var(--shadow-rgb) / 0.85);
  stroke-width: 1;
  stroke-linejoin: round;
}

.cursor__label {
  margin-left: 12px;
  padding: 1px 6px;
  border-radius: 3px;
  background: var(--cursor-color);
  color: var(--color-on-accent);
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.5;
  white-space: nowrap;
}

.cursor--you {
  --cursor-color: var(--color-cursor-you);
}

/* A comment, Figma cursor-chat style (hero.js, cursors.say): a bubble in
   the cursor's colour under its name tag, its corner by the arrow sharp.
   Absolutely placed, so it never changes the cursor's own size. It opens
   to the arrow's left (.is-say-flip) when it would run off the scene. */
.cursor__say {
  position: absolute;
  top: calc(100% + 4px);
  left: 12px;
  width: max-content;
  max-width: 24ch;
  padding: 6px 10px;
  border-radius: 2px 12px 12px 12px;
  background: var(--cursor-color);
  box-shadow: 0 6px 20px rgb(var(--shadow-rgb) / 0.18);
  color: var(--color-on-accent);
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 13px;
  font-weight: 500;
  line-height: 1.4;
  text-wrap: pretty;
  pointer-events: none;
  opacity: 0;
  scale: 0.85;
  transform-origin: top left;
  transition: opacity 0.2s ease, scale 0.2s ease;
}

.cursor.is-saying .cursor__say {
  opacity: 1;
  scale: 1;
}

/* B's "no": the same bubble, on solid red. */
.cursor.is-say-error .cursor__say {
  background: var(--color-error);
  color: var(--color-on-error);
}

.cursor.is-say-flip .cursor__say {
  left: auto;
  right: calc(100% - 18px);
  border-radius: 12px 2px 12px 12px;
  transform-origin: top right;
}

/* "You" escapes the hero: it trails the visitor's real pointer across the
   whole viewport (hero.js writes --tx / --ty) and picks up the colour of the
   section in view. */
.you-cursor {
  position: fixed;
  z-index: 999;
  top: 0;
  left: 0;
  transform: translate(var(--tx, -100px), var(--ty, -100px));
  opacity: 0;
  transition: opacity 0.3s ease;
}

.you-cursor.is-visible {
  opacity: 1;
}

.you-cursor .cursor__arrow {
  transition: fill 0.4s ease;
}

.you-cursor .cursor__label {
  transition: background-color 0.4s ease;
}

.you-cursor[data-hero-section="projects"] {
  --color-cursor-you: var(--color-cursor-you-projects);
}

.you-cursor[data-hero-section="principles"] {
  --color-cursor-you: var(--color-cursor-ai);
}

/* It stays out of the chat and the cookie toast, and out from behind a
   modal: it isn't in the top layer, so it would show through the backdrop,
   lagging behind the real pointer. */
:root:has(.stage-modal[open]) .you-cursor,
:root:has(.chat:hover) .you-cursor,
:root:has(.consent:hover) .you-cursor,
:root.chat-open .you-cursor,
:root.chat-resizing .you-cursor {
  opacity: 0;
}

/* Above the text, never in the way: no pointer events, and clipped to the
   scene so they can't wander out of the hero. */
.hero__cursors {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: clip;
  pointer-events: none;
}

/* Hidden until hero.js has placed them. */
.hero__cursor {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  will-change: transform;
  transition: opacity 0.4s ease;
}

.hero__cursor.is-ready {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .word-box::before,
  .word-box__handle,
  .hero__cursor,
  .cursor__say {
    transition: none;
  }

  .word-box,
  .ab-chip,
  .ab-chip__cta,
  .ab-chart__bar {
    transition: none;
  }

  .word-box.is-dragging,
  .word-box.is-settling {
    scale: none;
    rotate: none;
    box-shadow: none;
    transition: none;
  }
}

/* ---- Intro: the home assembled from its wireframe ----
   On landing, not on the way back from a case (hero.js). Each piece is a .skel: a dashed, hatched frame
   laid over it, opaque, so whatever is underneath (words, a card's mockup)
   only shows once .is-resolved dissolves it. The bio's frame is cut into
   one bar per line of text instead.
   The hatching is fixed-pixel, so it reads the same at every title size. */
.hero,
.projects,
.stage-modal {
  --hatch: repeating-linear-gradient(
    -45deg,
    color-mix(in oklab, var(--color-text-secondary) 28%, transparent) 0 1.5px,
    transparent 1.5px 7px
  );
  --hatch-line: color-mix(in oklab, var(--color-text-secondary) 60%, transparent);
}

:where(.skel) {
  position: relative;
}

.skel::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border: 1.5px dashed var(--hatch-line);
  border-radius: inherit;
  background: var(--hatch), var(--color-bg);
  pointer-events: none;
  transition: opacity 0.45s ease, border-color 0.2s ease;
}

.skel.is-resolved::after {
  opacity: 0;
}

/* Carried pieces pass over the bio, never under it. */
.hero__title .skel {
  z-index: 1;
}

/* A carried box: the frame in its carrier's colour, the handles on top. */
.word-box.skel::after {
  border-color: var(--frame);
}

.word-box[data-presence].skel::after {
  border-style: solid;
}

.word-box.skel .word-box__handle {
  z-index: 2;
}

:is(.hero__bio, .stage-modal__desc).skel,
:is(.hero__bio, .stage-modal__desc).skel * {
  transition: color 0.45s ease;
}

:is(.hero__bio, .stage-modal__desc).skel:not(.is-resolved),
:is(.hero__bio, .stage-modal__desc).skel:not(.is-resolved) * {
  color: transparent;
}

:is(.hero__bio, .stage-modal__desc).skel::after {
  border: none;
  -webkit-mask-image: repeating-linear-gradient(to bottom, transparent 0 0.3em, #000 0.3em 1.2em, transparent 1.2em 1.5em);
  mask-image: repeating-linear-gradient(to bottom, transparent 0 0.3em, #000 0.3em 1.2em, transparent 1.2em 1.5em);
}

/* No reveal fade on top of the intro's own. */
.hero__scene.is-intro,
.project-card.is-intro {
  transition: none;
}

/* Before hero.js takes over: the agent column waits off to the right, and
   what sits outside the scene's own reveal stays out of sight. If hero.js
   never runs, all of it comes back after 2s. */
:root.intro-pending :is(.projects h2, .cases__views, .hero__agent) {
  visibility: hidden;
  animation: intro-failsafe 0s 2s forwards;
}

@media (min-width: 1024px) {
  :root.intro-pending .chat {
    translate: 100% 0;
    animation: intro-failsafe 0s 2s forwards;
  }
}

@keyframes intro-failsafe {
  to { visibility: visible; translate: none; }
}

/* From 1024px the scene centres in the first screen, less the 57px top bar
   (until 1200px) and the peek of the next section. */
@media (min-width: 1024px) {
  .hero {
    display: flex;
    align-items: center;
    min-height: calc(100svh - 57px - var(--hero-peek));
  }

  /* The agent is the right-hand column here, so the hero carries no bar.
     Two classes: .agent-input's own display: flex comes later in the file
     and would otherwise win. */
  .hero .hero__agent {
    display: none;
  }

  /* B's sentence is a line shorter than A's (two lines to three, at every
     width and in both languages): that line is kept under the bio, so the
     centred scene starts as high as A's instead of sinking. One title line,
     sized as .hero__title is. */
  :root[data-hero-variant="b"] .hero__bio {
    margin-bottom: calc(1.1 * clamp(40px, 7vw, 104px));
  }

  @supports (font-size: 1cqi) {
    :root[data-hero-variant="b"] .hero__bio {
      margin-bottom: calc(1.1 * clamp(40px, 10.5cqi, 104px));
    }
  }
}

/* Below 1024px the conversation opens as a sheet; on tablets it floats over
   the page, so the hero's bar steps out while it is open, too. */
:root.chat-open .hero__agent {
  visibility: hidden;
}

/* Method loop pills and cursors read --x / --y from method-loop.js. */
@property --x {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

@property --y {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

/* Shared with the Method loop's phase pills (the hero's pills are gone). */
.method__pill {
  position: absolute;
  left: var(--x);
  top: var(--y);
  transform: translate(-50%, -50%);
  background: var(--color-bg-card-media);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  padding: var(--space-2) var(--space-4);
  font-size: var(--fs-small);
  font-style: italic;
  white-space: nowrap;
}

.method__cursor {
  position: absolute;
  top: var(--y);
  left: var(--x);
  will-change: top, left;
}

/* ==========================================================================
   Feature projects
   ========================================================================== */

/* The section's top hairline is the top edge of the frame; the h2 hangs off
   its centre as a label, drawn with the same hairline on its other sides.
   No top padding, so the label sits flush against the separator. */
.projects {
  border-top: 1px solid var(--color-border);
  padding-bottom: var(--space-8);
}

/* Shared with the Design principles and Method headings, and the case
   study sections, which hang off their separators the same way. */
.projects h2,
.principles h2,
.method h2,
.case-section .case-kicker {
  width: fit-content;
  margin: 0 auto var(--space-6);
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--color-border);
  border-top: none;
  background: var(--color-bg);
  font-size: 1rem;
  font-weight: 400;
}

.projects__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
}

/* Phones: each section's cards side by side in a strip that scrolls
   sideways, edge to edge, one card and the start of the next in view so
   the scroll is obvious. The block padding leaves room for the focus ring
   (overflow-x clips the other axis too). */
@media (max-width: 767.98px) {
  .projects__grid {
    display: flex;
    gap: var(--space-4);
    margin: calc(-1 * var(--space-2)) calc(-1 * var(--page-padding));
    padding: var(--space-2) var(--page-padding);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--page-padding);
    scrollbar-width: none;
  }

  .projects__grid::-webkit-scrollbar {
    display: none;
  }

  .projects__grid > .project-card {
    flex: 0 0 82%;
    scroll-snap-align: start;
  }

  /* A card that only peeks in never reaches reveal.js's threshold: the
     strip fades in as one, with its first card. */
  .projects__grid:has(> .is-visible) > [data-reveal] {
    opacity: 1;
    transform: none;
  }
}

/* Square corners. The section hairline only shows on hover or keyboard
   focus (see below); it is transparent at rest so nothing shifts. The
   mockup leads and the text follows; in the markup the header stays first,
   so the title is still read before the image. */
.project-card {
  display: flex;
  flex-direction: column;
  border: 1px solid transparent;
  overflow: hidden;
}

.project-card__media {
  order: -1;
}

/* flex: 1 so cards in the same row end level, whatever their text length. */
.project-card__header {
  flex: 1;
  background: var(--color-bg-card-header-glass);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.project-card__title {
  font-size: var(--fs-h2);
}

.project-card__title sup {
  font-size: var(--fs-small);
  font-weight: 400;
  color: var(--color-text-secondary);
  vertical-align: super;
  margin-left: var(--space-2);
}

.project-card__header p {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
}

.project-card__media {
  background: var(--color-bg-card-media-glass);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-radius: 0;
  border: none;
}

.project-card__media img {
  width: 100%;
  height: auto;
}

/* The card's one figure, as a tab growing out of the header into the mockup:
   flush with the picture's bottom edge, never full width. The header's colour
   but opaque, so it stays legible over dark mockups too. */
.project-card__media {
  position: relative;
}

.project-card__tab {
  position: absolute;
  left: var(--space-5);
  bottom: 0;
  max-width: calc(100% - 2 * var(--space-5));
  padding: var(--space-2) var(--space-3);
  background: var(--color-bg-card-header);
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.project-card__tab strong {
  font-weight: 500;
  color: var(--color-text);
}


/* The mockups are light-background rasters, so CSS cannot recolor them — on the
   night palette they would otherwise read as glaring white panels. Take the
   edge off without washing out the work itself; hovering restores full fidelity. */
:root[data-theme="dark"] .project-card__media img {
  filter: brightness(0.86) saturate(0.95);
  transition: filter 0.3s ease;
}

:root[data-theme="dark"] .project-card:hover .project-card__media img {
  filter: none;
}

/* ==========================================================================
   Cases: sphere or grid (cases-sphere.js)
   ========================================================================== */

/* One wrapper round the three case sections. Its own stacking context: the
   sphere's nodes carry z-indexes by depth, and they stay in here. */
.cases {
  position: relative;
  isolation: isolate;
  scroll-margin-top: calc(var(--space-8) + var(--space-5));
}

/* No border or heading between the hero and the cases: the view switch,
   centred, is what opens them. */
.cases__bar {
  position: relative;
  z-index: 1;
  display: flex;
  justify-content: center;
}

.cases__views {
  display: flex;
  border: 1px solid var(--color-border);
  background: var(--color-bg);
}

.cases__view {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
  cursor: pointer;
  transition: color 0.2s ease, background-color 0.2s ease;
}

.cases__view + .cases__view {
  border-left: 1px solid var(--color-border);
}

.cases__view svg {
  width: 16px;
  height: 16px;
  flex: none;
}

.cases__view:hover {
  color: var(--color-text);
}

.cases__view[aria-pressed="true"] {
  background: var(--color-accent-tint);
  color: var(--color-text);
  font-weight: 600;
}

:root:not([data-cases-view]) :is(.cases__bar, .cases__sphere, .cases__pager),
:root:not([data-cases-view="carousel"]) .cases__pager {
  display: none;
}

/* Locked cases (a password or a request): hovering the mockup says so, with
   a lock over a frosted picture. Same in the grid, the open card and the
   sphere's thumbnails. */
.project-card__lock {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--color-bg) 55%, transparent);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  color: var(--color-text);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}

.project-card__lock svg {
  width: 40px;
  height: 40px;
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  background: var(--color-bg);
}

.sphere-case__thumb .project-card__lock svg {
  width: 28px;
  height: 28px;
  padding: 5px;
}

.project-card__media:hover .project-card__lock,
.sphere-case:hover .project-card__lock,
.sphere-case:focus-visible .project-card__lock {
  opacity: 1;
}

/* Above the lock: the card's figure stays readable. */
.project-card__tab {
  z-index: 2;
}

/* ---- Both views ----
   The three sections fold into one stage: no borders or headings of their
   own, and their grids out of the way of the cards. */

:root[data-cases-view] .cases {
  padding-bottom: var(--space-7);
}

:root[data-cases-view] .cases > .projects {
  border-top: none;
  padding-bottom: 0;
}

:root[data-cases-view] .cases .projects h2,
:root[data-cases-view="sphere"] .cases .project-card:not(.is-focused) {
  display: none;
}

:root[data-cases-view] .cases .projects__grid {
  display: block;
  margin: 0;
  padding: 0;
  overflow: visible;
}

/* ---- Sphere view ---- */

.cases__sphere {
  position: relative;
  height: min(86vw, 420px);
  margin-top: var(--space-4);
  overflow: hidden;
  /* Sideways drags turn the sphere; up and down still scroll the page. */
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
  cursor: grab;
}

.cases__sphere.is-dragging {
  cursor: grabbing;
}

/* The sides go soft as well as the back: a band down each edge blurs what
   passes under it, stronger towards the edge. Off while a card is open, so
   its thoughts stay sharp wherever they gather, and while the cases line up
   across the whole width. */
:root[data-cases-view="sphere"] .cases__sphere:not(.skel)::before,
:root[data-cases-view="sphere"] .cases__sphere:not(.skel)::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 600;
  width: 12%;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  pointer-events: none;
  transition: opacity 0.3s ease;
}

:root[data-cases-view="sphere"] .cases__sphere:not(.skel)::before {
  left: 0;
  -webkit-mask-image: linear-gradient(to right, #000, transparent);
  mask-image: linear-gradient(to right, #000, transparent);
}

:root[data-cases-view="sphere"] .cases__sphere:not(.skel)::after {
  right: 0;
  -webkit-mask-image: linear-gradient(to left, #000, transparent);
  mask-image: linear-gradient(to left, #000, transparent);
}

.cases:is(.has-focus, .is-tense) .cases__sphere:not(.skel)::before,
.cases:is(.has-focus, .is-tense) .cases__sphere:not(.skel)::after {
  opacity: 0;
}

@media (min-width: 768px) {
  .cases__sphere {
    height: clamp(540px, 74vh, 660px);
  }
}

.cases__synapses {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.sphere-shell {
  fill: none;
  stroke: var(--color-border);
  stroke-width: 1;
}

.sphere-shell--back {
  stroke-dasharray: 2 5;
}

/* Solid strokes, never alpha: a hairline by default, ink for the near half
   (cases-sphere.js), the accent when lit. */
.sphere-synapse {
  stroke: var(--color-border);
  stroke-width: 1;
}

.sphere-synapse.is-near {
  stroke: color-mix(in oklab, var(--color-text-secondary) 40%, var(--color-bg));
}

.sphere-synapse.is-on {
  stroke: var(--color-accent);
  stroke-width: 1.25;
}

.sphere-pulse {
  fill: var(--color-accent);
}

/* Every node is placed by the script: translate3d to its projected point,
   centred on it, scaled by depth. */
.sphere-case,
.sphere-thought,
.sphere-label {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform, opacity;
}

/* A case: a small card of its own, the mockup over its name and its one
   figure, so it never reads as one more thought. */
.sphere-case {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  width: 152px;
  padding: 0 0 var(--space-2);
  border: 1px solid var(--color-border);
  background: var(--color-bg-card-header);
  color: var(--color-text);
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

.sphere-case__thumb {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  margin-bottom: var(--space-1);
  overflow: hidden;
  background: var(--color-bg-card-media);
}

.sphere-case__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.sphere-case__name,
.sphere-case__stat {
  padding: 0 var(--space-3);
}

.sphere-case__name {
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.2;
}

.sphere-case__stat {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: var(--fs-small);
  line-height: 1.3;
  color: var(--color-text-secondary);
}

.sphere-case.is-lit {
  border-color: var(--color-accent);
}

/* Phones: a smaller card, name only; the open card below has the rest. */
@media (max-width: 767.98px) {
  .sphere-case {
    width: 96px;
    padding-bottom: var(--space-1);
  }

  .sphere-case__name {
    padding: 0 var(--space-2);
    font-size: var(--fs-small);
  }

  .sphere-case__stat {
    display: none;
  }
}

.sphere-case[aria-expanded="true"] {
  border-color: transparent;
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

.sphere-case[aria-expanded="true"] .sphere-case__stat {
  color: inherit;
}

/* Round the back, behind the others: seen, not clicked. The keyboard still
   reaches them, and focusing one turns it to the front. */
.sphere-case.is-back {
  pointer-events: none;
}

/* A thought: a mono tag with its kind as a glyph. Hollow square a problem,
   filled one an insight, diamond a method, filled diamond a number, a bar
   the context. */
.sphere-thought {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 6px 2px 4px;
  border: 1px solid transparent;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 11px;
  line-height: 1.3;
  color: var(--color-text-secondary);
  white-space: nowrap;
  cursor: default;
  transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
}

.sphere-thought:not(.is-far) {
  background: var(--color-bg);
}

.sphere-thought::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  border: 1px solid currentColor;
}

.sphere-thought[data-kind="insight"]::before {
  background: currentColor;
}

.sphere-thought[data-kind="method"]::before,
.sphere-thought[data-kind="metric"]::before {
  rotate: 45deg;
}

.sphere-thought[data-kind="metric"]::before {
  background: currentColor;
}

.sphere-thought[data-kind="context"]::before {
  height: 0;
  border-width: 1px 0 0;
}

.sphere-thought.is-on {
  border-color: var(--color-border);
  background: var(--color-bg);
  color: var(--color-text);
}

.sphere-thought.is-on::before {
  color: var(--color-accent);
}

/* Round the back, and on phones unless it's the open case's, a thought is
   only its glyph: there is no room for every label (cases-sphere.js). */
.sphere-thought.is-far {
  gap: 0;
  padding: 0;
  font-size: 0;
}

.sphere-label {
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
  white-space: nowrap;
  pointer-events: none;
}

/* The focused case's own card: under the stage on a phone, over its centre
   from 768px, with the thoughts gathered round it. `translate`, not
   transform, so the card's hover lift still applies on top. */
:root[data-cases-view="sphere"] .cases .project-card.is-focused {
  border-color: var(--color-border);
  background: var(--color-bg);
  animation: sphere-card-in 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}

:root[data-cases-view="sphere"] .cases .project-card.is-focused .project-card__request {
  position: static;
  align-self: flex-start;
  margin-top: var(--space-2);
  transform: none;
  opacity: 1;
  pointer-events: auto;
  border-color: transparent;
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

@media (min-width: 768px) {
  :root[data-cases-view="sphere"] .cases .project-card.is-focused {
    position: absolute;
    top: var(--sphere-cy, 50%);
    left: 50%;
    z-index: 350;
    width: min(340px, 50%);
    translate: -50% -50%;
    box-shadow: 0 18px 48px rgb(var(--shadow-rgb) / 0.12);
  }
}

/* On a phone a tapped case opens in the sheet below, never under the
   stage: a case the agent or the nav focuses only lights up. */
@media (max-width: 767.98px) {
  :root[data-cases-view="sphere"] .cases .project-card.is-focused {
    display: none;
  }
}

/* ---- Phone sheet (cases-sphere.js) ----
   A dialog from the bottom holding the tapped case's real card, cut down
   to its mockup, its words and its link across the whole width. Rises in on opening, sinks on closing, and follows
   the finger while pulled down (the inline translate). */
.cases-sheet {
  position: fixed;
  inset: auto 0 0;
  width: 100%;
  max-width: none;
  max-height: 88dvh;
  margin: 0;
  padding: 0 0 env(safe-area-inset-bottom);
  border: 1px solid var(--color-border);
  border-bottom: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: var(--color-bg);
  color: var(--color-text);
  overflow-y: auto;
  overscroll-behavior: contain;
  translate: 0 100%;
  transition: translate 0.4s cubic-bezier(0.16, 1, 0.3, 1),
              overlay 0.4s ease allow-discrete,
              display 0.4s ease allow-discrete;
}

.cases-sheet[open] {
  translate: 0 0;
}

@starting-style {
  .cases-sheet[open] {
    translate: 0 100%;
  }
}

/* A lighter scrim than the stage modals': the page stays in sight behind,
   blurred. Faded in and out with the sheet. */
.cases-sheet::backdrop {
  background: rgba(255, 255, 255, 0.36);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  transition: opacity 0.3s ease,
              overlay 0.4s ease allow-discrete,
              display 0.4s ease allow-discrete;
}

:root[data-theme="dark"] .cases-sheet::backdrop {
  background: rgba(0, 0, 0, 0.42);
}

.cases-sheet[open]::backdrop {
  opacity: 1;
}

@starting-style {
  .cases-sheet[open]::backdrop {
    opacity: 0;
  }
}

/* Its own bar on top, the grip in the middle and the close button at the
   right, so neither ever sits over the mockup. Stays put while the card
   scrolls under it. */
.cases-sheet__bar {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  height: 52px;
  padding: 0 var(--space-3);
  background: var(--color-bg);
}

.cases-sheet__grip {
  position: absolute;
  top: var(--space-2);
  left: 50%;
  width: 36px;
  height: 4px;
  translate: -50% 0;
  border-radius: 999px;
  background: var(--color-border);
}

.cases-sheet__close {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--color-text-secondary);
  cursor: pointer;
}

.cases-sheet__close svg {
  width: 20px;
  height: 20px;
}

.cases-sheet__body {
  padding: 0 var(--space-5) var(--space-5);
}

/* The card, laid flat into the sheet: no frame, no glass, no lift. */
.cases-sheet .project-card {
  height: auto;
  gap: var(--space-4);
  border: 0;
  background: transparent;
  transform: none;
  cursor: default;
}

/* The mockup inside the margins, its corners rounded, without its figure
   or its lock: only the picture. */
.cases-sheet .project-card__media {
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-bg-card-media);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

.cases-sheet :is(.project-card__tab, .project-card__lock) {
  display: none;
}

.cases-sheet .project-card__header {
  padding: 0;
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* The link, the whole width under the words. */
.cases-sheet .project-card__request {
  position: static;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 48px;
  margin-top: var(--space-3);
  transform: none;
  opacity: 1;
  pointer-events: auto;
  border-color: transparent;
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
  font-size: var(--fs-body);
  text-align: center;
  white-space: normal;
}

/* ---- Carousel view ----
   The same cards, laid side by side by cases-sphere.js: each one's 3D turn
   is an inline transform, eased here. The stage only holds their height. */

:root[data-cases-view="carousel"] .cases {
  overflow: clip;
}

:root[data-cases-view="carousel"] :is(.sphere-case, .sphere-thought, .sphere-label, .cases__synapses) {
  display: none;
}

:root[data-cases-view="carousel"] .cases__sphere {
  height: 560px; /* until the script measures the cards */
  cursor: default;
}

:root[data-cases-view="carousel"] .cases .project-card {
  position: absolute;
  top: var(--deck-top, 0px);
  right: 0;
  left: 0;
  width: min(380px, 78%);
  margin: 0 auto;
  border-color: var(--color-border);
  background: var(--color-bg);
  box-shadow: 0 18px 48px rgb(var(--shadow-rgb) / 0.12);
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
  transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1), filter 0.6s ease;
}

/* Grabbable at the sides; the front card is still a link. Mid-drag the
   row follows the pointer with no easing at all. */
:root[data-cases-view="carousel"] :is(.cases__sphere, .cases .project-card:not(.is-current)) {
  cursor: grab;
}

:root[data-cases-view="carousel"] .cases.is-deck-dragging :is(.cases__sphere, .project-card) {
  cursor: grabbing;
}

:root[data-cases-view="carousel"] .cases.is-deck-dragging .project-card {
  transition: none;
}

:root[data-cases-view="carousel"] .cases .project-card img {
  -webkit-user-drag: none;
}

:root[data-cases-view="carousel"] .cases .project-card.is-away {
  visibility: hidden;
}

:root[data-cases-view="carousel"] .cases .project-card.is-current .project-card__request {
  position: static;
  align-self: flex-start;
  margin-top: var(--space-2);
  transform: none;
  opacity: 1;
  pointer-events: auto;
  border-color: transparent;
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

.cases__pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
}

.cases__step {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  cursor: pointer;
  transition: border-color 0.2s ease;
}

.cases__step:hover {
  border-color: var(--color-text);
}

.cases__step svg {
  width: 20px;
  height: 20px;
}

.cases__pager-label {
  min-width: 16ch;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 0.75rem;
  text-align: center;
  color: var(--color-text-secondary);
}

@keyframes sphere-card-in {
  from {
    opacity: 0;
    scale: 0.94;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cases__view,
  .cases__step,
  .sphere-case,
  .sphere-thought,
  .project-card__lock,
  .cases-sheet,
  .cases-sheet::backdrop,
  :root[data-cases-view="carousel"] .cases .project-card {
    transition: none;
  }

  :root[data-cases-view="sphere"] .cases .project-card.is-focused {
    animation: none;
  }
}

/* ==========================================================================
   Design principles
   ========================================================================== */

/* Each a card of the home (see .main:has(> .hero)): one centred column,
   inset like the page, the card's edge for a frame. */
.principles,
.method {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

.principles {
  overflow-x: clip; /* cards fly past the card's edge on swipe */
}

.principles > .section-inner,
.method > .section-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  max-width: none;
  padding: 0 var(--page-padding) var(--space-8);
  text-align: center;
}

.principles .principle-card {
  text-align: left;
}

.principles__heading {
  font-size: var(--fs-h2);
  text-wrap: balance;
  padding-bottom: var(--space-3);
}

.principles__intro {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
  max-width: 30rem;
  padding-bottom: var(--space-6);
}

.principles__game {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
  max-width: 30rem;
}

/* Every card sits in the same grid cell, so the deck is as tall as the
   tallest card and nothing needs a hard-coded height. The extra bottom
   padding is room for the cards peeking out behind the top one. */
.principles__deck {
  position: relative;
  display: grid;
  width: min(100%, 22rem); /* sticky-note size: square cards get too big at the full column width */
  padding-bottom: calc(var(--space-2) * 2);
}

.principle-card,
.principles__end {
  grid-area: 1 / 1;
}

/* Each card is a sticky note: paper-yellow in both themes (a touch
   dimmer at night), with its own resting tilt and a shadow that lifts at the
   bottom edge. The paper is always light, so it carries its own ink colours
   instead of the page's text tokens. */
.principle-card {
  --depth: 0;
  --drag-x: 0px;
  --drag-y: 0px;
  --drag-r: 0deg;
  --tilt: 0deg;
  --note-paper: #fbee9c;
  --note-ink: #2b2819;
  --note-ink-soft: rgb(43 40 25 / 0.72);
  --note-accent: color-mix(in oklab, var(--color-accent) 70%, #000);
  position: relative;
  z-index: calc(10 - var(--depth));
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-5);
  border-radius: 2px;
  color: var(--note-ink);
  /* The darker band along the top is the glued strip. */
  background:
    linear-gradient(to bottom, rgb(0 0 0 / 0.05), transparent 18%),
    var(--note-paper);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.08),
              0 18px 24px -14px rgb(0 0 0 / 0.45);
  transform-origin: 50% 100%;
  transform: translate(var(--drag-x), calc(var(--depth) * var(--space-2) + var(--drag-y)))
             rotate(calc(var(--tilt) + var(--drag-r)))
             scale(calc(1 - var(--depth) * 0.04));
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1),
              opacity 0.35s ease;
}

.principle-card:nth-child(4n + 1) { --tilt: -1.5deg; }
.principle-card:nth-child(4n + 2) { --tilt: 1.2deg; }
.principle-card:nth-child(4n + 3) { --tilt: -0.6deg; }
.principle-card:nth-child(4n + 4) { --tilt: 1.8deg; }

:root[data-theme="dark"] .principle-card {
  --note-paper: #e6d98b;
}

.principle-card[data-depth="0"] {
  cursor: grab;
}

.principle-card.is-dragging {
  cursor: grabbing;
  transition: none;
}

/* Only the top card and the two below it are visible; the rest wait. */
.principle-card[data-depth="3"],
.principle-card.is-gone {
  opacity: 0;
  pointer-events: none;
}

.principle-card:not([data-depth="0"]) {
  pointer-events: none;
}

.principle-card:not([data-depth="0"]) > * {
  opacity: 0.35;
}

.principle-card.is-out-right {
  --drag-x: 130%;
  --drag-r: 14deg;
  opacity: 0;
}

.principle-card.is-out-left {
  --drag-x: -130%;
  --drag-r: -14deg;
  opacity: 0;
}

.principle-card.is-out-up {
  --drag-y: -40%;
  opacity: 0;
}

.principle-card__stamp {
  position: absolute;
  top: var(--space-5);
  padding: var(--space-1) var(--space-3);
  border: 2px solid currentColor;
  border-radius: var(--radius);
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0;
  pointer-events: none;
}

.principle-card .principle-card__stamp {
  opacity: var(--stamp, 0);
}

.principle-card__stamp--yes {
  --stamp: var(--stamp-yes, 0);
  right: var(--space-5);
  color: var(--note-accent);
  transform: rotate(8deg);
}

.principle-card__stamp--no {
  --stamp: var(--stamp-no, 0);
  right: var(--space-5);
  color: var(--note-ink-soft);
  transform: rotate(-8deg);
}

.principle__number {
  color: var(--note-ink);
  font-weight: 600;
  font-size: var(--fs-body);
}

.principle__title {
  padding-right: var(--space-8); /* clear of the stamps */
}

.principle__desc {
  color: var(--note-ink-soft);
  font-size: var(--fs-small);
}

.principles__end:not([hidden]) {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  padding: var(--space-6) var(--space-5);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-lg);
  text-align: center;
}

.principles__end .access-modal__submit {
  align-self: center;
}

.principles__end-text {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
}

/* The match modal's heart, left behind as a shadow of it: muted and without
   the glow or the beat. */
.principles__echo:not([hidden]) {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  opacity: 0.35;
}

.principles__echo-icon {
  display: flex;
  width: 72px;
  height: 72px;
  margin-bottom: var(--space-2);
  color: var(--color-accent);
}

.principles__echo-icon svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}


.principles__actions {
  display: flex;
  align-items: center;
  gap: var(--space-5);
}

.deck-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-bg-card-media);
  color: var(--color-text-secondary);
  cursor: pointer;
  box-shadow: 0 4px 14px rgb(var(--shadow-rgb) / 0.08);
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1),
              background-color 0.25s ease,
              color 0.25s ease,
              opacity 0.25s ease;
}

.deck-btn svg {
  width: 22px;
  height: 22px;
}

.deck-btn:hover {
  transform: translateY(-2px);
  background: var(--color-bg-card-header);
  color: var(--color-text);
}

.deck-btn:active {
  transform: scale(0.94);
}

/* Skip is the non-decision, so it drops the round chrome and reads as a plain
   text link between the two real choices. */
.deck-btn--skip {
  width: auto;
  height: auto;
  padding: var(--space-2);
  border: none;
  background: none;
  box-shadow: none;
  font-size: var(--fs-small);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.deck-btn--skip:hover {
  transform: none;
  background: none;
}

.deck-btn--accept {
  width: 60px;
  height: 60px;
  color: var(--color-accent);
  border-color: color-mix(in oklab, var(--color-accent) 45%, var(--color-border));
}

.deck-btn--accept:hover {
  color: var(--color-accent);
}

.deck-btn--accept svg {
  width: 26px;
  height: 26px;
}

.deck-btn[disabled] {
  opacity: 0.4;
  cursor: default;
  transform: none;
}


/* ---- Match modal: the .stage-modal base plus confetti and a beating heart ---- */
.match-modal__confetti {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

/* Pieces are spawned by principles-deck.js with per-piece custom props, once
   the heart is built. The colour is always a tint/shade of the accent, so it
   follows the theme. */
.match-modal__confetti i {
  position: absolute;
  top: -24px;
  left: var(--x);
  width: var(--w);
  height: var(--h);
  --c: color-mix(in oklch, var(--color-accent) var(--mix), var(--toward));
  border: 1.5px solid var(--c);
  border-radius: var(--r);
  /* Outlined and hatched, like the heart. */
  background: repeating-linear-gradient(-45deg, var(--c) 0 1.5px, transparent 1.5px 5px);
  opacity: 0;
  animation: confetti-fall var(--dur) cubic-bezier(0.25, 0.6, 0.5, 1) var(--delay) forwards;
}

@keyframes confetti-fall {
  0%   { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  85%  { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--drift), 110vh) rotate(var(--rot)); }
}

/* The heart beats once it is put together (stage-build.js). */
.stage-modal.match-modal.is-built .stage-modal__icon {
  animation: heartbeat 1.2s ease-in-out infinite;
}

@keyframes heartbeat {
  0%, 40%, 100% { transform: scale(1); }
  12%           { transform: scale(1.14); }
  26%           { transform: scale(1.06); }
}

/* Swipe hint: on hovering the section the top card leans right, then left,
   flashing each stamp, then settles — played once per hover until the visitor
   has made a first decision or drag. It animates the individual translate /
   rotate properties so it composes with the card's own transform. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .principles:hover [data-deck]:not(.has-interacted) .principle-card[data-depth="0"] {
    animation: deck-hint 1.4s cubic-bezier(0.45, 0, 0.25, 1) 0.25s both;
  }

  .principles:hover [data-deck]:not(.has-interacted) .principle-card[data-depth="0"] .principle-card__stamp--yes {
    animation: deck-hint-yes 1.4s ease 0.25s both;
  }

  .principles:hover [data-deck]:not(.has-interacted) .principle-card[data-depth="0"] .principle-card__stamp--no {
    animation: deck-hint-no 1.4s ease 0.25s both;
  }
}

@keyframes deck-hint {
  0%, 100% { translate: 0; rotate: 0deg; }
  22%      { translate: 26px 0; rotate: 3deg; }
  44%      { translate: 0; rotate: 0deg; }
  66%      { translate: -26px 0; rotate: -3deg; }
  88%      { translate: 0; rotate: 0deg; }
}

@keyframes deck-hint-yes {
  0%, 44%, 100% { opacity: 0; }
  22%           { opacity: 0.7; }
}

@keyframes deck-hint-no {
  0%, 44%, 88%, 100% { opacity: 0; }
  66%                { opacity: 0.7; }
}

@media (prefers-reduced-motion: reduce) {
  .principle-card,
  .deck-btn {
    transition: none;
  }

  .deck-btn:hover,
  .deck-btn:active {
    transform: none;
  }

  .match-modal__confetti {
    display: none;
  }
}

/* ==========================================================================
   Method
   ========================================================================== */

.method__heading {
  font-size: var(--fs-h2);
  text-wrap: balance;
  padding-bottom: var(--space-3);
}

.method__tagline {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
  max-width: 30rem;
  padding-bottom: var(--space-6);
}

/* ---- The loop ----
   Mobile first: a vertical 240×400 drawing, problem on top. From 768px up
   the horizontal 400×240 one takes over (see the tablet block), which is the
   same drawing with x and y swapped, so every position below just swaps its
   two percentages there. The pills sit on the lobes' side extremes (12.5% /
   87.5% across, 19.4% / 80.6% down); each lobe's name and its two notes
   share one spot in the belly of that lobe. */
.method__graph {
  --pill-overlap: 0.625rem; /* how far a pill reaches back over its lobe */
  display: flex;
  justify-content: center;
  width: 100%;
  padding: var(--space-3) 0;
}

/* As wide as the column allows once the side pills (about 3.5rem each way)
   have their room. */
.method__loop {
  position: relative;
  width: min(14rem, calc(100% - 7rem));
}

.method__infinity {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.method__infinity--h {
  display: none;
}

/* Filled with the card-header tone: a step off the page, not the accent. */
.method__lobe {
  fill: var(--color-accent-tint);
  fill-opacity: 0;
  transition: fill-opacity 0.3s ease;
}

.method__track,
.method__chevron {
  fill: none;
  stroke: var(--color-text);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.method__chevron {
  stroke: var(--color-accent-fill);
  stroke-width: 2;
}

/* Build · Measure · Learn, written along the stroke: a quiet aside, the
   card-header tone taken 30% towards the text so it still reads. The size
   is in the drawing's own units, so it scales with the loop. */
.method__lean {
  font-family: "Inter", sans-serif;
  font-style: italic;
  font-size: 11px; /* ≈ 16px on screen, the lobe names' size (--fs-h3) */
  font-weight: 500;
  fill: color-mix(in oklab, var(--color-bg-card-header), var(--color-text) 30%);
}

/* The vertical drawing renders smaller on a phone, so its words take more
   units for the same ≈ 16px on screen. */
.method__infinity--v .method__lean {
  font-size: 18px;
}

/* The hero pill, as a button, with the chevrons' accent on its edge. It
   reaches outwards from the lobe rather than straddling it, so it never
   covers the lobe's text. */
.method__pill {
  z-index: 1;
  padding: var(--space-1) var(--space-3);
  border-color: var(--color-accent-fill);
  color: var(--color-text);
  font-family: inherit;
  line-height: inherit;
  cursor: pointer;
  transition: background-color 0.3s ease, color 0.3s ease;
}

.method__pill--discover { --x: 12.5%; --y: 19.4%; }
.method__pill--define   { --x: 87.5%; --y: 19.4%; }
.method__pill--develop  { --x: 12.5%; --y: 80.6%; }
.method__pill--deliver  { --x: 87.5%; --y: 80.6%; }

/* Each lobe's first phase reaches out on one side, its second on the other. */
.method__pill--discover,
.method__pill--develop {
  transform: translate(calc(-100% + var(--pill-overlap)), -50%);
}

.method__pill--define,
.method__pill--deliver {
  transform: translate(calc(-1 * var(--pill-overlap)), -50%);
}

/* ---- Inside the lobes: the lobe's name at rest, a phase's note when active ---- */
.method__space,
.method__note {
  position: absolute;
  left: 50%;
  width: 62%;
  transform: translate(-50%, -50%);
  text-align: center;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

.method__space--problem,
.method__note--discover,
.method__note--define {
  top: 23%;
}

.method__space--solution,
.method__note--develop,
.method__note--deliver {
  top: 77%;
}

/* The same h3 the principle cards use. */
.method__space {
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.15;
}

.method__note {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: 0.75rem;
  line-height: 1.35;
  visibility: hidden;
  opacity: 0;
}

.method__tool {
  font-weight: 600;
}

/* The phase's tools, as brand marks in the text colour. Tablet and up only:
   on a phone the lobe has no height left for another row. */
.method__stack {
  display: none;
  justify-content: center;
  gap: var(--space-2);
  list-style: none;
  margin: var(--space-1) 0 0;
  padding: 0;
}

.method__stack svg {
  display: block;
  width: 1rem;
  height: 1rem;
}

.method__quote {
  font-family: "Inter", sans-serif;
  font-style: italic;
  color: var(--color-text-secondary);
}

/* ---- Fran and AI, lapping the loop ----
   The hero's cursor, arrow and badge; method-loop.js moves --x / --y along
   the visible track. They step out of the way while a note is showing. */
.method__cursor {
  z-index: 2;
  transition: opacity 0.3s ease;
}

@media (hover: hover) {
  .method__graph:has(.method__pill:hover) .method__cursor {
    opacity: 0;
  }
}

.method__graph[data-active] .method__cursor {
  opacity: 0;
}

/* ---- Product vision: the loop as a pair of glasses ----
   Hovering the bridge (desktop only) clears the figure to look through the
   lenses; method-loop.js toggles .is-vision on the graph while it does. */
/* Rounded like the pills, but in the section labels' hairline and upright
   type, so it reads as the frame's bridge rather than another phase. While
   the vision shows, it inverts. */
.method__bridge {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 3;
  display: none;
  transform: translate(-50%, -50%);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-bg);
  color: var(--color-text);
  font-size: var(--fs-small);
  white-space: nowrap;
  cursor: default;
  user-select: none;
  -webkit-user-select: none;
  transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}

.method__bridge:hover {
  background: var(--color-bg-card-header);
}

@media (min-width: 768px) and (hover: hover) and (pointer: fine) {
  .method__bridge {
    display: inline-block;
  }
}

.method__bridge-vision {
  display: none;
}

.method__graph.is-vision .method__bridge {
  border-color: var(--color-text);
  background: var(--color-text);
  color: var(--color-on-inverse);
}

.method__graph.is-vision .method__bridge-sense {
  display: none;
}

.method__graph.is-vision .method__bridge-vision {
  display: inline;
}

/* The landscape: a flat editorial illustration, the same in both themes, so
   its palette is fixed. Only the sky follows the visitor's colour: the
   bright accent (--accent-fill) taken a step darker. */
.method__vision {
  --vision-bg: color-mix(in oklab, var(--accent-fill) 78%, #1a1a1a);
  --vision-bg-shade: color-mix(in oklab, var(--vision-bg) 72%, #000000);
  --vision-cream: #ddd5c8;
  --vision-tan: #c4ad8f;
  --vision-orange: #f5a623;
  --vision-blue: #5aa9f5;
  --vision-blue-shade: #3d86d4;
  --vision-brown: #4a2c14;
  --vision-ink: #111111;
  opacity: 0;
  transform-box: view-box;
  transform-origin: center;
  transform: scale(1.06);
  transition: opacity 0.4s ease, transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

.method__sky {
  fill: var(--vision-bg);
}

.method__cloud {
  fill: var(--vision-bg-shade);
}

/* The thin ink line round every solid shape; the hard black drop under them
   is the #method-hard-shadow filter in the markup. */
.method__ink {
  stroke: var(--vision-ink);
  stroke-width: 1.25;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.method__paint--cream { fill: var(--vision-cream); }
.method__paint--tan { fill: var(--vision-tan); }
.method__paint--orange { fill: var(--vision-orange); }
.method__paint--blue { fill: var(--vision-blue); }
.method__paint--blue-shade { fill: var(--vision-blue-shade); }
.method__paint--brown { fill: var(--vision-brown); }

.method__tower-bands {
  fill: none;
}

.method__window {
  fill: var(--vision-ink);
}

/* The problem lens is still in the mist; the solution one sees clear to the
   star. */
.method__mist {
  fill: var(--vision-cream);
  fill-opacity: 0.55;
}

.method__graph.is-vision .method__vision {
  opacity: 1;
  transform: none;
}

/* Everything but the frame steps aside. */
.method__graph.is-vision .method__pill,
.method__graph.is-vision .method__space,
.method__graph.is-vision .method__note,
.method__graph.is-vision .method__cursor,
.method__graph.is-vision .method__lean,
.method__graph.is-vision .method__chevron,
.method__graph.is-vision .method__lobe {
  opacity: 0;
  pointer-events: none;
}

.method__lean,
.method__chevron,
.method__track {
  transition: opacity 0.3s ease, stroke-width 0.3s ease;
}

/* The loop's stroke becomes the frame of the glasses. */
.method__graph.is-vision .method__track {
  stroke-width: 3;
}

/* ---- Active phase ----
   With a mouse, hovering a pill previews its phase and wins over the pinned
   one. On touch the tapped pill keeps a sticky :hover, so there the pinned
   phase (method-loop.js) is the only source. */
@media (hover: hover) {
  .method__graph:has(.method__pill--discover:hover) .method__lobe--problem {
    fill-opacity: 1;
  }

  .method__graph:has(.method__pill--discover:hover) .method__space--problem {
    opacity: 0;
  }

  .method__graph:has(.method__pill--discover:hover) .method__note--discover {
    visibility: visible;
    opacity: 1;
  }

  .method__graph:has(.method__pill--discover:hover) .method__pill--discover {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }

  .method__graph:has(.method__pill--define:hover) .method__lobe--problem {
    fill-opacity: 1;
  }

  .method__graph:has(.method__pill--define:hover) .method__space--problem {
    opacity: 0;
  }

  .method__graph:has(.method__pill--define:hover) .method__note--define {
    visibility: visible;
    opacity: 1;
  }

  .method__graph:has(.method__pill--define:hover) .method__pill--define {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }

  .method__graph:has(.method__pill--develop:hover) .method__lobe--solution {
    fill-opacity: 1;
  }

  .method__graph:has(.method__pill--develop:hover) .method__space--solution {
    opacity: 0;
  }

  .method__graph:has(.method__pill--develop:hover) .method__note--develop {
    visibility: visible;
    opacity: 1;
  }

  .method__graph:has(.method__pill--develop:hover) .method__pill--develop {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }

  .method__graph:has(.method__pill--deliver:hover) .method__lobe--solution {
    fill-opacity: 1;
  }

  .method__graph:has(.method__pill--deliver:hover) .method__space--solution {
    opacity: 0;
  }

  .method__graph:has(.method__pill--deliver:hover) .method__note--deliver {
    visibility: visible;
    opacity: 1;
  }

  .method__graph:has(.method__pill--deliver:hover) .method__pill--deliver {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }

  .method__graph[data-active="discover"]:not(:has(.method__pill:hover)) .method__lobe--problem {
    fill-opacity: 1;
  }

  .method__graph[data-active="discover"]:not(:has(.method__pill:hover)) .method__space--problem {
    opacity: 0;
  }

  .method__graph[data-active="discover"]:not(:has(.method__pill:hover)) .method__note--discover {
    visibility: visible;
    opacity: 1;
  }

  .method__graph[data-active="discover"]:not(:has(.method__pill:hover)) .method__pill--discover {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }

  .method__graph[data-active="define"]:not(:has(.method__pill:hover)) .method__lobe--problem {
    fill-opacity: 1;
  }

  .method__graph[data-active="define"]:not(:has(.method__pill:hover)) .method__space--problem {
    opacity: 0;
  }

  .method__graph[data-active="define"]:not(:has(.method__pill:hover)) .method__note--define {
    visibility: visible;
    opacity: 1;
  }

  .method__graph[data-active="define"]:not(:has(.method__pill:hover)) .method__pill--define {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }

  .method__graph[data-active="develop"]:not(:has(.method__pill:hover)) .method__lobe--solution {
    fill-opacity: 1;
  }

  .method__graph[data-active="develop"]:not(:has(.method__pill:hover)) .method__space--solution {
    opacity: 0;
  }

  .method__graph[data-active="develop"]:not(:has(.method__pill:hover)) .method__note--develop {
    visibility: visible;
    opacity: 1;
  }

  .method__graph[data-active="develop"]:not(:has(.method__pill:hover)) .method__pill--develop {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }

  .method__graph[data-active="deliver"]:not(:has(.method__pill:hover)) .method__lobe--solution {
    fill-opacity: 1;
  }

  .method__graph[data-active="deliver"]:not(:has(.method__pill:hover)) .method__space--solution {
    opacity: 0;
  }

  .method__graph[data-active="deliver"]:not(:has(.method__pill:hover)) .method__note--deliver {
    visibility: visible;
    opacity: 1;
  }

  .method__graph[data-active="deliver"]:not(:has(.method__pill:hover)) .method__pill--deliver {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }
}

@media (hover: none) {
  .method__graph[data-active="discover"] .method__lobe--problem {
    fill-opacity: 1;
  }

  .method__graph[data-active="discover"] .method__space--problem {
    opacity: 0;
  }

  .method__graph[data-active="discover"] .method__note--discover {
    visibility: visible;
    opacity: 1;
  }

  .method__graph[data-active="discover"] .method__pill--discover {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }

  .method__graph[data-active="define"] .method__lobe--problem {
    fill-opacity: 1;
  }

  .method__graph[data-active="define"] .method__space--problem {
    opacity: 0;
  }

  .method__graph[data-active="define"] .method__note--define {
    visibility: visible;
    opacity: 1;
  }

  .method__graph[data-active="define"] .method__pill--define {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }

  .method__graph[data-active="develop"] .method__lobe--solution {
    fill-opacity: 1;
  }

  .method__graph[data-active="develop"] .method__space--solution {
    opacity: 0;
  }

  .method__graph[data-active="develop"] .method__note--develop {
    visibility: visible;
    opacity: 1;
  }

  .method__graph[data-active="develop"] .method__pill--develop {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }

  .method__graph[data-active="deliver"] .method__lobe--solution {
    fill-opacity: 1;
  }

  .method__graph[data-active="deliver"] .method__space--solution {
    opacity: 0;
  }

  .method__graph[data-active="deliver"] .method__note--deliver {
    visibility: visible;
    opacity: 1;
  }

  .method__graph[data-active="deliver"] .method__pill--deliver {
    background: var(--color-accent-fill);
    color: var(--color-on-accent-fill);
  }
}

@media (prefers-reduced-motion: reduce) {
  .method__lobe,
  .method__pill,
  .method__space,
  .method__note,
  .method__cursor,
  .method__bridge,
  .method__vision,
  .method__lean,
  .method__chevron,
  .method__track {
    transition: none;
  }

  .method__vision {
    transform: none;
  }
}

/* ==========================================================================
   Footer
   ========================================================================== */

/* Under the cards, on the frame: no hairline of its own. */
.site-footer {
  margin-top: var(--frame-gap);
}

.site-footer__mobile-block {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  padding-top: var(--space-6);
  padding-bottom: var(--space-6);
}

.site-footer__mobile-block .brand {
  margin-bottom: var(--space-2);
}

.site-footer__back-to-top {
  display: none;
}

.site-footer__legal {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  padding-top: var(--space-5);
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

.site-footer__title {
  color: var(--color-text);
  font-weight: 600;
  margin-bottom: var(--space-1);
}

/* ==========================================================================
   Access request modal
   ========================================================================== */

/* The whole card is the trigger; the <button> inside exists only for keyboard. */
.project-card {
  position: relative;
  cursor: pointer;
  transition: border-color 0.3s ease,
              transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.project-card:hover {
  transform: translateY(-2px);
  border-color: var(--color-border);
}

/* Hidden with the same idiom as the two toggle checkboxes above: opacity 0
   keeps the button in the tab order, display:none would not. */
.project-card__request {
  position: absolute;
  left: 50%;
  top: var(--space-5);
  transform: translateX(-50%);
  z-index: 2;
  opacity: 0;
  pointer-events: none; /* the mouse uses the whole card instead */
  white-space: nowrap;
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-bg-card-media);
  font-size: var(--fs-small);
  font-weight: 500;
  transition: opacity 0.25s ease;
}

/* On tab the pill surfaces over the mockup, at the top of the card — no
   layout shift, no overlap with the header text. */
.project-card__request:focus-visible {
  opacity: 1;
}

/* …and the whole card takes the ring, so it is obvious which one is focused. */
.project-card:has(.project-card__request:focus-visible) {
  border-color: var(--color-border);
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}

/* The CV links became <button>; .text-link assumes an inline element. */
button.text-link {
  padding: 0;
  text-align: left;
  cursor: pointer;
}

/* ---- Stage modal ----
   Shared full-screen dialog (no card) for the access and match modals: a big
   icon, a title, a line of copy and one CTA, centred on a frosted scrim. Both
   sit outside .page: at >=1200px .page becomes flex-direction:row, and no
   ancestor here can ever carry a transform/filter. */
.stage-modal {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--color-text);
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.3s ease,
              overlay 0.3s ease allow-discrete,
              display 0.3s ease allow-discrete;
}

.stage-modal[open] {
  opacity: 1;
}

/* entry keyframe for the discrete display change; engines without
   @starting-style simply show the dialog instantly, which is fine */
@starting-style {
  .stage-modal[open] {
    opacity: 0;
  }
}

/* ::backdrop does not inherit custom properties from its originating element
   in older engines, hence literals. Frosted light / deep dark, dense enough
   for the text to read straight on the scrim. */
.stage-modal::backdrop {
  background: rgba(255, 255, 255, 0.72);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

:root[data-theme="dark"] .stage-modal::backdrop {
  background: rgba(0, 0, 0, 0.72);
}

.stage-modal__close {
  position: absolute;
  top: calc(var(--space-4) + env(safe-area-inset-top));
  right: calc(var(--space-4) + env(safe-area-inset-right));
  z-index: 2;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: background-color 0.25s ease, color 0.25s ease;
}

.stage-modal__close:hover {
  background: var(--color-bg-card-header);
  color: var(--color-text);
}

.stage-modal__close svg {
  width: 20px;
  height: 20px;
}

.stage-modal__stage {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-height: 100%;
  padding: var(--space-8) var(--space-4);
  text-align: center;
}

/* programmatic focus target */
.stage-modal__stage:focus {
  outline: none;
}

/* ---- Pager: the one previous / next control, shared by the carousels, the
   image zoom and the lapse. Square cells drawn with the hairline, like the
   theme strip's, boxed together and anchored to the bottom edge of whatever
   they step through, like a tag: the side on that edge has no line of its
   own. Back and forward at the ends, and in between where you are (dots, a
   count) or, in the lapse, pause. ---- */
.pager {
  display: flex;
  align-self: center;
  border: 1px solid var(--color-border);
}

.pager[hidden] {
  display: none;
}

/* Standing on the bottom of the screen (the zoom, the lapse). */
.pager--floor {
  border-bottom: 0;
  padding-bottom: env(safe-area-inset-bottom);
}

/* Hanging from the hairline above it (a carousel's track). */
.pager--hang {
  border-top: 0;
}

.pager > * + * {
  border-left: 1px solid var(--color-border);
}

.pager__btn {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  color: var(--color-text);
  cursor: pointer;
  transition: box-shadow 0.25s ease, opacity 0.2s ease;
}

.pager__btn:hover:not(:disabled) {
  box-shadow: inset 0 0 0 1px var(--color-accent);
}

.pager__btn:disabled {
  opacity: 0.35;
  cursor: default;
}

.pager__btn svg {
  width: 20px;
  height: 20px;
}

.pager__mid {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 44px;
  height: 44px;
  padding: 0 var(--space-3);
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
}

/* ---- Image zoom (image-zoom.js): the .stage-modal base, holding one image
   as large as the screen allows, scaled to fit, over a caption and the pager
   that steps through the case's images. ---- */
.js main img[data-zoom] {
  cursor: zoom-in;
}

:root:has(.zoom-modal[open]) {
  overflow: hidden;
}

.zoom-modal__stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  height: 100%;
  /* nothing below: the pager stands on the bottom edge */
  padding: calc(var(--space-8) + env(safe-area-inset-top)) var(--space-4) 0;
  cursor: zoom-out;
  /* the swipe is the script's; pinching still zooms */
  touch-action: pinch-zoom;
}

.zoom-modal__img {
  display: block;
  flex: 1;
  min-height: 0;
  width: 100%;
  object-fit: contain;
}

.zoom-modal__bar {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  max-width: 40rem;
  cursor: default;
}

.zoom-modal__caption {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  text-align: center;
  font-size: var(--fs-small);
  line-height: 1.35;
}

.zoom-modal__section {
  color: var(--color-text-secondary);
}

.zoom-modal__step {
  color: var(--color-text);
  font-size: var(--fs-body);
  font-weight: 500;
}

@media (min-width: 768px) {
  .zoom-modal__stage {
    padding: var(--space-8) var(--space-8) 0;
  }
}

/* Over the copy's frames: carried pieces pass over them, never under. */
.stage-modal__icon {
  position: relative;
  z-index: 2;
  display: flex;
  width: 160px;
  height: 160px;
  margin-bottom: var(--space-4);
  color: var(--color-accent);
}

/* ---- Stage figures: the lock and the heart (also the deck's echo) ----
   Outlines with a hatched fill, like the hero's frames. In the modals each
   piece is its own full-size <svg>, stacked, so stage-build.js can carry it
   in with translate / rotate. While the figure is being built the outlines
   are the frames' grey dashes; they turn solid accent as the frames
   dissolve. The keyhole and the spark are small solid accents. */
.stage-figure__defs {
  position: absolute;
  width: 0;
  height: 0;
}

/* The access modal carries a lock and a clock; it shows the one it builds. */
.stage-modal[data-stage-build="lock"] .stage-figure[data-stage-figure="clock"],
.stage-modal[data-stage-build="clock"] .stage-figure[data-stage-figure="lock"] {
  display: none;
}

.stage-figure__piece {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.stage-figure svg > :is(path, rect) {
  stroke: currentColor;
  stroke-width: 2;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  transition: stroke 0.45s ease, fill 0.45s ease;
}

.stage-figure__piece--solid path {
  fill: currentColor;
}

.stage-figure__hatch rect {
  fill: color-mix(in oklab, var(--color-accent) 55%, transparent);
  transition: fill 0.45s ease;
}

.stage-modal.is-building:not(.is-resolved) .stage-figure svg > :is(path, rect) {
  stroke: var(--hatch-line);
  stroke-dasharray: 5 4;
}

.stage-modal.is-building:not(.is-resolved) .stage-figure__piece--solid path {
  fill: var(--hatch-line);
}

.stage-modal.is-building:not(.is-resolved) .stage-figure__hatch rect {
  fill: color-mix(in oklab, var(--color-text-secondary) 28%, transparent);
}

/* Fran and AI, over everything in the stage; placed by stage-build.js. */
.stage-modal__cursors {
  position: absolute;
  inset: 0;
  z-index: 3;
  overflow: clip;
  pointer-events: none;
}

.stage-modal__cursor {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  will-change: transform;
  transition: opacity 0.3s ease;
}

.stage-modal__cursor.is-ready {
  opacity: 1;
}

.stage-modal__title {
  font-size: var(--fs-h1);
}

.stage-modal__desc {
  color: var(--color-text-secondary);
  max-width: 32ch;
}

/* The modals' CTA (also the chat's LinkedIn card, a size down): three blocks in a row, my photo · the label · LinkedIn,
   each split from the next by a full-height line, like the top bar's.
   Outlined, on the near-white card surface by day (the card surface by
   night). --cta is the row height and the side of each square block. */
.stage-modal__cta {
  --cta: 48px;
  display: inline-flex;
  align-items: stretch;
  margin-top: var(--space-4);
  border: 1px solid var(--color-text);
  background: var(--color-bg-card-media);
  color: var(--color-text);
  font-size: var(--fs-small);
  font-weight: 600;
  transition: background-color 0.25s ease;
}

.stage-modal__cta:hover {
  background: var(--color-bg-card-header);
}

.stage-modal__cta-box {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--cta);
  height: var(--cta);
}

.stage-modal__cta-label {
  display: flex;
  align-items: center;
  padding: 0 var(--space-5);
  border-inline: 1px solid var(--color-text);
}

/* The photo and the LinkedIn mark: the same round, with air around it. */
.stage-modal__cta-mark {
  width: calc(var(--cta) - 18px);
  height: calc(var(--cta) - 18px);
  border-radius: 50%;
}

.stage-modal__cta-mark--linkedin {
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0a66c2; /* LinkedIn brand blue, fixed across themes */
  color: #fff;
}

.stage-modal__cta-mark--linkedin svg {
  width: 18px;
  height: 18px;
}

/* The square button, also used outside the modals (the deck's "Start over",
   the chat's LinkedIn card). */
.access-modal__submit {
  align-self: flex-start;
  padding: var(--space-3) var(--space-6);
  border-radius: 0;
  background: var(--color-text);
  color: var(--color-on-inverse);
  font-size: var(--fs-small);
  font-weight: 600;
  cursor: pointer;
  transition: opacity 0.25s ease;
}

.access-modal__submit:hover {
  opacity: 0.88;
}

.access-modal__submit--ghost {
  background: none;
  color: var(--color-text);
  border: 1px solid var(--color-border);
  font-weight: 500;
}

/* ---- Password modal (password-modal.js): the .stage-modal base, with the
   field and its button in one outlined row, the CTA's height and line. It
   stacks on narrow screens. It shows up once the lock is built: hidden at
   once when the build starts (a transition there would fade it out from
   view), faded in when it resolves. Scrolls when the keyboard leaves too
   little room (password-modal.js fits it to what the keyboard leaves). ---- */
.password-modal {
  overflow-y: auto;
}

.password-modal__form {
  --agent-pad: var(--space-1);
  display: flex;
  gap: var(--space-1);
  width: min(100%, 360px);
  margin-top: var(--space-2);
  padding: var(--agent-pad);
  border: 1px solid var(--input-border);
  border-radius: var(--radius-input);
  background: var(--input-bg);
  cursor: text;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.password-modal__form:hover {
  border-color: var(--input-border-hover);
}

.password-modal.is-building:not(.is-resolved) .password-modal__form {
  opacity: 0;
}

.password-modal.is-resolved .password-modal__form {
  transition: opacity 0.3s ease;
}

.password-modal__field {
  flex: 1;
  min-width: 0;
  height: 40px;
  padding: 0 var(--space-3);
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--color-text);
  font: inherit;
  font-size: 1rem; /* 16px: iOS doesn't zoom into the field */
}

.password-modal__field:focus {
  outline: none;
}

.password-modal__form:focus-within {
  border-color: var(--color-accent-fill);
  box-shadow: 0 0 0 3px var(--input-ring);
}

.password-modal__field[aria-invalid="true"] {
  color: var(--color-error);
}

.password-modal__submit {
  align-self: stretch;
  flex-shrink: 0;
  padding-block: 0;
  padding-inline: var(--space-4);
  border-radius: 6px;
}

.password-modal__submit:disabled {
  opacity: 0.6;
  cursor: progress;
}

/* Holds its line even when empty, so a message doesn't push the CTA down. */
.password-modal__error {
  min-height: 1.5em;
  color: var(--color-error);
  font-size: var(--fs-small);
}

.password-modal .stage-modal__cta {
  margin-top: 0;
}

@media (max-width: 380px) {
  .password-modal__form {
    flex-direction: column;
  }

  .password-modal__submit {
    padding-block: var(--space-3);
  }
}

/* ---- A/B test modal (ab.js): the .stage-modal base, with more to read
   than the others, so it scrolls when a phone runs out of room. The two
   versions stand in for the icon, as the hero's frames: the visitor's in
   ink with its handles, the other an empty hatched one. ---- */
.ab-modal {
  overflow-y: auto;
}

.ab-modal__pair {
  display: flex;
  gap: var(--space-5);
  margin-bottom: var(--space-3);
  font-family: "Space Grotesk", var(--font-family);
  font-size: 56px;
  font-weight: 500;
  line-height: 1.1;
}

.ab-modal__frame {
  position: relative;
  padding: 0 0.25em;
  border: 1.5px dashed var(--hatch-line);
  background: var(--hatch);
  color: var(--color-text-secondary);
}

.ab-modal__frame.is-yours {
  border: 1.5px solid var(--color-text);
  background: none;
  color: var(--color-text);
}

.ab-modal__frame.is-yours::before,
.ab-modal__frame.is-yours::after {
  content: "";
  position: absolute;
  width: 9px;
  height: 9px;
  border: 1.5px solid var(--color-text);
  background: var(--color-bg);
}

.ab-modal__frame.is-yours::before { top: -5px; left: -5px; }
.ab-modal__frame.is-yours::after { right: -5px; bottom: -5px; }

.ab-modal__desc {
  max-width: 40ch;
}

/* The results: a two-bar chart of each version's conversion rate, the
   visitor's in the accent, the other in grey, the numbers always in text
   ink. */
.ab-results {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: 100%;
  max-width: 30rem;
  margin-top: var(--space-4);
  text-align: left;
}

.ab-results[hidden] {
  display: none;
}

.ab-results__note {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
  line-height: 1.45;
  text-align: center;
}

/* Two columns on one baseline. Each bar grows from it to --h of the plot
   (its rate over the axis top ab.js picks), rounded only at the data end;
   the rate rides just above it. */
.ab-chart {
  display: grid;
  grid-template-columns: 1fr 1fr;
  margin: 0;
}

/* No gap between the columns, so the baseline runs unbroken under both;
   the labels keep their own air instead. */
.ab-chart__col > p {
  padding-inline: var(--space-2);
}

.ab-chart__col {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  text-align: center;
}

.ab-chart__plot {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  width: 100%;
  height: 168px;
  border-bottom: 1px solid var(--color-text-secondary);
}

.ab-chart__bar {
  display: block;
  width: min(64px, 60%);
  height: var(--h, 0%);
  border-radius: 4px 4px 0 0;
  background: var(--color-text-secondary);
  transition: height 0.6s ease;
}

.ab-chart__col.is-yours .ab-chart__bar {
  background: var(--color-accent-fill);
}

.ab-chart__value {
  margin-bottom: 6px;
  color: var(--color-text);
  font-family: "Space Grotesk", var(--font-family);
  font-size: 1.5rem;
  font-weight: 500;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.ab-chart__name {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: var(--space-2);
  color: var(--color-text);
  font-size: var(--fs-small);
}

.ab-chart__name b {
  font-weight: 600;
}

.ab-chart__headline,
.ab-chart__count {
  color: var(--color-text-secondary);
  font-size: 0.75rem;
  line-height: 1.4;
}

.ab-chart__count {
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}

.ab-modal__switch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.ab-modal__switch .access-modal__submit {
  align-self: center;
}

@media (min-width: 768px) {
  .stage-modal__icon {
    width: 200px;
    height: 200px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .stage-modal,
  .stage-modal__close,
  .pager__btn,
  .project-card,
  .project-card__request,
  .access-modal__submit,
  .stage-modal__cta {
    transition: none;
  }

  .stage-modal.match-modal.is-built .stage-modal__icon {
    animation: none;
  }

  .project-card:hover {
    transform: none;
  }
}

/* ==========================================================================
   Tablet — >= 768px
   ========================================================================== */

@media (min-width: 768px) {
  :root {
    --page-padding: var(--space-7);
    --sticky-top: calc(2 * var(--frame-gap) + 56px + 2px);
  }

  .nav-topbar__row {
    --bar: 56px;
  }

  .nav-topbar__actions {
    display: flex;
    flex-shrink: 0;
  }

  .nav-topbar__mobile-actions {
    display: none;
  }


  .projects__grid {
    grid-template-columns: repeat(2, 1fr);
  }

  /* The Method loop turns horizontal: problem on the left, solution on the
     right, and every position swaps its two percentages. */
  .method__graph {
    padding: var(--space-5) 0; /* room for the pills above and below */
  }

  .method__loop {
    width: 100%;
    max-width: 36rem;
  }

  .method__infinity--v {
    display: none;
  }

  .method__infinity--h {
    display: block;
  }

  .method__pill--discover { --x: 19.4%; --y: 12.5%; }
  .method__pill--define   { --x: 19.4%; --y: 87.5%; }
  .method__pill--develop  { --x: 80.6%; --y: 12.5%; }
  .method__pill--deliver  { --x: 80.6%; --y: 87.5%; }

  .method__pill--discover,
  .method__pill--develop {
    transform: translate(-50%, calc(-100% + var(--pill-overlap)));
  }

  .method__pill--define,
  .method__pill--deliver {
    transform: translate(-50%, calc(-1 * var(--pill-overlap)));
  }

  .method__space,
  .method__note {
    top: 50%;
    width: 36%;
  }

  .method__stack {
    display: flex;
  }

  .method__space--problem,
  .method__note--discover,
  .method__note--define {
    left: 23%;
  }

  .method__space--solution,
  .method__note--develop,
  .method__note--deliver {
    left: 77%;
  }

  .principle-card {
    padding: var(--space-6);
  }

  .site-footer__mobile-block {
    display: none;
  }

  .site-footer__back-to-top {
    display: inline-block;
  }

  .site-footer__legal {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* ==========================================================================
   Desktop — >= 1200px
   ========================================================================== */

@media (min-width: 1200px) {
  :root {
    --page-padding: var(--space-8);
    --content-max-width: 620px;
    --sticky-top: 0px;
  }

  .page {
    flex-direction: row;
  }

  /* No top bar here — the sidebar below owns its own sticky. */
  .site-header {
    position: static;
    z-index: auto;
  }

  .nav-topbar {
    display: none;
  }

  section[id],
  .cases {
    scroll-margin-top: var(--space-8);
  }

  /* A card floating on the frame, a step lighter than the page (the
     agent's surface too), one screen tall and pinned while the page scrolls. */
  .nav-sidebar {
    background: var(--color-panel);
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
    align-self: flex-start;
    width: var(--sidebar-width);
    flex-shrink: 0;
    height: calc(100vh - 2 * var(--frame-gap));
    height: calc(100dvh - 2 * var(--frame-gap));
    padding: var(--space-6) var(--space-5);
    border: 1px solid var(--color-border);
    border-radius: var(--frame-radius);
    box-shadow: var(--shadow-panel);
    position: sticky;
    top: var(--frame-gap);
  }

  .nav-sidebar__footer .avatar {
    width: 60px;
    height: 60px;
  }

  .nav-sidebar__nav ul {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
  }

  .nav-sidebar__footer {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
    margin-top: auto;
  }

  /* The topbar is display:none up here and the nav is a left sidebar, so the
     controls get pinned to the top-right of the window, at the right end of
     the agent card's header (its 40px row, 4px in from the card's edges). */
  [data-theme-controls="desktop"] {
    display: flex;
    gap: 2px;
    position: fixed;
    top: calc(var(--frame-gap) + 1px + 4px);
    right: calc(var(--frame-gap) + 1px + 4px);
    z-index: 60;
  }

  /* Inside the card they are icon buttons like the header's own: no frame,
     a rounded wash on hover. */
  [data-theme-controls="desktop"] .mode-trigger,
  [data-theme-controls="desktop"] .lang-trigger {
    margin-left: 0;
    border-color: transparent;
    border-radius: 6px;
    background: transparent;
    transition: background-color 0.2s ease;
  }

  [data-theme-controls="desktop"] .mode-trigger:hover,
  [data-theme-controls="desktop"] .lang-trigger:hover,
  [data-theme-controls="desktop"] [aria-expanded="true"] {
    border-color: transparent;
    background: var(--chat-surface);
  }

  /* With no tag to hang off, the dropdowns float a step below their button
     as small cards of their own. */
  [data-theme-controls="desktop"] .theme-palette {
    margin-top: 4px;
    border-top: 1px solid var(--color-border);
    border-radius: 8px;
    box-shadow: var(--shadow-panel);
  }

  .content {
    flex: 1;
    min-width: 0;
  }

  .section-inner {
    max-width: calc(1440px - var(--sidebar-width));
    margin: 0 auto;
  }

  .hero {
    min-height: calc(100svh - var(--hero-peek));
    padding-top: var(--space-8);
    padding-bottom: var(--space-7);
  }

  .site-footer {
    padding-left: var(--space-8);
    padding-right: var(--space-8);
  }

  .site-footer__back-to-top {
    display: none;
  }

  .method__loop {
    max-width: 40rem;
  }

  /* The dialog lives outside .page, so the sidebar's flex-direction: row above
     never touches it — there is deliberately nothing to override here. */
}

/* ==========================================================================
   Frand Agent — the portfolio's chat (chat.js, agent-brain.js)
   Kept at the foot of the file so its breakpoints land after the layout
   ones.
     >= 1024px  a column pinned to the right edge of the window
     <  1024px  a pill at the bottom that opens into a sheet
   ========================================================================== */

:root {
  /* The panel is the menu's surface (--color-panel), a step lighter than
     the page, so it reads as a place you can use rather than a backdrop.
     Surface: chips, cards and hovers. Bubble: Frand's answers. Set on the
     root, so the page behind the open sheet on a phone can take it too. */
  --chat-bg: var(--color-panel);
  --chat-surface: var(--color-bg);
  --chat-bubble: var(--color-bg-card-header);
}

:root[data-theme="dark"] {
  --chat-surface: var(--color-bg-card-media);
  --chat-bubble: color-mix(in oklab, var(--color-bg-card-media), var(--color-text) 6%);
}

/* Room under the footer, so the pill never sits on its last lines. */
.site-footer {
  padding-bottom: calc(var(--space-8) + 76px);
}

.chat__panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* ---- Header ---- */
.chat__header {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 48px;
  padding: 0 var(--space-2) 0 var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

.chat__beta {
  margin-right: auto;
  padding: 1px 7px;
  border: 1px solid var(--color-border);
  border-radius: 0;
  color: var(--color-text-secondary);
  font-size: 0.625rem;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.chat__icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 0;
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: color 0.2s ease;
}

.chat__icon-btn:hover {
  color: var(--color-text);
}

.chat__icon-btn svg {
  width: 18px;
  height: 18px;
}

.chat[data-state="empty"] .chat__reset,
.chat__close {
  display: none;
}

/* ---- Body: the empty state, then the conversation ---- */
.chat__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-4);
}

.chat__empty {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: auto 0;
}

.chat[data-state="active"] .chat__empty,
.chat[data-state="empty"] .chat__log {
  display: none;
}

.chat[data-state="active"] .chat__log {
  animation: chat-fade-in 0.3s ease both;
}

.chat__icon {
  width: 40px;
  height: 40px;
  color: var(--color-text);
}

/* Filled with the panel's own colour, so the frame line stops at them like
   the word boxes' handles do on the page. */
.chat__icon-handles {
  fill: var(--chat-bg);
}

/* The same accent as the ✦ in the input: a mark, not text, so it wears
   the page's colour at full brightness, as its bands do. */
.chat__icon-spark {
  fill: var(--color-accent-fill);
}

/* "Agent" rides on the name like the brand's ™ (.brand__tm). */
.chat__headline {
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.2;
}

.chat__intro {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
}

/* Starters and follow-ups: a plain list of links, underlined like the
   site's .text-link, no dividers and no arrows. */
.chat__chips {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}

.chat__chip {
  padding: 2px 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--color-text);
  font-size: 0.875rem;
  line-height: 1.35;
  text-align: left;
  text-decoration: underline;
  text-decoration-color: color-mix(in oklab, var(--color-text) 35%, transparent);
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color 0.2s ease, text-decoration-color 0.2s ease, opacity 0.2s ease;
}

.chat__chip:hover,
.chat__chip:focus-visible {
  color: var(--color-accent);
  text-decoration-color: currentColor;
}

.chat.is-busy .chat__chip {
  opacity: 0.5;
  pointer-events: none;
}

.chat__log {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.chat__msg {
  display: flex;
}

.chat__msg--user {
  justify-content: flex-end;
}

.chat__msg--card,
.chat__msg--suggestions {
  display: block;
}

.chat__bubble {
  max-width: 88%;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-agent);
  font-size: 0.875rem;
  line-height: 1.5;
  /* the answers carry their paragraphs and lists as plain newlines */
  white-space: pre-line;
  overflow-wrap: anywhere;
}

.chat__msg--user .chat__bubble {
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

.chat__msg--agent .chat__bubble {
  background: var(--chat-bubble);
}

/* An answer told as steps (chat.js timelineRow): the case's staged-launch
   timeline (.case-timeline) at the chat's scale. A line down the left, a
   numbered circle per step on the panel's tone so the line runs behind it,
   and the last one filled: where the story lands. The steps come in one
   after another, as the answer is being told. */
.chat__msg--timeline {
  display: block;
}

.chat-timeline {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0;
  padding: 0 0 0 calc(1.5rem + var(--space-3));
  list-style: none;
  counter-reset: chat-step;
}

.chat-timeline::before {
  content: "";
  position: absolute;
  top: 0.75rem;
  bottom: 0.75rem;
  left: calc(0.75rem - 1px);
  width: 2px;
  background: var(--color-border);
}

.chat-timeline__step {
  counter-increment: chat-step;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 1.5rem;
  font-size: 0.875rem;
  line-height: 1.5;
}

.chat-timeline__step::before {
  content: counter(chat-step);
  position: absolute;
  top: 0;
  left: calc((1.5rem + var(--space-3)) * -1);
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border: 1px solid var(--color-accent);
  border-radius: 50%;
  background: var(--chat-bg);
  color: var(--color-text);
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1;
}

.chat-timeline__step:last-child::before {
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

.chat-timeline__meta {
  color: var(--color-text-secondary);
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.5rem;
}

.chat-timeline__title {
  font-weight: 600;
}

.chat-timeline__text {
  margin: 0;
  color: var(--color-text-secondary);
  overflow-wrap: anywhere;
}

@media (prefers-reduced-motion: no-preference) {
  .chat-timeline__step {
    animation: chat-step-in 0.4s ease both;
    animation-delay: calc(var(--step, 0) * 0.12s);
  }
}

@keyframes chat-step-in {
  from { opacity: 0; transform: translateY(6px); }
}

/* Images from the work, under an answer (chat.js mediaRow). One fills the
   column; several run in a strip that bleeds to the panel's edges and snaps
   image by image. A linked one carries a "See it in the case" tag. */
.chat__msg--media {
  display: block;
}

.chat__media {
  display: flex;
  gap: var(--space-2);
}

.chat__media--strip {
  margin-inline: calc(var(--space-4) * -1);
  padding-inline: var(--space-4);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--space-4);
  scrollbar-width: none;
}

.chat__media--strip::-webkit-scrollbar {
  display: none;
}

.chat__media-item {
  position: relative;
  flex: none;
  margin: 0;
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-agent);
  background: var(--chat-surface);
  scroll-snap-align: start;
}

.chat__media:not(.chat__media--strip) .chat__media-item {
  width: 100%;
}

.chat__media-img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 55vh;
  object-fit: cover;
}

/* In a strip every image shares one height and keeps its own shape, up to
   most of the column's width, so the next one always peeks in. */
.chat__media--strip .chat__media-item {
  max-width: 85%;
}

.chat__media--strip .chat__media-img {
  width: auto;
  max-width: 100%;
  height: 13rem;
  max-height: none;
}

:root[data-theme="dark"] .chat__media-img {
  filter: brightness(0.86) saturate(0.95);
}

.chat__media-cta {
  position: absolute;
  left: var(--space-2);
  bottom: var(--space-2);
  padding: 2px 10px;
  border-radius: 0;
  background: rgb(0 0 0 / 0.62);
  color: #fff;
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.5;
}

.chat__media-cta::after {
  content: " →" / "";
}

/* The value loop in a reply (chat.js valueLoop): drawn in the theme, on
   the frame's surface, with room under it for the "See the method" tag. */
.chat__media-item--diagram {
  width: 100%;
}

.chat-loop {
  display: block;
  width: 100%;
  height: auto;
  padding: var(--space-3) var(--space-2) calc(var(--space-6) + var(--space-2));
  overflow: visible;
  font-family: inherit;
}

.chat-loop__lobe {
  fill: var(--color-accent-tint);
}

.chat-loop__track {
  fill: none;
  stroke: var(--color-text-secondary);
  stroke-width: 1.5;
}

.chat-loop__space {
  fill: var(--color-text);
  font-size: 18px;
  font-weight: 600;
  text-anchor: middle;
  dominant-baseline: central;
}

.chat-loop__lean {
  fill: var(--color-text-secondary);
  font-size: 12px;
  font-style: italic;
}

.chat-loop__pill rect {
  fill: var(--chat-surface);
  stroke: var(--color-accent);
  stroke-width: 1.25;
}

.chat-loop__pill text {
  fill: var(--color-text);
  font-size: 12px;
  font-weight: 500;
  text-anchor: middle;
  dominant-baseline: central;
}

/* Back to the latest (chat.js): a square like the chat's other buttons,
   riding the foot of the conversation while it is read far back. */
.chat__latest {
  position: sticky;
  z-index: 1;
  bottom: 0;
  display: grid;
  flex: none;
  place-items: center;
  align-self: flex-end;
  width: 44px;
  height: 44px;
  margin-top: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 0;
  background: var(--chat-bg);
  color: var(--color-text);
  cursor: pointer;
  transition: box-shadow 0.25s ease;
}

.chat__latest[hidden] {
  display: none;
}

.chat__latest:hover {
  box-shadow: inset 0 0 0 1px var(--color-accent);
}

.chat__latest svg {
  width: 18px;
  height: 18px;
}

/* The case tags sit above the answer they belong to. */
.chat__msg--agent {
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
}

.chat__cases {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.chat__case {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 10px;
  border: 1px solid var(--color-border);
  border-radius: 0;
  color: var(--color-text);
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.5;
  text-decoration: none;
}

.chat__case-cta {
  color: var(--color-text-secondary);
  font-weight: 500;
}

.chat__case--link {
  transition: border-color 0.2s ease;
}

.chat__case--link:hover {
  border-color: var(--color-accent);
}

.chat__typing {
  display: inline-flex;
  gap: 4px;
  padding: 6px 0;
  vertical-align: middle;
}

.chat__typing span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-text-secondary);
  animation: chat-typing 1s ease-in-out infinite;
}

.chat__typing span:nth-child(2) { animation-delay: 0.15s; }
.chat__typing span:nth-child(3) { animation-delay: 0.3s; }

@keyframes chat-typing {
  0%, 60%, 100% { opacity: 0.3; transform: none; }
  30% { opacity: 1; transform: translateY(-3px); }
}

@keyframes chat-fade-in {
  from { opacity: 0; }
}

/* ---- The LinkedIn card (no answer, or contact questions) ---- */
.chat-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-agent);
  background: var(--chat-surface);
}

.chat-card__title {
  font-size: 0.875rem;
  font-weight: 600;
}

.chat-card__desc {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
}

/* The access modal's CTA (.stage-modal__cta), a size down. */
.chat-card__cta {
  --cta: 40px;
  align-self: flex-start;
  margin-top: var(--space-3);
}

.chat-card__cta .stage-modal__cta-label {
  padding-inline: var(--space-4);
}

.chat-card__cta .stage-modal__cta-mark--linkedin svg {
  width: 15px;
  height: 15px;
}

/* A question back (chat.js choiceForm), inside the agent's message: the
   bubble and its options read as one block, as wide as the wider of the
   two. One radio per option, and the access modal's submit, a size down. */
.chat__msg--has-choice {
  align-items: stretch;
  gap: 0;
  width: fit-content;
  max-width: 88%;
}

.chat__msg--has-choice .chat__bubble {
  max-width: none;
}

.chat-choice {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-3) var(--space-3);
  border-radius: var(--radius-agent);
  background: var(--chat-bubble);
  font-size: 0.875rem;
}

.chat-choice__options {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.chat-choice__option {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: 0.875rem;
  line-height: 1.35;
  cursor: pointer;
}

.chat-choice__radio {
  appearance: none;
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 1px solid var(--color-text-secondary);
  border-radius: 50%;
  background: var(--color-bg);
  cursor: pointer;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

/* The section's name, and what it holds under it. */
.chat-choice__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.chat-choice__title {
  font-weight: 500;
}

.chat-choice__hint {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
}

.chat-choice__option:hover .chat-choice__radio {
  border-color: var(--color-text);
}

.chat-choice__radio:checked {
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 4px var(--color-bg), inset 0 0 0 9px var(--color-accent);
}

.chat-choice__submit {
  padding: var(--space-2) var(--space-5);
}

.chat-choice__submit:disabled {
  opacity: 0.35;
  cursor: default;
}

.chat.is-busy .chat-choice {
  opacity: 0.5;
  pointer-events: none;
}

/* ---- Agent input ----
   The chat's field: the pill, the sheet and the column (chat.js). */
.agent-input {
  /* The inset around the field and the send button. */
  --agent-pad: var(--space-1);
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  padding: var(--agent-pad) var(--agent-pad) var(--agent-pad) var(--space-3);
  border: 1px solid var(--input-border);
  overflow: hidden;
  border-radius: var(--radius-input);
  background: var(--agent-input-bg, var(--input-bg));
  cursor: text;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.agent-input:hover {
  border-color: var(--input-border-hover);
}

.agent-input:focus-within {
  border-color: var(--color-accent-fill);
  box-shadow: 0 0 0 3px var(--input-ring);
}

/* The agent's mark, as in the chat's empty state, but drawn for 20px rather
   than scaled down: a 1px frame on the pixel grid, solid 3px handles, and
   the ✦ large in the middle. Frame and handles step back in grey so the
   star carries it. */
.agent-input__spark {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  margin-bottom: 6px; /* centred on the 32px send button, bottom-aligned */
  color: var(--color-text-secondary);
}

.agent-input__spark-star {
  fill: var(--color-accent-fill);
}

.agent-input__field {
  flex: 1;
  min-width: 0;
  max-height: 120px;
  padding: 7px 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 0.875rem;
  line-height: 1.3;
  resize: none;
}

/* Touch screens get 16px: below that, iOS zooms the whole page in when the
   field takes focus. The placeholder stays at the smaller size. */
@media (pointer: coarse) {
  .agent-input__field {
    font-size: 1rem;
    line-height: 1.15;
  }

  .agent-input__field::placeholder {
    font-size: 0.875rem;
  }
}

/* The form's border carries the focus instead. */
.agent-input__field:focus-visible {
  outline: none;
}

.agent-input__field::placeholder {
  color: var(--color-text-secondary);
  transition: opacity 0.2s ease;
}

/* The placeholder never wraps onto a second, clipped line in the one-line
   field; typed text still wraps. */
.agent-input__field:placeholder-shown {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Send: a rounded square inset in the field's bottom-right corner.
   Empty, it waits as a bare arrow; with something to send, it fills. */
.agent-input__send {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 6px;
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease;
}

.agent-input__send svg {
  width: 16px;
  height: 16px;
}

.agent-input__send:disabled {
  background: none;
  color: var(--color-text-secondary);
  cursor: default;
}

/* In the chat: inset from the panel's edges. */
.chat__form {
  margin: 0 var(--space-4);
}

.chat__disclaimer {
  padding: var(--space-2) var(--space-4) var(--space-3);
  color: var(--color-text-secondary);
  font-size: 0.6875rem;
  line-height: 1.4;
  text-align: center;
}

.chat__backdrop,
.chat__resize,
.chat__rail {
  display: none;
}

/* Full screen's toggle in the content corner: desktop only (from 1200px). */
.content__focus {
  display: none;
}

/* ---- Below 1024px: the pill, and the sheet it opens into ---- */
@media (max-width: 1023.98px) {
  /* The page stays put behind the open sheet. */
  html.chat-open {
    overflow: hidden;
  }

  .chat__panel {
    position: fixed;
    z-index: 70;
    left: 50%;
    bottom: max(var(--space-4), env(safe-area-inset-bottom));
    width: min(520px, calc(100% - 2 * var(--space-4)));
    transform: translateX(-50%);
  }

  .chat:not(.is-open) .chat__header,
  .chat:not(.is-open) .chat__body,
  .chat:not(.is-open) .chat__disclaimer {
    display: none;
  }

  /* Closed, the form is the pill: a lighter surface with a quiet neutral
     edge, so it stands off whatever scrolls behind it, even on the dark
     page, without taking the accent. Rounded like every field, and the very size of the field in the open sheet, so opening it doesn't
     jump. The hero's agent bar wears the same look, minus the float… */
  /* Closed or open, the chat's field is 52px tall, like the hero's bar: the
     32px line, 9px above and below, and its edge. */
  .chat__form {
    --agent-pad: 9px;
    padding-left: var(--space-4);
  }

  .chat:not(.is-open) .chat__form,
  .agent-input--bar {
    align-items: center;
    margin: 0;
    border-color: color-mix(in oklab, var(--color-text) 14%, transparent);
    background: var(--agent-pill);
  }

  /* …and floating, the pill is glass: the same material as Safari's own
     bar under it (iOS 26), so the two read as one family of floating
     controls over the page, with no solid block between them. Enough of
     the surface stays to keep the placeholder legible over any section;
     without backdrop-filter it falls back to the solid surface. */
  .chat:not(.is-open) .chat__form {
    /* A light edge on top, a tight shadow close under, and a wide soft one:
       lifted off the page on light and dark ground alike. */
    box-shadow:
      inset 0 1px 0 rgb(255 255 255 / 0.06),
      0 2px 6px rgb(var(--shadow-rgb) / 0.18),
      0 12px 32px rgb(var(--shadow-rgb) / 0.32);
  }

  @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .chat:not(.is-open) .chat__form {
      background: color-mix(in oklab, var(--agent-pill) 75%, transparent);
      -webkit-backdrop-filter: blur(20px) saturate(1.4);
      backdrop-filter: blur(20px) saturate(1.4);
    }
  }

  /* In the hero the bar sits still in the page: the field's own edge, no
     lift. */
  .agent-input--bar {
    --agent-pad: 6px;
    min-height: 52px;
    padding: var(--agent-pad) var(--agent-pad) var(--agent-pad) var(--space-4);
    border-color: var(--input-border);
  }

  .chat:not(.is-open) .agent-input__spark,
  .agent-input--bar .agent-input__spark {
    margin-bottom: 0;
  }

  .agent-input--bar .agent-input__send {
    width: 38px;
    height: 38px;
  }

  /* The placeholder says what the field is for (and, open, whose chat this
     is), so it stays put and legible while the field is empty: no italic,
     no fade, not on hover, not on focus. */
  .agent-input--bar .agent-input__field::placeholder,
  .chat .agent-input__field::placeholder {
    color: var(--color-text-secondary);
    font-family: inherit;
    font-size: 0.9375rem;
    font-style: normal;
    opacity: 1;
  }

  /* Closed, the send button is a sign, not a control: an unfilled box, the
     arrow at full colour, letting taps through to the form, which opens
     the sheet (chat.js). */
  .chat:not(.is-open) .agent-input__send {
    pointer-events: none;
  }

  .chat:not(.is-open) .agent-input__send,
  .chat:not(.is-open) .agent-input__send:disabled {
    background: none;
    color: var(--color-text);
  }

  /* Waiting: every few seconds the closed pill's edge lights up in the
     accent and draws itself round the silhouette, from the ✦ on the left,
     while a soft even fill of the accent rises inside; then both fade. The
     ✦ breathes as the line sets off. The light sits exactly on the edge, so
     the pill lets its content overflow; both layers follow its radius. Off for
     reduced motion; gone once the sheet opens. */
  @media (prefers-reduced-motion: no-preference) {
    .chat:not(.is-open) .chat__form {
      position: relative;
      overflow: visible;
    }

    /* The line: a conic sweep from the left, masked to a 1px ring. */
    .chat:not(.is-open) .chat__form::after {
      content: "";
      position: absolute;
      inset: -1px;
      padding: 1px;
      border-radius: calc(var(--radius-input) + 1px);
      background: conic-gradient(from 270deg,
        var(--color-accent-fill) var(--agent-draw),
        transparent var(--agent-draw));
      -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
      -webkit-mask-composite: xor;
      mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
      animation: agent-draw 5s ease-in-out infinite;
      pointer-events: none;
    }

    /* The light inside: flat, a tenth of the accent. */
    .chat:not(.is-open) .chat__form::before {
      content: "";
      position: absolute;
      inset: 0;
      border-radius: inherit;
      background: color-mix(in oklab, var(--color-accent-fill) 12%, transparent);
      opacity: 0;
      animation: agent-glow 5s ease-in-out infinite;
      pointer-events: none;
    }

    .chat:not(.is-open) .agent-input__spark-star {
      transform-box: fill-box;
      transform-origin: center;
      animation: agent-spark 5s ease-in-out infinite;
    }
  }

  /* On the home the pill waits while the hero's
     bar is on screen, and slides in once it has scrolled out — chat.js sets
     html.agent-bar-out. Pages without the bar show the pill straight away. */
  .chat:not(.is-open) .chat__panel {
    transition: opacity 0.25s ease, translate 0.25s ease, visibility 0s;
  }

  :root:has([data-agent-inline]):not(.agent-bar-out) .chat:not(.is-open) .chat__panel {
    opacity: 0;
    visibility: hidden;
    translate: 0 12px;
    transition: opacity 0.25s ease, translate 0.25s ease, visibility 0s 0.25s;
  }

  /* max-height beats the inline height autosize leaves behind. Taps pass
     through the field to the pill, which opens the sheet without focusing
     it (chat.js): no keyboard until the visitor taps the field in the sheet. */
  .chat:not(.is-open) .agent-input__field {
    max-height: 32px;
    overflow: hidden;
    pointer-events: none;
  }

  .chat__close {
    display: flex;
  }

  .chat.is-open .chat__backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 69;
    background: rgb(var(--shadow-rgb) / 0.4);
  }

  /* Phones: a card over the whole visible screen, --frame-gap in from its
     edges (clear of the notch and the home indicator), like the column it
     becomes at 1024px. chat.js keeps --vv-top / --vv-height on the visual
     viewport while the sheet is open, so with the keyboard up the header
     stays in view and the input sits right above the keys. */
  .chat.is-open .chat__panel {
    --sheet-top: max(var(--frame-gap), env(safe-area-inset-top));
    --sheet-bottom: max(var(--frame-gap), env(safe-area-inset-bottom));
    inset: auto var(--frame-gap);
    top: calc(var(--vv-top, 0px) + var(--sheet-top));
    height: calc(var(--vv-height, 100dvh) - var(--sheet-top) - var(--sheet-bottom));
    width: auto;
    transform: none;
    border: 1px solid var(--color-border);
    border-radius: var(--frame-radius);
    overflow: hidden;
    background: var(--chat-bg);
    box-shadow: var(--shadow-panel);
    animation: chat-sheet-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
  }

  /* With the keys up, the home indicator is under them: just the gap. */
  html.keyboard-open .chat.is-open .chat__panel {
    --sheet-bottom: var(--frame-gap);
  }
}

/* Phones: behind the sheet, the bare frame rather than a dimmed page,
   reaching well below the screen, so whatever the translucent keyboard bar
   or Safari's toolbar lets through is the frame the card floats on. */
@media (max-width: 767.98px) {
  .chat.is-open .chat__backdrop {
    bottom: -50vh;
    background: var(--color-frame);
  }

  /* With the keyboard up, iOS pans the page under the sheet, and its
     translucent keyboard and autofill bar show (and take their tint from)
     whatever is behind them: the backdrop alone doesn't reach. So the page
     itself steps out while the sheet is open (visibility keeps its scroll
     position) and the canvas is the frame: whatever shows around the
     card, above or under the keys, is its ground. */
  html.chat-open,
  html.chat-open body {
    background: var(--color-frame);
  }

  html.chat-open :is(.site-header, .content, .site-footer) {
    visibility: hidden;
  }

  /* While typing, the fine print gives its two lines to the conversation. */
  html.keyboard-open .chat.is-open .chat__disclaimer {
    display: none;
  }

  /* The card keeps the column's header row: the Beta badge, then new
     conversation (once there is one) and close as rounded icon buttons,
     4px in from the card's corner and a thumb's size. */
  .chat.is-open .chat__header {
    padding-right: 4px;
  }

  .chat.is-open :is(.chat__reset, .chat__close) {
    width: 40px;
    height: 40px;
    border-radius: 8px;
    color: var(--color-text);
  }

  /* Once the field is active the visitor has chosen to type: the starters,
     and the line that offers them, give way. */
  .chat.is-open[data-state="empty"]:has(.chat__form:focus-within) :is(.chat__intro, [data-chat-starters]) {
    display: none;
  }
}

/* Tablets: a floating sheet, about two thirds of the screen, riding up on
   top of the keyboard (the visual viewport, as on phones). A card, rounded
   like the column it becomes at 1024px. */
@media (min-width: 768px) and (max-width: 1023.98px) {
  .chat.is-open .chat__panel {
    --sheet-height: min(70vh, calc(var(--vv-height, 100dvh) - 2 * var(--space-5)));
    inset: auto auto auto 50%;
    top: calc(var(--vv-top, 0px) + var(--vv-height, 100dvh) - var(--sheet-height) - var(--space-5));
    width: min(560px, calc(100% - 2 * var(--space-5)));
    height: var(--sheet-height);
    transform: translateX(-50%);
    border: 1px solid var(--color-border);
    border-radius: var(--frame-radius);
    overflow: hidden;
    box-shadow: var(--shadow-panel), 0 24px 64px rgb(var(--shadow-rgb) / 0.28);
  }
}

/* The closed pill's wait (see .chat:not(.is-open) .chat__form): still for
   the first stretch of each cycle, then the edge draws itself round (the
   ✦ swelling as it starts), the inside lights up as it closes, and both
   fade together. --agent-draw is registered so the sweep can animate. */
@property --agent-draw {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

@keyframes agent-draw {
  0%, 40% { --agent-draw: 0%; opacity: 1; }
  72% { --agent-draw: 100%; opacity: 1; }
  86% { --agent-draw: 100%; opacity: 1; }
  100% { --agent-draw: 100%; opacity: 0; }
}

@keyframes agent-glow {
  0%, 55% { opacity: 0; }
  76%, 86% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes agent-spark {
  0%, 38%, 56%, 100% { transform: scale(1); opacity: 1; }
  46% { transform: scale(1.5); opacity: 0.75; }
}

@keyframes chat-sheet-in {
  from { opacity: 0; translate: 0 16px; }
}

/* ---- 1024px and up: the column ----
   A card floating over the frame at the window's right, one screen tall so
   the input stays at its foot, and the page makes room for it. Fixed rather
   than a flex column of .page, since .page only becomes a row at 1200px.
   chat.js keeps --chat-width on :root, so both sides read the same width.
   On its left, the top bar and the content's cards up to 1200px; from
   there the menu takes the bar's place (see the desktop block). */
@media (min-width: 1024px) {
  /* The cards make room for the column on their right. */
  .page {
    padding-right: calc(var(--chat-width, 320px) + 2 * var(--frame-gap));
  }

  .chat {
    position: fixed;
    z-index: 45;
    top: var(--frame-gap);
    right: var(--frame-gap);
    width: var(--chat-width, 320px);
    height: calc(100vh - 2 * var(--frame-gap));
    height: calc(100dvh - 2 * var(--frame-gap));
    border: 1px solid var(--color-border);
    border-radius: var(--frame-radius);
    background: var(--chat-bg);
    box-shadow: var(--shadow-panel);
  }

  .site-footer {
    padding-bottom: var(--space-8);
  }

  .chat__panel {
    height: 100%;
  }

  .chat__header .chat__icon-btn {
    width: 28px;
    height: 28px;
  }

  /* The ✦ marks the pill; in the column the input needs the width more. */
  .chat__form .agent-input__spark {
    display: none;
  }

  /* An 8px grab strip straddling the column's left border (chat.js); the
     border lights up in the accent while it is hovered, focused or dragged. */
  .chat__resize {
    display: block;
    position: absolute;
    z-index: 2;
    top: 0;
    bottom: 0;
    left: -4px;
    width: 8px;
    cursor: col-resize;
    touch-action: none;
  }

  .chat__resize::after {
    content: "";
    position: absolute;
    top: var(--frame-radius);
    bottom: var(--frame-radius);
    left: 3px;
    width: 2px;
    background: var(--color-accent-fill);
    opacity: 0;
    transition: opacity 0.2s ease;
  }

  .chat__resize:hover::after,
  .chat__resize:focus-visible::after,
  .chat-resizing .chat__resize::after {
    opacity: 1;
  }

  .chat__resize:focus-visible {
    outline: none;
  }

  /* While dragging: no text selection, and the resize cursor everywhere, not
     just over the strip the pointer has already left. */
  .chat-resizing,
  .chat-resizing * {
    cursor: col-resize;
    user-select: none;
  }
}

/* From 1200px the theme/language strip is pinned into the column's
   top-right corner (two 32px buttons, 2px apart, 4px in), so the header
   is a 40px row that leaves it the right-hand end: the strip reads as part
   of the header. */
@media (min-width: 1200px) {
  .chat__header {
    min-height: 40px;
    height: 40px;
    padding-right: calc(2 * 32px + 2px + 4px + var(--space-2));
  }

  /* Fit check and Method side by side, as tall as each other, under the
     hero and the cases. Half a card is too narrow for the hatched side
     columns' gutter: the content is inset like the cards' own padding. */
  .main:has(> .hero) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .main:has(> .hero) > :is(.principles, .method) > .section-inner {
    padding-left: var(--space-5);
    padding-right: var(--space-5);
  }
}

/* ==========================================================================
   Full screen (focus-mode.js) — from 1200px, html.is-focus folds the nav and
   Frand Agent's column down to thin rails, so a project gets the window.
   Instant rather than animated: the sidebars' contents would reflow through
   every in-between width.
   ========================================================================== */

@media (min-width: 1200px) {
  :root {
    --rail-width: 40px;
  }

  /* Everything that clears the nav (.section-inner, the cookie toast) reads
     this, so it follows the rail on its own. */
  :root.is-focus {
    --sidebar-width: var(--rail-width);
  }

  /* The nav: an empty rail. */
  .is-focus .nav-sidebar {
    padding: 0;
  }

  .is-focus .nav-sidebar > * {
    display: none;
  }

  /* The chat: chat.js keeps the visitor's width in an inline --chat-width,
     so the rail overrides the widths that read it rather than the variable,
     and the width is still there when the column comes back. */
  :root.is-focus .page {
    padding-right: calc(var(--rail-width) + 2 * var(--frame-gap));
  }

  .is-focus .chat {
    width: var(--rail-width);
  }

  .is-focus .chat__panel,
  .is-focus .chat__resize {
    display: none;
  }

  /* The whole rail is the button. */
  .is-focus .chat__rail {
    display: flex;
    justify-content: center;
    width: 100%;
    height: 100%;
    padding-top: calc(var(--space-2) + 7px); /* level with the shrink button */
    border-radius: calc(var(--frame-radius) - 1px);
    color: var(--color-text-secondary);
    cursor: pointer;
    transition: color 0.2s ease, background-color 0.2s ease;
  }

  .is-focus .chat__rail:hover {
    color: var(--color-accent);
    background: var(--chat-surface);
  }

  .is-focus .chat__rail:focus-visible {
    outline-offset: -2px;
  }

  .chat__rail svg {
    width: 18px;
    height: 18px;
  }

  /* Expand / shrink: a small rounded button in the content card's
     top-right corner, --space-2 in, that stays put once the page scrolls
     it up to the window's top. Sticky in the content column, with margins
     that cancel its own height so the card starts where it starts. */
  .content__focus {
    --inset: calc(1px + var(--space-2));
    display: flex;
    align-items: center;
    justify-content: center;
    position: sticky;
    z-index: 50;
    top: calc(var(--frame-gap) + var(--inset));
    align-self: flex-end;
    margin: var(--inset) var(--inset) calc(-32px - var(--inset)) 0;
    width: 32px;
    height: 32px;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    background: var(--color-panel);
    color: var(--color-text-secondary);
    cursor: pointer;
    transition: color 0.2s ease, border-color 0.25s ease;
  }

  .content__focus:hover {
    color: var(--color-text);
    border-color: var(--color-accent);
  }

  /* An open lapse (case-lapse.js) covers the content card and puts its own
     close in that corner, which this button (above the lapse's layer) would
     cover. Nothing to expand meanwhile, so it steps out until the lapse
     closes. */
  :root:has(.case-lapse.is-open) .content__focus {
    visibility: hidden;
  }

  /* The A/B banner: a box of its own on the frame, above the content card
     (first, though it comes after the expand button in the markup). It
     scrolls away with the hero. */
  .ab-chip {
    order: -1;
    width: auto;
    min-height: 40px;
    white-space: nowrap;
  }

  .ab-chip:hover {
    color: var(--color-text);
  }

  .content__focus svg {
    width: 16px;
    height: 16px;
  }

  .content__focus-shrink,
  .is-focus .content__focus-expand {
    display: none;
  }

  .is-focus .content__focus-shrink {
    display: block;
  }

  /* The theme / language strip is chrome too, and was pinned over
     the column's header, which is gone. */
  .is-focus [data-theme-controls="desktop"] {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .chat__typing span,
  .chat.is-open .chat__panel,
  .chat[data-state="active"] .chat__log {
    animation: none;
  }

  .chat__chip,
  .agent-input,
  .agent-input__send,
  .chat__icon-btn,
  .agent-input__field::placeholder,
  .chat:not(.is-open) .chat__panel {
    transition: none;
  }
}

/* ==========================================================================
   Cookie toast — analytics.js shows it until the visitor picks; Google
   Analytics only loads after "Accept". An inverse card, bottom-left.
   ========================================================================== */

.consent {
  position: fixed;
  z-index: 80;
  right: var(--space-4);
  bottom: max(var(--space-4), env(safe-area-inset-bottom));
  left: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-width: 360px;
  padding: var(--space-4);
  border-radius: 16px;
  background: var(--color-text);
  color: var(--color-bg);
  box-shadow: 0 16px 40px rgb(var(--shadow-rgb) / 0.22);
  font-size: var(--fs-small);
  line-height: 1.45;
  animation: consent-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.consent[hidden] {
  display: none;
}

/* inherit, not the global paragraph colour, which vanishes on the inverse
   card */
.consent__text {
  color: inherit;
}

.consent__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
}

.consent__btn {
  padding: 6px var(--space-4);
  border-radius: 999px;
  background: var(--color-bg);
  color: var(--color-text);
  font-size: var(--fs-small);
  font-weight: 600;
  cursor: pointer;
  transition: opacity 0.2s ease;
}

.consent__btn:hover {
  opacity: 0.88;
}

.consent__btn--ghost {
  border: 1px solid color-mix(in srgb, var(--color-bg) 35%, transparent);
  background: transparent;
  color: inherit;
  font-weight: 500;
}

.site-footer__cookies {
  margin-top: var(--space-2);
}

@keyframes consent-in {
  from { opacity: 0; translate: 0 12px; }
}

/* With Frand Agent's pill at the bottom, sit above it; and step aside while
   its sheet is open. */
@media (max-width: 1023.98px) {
  .consent {
    bottom: calc(max(var(--space-4), env(safe-area-inset-bottom)) + 68px);
  }

  :root.chat-open .consent {
    display: none;
  }
}

/* Desktop: clear of the left sidebar, in the content's corner. */
@media (min-width: 1200px) {
  .consent {
    left: calc(var(--sidebar-width) + 2 * var(--frame-gap) + var(--space-5));
    right: auto;
  }
}

@media (prefers-reduced-motion: reduce) {
  .consent {
    animation: none;
  }

  .consent__btn {
    transition: none;
  }
}

/* ==========================================================================
   Case study pages (nomo.html)
   An editorial layout on the home's tokens: a 60rem measure per section,
   a 12-column grid for two-column spreads (.case-cols), a 40rem reading
   column (.case-prose), and diagrams (.case-scheme) drawn in HTML so they
   translate and follow the theme.
   ========================================================================== */

.case-section > .section-inner,
.case-hero > .section-inner {
  max-width: calc(60rem + 2 * var(--page-padding));
  margin-inline: auto;
}

:where(.case-section, .case-hero) dl,
:where(.case-section, .case-hero) dd {
  margin: 0;
}

/* Block rhythm inside a section; zero specificity so any block can override. */
:where(.case-section) > .section-inner > * + * {
  margin-top: var(--space-7);
}

:where(.case-section) > .section-inner > .case-kicker + * {
  margin-top: 0;
}

.case-num {
  color: var(--color-accent);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  margin-right: var(--space-1);
}

/* ---- Navigation: a "Frand / Nomo" breadcrumb stands in for the brand ---- */
.case-crumbs ol {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  font-weight: 600;
}

.case-crumbs li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  white-space: nowrap;
}

.case-crumbs li + li::before {
  content: "/" / "";
  font-weight: 400;
  color: var(--color-text-secondary);
}

.case-crumbs a {
  font-weight: 400;
  color: var(--color-text-secondary);
  transition: color 0.25s ease;
}

.case-crumbs a:hover {
  color: var(--color-accent);
}

.case-crumbs [aria-current="page"] {
  color: var(--color-text);
}

.nav-topbar .case-crumbs {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  padding: 0 var(--space-4);
  border-right: 1px solid var(--color-border);
}

.nav-topbar .case-crumbs .brand--compact {
  display: inline;
  flex: none;
  border-right: none;
}

/* ---- Hero: title, standfirst and fact sheet ---- */
.case-hero {
  padding: var(--space-8) 0 var(--space-7);
}

.case-hero > .section-inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-7);
}

.case-hero__copy {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.case-hero__eyebrow {
  font-weight: 600;
}

/* The eyebrow's line: the case's sector on the left; its reading time and
   a TL;DR that asks Frand Agent for the summary on the right (case-hero.js,
   chat.js). The button is a square box on the section hairline, like the
   kicker tabs and the pager, with the agent's ✦ in the accent; hover lights
   its edge in the accent. */
.case-hero__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
}

/* Right of the eyebrow when the line has room; when it wraps (a phone, a
   longer language), under it from the left edge. */
.case-hero__tools {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.case-hero__read {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
  white-space: nowrap;
}

.case-hero__tldr {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 0;
  background: none;
  color: var(--color-text);
  font: inherit;
  font-size: var(--fs-small);
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.case-hero__tldr[hidden],
.case-hero__read[hidden] {
  display: none;
}

.case-hero__tldr:hover {
  border-color: var(--color-accent);
}

.case-hero__tldr svg {
  width: 12px;
  height: 12px;
  fill: var(--color-accent);
}

.case-hero__eyebrow sup {
  font-size: var(--fs-small);
  font-weight: 400;
  color: var(--color-text-secondary);
  margin-left: var(--space-2);
}

.case-hero h1 {
  max-width: 52rem;
  font-size: clamp(2rem, 5.5vw, 3.25rem);
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

/* A closed box opening a showcase block: text on the left, facts on the right. */
.case-box {
  display: grid;
  border: 1px solid var(--color-border);
  border-bottom: none;
  background: var(--color-bg-card-media);
}

.case-box__main {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-6) var(--space-5);
}

.case-hero__lead {
  font-size: 1.25rem;
  font-weight: 500;
  line-height: 1.4;
  text-wrap: pretty;
}

.case-hero__summary {
  color: var(--color-text-secondary);
}

.case-facts {
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--color-border);
}

/* Padding on the rows, not the list, so the rules run edge to edge. */
.case-facts > div {
  padding: var(--space-3) var(--space-5);
  border-bottom: 1px solid var(--color-border);
}

.case-facts > div:last-child {
  border-bottom: none;
}

.case-facts dt {
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

.case-facts dd {
  font-weight: 500;
}

/* The colour note of the opening: a solid block that runs to the box's foot. */
.case-facts > .case-facts__accent {
  flex: 1;
  border-bottom: none;
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

.case-facts__accent dt {
  color: inherit;
  opacity: 0.75;
}

.case-facts__accent dd {
  font-weight: 600;
}

@media (min-width: 768px) {
  .case-box {
    grid-template-columns: minmax(0, 8fr) minmax(0, 4fr);
  }

  .case-box__main {
    padding: var(--space-6);
  }

  .case-facts {
    border-top: none;
    border-left: 1px solid var(--color-border);
  }
}

/* Facts only, laid out as a row of cells; closes with a rule over what follows. */
.case-box--row {
  border-bottom: 1px solid var(--color-border);
  background: var(--color-bg-card-header);
}

.case-box--row .case-facts {
  border-top: none;
}

@media (min-width: 768px) {
  .case-box--row {
    grid-template-columns: none;
  }

  .case-box--row .case-facts {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    border-left: none;
  }

  .case-box--row .case-facts > div {
    padding: var(--space-4) var(--space-5);
    border-bottom: none;
    border-right: 1px solid var(--color-border);
  }

  .case-box--row .case-facts > div:last-child {
    border-right: none;
  }
}

.case-hero__media {
  width: 100%;
  border: 1px solid var(--color-border);
  background: var(--color-bg-card-media);
  overflow: hidden;
}

.case-hero__media img,
.case-hero__video {
  display: block;
  width: 100%;
  height: auto;
}

/* Same light-raster softening as the project cards. */
:root[data-theme="dark"] .case-hero__media > img,
:root[data-theme="dark"] .case-hero__media > picture > img,
:root[data-theme="dark"] .case-hero__video {
  filter: brightness(0.86) saturate(0.95);
}

/* ---- Nomo phone animation ---- */
.case-phone-figure {
  container-type: inline-size;
  aspect-ratio: 902 / 481;
}

@media (max-width: 767px) {
  .case-phone-figure {
    aspect-ratio: 390 / 844;
  }
}

.case-phone__stage {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  perspective: 1200px;
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  --phone-loop: 10s;
  --phone-camera: 14s;
  transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  transition: transform 0.1s ease-out;
}

@media (max-width: 767px) {
  .case-phone__stage {
    perspective: 800px;
  }
}

.case-phone__device {
  width: 40cqw;
  height: 100cqh;
  position: relative;
  border-radius: 2.2cqw;
  background: linear-gradient(135deg, #e8e8e8 0%, #d0d0d0 50%, #c0c0c0 100%);
  box-shadow:
    inset -1px -1px 0 rgba(0, 0, 0, 0.3),
    inset 1px 1px 0 rgba(255, 255, 255, 0.6),
    0 0 0 0.2cqw rgba(0, 0, 0, 0.1),
    0 2cqw 8cqw rgba(0, 0, 0, 0.15),
    0 0 30cqw color-mix(in oklab, var(--color-accent) 20%, transparent);
  transform-style: preserve-3d;
}

@media (max-width: 767px) {
  .case-phone__device {
    width: 56cqw;
  }
}

.js .case-phone {
  animation-play-state: paused;
}

.js .case-phone.is-playing {
  animation-play-state: running;
}

.case-phone__screen {
  position: absolute;
  top: 1.8cqw;
  left: 1cqw;
  right: 1cqw;
  bottom: 1.8cqw;
  border-radius: 1.8cqw;
  overflow: hidden;
  background: #000;
  transform-style: preserve-3d;
  isolation: isolate;
}

.case-phone__shot {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  animation: case-phone-banks var(--phone-loop) ease-in-out infinite;
}

.case-phone__shot--onboarding {
  animation: case-phone-onboarding var(--phone-loop) ease-in-out infinite;
}

@keyframes case-phone-banks {
  0% {
    opacity: 1;
    transform: translateX(0);
  }
  30% {
    opacity: 1;
    transform: translateX(0);
  }
  35% {
    opacity: 0.7;
    transform: translateX(-15%);
  }
  55% {
    opacity: 0;
    transform: translateX(-15%);
  }
  100% {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes case-phone-onboarding {
  0% {
    opacity: 0;
    transform: translateY(100%);
  }
  35% {
    opacity: 0;
    transform: translateY(100%);
  }
  42% {
    opacity: 1;
    transform: translateY(0);
  }
  70% {
    opacity: 1;
    transform: translateY(0);
  }
  77% {
    opacity: 0;
    transform: translateY(100%);
  }
  100% {
    opacity: 0;
    transform: translateY(100%);
  }
}

.case-phone__tap {
  position: absolute;
  top: 42%;
  right: 8%;
  width: 3cqw;
  height: 3cqw;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0.1) 70%, transparent 100%);
  pointer-events: none;
  opacity: 0;
  animation: case-phone-tap var(--phone-loop) ease-in-out infinite;
}

@keyframes case-phone-tap {
  28% {
    opacity: 0;
    transform: scale(1);
  }
  32% {
    opacity: 1;
    transform: scale(1);
  }
  34% {
    opacity: 0.8;
    transform: scale(0.95);
  }
  36% {
    opacity: 0;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: scale(1);
  }
}

.case-phone__ripple {
  position: absolute;
  top: 42%;
  right: 8%;
  width: 3cqw;
  height: 3cqw;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  animation: case-phone-ripple var(--phone-loop) ease-out infinite;
}

@keyframes case-phone-ripple {
  32% {
    opacity: 0;
    transform: scale(1);
  }
  34% {
    opacity: 1;
    transform: scale(1);
  }
  38% {
    opacity: 0;
    transform: scale(1.8);
  }
  100% {
    opacity: 0;
    transform: scale(1.8);
  }
}

.case-phone__sheen {
  position: absolute;
  top: 35%;
  left: 5%;
  width: 90%;
  height: 30%;
  background: linear-gradient(
    135deg,
    transparent 0%,
    rgba(255, 255, 255, 0.15) 20%,
    rgba(255, 255, 255, 0.3) 50%,
    rgba(255, 255, 255, 0.15) 80%,
    transparent 100%
  );
  border-radius: 1cqw;
  pointer-events: none;
  animation: case-phone-sheen var(--phone-loop) ease-in-out infinite;
}

@keyframes case-phone-sheen {
  0% {
    opacity: 0;
  }
  45% {
    opacity: 0;
  }
  52% {
    opacity: 0.8;
  }
  58% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

.case-phone__nfc-pulse {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 12cqw;
  height: 12cqw;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(100, 200, 255, 0.4) 0%, rgba(100, 200, 255, 0.2) 40%, transparent 70%);
  box-shadow:
    0 0 1.5cqw rgba(100, 200, 255, 0.3),
    inset 0 0 2cqw rgba(100, 200, 255, 0.2);
  pointer-events: none;
  opacity: 0;
  animation: case-phone-nfc var(--phone-loop) ease-in-out infinite;
}

@keyframes case-phone-nfc {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.8);
  }
  40% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.8);
  }
  42% {
    opacity: 0.8;
    transform: translate(-50%, -50%) scale(1);
  }
  50% {
    opacity: 0.6;
    transform: translate(-50%, -50%) scale(1.2);
  }
  68% {
    opacity: 0.3;
    transform: translate(-50%, -50%) scale(1);
  }
  75% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.8);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.8);
  }
}

.case-phone__glass {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 1.8cqw;
  background: linear-gradient(45deg, transparent 40%, rgba(255, 255, 255, 0.08) 50%, transparent 60%);
  pointer-events: none;
}

.case-phone__button {
  position: absolute;
  width: 0.25cqw;
  height: 2cqw;
  background: linear-gradient(90deg, #b8b8b8, #a0a0a0);
  border-radius: 0.1cqw;
  box-shadow:
    inset 0.5px 0 0 rgba(255, 255, 255, 0.4),
    inset -0.5px 0 0 rgba(0, 0, 0, 0.2);
}

.case-phone__button--top-left {
  top: 6cqw;
  left: -0.3cqw;
}

.case-phone__button--top-right {
  top: 8cqw;
  right: -0.3cqw;
}

.case-phone__button--bottom-left {
  top: 50%;
  left: -0.3cqw;
  transform: translateY(-50%);
  height: 2.5cqw;
}

.case-phone__button--bottom-right {
  bottom: 20cqw;
  right: -0.3cqw;
  height: 2.5cqw;
}

.case-phone__island {
  position: absolute;
  top: 0.35cqw;
  left: 50%;
  transform: translateX(-50%);
  width: 8cqw;
  height: 1cqw;
  background: #000;
  border-radius: 0.8cqw;
  box-shadow:
    inset 0 0 1cqw rgba(0, 0, 0, 0.8),
    0 0 0.5cqw rgba(0, 0, 0, 0.3);
  z-index: 10;
}

.case-phone__island::before {
  content: '';
  position: absolute;
  top: 50%;
  right: 1.2cqw;
  width: 0.6cqw;
  height: 0.6cqw;
  background: radial-gradient(circle at 30% 30%, #4a4a4a, #1a1a1a);
  border-radius: 50%;
  transform: translateY(-50%);
  box-shadow: 0 0 0.3cqw rgba(0, 0, 0, 0.5);
}

.case-phone__device {
  animation: case-phone-camera var(--phone-camera) ease-in-out alternate infinite;
}

@keyframes case-phone-camera {
  0% {
    transform: rotateX(8deg) rotateZ(-2deg) scale(1);
  }
  35% {
    transform: rotateX(8deg) rotateZ(-2deg) scale(1);
  }
  42% {
    transform: rotateX(12deg) rotateZ(-1deg) scale(1.06);
  }
  52% {
    transform: rotateX(10deg) rotateZ(0deg) scale(1.06);
  }
  70% {
    transform: rotateX(10deg) rotateZ(0deg) scale(1.03);
  }
  77% {
    transform: rotateX(8deg) rotateZ(-2deg) scale(1);
  }
  100% {
    transform: rotateX(8deg) rotateZ(-2deg) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-phone__stage {
    transition: none;
  }

  .case-phone__shot,
  .case-phone__shot--onboarding,
  .case-phone__tap,
  .case-phone__ripple,
  .case-phone__sheen,
  .case-phone__nfc-pulse,
  .case-phone__device {
    animation: none !important;
  }

  .case-phone__shot {
    opacity: 1 !important;
    transform: translateX(0) !important;
  }

  .case-phone__shot--onboarding {
    opacity: 1 !important;
    transform: translateY(0) !important;
  }

  .case-phone__nfc-pulse {
    opacity: 0 !important;
  }
}

/* ---- Sections: epigraph tab, headline, subheads, labels ---- */
.case-section {
  border-top: 1px solid var(--color-border);
  padding-bottom: var(--space-9);
}

.case-section .case-kicker {
  margin: 0 0 var(--space-6);
}

.case-headline {
  max-width: 46rem;
  font-size: clamp(1.75rem, 4.5vw, 2.5rem);
  line-height: 1.1;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

:where(.case-section) > .section-inner > .case-headline + * {
  margin-top: var(--space-6);
}

:where(.case-section) > .section-inner > .case-standfirst + .case-showcase {
  margin-top: var(--space-6);
}

.case-subhead {
  font-size: 1.25rem;
  line-height: 1.25;
  text-wrap: balance;
}

:where(.case-section) > .section-inner > .case-subhead--spaced {
  margin-top: var(--space-8);
}

:where(.case-section) > .section-inner > .case-subhead--spaced + * {
  margin-top: var(--space-5);
}

/* An intro line under the title keeps what it introduces close. */
:where(.case-section) > .section-inner > .case-subhead--spaced + .case-prose + * {
  margin-top: var(--space-5);
}

.case-label {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--color-text-secondary);
}
.case-caption {
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-border);
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

.case-prose {
  max-width: 40rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.case-prose strong {
  font-weight: 600;
}

.case-section h4 {
  margin: 0;
}

.case-standfirst {
  font-size: 1.125rem;
  line-height: 1.5;
}

/* ---- Two-column spreads ---- */
.case-cols {
  display: grid;
  /* minmax(0) keeps a wide child (a scrolling map, a carousel track) from
     stretching the only track, and the page with it, past the viewport. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
}

.case-cols > * {
  min-width: 0;
}

@media (min-width: 768px) {
  .case-cols {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--space-6);
  }

  .case-cols--even > * {
    grid-column: span 6;
  }

  /* A journey map under a two-column spread takes the whole row. */
  .case-cols > .case-journey {
    grid-column: 1 / -1;
  }

  .case-cols--even > :only-child {
    grid-column: span 8;
  }

  .case-cols--aside > :first-child {
    grid-column: span 4;
  }

  .case-cols--aside > :last-child {
    grid-column: span 8;
  }
}

/* ---- Highlight: a sentence the section turns on ---- */
.case-highlight {
  margin: var(--space-8) 0;
  padding: var(--space-6) 0;
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}

:where(.case-section) > .section-inner > .case-highlight {
  margin-block: var(--space-8);
}

.case-highlight::before {
  content: "";
  display: block;
  width: 2.5rem;
  height: 3px;
  margin-bottom: var(--space-4);
  background: var(--color-accent-fill);
}

.case-highlight p {
  max-width: 48rem;
  font-size: clamp(1.5rem, 4vw, 2.25rem);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.01em;
  text-wrap: balance;
}

/* ---- Diagrams ---- */
.case-scheme {
  padding: var(--space-5);
  border: 1px solid var(--color-border);
  background: var(--color-bg-card-header);
}

/* ---- At a glance: boxes hanging off the hero image, as its continuation ---- */
.case-showcase {
  display: flex;
  flex-direction: column;
}

/* An image slot inside a showcase, between the box above and the rows below. */
.case-showcase__media {
  margin: 0;
  border: 1px solid var(--color-border);
  border-bottom: none;
  background: var(--color-bg-card-media);
}

.case-showcase__slot {
  aspect-ratio: 21 / 9;
  border: none;
  border-radius: 0;
}

.case-showcase__media img,
.case-figure img {
  display: block;
  width: 100%;
  height: auto;
}

/* A loose image (white ground) sits in the same frame as the placeholders. */
.case-figure img {
  border: 1px solid var(--color-border);
  background: #ffffff;
}

/* …unless it is a mockup cut out on transparency, which carries its own
   shadow and needs neither. */
.case-figure img.case-figure__cutout {
  border: none;
  background: none;
}

/* Same light-raster softening as the hero image; a cutout has no light
   ground to soften. */
:root[data-theme="dark"] .case-showcase__media img,
:root[data-theme="dark"] .case-figure img:not(.case-figure__cutout) {
  filter: brightness(0.86) saturate(0.95);
}

/* Two images side by side; the 1px gap over a border fill draws the rule. */
.case-media-pair {
  display: grid;
  gap: 1px;
  border: 1px solid var(--color-border);
  border-bottom: none;
  background: var(--color-border);
}

.case-media-pair .case-showcase__media {
  border: none;
}

/* 16:9, the shape of the videos that fill these slots. */
.case-media-pair .case-showcase__slot,
.case-media-pair .case-showcase__video,
.case-media-pair .case-showcase__image {
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.case-showcase__video {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
  background: var(--color-bg-card-media);
}

@media (min-width: 768px) {
  .case-media-pair {
    grid-template-columns: 1fr 1fr;
  }
}

/* A fact sheet closing a showcase: label column, then content. */
.case-sheet {
  border: 1px solid var(--color-border);
  background: var(--color-bg-card-media);
}

.case-sheet > div {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-5);
}

.case-sheet > div + div {
  border-top: 1px solid var(--color-border);
}

.case-sheet dt {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--color-text-secondary);
}

.case-sheet dd {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 44rem;
}

@media (min-width: 768px) {
  .case-sheet > div {
    grid-template-columns: 9rem 1fr;
    column-gap: var(--space-6);
    padding: var(--space-5) var(--space-6);
  }
}

/* The 1px gap over a border-colored fill draws the shared rules. */
.case-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  border: 1px solid var(--color-border);
  border-top: none;
  background: var(--color-border);
}

.case-stat {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-5);
  background: var(--color-bg-card-media);
}

.case-stat__value {
  margin-bottom: var(--space-2);
  font-size: clamp(2.25rem, 4vw, 2.75rem);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.case-stat__label {
  font-weight: 500;
}

.case-nowrap {
  white-space: nowrap;
}

.case-stat__note {
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

@media (min-width: 1024px) {
  .case-stats {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* Two figures (Easor): the lead one twice as wide, and larger. */
.case-stats.case-stats--pair {
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
}

.case-stat--lead .case-stat__value {
  font-size: clamp(2.75rem, 6vw, 4rem);
}

/* A diagram in the hero's chain (GrowPro's system map): it hangs off the
   block above, so it drops its top rule. */
.case-hero .case-showcase > .case-scheme {
  border-top: none;
}

/* ---- Role and constraints: accordions closing the hero block ---- */
.case-accordions {
  border: 1px solid var(--color-border);
  border-top: none;
  background: var(--color-bg-card-media);
}

.case-accordion + .case-accordion {
  border-top: 1px solid var(--color-border);
}

.case-accordion summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  font-weight: 600;
  list-style: none;
  cursor: pointer;
}

.case-accordion summary::-webkit-details-marker {
  display: none;
}

.case-accordion summary::after {
  content: "+" / "";
  flex: none;
  width: 1ch;
  font-size: 1.25rem;
  font-weight: 400;
  line-height: 1;
  text-align: center;
  color: var(--color-text-secondary);
}

.case-accordion[open] summary::after {
  content: "−" / "";
}

/* Closed, an accordion sits on the tinted ground of the strips and schemes;
   open, it turns white like the page it reveals. */
.case-accordion:not([open]) {
  background: var(--color-bg-card-header);
  transition: background-color 0.2s ease;
}

.case-accordion:not([open]):hover {
  background: color-mix(in oklab, var(--color-bg-card-header) 70%, var(--color-bg-card-media));
}

.case-accordion summary:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

.case-accordion__body {
  max-width: 48rem;
  padding: 0 var(--space-5) var(--space-5);
}

/* A table as the body: edge to edge, its rows on the accordion's own
   hairlines instead of a frame inside the frame. Text before it keeps the
   body's inset, and so do the table's outer cells. */
.case-accordion__body--table {
  max-width: none;
  padding: 0;
}

.case-accordion__body--table > p {
  max-width: 48rem;
  padding: 0 var(--space-5) var(--space-4);
}

.case-accordion__body--table > .case-compare {
  border-width: 1px 0 0;
}

/* ---- Before and after around a phone (GrowPro's at a glance) ----
   On a phone: the stacked table, then the mockup alone on its own
   hairline. From tablet up: one grid, the old way, the mockup and the new
   way (1/4, 2/4, 1/4) between hairlines. Each side is a column of cells,
   its heading then one cell per topic, the topic named above its line;
   the sides share the grid's rows (subgrid), so a topic sits level on
   both sides, and the mockup is framed in the middle, across all rows. */
.case-ba {
  display: grid;
  gap: var(--space-6);
  padding: var(--space-6) var(--space-5);
  border-top: 1px solid var(--color-border);
}

.case-ba__side {
  display: none;
}

.case-ba__head {
  font-weight: 600;
}

.case-ba__sub {
  display: block;
  font-size: var(--fs-small);
  font-weight: 400;
  color: var(--color-text-secondary);
}

.case-ba__list dt {
  font-size: 0.75rem;
  color: var(--color-text-secondary);
}

.case-ba__list dd {
  margin: 0;
  font-size: var(--fs-small);
  line-height: 1.35;
}

/* The new way keeps the table's tinted strip and its weight. */
.case-ba__side--after {
  background: var(--color-accent-tint);
}

.case-ba__side--after dd {
  font-weight: 500;
}

@media (min-width: 768px) {
  .case-ba__table {
    display: none;
  }

  .case-ba {
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr);
    grid-template-rows: auto repeat(6, auto);
    gap: 0;
    padding: 0;
  }

  .case-ba__side {
    display: grid;
    grid-row: 1 / -1;
    grid-template-rows: subgrid;
  }

  .case-ba__side--before {
    grid-column: 1;
  }

  .case-ba__side--after {
    grid-column: 3;
  }

  /* The topics become the side's own cells. */
  .case-ba__list {
    display: contents;
  }

  .case-ba__head,
  .case-ba__list > div {
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--color-border);
  }

  .case-ba__list > div:last-child {
    border-bottom: none;
  }

  .case-ba .case-mockup {
    grid-column: 2;
    grid-row: 1 / -1;
    display: grid;
    align-content: center;
    max-width: none;
    padding: var(--space-6) var(--space-5);
    border-inline: 1px solid var(--color-border);
  }

  .case-ba .case-mockup__device {
    width: 100%;
    max-width: 18rem;
    margin: 0 auto;
  }
}

.case-accordion__body--table .case-compare--text tbody tr {
  padding-inline: var(--space-5);
}

@container (min-width: 28rem) {
  .case-accordion__body--table .case-compare th:first-child,
  .case-accordion__body--table .case-compare td:first-child {
    padding-left: var(--space-5);
  }

  .case-accordion__body--table .case-compare th:last-child,
  .case-accordion__body--table .case-compare td:last-child {
    padding-right: var(--space-5);
  }
}

.case-forward a {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  font-size: var(--fs-small);
  font-weight: 500;
  transition: background-color 0.25s ease;
}

.case-forward a:hover {
  background: var(--color-bg-card-header);
}

/* ---- Role and constraints ---- */
.case-bullets {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-left: 1.1em;
  list-style: disc;
}

.case-bullets li::marker {
  color: var(--color-accent);
}

.case-bullets strong {
  font-weight: 600;
}

/* ---- 01: the two accounts, and the two gains ---- */
/* A feature table: one row per capability, a mark per account. */
.case-compare {
  align-self: start;
  container-type: inline-size;
  padding: 0;
  background: var(--color-bg-card-media);
}

.case-compare table {
  width: 100%;
  border-collapse: collapse;
}

.case-compare th,
.case-compare td {
  padding: var(--space-3) var(--space-2);
  border-bottom: 1px solid var(--color-border);
  text-align: left;
  vertical-align: middle;
}

.case-compare tbody tr:last-child th,
.case-compare tbody tr:last-child td {
  border-bottom: none;
}

.case-compare thead th {
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
  vertical-align: bottom;
}

.case-compare th:first-child,
.case-compare td:first-child {
  padding-left: var(--space-4);
}

.case-compare th:last-child,
.case-compare td:last-child {
  padding-right: var(--space-4);
}

.case-compare tbody th {
  font-size: var(--fs-small);
  font-weight: 500;
}

.case-compare tbody td {
  width: 1%;
  text-align: center;
}

/* The Nomo column reads as one tinted strip from header to last row. */
.case-compare .is-ours,
.case-compare tbody td:last-child {
  background: var(--color-accent-tint);
}

.case-compare__head {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-2);
  font-weight: 600;
  white-space: nowrap;
}

.case-compare__sub {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--fs-small);
  font-weight: 400;
  color: var(--color-text-secondary);
}

/* Too narrow for three columns: each feature takes a full line, its two
   marks sit under the account headers. The full-width feature lines would
   chop the Nomo strip into loose blocks, so the tint moves to them instead:
   tinted bands dividing the rows, both marks on the same ground. */
@container (max-width: 28rem) {
  .case-compare thead tr,
  .case-compare tbody tr {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }

  .case-compare thead td {
    display: none;
  }

  .case-compare .is-ours,
  .case-compare tbody td:last-child {
    background: none;
  }

  .case-compare tbody th {
    grid-column: 1 / -1;
    padding-block: var(--space-2);
    border-bottom: none;
    background: var(--color-accent-tint);
  }

  .case-compare tbody td {
    width: auto;
  }

  .case-compare th,
  .case-compare td {
    padding-inline: var(--space-2);
  }

  .case-compare thead th:first-of-type,
  .case-compare tbody td:first-of-type,
  .case-compare tbody th {
    padding-left: var(--space-3);
  }

  .case-compare th:last-child,
  .case-compare td:last-child {
    padding-right: var(--space-2);
  }

  .case-compare__head {
    gap: var(--space-1);
    font-size: 0.9375rem;
  }
}

/* Before → after in words: the same table, with short phrases in place of
   the marks. The row names a topic; "after" keeps the tinted strip. */
.case-compare--text tbody th,
.case-compare--text tbody td {
  vertical-align: top;
}

.case-compare--text tbody th {
  width: 24%;
  color: var(--color-text);
}

.case-compare--text tbody td {
  width: 38%;
  text-align: left;
}

.case-compare--text tbody td:last-child {
  color: var(--color-text);
  font-weight: 500;
}

/* The column's name inside each cell, shown only once the table stacks
   (see .case-compare--who further down). */
.case-compare__col {
  display: none;
  color: var(--color-text-secondary);
  font-weight: 400;
}

.case-compare__col::after {
  content: ":";
}

/* Stacked (see .case-compare above): each phrase fills its half. */
@container (max-width: 28rem) {
  .case-compare--text tbody th,
  .case-compare--text tbody td {
    width: auto;
  }
}

/* An accordion group standing on its own in a section, not hanging off a box. */
.case-accordions--standalone {
  border-top: 1px solid var(--color-border);
}

.case-mark {
  display: inline-grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1;
}

.case-mark--yes {
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

.case-mark--no {
  border: 1.5px solid var(--color-border);
  color: var(--color-text-secondary);
}

.case-pill {
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--color-text-secondary);
  white-space: nowrap;
}

.case-pill--accent {
  border-color: var(--color-accent-fill);
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

/* ---- 02: the hypothesis, as a full-bleed break in the story ---- */
/* Striped side bands; the solid center column carries the text, so the
   stripes never sit behind it. */
.case-break {
  background-color: var(--color-accent-fill);
  /* Diagonal stripes in the text color, faint. */
  background-image: repeating-linear-gradient(
    135deg,
    color-mix(in srgb, var(--color-on-accent-fill) 9%, transparent) 0 2px,
    transparent 2px 14px
  );
  color: var(--color-on-accent-fill);
  text-align: center;
}

.case-break > .section-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
  width: min(calc(48rem + 2 * var(--page-padding)), calc(100% - 2 * var(--space-4)));
  margin-inline: auto;
  padding-block: var(--space-9);
  background: var(--color-accent-fill);
}

.case-break p {
  color: inherit;
}

.case-break__text {
  font-size: clamp(1.5rem, 3.5vw, 2.25rem);
  line-height: 1.15;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

/* ---- 03: findings as accordions, quotes as sticky notes ---- */
.case-accordion__title {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
}

.case-decisions__text h4 {
  font-size: 1.125rem;
  font-weight: 600;
  line-height: 1.3;
  text-wrap: balance;
}

.case-accordion__body.case-finding {
  max-width: none;
}

.case-finding {
  display: grid;
  gap: var(--space-5);
  align-items: start;
}

.case-finding__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Same paper, glue strip and shadow as the home's principle cards. It hangs
   past the accordion's right edge, by less than the page gutter. */
.case-postit {
  --tilt: -1.5deg;
  justify-self: end;
  width: min(100%, 18rem);
  margin: 0 calc((var(--space-5) + var(--space-4)) * -1) 0 0;
  padding: var(--space-6) var(--space-5);
  border-radius: 2px;
  color: #2b2819;
  background:
    linear-gradient(to bottom, rgb(0 0 0 / 0.05), transparent 18%),
    #fbee9c;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.08),
              0 18px 24px -14px rgb(0 0 0 / 0.45);
  font-family: "Inter", sans-serif;
  font-style: italic;
  font-size: 1.125rem;
  line-height: 1.4;
  text-wrap: pretty;
  transform: rotate(var(--tilt));
  transform-origin: 50% 0;
}

.case-accordion:nth-of-type(even) .case-postit {
  --tilt: 1.2deg;
}

/* Two notes side by side; only the last one hangs past the edge. */
.case-postits {
  display: grid;
  gap: var(--space-5);
}

.case-postits .case-postit {
  justify-self: stretch;
  width: auto;
  margin: 0;
}

.case-postits .case-postit:last-child {
  --tilt: 1.2deg;
  margin-right: calc((var(--space-5) + var(--space-4)) * -1);
}

.case-postit--note {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-family: inherit;
  font-style: normal;
  font-size: var(--fs-body);
}

.case-postit--note strong {
  font-size: 1.125rem;
  font-weight: 600;
}

:root[data-theme="dark"] .case-postit {
  background:
    linear-gradient(to bottom, rgb(0 0 0 / 0.05), transparent 18%),
    #e6d98b;
}

@media (min-width: 768px) {
  .case-finding {
    grid-template-columns: minmax(0, 1fr) minmax(0, 18rem);
    column-gap: var(--space-7);
  }

  .case-finding > .case-postit:only-child {
    grid-column: 2;
  }

  /* A finding beside its screen: text and image share the row. */
  .case-finding--media {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }

  .case-postit {
    margin-top: calc(var(--space-3) * -1);
  }

  .case-postits {
    grid-column: 1 / -1;
    grid-template-columns: repeat(2, minmax(0, 18rem));
    justify-content: end;
    column-gap: var(--space-6);
  }
}

/* ---- 03: the lapse, a thought that takes over the screen ---- */
/* Without the script the steps simply sit in the page, centred. */
.case-lapse__stage {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  text-align: center;
}

.case-lapse__veil {
  display: none;
}

.case-lapse__step {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
}

.case-lapse__verdict {
  max-width: 44rem;
  font-size: clamp(1.75rem, 4.5vw, 2.75rem);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.case-lapse__checks {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-width: 34rem;
  text-align: left;
}

.case-lapse__checks li {
  display: grid;
  grid-template-columns: 1.75rem 1fr;
  column-gap: var(--space-3);
  align-items: start;
  font-size: clamp(1.0625rem, 2.4vw, 1.25rem);
  line-height: 1.45;
}

.case-lapse__mark {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  margin-top: 0.1em;
  border-radius: 50%;
  font-size: 0.875rem;
  font-weight: 700;
  line-height: 1;
}

.case-lapse__checks .is-no .case-lapse__mark {
  border: 1.5px solid var(--color-error);
  color: var(--color-error);
}

.case-lapse__checks .is-yes .case-lapse__mark {
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

.case-lapse__label {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--color-text-secondary);
}

.case-lapse__from s {
  font-size: clamp(1.125rem, 3vw, 1.5rem);
  color: var(--color-text-secondary);
  text-decoration-thickness: 2px;
}

.case-lapse__closing {
  max-width: 36rem;
  font-size: clamp(1.125rem, 2.6vw, 1.375rem);
  line-height: 1.5;
  text-wrap: pretty;
}

.case-lapse .case-forward {
  justify-content: center;
}

.case-lapse__to {
  max-width: 44rem;
  font-size: clamp(2.25rem, 7vw, 4.5rem);
  font-weight: 600;
  line-height: 1.02;
  letter-spacing: -0.025em;
  text-wrap: balance;
}

/* With scripts on (.js, set before first paint by theme-boot.js): the block
   is only a marker that opens the lapse as a modal when scrolled past, and
   its stage a fixed overlay over the content column only (case-lapse.js sets
   its box), under the header (50) so the top bar stays in view. Keyed to .js
   rather than a class the deferred script adds, so the page is laid out this
   way from the first frame: switching it later made the steps flash over the
   hero while their fade-out ran. */
.js .case-lapse {
  height: 0;
}

/* Closed once: room for the replay button. */
.js .case-lapse.is-seen {
  height: auto;
}

/* While the lapse is open the page behind stays put. */
html.is-lapse-locked {
  overflow: hidden;
  scrollbar-gutter: stable;
}

/* No transitions until case-lapse.js has set the first state. */
.js .case-lapse:not(.is-ready) .case-lapse__stage,
.js .case-lapse:not(.is-ready) .case-lapse__veil,
.js .case-lapse:not(.is-ready) .case-lapse__progress,
.js .case-lapse:not(.is-ready) .case-lapse__step {
  transition: none;
}

.js .case-lapse .case-lapse__stage {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  z-index: 45;
  display: block;
  visibility: hidden;
  pointer-events: none;
  transition: visibility 0s linear 0.7s;
}

.js .case-lapse.is-open .case-lapse__stage {
  visibility: visible;
  pointer-events: auto;
  transition-delay: 0s;
  /* Holding pauses (case-lapse.js): no text selection or long-press callout. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Focus lands on the stage itself when it opens; it needs no ring. */
.case-lapse__stage:focus {
  outline: none;
}

/* The thought: the page goes soft behind a cloudy veil that opens from the
   centre. */
.js .case-lapse .case-lapse__veil {
  position: absolute;
  inset: 0;
  display: block;
  /* A light, nearly opaque base for contrast, lit from the centre. */
  background:
    radial-gradient(55% 45% at 50% 50%, var(--color-accent-tint), transparent 75%),
    color-mix(in oklab, var(--color-bg-card-media) 94%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(0.85);
  backdrop-filter: blur(18px) saturate(0.85);
  clip-path: circle(0% at 50% 50%);
  transition: clip-path 0.7s cubic-bezier(0.65, 0, 0.35, 1);
}

.case-lapse.is-open .case-lapse__veil {
  clip-path: circle(80% at 50% 50%);
}

/* At night a lighter base would cost contrast; keep it deep instead. */
:root.js[data-theme="dark"] .case-lapse .case-lapse__veil {
  background:
    radial-gradient(55% 45% at 50% 50%, var(--color-accent-tint), transparent 75%),
    color-mix(in oklab, var(--color-bg) 94%, transparent);
}

/* How much of the lapse is left: fills left to right as it scrolls. */
.case-lapse__progress {
  display: none;
}

/* One segment per slide, like stories: a gap between them shows the count. */
.js .case-lapse .case-lapse__progress {
  position: absolute;
  top: var(--space-2);
  right: var(--space-3);
  left: var(--space-3);
  z-index: 1;
  display: flex;
  gap: var(--space-1);
  height: 4px;
  opacity: 0;
  transition: opacity 0.4s ease;
}

.case-lapse.is-open .case-lapse__progress {
  opacity: 1;
  transition-delay: 0.3s;
}

/* Held: the bar dims while the clock stands still. */
.case-lapse.is-open.is-paused .case-lapse__progress {
  opacity: 0.5;
  transition-delay: 0s;
}

.case-lapse__progress span {
  flex: 1;
  overflow: hidden;
  border-radius: 2px;
  background: color-mix(in oklab, var(--color-text) 12%, transparent);
}

.case-lapse__progress i {
  display: block;
  height: 100%;
  background: var(--color-accent-fill);
  transform: scaleX(0);
  transform-origin: left;
}

/* Close, top right under the progress bar; back, pause and forward along the
   bottom. Without the script there's nothing for them to do. */
.case-lapse__close,
.case-lapse__controls {
  display: none;
}

.js .case-lapse .case-lapse__close,
.js .case-lapse .case-lapse__controls {
  position: absolute;
  z-index: 2;
  opacity: 0;
  transition: opacity 0.4s ease;
}

.js .case-lapse .case-lapse__close {
  top: calc(var(--space-2) + 4px + var(--space-2));
  right: var(--space-2);
  display: flex;
}

.case-lapse.is-open .case-lapse__close,
.case-lapse.is-open .case-lapse__controls {
  opacity: 1;
  transition-delay: 0.3s;
}

.case-lapse__close {
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: color 0.2s ease, background-color 0.2s ease;
}

.case-lapse__close:hover {
  color: var(--color-text);
  background: color-mix(in oklab, var(--color-text) 8%, transparent);
}

.case-lapse__close svg {
  width: 22px;
  height: 22px;
}

/* Paused by hand: the button offers play instead. */
.case-lapse__icon-play,
[data-lapse-pause][aria-pressed="true"] .case-lapse__icon-pause {
  display: none;
}

[data-lapse-pause][aria-pressed="true"] .case-lapse__icon-play {
  display: block;
}

/* Left in the lapse's place once it has been closed: a quiet way back in. */
.case-lapse__replay {
  display: none;
}

.js .case-lapse.is-seen .case-lapse__replay {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0 auto;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.js .case-lapse.is-seen .case-lapse__replay:hover {
  color: var(--color-text);
  border-color: var(--color-text-secondary);
}

.case-lapse__replay svg {
  width: 16px;
  height: 16px;
}

/* Touch steps through with taps on the sides; the buttons are for a mouse. */
@media (hover: hover) and (pointer: fine) {
  .js .case-lapse .case-lapse__controls {
    bottom: 0;
    left: 50%;
    display: flex;
    transform: translateX(-50%);
  }
}

.js .case-lapse .case-lapse__step {
  position: absolute;
  inset: 0;
  justify-content: center;
  padding: var(--space-8) var(--page-padding);
  opacity: 0;
  filter: blur(8px);
  transform: translateY(24px) scale(0.98);
  transition: opacity 0.6s ease,
              filter 0.6s ease,
              transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

.js .case-lapse .case-lapse__step.is-current {
  opacity: 1;
  filter: none;
  transform: none;
  transition-delay: 0.15s;
}

@media (prefers-reduced-motion: reduce) {
  .js .case-lapse .case-lapse__veil,
  .js .case-lapse .case-lapse__step {
    transition: none;
  }

  .js .case-lapse .case-lapse__step {
    filter: none;
    transform: none;
  }
}

.case-forward {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* ---- Visuals still to come: the .placeholder utility, sized ---- */
.case-figure {
  margin: 0;
}

/* A loose figure keeps the section's block rhythm above it. */
:where(.case-section) > .section-inner > * + .case-figure {
  margin-top: var(--space-7);
}

.case-figure__slot {
  aspect-ratio: 3 / 2;
}

/* ---- 04: pricing before and after, the call, the result ---- */
.case-verdicts > div {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-top: var(--space-4);
  border-top: 2px solid var(--color-text);
}


/* ---- 05: numbered decisions beside their visuals ---- */
.case-decisions > li {
  align-items: start;
  padding: var(--space-6) 0;
  border-top: 1px solid var(--color-border);
}

/* Decision 01: text first, its two images in a row underneath. */
.case-decisions__stacked {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.case-decisions__stacked > .case-decisions__text {
  max-width: 40rem;
}

/* Before / after in one frame (case-compare.js): both images stacked in the
   same cell, each clipped at --pos so the cutouts' shadows never double up,
   and a divider with a knob at the split. An invisible range input covers
   the frame: dragged, or stepped with the arrow keys once focused.
   Scoped to [data-compare]: the before/after tables share the class name,
   and this margin reset would otherwise eat their block rhythm. */
.case-compare[data-compare] {
  position: relative;
  display: grid;
  width: 100%;
  margin: 0;
  --pos: 50%;
}

.case-compare > img,
.case-compare > picture {
  display: block;
  grid-area: 1 / 1;
  width: 100%;
  height: auto;
  transition: clip-path 0.2s ease;
}

.case-compare__before {
  clip-path: inset(0 calc(100% - var(--pos)) 0 0);
}

.case-compare__after {
  clip-path: inset(0 0 0 var(--pos));
}

.case-tag.case-compare__tag-after {
  left: auto;
  right: var(--space-3);
}

.case-compare__range {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: ew-resize;
  /* A vertical swipe still scrolls the page; a sideways one drags. */
  touch-action: pan-y;
  -webkit-appearance: none;
  appearance: none;
}

.case-compare__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos);
  z-index: 1;
  width: 2px;
  margin-left: -1px;
  /* Faded at the ends, where the cutouts are only transparency and shadow. */
  background: linear-gradient(transparent, var(--color-accent-fill) 6%, var(--color-accent-fill) 94%, transparent);
  pointer-events: none;
  transition: left 0.2s ease;
}

/* The knob: the accent circle the pair's arrow used, now pointing both ways. */
.case-compare__handle::after {
  content: "‹ ›";
  position: absolute;
  top: 50%;
  left: 50%;
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
  font-weight: 600;
  letter-spacing: 0.1em;
  white-space: nowrap;
  box-shadow: 0 4px 14px rgb(var(--shadow-rgb) / 0.2);
  transform: translate(-50%, -50%);
  transition: transform 0.2s ease;
}

.case-compare.is-dragging .case-compare__handle::after {
  transform: translate(-50%, -50%) scale(1.1);
}

/* The input is invisible, so the knob shows its focus. */
.case-compare__range:focus-visible {
  outline: none;
}

.case-compare__range:focus-visible + .case-compare__handle::after {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}

.case-compare.is-dragging > img,
.case-compare.is-dragging > picture,
.case-compare.is-dragging .case-compare__handle {
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .case-compare > img,
  .case-compare > picture,
  .case-compare__handle,
  .case-compare__handle::after {
    transition: none;
  }
}

/* ---- A before → after inside a phone (GrowPro's storefront) ----
   The mockup is on top and lets the pointer through; under its transparent
   screen, both long screenshots share one scroll so they stay level, and
   the divider clips them as in .case-compare. The screen box is measured
   on the mockup (2048 × 4290: screen at 114, 92, 1811 × 4093, radius 200).
   Only the knob's strip drags, so the rest of the screen scrolls. */
.case-mockup {
  --pos: 50%;
  position: relative;
  width: 100%;
  max-width: 18rem;
  margin: 0 auto;
}

/* The mockup and its screen: the box the screen's percentages refer to. */
.case-mockup__device {
  position: relative;
}

.case-mockup__frame {
  position: relative;
  z-index: 3;
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
}

.case-mockup__screen {
  position: absolute;
  top: 2.14%;
  left: 5.57%;
  width: 88.43%;
  height: 95.41%;
  overflow: hidden;
  border-radius: 11% / 4.9%;
  background: #ffffff;
}

.case-mockup__scroll {
  height: 100%;
  overflow-y: auto;
  scrollbar-width: none;
}

.case-mockup__scroll::-webkit-scrollbar {
  display: none;
}

.case-mockup__scroll:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

.case-mockup__stack {
  display: grid;
}

.case-mockup__stack > img {
  grid-area: 1 / 1;
  display: block;
  width: 100%;
  height: auto;
  transition: clip-path 0.2s ease;
}

.case-mockup .case-compare__range {
  inset: 0 auto;
  left: calc(var(--pos) - 1.5rem);
  width: 3rem;
  touch-action: none;
  transition: left 0.2s ease;
}

.case-mockup .case-compare__handle {
  background: var(--color-accent-fill);
}

.case-mockup.is-dragging .case-mockup__stack > img,
.case-mockup.is-dragging .case-compare__handle,
.case-mockup.is-dragging .case-compare__range {
  transition: none;
}

.case-mockup.is-dragging .case-compare__handle::after {
  transform: translate(-50%, -50%) scale(1.1);
}

@media (prefers-reduced-motion: reduce) {
  .case-mockup__stack > img,
  .case-mockup .case-compare__range {
    transition: none;
  }
}

.case-tag {
  position: absolute;
  top: var(--space-3);
  left: var(--space-3);
  z-index: 1;
  padding: 0.125rem var(--space-2);
  border-radius: 999px;
  background: var(--color-text);
  color: var(--color-on-inverse);
  font-size: 0.75rem;
  font-weight: 600;
}

.case-tag--accent {
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

/* One storefront, two people on it: student and advisor either side, each
   with an arrow into the app between them. On phones it stacks: the student
   above pointing down, the advisor below pointing up. */
.case-actors-figure {
  margin-inline: 0;
  margin-bottom: 0;
}

/* The screens are cut out on transparency with their own shadow, so the
   whole row sits on a ground: the panel the case hero's video sits on. */
.case-actors {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-4);
  border: 1px solid var(--color-border);
  background: var(--color-bg-card-media);
}

.case-actors__actor {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}

.case-actors__icon {
  display: grid;
  place-items: center;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 50%;
  background: var(--color-text);
  color: var(--color-on-inverse);
}

.case-actors__actor--accent .case-actors__icon {
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

.case-actors__icon svg {
  width: 1.75rem;
  height: 1.75rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25;
  stroke-linecap: square;
  stroke-linejoin: miter;
}

.case-actors__name {
  font-size: var(--fs-small);
  font-weight: 600;
}

/* A square box, so the arrow can turn on phones without its layout box
   staying wide. */
.case-actors__arrow {
  display: grid;
  place-items: center;
  flex: none;
  width: 3rem;
  height: 3rem;
  color: var(--color-accent);
  transform: rotate(90deg);
}

.case-actors__arrow svg {
  width: 3rem;
  height: 0.75rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: square;
  stroke-linejoin: miter;
}

.case-actors__app {
  width: 100%;
  aspect-ratio: 16 / 9;
}

/* ---- Carousel (case-carousel.js): the storefront's screens, one at a time,
   on the actors' panel above. ---- */
.case-actors__app.case-carousel {
  display: flex;
  flex-direction: column;
  aspect-ratio: auto;
}

/* …or loose in a case study, in place of a single image. */
.case-figure.case-carousel {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.case-carousel__track {
  position: relative; /* so each slide's offsetLeft is measured from here */
  display: flex;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
}

.case-carousel__track::-webkit-scrollbar {
  display: none;
}

.case-carousel__track:focus-visible {
  outline-offset: 2px;
}

.case-carousel__slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
}

.case-carousel__slide img {
  display: block;
  width: 100%;
  height: auto;
}

/* Same light-raster softening as the hero image, and cutouts likewise left alone. */
:root[data-theme="dark"] .case-carousel__slide img:not(.case-figure__cutout) {
  filter: brightness(0.86) saturate(0.95);
}

/* The pager hangs from this line once the script has shown it. */
.js .case-carousel:has(> [data-carousel-controls]:not([hidden])) .case-carousel__track {
  border-bottom: 1px solid var(--color-border);
}

.case-carousel__dots span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-border);
  transition: background-color 0.2s ease;
}

.case-carousel__dots .is-active {
  background: var(--color-accent);
}

@media (prefers-reduced-motion: reduce) {
  .case-carousel__track {
    scroll-behavior: auto;
  }
}

/* ---- Brand picker: a white-label screen in each partner's colour, the
   swatches beside it swapping one for the other. Radios and :has, no script;
   without :has the images simply stay stacked. ---- */
.case-brand {
  display: grid;
  gap: var(--space-4);
  align-items: center;
}

/* Both images share one cell, so switching never shifts the layout. */
.case-brand__stage {
  display: grid;
}

.case-brand__stage > .case-brand__img {
  grid-area: 1 / 1;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

.case-brand:has(.case-brand__input[value="red"]:checked) .case-brand__img:not(.case-brand__img--red),
.case-brand:has(.case-brand__input[value="green"]:checked) .case-brand__img:not(.case-brand__img--green) {
  opacity: 0;
  visibility: hidden;
}

/* A row under the image on a phone, a column beside it from 768px, in the
   theme palette's compartments: one hairline box, a cell per colour. */
.case-brand__picker {
  display: flex;
  justify-self: center;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 1px solid var(--color-border);
}

.case-brand__option {
  position: relative;
  display: block;
}

.case-brand__option + .case-brand__option {
  border-left: 1px solid var(--color-border);
}

/* Hidden like the theme palette's inputs: still in the tab order. */
.case-brand__input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.case-brand__swatch {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  cursor: pointer;
}

.case-brand__swatch::before {
  content: "";
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--swatch);
  opacity: 0.55;
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.case-brand__swatch--red {
  --swatch: #dd482e;
}

.case-brand__swatch--green {
  --swatch: #5a9f0c;
}

/* No ring, as in the theme palette: the picked colour is the one at full strength. */
.case-brand__swatch:hover::before,
.case-brand__input:checked + .case-brand__swatch::before {
  opacity: 1;
}

.case-brand__input:checked + .case-brand__swatch::before {
  transform: scale(1.2);
}

.case-brand__input:focus-visible + .case-brand__swatch {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

@media (min-width: 768px) {
  .case-brand {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .case-brand__picker {
    flex-direction: column;
  }

  .case-brand__option + .case-brand__option {
    border-left: none;
    border-top: 1px solid var(--color-border);
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-brand__stage > .case-brand__img,
  .case-brand__swatch::before {
    transition: none;
  }
}

@media (min-width: 768px) {
  .case-actors {
    flex-direction: row;
    padding: var(--space-5);
  }

  .case-actors__arrow {
    transform: none;
  }

  .case-actors__app {
    flex: 1;
    min-width: 0;
  }
}

.case-decisions__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.case-origin {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--color-text-secondary);
}

.case-origin::before {
  content: "";
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--color-accent-fill);
}

.case-tradeoff {
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

/* ---- 06: staged launch ---- */
/* Stages on a line, each with its numbered circle. */
.case-timeline {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  padding-left: var(--space-7);
  counter-reset: case-stage;
}

.case-timeline::before {
  content: "";
  position: absolute;
  top: 0.875rem;
  bottom: 0.875rem;
  left: calc(0.875rem - 1px);
  width: 2px;
  background: var(--color-border);
}

/* Beside a taller column, the timeline keeps its own height so the line
   ends at the last step. */
.case-cols > .case-timeline {
  align-self: start;
}

.case-timeline li {
  counter-increment: case-stage;
  position: relative;
  min-height: 1.75rem;
}

.case-timeline li::before {
  content: counter(case-stage);
  position: absolute;
  top: 0;
  left: calc(var(--space-7) * -1);
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border: 1px solid var(--color-accent);
  border-radius: 50%;
  background: var(--color-bg);
  font-size: var(--fs-small);
  font-weight: 600;
}

.case-timeline li:last-child::before {
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
}

.case-timeline__meta {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--color-text-secondary);
}

/* ---- 06: agencies and providers, one tab each ----
   The case's closed box, with a row of tabs as its header: the open tab
   shares the box's surface and an accent rule on top; the others sit on the
   header tone, closed off by the box's border. The row of tabs pins under
   the top bar while its box scrolls by, so the other tabs stay in sight. */
.case-tabs {
  border: 1px solid var(--color-border);
  background: var(--color-bg-card-media);
  scroll-margin-top: var(--sticky-top); /* case-tabs.js scrolls back here */
}

.case-tabs__list {
  position: sticky;
  top: var(--sticky-top);
  z-index: 2;
  display: grid;
  grid-auto-columns: minmax(0, 1fr);
  grid-auto-flow: column;
  background: var(--color-bg-card-header);
}

.case-tabs__list[hidden] {
  display: none;
}

.case-tabs__tab {
  padding: var(--space-4) var(--space-5);
  border-top: 2px solid transparent;
  border-bottom: 1px solid var(--color-border);
  font-weight: 600;
  text-align: left;
  color: var(--color-text-secondary);
  cursor: pointer;
}

.case-tabs__tab + .case-tabs__tab {
  border-left: 1px solid var(--color-border);
}

.case-tabs__tab:hover {
  color: var(--color-text);
}

.case-tabs__tab[aria-selected="true"] {
  border-top-color: var(--color-accent);
  border-bottom-color: transparent;
  background: var(--color-bg-card-media);
  color: var(--color-text);
}

.case-tabs__tab:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

.case-tabs__panel {
  padding: var(--space-6) var(--space-5);
}

.case-tabs__panel:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* Without the script both panels show, one after the other. */
.case-tabs__panel + .case-tabs__panel {
  border-top: 1px solid var(--color-border);
}

.case-tabs__panel > * + * {
  margin-top: var(--space-6);
}

.case-tabs__panel > .case-prose > * + * {
  margin-top: var(--space-3);
}

/* Accordions usually hang off a box above; inside a panel they close on top. */
.case-tabs__panel > .case-accordions {
  border-top: 1px solid var(--color-border);
}

/* ---- 08: the cones ----
   Each stage a cone opening from where the last one ends, inside a widening
   envelope, along a time axis. Dubai is solid, lit from above; the stages
   still to come are dashed outlines. On phones the drawing shows whole and
   opens full screen to read (image-zoom.js). */
.case-cones {
  margin-inline: 0;
  margin-bottom: 0;
}

.case-cones__map {
  overflow-x: auto;
}

.case-cones__svg {
  display: block;
  width: 100%;
  height: auto;
  font-family: inherit;
}

@media (max-width: 767px) {
  .case-cones__svg {
    min-width: 34rem;
  }
}

.cone__envelope,
.cone__ring {
  fill: none;
  stroke: var(--color-text-secondary);
  stroke-width: 1.25;
  opacity: 0.55;
}

.cone__marker {
  stroke: var(--color-text-secondary);
  stroke-width: 1.5;
  stroke-dasharray: 4 5;
}

.cone__body {
  fill: var(--color-bg);
}

/* The design system's diagonal hatching, as SVG patterns, over every cone:
   hairlines in the line colour for the stages still to come (the empty
   funnel bar), accent for the ones done, and the hypothesis band's faint
   stripes over the accent for the stage in focus. */
.cone__hatch {
  fill: url(#cone-hatch-future);
}

.cone__hatch--future rect {
  fill: var(--color-border);
}

.cone__hatch--done rect {
  fill: color-mix(in oklab, var(--color-accent-fill) 55%, transparent);
}

.cone__hatch--active rect {
  fill: color-mix(in srgb, var(--color-on-accent-fill) 14%, transparent);
}

.cone__edge {
  fill: none;
}

.cone__edge,
.cone__base {
  stroke: var(--color-text-secondary);
  stroke-width: 1.5;
  stroke-dasharray: 6 5;
  stroke-linejoin: round;
}

.cone__base {
  fill: var(--color-bg);
}

/* The stage in focus follows the open tab: Dubai, the sales team, or the
   network (agencies and providers). Stages before it are done and fill in a
   muted tone; the ones after it stay dashed, still to come. */
.cone__label {
  transition: fill 0.2s ease;
}

#gtm:has(#gtm-tab-sales[aria-selected="true"]) .cone[data-stage="1"] .cone__hatch,
#gtm:has(#gtm-tab-network[aria-selected="true"]) .cone:is([data-stage="1"], [data-stage="2"]) .cone__hatch {
  fill: url(#cone-hatch-done);
}

#gtm:has(#gtm-tab-sales[aria-selected="true"]) .cone[data-stage="1"] .cone__base,
#gtm:has(#gtm-tab-network[aria-selected="true"]) .cone:is([data-stage="1"], [data-stage="2"]) .cone__base {
  fill: color-mix(in oklab, var(--color-accent-fill) 45%, var(--color-bg));
}

#gtm:has(#gtm-tab-dubai[aria-selected="true"]) .cone[data-stage="1"] .cone__body,
#gtm:has(#gtm-tab-sales[aria-selected="true"]) .cone[data-stage="2"] .cone__body,
#gtm:has(#gtm-tab-network[aria-selected="true"]) .cone:is([data-stage="3"], [data-stage="4"]) .cone__body {
  fill: url(#cone-solid);
}

#gtm:has(#gtm-tab-dubai[aria-selected="true"]) .cone[data-stage="1"] .cone__hatch,
#gtm:has(#gtm-tab-sales[aria-selected="true"]) .cone[data-stage="2"] .cone__hatch,
#gtm:has(#gtm-tab-network[aria-selected="true"]) .cone:is([data-stage="3"], [data-stage="4"]) .cone__hatch {
  fill: url(#cone-hatch-active);
}

#gtm:has(#gtm-tab-dubai[aria-selected="true"]) .cone[data-stage="1"] .cone__base,
#gtm:has(#gtm-tab-sales[aria-selected="true"]) .cone[data-stage="2"] .cone__base,
#gtm:has(#gtm-tab-network[aria-selected="true"]) .cone:is([data-stage="3"], [data-stage="4"]) .cone__base {
  fill: color-mix(in oklab, var(--color-accent-fill), #000 30%);
}

/* Filled stages, done or in focus, drop their dashed outline. */
#gtm:has(#gtm-tab-dubai[aria-selected="true"]) .cone[data-stage="1"] :is(.cone__edge, .cone__base),
#gtm:has(#gtm-tab-sales[aria-selected="true"]) .cone:is([data-stage="1"], [data-stage="2"]) :is(.cone__edge, .cone__base),
#gtm:has(#gtm-tab-network[aria-selected="true"]) .cone :is(.cone__edge, .cone__base) {
  stroke: none;
}

#gtm:has(#gtm-tab-dubai[aria-selected="true"]) .cone__label[data-stage="1"],
#gtm:has(#gtm-tab-sales[aria-selected="true"]) .cone__label[data-stage="2"],
#gtm:has(#gtm-tab-network[aria-selected="true"]) .cone__label:is([data-stage="3"], [data-stage="4"]) {
  fill: var(--color-accent);
}

.cone__lit {
  stop-color: color-mix(in oklab, var(--color-accent-fill), #fff 28%);
}

.cone__mid {
  stop-color: var(--color-accent-fill);
}

.cone__dim {
  stop-color: color-mix(in oklab, var(--color-accent-fill), #000 22%);
}

.cone__axis {
  fill: none;
  stroke: var(--color-text);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.cone__label {
  font-size: 16px;
  font-weight: 600;
  text-anchor: middle;
  fill: var(--color-text);
}

.cone__bracket {
  fill: none;
  stroke: var(--color-text-secondary);
  stroke-width: 1.5;
}

.cone__phase {
  font-size: 13px;
  font-weight: 600;
  text-anchor: middle;
  fill: var(--color-text-secondary);
}

/* ---- 06: a partner's journey ----
   Numbered steps along the top, the other actor's steps in a lane below,
   joined by dashed detours. Drawn as inline SVG; the labels sit in
   foreignObject so they wrap and translate like the rest of the page. On
   phones the map keeps a legible size and scrolls sideways. */
.case-journey {
  margin-inline: 0;
  margin-bottom: 0;
}

.case-journey__map {
  overflow-x: auto;
}

.case-journey__svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  font-family: inherit;
}

@media (max-width: 767px) {
  .case-journey__svg {
    min-width: 50rem;
  }
}

.case-journey__svg text {
  dominant-baseline: central;
  text-anchor: middle;
  font-size: 13px;
  font-weight: 600;
}

.case-journey__svg p {
  margin: 0;
  line-height: 1.25;
}

.jr-label {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  height: 100%;
  padding-bottom: 6px;
  font-size: 14px;
  font-weight: 600;
  text-align: center;
}

.jr-step circle {
  fill: var(--color-bg-card-media);
  stroke: var(--color-accent);
  stroke-width: 1.5;
}

.jr-step text {
  fill: var(--color-text);
}

.jr-step.is-last circle {
  fill: var(--color-accent-fill);
  stroke: none;
}

.jr-step.is-last text {
  fill: var(--color-on-accent-fill);
}

.jr-line {
  fill: none;
  stroke: var(--color-accent);
  stroke-width: 1.5;
}

.jr-arrow path {
  fill: none;
  stroke: var(--color-text-secondary);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.jr-arrow--main path {
  stroke: var(--color-accent);
}

.jr-lane {
  fill: var(--color-bg);
  stroke: var(--color-border);
}

.jr-lane__title {
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
}

.jr-edge {
  fill: none;
  stroke: var(--color-text-secondary);
  stroke-width: 1.5;
  stroke-dasharray: 3 4;
}

.jr-edge.is-solid {
  stroke-dasharray: none;
}

.jr-node circle {
  fill: var(--color-text);
}

.jr-node text {
  fill: var(--color-on-inverse);
}

.jr-node__label {
  font-size: 13px;
  text-align: center;
}

.jr-note {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  height: 100%;
  font-size: 12px;
  font-style: italic;
  text-align: right;
  color: var(--color-text-secondary);
}

/* ---- Problem space / Solution space (GrowPro, Nomo) ----
   A band across the content that groups the sections after it: the
   section hatching, and the label boxed in its middle. In the nav, the same
   two labels head their groups of links. */
.case-space {
  display: flex;
  justify-content: center;
  padding: var(--space-6) var(--page-padding);
  border-top: 1px solid var(--color-border);
  background:
    repeating-linear-gradient(-45deg, var(--color-border) 0 1px, transparent 1px 10px),
    var(--color-bg-card-header-glass);
}

.case-space__label {
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  font-size: var(--fs-small);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* In the nav, the two spaces read as a vertical timeline: one rule runs down
   both blocks, each label sits on a node of it, and the block holding the
   current section draws its stretch of rule and fills its node. */
.nav-space {
  --nav-space-indent: var(--space-4);
  padding-left: var(--nav-space-indent);
  border-left: 1px solid var(--color-border);
  transition: border-color 0.25s ease;
}

.nav-space:has(.nav-link.is-active) {
  border-left-color: var(--color-text);
}

.nav-space__label {
  position: relative;
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--color-text);
}

/* 7px node centred on the 1px rule, at the label's first line */
.nav-space__label::before {
  content: '';
  position: absolute;
  top: 0.35em;
  top: calc(0.5lh - 3.5px);
  left: calc(-1 * var(--nav-space-indent) - 4px);
  width: 7px;
  height: 7px;
  border: 1px solid var(--color-text);
  border-radius: 50%;
  background: var(--color-bg);
  transition: background-color 0.25s ease;
}

.nav-space:has(.nav-link.is-active) .nav-space__label::before {
  background: var(--color-text);
}

/* Sidebar: the list's gap would cut the rule between the two blocks, so
   adjacent blocks touch and pad themselves instead. */
.nav-sidebar__nav .nav-space {
  padding-block: var(--space-2);
}

.nav-sidebar__nav .nav-space + .nav-space {
  margin-top: calc(-1 * var(--space-2));
}

/* Once scrollspy.js has put the ball in, the sidebar's whole index is one
   rail instead: a hairline from the first link to the last, every link to
   its right, the space labels on hollow nodes of it and their links a step
   further in. The ball (.nav-ball) rides the rail with the scroll and the
   stretch above it is drawn in ink (.nav-rail__done); scrollspy.js sets
   --rail-top / --rail-bottom to the first and last link's middles. */
.nav-sidebar__nav .nav-rail {
  --nav-space-indent: var(--space-4);
  position: relative;
  padding-left: var(--nav-space-indent);
}

.nav-rail::before,
.nav-rail__done {
  content: '';
  position: absolute;
  top: var(--rail-top, 0);
  left: 0;
  width: 1px;
}

.nav-rail::before {
  height: calc(var(--rail-bottom, 0px) - var(--rail-top, 0px));
  background: var(--color-border);
}

.nav-rail__done {
  height: 0;
  background: var(--color-text);
  transition: height 0.15s ease-out;
}

/* 9px, centred on the 1px rule at the y scrollspy.js gives it; over the
   nodes, which sit over the inked rule. */
.nav-ball {
  position: absolute;
  z-index: 2;
  top: -4.5px;
  left: -4px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--color-text);
  transition: transform 0.15s ease-out;
}

.nav-sidebar__nav .nav-rail .nav-space {
  padding-left: 0;
  border-left: none;
}

.nav-rail .nav-space > ul {
  padding-left: var(--nav-space-indent);
}

/* The label's node sits on the rail, hollow on the menu's surface so the
   rule stops at it, until the ball has gone past it: then it fills in
   (scrollspy.js sets .is-passed), and the inked rule runs into it. */
.nav-rail .nav-space__label::before,
.nav-rail .nav-space:has(.nav-link.is-active) .nav-space__label::before {
  z-index: 1;
  left: calc(-1 * var(--nav-space-indent) - 3px);
  background: var(--color-panel);
}

.nav-rail .nav-space .nav-space__label.is-passed::before {
  background: var(--color-text);
}

@media (prefers-reduced-motion: reduce) {
  .nav-ball,
  .nav-rail__done {
    transition: none;
  }
}

/* Menu: the rule starts at the page gutter and the rows inside it start at
   the rule, so the blocks still read as a branch of the list. */
.nav-topbar__mobile-nav .nav-space {
  margin-left: var(--page-padding);
}

.nav-topbar__mobile-nav .nav-space__label {
  margin: 0;
  padding: var(--space-4) 0 var(--space-2);
}

.nav-topbar__mobile-nav .nav-space__label::before {
  top: calc(var(--space-4) + 0.35em);
  top: calc(var(--space-4) + 0.5lh - 3.5px);
}

.nav-topbar__mobile-nav .nav-space .nav-link {
  padding-left: 0;
}

/* ---- 07: the funnel and the studies behind it, as one sheet ---- */
/* The funnel opens the sheet; a strip introduces the studies; the three
   study cards close it, joined by shared rules. */
.case-funnel {
  margin: 0;
  padding: var(--space-5);
  border: 1px solid var(--color-border);
  border-bottom: none;
  background: var(--color-bg-card-media);
}

.case-strip {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--color-border);
  border-bottom: none;
  background: var(--color-bg-card-header);
}

.case-strip .case-subhead {
  font-size: 1.125rem;
}

.case-strip:last-child,
.case-funnel:last-child,
.case-csat:last-child {
  border-bottom: 1px solid var(--color-border);
}

/* Doubled class: .case-cards (later in the file) sets a wider gap. */
.case-cards.case-cards--joined {
  gap: 1px;
  border: 1px solid var(--color-border);
  background: var(--color-border);
}

.case-cards--joined .case-card {
  border: none;
  background: var(--color-bg-card-media);
}

@media (min-width: 768px) {
  .case-funnel {
    padding: var(--space-6);
  }

  .case-strip {
    padding: var(--space-4) var(--space-6);
  }
}

/* A Mixpanel-style funnel: one column per step on a 0–100% scale, where
   100% is the first step. Each fill is the share that reached that step;
   the pale column behind it is the previous step, so the gap between them
   is the drop-off. The pill sits on top of the fill; a tooltip on hover or
   focus gives the drop-off. Heights come from data-value / data-prev,
   because the CSP blocks inline styles. */
.case-funnel {
  --funnel-h: 13rem;
  --funnel-axis: 2rem;
  --funnel-gap: var(--space-1);
}

.case-funnel__legend {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-secondary);
}

.case-funnel__swatch {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: var(--color-accent-fill);
}

/* Nomo's awareness: a rollout with no figure, so it sits above the chart. */
.case-funnel__note {
  margin: 0 0 var(--space-5);
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

.case-funnel__note strong {
  color: var(--color-text);
}

.case-funnel__plot {
  position: relative;
  padding-left: var(--funnel-axis);
  /* Room for the 100% pill above the top line. */
  padding-top: var(--space-4);
}

/* The y axis: five labels, each centred on its grid line. */
.case-funnel__axis {
  position: absolute;
  top: var(--space-4);
  left: 0;
  width: var(--funnel-axis);
  height: var(--funnel-h);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-secondary);
}

.case-funnel__axis span {
  display: flex;
  align-items: center;
  height: 0;
}

.case-funnel__steps {
  position: relative;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--funnel-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Grid lines at 0, 25, 50, 75 and 100%, behind the columns. */
.case-funnel__steps::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--funnel-h);
  border-bottom: 1px solid var(--color-border);
  background: linear-gradient(var(--color-border) 1px, transparent 1px) 0 0 / 100% 25%;
  opacity: 0.7;
  pointer-events: none;
}

.case-funnel__step {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.case-funnel__step[tabindex]:focus-visible {
  outline-offset: 4px;
}

.case-funnel__col {
  position: relative;
  display: block;
  height: var(--funnel-h);
}

.case-funnel__ghost,
.case-funnel__fill {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: block;
}

/* The previous step: the pale column the drop-off is read against. */
.case-funnel__ghost {
  background: linear-gradient(
    color-mix(in oklab, var(--color-accent-fill) 18%, transparent),
    color-mix(in oklab, var(--color-accent-fill) 6%, transparent)
  );
}

.case-funnel__fill {
  min-height: 3px;
  background: var(--color-accent-fill);
  transform-origin: bottom;
  transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1) 0.2s;
}

.case-funnel__step:hover .case-funnel__fill,
.case-funnel__step:focus-visible .case-funnel__fill {
  background: color-mix(in oklab, var(--color-accent-fill) 85%, var(--color-text));
}

.case-funnel__pill {
  position: absolute;
  left: 50%;
  z-index: 1;
  padding: 2px var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: 3px;
  background: var(--color-bg-card-media);
  box-shadow: 0 1px 3px rgb(var(--shadow-rgb) / 0.12);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  transform: translate(-50%, 50%);
  transition: opacity 0.4s ease 0.8s;
}

.case-funnel__step[data-prev="100"] .case-funnel__ghost { height: 100%; }
.case-funnel__step[data-prev="70"] .case-funnel__ghost { height: 70%; }
.case-funnel__step[data-prev="32"] .case-funnel__ghost { height: 32%; }
.case-funnel__step[data-prev="18"] .case-funnel__ghost { height: 18%; }

.case-funnel__step[data-value="100"] .case-funnel__fill { height: 100%; }
.case-funnel__step[data-value="70"] .case-funnel__fill { height: 70%; }
.case-funnel__step[data-value="32"] .case-funnel__fill { height: 32%; }
.case-funnel__step[data-value="18"] .case-funnel__fill { height: 18%; }
.case-funnel__step[data-value="7"] .case-funnel__fill { height: 7%; }
.case-funnel__step[data-value="5"] .case-funnel__fill { height: 5%; }

.case-funnel__step[data-value="100"] .case-funnel__pill { bottom: 100%; }
.case-funnel__step[data-value="70"] .case-funnel__pill { bottom: 70%; }
.case-funnel__step[data-value="32"] .case-funnel__pill { bottom: 32%; }
.case-funnel__step[data-value="18"] .case-funnel__pill { bottom: 18%; }
.case-funnel__step[data-value="7"] .case-funnel__pill { bottom: 7%; }
.case-funnel__step[data-value="5"] .case-funnel__pill { bottom: 5%; }

/* Drop-off from the previous step, at the top right of the column. */
.case-funnel__tip {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: max-content;
  max-width: 12rem;
  padding: var(--space-2) var(--space-3) var(--space-2) calc(var(--space-3) + 3px);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-bg-card-media);
  box-shadow: 0 6px 20px rgb(var(--shadow-rgb) / 0.18);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  /* Faded, not hidden, so screen readers still read the drop-off. */
  opacity: 0;
  pointer-events: none;
  transform: translateY(4px);
  transition: opacity 0.15s ease, transform 0.15s ease;
}

.case-funnel__tip::before {
  content: "";
  position: absolute;
  top: var(--space-2);
  bottom: var(--space-2);
  left: var(--space-2);
  width: 3px;
  border-radius: 2px;
  background: var(--color-accent-fill);
}

.case-funnel__step:hover .case-funnel__tip,
.case-funnel__step:focus-visible .case-funnel__tip {
  opacity: 1;
  transform: none;
}

/* Narrow columns on phones: the number sits above a smaller name that
   may hyphenate, so neighbours never run into each other. */
.case-funnel__stage {
  font-size: 11px;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
  hyphens: auto;
}

.case-funnel__n {
  display: block;
  margin-right: var(--space-1);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-secondary);
}

/* What each figure means: under its column on wide screens, a numbered
   list below the chart on narrow ones. */
.case-funnel__notes {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: var(--space-5) 0 0;
  padding: var(--space-4) 0 0;
  border-top: 1px solid var(--color-border);
  list-style: none;
  font-size: var(--fs-small);
}

.case-funnel__notes li {
  display: flex;
  gap: var(--space-2);
}

.case-funnel__notes p {
  margin: 0;
}

/* The bars grow from the baseline as the figure is revealed. */
.js .case-funnel[data-reveal]:not(.is-visible) .case-funnel__fill {
  transform: scaleY(0);
}

.js .case-funnel[data-reveal]:not(.is-visible) .case-funnel__pill {
  opacity: 0;
}

@media (min-width: 768px) {
  .case-funnel {
    --funnel-h: 18rem;
    --funnel-axis: 2.25rem;
    --funnel-gap: var(--space-2);
  }

  .case-funnel__stage {
    font-size: var(--fs-small);
  }

  .case-funnel__stage .case-funnel__n {
    display: inline;
  }

  .case-funnel__notes {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: var(--funnel-gap);
    margin-top: var(--space-2);
    margin-left: var(--funnel-axis);
    padding-top: 0;
    border-top: none;
    color: var(--color-text-secondary);
  }

  /* The number is already on the stage label right above. */
  .case-funnel__notes .case-funnel__n {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-funnel__fill,
  .case-funnel__pill,
  .case-funnel__tip {
    transition: none;
  }
}

/* A Mixpanel-style trend line (case-trend.js), on the same sheet as the
   funnel: the query bar (the funnel's events, the metric, the interval and
   Line | Table), the headline figure, then the plot the script draws from
   the table. Without the script the table shows on its own. */
.case-trend {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0;
  padding: var(--space-5);
  border: 1px solid var(--color-border);
  border-bottom: none;
  background: var(--color-bg-card-media);
}

.case-trend:last-child {
  border-bottom: 1px solid var(--color-border);
}

.case-trend__query {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  font-size: 12px;
}

.case-trend__events {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  margin: 0 var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}

.case-trend__events li {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-weight: 600;
}

.case-trend__events li + li::before {
  content: "→";
  margin-right: var(--space-1);
  font-weight: 400;
  color: var(--color-text-secondary);
}

/* The step's number in its little box, as Mixpanel tags each event. */
.case-trend__step {
  display: inline-grid;
  place-items: center;
  min-width: 1.25rem;
  height: 1.25rem;
  border-radius: 3px;
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.case-trend__chip {
  padding: 1px var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: 4px;
  color: var(--color-text-secondary);
  white-space: nowrap;
}

/* Line | Table, pushed to the end of the bar. Hidden until the script runs. */
.case-trend__views {
  display: inline-flex;
  margin-left: auto;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  overflow: hidden;
}

.case-trend__views[hidden] {
  display: none;
}

.case-trend__views button {
  padding: 1px var(--space-2);
  border: none;
  background: none;
  color: var(--color-text-secondary);
  font: inherit;
  cursor: pointer;
}

.case-trend__views button + button {
  border-left: 1px solid var(--color-border);
}

.case-trend__views button[aria-pressed="true"] {
  background: var(--color-accent-tint);
  color: var(--color-text);
  font-weight: 600;
}

/* The headline figure, and beside it (wide screens) a sticky note. */
.case-trend__top {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.case-trend__headline {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

.case-trend__value {
  color: var(--color-text);
  font-size: 2.5rem;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.case-trend__delta {
  padding: 1px var(--space-2);
  border-radius: 3px;
  background: var(--color-accent-fill);
  color: var(--color-on-accent-fill);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.case-trend .case-trend__postit {
  --tilt: 1.5deg;
  justify-self: auto;
  align-self: flex-end;
  width: min(100%, 16rem);
  margin: 0 calc(var(--space-3) * -1) 0 0;
  padding: var(--space-5);
  font-size: 1rem;
}

.case-trend__plot {
  position: relative;
  outline-offset: 4px;
  touch-action: pan-y;
}

.case-trend__svg {
  display: block;
  overflow: visible;
}

.case-trend__grid {
  stroke: var(--color-border);
  stroke-width: 1;
}

.case-trend__tick {
  fill: var(--color-text-secondary);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.case-trend__area {
  fill: var(--color-accent-fill);
  opacity: 0.1;
}

.case-trend__line {
  fill: none;
  stroke: var(--color-accent-fill);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 1.4s cubic-bezier(0.16, 1, 0.3, 1) 0.2s;
}

.case-trend__dot {
  fill: var(--color-accent-fill);
  stroke: var(--color-bg-card-media);
  stroke-width: 2;
}

/* The comparison (Mixpanel's "previous period"): a dashed grey line under
   the current one, with no area and no draw-in. */
.case-trend__line--prev {
  stroke: var(--color-text-secondary);
  stroke-width: 1.5;
  stroke-dasharray: 5 4;
  opacity: 0.8;
  transition: none;
}

.case-trend__dot--prev {
  fill: var(--color-text-secondary);
}

/* The series, named above the plot: a short line key for each. */
.case-trend__legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-4);
  margin: 0;
  font-size: 12px;
  color: var(--color-text-secondary);
}

.case-trend__legend > span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* What the headline adds up to, under the plot. */
.case-trend__foot {
  margin: 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

.case-trend__foot strong {
  color: var(--color-text);
}

/* The release, as a Mixpanel annotation: a hairline and its flag on top. */
.case-trend__annot {
  stroke: var(--color-text);
  stroke-width: 1;
  opacity: 0.55;
}

.case-trend__flag {
  position: absolute;
  top: 0;
  padding: 1px var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: 3px;
  background: var(--color-bg-card-media);
  font-size: 11px;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
}

.case-trend__flag[hidden] {
  display: none;
}

/* Month ends: the same mark, fainter, so the release and the quarter close
   stand out. */
.case-trend__annot--minor {
  opacity: 0.2;
}

.case-trend__flag--minor {
  border-color: transparent;
  background: none;
  font-weight: 500;
  color: var(--color-text-secondary);
}

/* The volume: pale bars behind the times, in its own colour (validated
   against the accent for colour blindness, light and dark), on the right
   axis. The current period filled, the comparison only outlined. */
.case-trend {
  --trend-volume: #0e8f84;
}

:root[data-theme="dark"] .case-trend {
  --trend-volume: #25a093;
}

.case-trend__bar {
  fill: var(--trend-volume);
  fill-opacity: 0.28;
}

.case-trend__bar--prev {
  fill-opacity: 0.06;
  stroke: var(--trend-volume);
  stroke-opacity: 0.55;
  stroke-width: 1;
}

/* Its key in the legend and the tooltip: a small bar, filled or outlined. */
.case-trend__key.case-trend__key--bar {
  width: 8px;
  height: 10px;
  border-radius: 1px;
  background: color-mix(in oklab, var(--trend-volume) 45%, transparent);
}

.case-trend__key.case-trend__key--bar.case-trend__key--prev {
  background: none;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--trend-volume) 70%, transparent);
}

/* The crosshair, the hovered week's dot and its tooltip. */
.case-trend__cross,
.case-trend__dot--active,
.case-trend__tip {
  opacity: 0;
  transition: opacity 0.15s ease;
}

.case-trend__cross {
  stroke: var(--color-text-secondary);
  stroke-width: 1;
}

.case-trend__dot--active {
  stroke-width: 3;
}

.case-trend__plot.is-active .case-trend__cross,
.case-trend__plot.is-active .case-trend__dot--active,
.case-trend__plot.is-active .case-trend__tip {
  opacity: 1;
}

.case-trend__tip {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: max-content;
  max-width: 15rem;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-bg-card-media);
  box-shadow: 0 6px 20px rgb(var(--shadow-rgb) / 0.18);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* In a tooltip with both panels, each starts with its name. */
.case-trend__tip-group {
  margin-top: var(--space-1);
  font-size: 11px;
  font-weight: 600;
  color: var(--color-text-secondary);
}

.case-trend__tip-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--color-text-secondary);
}

.case-trend__tip-row b {
  color: var(--color-text);
}

/* The series key: a short stroke of the line's color; dashed and grey
   for the comparison. */
.case-trend__key {
  flex: none;
  width: 12px;
  height: 2px;
  border-radius: 1px;
  background: var(--color-accent-fill);
}

.case-trend__key--prev {
  background: repeating-linear-gradient(90deg, var(--color-text-secondary) 0 4px, transparent 4px 6px);
}

/* The table: the data, and the view behind the Table button. */
.case-trend__data {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
}

.case-trend__data th,
.case-trend__data td {
  padding: var(--space-1) var(--space-2);
  border-bottom: 1px solid var(--color-border);
  text-align: left;
}

.case-trend__data thead th {
  font-weight: 600;
  color: var(--color-text-secondary);
}

.case-trend__data tbody th {
  font-weight: 500;
}

.case-trend__data td {
  text-align: right;
}

.case-trend__unit {
  font-weight: 400;
}

.case-trend__data [data-trend-note] {
  margin-left: var(--space-2);
}

.case-trend.is-ready:not(.is-table) .case-trend__data,
.case-trend.is-table .case-trend__plot {
  display: none;
}

/* Hanging off a fact sheet (Nomo's result), the sheet's border is its top. */
.case-sheet + .case-trend {
  border-top: none;
}

/* The plan mix under the plot, as a second Metabase panel: one 100%
   stacked bar per period, the before in greys and the after in shades of
   the accent, with the shares in keys underneath (a 5% slice has no room
   for its label). The widths are fixed per bar in the grid, since the CSP
   blocks inline styles. */
.case-trend__mix {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  font-size: 12px;
}

.case-trend__mix-title {
  margin: 0;
  font-weight: 600;
}

/* The Sankey: the old plans on the left, the new ones and those who left on
   the right, each link as wide as its share. The SVG stretches to its box
   (preserveAspectRatio="none"), so its units are percent of it; the labels
   are HTML beside the bars, placed at each bar's middle in the same
   percent, so they keep their size at any width. */
.case-trend__flow {
  display: grid;
  grid-template-columns: minmax(0, 5.5rem) minmax(0, 1fr) minmax(0, 5.5rem);
  grid-template-rows: auto 15rem;
  column-gap: var(--space-2);
  row-gap: var(--space-2);
}

.case-trend__flow-head {
  margin: 0;
  color: var(--color-text-secondary);
  text-align: right;
}

.case-trend__flow-head--after {
  grid-column: 3;
  text-align: left;
}

.case-trend__flow-svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.case-trend__flow-nodes {
  position: relative;
  margin: 0;
  padding: 0;
  list-style: none;
}

.case-trend__flow-node {
  position: absolute;
  right: 0;
  left: 0;
  line-height: 1.25;
  text-align: right;
  color: var(--color-text-secondary);
  transform: translateY(-50%);
}

.case-trend__flow-nodes--after .case-trend__flow-node {
  text-align: left;
}

.case-trend__flow-node b {
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}

.case-trend__flow-node--standard { top: 30.8%; }
.case-trend__flow-node--premium { top: 86.8%; }
.case-trend__flow-node--basic { top: 34.03%; }
.case-trend__flow-node--essential { top: 76.07%; }
.case-trend__flow-node--advanced { top: 86.08%; }
.case-trend__flow-node--churn { top: 96.04%; }

/* The old plans in greys, the new tiers in shades of the accent, those
   who left in the grey of what's gone. */
.case-trend__flow-bar--standard,
.case-trend__flow-bar--premium {
  fill: color-mix(in oklab, var(--color-text-secondary) 80%, var(--color-bg-card-media));
}

.case-trend__flow-bar--basic,
.case-trend__flow-link--basic {
  fill: color-mix(in oklab, var(--color-accent-fill) 55%, var(--color-bg-card-media));
}

.case-trend__flow-bar--essential,
.case-trend__flow-link--essential {
  fill: color-mix(in oklab, var(--color-accent-fill) 80%, var(--color-bg-card-media));
}

.case-trend__flow-bar--advanced,
.case-trend__flow-link--advanced {
  fill: var(--color-accent-fill);
}

.case-trend__flow-bar--churn,
.case-trend__flow-link--churn {
  fill: var(--color-text-secondary);
}

.case-trend__flow-link {
  fill-opacity: 0.45;
  transition: fill-opacity 0.8s ease 0.4s;
}

.case-trend__flow-link--churn {
  fill-opacity: 0.22;
}

.js .case-trend[data-reveal]:not(.is-visible) .case-trend__flow-link {
  fill-opacity: 0;
}

@media (min-width: 768px) {
  .case-trend__flow {
    grid-template-columns: minmax(0, 9rem) minmax(0, 1fr) minmax(0, 9rem);
    column-gap: var(--space-3);
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-trend__flow-link {
    transition: none;
  }
}

/* The line draws itself in as the figure is revealed. */
.js .case-trend[data-reveal]:not(.is-visible) .case-trend__line:not(.case-trend__line--prev) {
  stroke-dashoffset: 1;
}

@media (min-width: 768px) {
  .case-trend {
    padding: var(--space-6);
  }

  .case-trend__top {
    flex-direction: row;
    justify-content: space-between;
    align-items: flex-start;
  }

  .case-trend .case-trend__postit {
    align-self: flex-start;
    margin: calc(var(--space-2) * -1) calc(var(--space-4) * -1) 0 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-trend__line,
  .case-trend__cross,
  .case-trend__dot--active,
  .case-trend__tip {
    transition: none;
  }
}

/* A before/after with a switch over it (Talenom): the switch swaps the
   plain screens for the same ones with the Tab route drawn on. CSS only,
   through :has on the checkbox, so the slider keeps its place. The route,
   in words, goes under it as a caption. */
.case-compare-toggle {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.case-compare .is-route,
.case-compare-toggle:has(.case-switch__input:checked) .case-compare > img:not(.is-route) {
  display: none;
}

.case-compare-toggle:has(.case-switch__input:checked) .case-compare > img.is-route {
  display: block;
}

.case-compare__caption {
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
  text-wrap: pretty;
}

/* A switch: a checkbox (role="switch") laid over the whole label, so a
   click anywhere on it toggles, with the track and its thumb drawn after. */
.case-switch {
  position: relative;
  display: inline-flex;
  align-self: flex-start;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-small);
  cursor: pointer;
  user-select: none;
}

.case-switch__input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.case-switch__track {
  position: relative;
  flex: none;
  width: 2.25rem;
  height: 1.25rem;
  border-radius: 999px;
  background: color-mix(in oklab, var(--color-text-secondary) 45%, var(--color-border));
  transition: background-color 0.2s ease;
}

.case-switch__track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: calc(1.25rem - 4px);
  height: calc(1.25rem - 4px);
  border-radius: 50%;
  background: #ffffff;
  box-shadow: 0 1px 2px rgb(var(--shadow-rgb) / 0.25);
  transition: transform 0.2s ease, background-color 0.2s ease;
}

.case-switch__input:checked + .case-switch__track {
  background: var(--color-accent-fill);
}

.case-switch__input:checked + .case-switch__track::after {
  background: var(--color-on-accent-fill);
  transform: translateX(1rem);
}

.case-switch__input:focus-visible + .case-switch__track {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .case-switch__track,
  .case-switch__track::after {
    transition: none;
  }
}

/* The heatmap inside a finding (Talenom's H1): under the text and the
   note, across the whole accordion. */
.case-finding > .case-heatmap {
  grid-column: 1 / -1;
  margin: 0;
}

/* A click heatmap (Talenom): the screen with the clicks painted on, and
   under it the scale, from fewer to more clicks, before what it shows. */
.case-heatmap__caption {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-3);
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

.case-heatmap__scale {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 12px;
  white-space: nowrap;
}

/* The same ramp the heatmap is painted with. */
.case-heatmap__ramp {
  width: 4.5rem;
  height: 8px;
  border-radius: 4px;
  background: linear-gradient(90deg, #3b82f6, #22c55e 30%, #facc15 55%, #f97316 78%, #dc2626);
}

/* A satisfaction score on the funnel's sheet (Easor): the figure, one
   stacked bar of satisfied and not, its legend, and a sticky note stuck on
   the chart saying what the score does and doesn't tell. The bar's split
   is in the classes, since the CSP blocks inline styles. */
.case-csat {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0;
  padding: var(--space-5);
  border: 1px solid var(--color-border);
  border-bottom: none;
  background: var(--color-bg-card-media);
}

.case-csat .case-subhead {
  font-size: 1.125rem;
}

.case-csat__chart {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.case-csat__score {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
}

.case-csat__value {
  color: var(--color-text);
  font-size: 2.5rem;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* The five faces as the survey shows them, red to green, and under them
   two brackets with the split: 1–3 and 4–5, the satisfied in the accent. */
.case-csat__survey {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 22rem;
}

.case-csat__faces {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.case-csat__faces svg {
  display: block;
  width: 100%;
  max-width: 44px;
  margin-inline: auto;
}

.case-csat__faces circle:first-child {
  stroke: none;
}

.case-csat__faces li:nth-child(1) circle:first-child { fill: #e0524a; }
.case-csat__faces li:nth-child(2) circle:first-child { fill: #f2a144; }
.case-csat__faces li:nth-child(3) circle:first-child { fill: #f2c94c; }
.case-csat__faces li:nth-child(4) circle:first-child { fill: #9ccc5a; }
.case-csat__faces li:nth-child(5) circle:first-child { fill: #45a868; }

.case-csat__eye {
  fill: #1d1b16;
}

.case-csat__faces path {
  fill: none;
  stroke: #1d1b16;
  stroke-width: 1.5;
  stroke-linecap: round;
}

.case-csat__groups {
  display: grid;
  grid-template-columns: 3fr 2fr;
  gap: var(--space-2);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-secondary);
}

.case-csat__group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding-top: var(--space-2);
  border-top: 2px solid color-mix(in oklab, var(--color-text-secondary) 38%, transparent);
  text-align: center;
}

.case-csat__group strong {
  color: var(--color-text);
  font-size: var(--fs-small);
}

.case-csat__group--yes {
  border-top-color: var(--color-accent-fill);
}

/* The faces pop in one after another as the figure is revealed. */
.case-csat__faces li {
  transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease;
}

.case-csat__faces li:nth-child(2) { transition-delay: 0.06s; }
.case-csat__faces li:nth-child(3) { transition-delay: 0.12s; }
.case-csat__faces li:nth-child(4) { transition-delay: 0.18s; }
.case-csat__faces li:nth-child(5) { transition-delay: 0.24s; }

.js .case-csat[data-reveal]:not(.is-visible) .case-csat__faces li {
  opacity: 0;
  transform: scale(0.6);
}

.case-csat__note {
  color: var(--color-text-secondary);
  font-size: var(--fs-small);
}

/* On a phone the note follows the chart; from 768px it is stuck on top of
   the chart's right end, tilted, hanging a little past the sheet. */
.case-csat .case-csat__postit {
  --tilt: 1.5deg;
  justify-self: auto;
  align-self: flex-end;
  width: min(100%, 16rem);
  margin: 0 calc(var(--space-3) * -1) 0 0;
  padding: var(--space-5);
  font-size: 1rem;
}

@media (min-width: 768px) {
  .case-csat {
    padding: var(--space-6);
  }

  /* The title stays clear of the note; the chart runs under its edge. */
  .case-csat > .case-subhead {
    padding-right: 14rem;
  }

  .case-csat__chart {
    padding-right: 9rem;
  }

  .case-csat .case-csat__postit {
    position: absolute;
    z-index: 2;
    top: var(--space-5);
    right: calc(var(--space-4) * -1);
    width: 16rem;
    margin: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-csat__faces li {
    transition: none;
  }
}

/* The Customer Effort Score as it's asked: the question, a 1–7 scale
   from red (very difficult) to green (very easy), and its two ends. */
.case-ces {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
}

.case-card .case-ces__q {
  color: var(--color-text);
  font-weight: 600;
}

.case-ces__scale {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.case-ces__scale li {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 32px;
  color: #1d1b16;
  font-size: var(--fs-small);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.case-ces__scale li:nth-child(1) { background: #e0524a; }
.case-ces__scale li:nth-child(2) { background: #ec7a45; }
.case-ces__scale li:nth-child(3) { background: #f2a144; }
.case-ces__scale li:nth-child(4) { background: #f2c94c; }
.case-ces__scale li:nth-child(5) { background: #b9d257; }
.case-ces__scale li:nth-child(6) { background: #7cc05f; }
.case-ces__scale li:nth-child(7) { background: #45a868; }

.case-card .case-ces__ends {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: 12px;
}

/* A ratio written as a fraction: the part over the whole. */
.case-ratio {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  text-align: center;
}

.case-card .case-ratio {
  color: var(--color-text);
  font-size: var(--fs-small);
  font-weight: 600;
}

.case-ratio__den {
  align-self: stretch;
  padding-top: var(--space-2);
  border-top: 2px solid var(--color-accent-fill);
}

/* Inside a tab (Easor's solutions): the title, the drag compare, the
   comparison table and, for H2, the step added before the form. */
.case-tabs__panel > .case-subhead + * {
  margin-top: var(--space-5);
}

.case-tabs__panel > .case-card__pills + .case-bullets {
  margin-top: var(--space-4);
}

/* Cards and accordions as one block (Easor's context): the accordions
   hang off the cards' bottom line, as they have no top one of their own.
   Not .case-sheet: that is Nomo's results sheet. */
.case-context > .case-accordions {
  margin-top: 0;
}

.case-context .case-accordion__body > .case-card__pills + .case-bullets {
  margin-top: var(--space-3);
}

.case-tabs__panel > * + .case-compare--text,
.case-tabs__panel > .case-sol__aside {
  margin-top: var(--space-6);
}

.case-tabs__panel > .case-prose + .case-cards {
  margin-top: var(--space-5);
}

.case-sol__steps {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Where H3's screen will go, until it exists. */
.case-tabs__panel > .case-sol__placeholder {
  aspect-ratio: 3 / 2;
  margin: var(--space-6) 0 0;
  border-radius: 0;
}

.case-sol__aside {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.case-sol__aside > .case-subhead {
  font-size: 1.125rem;
}

.case-sol__aside > .case-figure {
  margin: var(--space-2) 0 0;
}

/* Two screens of different shapes in one drag compare: a fixed frame, each
   image letterboxed inside it, so the divider splits them level. */
.case-compare--fit[data-compare] {
  aspect-ratio: 3 / 2;
  grid-template-rows: 100%;
  border: 1px solid var(--color-border);
  background: var(--color-bg-card-media);
}

.case-compare--fit > img {
  height: 100%;
  object-fit: contain;
}

/* A screen still to come: the placeholder takes the image's place. */
.case-compare--fit > .placeholder {
  grid-area: 1 / 1;
  height: 100%;
  border: none;
  border-radius: 0;
  transition: clip-path 0.2s ease;
}

.case-compare--fit > .placeholder.case-compare__after {
  justify-content: flex-end;
  background: color-mix(in oklab, var(--color-accent-fill) 10%, var(--color-placeholder-bg));
}

/* Each placeholder's words on its own half, clear of the divider. */
.case-compare--fit > .placeholder.case-compare__before {
  justify-content: flex-start;
}

.case-compare--fit > .placeholder > span {
  width: 50%;
  padding-inline: var(--space-5);
}

.case-tabs__panel > .case-compare + .case-compare {
  margin-top: var(--space-5);
}

/* A journey that plays on its own (Easor's H1): one lane per version,
   each a row of steps joined by a line. The steps light up in turn on one
   6s loop shared by both lanes, so the after lane is done while the
   before one is still on its way; the click count inks in on arrival. CSS
   only: each step position has its own keyframes. */
.case-path {
  /* Not reached yet: the steps and the count wait in a faint ink. */
  --path-idle: color-mix(in oklab, var(--color-text-secondary) 45%, transparent);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin: 0;
  padding: var(--space-5);
  border: 1px solid var(--color-border);
  background: var(--color-bg-card-media);
}

.case-path__lane {
  display: grid;
  gap: var(--space-3);
}

.case-path__lane--after > .case-label {
  color: var(--color-accent);
}

.case-path__steps {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}

.case-path__steps li {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  text-align: center;
}

/* The line from the previous step: a grey track, and the accent fill
   drawn over it when the step is reached. */
.case-path__steps li + li::before,
.case-path__steps li + li::after {
  content: "";
  position: absolute;
  top: 8px;
  left: -50%;
  width: 100%;
  height: 2px;
  margin-top: -1px;
  background: var(--color-border);
}

.case-path__steps li + li::after {
  background: var(--color-accent-fill);
}

.case-path__dot {
  position: relative;
  z-index: 1;
  width: 16px;
  height: 16px;
  border: 2px solid var(--color-border);
  border-radius: 50%;
  background: var(--color-bg);
}

.case-path__step {
  font-size: 12px;
  line-height: 1.3;
  color: var(--color-text);
  overflow-wrap: anywhere;
}

.case-path__count {
  font-size: var(--fs-small);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.case-path__dot,
.case-path__step,
.case-path__steps li + li::after,
.case-path__count {
  animation-duration: 6s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
  animation-fill-mode: both;
}

.case-path__steps li:nth-child(1) .case-path__dot { animation-name: case-path-dot-1; }
.case-path__steps li:nth-child(1)::after { animation-name: case-path-line-1; }
.case-path__steps li:nth-child(1) .case-path__step { animation-name: case-path-text-1; }
.case-path__steps li:nth-child(2) .case-path__dot { animation-name: case-path-dot-2; }
.case-path__steps li:nth-child(2)::after { animation-name: case-path-line-2; }
.case-path__steps li:nth-child(2) .case-path__step { animation-name: case-path-text-2; }
.case-path__steps li:nth-child(3) .case-path__dot { animation-name: case-path-dot-3; }
.case-path__steps li:nth-child(3)::after { animation-name: case-path-line-3; }
.case-path__steps li:nth-child(3) .case-path__step { animation-name: case-path-text-3; }
.case-path__steps li:nth-child(4) .case-path__dot { animation-name: case-path-dot-4; }
.case-path__steps li:nth-child(4)::after { animation-name: case-path-line-4; }
.case-path__steps li:nth-child(4) .case-path__step { animation-name: case-path-text-4; }

/* The count lands with the lane's last step. */
.case-path__lane .case-path__count { animation-name: case-path-text-4; }
.case-path__lane--after .case-path__count { animation-name: case-path-text-2; }

@keyframes case-path-dot-1 {
  0%, 0% { border-color: var(--color-border); background: var(--color-bg); }
  4%, 88% { border-color: var(--color-accent-fill); background: var(--color-accent-fill); }
  94%, 100% { border-color: var(--color-border); background: var(--color-bg); }
}

@keyframes case-path-line-1 {
  0%, 0% { width: 0; }
  4%, 88% { width: 100%; }
  94%, 100% { width: 0; }
}

@keyframes case-path-text-1 {
  0%, 0% { color: var(--path-idle); }
  4%, 88% { color: var(--color-text); }
  94%, 100% { color: var(--path-idle); }
}

@keyframes case-path-dot-2 {
  0%, 15% { border-color: var(--color-border); background: var(--color-bg); }
  19%, 88% { border-color: var(--color-accent-fill); background: var(--color-accent-fill); }
  94%, 100% { border-color: var(--color-border); background: var(--color-bg); }
}

@keyframes case-path-line-2 {
  0%, 15% { width: 0; }
  19%, 88% { width: 100%; }
  94%, 100% { width: 0; }
}

@keyframes case-path-text-2 {
  0%, 15% { color: var(--path-idle); }
  19%, 88% { color: var(--color-text); }
  94%, 100% { color: var(--path-idle); }
}

@keyframes case-path-dot-3 {
  0%, 30% { border-color: var(--color-border); background: var(--color-bg); }
  34%, 88% { border-color: var(--color-accent-fill); background: var(--color-accent-fill); }
  94%, 100% { border-color: var(--color-border); background: var(--color-bg); }
}

@keyframes case-path-line-3 {
  0%, 30% { width: 0; }
  34%, 88% { width: 100%; }
  94%, 100% { width: 0; }
}

@keyframes case-path-text-3 {
  0%, 30% { color: var(--path-idle); }
  34%, 88% { color: var(--color-text); }
  94%, 100% { color: var(--path-idle); }
}

@keyframes case-path-dot-4 {
  0%, 45% { border-color: var(--color-border); background: var(--color-bg); }
  49%, 88% { border-color: var(--color-accent-fill); background: var(--color-accent-fill); }
  94%, 100% { border-color: var(--color-border); background: var(--color-bg); }
}

@keyframes case-path-line-4 {
  0%, 45% { width: 0; }
  49%, 88% { width: 100%; }
  94%, 100% { width: 0; }
}

@keyframes case-path-text-4 {
  0%, 45% { color: var(--path-idle); }
  49%, 88% { color: var(--color-text); }
  94%, 100% { color: var(--path-idle); }
}

@media (min-width: 768px) {
  .case-path {
    padding: var(--space-6);
  }

  /* Label, steps and count on one row. */
  .case-path__lane {
    grid-template-columns: 4.5rem minmax(0, 1fr) 4.5rem;
    align-items: start;
  }

  .case-path__lane > .case-label,
  .case-path__count {
    padding-top: 0;
    line-height: 16px;
  }

  .case-path__count {
    text-align: right;
  }

  .case-path__step {
    font-size: var(--fs-small);
  }
}

/* Reduced motion: the journey shown complete, nothing moving. */
@media (prefers-reduced-motion: reduce) {
  .case-path__dot,
  .case-path__step,
  .case-path__steps li + li::after,
  .case-path__count {
    animation: none;
  }

  .case-path__dot {
    border-color: var(--color-accent-fill);
    background: var(--color-accent-fill);
  }
}

.case-tabs__panel > .case-path + .case-compare {
  margin-top: var(--space-5);
}

/* Before and after on one funnel (Easor's improvements): each step holds
   two bars on the same 0–100% scale, the before in neutral grey and the
   after in the accent, each with its own pill on top. Heights come from
   data-h, since the CSP blocks inline styles. */
.case-funnel__legend .case-funnel__swatch:not(:first-child) {
  margin-left: var(--space-3);
}

/* Three keys don't fit a phone on one line: each swatch and its label are
   one key, and the keys wrap whole. */
.case-funnel--compare .case-funnel__legend {
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
}

.case-funnel__key {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.case-funnel__bar {
  position: absolute;
  bottom: 0;
  left: calc(50% + 2px);
  right: 0;
  display: block;
  min-height: 3px;
  background: var(--color-accent-fill);
  transform-origin: bottom;
  transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1) 0.2s;
}

/* After .case-funnel__bar, so its grey wins over the accent. */
.case-funnel__bar--before {
  left: 0;
  right: calc(50% + 2px);
}

.case-funnel__swatch--before,
.case-funnel__bar--before {
  background: color-mix(in oklab, var(--color-text-secondary) 38%, transparent);
}

.case-funnel__bar .case-funnel__pill {
  bottom: 100%;
  padding: 1px var(--space-1);
  font-size: 11px;
  transform: translate(-50%, -4px);
}

.case-funnel__bar--before .case-funnel__pill {
  color: var(--color-text-secondary);
  font-weight: 500;
}

/* The projection (Easor's H3): the after bar continued, dashed and hatched
   like an empty frame, so it reads as the same bar but not yet real. It
   sits under the after bar, which covers it up to the real figure, and
   grows in once that one has. */
.case-funnel__swatch--proj,
.case-funnel__bar--proj {
  border: 1.5px dashed var(--color-accent-fill);
  background: repeating-linear-gradient(
    -45deg,
    color-mix(in oklab, var(--color-accent-fill) 45%, transparent) 0 1.5px,
    transparent 1.5px 6px
  );
}

.case-funnel__bar--proj {
  border-bottom: none;
  transition-delay: 0.6s;
}

.case-funnel__bar--proj .case-funnel__pill {
  font-weight: 500;
}

.case-funnel__bar[data-h="100"] { height: 100%; }
.case-funnel__bar[data-h="48"] { height: 48%; }
.case-funnel__bar[data-h="32"] { height: 32%; }
.case-funnel__bar[data-h="22"] { height: 22%; }
.case-funnel__bar[data-h="18"] { height: 18%; }
.case-funnel__bar[data-h="5"] { height: 5%; }

.js .case-funnel[data-reveal]:not(.is-visible) .case-funnel__bar {
  transform: scaleY(0);
}

@media (min-width: 768px) {
  .case-funnel__bar .case-funnel__pill {
    padding: 2px var(--space-2);
    font-size: 12px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-funnel__bar {
    transition: none;
  }
}

/* ---- A derived curve: the shape of a correlation, without figures ----
   GrowPro's conversion by days to proposal, on the funnel's sheet. Direct
   labels on the three moments, a recessive axis, and the line drawing
   itself in as the figure is revealed. The drawing scales with its box;
   the labels are HTML over it, placed in percent of the viewBox
   (520 × 270), so they keep a legend's size at any width. */
.case-curve {
  margin-inline: 0;
  margin-bottom: 0;
  padding: var(--space-5);
  border: 1px solid var(--color-border);
  background: var(--color-bg-card-media);
}

.case-curve__caption {
  font-size: 1.125rem;
  font-weight: 600;
}

.case-curve__svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.case-curve__chart {
  position: relative;
}

.case-curve__label {
  position: absolute;
  font-size: 0.75rem;
  line-height: 1.2;
  white-space: nowrap;
  color: var(--color-text-secondary);
}

.case-curve__label--value {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--color-text);
}

/* Beside each dot, sitting on its point's height. */
.case-curve__label--v1 { left: 10.4%; bottom: 83%; }
.case-curve__label--v2 { left: 45.8%; bottom: 48.2%; }
.case-curve__label--v3 { left: 88.2%; bottom: 37.8%; transform: translateX(-50%); }

/* Under the baseline: the first from the axis, the last to the end. */
.case-curve__label--s1 { left: 7.7%; top: 80.7%; }
.case-curve__label--s2 { left: 52.4%; top: 80.7%; transform: translateX(-50%); }
.case-curve__label--s3 { right: 2.9%; top: 80.7%; }

.case-curve__label--x { left: 52.4%; top: 91.5%; transform: translateX(-50%); }
.case-curve__label--y { left: 4.2%; top: 46.3%; transform: translate(-50%, -50%) rotate(-90deg); }

.case-curve__axis {
  fill: none;
  stroke: var(--color-border);
  stroke-width: 1;
}

.case-curve__band {
  stroke: var(--color-border);
  stroke-width: 1;
  stroke-dasharray: 3 4;
}

.case-curve__area {
  fill: var(--color-accent);
  opacity: 0.1;
}

.case-curve__line {
  fill: none;
  stroke: var(--color-accent);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 1.2s cubic-bezier(0.16, 1, 0.3, 1) 0.2s;
}

.case-curve__dot {
  r: 7px;
  fill: var(--color-accent);
  stroke: var(--color-bg-card-media);
  stroke-width: 3;
}

.js .case-curve[data-reveal]:not(.is-visible) .case-curve__line {
  stroke-dashoffset: 1;
}

@media (prefers-reduced-motion: reduce) {
  .case-curve__line {
    transition: none;
  }
}

/* The chart and its note: stacked on a phone, the note hanging past the
   sheet's edge like every note; from tablet up, chart two thirds and the
   note in the last third, beside it. */
.case-curve__body {
  display: grid;
  gap: var(--space-5);
  margin-top: var(--space-5);
}

@media (min-width: 768px) {
  .case-curve {
    padding: var(--space-6);
  }

  .case-curve__body {
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    align-items: center;
    column-gap: var(--space-6);
  }

  /* A third is narrow for a note: tighter padding and small type. */
  .case-curve__postit {
    justify-self: stretch;
    width: auto;
    margin: 0;
    padding: var(--space-5) var(--space-4);
    font-size: var(--fs-small);
  }

  .case-curve__postit strong {
    font-size: 1rem;
  }
}

.case-cards {
  display: grid;
  gap: var(--space-4);
}

.case-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5);
  border: 1px solid var(--color-border);
  background: var(--color-bg-card-header);
}

.case-card h4 {
  margin: 0;
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.3;
}

.case-card p {
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

:where(.case-section) > .section-inner > .case-prose + .case-cards {
  margin-top: var(--space-5);
}

@media (min-width: 768px) {
  .case-cards {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* A pair, when a row of three would leave a hole. */
@media (min-width: 768px) {
  .case-cards--two {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Which stage of the funnel each study looked at. */
.case-card__pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-bottom: var(--space-2);
}

/* The non-activators' answers, each pointing at the kind of fix it needs. */
.case-card__answers {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-top: var(--space-2);
  padding-left: 1.1em;
  list-style: disc;
  font-size: var(--fs-small);
}

.case-card__answers li::marker {
  color: var(--color-accent);
}

.case-card__answers span:first-child {
  font-family: "Inter", sans-serif;
  font-style: italic;
}

.case-card__arrow {
  color: var(--color-accent);
}

.case-card__answers strong {
  font-weight: 600;
  white-space: nowrap;
}

/* ---- The closure: how it ended and what came next, off the sheet above ---- */
/* A line drops from the sheet's bottom edge to an end circle, like the last
   stage of the launch timeline; the text sits beside the circle.
   --closure-x lines the circle up with the sheet's inner padding. */
.case-closure {
  --closure-x: var(--space-5);
  --closure-drop: var(--space-7);
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-width: calc(40rem + var(--closure-x) + 1.75rem + var(--space-4));
  padding: var(--closure-drop) 0 0 calc(var(--closure-x) + 1.75rem + var(--space-4));
}

:where(.case-section) > .section-inner > .case-closure {
  margin-top: 0;
}

.case-closure::before {
  content: "";
  position: absolute;
  top: 0;
  left: calc(var(--closure-x) + 0.875rem - 1px);
  width: 2px;
  height: var(--closure-drop);
  background: var(--color-border);
}

.case-closure::after {
  content: "";
  position: absolute;
  top: calc(var(--closure-drop) - 0.125rem);
  left: var(--closure-x);
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: var(--color-accent-fill);
}

.case-closure__lead {
  font-weight: 600;
}

.case-closure__soon {
  font-size: var(--fs-small);
  color: var(--color-text-secondary);
}

@media (min-width: 768px) {
  .case-closure {
    --closure-x: var(--space-6);
  }
}

/* ---- 09: the gap ---- */
.case-gap ol {
  display: grid;
  gap: var(--space-3);
}

.case-gap li {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-1);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  background: var(--color-bg);
}

.case-gap__value {
  font-size: 2rem;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.case-gap .case-gap__missing {
  border: 1px dashed var(--color-accent);
  background: transparent;
  text-align: center;
  font-weight: 600;
}

@media (min-width: 768px) {
  .case-gap ol {
    grid-template-columns: 1fr 1.4fr 1fr;
  }
}

.case-end {
  display: flex;
  justify-content: center;
}

.case-end__link {
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  font-size: var(--fs-small);
  font-weight: 500;
  transition: background-color 0.25s ease;
}

.case-end__link:hover {
  background: var(--color-bg-card-header);
}

/* ---- growpro.html: variants of the blocks above ---- */

/* Research: the facts row stands alone; the findings open as their own box,
   headed by a strip. */
.case-box--row + .case-strip {
  margin-top: var(--space-6);
}

/* Three stats: 2 + 1 on narrow screens, one row on wide ones. */
.case-stats--three > :last-child {
  grid-column: 1 / -1;
}

@media (min-width: 1024px) {
  .case-stats--three {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .case-stats--three > :last-child {
    grid-column: auto;
  }
}

/* Images stacked in one column, beside a timeline. */
.case-figure-stack {
  display: grid;
  gap: var(--space-4);
  align-content: start;
}

/* A wide placeholder, for a flow that reads left to right. */
.case-figure__slot--wide {
  aspect-ratio: 21 / 9;
}

/* Three notes in a row; the last still hangs past the edge. */
@media (min-width: 768px) {
  .case-postits--three {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    justify-content: stretch;
  }
}

/* 01: the passage from the tangle of tools to one platform, as two SVG
   panels on one scale with an arrow between them: the columns share the
   viewBox widths (712 / 340) so a label is the same size on both sides. */
.case-system {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin-inline: 0;
  margin-bottom: 0;
}

.case-system__caption {
  font-size: 1.125rem;
  font-weight: 600;
  line-height: 1.3;
}

.case-system__panels {
  display: grid;
  gap: var(--space-5);
}

.case-system__panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

/* Not a comparison but a passage: the arrow points down while the panels
   stack, and across once they sit side by side. */
.case-system__arrow {
  display: grid;
  place-items: center;
  justify-self: center;
  align-self: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  background: var(--color-accent-tint);
  color: var(--color-accent);
  transform: rotate(90deg);
}

.case-system__arrow svg {
  width: 1.25rem;
  height: 1.25rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: square;
  stroke-linejoin: miter;
}

.case-system__map {
  overflow-x: auto;
}

.case-system__svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  font-family: inherit;
}

/* On phones the map would shrink its labels past legibility; it scrolls. */
@media (max-width: 767px) {
  .case-system__svg--before {
    min-width: 34rem;
  }

  /* Same scale as the scrolling map: 34rem x 340 / 712. */
  .case-system__svg--after {
    max-width: 16.25rem;
    margin-inline: auto;
  }
}

.case-system text,
.zoom-modal__diagram .case-system__svg text {
  dominant-baseline: central;
  fill: var(--color-text);
}

.sys-node > rect:first-child {
  fill: var(--color-bg-card-media);
  stroke: var(--color-border);
  stroke-width: 1;
}

.sys-node text {
  font-weight: 600;
}

/* What each tool is, after its name: smaller and quieter. */
.case-system .sys-node__note,
.zoom-modal__diagram .case-system__svg .sys-node__note {
  font-size: 13px;
  font-weight: 400;
  fill: var(--color-text-secondary);
}

.sys-node__icon {
  fill: var(--color-accent-tint);
}

.sys-node__glyph {
  fill: none;
  stroke: var(--color-accent);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.sys-node--hub > rect:first-child {
  fill: var(--color-accent-fill);
  stroke: none;
}

.case-system .sys-node--hub text,
.zoom-modal__diagram .case-system__svg .sys-node--hub text {
  fill: var(--color-on-accent-fill);
}

.sys-actor > rect {
  fill: var(--color-accent-tint);
}

.sys-actor__face {
  fill: none;
  stroke: var(--color-text);
  stroke-width: 1.3;
  stroke-linecap: round;
}

.sys-tag > rect {
  fill: none;
  stroke: var(--color-border);
}

.case-system .sys-tag text,
.zoom-modal__diagram .case-system__svg .sys-tag text {
  fill: var(--color-text-secondary);
}

.sys-team {
  fill: none;
  stroke: var(--color-text-secondary);
  stroke-width: 1.3;
  stroke-linecap: round;
}

.sys-edge {
  fill: none;
  stroke: var(--color-text-secondary);
  stroke-width: 1.5;
}

.sys-edge.is-flow {
  stroke-dasharray: 3 4;
}

.sys-arrow path {
  fill: none;
  stroke: var(--color-text-secondary);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (min-width: 768px) {
  .case-system__panels {
    grid-template-columns: minmax(0, 712fr) auto minmax(0, 340fr);
    gap: var(--space-4);
  }

  .case-system__arrow {
    transform: none;
  }

  /* The GPX map sits level with the middle of the busy one. */
  .case-system__panel--after .case-system__map {
    margin-block: auto;
  }
}

/* 07: target metrics, one row per metric (today, target, why), grouped by
   area. */
.case-bar {
  margin-inline: 0;
  margin-bottom: 0;
  padding: 0;
  container-type: inline-size;
  background: var(--color-bg-card-media);
}

.case-bar table {
  width: 100%;
  border-collapse: collapse;
}

.case-bar th,
.case-bar td {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--fs-small);
  text-align: left;
  vertical-align: top;
}

.case-bar thead th {
  font-weight: 600;
  color: var(--color-text-secondary);
}

.case-bar tbody:last-child tr:last-child > * {
  border-bottom: none;
}

.case-bar tbody th[scope="row"] {
  font-weight: 500;
}

.case-bar__group th {
  background: var(--color-accent-tint);
  font-size: var(--fs-body);
  font-weight: 600;
}

.case-bar__value {
  font-weight: 600;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.case-bar__target {
  color: var(--color-accent);
}

/* Too narrow for four columns: metric, today and target on one line, the
   reason underneath. */
@container (max-width: 36rem) {
  .case-bar thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  .case-bar tbody tr:not(.case-bar__group) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    border-bottom: 1px solid var(--color-border);
  }

  .case-bar tbody:last-child tr:last-child {
    border-bottom: none;
  }

  .case-bar tbody tr:not(.case-bar__group) > * {
    border-bottom: none;
  }

  .case-bar tbody tr:not(.case-bar__group) > :first-child {
    padding-bottom: 0;
  }

  .case-bar tbody tr:not(.case-bar__group) > :nth-child(2),
  .case-bar tbody tr:not(.case-bar__group) > :nth-child(3) {
    padding-bottom: 0;
    padding-left: 0;
    text-align: right;
  }

  /* Without the header row, an arrow says which number is the target. */
  .case-bar__target::before {
    content: "→ " / "";
    color: var(--color-text-secondary);
  }

  .case-bar tbody tr:not(.case-bar__group) > :last-child {
    grid-column: 1 / -1;
    padding-top: var(--space-1);
    color: var(--color-text-secondary);
  }

  .case-bar__group {
    display: block;
  }

  .case-bar__group th {
    display: block;
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-crumbs a,
  .case-end__link,
  .case-forward a,
  .case-funnel__fill {
    transition: none;
  }

  .js .case-funnel[data-reveal]:not(.is-visible) .case-funnel__fill {
    transform: none;
  }
}

/* ==========================================================================
   Phones: the work, framed for a portrait screen
   Below 768px the visuals stay inside the page gutter and keep their
   frames, but take a portrait-friendly shape: heroes a phone cut (a cropped
   <picture> source, or object-fit for the video). The index mockups stay
   whole, as on desktop. An image that opens full size (image-zoom.js) says so with a
   small corner badge, since touch has no zoom-in cursor.
   ========================================================================== */
@media (max-width: 767.98px) {
  /* The video has no phone cut of its own; a still hero brings one. Full
     screen (its Expand) shows the whole frame again. */
  .case-hero__video {
    aspect-ratio: 4 / 5;
    height: auto;
    object-fit: cover;
    object-position: var(--crop-x, 50%) 50%;
  }

  .case-hero__video:fullscreen {
    aspect-ratio: auto;
    object-fit: contain;
  }
}

/* The zoom badge, for touch only: Expand's square (.expand-btn--over) in
   the picture's corner, drawing only the two edges that face the picture. */
@media (hover: none) {
  .js :is(.case-figure:not(:has(.case-carousel__slide)), .case-showcase__media, .case-hero__media, .case-carousel__slide):has(img[data-zoom]) {
    position: relative;
  }

  .js :is(.case-figure:not(:has(.case-carousel__slide)), .case-showcase__media, .case-hero__media, .case-carousel__slide):has(img[data-zoom])::after {
    content: "";
    position: absolute;
    right: 0;
    bottom: 0;
    width: 44px;
    height: 44px;
    border: 1px solid var(--color-border);
    border-right: none;
    border-bottom: none;
    background: var(--color-bg);
    pointer-events: none;
  }

  /* The icon sits on its own layer so a mask can tint it. */
  .js :is(.case-figure:not(:has(.case-carousel__slide)), .case-showcase__media, .case-hero__media, .case-carousel__slide):has(img[data-zoom])::before {
    content: "";
    position: absolute;
    z-index: 1;
    right: 13px;
    bottom: 13px;
    width: 18px;
    height: 18px;
    background: var(--color-text);
    mask: url("assets/icon-expand.svg") center / contain no-repeat;
    pointer-events: none;
  }
}

/* Before → after on a phone: one topic per block, and the change is the
   headline. "After" leads, large; "before" follows small and struck, as
   what it replaced. The header row stays for screen readers only. */
@media (max-width: 767.98px) {
  @container (max-width: 28rem) {
    .case-compare--text thead {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }

    .case-compare--text tbody tr {
      grid-template-columns: minmax(0, 1fr);
      padding: var(--space-4);
      border-bottom: 1px solid var(--color-border);
    }

    .case-compare--text tbody tr:last-child {
      border-bottom: none;
    }

    .case-compare--text tbody th,
    .case-compare--text tbody td,
    .case-compare--text tbody th:first-child,
    .case-compare--text tbody td:first-child,
    .case-compare--text tbody td:first-of-type,
    .case-compare--text tbody td:last-child {
      padding: 0;
      border: none;
      background: none;
    }

    .case-compare--text tbody th {
      order: 0;
      margin-bottom: var(--space-1);
      color: var(--color-text-secondary);
      font-size: var(--fs-small);
      font-weight: 500;
    }

    .case-compare--text tbody td:last-child {
      order: 1;
      font-size: 1.125rem;
      line-height: 1.3;
      font-weight: 600;
    }

    .case-compare--text tbody td:first-of-type {
      order: 2;
      margin-top: var(--space-1);
      color: var(--color-text-secondary);
      font-size: var(--fs-small);
      text-decoration: line-through;
      text-decoration-color: color-mix(in srgb, var(--color-text-secondary) 60%, transparent);
    }

    /* Who decided what (GrowPro): two owners, not a before and an after.
       Neither is struck; each cell names its column, since the header is
       hidden. */
    .case-compare--who tbody td:first-of-type {
      order: 1;
      margin-top: 0;
      color: var(--color-text);
      font-size: var(--fs-body);
      text-decoration: none;
    }

    .case-compare--who tbody td:last-child {
      order: 2;
      margin-top: var(--space-1);
      font-size: var(--fs-body);
      font-weight: 500;
    }

    .case-compare--who .case-compare__col {
      display: inline;
    }

    /* Marks: the feature and its two marks on one line, under the two
       account names, instead of a band and a row each. Every cell of a row
       stretches to the row's height, so the hairlines run straight across;
       the account names sit on the bottom, so their pills line up even
       when one name takes two lines. */
    .case-compare:not(.case-compare--text) thead tr,
    .case-compare:not(.case-compare--text) tbody tr {
      grid-template-columns: minmax(0, 1fr) 6rem 6rem;
      align-items: stretch;
    }

    .case-compare:not(.case-compare--text) thead td {
      display: block;
    }

    .case-compare:not(.case-compare--text) thead th {
      display: flex;
      flex-direction: column;
      justify-content: flex-end;
      padding-inline: var(--space-1);
      text-align: center;
    }

    .case-compare:not(.case-compare--text) .case-compare__head {
      flex-direction: column;
      gap: var(--space-1);
      white-space: normal;
      font-size: 0.8125rem;
      line-height: 1.25;
    }

    .case-compare:not(.case-compare--text) .case-compare__sub {
      display: none;
    }

    .case-compare:not(.case-compare--text) tbody th {
      display: flex;
      align-items: center;
      grid-column: auto;
      padding-block: var(--space-3);
      background: none;
      border-bottom: 1px solid var(--color-border);
    }

    .case-compare:not(.case-compare--text) tbody td {
      display: grid;
      place-items: center;
    }

    /* The Nomo column gets its strip back. */
    .case-compare:not(.case-compare--text) .is-ours,
    .case-compare:not(.case-compare--text) tbody td:last-child {
      background: var(--color-accent-tint);
    }
  }
}

/* Target metrics on a phone: tiles, two by two, under each group's band.
   The target is the figure; the benchmark it beats sits under it, struck,
   and the reason closes the tile. A group with an odd count lets its last
   tile take the full row. */
@media (max-width: 767.98px) {
  @container (max-width: 36rem) {
    .case-bar tbody {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 1px;
      background: var(--color-border);
      border-bottom: 1px solid var(--color-border);
    }

    .case-bar tbody:last-child {
      border-bottom: none;
    }

    .case-bar tbody tr.case-bar__group {
      grid-column: 1 / -1;
    }

    .case-bar tbody tr:not(.case-bar__group) {
      display: flex;
      flex-direction: column;
      gap: var(--space-1);
      padding: var(--space-4);
      border-bottom: none;
      background: var(--color-bg-card-media);
    }

    .case-bar tbody tr:not(.case-bar__group):last-child:nth-child(even) {
      grid-column: 1 / -1;
    }

    .case-bar tbody tr:not(.case-bar__group) > *,
    .case-bar tbody tr:not(.case-bar__group) > :nth-child(2),
    .case-bar tbody tr:not(.case-bar__group) > :nth-child(3),
    .case-bar tbody tr:not(.case-bar__group) > :last-child {
      padding: 0;
      border: none;
      text-align: left;
    }

    .case-bar tbody th[scope="row"] {
      order: 0;
      line-height: 1.3;
    }

    .case-bar tbody tr:not(.case-bar__group) > .case-bar__target {
      order: 1;
      margin-top: var(--space-1);
      font-size: 1.5rem;
      line-height: 1.15;
      white-space: normal;
    }

    .case-bar__target::before {
      content: none;
    }

    .case-bar tbody tr:not(.case-bar__group) > :nth-child(2) {
      order: 2;
      color: var(--color-text-secondary);
      font-weight: 400;
      text-decoration: line-through;
    }

    .case-bar tbody tr:not(.case-bar__group) > :last-child {
      order: 3;
      margin-top: var(--space-2);
      padding-top: 0;
      color: var(--color-text-secondary);
      line-height: 1.35;
    }
  }
}

@media (max-width: 767.98px) {
  /* A visual still to come is a grey box; on a phone it only costs scroll. */
  .case-figure:has(> .placeholder) {
    display: none;
  }
}

/* ---- Wide diagrams on a phone (image-zoom.js): whole, small, expandable ----
   The journeys and the system map are drawn for a wide screen. Instead of
   a sideways scroll that cuts them mid-step, a phone shows each one whole
   across the screen, as a shape, and Expand opens it full screen at a
   size that reads. The button only exists with the script. */
.expand-btn {
  display: none;
}

@media (max-width: 767.98px) {
  .case-journey,
  .case-cones,
  .case-system__panel:has(.case-system__svg--before) {
    position: relative;
  }

  .case-journey__map,
  .case-cones__map,
  .case-system__map:has(> .case-system__svg--before) {
    overflow: visible;
    cursor: zoom-in;
  }

  .case-journey__svg,
  .case-cones__svg,
  .case-system__svg--before {
    min-width: 0;
  }

  /* Expand, for what a phone shows cut down (the wide maps, the hero video;
     image-zoom.js, case-video.js): a square on the section hairline, like
     the pager's buttons, its name in aria-label. Under a map it sits on
     the map's right edge… */
  .expand-btn {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    margin: var(--space-2) 0 0 auto;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: 0;
    background: var(--color-bg);
    color: var(--color-text);
    cursor: pointer;
    transition: box-shadow 0.25s ease;
  }

  .expand-btn:hover {
    box-shadow: inset 0 0 0 1px var(--color-accent);
  }

  .expand-btn svg {
    width: 18px;
    height: 18px;
  }

  /* …and over a video, in the frame's corner: the frame draws its right
     and bottom edges, the square only the two that face the picture. */
  .case-hero__media {
    position: relative;
  }

  .expand-btn--over {
    position: absolute;
    right: 0;
    bottom: 0;
    margin: 0;
    border-right: none;
    border-bottom: none;
  }
}

/* The dialog, holding a diagram instead of an image: the page's ground, so
   the map's theme colours read as they do in place; edge to edge, to pan. */
.zoom-modal__diagram {
  display: none;
}

.zoom-modal.is-diagram {
  background: var(--color-bg);
}

.zoom-modal.is-diagram .zoom-modal__stage {
  padding-inline: 0;
  cursor: default;
  touch-action: auto;
}

.zoom-modal.is-diagram .zoom-modal__img,
.zoom-modal.is-diagram .pager {
  display: none;
}

.zoom-modal.is-diagram .zoom-modal__diagram {
  display: flex;
  align-items: center;
  flex: 1;
  min-height: 0;
  width: 100%;
  overflow: auto;
  overscroll-behavior: contain;
  padding: var(--space-4);
  touch-action: pan-x pan-y pinch-zoom;
}

.zoom-modal__diagram svg {
  flex: none;
  width: max(100%, 50rem);
  min-width: 0;
  height: auto;
  margin: auto;
}

/* The system map's viewBox is squarer: a little less width reads as well. */
.zoom-modal__diagram .case-system__svg--before {
  width: max(100%, 36rem);
}

.zoom-modal.is-diagram .zoom-modal__bar {
  padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom));
}
