/* Global components and actions (REDESIGN-PLAN.md §6.1, §6.2, §15).
   Colours come from the semantic layer, so every component works on every field.
   State classes is-hover, is-focus, is-active and is-loading force a state for the
   kitchen sink; pages never set them by hand. */

@layer components {
  /* ---- Skip link ----------------------------------------------------------------- */
  .c-skip {
    position: absolute;
    inset-inline-start: var(--s4);
    inset-block-start: calc(-1 * var(--s9));
    z-index: 40;
    display: inline-flex;
    align-items: center;
    min-block-size: var(--target);
    padding-inline: var(--s5);
    border-radius: var(--r-pill);
    background: var(--action);
    color: var(--on-action);
    font-size: var(--t-small);
    font-weight: calc(600 + var(--w-adjust));
    text-decoration: none;
  }
  .c-skip:is(:focus, .is-focus) {
    inset-block-start: var(--s4);
    outline: var(--focus-width) solid var(--focus);
    outline-offset: var(--focus-offset);
    border-radius: var(--r-pill);
  }

  /* ---- Header (§6.1) ------------------------------------------------------------------
     Sticky; 64px on desktop. Below 761px the name takes its own 48px row and the nav a
     56px row under it. The header sticks with a negative top, so the name scrolls away and
     only the 56px nav row stays (= --header-h, which the scroll padding uses). On phones
     header.js also slides the whole header up while scrolling down and back on scroll up
     (C9; motion, so only without reduced motion). Layout follows
     the header's own width, so the kitchen sink can show it at any width.

     Frosted ground (C42, the viewer's approach from C21): the header itself is clear. Behind
     its content, ::before blurs the page and ::after lays the ground over it at 90%, with the
     line once scrolled. The blur is masked: full strength down the top half, then fading out
     a little past the bottom edge (--header-tail), so text sliding under the header softens
     before it disappears. Nothing here moves on scroll but the page behind; C9's tuck moves
     the whole header with transform, pseudo-elements included. Solid ground where
     transparency is reduced, colours are forced, or backdrop blur is missing. */
  .c-header {
    --header-tint: var(--ground);
    --header-tail: var(--s4);
    container: c-header / inline-size;
    position: sticky;
    inset-block-start: 0;
    z-index: 30;
    background-color: transparent;   /* a field class on the header tints ::after instead */
    color: var(--fg);
  }
  .c-header::before,
  .c-header::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;          /* under the name and links, over nothing of the header's own */
    pointer-events: none;
  }
  .c-header::before {
    inset-block-end: calc(-1 * var(--header-tail));
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    -webkit-mask-image: linear-gradient(to bottom, var(--c-ink) 50%, transparent);
    mask-image: linear-gradient(to bottom, var(--c-ink) 50%, transparent);
  }
  .c-header::after {
    background-color: color-mix(in srgb, var(--header-tint) 90%, transparent);
    transition: background-color var(--d-fast) var(--ease-out), box-shadow var(--d-fast) var(--ease-out);
  }
  .c-header.is-scrolled { --header-tint: var(--surface); }
  .c-header.is-scrolled::after { box-shadow: inset 0 calc(-1 * var(--ring)) 0 var(--line); }
  /* On a dark field the surface would be paper; it keeps its ground and gains the line. */
  .c-header.is-scrolled:is(.f-cobalt, .f-ink) { --header-tint: var(--ground); }
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .c-header::before { display: none; }
    .c-header::after { background-color: var(--header-tint); }
  }
  @media (prefers-reduced-transparency: reduce) {
    .c-header::before { display: none; }
    .c-header::after { background-color: var(--header-tint); }
  }
  @media (forced-colors: active) {
    .c-header::before { display: none; }
    .c-header::after { background-color: Canvas; }
    .c-header.is-scrolled::after { border-block-end: 1px solid CanvasText; }
  }

  .c-header-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s5);
    min-block-size: var(--header-h-l);
    max-inline-size: var(--content-max);
    margin-inline: auto;
    padding-inline: var(--margin);
  }

  .c-header-name {
    display: inline-flex;
    align-items: center;
    gap: 0.55em;
    min-block-size: var(--target);
    color: var(--fg);
    font-family: var(--font-heading);
    font-size: var(--t-body);
    font-weight: calc(700 + var(--w-adjust));
    letter-spacing: calc(var(--tr-component) + var(--tr-adjust));
    line-height: 1.1;
    text-decoration: none;
    white-space: nowrap;
  }

  /* The sun-and-moon mark: a saffron sun rising out of a moon cup. Built here, not from
     shapes.css, so the header never depends on the shape layer. */
  .c-mark {
    position: relative;
    flex: none;
    inline-size: 1.3em;
    block-size: 1.3em;
  }
  .c-mark::before,
  .c-mark::after {
    content: "";
    position: absolute;
  }
  .c-mark::before {
    inset-block-start: 4%;
    inset-inline-start: 20%;
    inline-size: 60%;
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--shape-1);
  }
  .c-mark::after {
    inset-block-end: 0;
    inset-inline: 0;
    block-size: 50%;
    border-radius: 0 0 999em 999em;
    background: var(--mark-moon);
  }

  .c-nav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2);              /* clears the 3px focus offset of the neighbour */
  }
  .c-nav-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-block-size: var(--target);
    min-inline-size: var(--target);
    padding-inline: var(--s5);   /* the current-page pill needs room round its label (C84) */
    border-radius: var(--r-pill);
    color: var(--fg);
    font-size: var(--t-small);
    font-weight: calc(600 + var(--w-adjust));
    letter-spacing: var(--tr-adjust);
    text-decoration: none;
    white-space: nowrap;
    transition: background-color var(--d-fast) var(--ease-out), color var(--d-fast) var(--ease-out);
  }
  .c-nav-link:focus-visible,
  .c-nav-link.is-focus { outline: var(--focus-width) solid var(--focus); outline-offset: var(--focus-offset); border-radius: var(--r-pill); }
  .c-nav-link.is-hover { background-color: color-mix(in srgb, var(--fg) 9%, transparent); }
  @media (hover: hover) {
    .c-nav-link:hover { background-color: color-mix(in srgb, var(--fg) 9%, transparent); }
  }
  .c-nav-link:is(:active, .is-active) { background-color: color-mix(in srgb, var(--fg) 16%, transparent); }
  .c-nav-link[aria-current] { background-color: var(--action); color: var(--on-action); }
  /* Forced colours replace the pill's fill with the page colour, so the current page looked
     like every other link (WP21). It keeps a pill in system colours; adjusting is off for it
     alone, as the browser's text backplate would otherwise hide its label. */
  @media (forced-colors: active) {
    .c-nav-link[aria-current] { forced-color-adjust: none; background-color: SelectedItem; color: SelectedItemText; outline-color: CanvasText; }
  }

  /* Stacked header: the name on its own row above the links. On pages it follows the
     window, like the sticky offset and scroll padding below and in tokens.css, so the three
     always agree (a container query alone disagreed in the 761–776px band when a classic
     scrollbar narrows the page). The container query serves only boxes narrower than the
     wide layout needs (about 600px), such as the kitchen sink samples; on a page the header
     is never that narrow while the window is 761 or wider. Keep the two blocks identical. */
  @media (width < 761px) {
    .c-header-bar {
      flex-direction: column;
      align-items: stretch;
      gap: 0;
      min-block-size: 0;
    }
    .c-header-name { align-self: flex-start; min-block-size: var(--header-row); }
    /* Four links share the row and wrap rather than overflow under the text-spacing
       override (WCAG 1.4.12). The row reaches 8px from the screen edges, so a current pill
       at either end keeps 8px clear of them. Each side of a label gets as much room as the
       row allows (C85): the row is 100cqi less 16px, the labels about 185px, and 8px stays
       spare: 14px on a 320 phone, 20px from about 370 up. */
    .c-nav {
      justify-content: space-between;
      gap: 0;
      min-block-size: var(--header-h-s);
      margin-inline: calc(-1 * var(--s3));
    }
    .c-nav-link { padding-inline: clamp(var(--s3), (100cqi - 209px) / 8, 1.25rem); }
  }
  @container c-header (width < 600px) {
    .c-header-bar {
      flex-direction: column;
      align-items: stretch;
      gap: 0;
      min-block-size: 0;
    }
    .c-header-name { align-self: flex-start; min-block-size: var(--header-row); }
    /* Four links share the row and wrap rather than overflow under the text-spacing
       override (WCAG 1.4.12). The row reaches 8px from the screen edges, so a current pill
       at either end keeps 8px clear of them. Each side of a label gets as much room as the
       row allows (C85): the row is 100cqi less 16px, the labels about 185px, and 8px stays
       spare: 14px on a 320 phone, 20px from about 370 up. */
    .c-nav {
      justify-content: space-between;
      gap: 0;
      min-block-size: var(--header-h-s);
      margin-inline: calc(-1 * var(--s3));
    }
    .c-nav-link { padding-inline: clamp(var(--s3), (100cqi - 209px) / 8, 1.25rem); }
  }
  @media (width < 761px) {
    .c-header { inset-block-start: calc(-1 * var(--header-row)); }
    /* Focus on the name brings its row back into view (WCAG 2.4.11). */
    .c-header:has(.c-header-name:focus-visible) { inset-block-start: 0; }
  }
  /* A short window (400% zoom, a phone on its side): the header scrolls with the page (§15 R5). */
  @media (height < 500px) {
    .c-header { position: relative; inset-block-start: auto; }
  }

  /* ---- Footer (§6.1) -------------------------------------------------------------------
     Plain on bone, or inside the cobalt field on home: add f-cobalt to the footer, which
     stays a sibling of <main> (§15 R7). */
  .c-footer { color: var(--fg); }
  .c-footer-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3) var(--s6);
    max-inline-size: var(--content-max);
    margin-inline: auto;
    padding: var(--s5) var(--margin) var(--s7);
  }
  .c-footer-bar::before {
    content: "";
    flex: 1 0 100%;
    block-size: 0;
    margin-block-end: var(--s3);
    border-block-start: var(--ring) solid var(--line);
  }
  .c-footer-legal,
  .c-footer-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 var(--s5);
  }
  .c-footer-link {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    min-block-size: var(--target);
    color: var(--fg);
    font-size: var(--t-small);
    font-weight: calc(500 + var(--w-adjust));
    text-decoration-line: none;
  }
  .c-footer-link.is-hover { text-decoration-line: underline; text-decoration-thickness: var(--underline-hover); }
  @media (hover: hover) {
    .c-footer-link:hover { text-decoration-line: underline; text-decoration-thickness: var(--underline-hover); }
  }

  /* ---- Icons ---------------------------------------------------------------------------
     Phosphor regular arrows, inline SVG, currentColor, aria-hidden (§10 item 10). */
  .c-icon {
    display: inline-block;
    flex: none;
    inline-size: 1em;
    block-size: 1em;
    vertical-align: -0.125em;
  }

  /* ---- Text link (§6.2) ----------------------------------------------------------------
     Every <a> in running text already has the underline (base.css). These add the pressed
     and forced states, and space for a trailing icon. Visited looks the same as default. */
  :where(a:not([class]), .c-text-link):is(:active, .is-active) { text-decoration-thickness: var(--underline-hover); color: var(--fg); }
  :where(.c-text-link).is-hover { text-decoration-thickness: var(--underline-hover); }
  :where(.c-text-link).is-focus { outline: var(--focus-width) solid var(--focus); outline-offset: var(--focus-offset); border-radius: 2px; }
  .c-text-link .c-icon { margin-inline-start: 0.2em; }

  /* ---- Arrow pass (§6.2, the button) --------------------------------------------------------
     The arrow leaves in its own direction and comes back in from the other side. --dx and --dy
     come from the direction class; --travel is how far it goes to leave its box. The go link
     has its own version below (c-go-carry), timed with its disc. The curves are written out:
     a var() in a keyframe's animation-timing-function is dropped (Chromium, WebKit fall back to
     ease). They must equal --ease-in and --ease-out in tokens.css. */
  @keyframes c-arrow-pass {
    0% { translate: 0 0; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
    40% { translate: calc(var(--dx) * var(--travel)) calc(var(--dy) * var(--travel)); }
    40.01% { translate: calc(var(--dx) * var(--travel) * -1) calc(var(--dy) * var(--travel) * -1); animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); }
    100% { translate: 0 0; }
  }

  /* ---- Go link (§6.2, C43 version C) ---------------------------------------------------------
     A 44px circle with an arrow, then the label. Arrow rule: --down same page, --right
     (default) another page, --up-right external, --left back, --up top.
     Hover in, one way: the arrow leaves ahead in its own direction (the first 40% of 320ms,
     easing in), then the ink disc slides into the circle from the side the arrow comes back
     from, carrying the arrow on its centre (the last 60%, easing out). Disc and arrow travel
     the circle's own width on the same curve, so they move as one piece. Hover out: the disc
     slides back the way it came in 200ms; the arrow stays. Transform only: the disc is the
     whole circle, moved by translate and clipped by the circle. */
  .c-go {
    --dx: 1;
    --dy: 0;
    --go-size: var(--target);                 /* the circle; the disc and arrow travel this far */
    --go-lead: calc(var(--d-base) * 0.4);     /* the arrow leaving */
    --go-carry: calc(var(--d-base) * 0.6);    /* the disc and arrow coming in */
    display: inline-flex;
    align-items: center;
    gap: var(--s3);
    min-block-size: var(--target);
    padding-inline-end: var(--s2);   /* room inside the focus ring after the label */
    color: var(--fg);
    font-size: var(--t-small);
    font-weight: calc(600 + var(--w-adjust));
    letter-spacing: var(--tr-adjust);
    line-height: 1.3;
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
  }
  .c-go--left { --dx: -1; --dy: 0; }
  .c-go--up { --dx: 0; --dy: -1; }
  .c-go--down { --dx: 0; --dy: 1; }
  .c-go--up-right { --dx: 1; --dy: -1; }

  .c-go-circle {
    position: relative;
    isolation: isolate;
    display: grid;
    place-items: center;
    flex: none;
    inline-size: var(--go-size);
    block-size: var(--go-size);
    border: var(--ring) solid currentColor;
    border-radius: 50%;
    overflow: hidden;
    transition: border-color var(--d-out) var(--ease-out), scale var(--d-fast) var(--ease-out);
  }
  /* The disc waits one circle-width behind the arrow's entry side, outside the clip. */
  .c-go-circle::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: 50%;
    background: var(--action);
    translate: calc(var(--dx) * -100%) calc(var(--dy) * -100%);
    transition: translate var(--d-out) var(--ease-out);
  }
  /* Hover out: the disc uncovers the arrow within its first third, so the colour follows fast */
  .c-go-arrow {
    inline-size: var(--icon);
    block-size: var(--icon);
    transition: color var(--d-fast) var(--ease-out);
  }

  /* Leave ahead, then ride in on the disc. The jump between the two happens outside the
     circle, where the clip hides it; that is also where the arrow changes colour. The curves
     are --ease-in and --ease-out written out: var() in a keyframe's timing function is
     ignored (Chromium and WebKit fall back to ease), and the second half must match the
     disc's transition exactly. */
  @keyframes c-go-carry {
    0% { translate: 0 0; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
    40% { translate: calc(var(--dx) * var(--go-size)) calc(var(--dy) * var(--go-size)); }
    40.01% { translate: calc(var(--dx) * var(--go-size) * -1) calc(var(--dy) * var(--go-size) * -1); animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); }
    100% { translate: 0 0; }
  }

  /* Hover and keyboard focus share one look. Touch screens never get the hover. The disc
     starts once the arrow is out (the delay), on the arrow's own curve and time. */
  .c-go:is(:focus-visible, .is-hover, .is-focus) .c-go-circle { border-color: var(--action); }
  .c-go:is(:focus-visible, .is-hover, .is-focus) .c-go-circle::before {
    translate: 0 0;
    transition: translate var(--go-carry) var(--ease-out) var(--go-lead);
  }
  .c-go:is(:focus-visible, .is-hover, .is-focus) .c-go-arrow {
    color: var(--on-action);
    transition: color 0s linear var(--go-lead);
    animation: c-go-carry var(--d-base) both;
  }
  @media (hover: hover) {
    .c-go:hover .c-go-circle { border-color: var(--action); }
    .c-go:hover .c-go-circle::before {
      translate: 0 0;
      transition: translate var(--go-carry) var(--ease-out) var(--go-lead);
    }
    .c-go:hover .c-go-arrow {
      color: var(--on-action);
      transition: color 0s linear var(--go-lead);
      animation: c-go-carry var(--d-base) both;
    }
  }
  /* Pressed (and the only feedback on touch): the disc is in at once, the circle pushed in
     slightly; no arrow pass. */
  .c-go:is(:active, .is-active) .c-go-circle { border-color: var(--action); scale: 0.92; }
  .c-go:is(:active, .is-active) .c-go-circle::before { translate: 0 0; transition: translate var(--d-fast) var(--ease-out); }
  .c-go:is(:active, .is-active) .c-go-arrow { color: var(--on-action); transition: color var(--d-fast) var(--ease-out); }

  .c-go:focus-visible,
  .c-go.is-focus {
    outline: var(--focus-width) solid var(--focus);
    outline-offset: var(--focus-offset);
    border-radius: var(--r-pill);
  }

  /* ---- Button (§6.2) -------------------------------------------------------------------------
     Contact submit. The label is its own element ([data-label]) because contact.js rewrites the
     text while sending; the arrow stays. Loading follows aria-busy on the form. */
  .c-button {
    --dx: 1;
    --dy: 0;
    --travel: 130%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s3);
    min-block-size: calc(var(--target) + var(--s1));
    padding: var(--s2) var(--s5) var(--s2) var(--s6);
    border: 0;
    border-radius: var(--r-pill);
    background-color: var(--action);
    color: var(--on-action);
    font-size: var(--t-small);
    font-weight: calc(600 + var(--w-adjust));
    letter-spacing: var(--tr-adjust);
    line-height: 1.3;
    text-align: start;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background-color var(--d-fast) var(--ease-out), color var(--d-fast) var(--ease-out);
  }
  .c-button-icon {
    position: relative;
    display: grid;
    place-items: center;
    flex: none;
    inline-size: var(--icon);
    block-size: var(--icon);
    overflow: hidden;
  }
  .c-button-icon > svg { inline-size: 100%; block-size: 100%; }

  .c-button:is(:focus-visible, .is-hover, .is-focus) { background-color: var(--action-hover); }
  .c-button:is(:focus-visible, .is-hover, .is-focus) .c-button-icon > svg { animation: c-arrow-pass var(--d-base) both; }
  @media (hover: hover) {
    .c-button:hover { background-color: var(--action-hover); }
    .c-button:hover .c-button-icon > svg { animation: c-arrow-pass var(--d-base) both; }
  }
  /* Pressed: the hover colour, one step towards the resting colour, pushed down 1px */
  .c-button:is(:active, .is-active) { background-color: color-mix(in srgb, var(--action-hover) 78%, var(--action)); translate: 0 1px; }
  .c-button:focus-visible,
  .c-button.is-focus {
    outline: var(--focus-width) solid var(--focus);
    outline-offset: var(--focus-offset);
    border-radius: var(--r-pill);
  }

  /* Disabled: sending stays disabled on the live contact page, so this is a calm, legible state */
  .c-button:is(:disabled, [aria-disabled="true"]) {
    background-color: color-mix(in srgb, var(--fg) 9%, transparent);
    color: var(--fg-muted);
    cursor: not-allowed;
    translate: none;
  }
  .c-button:is(:disabled, [aria-disabled="true"]) .c-button-icon > svg { animation: none; }

  /* Loading: the action colour stays, the arrow becomes a still half-filled circle. No loop (§7.3). */
  .c-button:is(.is-loading, [aria-busy="true"] *),
  [aria-busy="true"] .c-button:disabled {
    background-color: var(--action);
    color: var(--on-action);
    cursor: progress;
  }
  :is(.c-button.is-loading, [aria-busy="true"] .c-button) .c-button-icon > svg { visibility: hidden; animation: none; }
  :is(.c-button.is-loading, [aria-busy="true"] .c-button) .c-button-icon::after {
    content: "";
    position: absolute;
    inset: 15%;
    border: var(--ring) solid currentColor;
    border-radius: 50%;
    background: linear-gradient(90deg, currentColor 50%, transparent 50%);
  }
}

/* ==== Home components (WP7, REDESIGN-PLAN.md §6.5): fill inside this block only ==== */
@layer components {
  /* Hero, client list, "Selected work." heading, project layouts, about field and contact
     field (§6.5, §9.2). Each is an .l-grid band, so it spans the window and can host one edge
     shape; children are placed here by grid line (line 2 is content column 1), so pages add
     no placement classes. Shapes come only from a cluster wrapper or an edge class
     (shapes.css). Type roles come from the type-* classes in the markup, so a page may use
     any heading level. Below 761px everything stacks on the 4-column grid. */

  /* ---- Node mark: a caps label on the line beside a coloured mark (§3, §6.3) ------------
     The mark takes its colour from a tone class on the label (t-1…t-4, as shapes do), saffron
     by default, with a ring in the text colour. The line (WP10) passes through it. */
  :is(.c-project-credit, .c-clients-title, .c-work-head, .c-about-title) {
    display: flex;
    align-items: flex-start;
    gap: var(--s3);
  }
  :is(.c-project-credit, .c-clients-title, .c-work-head, .c-about-title)::before {
    content: "";
    flex: none;
    inline-size: var(--node, var(--s3));
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--sh-tone, var(--shape-1));
    box-shadow: 0 0 0 var(--node-gap, 3px) var(--ground), 0 0 0 calc(var(--node-gap, 3px) + var(--ring)) var(--fg);
    margin-inline: calc(var(--node-gap, 3px) + var(--ring));
    margin-block-start: calc((1lh - var(--node, var(--s3))) / 2);   /* centred on the first line */
  }
  /* Headings with the mark scale it to their size: "Selected work." and "Get to Know Me"
     (Title) and "Selected clients" (Component), per the §4.2 level-to-role map for home.
     "Get to Know Me" is the line's last stop (C16); its two lines sit in one span, so the
     flex row keeps the break. */
  :is(.c-work-head, .c-clients-title, .c-about-title) { --node: 0.3em; --node-gap: 0.08em; gap: 0.35em; }
  .c-about-title > span { flex: 1; }   /* Safari sizes it to "Get to" and would break "Know Me" */
  .c-project-credit, .c-clients-title { color: var(--fg); }

  /* ---- Hero (§3 first viewport, §9.2) --------------------------------------------------
     Statement over columns 1–8 at display size (the 152px exception), the promise under it,
     the portrait arch on columns 8–12 in a cluster-sun-* wrapper, support text and the
     "Get to know me" go link (down) under the statement. One edge shape at most. */
  .c-hero {
    position: relative;
    overflow-x: clip;            /* cluster shapes at the window edge never scroll sideways */
    row-gap: 0;
    padding-block: var(--s6) var(--space-component);
  }
  /* Capped at its grid area: fitted to content alone, the title took the width of its
     longest word, so under the text-spacing override (WCAG 1.4.12) "products." ran 36px past
     a 320px window and was clipped. Capped, the word breaks instead (h1 has break-word). */
  .c-hero-title { justify-self: start; max-inline-size: 100%; }
  .c-hero-statement, .c-hero-promise { display: block; }
  .c-hero-statement > span { display: block; }
  .c-hero-accent { color: var(--accent); }
  .c-hero-promise { margin-block-start: var(--s5); max-inline-size: 13em; }
  .c-hero-art { margin-block-start: var(--s6); }
  .c-hero-support { margin-block-start: var(--s7); }
  .c-hero-support > p { color: var(--fg-muted); }
  .c-hero-support > .c-go { margin-block-start: var(--s5); }

  @media (width < 761px) {
    /* The 390px sketch (§9.6): the portrait sits on columns 3–5, right-aligned, capped so the
       support text and link stay near the first screen. It comes first, above the statement
       (C70, owner): under the text it left the statement alone on top and felt unbalanced.
       Only the visual order changes; the reading order is the HTML's (title first). */
    .c-hero > .c-hero-art {
      grid-column: 3 / span 3;
      justify-self: end;
      inline-size: min(100%, calc(32svh * 0.8));
      order: -1;
      margin-block-start: 0;
    }
    .c-hero > .c-hero-title { margin-block-start: var(--s7); }   /* as the support text had: clear of the moon under the portrait */
  }
  @media (width >= 761px) {
    .c-hero {
      align-content: center;
      min-block-size: calc(100svh - var(--header-h));
      padding-block: var(--s7) var(--s9);   /* room for the moon under the portrait */
    }
    .c-hero > .c-hero-title { grid-column: 2 / span 8; grid-row: 1; align-self: end; position: relative; z-index: 1; }
    .c-hero > .c-hero-art { grid-column: 9 / span 5; grid-row: 1 / span 2; align-self: center; margin-block-start: 0; }
    .c-hero > .c-hero-support { grid-column: 2 / span 6; grid-row: 2; }
  }

  /* ---- Client list (§6.6, D1, D8) -------------------------------------------------------
     Typeset names in a wrapped grid, not a scrolling band. A logo can come later: add an
     <svg class="c-clients-logo" aria-hidden="true"> and give the name u-sr, so the name is
     still announced once. The names are not links, so the rows are not sized as targets.
     No space below the list: "Selected work." brings one section gap of its own (§4.4). */
  .c-clients {
    row-gap: var(--space-after-heading);
    padding-block: var(--space-component) 0;
    align-items: start;
  }
  .c-clients-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 9rem), 1fr));
    column-gap: var(--gutter);
    padding: 0;
    list-style: none;
  }
  .c-clients-item {
    display: flex;
    align-items: center;
    padding-block: var(--s3);
    border-block-start: var(--ring) solid var(--line);
  }
  .c-clients-name {
    font-family: var(--font-heading);
    font-size: var(--t-lede);
    font-weight: calc(500 + var(--w-adjust));
    line-height: calc(var(--lh-component) + var(--lh-adjust));
    letter-spacing: calc(var(--tr-component) + var(--tr-adjust));
    text-wrap: pretty;
  }
  .c-clients-logo { block-size: var(--logo-h, var(--s6)); inline-size: auto; color: var(--fg-muted); }
  /* From 761px three columns, so 17 names end on a row of two and the longest ("Harvard Law
     Review") keeps one line. Beside the title from 1024px; below that the list needs the full
     width for three columns, so it goes under the title. */
  @media (width >= 761px) {
    .c-clients-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .c-clients > .c-clients-title { grid-column: 2 / span 12; }
    .c-clients > .c-clients-list { grid-column: 2 / span 12; }
  }
  @media (width >= 1024px) {
    .c-clients > .c-clients-title { grid-column: 2 / span 3; padding-block-start: var(--s3); }
    .c-clients > .c-clients-list { grid-column: 5 / span 9; }
  }

  /* ---- "Selected work." heading (D2: visible) ------------------------------------------- */
  /* A section opener: section space above, a component's lead-in below (above ≥ 2 × below). */
  .c-work-head { padding-block: var(--space-section) var(--space-before-h3); }
  /* The work list ends a section's space above the about field's arch, as every case page leaves
     before its arched field. With none, the last "Read the story" touched the arch's crown and the
     line had no room to cross into the field (owner, 2026-11-05, C62). */
  .c-work { padding-block-end: var(--space-section); }

  /* ---- Project (work index, §6.5, §9.2) -------------------------------------------------
     <article class="c-project c-project--band|sun|stack|pair [c-project--flip] l-grid">
       media (a cluster wrapper or a panel), credit, title (its link stretches over the card),
       description, "Read the story" go link.
     The DOM keeps today's order (credit before title); the credit is shown under the title,
     because a label never sits above a heading (§3). Every layout uses the same title role
     and rhythm, so the nine projects keep equal weight. --flip mirrors the layout. */
  .c-project {
    position: relative;
    overflow-x: clip;
    row-gap: 0;
    align-items: start;
  }
  .c-project + .c-project { margin-block-start: var(--space-component); }
  .c-project-media { position: relative; }
  /* The credit sits a heading's space below the title (16px, 24px on wide screens). At --s3 the
     caps label read as part of the title (owner, 2026-11-05, C59). */
  .c-project-title { margin-block-end: var(--space-after-heading); }
  .c-project-credit { margin-block-end: var(--space-after-heading); }
  .c-project:has(> .c-project-scope) > .c-project-credit { margin-block-end: var(--s2); }
  .c-project-scope { margin-block-end: var(--space-after-heading); color: var(--fg-muted); }
  .c-project-desc { color: var(--fg-muted); }
  .c-project--stack > .c-project-title { font-stretch: var(--stretch-narrow); }   /* §4.2: stack titles at 88% width */
  .c-project > .c-go { justify-self: start; margin-block-start: var(--s5); }

  /* Only the title is the link; its hit area stretches over the whole card (§15 #5).
     The go link sits above it, so it stays its own target. */
  .c-project-title a { color: inherit; text-decoration-line: none; }
  .c-project-title a::after { content: ""; position: absolute; inset: 0; z-index: 2; }
  .c-project > .c-go { position: relative; z-index: 3; }
  /* Media with its own controls (the C.S. Lewis carousel) sits above the stretched link too,
     so its buttons, track and swipe reach the carousel, not the title (WP13). */
  .c-project-media:has(.c-carousel) { z-index: 3; }
  .c-project-title a:focus-visible { outline-offset: var(--s1); }
  /* Pointing at the card underlines its title. The go link takes its hover state only when
     it is pointed at or focused itself (C39). */
  @media (hover: hover) {
    .c-project-title a:hover { text-decoration-line: underline; text-decoration-thickness: var(--underline-hover); }
  }

  /* Media pieces. A panel holds screens or a page on a ground (a field class may set it);
     a pair is two phone screens standing side by side, the second set higher. */
  .c-project-panel {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: var(--gutter);
    padding: var(--s5);
    border-radius: var(--r-soft);
    background-color: var(--placeholder);
  }
  .c-project-panel:is(.f-bone, .f-blush, .f-cobalt, .f-ink) { background-color: var(--ground); }
  /* A screen is three grid columns wide and shrinks to fit (--col comes from the .l-grid band);
     a window is eight. So every band's media stands about as tall as the other layouts'. */
  .c-project-screen { flex: 0 1 calc(3 * var(--col) + 2 * var(--gutter)); aspect-ratio: 1206 / 2622; border-radius: var(--s4); background-color: var(--surface); }
  .c-project-window { flex: 0 1 calc(8 * var(--col) + 7 * var(--gutter)); }
  .c-project-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: end;
    gap: var(--gutter);
    padding-inline: 8%;
  }
  .c-project-pair > :nth-child(2) { margin-block-end: 14%; }
  /* The status bar clock sits about 10% in from the edge, where a semicircle has risen 20%
     of the width; layout.css's 14% suits wider arches. */
  .c-project-pair > .l-frame--arch.l-frame--screen > img { padding-block-start: 20%; }
  /* Phone ratio; --arch-v makes the arch a true semicircle (half the width = 23% of the height). */
  .c-project-pair > .l-frame { background-color: var(--surface); aspect-ratio: 1206 / 2622; --arch-v: 23%; }
  /* Panel n of a five-panel presentation composite (Care to Cure today). Each panel has the
     screen ratio, so the frame shows one whole panel. */
  .c-project-crop > img { object-fit: cover; }
  .c-project-crop--2 > img { object-position: 25% 0; }
  .c-project-crop--3 > img { object-position: 50% 0; }

  /* A panel on a form, not a box (C49). The panel keeps its field class, so its colours are
     its own; the box goes, and a form of the case's set stands behind the screens, filled
     with a soft, static gradient from a lighter centre to the field's ground. Drawn by the
     panel's ::before, so it is not a shape: nothing moves it, the lens does not push it,
     and it never counts against a page's form set or motion budget.
       c-project-panel--moon   the screens stand on a moon (Care to Cure: the moon is the
                               case's form and the stand for phone pairs, §5.2)
       c-project-panel--round  the window lies across a round (Monocle: the round, a sun,
                               is the one form of arch, moon and round in its set)        */
  .c-project-panel:is(.c-project-panel--moon, .c-project-panel--round) {
    position: relative;
    isolation: isolate;
    border-radius: 0;
    background-color: transparent;
  }
  :is(.c-project-panel--moon, .c-project-panel--round)::before {
    content: "";
    position: absolute;
    z-index: -1;
    pointer-events: none;
  }
  /* The moon reaches a fifth past the screens on each side and its base is the panel's foot, so the
     screens stand on it and their upper halves rise above its curve. The gradient follows the
     curve: lighter at the middle of the base, the field's ground at the rim. */
  .c-project-panel--moon::before {
    inset-inline: -20%;
    inset-block-end: 0;
    aspect-ratio: 2;
    border-radius: var(--r-pill) var(--r-pill) 0 0;
    background: radial-gradient(farthest-side at 50% 100%, color-mix(in srgb, var(--ground) 70%, var(--surface)), var(--ground) 85%);
  }
  /* On a phone the media fills the content width; the moon still reaches past the screens, to
     the window's edges (the page margin), so it does not shrink to a hump behind them (T15). */
  @media (width < 761px) {
    .c-project-panel--moon::before { inset-inline: calc(-1 * var(--margin)); inset-block-end: var(--s4); }
  }
  /* The round is 0.6 of the window's width (T15: at 0.86 and centred it made the card 29% taller
     and read as a tall dark disc). The panel keeps its old block padding, so the card keeps its
     height. On wide screens the round stands centred on the window's end edge, level with its
     middle, so half of it shows beside the window in the panel's free columns. On a phone the
     window fills the panel, so the round stands at the top end and the window covers its lower
     half; the panel's top padding makes room for the rest. --c-window-w is the window's width.
     The gradient lifts the middle of the ink towards ink-2 (the field's placeholder tone), away
     from the window's dark page. */
  .c-project-panel.c-project-panel--round {
    --c-window-w: calc(4 * var(--col) + 3 * var(--gutter) - 2 * var(--s4));
    --c-round: calc(var(--c-window-w) * 0.6);
    padding-block-start: calc(var(--c-round) * 0.45);
  }
  .c-project-panel--round::before {
    inset-block-start: 0;
    inset-inline-end: 0;
    inline-size: var(--c-round);
    aspect-ratio: 1;
    border-radius: 50%;
    background: radial-gradient(closest-side, var(--placeholder), var(--ground));
  }
  @media (width >= 761px) {
    .c-project-panel.c-project-panel--round {
      --c-window-w: calc(8 * var(--col) + 7 * var(--gutter));
      padding-block: var(--s5);
    }
    .c-project-panel--round::before {
      inset-block: 0;
      inset-inline: calc(50% + var(--c-window-w) / 2 - var(--c-round) / 2) auto;
      margin-block: auto;
    }
  }

  @media (width < 761px) {
    /* Phone order, per the sketch: title, credit, media, description, go link. */
    .c-project > .c-project-title { grid-row: 1; }
    .c-project > .c-project-credit { grid-row: 2; }
    .c-project > .c-project-scope { grid-row: 3; }
    .c-project > .c-project-media { grid-row: 4; margin-block-end: var(--s5); }
    /* Cluster shapes stand up to 10% above their media: keep them clear of the credit, and
       let the text boxes hug their words. */
    .c-project > .c-project-media:is(.cluster-sun-a, .cluster-sun-b, .cluster-sun-c, .cluster-pair-a, .cluster-pair-b) { margin-block-start: var(--s7); }
    .c-project > :is(.c-project-title, .c-project-credit, .c-project-scope) { justify-self: start; }
    .c-project > .c-project-desc { grid-row: 5; }
    .c-project > .c-go { grid-row: 6; }
    .c-project--stack > .c-project-media { grid-column: 3 / span 3; }
    .c-project-panel { padding: var(--s4); }
    .c-project-panel > :nth-child(n + 3) { display: none; }   /* two screens on a phone, as sketched */
    .c-project-panel > .c-project-screen:nth-child(2) { margin-block-start: var(--s7); }
    .c-project-pair { padding-inline: 4%; }
  }

  @media (width >= 761px) {
    /* Band: wide media on all 12 columns; title and credit on 1–6, description and link on 8–11. */
    .c-project--band > .c-project-media { grid-column: 2 / span 12; grid-row: 1; margin-block-end: var(--s6); }
    .c-project--band > .c-project-title { grid-column: 2 / span 6; grid-row: 2; }
    .c-project--band > .c-project-credit { grid-column: 2 / span 6; grid-row: 3; }
    .c-project--band > .c-project-desc { grid-column: 9 / span 4; grid-row: 2; }
    .c-project--band > .c-go { grid-column: 9 / span 4; grid-row: 3 / span 2; margin-block-start: var(--s3); }
    .c-project--band > .c-project-scope { grid-column: 2 / span 6; grid-row: 4; }
    /* A panel of phone screens fits its screens, centred in the band: stretched over all 12
       columns, two screens left 330px of empty panel each side at 1440. A window panel keeps
       the full width (the page inside it is eight columns wide). */
    .c-project--band .c-project-panel:not(:has(> .c-project-window)) { inline-size: fit-content; margin-inline: auto; }
    /* WebKit sizes a flex item's max-content from its picture (6,746px wide), not its basis: an
       explicit width keeps the panel fitted there too. */
    .c-project--band .c-project-panel > .c-project-screen { inline-size: calc(3 * var(--col) + 2 * var(--gutter)); }
    /* Band flip: the text row above the media, so the title still reads first. */
    .c-project--band.c-project--flip > .c-project-title { grid-row: 1; }
    .c-project--band.c-project--flip > :is(.c-project-credit, .c-go) { grid-row: 2; }
    .c-project--band.c-project--flip > .c-project-desc { grid-row: 1; }
    .c-project--band.c-project--flip > .c-project-scope { grid-row: 3; }
    .c-project--band.c-project--flip > .c-project-media { grid-row: 5; margin-block: var(--s6) 0; }

    /* Sun, stack and pair: media beside a text column, centred on it. */
    :is(.c-project--sun, .c-project--stack, .c-project--pair) {
      grid-template-rows: 1fr repeat(5, auto) 1fr;
    }
    :is(.c-project--sun, .c-project--stack, .c-project--pair) > .c-project-media { grid-row: 1 / -1; align-self: center; }
    :is(.c-project--sun, .c-project--stack, .c-project--pair) > .c-project-title { grid-row: 2; }
    :is(.c-project--sun, .c-project--stack, .c-project--pair) > .c-project-credit { grid-row: 3; }
    :is(.c-project--sun, .c-project--stack, .c-project--pair) > .c-project-scope { grid-row: 4; }
    :is(.c-project--sun, .c-project--stack, .c-project--pair) > .c-project-desc { grid-row: 5; }
    :is(.c-project--sun, .c-project--stack, .c-project--pair) > .c-go { grid-row: 6; }

    /* Sun: media on 1–6 (an arch or soft frame in a cluster-sun-* wrapper), text on 8–12. */
    .c-project--sun > .c-project-media { grid-column: 2 / span 6; }
    .c-project--sun > :not(.c-project-media) { grid-column: 9 / span 5; }
    .c-project--sun.c-project--flip > .c-project-media { grid-column: 8 / span 6; }
    .c-project--sun.c-project--flip > :not(.c-project-media) { grid-column: 2 / span 5; }
    /* The sun stands behind the frame's top outer corner, and on that side the frame reaches the
       page margin, so the window cut most of what stood out (a quarter of the sun showed at
       1440). The media keeps a band free above the frame and on its outer side, and the cluster
       places the sun in it, so about half of it shows. The cluster still sets the slot. */
    .c-project--sun > .c-project-media { padding-block-start: var(--sun-room); padding-inline-start: var(--sun-room); --sun-room: 10%; }
    .c-project--sun.c-project--flip > .c-project-media { padding-inline: 0 var(--sun-room); }

    /* Stack: type leads on 1–7, a round image on 9–12 keeps it company. */
    .c-project--stack > .c-project-media { grid-column: 10 / span 4; }
    .c-project--stack > :not(.c-project-media) { grid-column: 2 / span 7; }
    .c-project--stack > .c-project-desc { grid-column-end: span 6; }
    .c-project--stack.c-project--flip > .c-project-media { grid-column: 2 / span 4; }
    .c-project--stack.c-project--flip > :not(.c-project-media) { grid-column: 7 / span 7; }

    /* Pair: two screens on 1–6, text on 8–12. */
    .c-project--pair > .c-project-media { grid-column: 2 / span 6; }
    .c-project--pair > :not(.c-project-media) { grid-column: 9 / span 5; }
    .c-project--pair.c-project--flip > .c-project-media { grid-column: 8 / span 6; }
    .c-project--pair.c-project--flip > :not(.c-project-media) { grid-column: 2 / span 5; }
  }

  /* ---- Image lens (§6.4, WP11): made by lens.js, pointer only, off with reduced motion ----
     <span class="c-lens" aria-hidden="true"><span class="c-lens-window"><canvas class="c-lens-duo"></canvas></span></span>
     inside the image's frame, so the frame's own shape clips it. The window and the canvas
     move by transform only (the canvas the opposite way), and the window opens with scale
     and opacity. lens.js sets their sizes on paint and resize, never while moving. */
  .c-lens { position: absolute; inset: 0; pointer-events: none; }
  /* A frame that holds a lens is its box: an .l-frame already is; a carousel frame (C41) is not. */
  :where(:has(> .c-lens)) { position: relative; }
  .c-lens-window {
    position: absolute;
    inset: 0 auto auto 0;   /* physical: lens.js measures from the top left */
    box-sizing: border-box;
    overflow: hidden;
    border: 2px solid var(--c-bone);   /* the ring; the canvas is clipped inside it */
    border-radius: 50%;
    /* The faint outline is a 1px spread shadow, not an outline: with both an outline and a
       shadow, Chromium repainted the window every time the canvas inside moved (WP11 trace). */
    box-shadow:
      0 0 0 1px color-mix(in srgb, var(--c-ink) 25%, transparent),
      0 10px 30px -8px color-mix(in srgb, var(--c-ink) 45%, transparent);
    opacity: 0;
    transform-origin: 50% 50%;
    will-change: transform, opacity;
  }
  .c-lens-duo { position: absolute; inset: 0 auto auto 0; max-inline-size: none; transform-origin: 0 0; will-change: transform; }
  /* Shapes lean on `translate` while a lens is open (their transform stays shapes.js's);
     lens.js sets their will-change inline for that time, so no rule here selects a shape. */

  /* ---- About field (§6.5): blush arch, portrait, two-column text, facts ------------------ */
  .c-about { row-gap: 0; align-items: start; overflow-x: clip; }
  .c-about-lede { margin-block-start: var(--space-after-heading); }
  .c-about-portrait { margin-block-start: var(--space-before-h3); inline-size: 80%; }
  .c-about-text { margin-block-start: var(--space-before-h3); }
  .c-about-text > * + * { margin-block-start: var(--space-stack); }
  .c-about-text > .c-go { margin-block-start: var(--s5); }
  /* Stacked, the portrait's quarter stands 8% of the portrait's height above it (cluster-sun-b)
     and sways sideways under the lede. The portrait is 80% of the column and round, so that
     rise is 6.4% of the column: added to the margin, the quarter keeps the lede's gap. */
  @media (width < 761px) {
    .c-about-portrait.cluster-sun-b { margin-block-start: calc(var(--space-before-h3) + 6.4%); }
  }
  .c-about-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
    gap: var(--s5) var(--gutter);
    margin-block-start: var(--s7);
    padding-block-start: var(--s5);
    border-block-start: var(--ring) solid var(--line);
  }
  .c-about-facts dt { margin-block-end: var(--s1); }
  .c-about-facts dd { margin: 0; }
  @media (width >= 761px) {
    .c-about > .c-about-title { grid-column: 2 / span 7; grid-row: 1; align-self: end; }
    .c-about > .c-about-lede { grid-column: 9 / span 5; grid-row: 1; align-self: end; margin-block-start: 0; }
    .c-about > .c-about-portrait { grid-column: 2 / span 4; grid-row: 2 / span 2; inline-size: auto; margin-block-start: var(--space-before-h2); }
    .c-about > .c-about-text { grid-column: 7 / span 7; grid-row: 2; margin-block-start: var(--space-before-h2); }
    .c-about > .c-about-facts { grid-column: 7 / span 7; grid-row: 3; }
  }
  /* Two columns only where each holds a comfortable line (about 40 characters or more). */
  @media (width >= 1180px) {
    .c-about-text { columns: 2; column-gap: var(--s7); }
    .c-about-text > * { break-inside: avoid; }
    .c-about-text > * + * { margin-block-start: 0; }
    .c-about-text > p { margin-block-end: var(--space-stack); }
  }

  /* ---- Contact field (§6.5, §6.1): cobalt arch, the links, the footer continues it ------- */
  .c-contact {
    row-gap: 0;
    align-items: start;
    padding-block-end: var(--space-component);
  }
  .c-contact-accent { color: var(--accent); }   /* saffron on cobalt, 4.3:1: title sizes only */
  .c-contact-text { margin-block-start: var(--space-after-heading); color: var(--fg-muted); }
  .c-contact-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s4) var(--s6);
    margin-block-start: var(--s6);
  }
  /* On a phone the text fills the width, so an edge shape cannot sit beside it: the
     contact field's edge shape carries u-wide-only and is left out below 761px. */
  @media (width >= 761px) {
    .c-contact > .c-contact-title { grid-column: 2 / span 8; }
    .c-contact > :is(.c-contact-text, .c-contact-actions) { grid-column: 2 / span 6; }
  }
}
/* ==== end home components ==== */

/* ==== Case and page components (WP8, REDESIGN-PLAN.md §6.5): fill inside this block only ==== */
@layer components {
  /* Case, Me & AI, contact and legal components. Each layout component is a size container
     (c-case), so it lays out by its own width: in a page, in a narrow column or in a kitchen
     sink frame alike. Inside, .c-case-grid has the page's columns: 4, or 12 once the
     component's content box reaches 692px, which is the page's content width at a 761px
     window. Full-bleed components pad their content box to the page margins, so the same
     threshold holds for them. Headings take their role by class, not by level, so a page
     keeps its HTML outline (§4.2 level-to-role map: case h1 Title, h2 Section, h3 Component). */

  :is(.c-opener, .c-chapter, .c-tools, .c-next, .c-timeline, .c-legal, .c-quote) {
    container: c-case / inline-size;
  }
  .c-case-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    column-gap: var(--gutter);
    align-items: start;
  }
  .c-case-grid > * { grid-column: 1 / -1; min-inline-size: 0; }
  @container c-case (width >= 692px) {
    .c-case-grid { grid-template-columns: repeat(12, minmax(0, 1fr)); }
  }
  /* Full bleed: the band reaches the window edges; its content lines up with the page grid,
     also past the 1440px content maximum. */
  .c-case-bleed {
    --c-bleed-pad: max(var(--margin), calc((100% - var(--content-max)) / 2 + var(--margin)));
    padding-inline: var(--c-bleed-pad);
  }

  /* ---- Type roles by class (§4.2) ---------------------------------------------------- */
  :is(.c-opener-title, .c-chapter-title, .c-tools-title, .c-quote-text, .c-next-name, .c-timeline-year,
    .c-tools-stat-title, .c-process-title, .c-timeline-card-title, .c-legal-title, .c-legal-section-title, .c-form-success-title) {
    font-family: var(--font-heading);
    font-variant-numeric: lining-nums;
    text-wrap: balance;
    overflow-wrap: break-word;
  }
  /* A title written as two sentences or halves keeps its <br> only where the first half fits
     on one line; elsewhere the break is dropped and the title balances as one, so no half
     leaves a word alone on a line (T1). At Title size the opener's seven columns never hold a
     half on one line, so an opener title always balances. A chapter title keeps its break from
     a 1100px container, where it takes nine columns (4–12) and both halves of every case title
     fit; `pretty` keeps a last word from standing alone where a second half still wraps. The
     words never change: the <br> is followed by a space in the markup. */
  .c-opener-title br { display: none; }
  @container c-case (width < 1100px) {
    .c-chapter-title br { display: none; }
  }
  @container c-case (width >= 1100px) {
    .c-chapter-title:has(br) { text-wrap: pretty; }
  }
  :is(.c-opener-title, .c-tools-stat-title, .c-legal-title) {
    font-size: var(--t-title);
    line-height: calc(var(--lh-title) + var(--lh-adjust));
    letter-spacing: calc(var(--tr-title) + var(--tr-adjust));
    font-weight: calc(var(--w-title) + var(--w-adjust));
  }
  .c-opener-title--long {
    font-size: var(--t-title-long);
  }
  :is(.c-chapter-title, .c-tools-title, .c-next-name, .c-form-success-title) {
    font-size: var(--t-section);
    line-height: calc(var(--lh-section) + var(--lh-adjust));
    letter-spacing: calc(var(--tr-section) + var(--tr-adjust));
    font-weight: calc(var(--w-section) + var(--w-adjust));
  }
  :is(.c-process-title, .c-timeline-card-title, .c-tools-row-title, .c-legal-section-title) {
    font-family: var(--font-heading);
    font-size: var(--t-component);
    line-height: calc(var(--lh-component) + var(--lh-adjust));
    letter-spacing: calc(var(--tr-component) + var(--tr-adjust));
    font-weight: calc(var(--w-component) + var(--w-adjust));
  }
  .c-opener-lede {
    font-size: var(--t-lede);
    line-height: calc(var(--lh-lede) + var(--lh-adjust));
    letter-spacing: calc(var(--tr-lede) + var(--tr-adjust));
    font-weight: calc(var(--w-lede) + var(--w-adjust));
  }
  :is(.c-credit, .c-next-label, .c-timeline-head, .c-quote-label) {
    font-family: var(--font-text);
    font-size: var(--t-label);
    line-height: calc(var(--lh-label) + var(--lh-adjust));
    letter-spacing: calc(var(--tr-label) + var(--tr-adjust));
    font-weight: calc(var(--w-label) + var(--w-adjust));
    text-transform: uppercase;
  }
  :is(.c-figure-caption, .c-field-hint, .c-field-error, .c-form-note, .c-form-privacy, .c-form-status, .c-form-feedback, .c-legal-pending) {
    font-size: var(--t-small);
    line-height: calc(var(--lh-small) + var(--lh-adjust));
    font-weight: calc(var(--w-body) + var(--w-adjust));
  }

  /* ---- Credit / label (§4.2, §6.3) -----------------------------------------------------
     Caps label beside a 12px mark. The mark becomes the line's node in WP10. Three marks:
     sun (default, tone 1), moon (tone 3, a half disc) and ring (an open node in the text
     colour). It sits beside a title or below it, never directly above. */
  .c-credit {
    display: inline-flex;
    align-items: center;
    gap: var(--s3);
    margin: 0;
    color: var(--fg);
  }
  .c-credit::before {
    content: "";
    flex: none;
    inline-size: var(--s3);
    block-size: var(--s3);
    border-radius: 50%;
    background: var(--shape-1);
  }
  .c-credit--ring::before {
    background: transparent;
    border: var(--ring) solid var(--fg);
  }

  /* ---- Case opener (§6.5, §9.3) ----------------------------------------------------------
     Title, then the credit below it, then the intro; the media sits beside on wide
     containers and after the text on narrow ones. The credit comes first in the HTML, as on
     the live pages, and is placed below the title here. Three variants:
       c-opener--arch    an arched frame held by a cluster (cluster-sun-a), cols 9–12
       c-opener--field   a full-bleed field (add an f-* class); the media straddles its foot
       c-opener--stage   a device stage: phones in a pair (c-pair), cols 8–12. Below a
                         692px container the stage comes after the title and credit, before
                         the intro (C47), by grid rows only: the HTML order stays.        */
  .c-opener { padding-block: var(--s6) var(--space-component); }
  .c-opener--field { padding-block-start: var(--space-before-h2); }
  .c-opener-back { grid-row: 1; margin-block-end: var(--s7); }
  .c-opener-title { grid-row: 2; }
  .c-opener-credit { grid-row: 3; margin-block-start: var(--s5); }
  .c-opener-intro { grid-row: 4; margin-block-start: var(--space-before-h3); }
  .c-opener-intro > * + * { margin-block-start: var(--space-stack); }
  .c-opener-intro > :not(.c-opener-lede) { color: var(--fg-muted); }
  .c-opener-media { grid-row: 5; margin-block-start: var(--s9); }

  @container c-case (width < 692px) {
    /* Phones keep the last column free for the cluster, which reaches out at the end side:
       a shape past the start edge only leaves the screen, one past the end would widen the
       page (§15 #1, no sideways scroll). */
    :is(.c-opener--arch, .c-opener--stage) .c-opener-media { grid-column: 1 / span 3; }
    /* Phones are the first thing a phone reader should see after the title (C47). The arch
       opener follows the same rule (T2): its media shows after the title and credit, before
       the lede. */
    :is(.c-opener--arch, .c-opener--stage) .c-opener-media { grid-row: 4; margin-block-start: var(--space-before-h3); }
    :is(.c-opener--arch, .c-opener--stage) .c-opener-intro { grid-row: 5; }
    /* The arch's cluster sun rises out of the media's top end and drifts up to 48px: room for it
       under the credit */
    .c-opener--arch .c-opener-media.cluster-sun-a { margin-block-start: calc(var(--space-before-h3) + var(--s7)); }
  }
  @container c-case (width >= 692px) {
    .c-opener-title { grid-column: 1 / span 7; }
    .c-opener-credit { grid-column: 1 / span 7; }
    .c-opener-intro { grid-column: 1 / span 6; }
    .c-opener-back { grid-column: 1 / span 6; }
    /* Media beside the text, one free column between them for the cluster */
    :is(.c-opener--arch, .c-opener--stage) .c-opener-media {
      grid-column: 9 / span 4;
      grid-row: 1 / span 4;
      align-self: center;
      margin-block-start: 0;
    }
    .c-opener--stage .c-opener-media { grid-column: 8 / span 5; }
    /* Spare height from tall media goes below the intro, not between the text rows */
    :is(.c-opener--arch, .c-opener--stage) > .c-case-grid { grid-template-rows: auto auto auto 1fr; }
    .c-opener--field .c-opener-title { grid-column: 1 / span 10; }
    .c-opener--field .c-opener-intro { grid-column: 1 / span 7; }
  }
  /* The field ends part-way down the media, so the media stands across its foot. The field is
     the opener's whole box and the media hangs out of it by --c-foot; the section after the
     opener keeps that much more room above it, so the page measures as before. The opener's
     line SVG then covers only the field: the line takes the field's light tone inside it and
     the next section's colour (ink 35% on bone) from the field edge down (WP15 open item). On
     ink the field table has no line (§6.3), so on an ink opener it starts at the field edge
     (C75; it was bone 35% there until the whole-site review).
     The opener stands above the next section, so nothing of that section paints over the
     hanging media. */
  .c-opener--field {
    --c-foot: var(--s10);
    position: relative;
    z-index: 1;
    padding-block-end: 0;
    background-color: var(--ground);
  }
  .c-opener--field .c-opener-media { grid-row: 6; margin-block: var(--space-before-h2) calc(-1 * var(--c-foot)); }
  /* After an opener without a field, the first chapter keeps the section rhythm: the opener's
     own foot (--space-component) and the chapter's top add up to one --space-section, as between
     chapters (C82; they added up to 240px wide, 160px on phones, a gap the review saw). */
  .c-opener:not(.c-opener--field) + .l-section { padding-block-start: calc(var(--space-section) - var(--space-component)); }
  .c-opener--field + :is(.l-section, .l-field) { padding-block-start: calc(var(--space-section) + var(--s10)); }
  .c-opener--field + .l-field--arch { padding-block-start: calc(var(--field-pad-top) + var(--s10)); }
  /* Tall media is capped at ten columns (2–11), so a focused media link fits between the header
     and the window's foot at 1440 × 800 (T4: the Nobel window was 852px tall at twelve). A case
     may narrow its media further in its pages.css block; the viewer still shows the whole file. */
  @container c-case (width >= 692px) {
    .c-opener--field .c-opener-media { grid-column: 2 / span 10; }
  }
  /* A focused media link crosses the field's foot onto bone, where the field's saffron ring is
     1.6:1. This ring holds on both grounds (T5): a cobalt ring (3:1 and more on bone) with a bone
     band inside it, which carries the contrast on ink and cobalt. */
  .c-opener--field .c-opener-media a:focus-visible {
    outline-color: var(--c-cobalt);
    box-shadow: 0 0 0 var(--focus-offset) var(--c-bone);
  }
  /* The caption sits below the foot, on bone, so it takes bone's text roles, not the field's (T6) */
  .c-opener--field .c-opener-media > .c-figure-caption {
    --fg: var(--c-ink);
    --fg-muted: var(--c-ink-2);
    --link: var(--c-cobalt);
    --focus: var(--c-cobalt);
    --w-adjust: 0;
    --tr-adjust: 0em;
    --lh-adjust: 0;
    color: var(--fg-muted);
  }
  /* The field's end shape (T7): one still shape in a named cluster (cluster-field-end in
     shapes.css), level with the lede in columns 10–12. A narrow container has no room beside the
     intro, so the cluster becomes a band between the intro and the media, and shapes.css puts
     the shape off the window's end edge, partly behind the media. The opener clips sideways, so
     the edge shape never widens the page; the media still hangs out of the field below. */
  .c-opener--field { overflow-x: clip; }
  .c-opener--field .c-case-grid > .cluster-field-end { grid-row: 5; margin-block-start: var(--s5); }
  @container c-case (width >= 692px) {
    .c-opener--field .c-case-grid > .cluster-field-end { grid-column: 10 / span 3; grid-row: 4; align-self: center; margin-block-start: 0; }
  }

  /* ---- Chapter (§6.5, §9.3) ----------------------------------------------------------------
     Credit beside the title on wide containers (cols 1–3, on the title's first baseline), below
     it on narrow ones. Variants:
       c-chapter--figure   text and one figure side by side; add c-chapter--flip to swap sides
       c-chapter--bleed    the figure runs to the window edges under the text (needs c-case-bleed)
       c-chapter--field    on a field: add f-blush, f-cobalt or f-ink (at most one per case)  */
  .c-chapter-credit { grid-row: 2; margin-block-start: var(--s3); }
  .c-chapter-title { grid-row: 1; }
  .c-chapter-text { grid-row: 3; margin-block-start: var(--space-after-heading); }
  .c-chapter-text > * + * { margin-block-start: var(--space-stack); }
  /* A compound that must not break at its hyphen ("data-science") */
  .c-nowrap { white-space: nowrap; }
  .c-chapter-media { grid-row: 4; margin-block-start: var(--space-before-h3); }
  .c-chapter-media + .c-chapter-media { grid-row: 5; }

  @container c-case (width >= 692px) {
    /* Beside the title, the label's cap centre sits on the title's x-height centre (optical),
       not on its baseline. Ratios measured from the fonts: Bricolage x-height 0.522 em, Hanken
       cap height 0.697 em. A relative offset moves the box (and the line's node) without layout. */
    .c-chapter-credit {
      grid-column: 1 / span 3; grid-row: 1; align-self: first baseline; margin-block-start: 0;
      position: relative;
      inset-block-start: calc((var(--t-label) * 0.697 - var(--t-section) * 0.522) / 2);
    }
    .c-chapter-title { grid-column: 4 / span 8; align-self: first baseline; }
    .c-chapter-text { grid-column: 4 / span 7; grid-row: 2; }
    .c-chapter-media { grid-row: 3; margin-block-start: var(--space-before-h2); }
    .c-chapter-media + .c-chapter-media { grid-row: 3; }

    /* Text with a figure: text cols 1–6 (credit below the title), figure cols 8–12 */
    .c-chapter--figure .c-chapter-title { grid-column: 1 / span 6; }
    .c-chapter--figure .c-chapter-credit { grid-column: 1 / span 6; grid-row: 2; align-self: start; margin-block-start: var(--s3); inset-block-start: 0; }
    .c-chapter--figure .c-chapter-text { grid-column: 1 / span 6; grid-row: 3; }
    .c-chapter--figure > .c-case-grid { grid-template-rows: auto auto 1fr; }
    .c-chapter--figure .c-chapter-media { grid-column: 8 / span 5; grid-row: 1 / span 3; align-self: start; margin-block-start: 0; }
    .c-chapter--flip .c-chapter-title,
    .c-chapter--flip .c-chapter-credit,
    .c-chapter--flip .c-chapter-text { grid-column: 8 / span 5; }
    .c-chapter--flip .c-chapter-media { grid-column: 1 / span 6; }
  }
  /* On wide containers a trio of screens takes six columns (7–12), so each screen is about
     160px wide at 1440; the text keeps columns 1–6 and its measure (54ch) leaves the gap. */
  /* Below a 1100px container five columns leave a flipped title four lines deep beside its
     figure (Nobel's museum, T8): the title takes six (7–12), the text keeps its five. */
  @container c-case (692px <= width < 1100px) {
    .c-chapter--flip .c-chapter-title { grid-column: 7 / span 6; }
  }
  @container c-case (width >= 1100px) {
    /* A plain chapter title takes one more column (4–12), so a title's second half fits on its
       line at Section size (T1); the prose keeps its measure (cols 4–10). */
    .c-chapter-title { grid-column: 4 / span 9; }
    .c-chapter--figure:not(.c-chapter--flip) .c-chapter-media:has(> .c-trio) { grid-column: 7 / span 6; }
    .c-chapter--flip .c-chapter-media:has(> .c-trio) { grid-column: 1 / span 6; }
  }
  /* The figure breaks out of the content to the window (as l-breakout); its caption keeps
     the page margins. The percentages resolve on the caption, whose box is the window. */
  .c-chapter--bleed .c-chapter-media { margin-inline: calc(50% - 50vw); }
  .c-chapter--bleed .c-chapter-media > .c-figure-caption {
    padding-inline: max(var(--margin), calc((100% - var(--content-max)) / 2 + var(--margin)));
  }
  .c-chapter--field { padding-block: var(--space-section); }
  .c-chapter--field.l-field--arch { padding-block-start: var(--field-pad-top); }

  /* ---- Figures and frames (§4.5, §6.5) ------------------------------------------------------
     .c-figure holds any frame and its caption. The arch, round, pill and soft frames are the
     l-frame classes; these add the window, the phone, the phone stage, the trio and the pair. */
  .c-figure { display: grid; gap: var(--s3); }
  .c-figure-caption {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--s1) var(--s5);
    color: var(--fg-muted);
  }
  /* Small text, so the link box is lifted to the 24px target floor (§15). */
  .c-figure-caption a { display: inline-block; min-block-size: var(--target-min); color: var(--link); }
  .c-figure-caption .c-icon { margin-inline-start: 0.2em; }
  /* A linked frame (view full image) keeps the focus ring on the frame's own curve */
  a:is(.l-frame, .c-window, .c-phone) { display: block; color: inherit; }
  a:is(.l-frame, .c-phone):focus-visible { outline-offset: var(--focus-offset); }
  /* An image that opens in the viewer lifts a step on hover and focus, so it reads as
     openable (the CSL covers use the same values). The translate property only: transform
     stays the motion channel. With reduced motion it does not move; the ring still shows. */
  @media (prefers-reduced-motion: no-preference) {
    :root:not(.is-reduced-motion) a:is(.l-frame, .c-phone) { transition: translate var(--d-base) var(--ease-out); }
    :root:not(.is-reduced-motion) a:is(.l-frame, .c-phone):is(:focus-visible, .is-hover) { translate: 0 calc(-1 * var(--s2)); }
  }
  @media (prefers-reduced-motion: no-preference) and (hover: hover) {
    :root:not(.is-reduced-motion) a:is(.l-frame, .c-phone):hover { translate: 0 calc(-1 * var(--s2)); }
  }

  /* Window: a long page scrolls inside its frame on hover or keyboard focus (§7.2, Monocle).
     It moves by translate only, and only with motion allowed; otherwise it shows the top.
     --c-window-pan is the scroll's duration. The default suits a page about twice as tall as
     it is wide (Monocle, 1440 × 3040). c-window--long is for a page over four times as tall
     (Nobel, 1512 × 6772): it travels about 2.5 times as far, so it gets 2.5 times as long and
     reads at Monocle's pace (C51). */
  .c-window {
    --c-window-ratio: 0.625;   /* 16:10 view */
    --c-window-pan: calc(var(--d-slow) * 8);
    overflow: hidden;
    border: var(--ring) solid var(--line);
    border-radius: var(--r-soft);
    background-color: var(--surface);
    text-decoration: none;
  }
  .c-window-bar {
    display: block;
    block-size: var(--s6);
    border-block-end: var(--ring) solid var(--line);
    background:
      radial-gradient(circle, var(--fg-muted) 0 calc(var(--s1) + 0.5px), transparent calc(var(--s1) + 1px)) var(--s4) 50% / var(--s4) var(--s4) no-repeat,
      radial-gradient(circle, var(--fg-muted) 0 calc(var(--s1) + 0.5px), transparent calc(var(--s1) + 1px)) var(--s6) 50% / var(--s4) var(--s4) no-repeat;
  }
  .c-window-view {
    display: block;
    container-type: inline-size;
    aspect-ratio: 1 / var(--c-window-ratio);
    overflow: hidden;
    background-color: var(--placeholder);
  }
  .c-window--long { --c-window-pan: calc(var(--d-slow) * 20); }
  .c-window-view > img { inline-size: 100%; block-size: auto; }
  @media (prefers-reduced-motion: no-preference) {
    :root:not(.is-reduced-motion) .c-window-view > img {
      transition: translate calc(var(--d-slow) * 3) var(--ease-out);
    }
    :root:not(.is-reduced-motion) :is(.c-window:focus-visible, .c-window.is-hover) .c-window-view > img {
      translate: 0 calc(-100% + 100cqi * var(--c-window-ratio));
      transition: translate var(--c-window-pan) var(--ease-in-out);
    }
  }
  @media (prefers-reduced-motion: no-preference) and (hover: hover) {
    :root:not(.is-reduced-motion) .c-window:hover .c-window-view > img {
      translate: 0 calc(-100% + 100cqi * var(--c-window-ratio));
      transition: translate var(--c-window-pan) var(--ease-in-out);
    }
  }
  .c-window:focus-visible { outline-offset: var(--focus-offset); }

  /* Phone: a whole screenshot with phone corners. Never cropped (§4.5). */
  .c-phone {
    overflow: hidden;
    border-radius: 13% / 6%;
    background-color: var(--placeholder);
  }
  .c-phone > img, .c-phone > a > img { inline-size: 100%; block-size: auto; }

  /* Phone stage: one phone standing on a soft coloured stage */
  .c-phone-stage {
    display: grid;
    justify-items: center;
    padding: var(--s7) var(--s6);
    border-radius: var(--r-soft);
    background-color: var(--shape-2);   /* a field tone, not the loading colour: blush on bone, cobalt on blush */
  }
  .c-phone-stage > .c-phone { inline-size: min(100%, 18rem); background-color: var(--surface); }

  /* Trio: three screens stepping down across a pill in tone 2 */
  .c-trio {
    position: relative;
    isolation: isolate;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(var(--s3), 3vw, var(--s8));
    align-items: start;
    padding-inline: clamp(var(--s2), 6%, var(--s10));
  }
  .c-trio::before {
    content: "";
    position: absolute;
    z-index: -1;
    inset: 34% 0 8%;
    border-radius: var(--r-pill);
    background: var(--shape-2);
  }
  .c-trio > :nth-child(2) { margin-block-start: clamp(var(--s5), 6%, var(--s7)); }
  .c-trio > :nth-child(3) { margin-block-start: clamp(var(--s7), 12%, var(--s9)); }

  /* Pair on a moon: two screens standing on the moon of cluster-pair-a. Both stand on the moon's flat base, which is the box's foot (T9, T10: a step
     up or a padding below left them floating 40–70px above it). */
  .c-pair {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--gutter);
    align-items: end;
    padding: 0 8%;
  }
  /* Pair in a doorway (cluster-pair-c, C46, C56): the box takes a 2:3 arch and the arch fills
     it, so the arch is always taller than the screens and its base is their foot line. Both
     screens stand on that line, and the narrow side padding lets them fill the doorway while
     their tops stay just under the spring of the curve at every width. At 3:4 with 10% sides
     the screens were 199px wide at 1440 under a mostly empty arch (C56). */
  .c-pair--door { aspect-ratio: 2 / 3; align-content: end; padding: 0 3%; }

  /* ---- Pull quote (§4.2 derived role: Section, weight 500, saffron mark) ------------------ */
  .c-quote { margin: 0; }
  .c-quote::before {
    content: "";
    display: block;
    margin-inline-start: 0;
    inline-size: var(--s7);
    aspect-ratio: 1;
    margin-block-end: var(--s5);
    border-radius: 100% 0 0 0;
    background: var(--shape-1);
  }
  .c-quote-text {
    max-inline-size: 22ch;
    font-size: var(--t-section);
    line-height: calc(var(--lh-section) + var(--lh-adjust));
    letter-spacing: calc(var(--tr-section) + var(--tr-adjust));
    font-weight: calc(500 + var(--w-adjust));
    hanging-punctuation: first last;
  }
  /* A quote may carry a caps label between its mark and its text (Pomli's "The product goal",
     a <blockquote>): it starts where the text starts. It is not a credit, so not a line node. */
  .c-quote-label { margin-block-end: var(--s4); color: var(--fg); }
  @container c-case (width >= 692px) {
    :is(.c-quote-text, .c-quote-label) { margin-inline-start: calc((100% + var(--gutter)) / 12 * 3); }
  }
  @container c-case (width >= 692px) {
    .c-quote::before { margin-inline-start: calc((100% + var(--gutter)) / 12 * 3); }
  }
  /* The label stands on one row beside the mark, never above the text as an eyebrow (§3, T9) */
  .c-quote:has(> .c-quote-label) { display: flex; flex-wrap: wrap; align-items: center; }
  .c-quote:has(> .c-quote-label)::before { margin-block-end: 0; }
  .c-quote:has(> .c-quote-label) > .c-quote-label { flex: 1 1 0; margin: 0 0 0 var(--s4); }
  .c-quote:has(> .c-quote-label) > .c-quote-text { flex: 0 0 100%; margin-block-start: var(--s5); }
  /* A quote may stand in a chapter's text column, under the prose it lifts (Pomli's goal, where
     the live page had it): a section step above it, and it starts at the text's edge. */
  .c-chapter-text > .c-quote { margin-block-start: var(--space-before-h3); }

  /* ---- Process list (Pomli): numbered steps on the step form -----------------------------
     On wide containers each step stands on a riser one step taller than the last, and the
     risers join across the gutters, so the three make one step form (T9: separate slabs read
     as placeholders). The risers take tone 2 (blush on cobalt and bone, cobalt on blush), which
     reads as a shape on the field. Numbers sit on the risers, never on them; text starts on one
     line below. A narrow container stacks the steps, so it shows the step form once, as the
     list's mark, and the steps carry no risers. */
  .c-process {
    --c-step: var(--s5);
    display: grid;
    gap: var(--space-before-h3) var(--gutter);
    margin-inline: 0;
    margin-block-end: 0;
    padding: 0;
    list-style: none;
  }
  .c-process::before {
    content: "";
    display: block;
    inline-size: calc(var(--s6) * 3);
    block-size: calc(var(--s4) * 3);
    margin-block-end: calc(var(--s5) - var(--space-before-h3));
    background: var(--shape-2);
    clip-path: polygon(0 100%, 0 66.67%, 33.33% 66.67%, 33.33% 33.33%, 66.67% 33.33%, 66.67% 0, 100% 0, 100% 100%);
  }
  .c-process-step { display: flex; flex-direction: column; }
  .c-process-step:nth-child(2) { --n: 2; }
  .c-process-step:nth-child(3) { --n: 3; }
  .c-process-step:nth-child(4) { --n: 4; }
  .c-process-num {
    order: 0;
    font-family: var(--font-heading);
    font-size: var(--t-component);
    line-height: 1;
    font-weight: calc(var(--w-component) + var(--w-adjust));
    font-variant-numeric: tabular-nums lining-nums;
  }
  .c-process-title { order: 2; margin-block-start: var(--s3); }
  .c-process-step > p { order: 3; margin-block-start: var(--space-stack); }
  @container c-case (width >= 692px) {
    /* Start-aligned: the padding below evens the rise, so a wrapping title never lifts its riser */
    .c-process { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; }
    .c-process::before { content: none; }
    .c-process-title { margin-block-start: 0; }
    .c-process-step::before {
      content: "";
      order: 1;
      display: block;
      inline-size: 100%;
      block-size: calc(var(--n, 1) * var(--c-step));
      margin-block: var(--s4) var(--s5);
      background: var(--shape-2);
    }
    /* Each riser runs on under the gutter to the next one, so the form has no gaps */
    .c-process-step:not(:last-child)::before { inline-size: calc(100% + var(--gutter)); }
    /* The rise is taken from the space above, so the titles share one line */
    .c-process-step { padding-block-start: calc((3 - var(--n, 1)) * var(--c-step)); }
  }
  /* The list needs a container to measure: it sits in a chapter, or in this wrapper */
  .c-process-wrap { container: c-case / inline-size; }

  /* ---- Tools field (CSL, on cobalt) -------------------------------------------------------
     A cobalt panel with large soft corners, inside the content width. Title, the stat
     (Title size, saffron: 4.3:1 is fine from 24px), two rows and the result. A media module
     (the CSL cover fan, pages.css) may take .c-tools-media; the stat then moves beside it. */
  /* The padding sits on the inner grid, not on .c-tools, so the container's content box is
     the page's content width and the 692px threshold holds here as everywhere else. */
  .c-tools { border-radius: calc(var(--r-soft) * 2); }
  .c-tools > .c-case-grid { padding: var(--s9) var(--s5) var(--s8); }
  .c-tools-title { grid-row: 1; }
  .c-tools-media { grid-row: 2; margin-block-start: var(--space-before-h3); }
  .c-tools-stat { grid-row: 3; margin-block-start: var(--space-before-h3); }
  .c-tools-stat-title { color: var(--accent); }
  .c-tools-stat > p { margin-block-start: var(--space-after-heading); max-inline-size: 46ch; }
  .c-tools-row { margin-block-start: var(--space-before-h3); padding-block-start: var(--s5); border-block-start: var(--ring) solid var(--line); }
  .c-tools-row > p { margin-block-start: var(--s3); }
  .c-tools-row-title { display: flex; align-items: center; gap: var(--s3); }
  .c-tools-row-title::before { content: ""; flex: none; inline-size: var(--s4); aspect-ratio: 1; border-radius: 50%; background: var(--shape-1); }
  /* The second row's mark is a quarter, a form of the case's set; a half disc read as a moon. */
  .c-tools-row + .c-tools-row .c-tools-row-title::before { border-radius: 100% 0 0 0; background: var(--shape-2); }
  /* Every part has its row, so an absent media module leaves an empty, zero-height row 2
     and the reading order stays the HTML order (WCAG 1.3.2). */
  .c-tools-row { grid-row: 4; }
  .c-tools-row + .c-tools-row { grid-row: 5; }
  /* The result is the field's conclusion: its own row across the field, under a rule, at
     Lede size in the muted text colour with the figure in bone (§4.1: both pass on cobalt). */
  .c-tools-result {
    grid-row: 6;
    margin-block-start: var(--space-before-h3);
    padding-block-start: var(--s5);
    border-block-start: var(--ring) solid var(--line);
    color: var(--fg-muted);
    font-size: var(--t-lede);
    line-height: calc(var(--lh-lede) + var(--lh-adjust));
    letter-spacing: calc(var(--tr-lede) + var(--tr-adjust));
    font-weight: calc(var(--w-lede) + var(--w-adjust));
  }
  .c-tools-result > p { max-inline-size: 60ch; }
  .c-tools-result strong { color: var(--fg); }
  @container c-case (width >= 692px) {
    .c-tools-title { grid-column: 1 / span 8; }
    .c-tools-stat { grid-column: 1 / span 7; grid-row: 2; }
    .c-tools-media { grid-column: 1 / span 6; }
    .c-case-grid:has(> .c-tools-media) > .c-tools-stat { grid-column: 7 / span 6; grid-row: 2; align-self: center; }
    .c-tools-row { grid-column: 1 / span 6; grid-row: 3; margin-block-start: var(--space-before-h2); }
    .c-tools-row + .c-tools-row { grid-column: 7 / span 6; grid-row: 3; }
    .c-tools-result { grid-column: 1 / -1; grid-row: 5; margin-block-start: var(--space-before-h2); }
  }
  @media (width >= 761px) {
    .c-tools > .c-case-grid { padding: var(--s9) clamp(var(--s6), 5vw, var(--s9)) var(--s9); }
  }

  /* ---- Outcome: the closing chapter, on a blush arch or on bone -------------------------------
     <section class="c-chapter c-outcome c-case-bleed l-field l-field--arch f-blush">. One sun,
     clear of the text at every width, that only grows to its place (mo-grow), so it never
     reaches the text. On a phone it sits in the arch's padding. From 761px the arch padding
     is shallow (tokens.css), so it sits in the empty columns right of the prose, level with
     the end of the text. */
  .c-outcome { position: relative; isolation: isolate; }
  .c-outcome > .shape {
    position: absolute;
    z-index: -1;
    inset-inline-end: calc(var(--c-bleed-pad, var(--margin)) + 8%);
  }
  @media (width < 761px) {
    .c-outcome > .shape {
      inset-block-start: calc(var(--arch-h) * 0.55);
      --sh-cap: calc(var(--field-pad-top) - var(--arch-h) * 0.55 - var(--s5));
    }
  }
  @media (width >= 761px) {
    .c-outcome > .shape { inset-block-end: var(--space-section); }
  }
  /* Below 1180px the columns are narrow and the prose runs closer to the end columns: an M sun
     came within 17–33px of the text on most cases. There it is one column (S), so it keeps
     about a column clear (C76, whole-site review). */
  @media (761px <= width < 1180px) {
    .c-outcome > .shape { --sh-cap: var(--sh-s); }
    /* On an arch the last lines of prose reach the shape's height, so it moves down into the
       field's empty foot, under the text. */
    .c-outcome.l-field--arch > .shape { inset-block-end: var(--s6); }
  }
  /* On bone (no arch, as on C.S. Lewis, C38) the sun fills the empty end columns of the
     closing chapter: an M sun is exactly columns 11–12, right of the prose (cols 4–10), its
     foot level with the end of the text, so it stays below the title. A phone has no room
     beside the prose, so there it is hidden (by visibility: the shapes layer sets display). */
  @media (width >= 761px) {
    .c-outcome:not(.l-field--arch) > .shape { inset-inline-end: var(--c-bleed-pad, var(--margin)); }
  }
  @media (width < 761px) {
    .c-outcome:not(.l-field--arch) > .shape { visibility: hidden; }
  }

  /* ---- Next story (§6.2, §6.5) ---------------------------------------------------------------
     The back link (go link, left arrow) and the next link: "Next story" as a caps label, the
     next project's name at Section size, then the go-link circle with a right arrow. The next
     link is a .c-go, so it shares the go link's fill and arrow pass. Pomli's back link is
     "Me & AI"; the component is the same. */
  .c-next { padding-block: var(--s7) var(--space-component); }
  .c-next-bar {
    display: grid;
    gap: var(--s7) var(--gutter);
    padding-block-start: var(--s6);
    border-block-start: var(--ring) solid var(--line);
  }
  .c-next-back { justify-self: start; }
  .c-next-link {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--s3) var(--s5);
    justify-self: stretch;
    padding-inline-end: 0;
    border-radius: var(--r-soft);
  }
  .c-next-label { grid-column: 1 / -1; display: flex; align-items: center; gap: var(--s3); }
  .c-next-label::before { content: ""; flex: none; inline-size: var(--s3); aspect-ratio: 1; border-radius: 50%; background: var(--shape-1); }
  .c-next-name { display: block; text-wrap: balance; }
  /* The next link's circle is the go link's own size, the same as "All work" beside it (C50):
     the name at Section size carries the link's weight. */
  /* Room inside the focus ring, without moving the link off the grid; the ring is drawn
     just outside the text, so it clears the label's mark and stays in the margin */
  .c-next-link { padding: var(--s2); margin: calc(-1 * var(--s2)); }
  .c-next-link:is(:focus-visible, .is-focus) { outline-offset: calc(-1 * var(--s1)); }
  /* In a very narrow container the circle takes its own row, so the name keeps the width */
  @container c-case (width < 400px) {
    .c-next-link { grid-template-columns: minmax(0, 1fr); }
  }
  @container c-case (width >= 692px) {
    .c-next-bar { grid-template-columns: repeat(12, minmax(0, 1fr)); align-items: end; }
    .c-next-back { grid-column: 1 / span 3; }
    .c-next-link { grid-column: 4 / span 9; }
  }

  /* ---- Timeline (Me & AI, §9.4) ----------------------------------------------------------------
     Years are nodes on the line. Wide: the era (year and what AI could do) in cols 1–3, the
     line through col 4, the cards in cols 5–12, one per row (C53). Narrow: a rail at the start, everything after
     it. The progress bar is full ink (§15 R2): drawn in full without JS or with reduced motion;
     timeline.js scales it with the scroll inside the motion gate.
     On a page the signature line (line.js) runs down the same rail: the timeline holds a
     <svg class="c-line" data-line> and each year is <h2 class="c-timeline-year"
     data-line-node="rail">, whose mark stands on the rail, so the line passes straight through
     it (WP17). Once the line is drawn it is the progress (M3, WP17–18 fixes): it draws in full
     ink, tip and all, over the rail's pale track, and the ink bar steps aside, so the two can
     never disagree. The closing row draws a track of its own from its top into its ring, so
     the progress reaches the ring. Without JS the rail's stroke, the full bar and a full-ink
     track into the closing ring stand in. */
  .c-timeline {
    --c-rail: calc(var(--s3) / 2 + var(--s2));       /* the rail's centre on narrow containers */
    --c-node: var(--s5);
    position: relative;
    isolation: isolate;
    padding-block: var(--s5) var(--s7);
  }
  .c-timeline-line {
    position: absolute;
    z-index: -1;
    inset-block: 0;
    inset-inline-start: calc(var(--c-rail) - var(--ring) / 2);
    inline-size: var(--ring);
    background: var(--line);
  }
  .c-timeline > .c-line .c-line-path { stroke: var(--fg); }
  .c-timeline:has(> .c-line.is-drawn) > .c-timeline-line > .c-timeline-progress { visibility: hidden; }
  .c-timeline-progress {
    display: block;
    inline-size: 100%;
    block-size: 100%;
    background: var(--fg);
    transform-origin: 50% 0;
  }
  .c-timeline-head { display: none; color: var(--fg-muted); }
  .c-timeline-row {
    --c-row-pad: calc(var(--c-rail) * 2 + var(--s4));
    position: relative;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    column-gap: var(--gutter);
    padding-inline-start: var(--c-row-pad);
  }
  .c-timeline-row + .c-timeline-row { margin-block-start: var(--space-component); }
  .c-timeline-row > * { grid-column: 1 / -1; min-inline-size: 0; }
  /* The year's node: a ring on the line, filled with the ground so the line passes behind. It is
     the year's own mark (line.js reads a node from its ::before): out of the flow at the year's
     first line, moved onto the rail by its start margin (the row is its containing block, so a
     percentage in --c-rail resolves on the row as the rail's does), and centred on that line by
     its top margin. line.js scales it through --line-node when the line arrives.
     It takes the case node's style (M4): a sun-tone disc, a ground gap and a ring in the text
     colour, as the credit marks on a case's line. The gap hides the line where it meets it. */
  .c-timeline-year::before, .c-timeline-end-title::before {
    border-radius: 50%;
    background: var(--shape-1);
    box-shadow: 0 0 0 var(--node-gap, 3px) var(--ground), 0 0 0 calc(var(--node-gap, 3px) + var(--ring)) var(--fg);
  }
  .c-timeline-year::before {
    content: "";
    position: absolute;
    inline-size: var(--c-node);
    aspect-ratio: 1;
    margin-inline-start: calc(var(--c-rail) - var(--c-node) / 2 - var(--c-row-pad));
    margin-block-start: calc((1lh - var(--c-node)) / 2);
    scale: var(--line-node, 1);
  }
  .c-timeline-year {
    font-size: var(--t-title);
    line-height: calc(var(--lh-title) + var(--lh-adjust));
    letter-spacing: calc(var(--tr-title) + var(--tr-adjust));
    font-weight: calc(var(--w-title) + var(--w-adjust));
    font-variant-numeric: tabular-nums lining-nums;
  }
  .c-timeline-era > p { margin-block-start: var(--s4); color: var(--fg-muted); max-inline-size: 36ch; }
  .c-timeline-work {
    container: c-timeline-work / inline-size;
    display: grid;
    /* One card per row at every width (C53). Two 17rem cards used to share a row from about
       560px to the 761px breakpoint (a 1440 window at 200% zoom), where the 34rem rule below
       still set each card's image beside its text, so the card clipped its text (WP21, 1.4.10). */
    grid-template-columns: minmax(0, 1fr);
    gap: var(--gutter);
    margin-block-start: var(--s6);
  }
  .c-timeline-card {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border-radius: var(--r-soft);
    background-color: var(--surface);
  }
  /* Each image keeps its own shape (M1): width and height are set in the HTML, so the box is
     reserved before it loads. No fixed ratio and no band colour round it. Screens are never
     cropped (§4.5), with two exceptions:
       c-timeline-visual--crop   a very wide screen (the Monocle markup, 2048 × 764): in a card
                                 under 30rem (a phone) it takes 16:9 and is cut from its top
                                 start, so it is not a thin strip; in a wider card it shows
                                 whole. The link opens the whole file in the viewer
       c-phone-stage             a phone screen stands on a stage (add <span class="c-phone">)
     A card 34rem wide or more (one card per row on wide pages, C53) sets its image beside its
     text, so a card is about as tall as its text instead of image plus text. A screenshot
     stands in the middle of a full-height white stage (C60). A very wide screen (--crop) would be a thin strip in half a card,
     so that card keeps its image above its text, across the whole card. */
  .c-timeline-card { container: c-timeline-card / inline-size; }
  .c-timeline-visual { display: block; overflow: hidden; }
  .c-timeline-visual img { inline-size: 100%; block-size: auto; }
  @container c-timeline-card (width < 30rem) {
    .c-timeline-visual--crop { aspect-ratio: 16 / 9; }
    .c-timeline-visual--crop img { block-size: 100%; object-fit: cover; object-position: 0 0; }
  }
  /* Image beside text (C53): the column is the container, as a card cannot query itself */
  @container c-timeline-work (width >= 34rem) {
    .c-timeline-card:not(:has(> .c-timeline-visual--crop)) {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    /* A phone stage runs the card's full height, so it reaches the card's foot, with the phone
       in its middle. A screenshot, the covers and an experiment's panel keep to the top of
       their half, on the card's own ground. */
    .c-timeline-card > .c-timeline-visual:not(.c-phone-stage) { align-self: start; }
    .c-timeline-card > .c-timeline-visual.c-phone-stage { align-content: center; }
    /* A screenshot beside taller text stood at the top of its half with up to 234px of nothing
       under it (Velcro at 1440). It stands on a stage instead: white, like the screenshots' own
       pages, running the card's full height, with the screenshot whole in its middle (C60,
       owner 2026-11-05; a grey tint with the image at the top looked broken). Not
       a letterbox (M1): the box has no ratio of its own and the image keeps its shape. */
    .c-timeline-card:not(.c-timeline-card--experiment, :has(> .c-timeline-visual--crop)) > .c-timeline-visual:not(.c-phone-stage, .c-me-ai-covers) {
      align-self: stretch;
      display: grid;
      align-content: center;
      padding: var(--s5);
      background-color: var(--screen-ground);
    }
    .c-timeline-card:not(.c-timeline-card--experiment, :has(> .c-timeline-visual--crop)) > .c-timeline-visual:not(.c-phone-stage, .c-me-ai-covers) > img { border-radius: calc(var(--r-soft) / 4); }
    .c-timeline-card--experiment:not(:has(> .c-timeline-visual--crop)) > :is(.c-timeline-visual, .c-timeline-tile) { margin: var(--s3) 0 var(--s3) var(--s3); }
  }
  .c-timeline-visual.c-phone-stage { display: grid; padding: var(--s6) var(--s5); border-radius: 0; }
  .c-timeline-visual.c-phone-stage > .c-phone { inline-size: min(100%, 10rem); }
  .c-timeline-caption { display: grid; align-content: start; gap: var(--s3); padding: var(--s5) var(--s5) var(--s6); }   /* beside a taller image, the rows keep together */
  .c-timeline-caption > .c-credit { margin-block-end: var(--s1); }
  .c-timeline-card-title { font-size: var(--t-component); text-wrap: pretty; }
  .c-timeline-caption > p { color: var(--fg-muted); }
  .c-timeline-link { justify-self: start; margin-block-start: var(--s2); }
  /* The experiment card stands on its own field (add f-blush) with an open ring */
  .c-timeline-card--experiment { background-color: var(--ground); }
  .c-timeline-card--experiment .c-timeline-caption > p { color: var(--fg); }
  .c-timeline-card--experiment .c-timeline-visual { background-color: var(--surface); margin: var(--s3) var(--s3) 0; border-radius: calc(var(--r-soft) - var(--s3)); }
  /* A visual that opens the viewer (C32): the card clips its overflow, so the ring is drawn inside */
  a.c-timeline-visual:focus-visible { outline-offset: calc(-1 * var(--focus-width)); }
  /* The card labels are not line nodes: on the line's section they keep the plain mark */
  .c-timeline-card .c-timeline-caption > .c-credit::before { box-shadow: none; margin-inline: 0; }
  /* The Generative UI tile (C19) takes the visual's place in an experiment card: the phone pair
     stage (c-pair-tile) is the link to the large images, with its note below it. */
  .c-timeline-tile { display: grid; gap: var(--s3); margin: var(--s3) var(--s3) 0; }
  .c-timeline-tile > .c-pair-tile { border-radius: calc(var(--r-soft) - var(--s3)); }
  .c-timeline-note {
    padding-inline: var(--s2);
    color: var(--fg);   /* on blush the card's text keeps the full text colour (WP8) */
    font-size: var(--t-small);
    line-height: calc(var(--lh-small) + var(--lh-adjust));
  }
  /* One step (§5.2: the form of the Me & AI timeline) may stand at the foot of a row's era
     column, below the era text. A narrow container has no room beside the cards, so there it is
     taken out of the flow and hidden (by visibility: the shapes layer sets display). */
  .c-timeline-row > .shape { position: absolute; visibility: hidden; }
  /* The closing row: <section class="c-timeline c-timeline--end"> after the timeline, one row
     holding <div class="c-timeline-end"> in the work column: a statement at Section size, which
     is the line's last stop (its mark stands on the rail, data-line-node="rail"), and a go link.
     It has no rail stroke or progress of its own. */
  .c-timeline--end { padding-block: var(--space-component) var(--space-section); }
  .c-timeline-row { --c-work-at: 0px; }
  .c-timeline-end { display: grid; justify-items: start; gap: var(--s6); }
  .c-timeline-end-title {
    font-family: var(--font-heading);
    font-size: var(--t-section);
    line-height: calc(var(--lh-section) + var(--lh-adjust));
    letter-spacing: calc(var(--tr-section) + var(--tr-adjust));
    font-weight: calc(var(--w-section) + var(--w-adjust));
    text-wrap: balance;
    overflow-wrap: break-word;
  }
  .c-timeline-end-title::before {
    content: "";
    position: absolute;
    inline-size: var(--c-node);
    aspect-ratio: 1;
    margin-inline-start: calc(var(--c-rail) - var(--c-node) / 2 - var(--c-row-pad) - var(--c-work-at));
    margin-block-start: calc((1lh - var(--c-node)) / 2);
    scale: var(--line-node, 1);
  }
  /* The closing row's track (M3): from the row's top edge, through the section's top padding,
     into the ring's centre, on the rail (the row is the containing block). Pale under the drawn
     line; full ink without it, as the bar is. */
  .c-timeline-end-title::after {
    content: "";
    position: absolute;
    z-index: -1;
    inset-block-start: calc(-1 * var(--space-component));
    inset-inline-start: calc(var(--c-rail) - var(--ring) / 2);
    inline-size: var(--ring);
    block-size: calc(var(--space-component) + 0.5lh);
    background: var(--line);
  }
  .c-timeline--end:has(> .c-line:not(.is-drawn)) .c-timeline-end-title::after,
  .c-timeline--end:not(:has(> .c-line)) .c-timeline-end-title::after { background: var(--fg); }
  /* On phones (rings only) the closing row keeps one small sun, beside the go link (M5) */
  @container c-case (width < 692px) {
    .c-timeline--end .c-timeline-end { grid-row: 1; }
    .c-timeline--end .c-timeline-row > .shape {
      --sh-cap: var(--s7);
      position: static;
      visibility: visible;
      grid-row: 1;
      justify-self: end;
      align-self: end;
    }
  }

  @container c-case (width >= 692px) {
    /* Set on the children: the timeline is the container, so it cannot query itself.
       The rail is the centre of column 4 (C53); children are as wide as the timeline. */
    .c-timeline > * {
      --c-rail: calc((100% + var(--gutter)) / 12 * 3.5 - var(--gutter) / 2);
      --c-node: var(--s6);
    }
    /* The two column labels stand on the rail, one each side of a small node at its head (M4):
       a label beside its mark, as a credit, not a caps line above the first year. */
    .c-timeline-head {
      display: block;
      position: relative;
      block-size: 1lh;
      margin-block-end: var(--s8);
    }
    .c-timeline-head > * { position: absolute; inset-block-start: 0; white-space: nowrap; }
    .c-timeline-head > :first-child { inset-inline-end: calc(100% - var(--c-rail) + var(--s5)); }
    .c-timeline-head > :last-child { inset-inline-start: calc(var(--c-rail) + var(--s5)); }
    .c-timeline-head::before {
      content: "";
      position: absolute;
      inset-inline-start: calc(var(--c-rail) - var(--s3) / 2);
      inset-block-start: calc(0.5lh - var(--s3) / 2);
      inline-size: var(--s3);
      aspect-ratio: 1;
      border-radius: 50%;
      background: var(--shape-1);
      box-shadow: 0 0 0 var(--node-gap, 3px) var(--ground), 0 0 0 calc(var(--node-gap, 3px) + var(--ring)) var(--fg);
    }
    .c-timeline-row {
      --c-row-pad: 0px;
      --c-work-at: calc((100% + var(--gutter)) / 12 * 4);   /* where column 5 starts, on the row */
      grid-template-columns: repeat(12, minmax(0, 1fr));
    }
    /* The era is its year mark's containing block, so the rail is worked out on the era
       (cols 1–3, C53): the rail, the centre of column 4, is (era + gutter) × 7/6 − gutter / 2
       from its start. The year is end-aligned and runs to a step before its node (M4), so the
       numeral meets its ring. */
    .c-timeline-era {
      --c-rail-era: calc((100% + var(--gutter)) * 7 / 6 - var(--gutter) / 2);
      position: relative;
      grid-column: 1 / span 3;
      grid-row: 1;
    }
    .c-timeline-year { text-align: end; margin-inline-end: calc(100% - var(--c-rail-era) + var(--c-node) / 2 + var(--node-gap, 3px) + var(--s4)); }
    .c-timeline-year::before { inset-inline-start: 0; margin-inline-start: calc(var(--c-rail-era) - var(--c-node) / 2); }
    .c-timeline-era > p { margin-inline-start: auto; }   /* toward the rail; ragged right as all body text */
    .c-timeline-work, .c-timeline-end { grid-column: 5 / span 8; grid-row: 1; margin-block-start: 0; }
    .c-timeline-work { grid-template-columns: minmax(0, 1fr); }   /* one card per row (C53) */
    .c-timeline-row:not(:has(.c-timeline-year)) .c-timeline-era { padding-block-start: var(--s2); }
    .c-timeline-row > .shape { position: static; visibility: visible; grid-column: 1 / span 2; grid-row: 1; align-self: end; }
  }
  /* From 1024px the era column holds still below the header while its cards pass (M2): the year
     and its text stay beside the work they describe, instead of a tall empty column. In a row
     with a step at its foot, the era keeps a bottom margin as tall as the step plus a gap: a
     sticky box keeps its margin inside its row, so the era stops above the step. The step is
     an M shape (two columns and a gutter), sized as shapes.css sizes it, in lengths only:
     Chrome resolves a percentage margin against the row, not the era, when it places a sticky
     box. (The cards keep to the right: alternating sides would break the "AI tools / My work"
     columns.) */
  @media (width >= 1024px) {
    @container c-case (width >= 692px) {
      .c-timeline-era { position: sticky; inset-block-start: calc(var(--header-h) + var(--s6)); align-self: start; }
      .c-timeline-row:has(> .shape) > .c-timeline-era {
        --c-col: calc((min(100vw, var(--content-max)) - 2 * var(--margin) - (var(--cols) - 1) * var(--gutter)) / var(--cols));
        margin-block-end: calc(2 * var(--c-col) + var(--gutter) + var(--s7));
      }
    }
  }

  /* ---- Form (contact, §6.5, §15 R4) ------------------------------------------------------------
     The form sits on a paper panel: <div class="c-form-panel f-bone">, so its text keeps the
     bone roles on any field. Inputs have ink-2 borders (7.9:1 on paper); invalid is a 2px
     border plus the text message. The submit is the WP6 button. Hooks that contact.js uses
     (IDs, names, hidden, aria-busy, aria-invalid) are styled, never renamed. */
  /* Paper on bone is 1.09:1, so a 1px hairline (ink at 14%) keeps the panel's edge, and the
     foot it hangs across the contact field with, from dissolving (F2). */
  .c-form-panel {
    display: grid;
    gap: var(--s5);
    padding: var(--s6) var(--s5);
    border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
    border-radius: var(--r-soft);
    background-color: var(--surface);
    color: var(--fg);
  }
  @media (width >= 761px) {
    .c-form-panel { padding: var(--s7) var(--s7) var(--s6); }
  }
  .c-form { display: grid; gap: var(--s5); }
  .c-form-note, .c-form-privacy { color: var(--fg-muted); }
  .c-field { display: grid; gap: var(--s2); }
  .c-field-label {
    font-size: var(--t-small);
    line-height: calc(var(--lh-small) + var(--lh-adjust));
    font-weight: calc(600 + var(--w-adjust));
  }
  .c-field-input {
    inline-size: 100%;
    min-block-size: calc(var(--target) + var(--s1));
    padding: var(--s3) var(--s4);
    border: var(--ring) solid var(--fg-muted);
    border-radius: var(--s3);
    background-color: var(--ground);
    color: var(--fg);
    font-size: var(--t-body);
    line-height: 1.4;
    transition: border-color var(--d-fast) var(--ease-out), box-shadow var(--d-fast) var(--ease-out);
  }
  textarea.c-field-input { min-block-size: calc(var(--s9) + var(--s7)); resize: vertical; line-height: calc(var(--lh-body) + var(--lh-adjust)); }
  .c-field-input:is(.is-hover) { border-color: var(--fg); }
  @media (hover: hover) {
    .c-field-input:hover { border-color: var(--fg); }
  }
  .c-field-input:is(:focus-visible, .is-focus) {
    border-color: var(--fg);
    outline: var(--focus-width) solid var(--focus);
    outline-offset: var(--focus-offset);
    border-radius: var(--s3);
  }
  /* Invalid: 2px, drawn inside so the field does not move */
  .c-field-input[aria-invalid="true"] {
    border-color: var(--fg);
    box-shadow: inset 0 0 0 var(--ring) var(--fg);
    background-color: color-mix(in srgb, var(--shape-2) 28%, var(--ground));   /* a blush tint: not the focus colour */
  }
  .c-field-hint { color: var(--fg-muted); }
  .c-field-error, .c-form-feedback {
    display: flex;
    align-items: baseline;
    gap: var(--s2);
    color: var(--fg);
    font-weight: calc(600 + var(--w-adjust));
  }
  /* An ink diamond marks every error (cobalt stays the focus colour); the text carries the meaning */
  .c-field-error::before, .c-form-feedback::before {
    content: "";
    flex: none;
    inline-size: var(--s3);
    aspect-ratio: 1;
    translate: 0 0.05em;
    clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
    background: var(--fg);
  }
  .c-form-feedback { padding: var(--s3) var(--s4); border-radius: var(--s3); background-color: var(--ground); }
  /* Notes in a box run the panel's full width, like the inputs (WP18) */
  :is(.c-form-status, .c-form-feedback) { max-inline-size: none; }
  /* Availability: a calm note with an open ring, the live preview state */
  .c-form-status {
    display: flex;
    align-items: baseline;
    gap: var(--s3);
    padding: var(--s3) var(--s4);
    border-radius: var(--s3);
    background-color: var(--ground);
    color: var(--fg);
  }
  .c-form-status::before {
    content: "";
    flex: none;
    inline-size: var(--s3);
    aspect-ratio: 1;
    translate: 0 0.05em;
    border: var(--ring) solid var(--fg);
    border-radius: 50%;
  }
  .c-form-actions { display: grid; justify-items: start; gap: var(--s4); }
  .c-form-actions > .c-button { white-space: nowrap; }   /* the label stays on one line in narrow panels */
  .c-form-honeypot { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  /* Sending: the fields stay readable and visibly paused */
  .c-form[aria-busy="true"] .c-field-input { border-style: dashed; color: var(--fg-muted); }
  .c-form-success { display: grid; justify-items: start; gap: var(--s4); }
  .c-form-success-mark {
    display: grid;
    place-items: center;
    inline-size: var(--s8);
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--shape-1);
    color: var(--c-ink);   /* always on saffron: ink 8.9:1, whatever the field */
  }
  .c-form-success-mark > svg { inline-size: var(--s6); block-size: var(--s6); }
  .c-form-success-title { margin-block-start: var(--s2); }

  /* ---- Legal prose (Impressum, privacy, §9.5) -----------------------------------------------------
     Read mode: bone only, prose at the measure, one small shape beside the title. The draft
     status is a credit, placed below the title. Pending items are quiet paper notes.
     --legal-text is the text column: the measure, and from a 600px container at most ten of
     twelve columns, so the shape keeps the last two (set on the children: a container query
     never styles its own container). It resolves where it is used: in the head and sections,
     and in the shape's inset, where % and ch mean the same widths. */
  .c-legal {
    --legal-text: var(--measure);
    position: relative;
    padding-block: var(--s6) var(--space-section);
  }
  .c-legal-head { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s5); max-inline-size: var(--legal-text); }
  .c-legal-head > :not(.shape) { grid-column: 1; }
  .c-legal-title { grid-row: 1; }
  .c-legal-status { grid-row: 2; }
  .c-legal-head > p:not(.c-legal-status, .c-legal-title) { grid-row: 3; color: var(--fg-muted); }
  /* One gutter after the text column, level with the head's top; capped to the room left */
  .c-legal-head > .shape {
    --sh-cap: calc(100% - var(--legal-text) - var(--gutter));
    position: absolute;
    inset-block-start: var(--s6);
    inset-inline-start: calc(var(--legal-text) + var(--gutter));
  }
  .c-legal-section { max-inline-size: var(--legal-text); margin-block-start: var(--space-before-h2); padding-block-start: var(--s5); border-block-start: var(--ring) solid var(--line); }
  .c-legal-body { margin-block-start: var(--space-after-heading); }
  .c-legal-body > * + * { margin-block-start: var(--space-stack); }
  .c-legal-body > p { max-inline-size: none; }   /* the section holds the measure, so smaller notes keep its width (WP18) */
  /* Code on a paper chip at 500, so it no longer reads as bold (strong is 600). Paper on bone
     needs the panel's hairline to show (F2). */
  .c-legal-body code {
    padding: 0.05em 0.3em;
    border-radius: var(--s1);
    background-color: var(--surface);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 14%, transparent);
    font-family: var(--font-text);
    font-weight: calc(500 + var(--w-adjust));
    overflow-wrap: anywhere;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
  }
  .c-legal-body a { overflow-wrap: anywhere; }
  .c-legal-pending { padding: var(--s3) var(--s4); border-radius: var(--s3); background-color: var(--surface); color: var(--fg-muted); }
  /* Below a 600px container the title fills the row, so the shape steps out (the page column is
     615px at 1024). Hidden by visibility, because the shapes layer, which comes later, sets display;
     parked at the start, so its box never reaches past a narrow window. */
  @container c-case (width < 600px) {
    .c-legal-head > .shape { visibility: hidden; inset-inline-start: 0; }
  }
  @container c-case (width >= 600px) {
    .c-legal > * { --legal-text: min(var(--measure), calc((100% + var(--gutter)) / 12 * 10 - var(--gutter))); }
  }
}
/* ==== end case and page components ==== */

/* ==== Carousel and dialog (WP9, REDESIGN-PLAN.md §6.5, §6.7): fill inside this block only ==== */
@layer components {
  /* ---- Carousel (§6.5, §6.7, §15 rows 6, 8, 9) -----------------------------------------
     One component, three modes on data-carousel: centred (CSL showreel), panels (Care's five
     panels from one composite) and links (Rapha, POLITICO: slides are links). The track is a
     native scroller with snap, so without JS every slide is reachable by scrolling and the
     controls stay hidden (the hidden attribute; carousel.js shows them). Sizes come from the
     carousel's own width (container c-carousel), so it fits a page column or a sink frame.
     The stage is tone 2 of the field it sits on, like the phone stage. */
  .c-carousel {
    --c-carousel-pad: var(--s5);           /* stage padding round the slides */
    --c-carousel-gap: var(--gutter);
    container: c-carousel / inline-size;
    display: grid;
    gap: var(--s3);
    min-inline-size: 0;
  }
  .c-carousel-track {
    position: relative;                    /* slides measure their offsets from here */
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-inline: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--c-carousel-pad);
    scroll-behavior: auto;                 /* carousel.js decides how each move travels */
    border-radius: var(--r-soft);
    background-color: var(--shape-2);
  }
  /* With JS the buttons take over from the scrollbar; touch and trackpads still scroll. */
  .c-carousel.is-ready .c-carousel-track { scrollbar-width: none; }
  .c-carousel.is-ready .c-carousel-track::-webkit-scrollbar { display: none; }
  /* Every slide fits (carousel.js): nothing to browse, so no hand scroll of a few pixels of
     stage padding either; the script keeps the group in the middle. */
  .c-carousel.is-fitted .c-carousel-track { overflow-x: hidden; scroll-snap-type: none; }
  /* While the rail slides into place, snapping waits (carousel.js) */
  .c-carousel.is-moving .c-carousel-track { scroll-snap-type: none; }
  .c-carousel-track:focus-visible { outline-offset: var(--focus-offset); border-radius: var(--r-soft); }
  /* Inside the stage the colours follow the stage, not the field: the stage is cobalt on a
     blush field and blush on cobalt and ink, so link focus rings and the selected-slide sun
     take that colour's roles (§15: 2px ring, cobalt on light, saffron on cobalt). */
  .f-blush .c-carousel-rail { --focus: var(--c-saffron); --fg: var(--c-bone); }
  :is(.f-cobalt, .f-ink) .c-carousel-rail { --focus: var(--c-cobalt); --fg: var(--c-ink); }

  .c-carousel-rail {
    display: flex;
    align-items: flex-start;
    gap: var(--c-carousel-gap);
    inline-size: max-content;              /* its own padding gives the end its margin too */
    min-inline-size: 100%;
    padding: var(--c-carousel-pad);
  }
  /* When every slide fits, the group sits in the middle of the stage. Auto margins fall to 0
     as soon as the slides overflow, so nothing is pushed out of reach. */
  .c-carousel-slide:first-child { margin-inline-start: auto; }
  .c-carousel-slide:last-child { margin-inline-end: auto; }
  .c-carousel-slide {
    flex: none;
    position: relative;
    scroll-snap-align: start;
    scroll-snap-stop: always;
  }
  .c-carousel-frame {
    display: block;
    overflow: hidden;
    border-radius: var(--s4);
    background-color: var(--surface);      /* behind a screen while it loads */
  }
  .c-carousel-frame > img { inline-size: 100%; block-size: auto; }

  /* Centred: phone screens, the selected one centred (the ends stay full, no empty gutter).
     No mark under the selected screen: it read as a page dot (owner, 2026-11-04); the counter says it. */
  .c-carousel--centred .c-carousel-slide {
    inline-size: min(58cqi, 15rem);
    scroll-snap-align: center;
    scroll-snap-stop: normal;
  }
  .c-carousel--centred .c-carousel-frame { aspect-ratio: 1290 / 2796; }
  /* A wide stage (1200px and up: the home band at 1440) shows six whole screens with the same
     inset at both ends, so the C.S. Lewis showreel fits whole: nothing to browse, so the
     controls hide (C52). With five across the sixth was the only one out of view, so the
     counter stepped through slides the view could not move to. */
  @container c-carousel (inline-size >= 1200px) {
    .c-carousel--centred .c-carousel-slide { inline-size: calc((100cqi - 2 * var(--c-carousel-pad) - 5 * var(--c-carousel-gap)) / 6); }
  }

  /* Panels: five screens cut from one composite image by class. Each frame has one panel's
     ratio, so object-fit: cover shows exactly one panel and object-position picks which:
     (n − 1) × 25% of the overflow. Five across from 1000px, two and a half from 600px, one
     and a peek below. */
  .c-carousel--panels .c-carousel-slide {
    inline-size: calc((100cqi - 2 * var(--c-carousel-pad)) * 0.8);
  }
  @container c-carousel (width >= 600px) {
    .c-carousel--panels .c-carousel-slide { inline-size: calc((100cqi - 2 * var(--c-carousel-pad) - 2 * var(--c-carousel-gap)) / 2.5); }
  }
  @container c-carousel (width >= 1000px) {
    .c-carousel--panels .c-carousel-slide { inline-size: calc((100cqi - 2 * var(--c-carousel-pad) - 4 * var(--c-carousel-gap)) / 5); }
  }
  .c-carousel--panels .c-carousel-frame { aspect-ratio: 1290 / 2796; }
  .c-carousel--panels .c-carousel-frame > img { block-size: 100%; object-fit: cover; object-position: 0 0; }
  .c-carousel--panels .c-carousel-crop--2 > img { object-position: 25% 0; }
  .c-carousel--panels .c-carousel-crop--3 > img { object-position: 50% 0; }
  .c-carousel--panels .c-carousel-crop--4 > img { object-position: 75% 0; }
  .c-carousel--panels .c-carousel-crop--5 > img { object-position: 100% 0; }

  /* Links: each slide opens its image. Screens of different shapes stand at one height and
     keep their own widths (width and height are set in the HTML). */
  .c-carousel--links .c-carousel-rail { align-items: flex-end; }
  .c-carousel--links .c-carousel-frame > img { inline-size: auto; block-size: min(30rem, 110cqi); }
  .c-carousel-link { display: block; color: inherit; border-radius: var(--s4); }
  .c-carousel-link:focus-visible { outline-offset: var(--focus-offset); }

  /* Controls: previous, counter, next, at the end of the stage. The buttons are go-link
     circles (c-go), so hover, focus and pressed match every other action. */
  .c-carousel-controls {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--s2);
  }
  .c-carousel-button.c-go { gap: 0; padding: 0; border-radius: 50%; }
  .c-carousel-button.c-go:is(:focus-visible, .is-focus) { border-radius: 50%; }
  .c-carousel-status {
    position: relative;                    /* holds the visually hidden title, so it never widens a scroller's page */
    min-inline-size: 7ch;
    margin: 0;
    color: var(--fg);
    font-family: var(--font-text);
    font-size: var(--t-label);
    font-variant-numeric: tabular-nums lining-nums;
    font-weight: calc(var(--w-label) + var(--w-adjust));
    letter-spacing: calc(var(--tr-label) + var(--tr-adjust));
    line-height: var(--target);
    text-align: center;
  }
  /* At an end the button stays focusable (aria-disabled, §15 row 8) but reads as resting:
     a faint ring, a muted arrow, and no fill on hover or press. */
  .c-carousel-button[aria-disabled="true"] { cursor: default; }
  .c-carousel-button[aria-disabled="true"] .c-go-circle,
  .c-carousel-button[aria-disabled="true"]:is(:hover, :focus-visible, :active, .is-hover, .is-focus, .is-active) .c-go-circle {
    border-color: color-mix(in srgb, var(--fg) 30%, transparent);
    scale: 1;
  }
  .c-carousel-button[aria-disabled="true"]:is(:hover, :focus-visible, :active, .is-hover, .is-focus, .is-active) .c-go-circle::before { scale: 0; }
  .c-carousel-button[aria-disabled="true"] .c-go-arrow,
  .c-carousel-button[aria-disabled="true"]:is(:hover, :focus-visible, :active, .is-hover, .is-focus, .is-active) .c-go-arrow {
    color: var(--fg-muted);
    animation: none;
  }

  /* ---- Dialog (§6.5) --------------------------------------------------------------------
     The site's one dialog: a native modal (dialog.js), which the image viewer is built on (the
     phone pair dialog retired with C19, WP17). It carries its own field class, since the top
     layer sits on no field. The backdrop is ink; the page behind does not scroll. */
  :root:has(.c-dialog:modal) { overflow: hidden; scrollbar-gutter: stable; }   /* no jump where scrollbars take space */
  .c-dialog {
    inline-size: min(56rem, 100% - 2 * var(--margin));
    max-inline-size: none;
    max-block-size: calc(100dvh - 2 * var(--s5));
    margin: auto;                          /* the reset's margin: 0 would pin the modal to the corner */
    padding: 0;
    border: 0;
    border-radius: var(--r-soft);
    overflow: auto;
    overscroll-behavior: contain;
    background-color: var(--ground);
    color: var(--fg);
    container: c-dialog / inline-size;   /* queried by the viewer's layout (c-viewer-inner) */
  }
  .c-dialog::backdrop { background-color: color-mix(in srgb, var(--c-ink) 78%, transparent); }
  /* Close: a 44px ring with the × the live page uses; it fills with the action colour on
     hover and focus, like the go link. */
  .c-dialog-close {
    display: grid;
    place-items: center;
    flex: none;
    inline-size: var(--target);
    block-size: var(--target);
    border: var(--ring) solid currentColor;
    border-radius: 50%;
    color: var(--fg);
    font-size: var(--t-lede);
    line-height: 1;
    -webkit-tap-highlight-color: transparent;
    transition: background-color var(--d-fast) var(--ease-out), border-color var(--d-fast) var(--ease-out), color var(--d-fast) var(--ease-out);
  }
  .c-dialog-close:is(:focus-visible, .is-hover, .is-focus, :active, .is-active) {
    border-color: var(--action);
    background-color: var(--action);
    color: var(--on-action);
  }
  @media (hover: hover) {
    .c-dialog-close:hover { border-color: var(--action); background-color: var(--action); color: var(--on-action); }
  }
  .c-dialog-close:is(:active, .is-active) { scale: 0.92; }
  .c-dialog-close:is(:focus-visible, .is-focus) { outline: var(--focus-width) solid var(--focus); outline-offset: var(--focus-offset); border-radius: 50%; }

  /* ---- Phone pair (§6.5): the Generative UI tile on Me & AI ------------------------------
     The draft screen is cut from a wider capture by class: the frame has the phone's ratio
     and the image is placed inside it, as generative-ui-phones.css did. */
  .c-pair-phones {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s4);
    block-size: 100%;
  }
  .c-pair-phone {
    position: relative;
    display: block;
    flex: none;
    block-size: 100%;
    overflow: hidden;
    border-radius: 13% / 6%;
    background-color: var(--surface);
  }
  .c-pair-phone--draft { aspect-ratio: 831 / 1514; }
  .c-pair-phone--draft > img {
    position: absolute;
    inset-block-start: -2.9062%;
    inset-inline-start: -93.3815%;
    inline-size: 208.6643%;
    max-inline-size: none;
    block-size: auto;
  }
  .c-pair-phone--events { aspect-ratio: 393 / 852; }
  .c-pair-phone--events > img { inline-size: 100%; block-size: 100%; object-fit: contain; }
  /* The two screens together are 1.01 times as wide as they are tall, plus the gap: the
     heights below keep them inside their box at any width. */

  /* The tile: the whole stage is the link to the large images, which viewer.js opens (C19) */
  .c-pair-tile {
    container: c-pair-tile / inline-size;
    display: block;
    inline-size: 100%;
    padding: var(--s4);
    border-radius: var(--r-soft);
    background-color: var(--shape-2);
    cursor: default;
    -webkit-tap-highlight-color: transparent;
  }
  .c-pair-tile.is-ready { cursor: zoom-in; }   /* only once viewer.js can open it */
  .c-pair-tile > .c-pair-phones { block-size: min(20rem, (100cqi - var(--s4)) / 1.02); }
  /* Hover, focus and press deepen the stage: colour only, nothing moves */
  .c-pair-tile { transition: background-color var(--d-fast) var(--ease-out); }
  .c-pair-tile:is(:focus-visible, .is-hover, .is-focus) { background-color: color-mix(in srgb, var(--shape-2) 82%, var(--fg)); }
  @media (hover: hover) {
    .c-pair-tile:hover { background-color: color-mix(in srgb, var(--shape-2) 82%, var(--fg)); }
  }
  .c-pair-tile:is(:active, .is-active) { background-color: color-mix(in srgb, var(--shape-2) 70%, var(--fg)); }
  .c-pair-tile:is(:focus-visible, .is-focus) { outline: var(--focus-width) solid var(--focus); outline-offset: var(--focus-offset); border-radius: var(--r-soft); }
}
/* ==== end carousel and dialog ==== */

/* ==== Image viewer (WP10b, REDESIGN-PLAN.md §6.5, C13, C14): fill inside this block only ==== */
@layer components {
  /* The site dialog, near full screen (viewer.js). The box is the whole viewport and is
     transparent; the ink backdrop is the surface. Its parts sit on a grid that the box's own
     width lays out: on phones close on top and the controls in one row under the image; from
     761px previous and next stand either side of it. */
  .c-viewer {
    inline-size: 100%;
    block-size: 100%;
    max-block-size: none;
    border-radius: 0;
    overflow: hidden;
    background-color: transparent;
  }
  /* Frosted glass behind the image: the page is blurred a little everywhere (the backdrop)
     and more towards the edges (::before, a progressive blur through a radial mask), under an
     ink gradient that is lighter round the image, darker at the edges and the foot where the
     controls sit, with a faint sheen from the top left (::after). Nothing here moves: the page
     behind is locked, so the blur is drawn once. Solid ink where transparency is reduced or
     backdrop blur is missing. */
  .c-viewer::backdrop {
    background-color: color-mix(in srgb, var(--c-ink) 68%, transparent);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
  }
  .c-viewer::before,
  .c-viewer::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
  }
  .c-viewer::before {
    -webkit-backdrop-filter: blur(32px);
    backdrop-filter: blur(32px);
    -webkit-mask-image: radial-gradient(ellipse 75% 70% at 50% 45%, transparent 35%, var(--c-ink) 100%);
    mask-image: radial-gradient(ellipse 75% 70% at 50% 45%, transparent 35%, var(--c-ink) 100%);
  }
  .c-viewer::after {
    background:
      linear-gradient(155deg, color-mix(in srgb, var(--c-bone) 8%, transparent), transparent 40%),
      linear-gradient(to bottom, transparent 65%, color-mix(in srgb, var(--c-ink) 30%, transparent)),
      radial-gradient(ellipse 80% 75% at 50% 45%, color-mix(in srgb, var(--c-ink) 30%, transparent), color-mix(in srgb, var(--c-ink) 65%, transparent));
  }
  .c-viewer-inner { position: relative; z-index: 1; }
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .c-viewer::backdrop { background-color: color-mix(in srgb, var(--c-ink) 90%, transparent); }
  }
  @media (prefers-reduced-transparency: reduce) {
    .c-viewer::backdrop { background-color: var(--c-ink); -webkit-backdrop-filter: none; backdrop-filter: none; }
    .c-viewer::before { display: none; }
  }
  .c-viewer-inner {
    display: grid;
    grid-template-columns: var(--target) minmax(0, 1fr) var(--target);
    grid-template-rows: var(--target) minmax(0, 1fr) var(--target);
    grid-template-areas:
      ".     .      close"
      "stage stage  stage"
      "prev  status next";
    gap: var(--s3);
    block-size: 100%;
    padding: max(var(--s3), env(safe-area-inset-top)) max(var(--s3), env(safe-area-inset-right)) max(var(--s3), env(safe-area-inset-bottom)) max(var(--s3), env(safe-area-inset-left));
  }
  @container c-dialog (width >= 761px) {
    .c-viewer-inner {
      grid-template-areas:
        ".    .      close"
        "prev stage  next"
        ".    status .";
      gap: var(--s4) var(--s5);
      padding: max(var(--s4), env(safe-area-inset-top)) max(var(--s5), env(safe-area-inset-right)) max(var(--s4), env(safe-area-inset-bottom)) max(var(--s5), env(safe-area-inset-left));   /* a phone on its side */
    }
    .c-viewer-inner > :is(.c-viewer-prev, .c-viewer-next) { align-self: center; }
  }
  .c-viewer-inner > [hidden] { display: none; }   /* a single image: no previous, next or counter */
  .c-viewer-close { grid-area: close; justify-self: end; }
  .c-viewer-close-icon { inline-size: 1.25rem; block-size: 1.25rem; }   /* an icon, so it sits on the centre whatever the font's metrics */
  .c-viewer-prev { grid-area: prev; }
  .c-viewer-next { grid-area: next; justify-self: end; }
  .c-viewer-status { grid-area: status; justify-self: center; align-self: center; }
  .c-viewer-inner:has(> .c-viewer-prev[hidden]) > .c-viewer-stage { grid-row: 2 / -1; }   /* the image takes the controls' row */

  /* Stage: the image is fitted inside it and never enlarged past its own size. The stage
     keeps vertical scrolling and pinch zoom for the browser; viewer.js reads sideways swipes.
     A tap on the empty stage closes, so it shows the zoom-out cursor and the image does not. */
  .c-viewer-stage {
    grid-area: stage;
    container: c-viewer-stage / size;
    display: grid;
    place-items: center;
    min-block-size: 0;
    overflow: auto;
    overscroll-behavior: contain;
    touch-action: pan-y pinch-zoom;
    cursor: zoom-out;
    border-radius: var(--r-soft);
  }
  .c-viewer-stage:focus-visible { outline: var(--focus-width) solid var(--focus); outline-offset: calc(-1 * var(--focus-width)); }
  .c-viewer-image {
    inline-size: auto;
    block-size: auto;
    max-inline-size: 100cqi;
    max-block-size: 100cqb;
    border-radius: var(--s2);
    background-color: var(--c-paper);      /* a transparent file (Ayala's mission design) shows on paper, not the blurred page */
    color: var(--c-ink);                   /* a file that fails shows its alt text in ink on that paper, not the field's bone (C88) */
    cursor: default;
    user-select: none;
    -webkit-user-drag: none;
  }
  .c-viewer-image:not([src]) { visibility: hidden; }
  /* A long capture fills the width (up to its own) and scrolls from the top */
  .c-viewer-stage.is-tall { place-items: start center; }
  .c-viewer-stage.is-tall > .c-viewer-image { max-block-size: none; }
  /* A wide image on a portrait stage (a phone) fills the stage's height, up to its own, and
     pans sideways from its start; the finger pans it, so a sideways swipe no longer steps
     (the buttons do). viewer.js decides (isWide). */
  .c-viewer-stage.is-wide { place-items: center start; touch-action: pan-x pan-y pinch-zoom; }
  .c-viewer-stage.is-wide > .c-viewer-image { max-inline-size: none; }

  /* Links the viewer has taken over show the zoom-in cursor */
  .is-ready[aria-haspopup="dialog"] { cursor: zoom-in; }
}
/* ==== end image viewer ==== */

/* ==== The line (WP10, REDESIGN-PLAN.md §6.3): fill inside this block only ==== */
@layer components {
  /* One route per page, drawn by line.js. Each section it runs through holds one
       <svg class="c-line" data-line aria-hidden="true" focusable="false"><path class="c-line-path"/></svg>
     as its first child. The SVG sits behind everything in its section (pictures, shapes, text),
     and line.js sizes it to the route's width and the section's band. The stroke takes the
     field's --line role (tokens.css): ink 35% on bone, ink 60% on blush, bone 50% on cobalt,
     none on ink. Nodes are the marks of the labels that carry data-line-node. */

  /* The rail's distance left of a text column (C40: more space than the first 24px). Wide: 48px,
     or the page margin less 16px when that is less, so the line keeps 16px from the window's
     edge (48px at 1440, 30px at 1024, 18px at 761). Below 761px it stays in the margin: 60% of it,
     12px on a phone, with swings of about 3px (line.js), so 9px or more from the text and 5px or
     more from the edge. Set on the section, so the no-JS connector below has the same length.
     Registered as a length, so line.js reads it in pixels. */
  @property --line-rail {
    syntax: "<length>";
    inherits: true;
    initial-value: 0px;
  }

  /* A section that holds the line is its box and its stacking context. line.js may add a bridge
     SVG (data-line-bridge) to the parent of two sections, for the margin between them, so that
     parent is one too. */
  :where(:has(> .c-line)) {
    --line-rail: min(var(--s7), var(--margin) - var(--s4));
    position: relative;
    isolation: isolate;
  }
  @media (width < 761px) {
    :where(:has(> .c-line)) { --line-rail: calc(var(--margin) * 0.6); }
  }
  .c-line {
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    max-inline-size: none;    /* base.css caps svg at 100%; the line spans the whole route */
    z-index: -1;              /* behind the section's pictures and text (§6.3) */
    overflow: hidden;
    pointer-events: none;
  }
  /* In a grid band, an absolute child placed in grid columns measures from those columns.
     Layout rules such as `.c-project--stack.c-project--flip > :not(.c-project-media)` place every
     child, so the line repeats its class to outrank them (0,4,0) and stays on the band's own box. */
  .c-line.c-line.c-line.c-line { grid-area: auto; }
  .c-line:not(.is-drawn) { inline-size: 0; block-size: 0; }
  .c-line-path {
    fill: none;
    stroke: var(--line);
    stroke-width: var(--ring);
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  /* The end of a line (C80): a small ring, ground inside and the text colour round it, as the
     node marks are drawn, so it reads as the line's end, not a stray dot. None on ink, where the
     field has no line. */
  .c-line-knot { fill: var(--ground); stroke: var(--fg); stroke-width: var(--ring); }
  .f-ink .c-line-knot { visibility: hidden; }

  /* Node and label are one object (§6.3, §6.5): on the line, the plain credit mark becomes a
     target like the home marks (a ground gap and a ring in the text colour); the ring credit is
     already a ring. line.js scales the mark through --line-node when the line arrives. */
  :where(:has(> .c-line)) .c-credit:not(.c-credit--ring)::before {
    box-shadow: 0 0 0 var(--node-gap, 3px) var(--ground), 0 0 0 calc(var(--node-gap, 3px) + var(--ring)) var(--fg);
    margin-inline: calc(var(--node-gap, 3px) + var(--ring));
  }
  :where(:has(> .c-line)) :is(.c-credit, .c-project-credit, .c-clients-title, .c-work-head, .c-about-title)::before {
    scale: var(--line-node, 1);
  }

  /* Without JS (or until the route is drawn), each node shows a short static connector: the
     line's stub, running from the margin side into the node. */
  :where(:has(> .c-line:not(.is-drawn))) :is(.c-credit, .c-project-credit, .c-clients-title, .c-work-head, .c-about-title) {
    position: relative;
  }
  :where(:has(> .c-line:not(.is-drawn))) :is(.c-credit, .c-project-credit, .c-clients-title, .c-work-head, .c-about-title)::after {
    content: "";
    position: absolute;
    inset-inline-end: 100%;
    inset-block-start: calc(0.5lh - var(--ring) / 2);
    inline-size: var(--line-rail);
    border-block-start: var(--ring) solid var(--line);
  }
}
/* ==== end the line ==== */
