/* ==========================================================================
   Zayn portfolio - styles.css
   Stage 1: scaffold, tokens, self-hosted fonts, dot grid, static desktop layout.
   Stage 2: load-in (decode + staggered fade), row states R1 to R4, pointer
            trails, CTA pill (drift, light pass, rim, edge lift, hover mist,
            press bloom).
   Stage 3: panel views V2 project (placeholder video, title, sub, caption,
            visit pill), V3 About/Pricing, V4 contact form, V5 swap animation,
            aria-live region.
   Stage 4: contact form states (success/failure overlay, in-flight submit).
            Vanilla CSS only. UK English. No em dashes.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Self-hosted fonts (variable woff2, latin subset, font-display: swap)

   Source: Google Fonts CSS v2 API, queried with a desktop Chrome user agent
   which returns variable woff2 URLs. Files downloaded once into fonts/ so
   there is no runtime dependency on Google's servers.

   Archivo (weights 400 to 600), latin subset:
     https://fonts.googleapis.com/css2?family=Archivo:wght@400..600&display=swap
     -> https://fonts.gstatic.com/s/archivo/v25/k3kPo8UDI-1M0wlSV9XAw6lQkqWY8Q82sLydOxI.woff2

   Martian Mono (weights 300 to 500), latin subset:
     https://fonts.googleapis.com/css2?family=Martian+Mono:wght@300..500&display=swap
     -> https://fonts.gstatic.com/s/martianmono/v6/2V0PKIcADoYhV6w87xrTKjs4CYElh_VS9YA4TlTnaTq9wQ.woff2
   -------------------------------------------------------------------------- */

@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 600;
  font-stretch: 100%;
  font-display: swap;
  src: url('../fonts/archivo-latin-var.woff2?v=19') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Martian Mono';
  font-style: normal;
  font-weight: 300 500;
  font-stretch: 100%;
  font-display: swap;
  src: url('../fonts/martian-mono-latin-var.woff2?v=19') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --------------------------------------------------------------------------
   Design tokens - reproduced verbatim from CONCEPT.md 3.1
   -------------------------------------------------------------------------- */

:root {
  /* Ground & ink */
  --ground:            #FAFAFA;
  --ink:               #141416;
  --ink-body:          #43434A;
  --ink-mid:           #54545C;
  --grey-label:        #8E8E96;
  --grey-soft:         #A6A6AE;
  --grey-placeholder:  #9A9AA2;

  /* Accent, sea blue */
  --accent:            #1A6FD4;
  --accent-rgb:        26,111,212;
  --accent-fill-active:#E7F1FB;
  --accent-fill-hover: #EDF4FC;
  --accent-hairline:   rgba(26,111,212,0.16);
  --accent-innershade: rgba(26,111,212,0.08);
  --accent-ghost:      rgba(26,111,212,0.07);

  /* Pointer trails (row hover) */
  --trail-a:           #8FC3EE;
  --trail-b:           #B9DDF5;
  --trail-opacity:     0.32;

  /* Borders & surfaces */
  --border:            #E8E8EC;
  --border-width:      1px;      /* the only border width on the page */
  --surface-white:     #FFFFFF;
  --surface-field:     #FAFAFA;
  --surface-preview:   #F4F4F7;
  --surface-panelhead: rgba(250,250,250,0.7);
  --submit-ink:        #141416;
  --submit-ink-hover:  #000000;

  /* CTA pill (row 06), pastel water fill drifting on x and y */
  --cta-layer1: radial-gradient(70% 180% at 20% 15%, #DCEEF9 0%, rgba(220,238,249,0) 62%);
  --cta-layer2: radial-gradient(60% 170% at 75% 85%, #CFDDF3 0%, rgba(207,221,243,0) 64%);
  --cta-layer3: linear-gradient(100deg, #D6E9F7, #C6D9F1, #D9E7F6, #D6E9F7);
  --cta-bg-size: 260% 260%, 240% 240%, 300% 200%;
  --cta-lightpass: radial-gradient(55% 150% at 50% 50%, rgba(255,255,255,0.30) 0%, rgba(255,255,255,0) 68%);
  --cta-edge-lift: radial-gradient(120% 160% at 50% 50%, rgba(255,255,255,0) 42%, rgba(255,255,255,0.34) 100%);
  --cta-rim:    inset 0 0 0 1px rgba(255,255,255,0.62), inset 0 1px 0 rgba(255,255,255,0.85);
  --cta-hover-mist: radial-gradient(circle, rgba(233,246,253,0.55) 0%, rgba(233,246,253,0.26) 44%, rgba(233,246,253,0) 72%);
  --cta-press-mist: radial-gradient(circle, rgba(255,255,255,0.75) 0%, rgba(255,255,255,0.40) 42%, rgba(255,255,255,0) 72%);
  --cta-text:          #141416;
  --cta-index:         rgba(20,20,22,0.5);

  /* Dot grid (canvas, fixed, behind everything) */
  --dot-rest:  rgba(20,20,22,0.06);
  --dot-warm:  rgb(26,111,212);
  --dot-size:  1.5px;  --dot-spacing: 26px;

  /* Radii, exactly two values */
  --radius-pill:    999px;
  --radius-surface: 16px;

  /* Spacing scale (all values in use) */
  --sp-2: 2px;  --sp-4: 4px;  --sp-6: 6px;  --sp-10: 10px; --sp-12: 12px;
  --sp-14: 14px; --sp-16: 16px; --sp-18: 18px; --sp-22: 22px; --sp-26: 26px;
  --sp-32: 32px; --sp-36: 36px; --sp-48: 48px; --sp-56: 56px; --sp-76: 76px;
  --sp-80: 80px;
  /* Row vertical padding 11px. Field padding 14px 18px. Submit 15px 26px. */

  /* Shadows: no drop shadows anywhere, inset only */
  --shadow-row-hover: inset 0 0 0 1px rgba(26,111,212,0.16), inset 0 -3px 12px rgba(26,111,212,0.08);
  --shadow-field-focus: inset 0 0 0 1px #1A6FD4;
}

/* --------------------------------------------------------------------------
   Non-token helpers (font families, z-layers). Families are not tokenised in
   3.1; kept out of the verbatim block above and declared here instead.
   -------------------------------------------------------------------------- */

:root {
  --font-sans: 'Archivo', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-mono: 'Martian Mono', ui-monospace, 'SFMono-Regular', 'Cascadia Mono', Menlo, Consolas, monospace;
  --font-system: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

/* --------------------------------------------------------------------------
   Owner-directed amendment (Client feedback round 1, 2026-07-27). The index
   restyle moves to the Moritz Petersen reference: quiet grey rows at rest, the
   selected row a raised white rounded-rectangle card. House shadow language is
   inset-only (CONCEPT 3.1), but the raised-card look needs real elevation and
   the owner's direction wins. This one restrained drop shadow is scoped to the
   active row card ONLY and is used nowhere else on the page. Tuned by eye.
   Recorded in PROGRESS.md as a supersession of the "no drop shadows" rule.
   -------------------------------------------------------------------------- */

:root {
  --shadow-row-active: 0 1px 3px rgba(20, 20, 22, 0.06);
}

/* --------------------------------------------------------------------------
   Owner-directed amendment (Client feedback round 5, 2026-07-27). A theme
   toggle (system / light / dark) is added. CONCEPT section 13's "no dark mode"
   is SUPERSEDED by owner direction (recorded in PROGRESS.md).

   ONE mechanism (reported): system is resolved to a concrete theme in JS, and
   the resolved theme is written as data-theme="light" | "dark" on <html> (an
   inline head script applies it before first paint, so there is no flash). CSS
   therefore only ever reads [data-theme="dark"]; there is no
   prefers-color-scheme media query in the styles. The 3.1 light token block
   above stays verbatim; the dark values are NEW tokens in this clearly-labelled
   block and take effect only under [data-theme="dark"].

   The dark palette is derived from the existing design language, not a new
   aesthetic: a near-black ground, panel/card surfaces stepped up from it, ink
   inverted to near-white, greys re-derived to keep the SAME contrast rules the
   light theme enforces (AA at mobile rest and focus; the desktop-rest greys are
   the same accepted sub-AA exception, mirrored), the accent lifted within the
   sea-blue family so it clears AA as text on dark, and the CTA pastel drift
   redrawn in deep water. Computed contrast (WCAG) on the dark ground #141416:
   ink 16.2:1, ink-body 9.3:1, ink-mid 7.6:1 (6.7:1 on the dark field, for
   placeholders), accent #4E95E8 5.95:1 (5.0:1 on the lifted card), CTA text
   11.7:1 on the pill; the desktop-rest greys #74747C (4.0:1) and #68686F
   (3.3:1) mirror the light exception. Recorded in PROGRESS.md. */

:root {
  color-scheme: light;
}

[data-theme="dark"] {
  color-scheme: dark;

  /* Ground & ink */
  --ground:            #141416;
  --ink:               #F0F0F2;
  --ink-body:          #B8B8C0;
  --ink-mid:           #A6A6AE;   /* the AA-pass grey: 7.6:1 ground, 6.7:1 field */
  --grey-label:        #74747C;   /* quiet desktop-rest label (sub-AA exception) */
  --grey-soft:         #68686F;   /* fainter desktop-rest index/tag (sub-AA) */
  --grey-placeholder:  #8E8E96;   /* defined for parity; unused since stage 8 */

  /* Accent, sea blue lifted so it clears AA as text/icon/ring on dark. */
  --accent:            #4E95E8;
  --accent-rgb:        78,149,232;
  --accent-ghost:      rgba(94, 166, 240, 0.10);   /* faint ghost arrow on dark */

  /* Borders & surfaces. Stepped: ground < panel/preview < field < lifted card. */
  --border:            #33333A;
  --surface-white:     #242428;   /* lifted dark card (active row, form, status) */
  --surface-field:     #202024;
  --surface-preview:   #1B1B1E;   /* panel + neutral preview frame for light clips */
  --surface-panelhead: rgba(255, 255, 255, 0.04);
  --submit-ink:        #EAEAEE;   /* inverted: a light button with a dark label */
  --submit-ink-hover:  #FFFFFF;

  /* CTA pill: dark-water variant. Deep blues at low luminance, same drift design
     (the keyframes and --cta-bg-size are theme-neutral and unchanged). */
  --cta-layer1: radial-gradient(70% 180% at 20% 15%, #1B3A5C 0%, rgba(27,58,92,0) 62%);
  --cta-layer2: radial-gradient(60% 170% at 75% 85%, #16324A 0%, rgba(22,50,74,0) 64%);
  --cta-layer3: linear-gradient(100deg, #163049, #12283D, #183453, #163049);
  --cta-lightpass: radial-gradient(55% 150% at 50% 50%, rgba(150,190,235,0.14) 0%, rgba(150,190,235,0) 68%);
  --cta-edge-lift: radial-gradient(120% 160% at 50% 50%, rgba(150,190,235,0) 42%, rgba(150,190,235,0.16) 100%);
  --cta-rim:    inset 0 0 0 1px rgba(190,216,244,0.20), inset 0 1px 0 rgba(200,224,248,0.30);
  --cta-hover-mist: radial-gradient(circle, rgba(120,170,230,0.30) 0%, rgba(120,170,230,0.14) 44%, rgba(120,170,230,0) 72%);
  --cta-press-mist: radial-gradient(circle, rgba(200,224,248,0.40) 0%, rgba(200,224,248,0.20) 42%, rgba(200,224,248,0) 72%);
  --cta-text:          #EAF2FB;
  --cta-index:         rgba(234, 242, 251, 0.55);

  /* Dot grid: light dots on the dark ground, warming toward the lifted accent.
     The canvas re-reads these two tokens and repaints on a theme change (main.js). */
  --dot-rest:  rgba(240, 240, 242, 0.07);
  --dot-warm:  rgb(94, 166, 240);

  /* Focus ring uses the lifted accent so it stays visible on dark. */
  --shadow-field-focus: inset 0 0 0 1px #4E95E8;

  /* The single quiet card shadow, deepened so it reads on dark surfaces (still
     the only non-inset shadow on the page). */
  --shadow-row-active: 0 1px 3px rgba(0, 0, 0, 0.45);
}

/* --------------------------------------------------------------------------
   Reset & base
   -------------------------------------------------------------------------- */

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

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

body {
  font-family: var(--font-sans);
  color: var(--ink);
  background: var(--ground);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

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

/* --------------------------------------------------------------------------
   Load-in: block fade (CONCEPT.md 3.3 "Block fade-in")
   opacity 0 to 1, translateY 6px to 0, 500ms ease. Per-block stagger delay is
   set inline via --fade-delay (0/120/180/240/320/350/400ms across the left
   column blocks and the panel). Transform + opacity only, so there is no
   layout shift after the load-in (performance budget, section 2).
   The .is-in class is added once by main.js after first paint.
   -------------------------------------------------------------------------- */

.fade-block {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 500ms ease, transform 500ms ease;
  transition-delay: var(--fade-delay, 0ms);
}

.fade-block.is-in {
  opacity: 1;
  transform: none;
}

/* --------------------------------------------------------------------------
   Dot grid canvas - fixed, behind everything, decorative
   -------------------------------------------------------------------------- */

.dot-grid {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  display: block;
}

/* --------------------------------------------------------------------------
   Page shell - desktop layout (CONCEPT.md 3.1 layout paragraph)
   -------------------------------------------------------------------------- */

.page {
  position: relative;
  z-index: 1;
  height: 100vh;
  min-height: 860px;
  display: grid;
  grid-template-columns: 460px 1fr;
  gap: var(--sp-80);
  padding: var(--sp-76) var(--sp-76) var(--sp-56);
}

/* Left column: flex column, proof strip pinned to the bottom */
.col-left {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* --------------------------------------------------------------------------
   Name block
   -------------------------------------------------------------------------- */

.name-block {
  display: flex;
  align-items: baseline;
  gap: var(--sp-14);
}

/* Type: Name "Zayn" - Archivo 31px / 600 / -0.015em / normal / as typed */
.name {
  font-family: var(--font-sans);
  font-size: 31px;
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: normal;
  color: var(--ink);
}

/* Type: FREELANCE / WEB - Mono 10px / 400 / normal / normal / UPPER */
.name-kicker {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: normal;
  line-height: normal;
  text-transform: uppercase;
  color: var(--grey-label);
}

/* --------------------------------------------------------------------------
   Positioning line
   -------------------------------------------------------------------------- */

/* Type: Positioning line - Archivo 16px / 400 / normal / 1.6 / sentence */
.positioning {
  font-family: var(--font-sans);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--ink-body);
  margin-top: var(--sp-16);
  max-width: 34ch;
}

/* Type: "£300" inline - Mono 14px / 400 / normal / inherit / accent colour */
.price {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 400;
  line-height: inherit;
  color: var(--accent);
}

/* --------------------------------------------------------------------------
   About expander (Client feedback round 8). About folds into the masthead under
   the positioning line: a quiet mono "More" disclosure that reveals the approved
   About prose in place. Collapsed by default on every breakpoint. The reveal is
   a grid-template-rows 0fr -> 1fr height animation plus an opacity fade over
   ~180ms; under prefers-reduced-motion the global guard zeroes both, so it is
   instant. All tokens, so both themes work with no new values.
   -------------------------------------------------------------------------- */

/* Height-budget compaction (Client feedback round 8, measured): removing the two
   index rows freed space, but the always-on pricing block plus the About expander
   net-add more than that, so the left-column section rhythm is tightened from the
   existing scale so the collapsed masthead + 4-row index + pricing + how-it-works
   + proof + footer all fit at the 860 min-height with the proof pinned and no
   scroll (measured docScrollH 860, footer gap ~23px). Tokens only. */
.about {
  margin-top: var(--sp-10);
}

/* Quiet mono disclosure button (10px UPPER, --grey-label), matching the masthead
   kicker type; the chevron is the one consistent glyph treatment (inline SVG,
   same stroke family as the theme-toggle icons). */
.about-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-6);
  padding: var(--sp-4) 0;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  line-height: normal;
  text-transform: uppercase;
  color: var(--grey-label);
  transition: color 150ms ease;
}

@media (hover: hover) and (pointer: fine) {
  .about-toggle:hover {
    color: var(--ink);
  }
}

.about-toggle:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
  border-radius: var(--radius-surface);
}

.about-toggle-glyph {
  display: block;
  width: 12px;
  height: 12px;
  transition: transform 180ms ease;
}

/* Chevron flips up when the disclosure is open. */
.about-toggle[aria-expanded="true"] .about-toggle-glyph {
  transform: rotate(180deg);
}

/* The reveal: a grid track that animates from 0fr (collapsed) to 1fr (open), so
   the prose height eases in without measuring it in JS. The inner wrapper clips
   the content while collapsed and fades with the height. */
.about-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 180ms ease;
}

.about-panel.is-open {
  grid-template-rows: 1fr;
}

.about-panel-inner {
  overflow: hidden;
  opacity: 0;
  transition: opacity 180ms ease;
}

.about-panel.is-open .about-panel-inner {
  opacity: 1;
}

/* About prose - Archivo 14px (section 6). Small top gap sits inside the animated
   track so it collapses with the panel. */
.about-prose {
  padding-top: var(--sp-12);
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--ink-body);
  max-width: 46ch;
}

/* --------------------------------------------------------------------------
   Index (rows 01 to 04)
   -------------------------------------------------------------------------- */

/* Type: INDEX / HOW IT WORKS labels - Mono 10px / 400 / normal / normal / UPPER */
.section-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  line-height: normal;
  text-transform: uppercase;
  color: var(--grey-label);
}

.index {
  margin-top: var(--sp-26);
}

/* Client feedback round 1 (2026-07-27): the hairline separators are retired in
   favour of clear space between rows (change 4). The list is a flex column with
   a gap from the spacing scale. */
.index-list {
  margin-top: var(--sp-12);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* A single index row (R1 rest state).
   Client feedback round 1 (change 1): rows are rounded-rectangle cards at
   --radius-surface (16px), no longer full-bleed hairline-separated rows. At
   rest the card is transparent with a transparent border, so activating a row
   (which sets the border colour and a raised-card shadow) causes no 1px content
   shift. Vertical padding is --sp-14 (change 4, "a touch more"), grown to
   --sp-18 on the active row (change 5). The transition covers padding so the
   grow/shrink pushes neighbours smoothly, plus background, border and shadow. */
.row {
  position: relative;
  display: grid;
  grid-template-columns: 26px 1fr auto;
  align-items: center;
  column-gap: var(--sp-16);
  width: 100%;
  padding: var(--sp-14) var(--sp-16);
  text-align: left;
  border: var(--border-width) solid transparent;
  border-radius: var(--radius-surface);
  background: transparent;
  transition: background 160ms ease, border-color 160ms ease,
              box-shadow 200ms ease, padding 200ms ease;
}

/* Client feedback round 10 (items 3 and 4): project rows (01 to 03) gain the CTA's
   cursor-following hover mist and its glassy rim. The mist is clipped to the row's
   16px radius, so the row needs overflow:hidden; isolation:isolate makes the row a
   stacking context so the mist's negative z-index is scoped to the row (above the
   card fill, below the text) instead of falling behind the page. The inset focus
   ring and the single outset card shadow are the row's OWN box-shadows, which an
   element's own overflow never clips, and the padding grow is internal, so neither
   is affected. The CTA keeps its own overflow:hidden and is excluded here. */
.row:not(.row--cta) {
  overflow: hidden;
  isolation: isolate;
}

/* Type: Row index 01 to 06 - Mono 11px / 400 / normal / normal / zero padded */
.row-index {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 400;
  line-height: normal;
  color: var(--grey-soft);
  transition: color 160ms ease;
}

/* Type: Row name - Archivo 15.5px / 500 / normal / normal / as typed.
   Client feedback round 1 (change 3): rest name colour moves from --ink-mid to
   --grey-label so unselected rows read as quiet grey (the reference look). This
   is below WCAG AA on --ground at desktop rest; the established remediation
   pattern darkens it to --ink-mid at the mobile breakpoint and under
   :focus-visible (see those rules), and hover/active darken it further to --ink.
   Recorded in PROGRESS.md as an extension of the accepted desktop-rest exception. */
.row-name {
  font-family: var(--font-sans);
  font-size: 15.5px;
  font-weight: 500;
  line-height: normal;
  color: var(--grey-label);
  transition: color 160ms ease;
}

/* End cell holds the niche tag and the (hidden at rest) expand glyph */
.row-end {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  min-width: 0;
}

/* Type: Niche tag - Mono 10px / 400 / normal / normal / UPPER */
.row-tag {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  line-height: normal;
  text-transform: uppercase;
  color: var(--grey-soft);
  transition: opacity 160ms ease;
}

/* Type: Expand glyph (rows/CTA) - system 14px / 400 / normal / 1 */
.row-glyph {
  position: absolute;
  right: 0;
  font-family: var(--font-system);
  font-size: 14px;
  font-weight: 400;
  line-height: 1;
  color: var(--accent);
  opacity: 0;              /* R1 rest: glyph hidden. Reveal is stage 2. */
  transform: translateX(-4px);
  pointer-events: none;
}

/* Focus-visible: accent inset ring, same treatment as field focus (CONCEPT 10) */
.row:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
}

/* Contrast (CONCEPT.md section 10): on focus-visible the 10px mono tags and the
   row indices are darkened from --grey-soft (#A6A6AE, 3.1:1, fails AA) to
   --ink-mid (#54545C, passes 4.5:1 on --ground). Client feedback round 1
   (change 3) extends the same guard to the row name, which now rests at
   --grey-label (also sub-AA on --ground): a keyboard-focused-at-rest row shows
   its name at --ink-mid. Declared before the hover and active colour rules below
   so an active or hovered row still wins (name to --ink), and this only bites
   the keyboard-focused-at-rest state. */
.row:focus-visible .row-index,
.row:focus-visible .row-tag,
.row:focus-visible .row-name {
  color: var(--ink-mid);
}

/* R2 hover (index rows 01 to 05). Motion: 160ms colour / 200ms shadow ease.
   Client feedback round 6 (owner-directed): the grey-wash hover is RETIRED. On
   hover a row now takes the same raised white card treatment as the active row
   (background --surface-white, a 1px --border rim via border-color, the single
   quiet --shadow-row-active), index to accent, name to --ink, tag faded out and
   glyph faded in. The one difference from the active row is the selection grow
   (padding --sp-14 to --sp-18): hover raises but does NOT grow, so hovering a
   row never pushes its neighbours, while the selected row is both raised AND
   grown. Recorded in PROGRESS.md as a supersession of round-1 change-2's neutral
   grey wash and of CONCEPT 3.3 "Row hover" / 3.4 R2.

   The whole hover block is gated behind (hover: hover) and (pointer: fine) so it
   never applies on touch. Without this, a tap leaves a sticky :hover on the
   tapped row and the tag-hide rule would beat the mobile "tag stays visible"
   rule, hiding the niche tag on the active row (CONCEPT section 8: tags stay
   visible on touch). This mirrors the JS isTouchDevice guard. */
@media (hover: hover) and (pointer: fine) {
  /* Client feedback round 10 (item 4): the hover card also takes the CTA's glassy
     rim language, --cta-rim (an inset white rim plus a brighter top-highlight),
     composed over the single outset card shadow so the row reads like glass the
     way the CTA pill does. Inset-only, so the two-radii / inset-except-the-one-card-
     shadow discipline holds. --cta-rim carries its own dark-theme values, so both
     themes work with no new tokens. No pastel drift is added to the rows. */
  .row:not(.row--cta):hover {
    background: var(--surface-white);
    border-color: var(--border);
    box-shadow: var(--cta-rim), var(--shadow-row-active);
  }

  .row:not(.row--cta):hover .row-index {
    color: var(--accent);
  }

  .row:not(.row--cta):hover .row-name {
    color: var(--ink);
  }

  .row:not(.row--cta):hover .row-tag {
    opacity: 0;
  }

  .row:not(.row--cta):hover .row-glyph {
    opacity: 1;
    transform: translateX(0);
  }

  /* Hovered AND keyboard-focused: layer the accent ring over the card elevation
     (the hover box-shadow, specificity 0,3,0, would otherwise clobber the
     .row:focus-visible ring at 0,2,0). Mirrors .row.is-active:focus-visible. */
  .row:not(.row--cta):hover:focus-visible {
    box-shadow: var(--shadow-field-focus), var(--cta-rim), var(--shadow-row-active);
  }
}

.row-glyph {
  transition: opacity 160ms ease, transform 160ms ease;
}

/* R3 active, and R4 active plus hover.
   Client feedback round 1 (change 2, reference look): the selected row is a
   raised white card, not the accent fill. Background --surface-white, a 1px
   --border rim (via border-color, the transparent rest border keeps geometry
   stable), and one restrained drop shadow --shadow-row-active (owner-directed
   supersession of the inset-only rule, scoped to this card only). The
   --accent-fill-active row treatment is retired (recorded in PROGRESS.md).
   Change 5 (selection grow-and-push): the active card grows its vertical
   padding from --sp-14 to --sp-18 (+4px), transitioned by the .row padding
   transition (~200ms ease) so neighbours slide as the layout reflows; under
   prefers-reduced-motion the global guard makes it instant. Index accent, name
   ink, tag hidden, glyph shown. Listed after :hover so the white card wins when
   a row is both active and hovered (R4). */
.row.is-active,
.row.is-active:hover {
  padding: var(--sp-18) var(--sp-16);
  background: var(--surface-white);
  border-color: var(--border);
  /* Round 10 (item 4): the active/open card carries the same glassy rim as hover. */
  box-shadow: var(--cta-rim), var(--shadow-row-active);
}

/* Keep the focus-visible accent inset ring exactly as before, even on the active
   card (change 2: "accessibility unchanged"). The new active-card box-shadow
   (0,2,0) would otherwise clobber the .row:focus-visible ring (0,2,0, earlier in
   source) on a focused active row. This 0,3,0 rule wins over both the active
   box-shadow and .row.is-active:hover, and layers the ring over the card
   elevation so a keyboard-focused active row shows the ring and stays raised. */
.row.is-active:focus-visible {
  box-shadow: var(--shadow-field-focus), var(--cta-rim), var(--shadow-row-active);
}

.row.is-active .row-index {
  color: var(--accent);
}

.row.is-active .row-name {
  color: var(--ink);
}

.row.is-active .row-tag {
  opacity: 0;
}

.row.is-active .row-glyph {
  opacity: 1;
  transform: translateX(0);
}

/* --------------------------------------------------------------------------
   CTA row 06 - pastel water pill (CONCEPT.md 3.1 tokens, 3.4 C1 to C5)
   -------------------------------------------------------------------------- */

/* C1 rest: three drifting pastel layers at --cta-bg-size, white inset rim,
   ink text, index at --cta-index. Fill drift 22s rest. overflow hidden clips
   the press bloom, which now follows the new 16px radius.
   Client feedback round 1 (change 1): the CTA becomes a 16px rounded rectangle
   (--radius-surface) to match the index cards, not a full pill. Only the radius
   and geometry change; the pastel drift fill, white rim, edge lift, hover mist
   and press bloom are all kept. The top margin is dropped so the .index-list
   flex gap governs the spacing before it uniformly with the other rows. The
   two-radii discipline holds: --radius-pill still serves the visit pill and the
   submit button. Recorded in PROGRESS.md. */
.row--cta {
  position: relative;
  overflow: hidden;
  /* Client feedback round 3 (item 4): the CTA inherited the base .row
     `border: 1px solid transparent`. With the default border-box background
     clip the pastel drift paints out to the border-box edge UNDER that
     transparent border, while the white --cta-rim (an inset shadow) is drawn
     inside the border, so the rim sat 1px in from the visible edge, leaving a
     1px pastel band all round and a radius-mismatch crescent at each rounded
     corner (confirmed at DPR4: outer row is pastel (205,221,242), the rim one
     CSS px inside). The transparent border served no purpose here (the CTA
     never gains a coloured border, unlike the index rows), so it is removed and
     the rim now hugs the true 16px edge. The removed 1px is added back to the
     padding via the --border-width token so the box geometry, and the item-1
     grow delta, are byte-identical (border-box: outer size unchanged; content
     inset unchanged). No hard-coded value, still one border width on the page. */
  border: 0;
  border-radius: var(--radius-surface);
  padding: calc(var(--sp-14) + var(--border-width)) calc(var(--sp-18) + var(--border-width));
  background-image: var(--cta-layer1), var(--cta-layer2), var(--cta-layer3);
  background-size: var(--cta-bg-size);
  background-repeat: no-repeat;
  box-shadow: var(--cta-rim);
  transition: filter 240ms ease, padding 200ms ease, box-shadow 200ms ease;
  animation: cta-drift 22s ease-in-out infinite;
}

/* Client feedback round 2 (item 1): when the form view (V4) is active the CTA
   grows exactly like an index row (vertical padding --sp-14 to --sp-18, the same
   ~200ms ease as the index-row grow, pushing its neighbour). It gains NO white
   card, border or accent styling: only the padding changes, so the pastel drift
   fill, white rim, edge lift, hover mist and press bloom are all untouched.
   A parallel `is-open` class (not `.is-active`) is used deliberately so none of
   the `.row.is-active` colour/border/shadow rules can match the CTA. This
   supersedes CONCEPT 3.4 C5 "no persistent selected styling" to the amended
   "no colour/selection styling, but it does grow while its view is active"
   (recorded in PROGRESS.md). Horizontal padding stays --sp-18 (the CTA rest
   value), so only the vertical grow reads. Reduced motion: the global guard
   forces the padding transition instant, matching the index-row grow. */
.row--cta.is-open {
  /* Grow padding matches the rest state's --border-width compensation (item 4)
     so the +4px vertical grow (item 1) is preserved exactly with the border
     removed. */
  padding: calc(var(--sp-18) + var(--border-width)) calc(var(--sp-18) + var(--border-width));
  /* Client feedback round 5 (item 3): when the form view is open the CTA takes
     the SAME quiet elevation as the active index-row card, composed with (not
     replacing) the existing white inset rim: rim first, then the drop shadow.
     Renders in both themes (--shadow-row-active is deepened under [data-theme=
     "dark"]). Recorded in PROGRESS.md as the C5 amendment update. */
  box-shadow: var(--cta-rim), var(--shadow-row-active);
}

/* Open AND keyboard-focused: keep the accent ring, and keep both the rim and the
   new active elevation. Without this, the 0,2,0 .row--cta:focus-visible rule later
   in source would replace the box-shadow with the ring alone (dropping the rim and
   the item-3 elevation). This 0,3,0 rule wins and layers all three (ring, rim,
   active shadow), mirroring the .row.is-active:focus-visible pattern. */
.row--cta.is-open:focus-visible {
  box-shadow: var(--shadow-field-focus), var(--cta-rim), var(--shadow-row-active);
}

/* Light pass (--cta-lightpass), 2D reverse path, 30s rest, on its own layer.
   Sized larger than the pill so the moving position reads as a light sweep. */
.row--cta::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: var(--cta-lightpass);
  background-size: 200% 200%;   /* no token for the light-pass size */
  background-repeat: no-repeat;
  pointer-events: none;
  animation: cta-lightpass 30s ease-in-out infinite;
}

/* Static edge lift vignette (--cta-edge-lift). */
.row--cta::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: var(--cta-edge-lift);
  pointer-events: none;
}

/* Keep the label content above every decorative layer. */
.row--cta > * {
  position: relative;
  z-index: 5;
}

/* C2 hover: drift quickens to 14s, light pass to 19s, saturate/brighten.
   Gated behind (hover: hover) and (pointer: fine) so a tap on touch does not
   leave a sticky saturate/brighten on the pill (which would read as persistent
   selected styling, against C5). Touch keeps only the sanctioned rest drift. */
@media (hover: hover) and (pointer: fine) {
  .row--cta:hover {
    filter: saturate(1.25) brightness(1.03);
    animation-duration: 14s;
  }

  .row--cta:hover::before {
    animation-duration: 19s;
  }
}

/* Cursor-following hover mist (CONCEPT.md 3.4 C2). ONE node, reused generically by
   the CTA pill and, from Client feedback round 10 (item 3), the project index rows.
   It follows the cursor with a 900ms lag (the lag lives in the CSS transition) and
   fades opacity 0 to 0.5 in 420ms. Fully round (--radius-pill), clipped to the host
   by the host's own overflow:hidden. Background is --cta-hover-mist, the same token
   in both themes (a pale sea-blue on light, a soft blue glow on dark), so it reads
   correctly on the CTA pastel pill AND on the white / dark row hover card. The
   per-element wiring is one JS function, wireHoverMist (main.js). */
.hover-mist {
  position: absolute;
  top: 0;
  left: 0;
  width: 200px;
  height: 200px;
  border-radius: var(--radius-pill);
  background: var(--cta-hover-mist);
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, -50%);
  transition: transform 900ms cubic-bezier(0.22, 0.61, 0.36, 1), opacity 420ms ease;
  will-change: transform, opacity;
}

/* CTA: the mist sits above the drift / light-pass layers but below the label
   (the label content is z-index 5 via .row--cta > *). This 0,2,0 rule beats that. */
.row--cta .hover-mist {
  z-index: 2;
}

/* Project rows: the mist sits just above the hover card fill but below the row
   text. isolation:isolate on the row (see the .row:not(.row--cta) rule) scopes this
   negative index to the row, so the mist cannot fall behind the page. */
.row:not(.row--cta) .hover-mist {
  z-index: -1;
}

/* Press bloom node (--cta-press-mist), one per press, scale 0.25 to 1. */
.cta-bloom {
  position: absolute;
  top: 0;
  left: 0;
  width: 260px;
  height: 260px;
  border-radius: var(--radius-pill);
  background: var(--cta-press-mist);
  opacity: 0;
  z-index: 3;
  pointer-events: none;
  will-change: transform, opacity;
}

/* CTA fill drift (layer 1 set): background-position, 2D four-point path. */
@keyframes cta-drift {
  0%   { background-position: 0% 0%,     100% 100%, 0% 50%; }
  25%  { background-position: 100% 0%,   0% 100%,   50% 0%; }
  50%  { background-position: 100% 100%, 0% 0%,     100% 50%; }
  75%  { background-position: 0% 100%,   100% 0%,   50% 100%; }
  100% { background-position: 0% 0%,     100% 100%, 0% 50%; }
}

/* CTA light pass (layer 2): background-position, 2D reverse path. */
@keyframes cta-lightpass {
  0%   { background-position: 100% 100%; }
  25%  { background-position: 0% 100%; }
  50%  { background-position: 0% 0%; }
  75%  { background-position: 100% 0%; }
  100% { background-position: 100% 100%; }
}

/* Type: Row name (CTA 06) - Archivo 15.5px / 600 / normal / normal / as typed */
.row--cta .row-name {
  font-weight: 600;
  color: var(--cta-text);
}

.row--cta .row-index {
  color: var(--cta-index);
}

/* Contrast (CONCEPT.md section 10, same remediation pattern approved for the
   grey tags): --cta-index (rgba(20,20,22,0.5)) on the pastel fill measures
   ~3.31:1, below AA. Desktop rest keeps --cta-index per the 3.1 token and the
   3.4 C1 state (documented desktop-rest exception, as with the grey-soft one);
   under :focus-visible the CTA index darkens to --ink-mid. Specificity 0,3,0
   plus later source order both beat .row--cta .row-index (0,2,0). The mobile
   at-rest remediation lives in the max-width:900px block below. */
.row--cta:focus-visible .row-index {
  color: var(--ink-mid);
}

.row--cta .row-tag {
  color: var(--ink-mid);
}

.row--cta:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
}

/* --------------------------------------------------------------------------
   Hidden testimonials slot (real markup and styles, no content yet)
   -------------------------------------------------------------------------- */

.testimonials {
  margin-top: var(--sp-22);
}

.testimonial-quote {
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 400;
  font-style: normal;
  line-height: 1.5;
  color: var(--ink-body);
}

.testimonial-meta {
  display: flex;
  gap: var(--sp-6);
  margin-top: var(--sp-6);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  text-transform: uppercase;
  color: var(--grey-label);
}

/* --------------------------------------------------------------------------
   Pricing static block (Client feedback round 8). Always visible on every
   breakpoint, between the index and HOW IT WORKS. The "PRICING" label matches the
   INDEX / HOW IT WORKS section labels; each line is a compact mono head (with the
   accent figure) plus an Archivo description matching the how-it-works type. The
   optional honest-floors note is omitted to keep the collapsed left column inside
   the 860 min-height (see the height-budget compaction note above). All tokens,
   so both themes work unchanged.
   -------------------------------------------------------------------------- */

.pricing {
  margin-top: var(--sp-26);
}

.pricing-list {
  margin-top: var(--sp-12);
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}

.pricing-line {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* Mono head - 10.5px UPPER, the "£" figure in accent. */
.pricing-head {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 400;
  letter-spacing: 0.04em;
  line-height: normal;
  text-transform: uppercase;
  color: var(--ink);
}

.pricing-head .amt {
  color: var(--accent);
}

/* Description - Archivo 13.5px, matching the how-it-works text. */
.pricing-desc {
  font-family: var(--font-sans);
  font-size: 13.5px;
  font-weight: 400;
  line-height: normal;
  color: var(--ink-body);
}

/* --------------------------------------------------------------------------
   How it works
   -------------------------------------------------------------------------- */

.how {
  margin-top: var(--sp-26);
}

.how-list {
  margin-top: var(--sp-12);
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}

.how-item {
  display: flex;
  align-items: baseline;
  gap: var(--sp-12);
}

/* Type: How-it-works index - Mono 10px / 400 / normal / normal / zero padded */
.how-index {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  line-height: normal;
  color: var(--grey-soft);
}

/* Type: How-it-works text - Archivo 13.5px / 400 / normal / normal / sentence */
.how-text {
  font-family: var(--font-sans);
  font-size: 13.5px;
  font-weight: 400;
  line-height: normal;
  color: var(--ink-body);
}

/* --------------------------------------------------------------------------
   Proof strip - pinned to the bottom of the left column
   -------------------------------------------------------------------------- */

/* Type: Proof strip - Mono 10.5px / 400 / normal / 1.7 / UPPER */
.proof {
  margin-top: auto;
  padding-top: var(--sp-22);
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 400;
  line-height: 1.7;
  text-transform: uppercase;
  color: var(--grey-label);
}

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */

.footer {
  margin-top: var(--sp-12);
}

/* Type: Footer email line - Archivo 13px / 400 / normal / normal / sentence */
.footer-email {
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 400;
  line-height: normal;
  color: var(--ink-body);
}

/* The mailto sits inside the footer sentence, so colour alone does not
   distinguish it (stage 9 final-verify FAIL 2, link-in-text-block). Add a quiet
   underline at rest, keeping the accent colour. text-underline-offset is set from
   the spacing scale (--sp-2, 2px) so the underline clears the descenders in the
   address (the "p" in the placeholder) rather than cutting through them. */
.footer-email a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: var(--sp-2);
}

.footer-email a:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
}

/* Copy email button (Client feedback round 8). A small quiet mono control beside
   the mailto: clicking copies the address and flips the label to "Copied" for
   ~1.5s (handled in main.js; instant flip, no animation loop). Works in both
   themes via tokens; the label change is announced through aria-live. */
.copy-email {
  padding: 0 var(--sp-4);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.04em;
  line-height: normal;
  text-transform: uppercase;
  color: var(--grey-label);
  transition: color 150ms ease;
}

@media (hover: hover) and (pointer: fine) {
  .copy-email:hover {
    color: var(--ink);
  }
}

.copy-email:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
  border-radius: var(--radius-surface);
}

.footer-copy {
  margin-top: var(--sp-6);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  text-transform: uppercase;
  color: var(--grey-soft);
}

/* --------------------------------------------------------------------------
   Theme toggle (Client feedback round 5). A small segmented control in the
   footer line: the email on the left, the toggle on the right, same row
   (reference: the Moritz Petersen monitor / sun / moon segmented control). On
   mobile the same footer flows at the page bottom, so the row layout carries
   over unchanged. Three buttons with aria-pressed (the group is a role="group";
   exactly one is pressed at a time), quiet --grey-label icons, the active
   segment lifted with the established active treatment (white/dark card surface,
   ink icon, the single quiet --shadow-row-active). 16px radius family.
   -------------------------------------------------------------------------- */

.footer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-16);
}

/* The email line takes the remaining width and may wrap; the toggle never shrinks. */
.footer-row .footer-email {
  min-width: 0;
}

.theme-toggle {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-surface);
  background: var(--surface-field);
}

.theme-opt {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-6);
  border-radius: var(--radius-surface);
  color: var(--grey-label);
  background: transparent;
  transition: color 150ms ease, background 150ms ease, box-shadow 150ms ease;
}

.theme-opt svg {
  display: block;
  width: 15px;
  height: 15px;
}

/* Active segment: the established active-card treatment (lifted surface, ink
   icon, the single quiet shadow). */
.theme-opt[aria-pressed="true"] {
  color: var(--ink);
  background: var(--surface-white);
  box-shadow: var(--shadow-row-active);
}

/* House-style focus ring; layered over the active elevation when the active
   segment is the focused one (mirrors the .row.is-active:focus-visible pattern). */
.theme-opt:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
}

.theme-opt[aria-pressed="true"]:focus-visible {
  box-shadow: var(--shadow-field-focus), var(--shadow-row-active);
}

@media (hover: hover) and (pointer: fine) {
  .theme-opt:not([aria-pressed="true"]):hover {
    color: var(--ink);
  }
}

/* --------------------------------------------------------------------------
   Right column - preview panel (V1 welcome, shown statically)
   -------------------------------------------------------------------------- */

.col-right {
  min-height: 0;
}

.panel {
  height: 100%;
  display: flex;
  flex-direction: column;
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-surface);
  overflow: hidden;
  background: var(--surface-preview);
}

.panel-head {
  flex: 0 0 auto;
  padding: var(--sp-16) var(--sp-22);
  border-bottom: var(--border-width) solid var(--border);
  background: var(--surface-panelhead);
}

/* Type: Panel header label - Mono 10.5px / 400 / normal / normal / UPPER */
.panel-head-label {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 400;
  line-height: normal;
  text-transform: uppercase;
  color: var(--grey-label);
}

/* Panel body: 1fr by 1fr grid so every state centres */
.panel-body {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  place-items: center;
}

/* V1 welcome, no selection */
.welcome {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
}

/* Type: Expand glyph (welcome) - system 340px / 400 / normal / 0.8
   Client feedback round 2 (item 6): optically centre the "SELECT A PROJECT" line
   on the giant ghost arrow's visual mass. Measured (canvas TextMetrics + a
   boosted-colour screenshot pixel scan, Segoe UI at 340px): the arrow ink is
   horizontally centred in its advance (dX about 0), but with line-height 0.8 the
   ink mass sits about 43px BELOW the line-box centre where the text is. (The
   brief's "up-right" holds for the em square, not for the actual line-height-0.8
   layout, so only a vertical offset is applied; reported in PROGRESS.md.) The
   glyph is nudged UP by 43px so its mass centres on the text, which stays at the
   true panel centre. Mobile (160px) uses the proportional 20px (43 * 160/340). */
.welcome-glyph {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, calc(-50% - 43px));
  font-family: var(--font-system);
  font-size: 340px;
  font-weight: 400;
  line-height: 0.8;
  color: var(--accent-ghost);
  user-select: none;
  pointer-events: none;
}

/* Type: Welcome line - Mono 11px / 400 / 0.1em / normal / UPPER */
.welcome-line {
  position: relative;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.1em;
  line-height: normal;
  text-transform: uppercase;
  color: var(--grey-label);
}

/* No-JS contact fallback (deviation 5c). Shown only when scripting is off: while
   JS runs the <noscript> renders nothing (0x0, contents are raw text), so this
   never shows to a normal visitor. When JS is off the panel body still holds the
   decorative welcome (centred in the grid cell), so the noscript is aligned to
   the bottom of the same cell to sit clear of it rather than overlapping. */
.panel-body > noscript {
  align-self: end;
}

/* Set in the site's prose type with existing tokens. */
.noscript-contact {
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 400;
  line-height: 1.6;
  text-align: center;
  color: var(--ink-body);
  max-width: 34ch;
  padding: var(--sp-32);
}

/* Same link-in-text-block fix as the footer mailto (stage 9 FAIL 2): this
   mailto sits inside the no-JS sentence, so it is underlined at rest too. */
.noscript-contact a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: var(--sp-2);
}

/* --------------------------------------------------------------------------
   Panel views (CONCEPT.md 3.4 V2 to V5, section 5, section 6)

   The panel body holds exactly one view at a time. V1 welcome is in the markup;
   V2/V3/V4 are cloned from <template>s by main.js on selection. Every view
   fills the 1fr by 1fr panel body and is centred by it.
   -------------------------------------------------------------------------- */

/* Swap animation (V5). Body fades and lifts 6px in 130ms, content switches at
   the midpoint, reverse 130ms back in (260ms total, driven by main.js toggling
   .is-swapping). Under reduced motion the global rule below forces this instant;
   main.js also skips the timing and swaps content immediately. */
.panel-body {
  transition: opacity 130ms ease, transform 130ms ease;
}

.panel-body.is-swapping {
  opacity: 0;
  transform: translateY(6px);
}

/* A view fills the body; width is capped for readable prose and framing. */
.view {
  width: 100%;
  max-width: 560px;
  height: 100%;
  margin: 0 auto;
  padding: var(--sp-32);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-18);
}

/* Quiet secondary pill (ink outline), NOT the CTA treatment. */
.visit-pill {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-6);
  padding: var(--sp-10) var(--sp-16);
  border: var(--border-width) solid var(--ink);
  /* Client feedback round 2 (item 2, owner judgement call flagged for veto):
     the "Visit live site" pill moves from --radius-pill (999px) to
     --radius-surface (16px) for consistency with the new button language
     (the submit button, item 2). Recorded in PROGRESS.md. */
  border-radius: var(--radius-surface);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
  transition: background 150ms ease, color 150ms ease;
}

.visit-pill:hover {
  background: var(--ink);
  color: var(--surface-white);
}

.visit-pill:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
}

/* ---- V4 contact form view -------------------------------------------- */

.view--form {
  align-items: center;
  justify-content: center;
}

/* White card, 16px radius, 440px wide, centred (CONCEPT 3.4 V4).
   position: relative anchors the success/failure overlay (see .form-status). */
.contact-card {
  position: relative;
  width: 440px;
  max-width: 100%;
  background: var(--surface-white);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-surface);
}

/* The interactive part of the card. Holds the layout (flex column, gap,
   padding) so its size defines the card; the status overlay fills that box
   without changing it, so success/failure never move the panel (section 7). */
.form-fields {
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
  padding: var(--sp-32);
}

/* Type: Form label - Mono 10px / 400 / UPPER */
.form-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  line-height: normal;
  text-transform: uppercase;
  color: var(--grey-label);
}

.field-group {
  display: flex;
  flex-direction: column;
  gap: var(--sp-12);
}

/* Type: Form inputs/textarea - Archivo 14px / 400 / sentence.
   F1 rest: --surface-field, 1px --border. */
.field {
  width: 100%;
  padding: 14px var(--sp-18);
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 400;
  line-height: normal;
  color: var(--ink);
  background: var(--surface-field);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-surface);
  transition: background 150ms ease, border-color 150ms ease, box-shadow 150ms ease;
}

/* Contrast (CONCEPT.md section 10 AA mandate): the placeholder is the field's
   only visible label, and --grey-placeholder (#9A9AA2) on --surface-field
   measures ~2.68:1, below AA. Darkened to --ink-mid (#54545C, ~7.2:1). The
   --grey-placeholder token is left in the 3.1 block verbatim but is now unused.
   Fields also carry aria-label so AT users do not depend on placeholder text. */
.field::placeholder {
  color: var(--ink-mid);
}

/* F2 focus: accent border, white background, inset accent ring, no outer halo. */
.field:focus {
  outline: none;
  background: var(--surface-white);
  border-color: var(--accent);
  box-shadow: var(--shadow-field-focus);
}

/* F3 filled: as rest with ink text (already --ink; kept explicit for clarity). */
.field:not(:placeholder-shown) {
  color: var(--ink);
}

.field--area {
  resize: none;
}

/* Honeypot: off-screen, out of the tab order, hidden from assistive tech. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Type: Submit button - Archivo 14px / 600. F4 rest: ink pill, white label,
   no border, no shadow. F5 hover: pure black in 150ms. F6 no press state. */
.submit {
  padding: 15px var(--sp-26);
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 600;
  line-height: normal;
  color: var(--surface-white);
  background: var(--submit-ink);
  border: 0;
  /* Client feedback round 2 (item 2): the submit button moves from
     --radius-pill (999px) to --radius-surface (16px), the new button language.
     The two-radii rule stands; --radius-pill still serves the hover mist (CTA and
     rows) and the CTA press bloom (both fully-round nodes), so it is NOT
     defined-but-unused. Recorded in PROGRESS.md. */
  border-radius: var(--radius-surface);
  transition: background 150ms ease;
}

.submit:hover {
  background: var(--submit-ink-hover);
}

.submit:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
}

/* In flight: disabled to prevent double sends. Static label change to
   "Sending" (no looping spinner; only the CTA drift is sanctioned). */
.submit:disabled {
  cursor: default;
  opacity: 0.7;
}

.submit:disabled:hover {
  background: var(--submit-ink);   /* no hover shift while disabled */
}

/* ---- V4 form: success / failure state (section 7) --------------------- */

/* Overlays the fields, filling the card so its dimensions stay fixed between
   states (the panel never reflows). Hidden at rest; revealed by main.js. */
.form-status {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-16);
  padding: var(--sp-32);
  text-align: center;
  background: var(--surface-white);
  border-radius: var(--radius-surface);
}

.form-status.is-shown {
  display: flex;
}

/* Focused programmatically (tabindex -1) so a keyboard user is not stranded;
   no ring needed on a non-interactive message container. */
.form-status:focus {
  outline: none;
}

/* Outcome message. Archivo 14px body, same size as the form inputs. */
.form-status-msg {
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--ink-body);
  max-width: 34ch;
}

/* Same link-in-text-block fix as the footer mailto (stage 9 FAIL 2): the failure
   mailto sits inside the sentence "Something broke. Email me directly at ...", so
   it is underlined at rest too. */
.form-status-msg a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: var(--sp-2);
}

.form-status-msg a:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
}

/* Quiet "try again" control on the failure overlay: a plain mono text button
   (not the CTA or submit treatment), tokens throughout. Returns to the intact
   form so the typed message can be copied or resent. */
.form-retry {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.04em;
  line-height: normal;
  text-transform: uppercase;
  color: var(--accent);
  background: none;
  border: 0;
  padding: var(--sp-4);
}

.form-retry:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
}

/* Visually hidden aria-live region for panel-change announcements. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   Project detail state (Client feedback round 6). Owner-directed structural
   redesign of the project viewing pattern, adapted from the Moritz Petersen
   detail-state pattern but NOT a copy: we keep the two-column structure, dot
   grid, light ground, tokens, type and motion language.

   When a project row (01 to 03) is chosen, the left column morphs: the index,
   HOW IT WORKS and the proof strip fade out and collapse, a project header block
   (.detail) fades in near the top where the index was, and a conversion cluster
   (.conversion) pins to the bottom. The panel (right) shows a scrollable stack
   of section cards (.section-stack). The state hook is .page.is-detail so it can
   drive BOTH columns (they are display: contents on mobile). About / Pricing /
   form (04 to 06) keep the normal index model unchanged. This SUPERSEDES the V2
   single-video project layout and parts of CONCEPT 3.4's state inventory
   (recorded precisely in PROGRESS.md).

   Motion: the crossfade is driven by main.js (leaving set fades to 0, then the
   entering set fades from 0 to 1), ~200ms ease each phase via inline styles so it
   never conflicts with the load-in .fade-block transition. Under reduced motion
   main.js toggles visibility instantly (no inline transition set).
   -------------------------------------------------------------------------- */

/* The detail header and conversion cluster are hidden in the normal model. They
   use the `hidden` attribute (display: none) at rest; main.js removes it to show
   them. Kept out of flow entirely when hidden so the normal index layout, and
   its measured min-height 860 fit, are untouched. */
.detail[hidden],
.conversion[hidden] {
  display: none;
}

/* Detail header block: sits where the index was (same top margin), flex column. */
.detail {
  margin-top: var(--sp-48);
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
}

/* Back control: a quiet mono button with a left-pointing arrow, same glyph
   treatment family as the row expand glyph. UPPER via text-transform to match
   the other mono labels. Aligned to the start; a self-start width so only the
   text is the hit target, not the whole column. */
.detail-back {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-6);
  padding: var(--sp-4) 0;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.04em;
  line-height: normal;
  text-transform: uppercase;
  color: var(--ink-mid);
  transition: color 150ms ease;
}

.detail-back-glyph {
  font-family: var(--font-system);
  font-size: 13px;
  line-height: 1;
  color: var(--accent);
}

@media (hover: hover) and (pointer: fine) {
  .detail-back:hover {
    color: var(--ink);
  }
}

.detail-back:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
  border-radius: var(--radius-surface);
}

/* Project chips (Client feedback round 7). The two OTHER projects sit here as
   small chips directly under the back control; clicking one switches the detail
   in place. A quiet chip at rest (16px radius, 1px --border, --surface-field
   fill, niche-tag type: mono 10px UPPER, --grey-label). Real buttons, so they
   carry focus-visible; the accent inset ring matches the fields and rows. The
   chips row wraps if the two labels do not fit (mobile). Both themes work with
   existing tokens (dark: field #202024, border #33333A, label #74747C, and the
   hover card lifts to --surface-white #242428). */
.detail-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-6);
}

/* No chips (welcome / instant states): collapse so the .detail gap does not
   leave a phantom row. */
.detail-chips:empty {
  display: none;
}

.chip {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-6) var(--sp-12);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-surface);
  background: var(--surface-field);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: normal;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--grey-label);
  transition: background 160ms ease, border-color 160ms ease,
              box-shadow 200ms ease, color 160ms ease;
}

/* Hover adopts the card treatment scaled down (mouse only, mirroring the row
   hover guard so a tap leaves no sticky hover): white fill and the single quiet
   card shadow, label to --ink. */
@media (hover: hover) and (pointer: fine) {
  .chip:hover {
    background: var(--surface-white);
    border-color: var(--border);
    box-shadow: var(--shadow-row-active);
    color: var(--ink);
  }
}

.chip:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
}

/* FLIP travel clones (Client feedback round 7). Clones fly a title or chip label
   between its list and detail positions and are removed as soon as the real
   element is revealed. They live inside .flip-layer, a fixed viewport-sized,
   overflow-clipped, pointer-inert overlay: a clone edge that briefly crosses the
   viewport during the travel is clipped rather than adding a horizontal scrollbar
   on mobile (the clone is absolutely positioned, so the layer's overflow:hidden
   clips it). The layer never affects layout or hit-testing. Not spawned at all
   under reduced motion (main.js guards). The clone borrows the .detail-title or
   .chip type so the travelling text matches its destination. */
.flip-layer {
  position: fixed;
  inset: 0;
  z-index: 50;
  overflow: hidden;
  pointer-events: none;
}
.flip-clone {
  position: absolute;
  margin: 0;
  pointer-events: none;
  transform-origin: top left;
}

/* Per-project copy, cloned in from the project template. */
.detail-copy {
  display: flex;
  flex-direction: column;
  gap: var(--sp-12);
}

/* Type: project title - Archivo 34px / 600 / -0.015em / as typed (as the panel
   project title was). */
.detail-title {
  font-family: var(--font-sans);
  font-size: 34px;
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: normal;
  color: var(--ink);
}

/* Type: sub line - Mono 10.5px / 400 / lower (as the panel sub line was). */
.detail-sub {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 400;
  line-height: normal;
  text-transform: lowercase;
  color: var(--ink-mid);
}

/* Approved blurb paragraph - Archivo 14px / --ink-body. */
.detail-blurb {
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--ink-body);
  max-width: 42ch;
}

/* "See it live" reuses the quiet secondary .visit-pill styling; a small top gap
   separates it from the blurb. */
.detail-live {
  margin-top: var(--sp-4);
}

/* Conversion cluster: pinned to the bottom of the left column (margin-top auto,
   where the proof strip pins in the normal model). Holds the quiet pricing line
   (a button that opens the Pricing view) and, directly beneath it, the Get in
   touch CTA row 06 (moved here by main.js). */
.conversion {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-12);
  padding-top: var(--sp-32);
}

/* The pricing line is a quiet static line: mono 10.5px lower case, --ink-mid
   (Client feedback round 8: no longer a button, since there is no Pricing view;
   pricing is the always-on block in the left column). Same mono line as before. */
.conversion-price {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 400;
  line-height: 1.6;
  text-transform: lowercase;
  color: var(--ink-mid);
}

/* --------------------------------------------------------------------------
   Panel in the detail state: a scrollable stack of section cards (round 6).
   Replaces the single tour video. The stack fills the panel body (overriding the
   grid place-items: center for this child) and scrolls internally on desktop; a
   card peeking below the fold is its own "more below" cue, so the old bottom fade
   is retired. The scrollbar is left native (a thin scrollbar is an honest scroll
   affordance; reported in PROGRESS.md).
   -------------------------------------------------------------------------- */

.section-stack {
  align-self: stretch;
  justify-self: stretch;
  width: 100%;
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
  padding: var(--sp-22);
}

/* Keyboard-focusable scroll region (verifier round-6 FAIL 2): the stack is a Tab
   stop (tabindex 0, role region, per-project aria-label) so keyboard users can
   focus it and scroll with PageDown / arrows. Focus-visible shows the house
   accent inset ring, hugging the panel edge (same --shadow-field-focus treatment
   as fields and rows). The ring is inset so it is not clipped by the panel's
   overflow and stays put while the content scrolls. */
.section-stack:focus-visible {
  outline: none;
  box-shadow: var(--shadow-field-focus);
}

/* One section card: a full content-width media block at a fixed 16:9 aspect,
   16px radius, 1px border, with a small mono caption underneath. */
.section-card {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
  margin: 0;
}

.section-media {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-surface);
  overflow: hidden;
  background: var(--surface-preview);
}

.section-img,
.section-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--surface-preview);
}

/* Type: Caption bar - Mono 10.5px / 400 / 0.04em / UPPER (CONCEPT 3.2). */
.section-caption {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 400;
  letter-spacing: 0.04em;
  line-height: normal;
  text-transform: uppercase;
  color: var(--grey-label);
}

/* --------------------------------------------------------------------------
   Row pointer trails: REMOVED (Client feedback round 10, item 2). The owner read
   the hover trails as stray whitish particles (worst in dark theme) and asked for
   them gone: the spawn code, listeners and the .trail-dot rule are deleted. The
   --trail-a / --trail-b / --trail-opacity tokens stay in the verbatim 3.1 block
   above but are now unused (noted in PROGRESS.md as a supersession of CONCEPT 3.3
   "Row pointer trail"). The cursor-following mist the CTA uses now also serves the
   rows (item 3), so the hover still has motion, just not the trail particles.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   Intermediate two-column widths (Client feedback round 10, item 1). Between the
   900px single-column breakpoint and about 1250px the base desktop grid (460px
   left column + 80px gap + 76px side padding) starves the 1fr preview panel: at
   1000px the panel resolves to only about 304px and the welcome glyph overlaps
   the SELECT A PROJECT line. In this band the left column, the gap and the page
   padding step down from the spacing scale so the panel always keeps a workable
   share (about 430px from a 950px viewport up), and the welcome glyph plus its
   optical-centre offset (round 2, item 6) scale down proportionally so the giant
   arrow never overlaps the welcome line. Bounded at min-width 901px so it never
   touches the single-column layout, and capped at 1250px so 1251px and up (1440
   included) stay pixel-identical to today.
   -------------------------------------------------------------------------- */

@media (min-width: 901px) and (max-width: 1250px) {
  .page {
    grid-template-columns: 400px 1fr;
    /* gap and side padding are --sp-36 (not the ~48 the brief floated): with a
       classic 15px scrollbar reserved, the wider values leave the panel at ~415px
       at a 950px viewport, just under the ~420 floor; --sp-36 clears ~425px there
       and grows from about 475px at 1000px up. Reported in PROGRESS.md. */
    gap: var(--sp-36);
    padding: var(--sp-36) var(--sp-36) var(--sp-48);
  }

  /* Glyph scales from 340px to 240px; the optical-centre offset scales with it
     (43 * 240/340 is about 30px up), so the ink mass still centres on the text. */
  .welcome-glyph {
    font-size: 240px;
    transform: translate(-50%, calc(-50% - 30px));
  }
}

/* --------------------------------------------------------------------------
   Mobile pass (CONCEPT.md section 8, 10). Below 900px the two-column grid
   collapses to a single column in the section-8 order: name block, positioning,
   INDEX, panel, HOW IT WORKS, proof strip, footer.

   The panel lives in the markup inside .col-right (a grid sibling of .col-left),
   so to interleave it between the INDEX and HOW blocks, both columns are set to
   display: contents and every one of the eight flattened children carries an
   explicit `order`. Explicit orders on all children are required: an unordered
   flex item defaults to order 0 and would jump to the top (this also keeps the
   hidden testimonials slot under the INDEX for when it is later enabled).

   The page height is released here (height auto, min-height 0) so the desktop
   860px min-height never forces a tall phone page, and nothing critical relies
   on 100vh: the page simply flows.
   -------------------------------------------------------------------------- */

@media (max-width: 900px) {
  .page {
    display: flex;
    flex-direction: column;
    height: auto;
    min-height: 0;
    gap: 0;
    padding: var(--sp-48);
  }

  /* Flatten both columns so their children share one flex flow. */
  .col-left,
  .col-right {
    display: contents;
  }

  /* Section-8 single-column order (Client feedback round 8 order: name block,
     positioning + About expander, INDEX (4 rows), panel, PRICING block, HOW IT
     WORKS, proof, footer). Every flattened child is ordered (an unordered flex
     item defaults to order 0 and would jump to the top). In the detail state
     (round 6) the index / pricing / how / proof are hidden by main.js, and
     .detail (the project header) takes the index's slot while .conversion takes
     the proof's slot, so the detail-state order reads: name, positioning, About,
     detail header, panel (section cards), conversion cluster, footer. */
  .name-block  { order: 1; }
  .positioning { order: 2; }
  .about       { order: 3; }    /* masthead expander, always visible */
  .index       { order: 4; }
  .detail      { order: 4; }    /* takes the index slot in the detail state */
  .testimonials { order: 5; }   /* hidden; kept under INDEX for later */
  .panel       { order: 6; }
  .pricing     { order: 7; }    /* between the panel and HOW IT WORKS */
  .how         { order: 8; }
  .proof       { order: 9; }
  .conversion  { order: 9; }    /* takes the proof slot in the detail state */
  .footer      { order: 10; }

  /* Detail header spacing steps down on mobile (no 48px gap needed above it in
     the flowing single column). */
  .detail {
    margin-top: var(--sp-32);
  }

  /* Mobile amendment for the detail state ONLY (round 6, recorded in PROGRESS.md
     as a deliberate supersession of the no-reflow / fixed 60vh constraint FOR THE
     DETAIL STATE): the section cards flow as page content and the page scrolls,
     so the panel is allowed to grow past 60vh. The 60vh budget still governs the
     normal index-model views (V1 welcome, About, Pricing, form). */
  .page.is-detail .panel {
    min-height: 0;
  }
  /* Client feedback round 10 (item 5): on phones the stack previously ran padding:0
     in the detail state, so the mono section captions (THE PRICE MENU etc.) touched
     the panel's left edge and the last one clipped against the panel bottom. Give
     the stack a horizontal inset (so the media AND its caption sit a comfortable
     --sp-16 in from the edge, still aligned to each other) and a --sp-32 bottom
     inset so the last caption clears the panel end and the conversion cluster. */
  .page.is-detail .section-stack {
    height: auto;
    overflow: visible;
    overscroll-behavior: auto;
    padding: 0 var(--sp-16) var(--sp-32);
  }

  /* Proof strip is no longer pinned with margin-top auto (a desktop flex
     behaviour); in single column it just flows. */
  .proof {
    margin-top: var(--sp-48);
  }

  /* Panel sits directly under the INDEX, flows to its content height, and
     reserves about 60vh so V1 to V4 do not reflow the page as they swap. */
  .panel {
    height: auto;
    min-height: 60vh;
    margin-top: var(--sp-32);
  }

  /* Disable scroll anchoring while the single-column layout is active. The panel
     view swap (replaceChildren on .panel-body) otherwise triggered the browser's
     scroll-anchoring adjustment and nudged the page scroll by about 13px, which
     pushed the panel top just past the "already placed" band and caused a
     redundant re-scroll on an immediate second selection. With anchoring off the
     panel stays pinned where scrollPanelIntoView put it. */
  body {
    overflow-anchor: none;
  }

  /* Welcome glyph scales down to about 160px (section 8). Optical-centre offset
     (item 6) scales with it: 43 * 160/340 is about 20px up. */
  .welcome-glyph {
    font-size: 160px;
    transform: translate(-50%, calc(-50% - 20px));
  }

  /* The panel body centres its single view with place-items: center, which on
     the desktop's wide panel is right. On a narrow phone that shrink-wraps the
     view to its content, so the 440px form card's max-width: 100% would resolve
     against an oversized box and overflow (clipped by the panel). Two changes
     fix it: minmax(0, 1fr) lets the single column shrink below its content's
     min size (a plain 1fr keeps an automatic content minimum, so the 440px card
     blows the track out past the panel), and justify-items: stretch fills the
     view to that column so max-width: 100% inside it (the card, the prose) is
     measured against the real, narrow panel. */
  .panel-body {
    grid-template-columns: minmax(0, 1fr);
    justify-items: stretch;
  }
  .view {
    max-width: 100%;
    gap: var(--sp-14);   /* tighter vertical rhythm so V2 fits the 60vh budget */
  }

  .form-fields {
    padding: var(--sp-22);
  }

  /* Contrast (section 10): at rest on --ground the 10px mono tags and the row
     indices are --grey-soft (fails AA); darken them to --ink-mid, which passes
     4.5:1. Client feedback round 1 (change 3): the row name now rests at
     --grey-label (also sub-AA), so it is darkened here too, the same
     mobile-at-rest remediation. Desktop rest colours are untouched (this is
     inside the breakpoint). */
  .row-index,
  .row-tag,
  .row-name {
    color: var(--ink-mid);
  }

  /* CTA row index (section 10, FLAG 5): --cta-index (~3.31:1 on the pastel
     fill) fails AA. On mobile at rest darken it to --ink-mid. The plain
     .row-index rule above is 0,1,0 and loses to .row--cta .row-index (0,2,0),
     so this equal-specificity (0,2,0) rule, later in source, is required to
     win. */
  .row--cta .row-index {
    color: var(--ink-mid);
  }

  /* Contrast (section 10, stage 9 final-verify FAIL 1): the five 10 to 10.5px
     mono labels sit at --grey-label or --grey-soft on --ground, all below AA
     (measured 2.31 to 3.11:1). Darken them to --ink-mid (7.19:1) on mobile at
     rest, the same remediation pattern as the row tags and the CTA index above.
     Desktop rest keeps the prototype --grey-label / --grey-soft (a documented
     desktop-rest exception, same accepted class as the row tags and CTA index).
     These are non-interactive labels, so there is no focus state to darken. */
  .section-label,
  .how-index,
  .proof,
  .footer-copy,
  .panel-head-label {
    color: var(--ink-mid);
  }

  /* Same mobile-at-rest remediation for the two new interactive mono controls
     (round 8): the About "More" toggle and the footer "Copy" button both rest at
     --grey-label (sub-AA on --ground). Darken their rest colour to --ink-mid on
     mobile; the focus-visible ring is unaffected. */
  .about-toggle,
  .copy-email {
    color: var(--ink-mid);
  }

  /* Touch: all hover-revealed information must be visible at rest (section 8).
     The niche tag stays visible in every row state, and the expand glyph is
     always shown. The glyph moves from an absolute overlay into normal flow so
     it sits beside the tag rather than on top of it. */
  .row-end {
    column-gap: var(--sp-10);
  }
  .row-glyph {
    position: static;
    opacity: 1;
    transform: none;
  }
  .row.is-active .row-tag {
    opacity: 1;   /* beats the desktop active rule (tag hidden) via source order */
  }
}

/* Narrow phones: step the padding down again and tighten the panel view padding
   so the 440px form card (max-width 100%) fits at 360px with no horizontal
   scroll. Values from the spacing scale. */
@media (max-width: 480px) {
  .page {
    padding: var(--sp-22);
  }
  .view {
    padding: var(--sp-22);
  }
}

/* --------------------------------------------------------------------------
   Reduced motion - global guard (JS guards live in main.js)
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* Load-in shows instantly, no transform or opacity transition. */
  .fade-block {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  /* CTA drift and light pass: animation none with a static gradient. */
  .row--cta,
  .row--cta::before {
    animation: none !important;
    background-position: 0% 0% !important;
  }
}
