/* ==================================================================
   Shared stylesheet for every page. Single source of truth: change a
   token or a component here and it lands on all pages at once.
   ================================================================== */

/* ==================================================================
     DESIGN TOKENS  — named to match your Figma styles
     ================================================================== */
  :root {
    /* Color styles */
    --background-primary:   #F9F9F9;
    --background-secondary: #FBFBFB;
    --label-primary:        #000000;
    --label-secondary:      #35353B;   /* body copy + side-panel intro (Marina, 2 Oct; kept 5 Oct — 11.6:1 on #F9F9F9; #2C2C30 and #2E2E33 tried 5 Oct) */
    --fill-purple:    #493075;   /* deep purple (Marina, 9 Oct): Unlock button fill; white label 10.7:1 */
    --fill-olive:     #697500;   /* olive (Marina, 9 Oct; was #59653D): kept as an option, not in use (Unlock button moved to fill-purple, 9 Oct); white label 5.1:1 */
    --label-tertiary:       #4D4D4D;   /* was --label-sec until 2 Oct; was #444A56 "label sec 2.0" (22 Sep) */

    /* A hairline used for the avatar stroke, nav divider, card borders */
    --side-panel:  372px;            /* side panel width; nav divider follows it */
    /* One stroke for everything. The 8% hairline and the #D8D8D8 card border
       were within a couple of percent of this, so they collapsed into it. */
    --border-primary:   rgba(0, 0, 0, 0.15);
    --fill-primary: #F3F3F3;         /* quiet filled surfaces: exhibit canvases, the eye button's hover disc (side-panel chips moved to fill-badge 5 Oct) */
    --label-disabled: #90959A;       /* text on disabled controls (contrast-exempt per WCAG 1.4.3); Marina, 5 Oct (was #8C9196) */
    --fill-badge:   #F0F0EE;         /* every chip (side-panel chips, code chips, colour chips) and disabled controls (5 Oct). Was #D6D6D6 */
    --fill-lavender:   #F5F8FF;      /* light lavender: tint panel skin (was --fill-tertiary until 9 Oct; before that the old accent token) */
    --fill-tertiary: #F1F2F6;      /* Firstday data canvas ground (was --fill-quaternary until 9 Oct; new 22 Sep) */
    --fill-secondary: #EDEDEC;       /* frame ground, HomeAds canvases */

    /* Chip metrics. One size only: the 32px chip. */
    --chip-h:         32px;
    --chip-radius:    16px;
    --chip-icon:      28px;
    --chip-glyph:     16px;
    --chip-gap:        5px;
    --chip-pad-right: 10px;
    --icon-success: #20A740;         /* presence / success indicator */

    /* Grid background — case studies page only. Two identical layers: a
       resting one, and a brighter one masked to a circle under the cursor. */
    --grid-size:     60px;
    --icon-arrow-forward: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linejoin='round' stroke-linecap='round'><path d='M1.25 8.25H9.75'/><path d='M9.75 3.5L14.75 8.25L9.75 13Z'/></svg>");   /* 16 x 16 forward arrow, hollow head (Marina, 8 Oct): links and buttons only; list bullets keep the dashed glyph */
    --grid-line:     rgba(0, 0, 0, 0.05);
    --grid-line-lit: rgba(0, 0, 0, 0.30);
    --gate-grid-line-lit: rgba(0, 0, 0, 0.11);   /* locked Firstday screen only: lines near the cursor darken slightly (Marina, 8 Oct) */
    --grid-cross:    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='60' height='60'><path d='M30 25v10M25 30h10' stroke='black' stroke-width='1'/></svg>");
    --grid-reveal:   190px;
    /* Locked screen hover (Marina, 8 Oct): length = how far a lit line runs
       along itself from the cursor; spread = how many neighbouring lines
       catch the light. Horizontal lines run long, vertical lines short. */
    --grid-h-length: 900px;
    --grid-h-spread: 130px;
    --grid-v-length: 110px;
    --grid-v-spread: 190px;
    --label-quaternary: #676C70;     /* H1 descriptor (was --label-tertiary until 2 Oct) */

    /* Faux semibold for hover states. Thickens the Regular drawing with two
       zero-blur horizontal shadows, so the glyph advance widths never change
       and nothing shifts. Use this INSTEAD of font-weight whenever a hover or
       toggle would otherwise reflow the layout. Real font-weight stays the
       rule for permanent states, where nothing moves anyway. */
    --faux-bold: 0.22px 0 0 currentColor, -0.22px 0 0 currentColor;

    /* Card focus-hover effect (hover one → the OTHER cards' images blur) */
    --card-blur:  6px;    /* blur applied to non-hovered card images only */
    --card-zoom:  1.012;  /* hover image grow ≈ an 8→6px padding tighten (scale, no reflow) */
    --card-ease:  0.35s;  /* transition duration */

    /* Space above a list, measured from the paragraph that introduces it.
       This margin collapses against that paragraph's 16px bottom margin,
       so the value here IS the finished gap, not an addition to it.
       One number, every list, both pages. */
    --list-gap-top: 16px;

    /* Space above a heading. H2 stays clear of H3 so proximity, not size
       alone, shows nesting: a section break reads bigger than a subhead. */
    --h2-gap-top: 60px;
    --h3-gap-top: 44px;

    /* Reading measure for body text and captions. Images run wider (1128). */
    --text-measure: 696px;

    /* Space around a figure. Top collapses against the preceding block,
       so this value is the finished gap. Bottom measures from the caption
       when there is one, not from the image. */
    --figure-gap-top: 60px;
    --figure-gap-bottom: 52px;

    /* Glass tile. Figma: Light -45deg 75%, Refraction 1, Depth 17,
       Dispersion 4, Frost 1.5, Splay 0 + drop shadow 0/3/22/0 #000 5%.
       Only frost and the highlight survive the translation to CSS —
       see .exhibit__glass for what the tint and rim are matched to. */
    --glass-radius: 16px;
    --glass-frost:  1.5px;
    /* Body: darker than the ground at the top-left, LIGHTER at the
       bottom-right. Measured on the frame: L229 -> L246 across a ground
       of 239. A plain white translucent plate gets this backwards. */
    --glass-tint: linear-gradient(135deg,
                    rgba(0,0,0,0.045)   0%,
                    rgba(0,0,0,0)      45%,
                    rgba(255,255,255,0.50) 100%);
    /* Specular rim: the light is at -45deg, so the highlight lives on the
       TOP and LEFT edges and all but vanishes on the bottom and right.
       Conic angles run clockwise from 12 o'clock: 0 top, 90 right,
       180 bottom, 270 left. The dip at 315 is the null at the corner
       nearest the light, which the frame has and a plain ring does not. */
    --glass-rim: conic-gradient(
                   rgba(255,255,255,0.95)   0deg,   /* top edge, centre */
                   rgba(255,255,255,0.95)  18deg,
                   rgba(255,255,255,0.10)  60deg,   /* right edge: nothing */
                   rgba(255,255,255,0.10) 210deg,   /* bottom edge: nothing */
                   rgba(255,255,255,0.95) 238deg,   /* left edge lights up */
                   rgba(255,255,255,0.95) 305deg,
                   rgba(255,255,255,0.25) 322deg,   /* null at the top-left corner */
                   rgba(255,255,255,0.95) 342deg,
                   rgba(255,255,255,0.95) 360deg);
    --glass-shadow: 0 3px 22px rgba(0,0,0,0.05);

    --font: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  }

  * { box-sizing: border-box; }

  html, body {
    margin: 0;
    padding: 0;
    min-height: 100%;
  }

  body {
    background: var(--background-primary);
    color: var(--label-primary);
    font-family: var(--font);
    -webkit-font-smoothing: antialiased;
  }

  /* ==================================================================
     TEXT STYLES  — class names match your Figma text styles 1:1
     ================================================================== */
  /* 14 / 20 / -0.09. Two weights of one size: -bold (SemiBold 600) for
     ACTIVE states and headings (name, Expertise, card links), plain for
     INACTIVE ones (nav, back link, both crumbs). Replaced label-15 / label-15-bold
     (15 / 20 / -0.2) on 24 Sep; label-15 no longer exists (Marina). */
  .label-14 {
    font-weight: 400;
    font-size: 14px;
    line-height: 20px;
    letter-spacing: -0.09px;
  }
  .label-14-bold {
    font-weight: 600;
    font-size: 14px;
    line-height: 20px;
    letter-spacing: -0.09px;
  }
  .label-13 {           /* 13 / 16 / 0  — was .label-small, before that .subtext */
    font-weight: 400;
    font-size: 13px;
    line-height: 16px;
    letter-spacing: 0;
  }
  .caption-1 {          /* 14 / 18 / -0.09 (tracking was 0 until 3 Oct) — was called .label-small */
    font-weight: 400;
    font-size: 14px;
    line-height: 18px;
    letter-spacing: -0.09px;
  }
  .mono-body {          /* Geist Mono 14 / 16 / -1  — annotation body copy.
                           Was .caption-2; renamed Sep 2026. Changed from Geist
                           sans at the same time; deliberate. The -1px tracking
                           belongs to this style only, not to the mono labels. */
    font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-weight: 400;
    font-size: 14px;
    line-height: 16px;
    letter-spacing: -1px;
  }
  .body-large {
    font-weight: 400;      /* Regular — was Light 22/24/-0.7 (Marina, 22 Sep) */
    font-size: 16px;
    line-height: 18px;
    letter-spacing: -0.09px;   /* body-regular's tracking */
  }
  /* The about text reads in label-secondary (2 Oct); the bold lead-in and the braces stay
     label-primary. `.sidepanel .body-large` outranks `.sidepanel p`. */
  .sidepanel .body-large { color: var(--label-secondary); }   /* 2 Oct */
  .body-large strong,
  .body-large__brace { color: var(--label-primary); }
  /* The lead-in sits at body-bold's weight without inheriting its 16/24. */
  .body-large strong { font-weight: 600; }
  /* The braces that open and close the about paragraph. Ornament, so both
     are aria-hidden in the markup; SemiBold to match the lead-in. */
  .body-large__brace { font-weight: 600; }
  .body-regular {
    font-weight: 400;
    font-size: 16px;
    line-height: 24px;
    letter-spacing: -0.09px;
  }
  /* Body copy colour (Marina, 2 Oct): body-regular text in every case study
     reads in label-secondary; bold lead-ins and phrases stay label-primary. */
  .article .body-regular { color: var(--label-secondary); }
  .article .body-regular .body-bold { color: var(--label-primary); }
  .body-bold {
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    letter-spacing: -0.09px;
  }
  /* .mono-label-14-bold — Geist Mono Medium 14 / 16 / 0.
     One job: annotation-2 headlines. Every other canvas label and
     annotation is .mono-body (Marina, 21 Sep 2026).
     Was .naming-mono until Sep 2026. A Regular sibling, .mono-label-regular,
     existed briefly and was dropped; .mono-body took its place. */
  .mono-label-13 {      /* Geist Mono 13 / 16 / -0.6 regular — new 23 Sep.
                           Specialty chips and the current-page breadcrumb. */
    font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-weight: 400;
    font-size: 13px;
    line-height: 16px;
    letter-spacing: -0.6px;
  }
  .mono-label-14-bold {
    font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-weight: 500;
    font-size: 14px;
    line-height: 16px;
    letter-spacing: 0;
    color: var(--label-primary);
  }

  /* Visible to screen readers only. Used for headings the design conveys
     visually (layout, grouping) but that have no on-screen label. */
  .sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  .h1 {                 /* Geist Bold 44 / 48 / -0.5 */
    font-weight: 700;
    font-size: 44px;
    line-height: 48px;
    letter-spacing: -0.5px;
    margin: 0;
  }
  /* Two-tone heading: the project name stays label-primary, the descriptor
     that follows drops to --label-quaternary. Same size and weight, colour is
     the only separation. */
  .h1__descriptor { color: var(--label-quaternary); }

  .h2 {                 /* Geist SemiBold 22 / 26 / -0.25 */
    font-weight: 600;
    font-size: 22px;
    line-height: 26px;
    letter-spacing: -0.25px;
  }

  .h3 {                 /* Geist SemiBold 17 / 22 / -0.2 */
    font-weight: 600;
    font-size: 17px;
    line-height: 22px;
    letter-spacing: -0.2px;
  }
  .h4 {                 /* Geist SemiBold 16 / 24 / -0.09 — new 23 Sep */
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    letter-spacing: -0.09px;
  }

  /* ==================================================================
     INTERACTIVE DOT BACKGROUND  (fixed, full-viewport, behind all)
     ================================================================== */
  #dotGrid {
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100vh;
    display: block;
    z-index: 0;
    pointer-events: none;
  }

  /* ---- Grid background — case studies page only --------------------
     A resting grid, plus a lit layer that is masked TWICE by nesting:
     the wrapper is cut to a soft circle under the cursor, and its child is
     cut to a repeating "+" so only the intersections survive. A mask is CSS
     for "show this layer only where the mask is opaque"; nesting two of them
     means a pixel has to satisfy both. Moving the circle's centre is the
     whole interaction — no canvas, nothing repainted per element. */
  .grid--base {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    color: var(--grid-line);
    background-image:
      repeating-linear-gradient(to right,  currentColor 0 1px, transparent 1px var(--grid-size)),
      repeating-linear-gradient(to bottom, currentColor 0 1px, transparent 1px var(--grid-size));
  }
  .grid-lit {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    -webkit-mask-image: radial-gradient(circle var(--grid-reveal) at var(--x, -9999px) var(--y, -9999px),
                          #000 0%, rgba(0,0,0,.6) 45%, transparent 100%);
    mask-image: radial-gradient(circle var(--grid-reveal) at var(--x, -9999px) var(--y, -9999px),
                  #000 0%, rgba(0,0,0,.6) 45%, transparent 100%);
  }
  /* Only the crossing points pick up. The background-colour supplies the
     colour; the mask supplies the "+" shape, offset half a tile so the marks
     land ON the lines rather than in the middle of each cell. */
  .grid-lit__cross {
    position: absolute;
    inset: 0;
    background-color: var(--grid-line-lit);
    -webkit-mask-image: var(--grid-cross);
    mask-image: var(--grid-cross);
    -webkit-mask-size: var(--grid-size) var(--grid-size);
    mask-size: var(--grid-size) var(--grid-size);
    -webkit-mask-position: calc(var(--grid-size) / -2 + 0.5px) calc(var(--grid-size) / -2 + 0.5px);
    mask-position: calc(var(--grid-size) / -2 + 0.5px) calc(var(--grid-size) / -2 + 0.5px);
    -webkit-mask-repeat: repeat;
    mask-repeat: repeat;
  }
  /* Alternative: the whole lines light up. Swap the child element to switch. */
  .grid-lit__lines {
    position: absolute;
    inset: 0;
    color: var(--grid-line-lit);
    background-image:
      repeating-linear-gradient(to right,  currentColor 0 1px, transparent 1px var(--grid-size)),
      repeating-linear-gradient(to bottom, currentColor 0 1px, transparent 1px var(--grid-size));
  }
  /* Locked Firstday screen only (Marina, 8 Oct): the lines themselves darken
     a little near the cursor, no crosses. Horizontal and vertical lines are
     lit separately, each by its own ellipse: the horizontal lines stay lit
     far to the left and right (--grid-h-length), the vertical lines only a
     short way up and down (--grid-v-length). The Case studies page keeps its
     crosses above. */
  .gate-grid-lit {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
  }
  .gate-grid-lit__h,
  .gate-grid-lit__v {
    position: absolute;
    inset: 0;
    color: var(--gate-grid-line-lit);
  }
  .gate-grid-lit__h {
    background-image: repeating-linear-gradient(to bottom, currentColor 0 1px, transparent 1px var(--grid-size));
    -webkit-mask-image: radial-gradient(ellipse var(--grid-h-length) var(--grid-h-spread) at var(--x, -9999px) var(--y, -9999px), #000 0%, rgba(0,0,0,.55) 40%, transparent 100%);
    mask-image: radial-gradient(ellipse var(--grid-h-length) var(--grid-h-spread) at var(--x, -9999px) var(--y, -9999px), #000 0%, rgba(0,0,0,.55) 40%, transparent 100%);
  }
  .gate-grid-lit__v {
    background-image: repeating-linear-gradient(to right, currentColor 0 1px, transparent 1px var(--grid-size));
    -webkit-mask-image: radial-gradient(ellipse var(--grid-v-spread) var(--grid-v-length) at var(--x, -9999px) var(--y, -9999px), #000 0%, rgba(0,0,0,.55) 40%, transparent 100%);
    mask-image: radial-gradient(ellipse var(--grid-v-spread) var(--grid-v-length) at var(--x, -9999px) var(--y, -9999px), #000 0%, rgba(0,0,0,.55) 40%, transparent 100%);
  }
  @media (prefers-reduced-motion: reduce) { .gate-grid-lit { display: none; } }
  @media (prefers-reduced-motion: reduce) { .grid-lit { display: none; } }

  /* Everything real sits above the dots */
  .app { position: relative; z-index: 1; }

  /* ==================================================================
     TOP NAV  — height 60, pad-left 20, pad-right 44, background-secondary
     ================================================================== */
  .topnav {
    position: sticky;             /* stays put as the page scrolls */
    top: 0;
    z-index: 10;
    height: 60px;
    padding-left: 20px;
    padding-right: 44px;
    background: var(--background-secondary);
    border-bottom: 1px solid var(--border-primary);
    display: flex;
    align-items: center;
    justify-content: flex-start;
  }
  /* Vertical divider, 44px tall and centered in the 60px bar. It marks the
     RIGHT EDGE OF THE IDENTITY BLOCK, not the side panel's column, which is
     why it can survive on tablets and phones where the panel stacks away.

     On desktop the two are the same pixel: the identity is sized
     calc(var(--side-panel) - 20px) and starts at the nav's 20px padding, so
     it ends at 372 and this line — pulled back 1px to overlap its last
     pixel — occupies 371-372, exactly where the absolutely positioned
     version used to sit. Below 1000 the identity hugs its content and the
     line simply travels in with it. */
  .topnav__divider {
    flex: none;
    align-self: center;
    width: 1px;
    height: 44px;
    margin-left: -1px;
    background: var(--border-primary);
  }

  /* --- Avatar + identity block --- */
  .identity {
    display: flex;
    align-items: center;
    gap: 12px;               /* horizontal spacing avatar ↔ text = 12px */
    /* + the nav's 20px left padding = the side panel width, so the divider
       lands exactly on the column edge */
    flex: 0 0 calc(var(--side-panel) - 20px);
  }
  .avatar {
    position: relative;
    width: 44px;
    height: 44px;
    border-radius: 16px;
    /* inner stroke: 15% black, 0.3px, INSET so it sits inside the bounds */
    box-shadow: inset 0 0 0 0.3px var(--border-primary);
    flex: none;
  }
  .avatar__photo {
    width: 100%;
    height: 100%;
    display: block;
    object-fit: cover;
    border-radius: inherit;
  }
  /* Presence dot: 10×10 flush to the frame's right and bottom. Its 4px white
     ring is an OUTSIDE stroke, so it overhangs the rounded corner by 4px.
     Nothing clips it — .avatar has no overflow rule, the photo rounds itself. */
  .avatar__status {
    position: absolute;
    right: 0;
    bottom: 0;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--icon-success);
    box-shadow: 0 0 0 2px #ffffff;   /* 2px outside white stroke */
  }
  /* Gentle "live" pulse — a thin ring radiating out from the dot and fading. */
  .avatar__status::after {
    content: "";
    position: absolute;
    inset: -1px;
    border-radius: 50%;
    border: 1.5px solid #2FA84F;
    opacity: 0;
    animation: statusPulse 2.4s cubic-bezier(0.2, 0.6, 0.3, 1) infinite;
  }
  @keyframes statusPulse {
    0%   { transform: scale(1);   opacity: 0.55; }
    70%  { opacity: 0; }
    100% { transform: scale(2.4); opacity: 0; }
  }
  @media (prefers-reduced-motion: reduce) {
    .avatar__status::after { animation: none; }
  }
  .identity__text {
    display: flex;
    flex-direction: column;
    gap: 0;                  /* vertical spacing name ↔ location = 0 */
  }
  .identity__name { color: var(--label-primary); }
  .identity__loc  { color: var(--label-tertiary); }

  /* --- Nav links --- */
  .nav-links {
    display: flex;
    align-items: center;
    margin-left: 20px;            /* 20px clear of the divider line */
  }
  .nav-link {
    position: relative;           /* anchors the invisible 60px hit area */
    display: inline-flex;
    align-items: center;
    gap: 8px;                     /* label → arrow */
    height: 20px;                 /* 23 Sep: one box height for every text link,
                                     so every focus ring matches (label-13's own
                                     line is 16; it centers inside the 20) */
    color: var(--label-primary);
    text-decoration: none;
  }
  /* Invisible 60px-tall target, centered on the link: the click area still
     fills the bar, while the focus ring hugs the 20px link like Back's. */
  .nav-link::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: 60px;
  }
  /* Default is regular; hover thickens with --faux-bold rather than a real
     weight change, so the label measures identically in both states. */
  .nav-link__label { display: inline-block; }
  /* ---- Keyboard focus -------------------------------------------------
     Only shows for keyboard users (:focus-visible), never on mouse click. */
  a:focus-visible {
    outline: 2px solid var(--label-primary);
    outline-offset: 2px;
    border-radius: 4px;
  }
  /* The card's link is stretched over the whole card, so the default ring
     would draw around the small text. Put it on the card instead. */
  .card__link:focus-visible { outline: none; }
  .card:has(.card__link:focus-visible) {
    outline: 2px solid var(--label-primary);
    outline-offset: 2px;
  }

  /* Default = regular weight. Active (the page you are on) = semibold,
     no arrow, and no hover response, because it leads nowhere. */
  /* Weight comes from the style: .label-14 inactive, .label-14-bold active
     (the swap happens in site-chrome.js). */
  .nav-link:hover .nav-link__label { text-shadow: var(--faux-bold); }
  /* Active is permanent, so it uses the real SemiBold cut — better drawn
     counters — and never hovers into anything. */
  .nav-link--active:hover .nav-link__label { text-shadow: none; }

  /* --- Breadcrumbs ---------------------------------------------------
     An <ol> so the trail announces as a list; the double colon (::, code for "inside") is a separate
     aria-hidden span so it is never read aloud. Everything sits in
     label-primary (Marina, 22 Sep). Both crumbs are label-14 (24 Sep;
     the current page was label-13, before that Geist Mono 13). The current page is a
     link to itself —
     pressing it reloads the page. Both are .nav-link, so both get the
     60px-tall hit area and the faux-bold on hover. */
  .crumbs {
    display: flex;
    align-items: center;
    list-style: none;
    margin: 0;
    padding: 0;
  }
  .crumbs__item { display: flex; align-items: center; }
  .crumbs__dot { font-weight: 400; }   /* the · in the current crumb stays Regular inside the SemiBold name (Marina, 5 Oct) */
  .crumbs__sep {
    margin: 0 8px;                /* the nav's own label-to-glyph gap */
    /* Sized explicitly (inheriting took the body's 16). */
    font-size: 14px;              /* the double colon at 14, level with the label-14 crumbs (24 Sep; was 15) */
    line-height: 20px;
    color: var(--label-secondary);   /* label-secondary (Marina, 5 Oct; was label-primary) */
    font-weight: 700;             /* Bold (Marina, 5 Oct) */
  }
  /* The current-page crumb: label-14 like the root (Marina, 24 Sep; was
     label-13 with a 1px nudge down), regular weight, bold on hover and
     press like "Case studies" (Marina, 23 Sep). */
  /* Current page = active, so it uses label-14-bold (SemiBold) like the index's
     active nav link, and hover changes only the cursor (Marina, 27 Sep). */
  .crumbs__current:hover .nav-link__label,
  .crumbs__current:active .nav-link__label { text-shadow: none; }
  .crumbs__current { cursor: pointer; }
  /* "Case studies" crumb rests in label-tertiary; hover/press bring it to
     label-primary with the faux-bold (Marina, 3 Oct). */
  .crumbs__item:first-child .nav-link { color: var(--label-secondary); }   /* label-secondary (Marina, 5 Oct; was label-tertiary) */
  .crumbs__item:first-child .nav-link:hover,
  .crumbs__item:first-child .nav-link:active { color: var(--label-primary); }


  /* ==================================================================
     BODY LAYOUT  — side panel (--side-panel) + flexible main
     ================================================================== */
  /* Two-column grid. The H1 is placed in its own cell in the right column
     ABOVE the main region, which lets the markup run H1 -> aside -> cards
     so the heading outline descends properly, while the panel still renders
     on the left. */
  .layout {
    display: grid;
    grid-template-columns: var(--side-panel) minmax(0, 1fr);
    grid-template-rows: auto 1fr auto;   /* head, main, footer */
    min-height: calc(100vh - 60px);
  }
  .layout__head {
    grid-column: 2;
    grid-row: 1;
    padding: 44px 20px 0;      /* top 44 + 60px nav = H1 top at y 104 */
    min-width: 0;
  }
  /* A head that carries a back button starts 28 below the nav instead of 44.
     The back button is a utility control, not part of the title block, so it
     sits closer to the nav and further from the H1. Net effect on the H1 is
     nil: 28 + 20 + 60 is the same 108 as the old 44 + 20 + 44, so the title
     stays at y 168 and only the back button moves up 16.
     Scoped with :has() so the index — which has no back button — keeps its
     44. (On desktop the index head is re-laid out by the centering rule
     further down, so its H1 no longer sits at y 104.) */
  .layout__head:has(.back-row) { padding-top: 28px; }

  /* --- Side panel: 352 fixed, 20px L/R padding, body-large --- */
  .sidepanel {
    width: var(--side-panel);
    flex: none;
    padding: 28px 19px 59px 20px;   /* 19 right buys the 1px that lets all three
                                       domain chips sit on one row. 59 at the
                                       foot, not 60: the rule below the chips is
                                       the 60th pixel, exactly as the footer's
                                       hairline is the 60th of its band. That is
                                       what puts the two lines on one row. */
    display: flex;          /* lets the chips pin to the bottom of the panel */
    flex-direction: column;
    background: var(--background-secondary);
    border-right: 1px solid var(--border-primary);
    /* Pinned top to bottom: holds its place while the main column scrolls.
       align-self is required — a stretched flex item cannot be sticky. */
    position: sticky;
    top: 60px;                    /* sits directly under the sticky nav */
    align-self: start;            /* a stretched grid item cannot be sticky */
    height: calc(100vh - 60px);
    grid-column: 1;
    grid-row: 1 / -1;             /* runs past the footer, which is column 2 only */
  }
  .sidepanel p {
    margin: 0 0 44px;       /* minimum gap to the chips if the panel gets short */
    color: var(--label-primary);
  }

  /* --- Specialties badges --- */
  .specialties {
    margin-top: auto;             /* pin the chips to the bottom of the panel */
    margin-bottom: 0;             /* the panel's 44px bottom padding does the rest */
  }
  /* Domains then Skills (27 Sep): only the first group is pinned by
     margin-top:auto; the second follows 20px below it. */
  .specialties + .specialties { margin-top: 20px; }
  .specialties__title {
    margin: 0 0 8px;              /* heading → chips = 8 */
    color: var(--label-primary);
  }
  .badges {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 4px 8px;                 /* 4 between rows, 8 between chips */
  }
  /* Forced row break inside a chip list (Domains: Proptech + Healthcare start a row, 8 Oct).
     A row gap would also open above the empty break line, so lists with a break move the
     4px row spacing onto the chips (each chip 4 down, the list 4 up): rows stay 4 apart. */
  .badges__break { flex-basis: 100%; height: 0; }
  .badges:has(.badges__break) { row-gap: 0; margin-top: -4px; }
  .badges:has(.badges__break) > .badge { margin-top: 4px; }
  /* Solid chip: no outline. The fill separates it from the panel, and the
     icon sits in a black disc inset 2px from the chip's top, bottom and left
     (28 high − 24 disc = 2 either side, matching the 2px left padding). */
  .badge {
    position: relative;           /* anchors the dashed outline */
    display: inline-flex;
    align-items: center;
    gap: var(--chip-gap);         /* icon → text spacing */
    height: var(--chip-h);
    padding: 0 var(--chip-pad-right) 0 2px;   /* 2 left, token right */
    background: var(--fill-badge);   /* #F0F0EE, like the code and colour chips (Marina, 5 Oct; was fill-primary #F3F3F3) */
    border-radius: var(--chip-radius);        /* pill */
    color: var(--label-primary);
    white-space: nowrap;           /* type: .mono-label-13 in the markup */
  }
  /* The outline is an SVG stroke, not a CSS border, because CSS gives no
     control over dash and gap length. This is exactly 2 on, 2 off. */
  .badge__border {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }
  .badge__border rect {
    x: 0.5px;
    y: 0.5px;
    width: calc(100% - 1px);
    height: calc(100% - 1px);
    rx: calc(var(--chip-radius) - 0.5px);
    fill: none;
    stroke: var(--border-primary);
    stroke-width: 1px;
    stroke-dasharray: 2 2;
  }
  .badge__icon {
    flex: none;
    width: var(--chip-icon);
    height: var(--chip-icon);
    border-radius: 50%;
    background: #ffffff;
    /* inset so the ring sits inside the 24px disc and costs no layout space */
    box-shadow: inset 0 0 0 1px var(--border-primary);   /* #000000 @ 15% */
    color: var(--label-primary);   /* the glyph draws with currentColor */
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .badge__icon svg {              /* glyph inside the disc */
    width: var(--chip-glyph);
    height: var(--chip-glyph);
    display: block;
  }

  /* --- Main content --- */
  /* The main region wraps the title block and .main but draws no box of its
     own, so both stay separate grid items exactly as before (24 Sep). */
  .layout__main { display: contents; }
  .main {
    grid-column: 2;
    grid-row: 2;
    padding: 0 20px 44px;
    min-width: 0;
  }
  .main__heading { margin-bottom: 44px; }   /* H1 -> cards, and H1 -> Overview on a case study */

  /* ==================================================================
     FOOTER  — 60 tall, main column only
     ------------------------------------------------------------------
     The footer occupies the last grid row of the RIGHT column. The side
     panel spans every row, so it carries on down beside the footer and
     its own border keeps running — which is why the panel closes itself
     with .sidepanel__rule instead of the footer's edge crossing it.

     The hairline lives INSIDE the 60: the band is 60 from the line to the
     bottom of the page, not 61.

     Nothing here pins the footer to the window. It does not need to be
     pinned: `.layout` is at least the window height minus the nav, and
     the footer is its last row, so on a page whose content fits — the
     index — the footer lands on the bottom of the window by itself. On
     a case study the content is taller, so the row is pushed down and
     the footer scrolls into view at the end, which is what it should do.
     ================================================================== */
  .footer {
    position: relative;           /* anchors the hairline */
    grid-column: 2;
    grid-row: -2 / -1;            /* last row, whatever the row count is */
    height: 60px;
    padding-left: 20px;
    background: var(--background-secondary);   /* matches the side panel */
    display: flex;
    align-items: center;
  }
  /* The hairline runs the full width of the main column. Drawn as a
     pseudo-element rather than a border so it costs no layout and the
     full 60 stays available to centre the copy. */
  .footer::before {
    content: "";
    position: absolute;
    left: 0;                      /* edge to edge: panel's right edge to the window's */
    right: 0;
    top: 0;
    height: 1px;
    background: var(--border-primary);
  }
  .footer__copy {
    margin: 0;
    color: var(--label-tertiary);
  }

  /* The line that closes the side panel, level with the footer's top edge.
     The panel's content box is exactly 332 wide (372 - 20 - 19 - 1px
     border), so `width: 100%` IS the 332, already centred by the padding.
     `flex: none` stops the column stretching it. */
  .sidepanel__rule {
    flex: none;
    width: 100%;
    height: 1px;
    margin-top: 28px;             /* chips -> rule */
    background: var(--border-primary);
  }

  /* ---- Index: title + cards centered as one block -------------------
     Desktop only (the two-column layout). The head and the card row sit in
     their own auto rows between two flexible rows that split the leftover
     height equally, so the block is vertically centered in the space under
     the nav whatever the window height. The flexible rows never shrink below
     44px, so when the block is taller than the window (two card rows) it
     falls back to top-aligned with the original 44 above and below, and
     scrolls. Scoped to the index by :has(.card); case-study pages keep
     their layout. The side panel spans all four rows. */
  @media (min-width: 1001px) {
    .layout:has(.card) { grid-template-rows: minmax(44px, 1fr) auto auto minmax(44px, 1fr) auto; }
    /* Two-column cards (1001–1439): the footer is NOT pinned; it sits below
       the cards and scrolls into view at the end (Marina, 28 Sep). */
    .layout:has(.card) .layout__head { grid-row: 2; padding-top: 0; }
    .layout:has(.card) .main         { grid-row: 3; padding-bottom: 0; }
    .layout:has(.card) .sidepanel    { grid-row: 1 / -1; }
  }

  /* From 1440 the cards sit three in a row, so the block is short enough to
     fit between the nav and the footer at any sane window height. There the
     flexible rows drop their 44 floor and the block is centred exactly.

     The floor was the bug: the moment 44 above plus 44 below no longer fitted,
     the grid overflowed, the page started scrolling, and the sticky footer
     slid over the bottom of the cards. Without the floor the gaps simply
     close until they meet, and the block stays centred. */
  @media (min-width: 1440px) {
    .layout:has(.card) {
      grid-template-rows: minmax(0, 1fr) auto auto minmax(0, 1fr) auto;
    }
    /* Three-column cards only: the index footer stays on the bottom of the
       WINDOW, so its line and the side panel's rule share a row even in a
       short window (Marina, 28 Sep: pinned only in the 3-card layout). */
    .layout:has(.card) .footer { position: sticky; bottom: 0; z-index: 5; }
  }

  /* The frame holds heading + cards + footer.
     It's a grid of fixed 352px card columns — only as many as actually fit.
     `justify-content: center` centers that column GROUP, so the group is
     always centered no matter how many cards fit per row, while the cards
     themselves stay left-aligned inside it.
     The H1 and footer span the full column group, so their left edge lands
     exactly on the first card's left edge. */
  /* Cards flex between 316 and 352px wide.
     The column COUNT is set explicitly at each breakpoint, and the frame's
     max-width caps the cards at 352 for that count (n×352 + gutters).
     Below the cap the frame shrinks and the cards shrink with it, never
     going under 316 — that's where the next breakpoint takes over.
        3 columns ≥1440px  ·  2 columns ≥692px  ·  1 column below
     1440 is the smallest viewport that shows 3 cards, by design.      */
  .content-frame {
    max-width: 1840px;            /* 3 × 600 + 2 × 20 gutters: cards grow to 600 on large monitors */
    margin-inline: auto;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 20px;                    /* gutter */
  }
  @media (max-width: 1439px) {
    .content-frame {
      max-width: 884px;           /* 2 × 432 + 20 */
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }
  @media (max-width: 691px) {
    .content-frame {
      max-width: 432px;
      grid-template-columns: minmax(0, 1fr);
    }
  }
  .content-frame > .h1 { grid-column: 1 / -1; }

  .card {
    position: relative;
    cursor: pointer;              /* hand cursor across the whole card */
    border: 1px solid var(--border-primary);  /* 1px inside stroke, #D8D8D8 */
    border-radius: 26px;
    padding: 8px;                 /* card padding = 8px */
    background: #ffffff;          /* white card fill within the border */
    /* Column so the label can take up whatever height the row gives the
       card. Cards in a row are stretched to the tallest one; the spare
       height goes to the gap above the CTA, not below it. */
    display: flex;
    flex-direction: column;
  }

  .card__photo {
    transform-origin: center;
    transition: transform var(--card-ease) ease, filter var(--card-ease) ease;
  }
  /* Hovering ANY card blurs every card's IMAGE (labels never blur)... */
  .content-frame:has(.card:hover) .card__photo {
    filter: blur(var(--card-blur));
  }
  /* ...except the hovered one, which stays sharp and grows via scale.
     Scale (not padding) means no reflow, so the label stays perfectly still. */
  .content-frame:has(.card:hover) .card:hover .card__photo {
    filter: none;
    transform: scale(var(--card-zoom));
  }

  /* Touch devices have no hover, so nothing blurs or grows. */
  @media (hover: none) {
    .content-frame:has(.card:hover) .card__photo { filter: none; }
    .content-frame:has(.card:hover) .card:hover .card__photo { transform: none; }
  }
  /* Reduced-motion: keep the effect, drop the transition. */
  @media (prefers-reduced-motion: reduce) {
    .card__photo, .card__arrow { transition: none; }
  }
  .card__photo {
    aspect-ratio: 1.15;           /* near-square, measured from your screenshot */
    width: 100%;
    border-radius: 20px;          /* image corner radius */
    box-shadow: inset 0 0 0 0.3px var(--border-primary); /* inner stroke, 15% black, 0.3px */
    /* placeholder photo */
    background:
      linear-gradient(135deg, #ECECE6 0%, #DEDED7 100%);
    display: flex;
    align-items: center;
    justify-content: center;
    color: rgba(68, 74, 86, 0.45);
    overflow: hidden;
  }
  .card__photo svg { width: 40px; height: 40px; }
  .card__photo img {          /* real photo fills the frame, center-cropped */
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }

  /* Label block — its own padding, ON TOP OF the card's 8px padding */
  .card__label {
    padding: 20px 12px 19px 12px;   /* top 20, sides 12, bottom 19: link -> card's outer edge = 28 (19 + 8 card padding + 1 border) */
    flex: 1;                        /* absorbs the height the row adds */
    display: flex;
    flex-direction: column;
    align-items: flex-start;        /* the CTA keeps its own width */
  }
  .card__title { margin: 0; color: var(--label-primary); }
  .card__meta  { margin: 12px 0 20px; color: var(--label-secondary); }   /* label-secondary since 3 Oct */   /* H2 → description = 12; 20 is the MINIMUM to the CTA */
  .card__link  {
    display: inline-block;
    margin-top: auto;             /* pinned to the bottom: extra height grows the gap above, never the padding below */
    color: var(--label-primary);
    text-decoration: none;
  }
  /* Stretches the link's click target over the whole card, so the hand
     cursor is honest — clicking anywhere on the card follows this link. */
  .card__link::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
  }
  /* The arrow nudges 2px right on hover, instead of the label underlining.
     Glyph comes from ::before so the mark lives in CSS, next to the list
     marker it matches, rather than being retyped in every card's markup. */
  .card__arrow {
    display: inline-block;
    transition: transform 0.2s ease;
  }
  /* Decorative glyphs are silenced for screen readers with the "/ ''" alt
     syntax; the first line is the fallback for browsers that lack it. */
  .card__arrow::before { content: ""; display: inline-block; width: 16px; height: 16px; vertical-align: -3px; background: currentColor; -webkit-mask: var(--icon-arrow-forward) center / 16px 16px no-repeat; mask: var(--icon-arrow-forward) center / 16px 16px no-repeat; }   /* forward arrow icon (8 Oct; was the dashed glyph); -3 centres it on the label's capitals */
  .card:hover .card__arrow {
    transform: translateX(2px);
  }



  /* ==================================================================
     ARTICLE  — case study page. Reading column = --text-measure, images to 1128px.
     Both are centered, so images bleed wider than the text either side.
     ================================================================== */
  .article {
    max-width: 1128px;
    margin-inline: auto;
  }
  .article > :not(figure) {
    max-width: var(--text-measure);
    margin-inline: auto;
  }
  .article h2 { margin: var(--h2-gap-top) auto 20px; } /* 60 above, 20 to body */
  .article h3 { margin: var(--h3-gap-top) auto 16px; } /* 44 above, 16 to body */
  /* h4 — the rank below h3 (e.g. under "Constraint 2" in Firstday part 2).
     Its own style since 23 Sep (Marina); 28 above, 16 to body, so it reads
     as a step inside the h3 rather than a sibling of it. */
  .article h4 { margin: 28px auto 16px; }
  .article > :first-child { margin-top: 0; }
  .article p { margin: 0 auto 16px; }   /* paragraph spacing = 16 (body styles) */
  .article p:last-of-type { margin-bottom: 0; }
  /* Exhibits keep their own 8px: prose paragraph spacing stops at the figure.
     (Annotation-card and persona gaps inside figures relied on the old 8px.) */
  .article figure p { margin-bottom: 8px; }

  .article figure {
    margin: var(--figure-gap-top) auto var(--figure-gap-bottom);
    max-width: 1128px;
  }
  /* A figure that ends the article drops its bottom margin, so caption → footer
     is the same 44 as body copy → footer on every other page. */
  .article > figure:last-child { margin-bottom: 0; }
  .article figure img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: 12px;      /* standard image corner radius */
  }

  /* ---- Caption -----------------------------------------------------
     Sits in a <figcaption> under the image. Type comes from the
     .caption-1 text style, exactly as .bulleted-list takes its type
     from .body-regular, so this rule only sets color, measure and space.
     Held to the --text-measure reading column and centered under the image, so
     its left edge lands on the same line as every paragraph on the page.
     The figure's 52px bottom margin now measures from the caption, so
     adding one doesn't lengthen the gap to the next section. */
  .caption {
    max-width: var(--text-measure);
    margin: 20px auto 0;      /* 20 under the image; auto centers on the text column */
    color: var(--label-tertiary);
  }

  /* ---- Bulleted list ----------------------------------------------
     Named for what a screen reader announces for a <ul>.
     The base holds layout and spacing; the marker glyph comes from a
     variant class, so a new marker is a variant, never a new component.
     Variants:  .bulleted-list-arrow   =  dashed rightwards arrow
     margin-block only — margin-inline stays auto so the list keeps the
     reading-column centering from .article > :not(figure) */
  .bulleted-list { list-style: none; margin-block: var(--list-gap-top); padding: 0; }
  .bulleted-list li { position: relative; padding-left: 22px; }
  .bulleted-list li + li { margin-top: 8px; }   /* paragraph spacing = 8 */
  .bulleted-list li::before {
    position: absolute;
    left: 0;
    color: var(--label-primary);
  }
  .bulleted-list-arrow li::before { content: "\21E2"; content: "\21E2" / ""; }   /* silent: decorative */

  /* ---- Marked paragraph --------------------------------------------
     One emphasized statement standing alone in prose flow. Wears the
     same marker and 22px indent as .bulleted-list-arrow so it looks
     identical, but it is a <p>, not a list, so a screen reader does not
     announce a one-item list. Spacing comes from .article p (8px),
     because in the flow it IS a paragraph. */
  .marked-paragraph { position: relative; padding-left: 22px; }
  .marked-paragraph::before {
    content: "\21E2";
    content: "\21E2" / "";      /* silent: decorative */
    position: absolute;
    left: 0;
    color: var(--label-primary);
  }

  /* ---- Numbered list ----------------------------------------------
     A separate component, not a variant: <ol> carries different meaning
     and announces as a numbered list. Numerals sit in the same 22px
     hanging indent as the bullet markers, so both lists share a left edge. */
  .numbered-list {
    list-style: none;
    margin-block: var(--list-gap-top);
    padding: 0;
    counter-reset: numbered-list;
  }
  .numbered-list li {
    position: relative;
    padding-left: 22px;
    counter-increment: numbered-list;
  }
  .numbered-list li + li { margin-top: 8px; }
  /* An item built from paragraphs (a problem and its resolution) needs more
     air before the next item than the 8px separating the two paragraphs
     inside it. Keyed off :has() so no new class is needed. */
  .numbered-list li > p { margin: 0; }
  .numbered-list li > p + p { margin-top: 8px; }
  .numbered-list li:has(p) + li { margin-top: 20px; }
  .numbered-list li::before {
    content: counter(numbered-list) ".";
    position: absolute;
    left: 0;
    color: var(--label-primary);
  }

  /* ---- Numbered sections -------------------------------------------
     Headed subsections (an h3 + its paragraphs) numbered for scanning.
     NOT a list: the headings already carry the structure, so nothing is
     announced as "list, n items". Same 22px hanging indent as the lists.
     The number is visual only: the "/ ''" alt text hides it from screen
     readers (a plain counter would be read as a stray "."). The first
     content line is the fallback for browsers without alt-text support. */
  .numbered-sections { counter-reset: numbered-sections; }
  .numbered-section { position: relative; padding-left: 22px; }
  .numbered-section > .h3 { counter-increment: numbered-sections; }
  .numbered-section > .h3::before {
    content: counter(numbered-sections) ".";
    content: counter(numbered-sections) "." / "";
    position: absolute;
    left: 0;
    font-weight: 400;
    font-size: 16px;
    line-height: 24px;
    letter-spacing: -0.09px;
    color: var(--label-primary);
  }

  /* ---- Inline code chip -------------------------------------------
     For markup and code names inside prose, e.g. <h1>. Monospace at 14px
     so it doesn't out-measure the 16px body text. */
  /* ==================================================================
     COLOUR CHIP — NEW Sep 2026
     A swatch and its hex value in one pill. Same fill and inner border as
     .code-inline. Height 24; 4 in on the left, 8 on the right; 4 between
     swatch and text; corner 6.
     The swatch is 16 x 16, corner 4, with a 1px inner stroke whose colour
     comes from --swatch-stroke, set per chip in the HTML.
     Text: .mono-label-14-bold.
     ================================================================== */
  .color-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 24px;
    padding: 0 8px 0 4px;
    border-radius: 6px;
    background: var(--fill-badge);
    box-shadow: inset 0 0 0 1px var(--border-primary);
    white-space: nowrap;
    vertical-align: middle;
  }
  .color-chip__swatch {
    flex: none;
    width: 16px;
    height: 16px;
    border-radius: 4px;
    box-shadow: inset 0 0 0 1px var(--swatch-stroke, var(--border-primary));
  }

  /* ==================================================================
     SWATCH GRID — NEW Sep 2026
     Eight columns, 16 apart. Each column: the colour tile, its name and
     hex, then 28 lower its grayscale tile and value.
     ================================================================== */
  .swatch-grid {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(8, minmax(0, 1fr));
    column-gap: 16px;
  }
  .swatch-grid li { display: flex; flex-direction: column; min-width: 0; }
  .swatch-tile {
    position: relative;
    height: 56px;
    border-radius: 8px;
  }
  .swatch-tile--color { box-shadow: inset 0 0 0 1px var(--swatch-stroke); }
  .swatch-grid .swatch-name { margin: 8px 0 0; color: var(--label-primary); }
  .swatch-grid .swatch-hex  { margin: 2px 0 0; }
  .swatch-grid .swatch-tile--gray { margin-top: 28px; }
  .swatch-grid .swatch-gray { margin: 8px 0 0; color: var(--label-primary); }
  .swatch-badge {
    position: absolute;
    left: 4px;
    bottom: 4px;
    height: 18px;
    padding: 0 4px;
    display: flex;
    align-items: center;          /* label centred in the 18 */
    border-radius: 4px;
    background: #FFFFFF;
    color: var(--label-primary);
    box-shadow: 0 0 0 0.5px var(--border-primary);
  }
  .article .figure-swatches { max-width: var(--text-measure); margin-top: 55px; margin-bottom: 60px; }   /* 55 box = 60 from the letters above */

  /* ==================================================================
     CVD GRID — NEW Sep 2026
     The eight fills in five renderings. Text width (696), labels included. Row
     labels in .caption-1, right-aligned, 28 from the first tile; tiles
     16 apart both ways, 44 tall, corner 8, 1px inner border.
     ================================================================== */
  .cvd-grid {
    width: 100%;
    border-collapse: separate;
    border-spacing: 16px;
    margin: -16px;                /* the spacing sits between tiles, not round the outside */
    width: calc(100% + 32px);
  }
  .cvd-grid th {
    padding: 0 12px 0 0;          /* + the 16 spacing = 28 to the first tile */
    font-weight: 400;
    text-align: right;
    white-space: nowrap;
    color: var(--label-primary);
    width: 1%;                    /* shrink to the longest label */
  }
  .cvd-grid td { padding: 0; }
  .cvd-tile {
    height: 44px;
    border-radius: 8px;
    box-shadow: inset 0 0 0 1px var(--swatch-stroke);
  }
  .article .figure-cvd { max-width: var(--text-measure); margin-top: 55.5px; margin-bottom: 60px; }   /* 55.5 box = 60 from the letters above */

  /* ==================================================================
     DATA TABLE — NEW Sep 2026
     A text-width table (696, the reading measure). Header in .caption-1
     over a 1.5px --label-primary rule; body rows 44 tall with a 0.5px
     --border-primary divider under each, the last row included. The outer
     columns are inset 20 so the text sits inside the rules' ends.
     ================================================================== */
  /* 60 above measured TEXT TO TEXT: from the lowest ink of the paragraph
     above to the cap tops of the header. The boxes sit 51 apart for that;
     the other 9 is the paragraph's line spacing and the header's ascent. */
  /* ---- Dashboard canvas (part 2, end of section I) — NEW 22 Sep ------
     Colour chips, their glass tiles, the enlarged hover panel and the
     annotation card with a colour dot instead of an icon. Chip fills and
     borders come from the colour table, which is the source of truth. */

  .cchip {
    width: 96px; height: 96px;
    border-radius: 24px;
    border: 2px solid;
    display: flex; flex-direction: column; justify-content: flex-end;
    padding: 0 10px 10px;         /* 10 in from the left and bottom */
    color: var(--label-primary);
    z-index: 2;
  }
  .cchip--light, .cchip--light .cchip__hex { color: #FFFFFF; }
  .cchip .cchip__name { position: static; }
  .cchip .cchip__hex  { position: static; margin-top: 2px; }



  /* The product's hover panel, drawn at the size it appears in the frame
     (about 1.5x the real control) so it can be read inside the canvas. */
  .tipbig {
    width: 312.5px;
    padding: 13px 16px;
    box-shadow: 0 3px 12px rgba(0,0,0,0.10);   /* same plate and depth as .annotation-2 */
    font-family: "Roboto", Arial, sans-serif;
    color: #000;
    z-index: 4;
  }
  .tipbig p.tipbig__date { font-size: 16px; line-height: 20px; font-weight: 600; margin: 0 0 6px; position: static; }
  .tipbig__row { display: flex; align-items: center; justify-content: space-between; gap: 16px; height: 31.5px; font-size: 17px; line-height: 20px; white-space: nowrap; }
  .tipbig__name { display: flex; align-items: center; gap: 8px; }
  .tipbig__sw { width: 13px; height: 13px; border-radius: 100px; border: 1px solid; box-sizing: border-box; flex: none; }

  /* The colour-dot variant of the annotation card (part 2 dashboard
     canvas): the same glass plate, with a 16px dot in the chart's colours
     instead of the icon, and 12 from the headline to the body (Marina). */
  .annotation-2__dot { flex: none; width: 16px; height: 16px; border-radius: 100px; border: 2px solid; box-sizing: border-box; }
  .annotation-2--dot .annotation-2__body { margin-top: 12px; }


  /* ---- Hover-panel chart (part 2, V "Reading exact values") — NEW 22 Sep --
     A redraw of Marina's chart-hover-3day mock at text width (696), sitting
     bare on the page. Set in Roboto, the product's own font, because it is a
     piece of product UI. The SVG is static; the panel is pinned open on the
     middle date group, with its hit area shaded. */
  .article .figure-hover { max-width: var(--text-measure); margin-top: 47px; margin-bottom: 60px; }   /* 47 box = 60 from the letters above to the top axis label */
  .hover-chart { font-family: "Roboto", Arial, sans-serif; color: #000; }
  .hover-chart__plot { position: relative; width: 100%; max-width: 696px; }
  .hover-chart__plot svg { display: block; font-family: inherit; }
  .hover-chart__tip {
    position: absolute; min-width: 236px; padding: 10px;
    background: rgba(255,255,255,.6);
    -webkit-backdrop-filter: blur(22px); backdrop-filter: blur(22px);
    border: 1px solid #D8D8D8; border-radius: 12px;
    box-shadow: 0 2px 14px rgba(0,0,0,.10);
  }
  .hover-chart p.hover-chart__date { font-size: 12px; line-height: 16px; font-weight: 600; margin: 0 0 4px; }
  .hover-chart__row { display: flex; align-items: center; justify-content: space-between; gap: 16px; height: 24px; font-size: 13px; line-height: 16px; white-space: nowrap; }
  .hover-chart__name { display: flex; align-items: center; gap: 4px; }
  .hover-chart__sw { width: 10px; height: 10px; border-radius: 100px; border: 1px solid; box-sizing: border-box; flex: none; }
  /* Phones (chart under 500 wide, toggled by the chart script): the panel
     hugs its longest row, with 12px text on 20px rows. */
  .hover-chart__tip--compact { min-width: 0; padding: 8px; }
  .hover-chart__tip--compact .hover-chart__row { height: 20px; gap: 8px; font-size: 12px; }
  .hover-chart .hover-chart__tip--compact p.hover-chart__date { font-size: 11px; line-height: 14px; }

  .article .figure-text { max-width: var(--text-measure); margin-top: 51px; margin-bottom: 60px; }
  .data-table {
    width: 100%;
    /* separate, not collapse: collapsed borders get snapped to whole pixels,
       which drew the 1.5 rule and the 0.5 dividers both at 1. */
    border-collapse: separate;
    border-spacing: 0;
    table-layout: fixed;
  }
  .data-table th {
    padding: 0 0 9.5px;           /* 18 line + 8 of air + the 1.5 rule painted in the last 1.5 */
    font-weight: 400;
    text-align: left;
    color: var(--label-primary);
    /* Rules are PAINTED, not borders. Chrome snaps table-cell borders to
       whole pixels, which drew the 1.5 and the 0.5 both at 1. A background
       line is drawn at the screen's real resolution. */
    background: linear-gradient(var(--label-primary), var(--label-primary)) left bottom / 100% 1.5px no-repeat;
  }
  .data-table td {
    height: 44px;
    padding: 0;
    vertical-align: middle;
    color: var(--label-primary);
  }
  .data-table tbody td {         /* 0.5 divider under every row, the last included */
    background: linear-gradient(var(--border-primary), var(--border-primary)) left bottom / 100% 0.5px no-repeat;
  }
  .data-table .num { text-align: right; }
  .data-table th:first-child, .data-table td:first-child { padding-left: 20px; }
  .data-table th:last-child,  .data-table td:last-child  { padding-right: 20px; }

  /* ---- Part 2 figures on narrow screens (22 Sep) ----------------------
     Below 768 the text column is narrower than 696, so each figure fits
     itself to the column instead of scrolling sideways. The hover chart
     redraws itself (script next to it); these rules handle the rest. */
  @media (max-width: 767px) {
    .cvd-grid { border-spacing: 8px; margin: -8px; width: calc(100% + 16px); }
    .cvd-grid th { padding-right: 4px; }              /* + the 8 spacing = 12 to the first tile */
    .data-table { table-layout: auto; }
    .data-table col { width: auto !important; }        /* the desktop column widths are inline */
    .data-table th:first-child, .data-table td:first-child { padding-left: 0; padding-right: 8px; }
    .data-table th:last-child,  .data-table td:last-child  { padding-right: 0; }
  }
  @media (max-width: 660px) {
    .swatch-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); row-gap: 28px; }
  }

  .code-inline {
    font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 14px;
    padding: 0 2px;
    background: var(--fill-badge);
    border-radius: 4px;
    /* 1px INNER border. An inset shadow rather than a border, so it adds
       nothing to the chip's size and the line it sits in does not move. */
    box-shadow: inset 0 0 0 1px var(--border-primary);
    white-space: nowrap;
  }

  /* Back link — regular by default, faux-semibold on hover, arrow nudges
     LEFT. Faux rather than real weight so the arrow's 2px nudge is the only
     movement on the whole component. */
  /* display:flex so the row hugs the link's own 20px line box. As a plain
     block it was 23px tall — the extra 3px is inline baseline slack — which
     made the measured container-to-container gap 47, not the 44 set here. */
  .back-row { display: flex; margin-bottom: 60px; }

  /* An H1 that follows a back row is a case-study title: 44 above (the rule
     immediately above), 44 below. The index H1 has no back row, so it keeps
     the 44 set on .main__heading and its gap to the cards is untouched.
     NOTE: if an anchor row is later added to the index using .back-row, this
     will apply there too — scope it then. */
  .back-row + .main__heading { margin-bottom: 44px; }
  .back-link {
    position: relative;           /* anchors the invisible 44px hit area */
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--label-secondary);   /* label-secondary at rest, label-primary on hover/press, like the "Case studies" crumb (Marina, 5 Oct; was label-primary) */
    text-decoration: none;
  }
  .back-link:hover, .back-link:active { color: var(--label-primary); }
  .back-link:hover .back-link__label { text-shadow: var(--faux-bold); }
  /* Invisible 44px-tall target, centered on the link. Costs no layout space,
     so the focus ring stays on the 20px link while the tap area meets 44px. */
  .back-link::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: 44px;
  }
  .back-link__label { display: inline-block; }
  .back-link__arrow {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--label-primary);   /* black at rest and on hover; only the label changes colour (Marina, 5 Oct) */
    transition: transform var(--card-ease) ease;   /* 0.35, same as the cards and the gate (Marina, 3 Oct; was 0.2) */
  }
  .back-link:hover .back-link__arrow { transform: translateX(-2px); }
  @media (prefers-reduced-motion: reduce) {
    .back-link__arrow { transition: none; }
  }


  /* ==================================================================
     PASSWORD GATE (Marina's Figma, 2 Oct 2026) — Firstday 01 and 02
     ------------------------------------------------------------------
     Only on the locked shells written by tools/build_locked.py. While
     locked the page does not scroll; the card sits 4 above the footer,
     over the public text (Overview through the text before the first
     figure). The glass is the annotation cards' plate (shared rule
     above); only the fill differs: transparent at the top, #F7F7F7 by
     60% down. Measured off her 1920 x 830 frame; 1px edges are taken
     out of the padding so the outer box matches (rule 1).
     ================================================================== */
  html.gate-locked { overflow: hidden; }   /* the window, not body: body must not become the sticky card's scroller */
  html.gate-locked .footer { position: sticky; bottom: 0; z-index: 5; }
  html.gate-checking .gate-card { visibility: hidden; }
  /* The 28 between the card and the footer is plain page, never text (3 Oct). */
  .gate-band {
    position: fixed;              /* placed by the gate script over the footer's full width */
    left: var(--side-panel); right: 0; bottom: 60px;
    height: 28px;
    margin: 0;
    z-index: 3;                   /* over the text, under the card and its shadow */
    background-color: var(--background-primary);   /* the gate script adds the locked screen's dots, lined up with the canvas (8 Oct) */
    pointer-events: none;
  }

  /* .article > .gate-card outranks the shared glass rule further down,
     which would otherwise reset the radius to 15 and the fill. */
  .article > .gate-card {
    /* Fixed to the window (the page doesn't scroll while locked). The gate
       script sets bottom = footer + 28, the width (784, or the main column
       less 20 a side when narrower) and a whole-pixel left, centred on the
       reading column, so it holds at every window size (3 Oct). */
    position: fixed;
    bottom: 88px;                 /* fallback; the script measures the real footer */
    z-index: 4;
    width: 784px;                 /* text column 696 + 44 either side (Marina, 5 Oct: back to 784 from 752) */
    padding: 28px 44px 28px;      /* 28 top and bottom, 44 sides (Marina, 5 Oct; was 44/28/28 in getux-site-5oct-e); edge is drawn inside (inset), so no shaving */
    border: 0;                    /* inner edge only, on ::after: a real border is translucent and lets the page show through */
    border-radius: 36px;          /* Marina, 3 Oct */
    box-shadow: 0 3px 12px rgba(0,0,0,0.10);
    background-image:
      linear-gradient(180deg, rgba(0,0,0,0.043)  0,
                              rgba(0,0,0,0.038)  2px,
                              rgba(0,0,0,0.026)  4px,
                              rgba(0,0,0,0.022)  6px,
                              rgba(0,0,0,0.018) 10px,
                              rgba(0,0,0,0.010) 16px,
                              rgba(0,0,0,0)     24px),
      linear-gradient(0deg,   rgba(255,255,255,0.81)  0,
                              rgba(255,255,255,0.81)  2px,
                              rgba(255,255,255,0.70)  4px,
                              rgba(255,255,255,0.43)  7px,
                              rgba(255,255,255,0.32) 11px,
                              rgba(255,255,255,0.15) 15px,
                              rgba(255,255,255,0)    27px),
      /* her fill: translucent at the top, opaque by 60% */
      linear-gradient(180deg, rgba(255,255,255,0) 0%, #FFFFFF 60%);   /* white (3 Oct; was #F7F7F7) */
  }
  .article > .gate-card { max-width: none; margin: 0; }
  .article > .gate-band { max-width: none; margin: 0; }   /* the footer's full width, not the text column */
  /* Glass (3 Oct): Chrome limits a backdrop blur to the element's visible box,
     so text just under the card's top edge stayed half sharp. Instead the gate
     script lays a blurred copy of the public text under the card, trimmed to
     its shape (.gate-frost, below); the card carries only fill and edge.
     Same 11px as the annotation cards. */
  .article > .gate-card {
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    /* her shadows: the cards' drop shadow, plus a glow lifted above the card
       (0 -6 50 #F8F8F8, Marina, 3 Oct) that fades the page text over its top
       edge without touching the glass */
    box-shadow: 0 3px 12px rgba(0,0,0,0.10), 0 -6px 50px #F8F8F8;
  }
  .gate-frost {
    position: fixed;
    z-index: 3;                   /* under the card (4), over the page */
    margin: 0;
    box-sizing: border-box;
    pointer-events: none;
    background: var(--background-primary);
    filter: blur(11px);
  }
  .gate-frost .sidepanel { position: static; }   /* the copy doesn't scroll; keep the panel where it sits */
  .article > .gate-card::after {
    content: "";
    position: absolute;
    pointer-events: none;
    box-shadow: inset 0 0 0 1px var(--border-primary);   /* the card's edge, drawn inside */
  }
  .article > .gate-card::after {
    content: "";
    position: absolute;
    pointer-events: none;
  }
  .article > .gate-card::before {
    inset: -33px;                         /* padding box + 32 */
    z-index: -2;
    backdrop-filter: blur(11px);
    -webkit-backdrop-filter: blur(11px);
  }
  .article > .gate-card::after {
    inset: 0;
    z-index: -1;
    border-radius: 36px;
    background-image:
      linear-gradient(180deg, rgba(0,0,0,0.043)  0,
                              rgba(0,0,0,0.038)  2px,
                              rgba(0,0,0,0.026)  4px,
                              rgba(0,0,0,0.022)  6px,
                              rgba(0,0,0,0.018) 10px,
                              rgba(0,0,0,0.010) 16px,
                              rgba(0,0,0,0)     24px),
      linear-gradient(0deg,   rgba(255,255,255,0.81)  0,
                              rgba(255,255,255,0.81)  2px,
                              rgba(255,255,255,0.70)  4px,
                              rgba(255,255,255,0.43)  7px,
                              rgba(255,255,255,0.32) 11px,
                              rgba(255,255,255,0.15) 15px,
                              rgba(255,255,255,0)    27px),
      linear-gradient(180deg, rgba(255,255,255,0) 0%, #FFFFFF 60%);   /* white (Marina, 3 Oct) */
  }
  .gate-card__tile-icon {        /* Marina's glass pulse tile (3 Oct): 2x PNG, body 60 x 60 */
    position: relative;
    width: 60px; height: 60px;
  }
  .gate-card__tile-icon img {    /* the export carries its shadow: 33 each side, 28 above, 36 below */
    position: absolute;
    left: -33px; top: -28px;
    width: 126px; height: 124px;
    display: block;
    pointer-events: none;
  }
  /* Headline = the site's h2; description = caption-1 (Marina, 3 Oct; was
     Geist Mono). No visible field label: the field is named by the headline. */
  .article .gate-card__title { margin: 28px 0 0; }
  .article .gate-card__body {
    margin: 8px 0 0;
    color: var(--label-secondary);
  }
  .gate-card__row { display: flex; gap: 12px; margin-top: 20px; }
  .gate-card__field { position: relative; flex: 1 1 auto; min-width: 0; }
  .gate-card__input {
    display: block; width: 100%; height: 44px;
    padding: 0 45px 0 12px;       /* 12 to the text; the eye button covers the right */
    border: 0;
    box-shadow: inset 0 0 0 1px #8B9095;   /* inner edge; was #90959A (2.8:1), 3.0:1 now (3 Oct) */
    border-radius: 12px;          /* 12 (Marina, 3 Oct; was 10) */
    background: #FFFFFF;          /* white (Marina, 8 Oct; was background-primary #F9F9F9) */
    font: 400 14px/16px "Geist Mono", ui-monospace, monospace;
    letter-spacing: -0.4px;
    color: var(--label-primary);
  }
  /* Hidden password: dots drawn in Geist (sans), whose bullet fills its own
     slot, so the cursor lands just past each dot (Geist Mono centred them in a
     letter-wide slot and the cursor fell mid-dot). 23.5px / -0.3 gives 5 x 5
     dots 2 apart on a 1x screen; 10 of padding + the dot's 2 of side space puts
     the first dot 12 from the edge (Marina, 3 Oct; measured). */
  .gate-card__input[type="password"] { font-family: "Geist", sans-serif; font-size: 23.5px; letter-spacing: -0.3px; padding-left: 11px; padding-bottom: 4px; }   /* bottom 4 lifts the dots 2 to her line */
  /* Wrong password: the banner carries the error; the field keeps its normal edge (Marina, 3 Oct). */
  .gate-card__eye {
    position: absolute; top: 0; right: 0;     /* 44 square flush with the field's right end; icon 12 in from the edge */
    width: 44px; height: 44px;    /* 44 tap area, 20 icon */
    display: flex; align-items: center; justify-content: center;
    padding: 0; border: 0; background: none; cursor: pointer;
    color: var(--label-primary); border-radius: 10px;
  }
  /* Hover tile (Marina, 3 Oct): the arrow tile's shape (38 square, radius 8,
     3 in from the field edge), shown behind the eye on hover; the icon stays 20.
     Final (3 Oct): fill-primary, no edge, on the white field. */
  .gate-card__eye::before {
    content: ""; position: absolute; inset: 4px; border-radius: 10px;   /* 36 square, 4 in, radius 10, like the arrow tile */
    background: var(--background-primary);   /* #F9F9F9 on the white field (Marina, 8 Oct; was white) */
    box-shadow: inset 0 0 0 1px var(--border-primary);   /* 1px inner edge, #000 @ 15% like the chip icon discs, so the hover shows on the #F8F8F8 field (Marina, 5 Oct; was fill only) */
    opacity: 0; transition: opacity var(--card-ease) ease;
  }
  .gate-card__eye:hover::before { opacity: 1; }
  .gate-card__eye svg { position: relative; width: 18px; height: 14px; display: block; }   /* 18 wide x 14 high, no ring (Marina, 8 Oct; was 20 square with a ring) */
  .gate-card__eye .icon-eye-off { display: block; }
  .gate-card__eye .icon-eye-on  { display: none; }
  .gate-card__eye[aria-pressed="true"] .icon-eye-off { display: none; }
  .gate-card__eye[aria-pressed="true"] .icon-eye-on  { display: block; }
  .gate-card__btn {
    flex: none; width: auto; min-width: 200px; height: 44px;   /* hugs its content: 16 + label 143.3 + 8 + arrow 16 + 16 = 199.3, min 200 (Marina, 9 Oct: no tile; was 208 with the 36 tile) so the row stays on whole pixels (text soft on 1x screens otherwise); the field takes the rest of the row (Marina, 5 Oct; was a fixed 211) */
    display: flex; align-items: center; justify-content: space-between; gap: 8px;   /* label → tile 8 (8.4 with the rounding; Marina, 5 Oct; was 12; 32/38/40/44 tried) */
    padding: 0 16px;              /* 16 both sides (Marina, 9 Oct; was 16 left, 4 right of the tile) */
    border: 0; border-radius: 12px;   /* 12 at rest and disabled; hover/press → pill (Marina, 9 Oct) */
    transition: border-radius var(--card-ease) ease;
    background: var(--fill-purple); color: #ffffff; cursor: pointer;   /* purple (Marina, 9 Oct; olive #697500 before, black before that) */
    font: 500 14px/16px "Geist Mono", ui-monospace, monospace; letter-spacing: 0;   /* Geist Mono Medium 14/16, 0 tracking (Marina, 9 Oct; was Regular 400, -0.6 from 5 Oct); colour stays white */
    white-space: nowrap;
  }
  .gate-card__tile {               /* no tile since 9 Oct (Marina): just the 16 arrow, in the label's colour */
    width: 16px; height: 16px; flex: none;
    background: none; color: inherit;
    display: flex; align-items: center; justify-content: center;
  }
  /* The card CTAs' dashed arrow: same glyph, same label-14-bold type (3 Oct). */
  .gate-card__arrow {
    display: inline-block;
    font-family: var(--font);
    font-weight: 600; font-size: 14px; line-height: 20px; letter-spacing: -0.09px;
  }
  .gate-card__arrow::before { content: ""; display: block; width: 16px; height: 16px; background: currentColor; -webkit-mask: var(--icon-arrow-forward) center / 16px 16px no-repeat; mask: var(--icon-arrow-forward) center / 16px 16px no-repeat; }   /* forward arrow icon (8 Oct; was the dashed glyph) */
  /* No nudge here: the tile turning into a circle is the hover cue, and a
     moving arrow would sit off-centre in it (Marina, 3 Oct). */
  /* Hover (Marina, 3 Oct): the field's edge turns black; the button's arrow
     tile rounds into a circle. Both ease over 0.2s. */
  .gate-card__input { transition: box-shadow 0.2s ease; }
  .gate-card__field:hover .gate-card__input { box-shadow: inset 0 0 0 1px var(--label-primary); }
  /* Hovering the eye shows its tile only; the field edge stays grey (Marina, 3 Oct) */
  .gate-card__field:has(.gate-card__eye:hover) .gate-card__input:not(:focus) { box-shadow: inset 0 0 0 1px #8B9095; }
  /* Hover and press (Marina, 9 Oct): the button rounds into a pill. 22 = half its 44 height, which is
     what her 100 renders as; animating to 22 rather than 100 keeps the 0.35 ease even. */
  .gate-card__btn:hover, .gate-card__btn:active { border-radius: 22px; }
  /* Arrow nudges 2px right on hover/press (Marina, 9 Oct), same 0.35 ease; never when disabled. */
  .gate-card__tile { transition: transform var(--card-ease) ease; }
  .gate-card__btn:not([disabled]):hover .gate-card__tile,
  .gate-card__btn:not([disabled]):active .gate-card__tile { transform: translateX(2px); }
  /* Disabled (Marina, 3 Oct): until the field has something in it, and while
     a password is being checked. Grey fill and label; the white tile and its
     arrow stay; no hover change. */
  .gate-card__btn[disabled] { background: var(--fill-secondary);   /* #EDEDEC (Marina, 5 Oct; was fill-badge #F0F0EE) */ color: var(--label-disabled); cursor: default; }
  .gate-card__btn[disabled]:hover, .gate-card__btn[disabled]:active { border-radius: 12px; }   /* disabled: no hover change */   /* arrow in label-disabled (Marina, 3 Oct) */   /* disabled tile: 1px inline edge, #000 @ 15%, same as the chip icon discs (Marina, 3 Oct) */
  .gate-card__btn.is-checking { cursor: progress; }
  /* Typing (focus) state (Marina, 3 Oct), click and keyboard alike: the
     field's own 1px inner edge turns black, and a 1px dashed ring (2 on, 2 off,
     like the chips) sits 3px outside it, radius 14 against the field's 12.
     Both lines are drawn inside their shapes, so nothing changes size.
     Alternatives kept in mind: the site ring (2px outline, 2px clear) and a
     2px centred black stroke. */
  .gate-card__input:focus,
  .gate-card__input:focus-visible { outline: none; box-shadow: inset 0 0 0 1px #8B9095; }   /* typing: the field keeps its default edge; only the dashed ring is black (Marina, 9 Oct; edge was black) */
  .gate-card__field:has(.gate-card__input:focus) .gate-card__input { box-shadow: inset 0 0 0 1px #8B9095; }   /* also while hovered */
  .gate-card__focus-ring {
    position: absolute; inset: -3px;
    width: calc(100% + 6px); height: calc(100% + 6px);
    pointer-events: none;
    display: none;
  }
  .gate-card__focus-ring rect {
    x: 0.5px; y: 0.5px;
    width: calc(100% - 1px); height: calc(100% - 1px);
    rx: 13.5px;
    fill: none;
    stroke: var(--label-primary);
    stroke-width: 1px;
    stroke-dasharray: 6 3;   /* 6 on, 3 off (Marina, 8 Oct; 8 4 tried; was 1 2, and 2 2 before that) */
  }
  .gate-card__field:has(.gate-card__input:focus) .gate-card__focus-ring { display: block; }
  .gate-card__eye:focus-visible,
  .gate-card__btn:focus-visible { outline: 2px solid var(--label-primary); outline-offset: 2px; }
  /* Error banner (Marina's design, 3 Oct): as wide as the field, 20 above and
     below, 12 padding, radius 10, 1px inner edge #E5442A on #FFF4F4. Icon =
     outlined warning triangle in the set style (16, 1.5 lines). The card grows
     upward, since its bottom is pinned. */
  .gate-card__error {
    display: flex; align-items: flex-start; gap: 8px;                     /* icon → text 8 (Marina, 3 Oct; was 12) */
    width: 100%;                  /* spans the whole row, field to button, at every size (Marina, 3 Oct) */
    margin: 20px 0 0;
    padding: 12px;
    border-radius: 10px;
    background: #FFF4F4;
    box-shadow: inset 0 0 0 1px #E5442A;
  }
  .gate-card__error[hidden] { display: none; }
  .gate-card__error-icon { flex: none; width: 16px; height: 16px; margin-top: 2px; color: #E5442A; }
  .article .gate-card__error-title { margin: 0; color: var(--label-primary); }
  .article .gate-card__error-body  { margin: 5px 0 0; color: var(--label-tertiary); }   /* measured off her frame */
  @media (prefers-reduced-motion: reduce) { .gate-card__arrow, .gate-card__input, .gate-card__tile, .gate-card__btn, .gate-card__eye::before { transition: none; } }
  @media (max-width: 620px) {     /* phones (Marina, 3 Oct, rev 2): card 4 from each edge and 4 above
                                     the footer, radius 28, padding 16 (content 20 in = the text column);
                                     one row, the field shrinks; button 12 left padding, 8 label to tile */
    .article > .gate-card { padding: 16px 16px 20px; border-radius: 28px; }   /* 16 top and sides, 20 bottom (3 Oct) */
    .article .gate-card__title { margin-top: 20px; }                  /* tile → headline 20 on phones (3 Oct) */
    .article > .gate-card::after { border-radius: 28px; }
    .gate-card__btn { padding: 0 16px; gap: 8px; min-width: 200px; }   /* phones = desktop (Marina, 9 Oct): 16 + label 143.3 + 8 + arrow 16 + 16 = 199.3, min 200 (9 Oct; was 193) for whole pixels (5 Oct; was a fixed 203) */
  }

  /* ==================================================================
     RESPONSIVE
     ================================================================== */
  @media (max-width: 1000px) {
    /* Single column: panel, then heading, then main. */
    .layout { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto 1fr auto; }
    .layout__head { grid-column: 1; grid-row: 2; }
    .main         { grid-column: 1; grid-row: 3; }
    .footer       { grid-column: 1; }
    .sidepanel__rule { display: none; }   /* the panel is no longer beside the footer */
    .sidepanel {
      grid-column: 1;
      grid-row: 1;
      width: 100%;
      position: static;
      height: auto;
      border-right: none;
      border-bottom: 1px solid var(--border-primary);
      /* 27 + the 1px border = 28 from the chips to the line, the same
         "the rule is the last pixel" arithmetic as the desktop 59 + 1. */
      padding-bottom: 27px;
    }
    .h1 { font-size: 44px; line-height: 52px; }
    /* The panel stacks here, but the divider now marks the identity block,
       so it stays; the identity stops reserving the panel's width and hugs
       its own content, bringing the line and the nav in with it. The -1px
       overlap is dropped and replaced by a real 20px gap, matching the 20
       on the nav's side: once the identity hugs, that last pixel is the
       final glyph of "Toronto, Canada" rather than empty space. */
    .identity { flex: 0 0 auto; }
    .topnav__divider { margin-left: 20px; }
  }
  @media (max-width: 620px) {
    .layout__head { padding: 28px 20px 0; }
    .main { padding: 0 20px 48px; }
    .h1 { font-size: 34px; line-height: 40px; }
    .topnav { padding-right: 20px; }
  }
  /* The full trail (mono crumb, 23 Sep) ends at about x500, so it needs a
     520px window to clear the 20px gutter. Below 540 — that plus 20 of
     slack — it falls back to the "Case studies" link alone. No loss: the H1
     repeats the page name a few lines further down. The divider stays. */
  @media (max-width: 540px) {
    .crumbs__item + .crumbs__item { display: none; }
  }

  /* ==================================================================
     ANNOTATION-2
     ------------------------------------------------------------------
     A glass card carrying one annotation: an icon + headline row, then
     body copy. 328 wide, height hugs the text.

     Type comes from the shared styles — .mono-label-14-bold for the headline,
     .mono-body for the body — so this rule only sets box, fill and depth.

     WHAT FIGMA'S GLASS DOES THAT CSS CANNOT
     Figma's Glass effect has six controls. Frost maps to backdrop-filter
     and the light angle maps to a gradient, but Refraction, Depth and
     Dispersion bend and colour-split the backdrop live, and the browser
     has no equivalent. So the look below is reconstructed by measuring
     Marina's export rather than translated from the parameters.

     Measured from that export, over an #F1F1F1 ground:
       outer box  328 x 178 incl. a 1px edge   ->  176 inner
       edge       flat 1px dark line, all four sides, ~15% black
       fill       234 at the top, 244 through the middle, 253 at the
                  bottom: an inner shade under the top edge and an inner
                  highlight along the bottom one, over a light base
     Built as translucent layers, not opaque greys, so the card still
     tints with whatever canvas it sits on.

     THE MARKUP (the icon lives in the HTML so it can vary per note):
       <div class="annotation-2">
         <p class="annotation-2__head mono-label-14-bold">
           <svg class="annotation-2__icon" viewBox="0 0 16 16" aria-hidden="true">
             <rect x="0.85" y="0.85" width="14.6" height="14.6" rx="1.6"
                   stroke-dasharray="3 1.8"/>
             <path d="M8 4.6v0.01"/>
             <path d="M8 7.5v3.8"/>
           </svg>
           Headline
         </p>
         <p class="annotation-2__body mono-body">Body copy.</p>
       </div>

     The blur IS calibrated now — see the note on backdrop-filter below.
     On a flat canvas it stays a no-op; blurring a solid colour returns the
     same colour, so it only shows where a card overlaps a screenshot. ==== */
  /* The glass plate itself — border, layered fill and frost. Shared with
     .quote-card, which is the same plate at a different size. Box, padding
     and depth stay with each card, because those are what differ. */
  .annotation-2,
  .quote-card,
  .tipbig,
  .gate-card {
    box-sizing: border-box;
    border: 1px solid var(--border-primary);
    border-radius: 15px;
    background-clip: padding-box;
    background-image:
      /* inner shade under the top edge — steep for 4px, then a long tail */
      linear-gradient(180deg, rgba(0,0,0,0.043)  0,
                              rgba(0,0,0,0.038)  2px,
                              rgba(0,0,0,0.026)  4px,
                              rgba(0,0,0,0.022)  6px,
                              rgba(0,0,0,0.018) 10px,
                              rgba(0,0,0,0.010) 16px,
                              rgba(0,0,0,0)     24px),
      /* inner highlight along the bottom edge */
      linear-gradient(0deg,   rgba(255,255,255,0.81)  0,
                              rgba(255,255,255,0.81)  2px,
                              rgba(255,255,255,0.70)  4px,
                              rgba(255,255,255,0.43)  7px,
                              rgba(255,255,255,0.32) 11px,
                              rgba(255,255,255,0.15) 15px,
                              rgba(255,255,255,0)    27px),
      /* the light base the glass adds over its ground */
      linear-gradient(rgba(255,255,255,0.24) 0 100%);
    /* CALIBRATED Sep 2026. Measured against the HomeAds composition where a
       card overlaps a phone screenshot: swept blur 0-26px and compared the
       mean and standard deviation of the backdrop showing through. Figma's
       Frost 22 lands at 11px — mean within 2 levels, sd within 1.2.
       Rule of thumb: CSS blur px is about half Figma's Frost value. */
    backdrop-filter: blur(11px);
    -webkit-backdrop-filter: blur(11px);
  }
  .annotation-2 {
    width: 328px;
    padding: 12px;
    box-shadow: 0 3px 12px rgba(0,0,0,0.10);
  }
  .annotation-2__head {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin: 0;
  }
  .annotation-2__icon {
    flex: none;
    width: 16px;
    height: 16px;
    display: block;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
  }
  /* The dashed border carries a slightly lighter stroke than the glyph.
     At 16px a 1.7 stroke with 1.8px gaps antialiases the gaps shut and the
     square reads solid; 1.4 is the heaviest weight where the dashes survive. */
  .annotation-2__icon rect { stroke-width: 1.4; }
  /* HomeAds cards (23 Sep): the dashed square became corner brackets, same
     16px box, 5px arms, 2.2 inner corners, 1.4 against the glyph's 1.7. */
  .annotation-2__icon .annotation-2__frame { stroke-width: 1.4; stroke-linejoin: round; }
  .annotation-2__body {
    margin: 8px 0 0;
    color: var(--label-tertiary);
  }

  /* ==================================================================
     QUOTE CARD
     ------------------------------------------------------------------
     The same glass plate as .annotation-2 — same border, fill and frost,
     measured off the same export — but wider, with no drop shadow, and
     laid out as a quote mark ABOVE the copy rather than an icon beside a
     headline. Type comes from .mono-body.

     Padding is 20 at the top and 12 on the other three sides; the gap
     under the mark is 12. Height is left to the copy: 20 + 20 (mark) +
     12 + 16 per line + 12, plus the 1px edge top and bottom, so four
     lines makes 130. That matters — the cards are stacked to a fixed
     overlap and centred in the map, so one line more or less in any of
     them moves everything below it. =============================== */
  .quote-card {
    width: 420px;
    padding: 20px 12px 12px;
  }
  .quote-card__icon {
    display: block;
    width: 27px;
    height: 20px;
    margin-bottom: 12px;
    fill: none;
    stroke: #90959A;
  }
  .quote-card__body {
    margin: 0;
    color: var(--label-tertiary);
  }

  /* ==================================================================
     COVERAGE MAP
     ------------------------------------------------------------------
     A flat map export with the town pins rebuilt as live text, so the
     names stay crisp when the canvas is scaled and read as text rather
     than picture. The export is 2x, placed at 672 x 468; the inner box
     is pulled 1px up and left so the stroke lands ON the art's outer
     edge, which is how Figma draws an inside stroke.
     ================================================================== */
  .ha-map {
    width: 672px;
    height: 468px;
    border: 1px solid #90959A;
    border-radius: 10px;
    overflow: hidden;
  }
  .ha-map__inner {
    position: absolute;
    left: -1px;
    top: -1px;
    width: 672px;
    height: 468px;
  }
  .ha-map__inner img {
    position: absolute;
    left: 0;
    top: 0;
    width: 672px;
    height: 468px;
  }

  /* A town pin. Carries the product's own type — Urbanist SemiBold
     12/16 — because it belongs to the map, not to this site.

     `left` is the BEAK'S POINT, not the pill's edge: the pill is centred
     on it with a transform, so a longer name grows both ways and the pin
     stays anchored to the same spot on the map.

     The shadow is a filter rather than a box-shadow. A box-shadow traces
     the element's box, which would leave the beak unlit and draw a seam
     where the two meet; a drop-shadow filter traces what is actually
     painted, pseudo-element included, so the pin casts one silhouette. */
  .map-pin {
    position: absolute;
    transform: translateX(-50%);
    padding: 4px 6px;
    border-radius: 6px;
    background: #FFFFFF;
    font-family: "Urbanist", var(--font);
    font-weight: 600;
    font-size: 12px;
    line-height: 16px;
    letter-spacing: 0;
    color: var(--label-primary);
    white-space: nowrap;
    filter: drop-shadow(0 0 0.77px rgba(0,0,0,0.30))
            drop-shadow(0 0.77px 11.61px rgba(0,0,0,0.12));
  }
  /* 11 x 5.5, centred, point down. The 0.77 corner on the point is below
     a pixel at this size and CSS cannot round a clipped corner anyway. */
  .map-pin::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 100%;
    width: 11px;
    height: 5.5px;
    margin-left: -5.5px;
    background: #FFFFFF;
    clip-path: polygon(0 0, 100% 0, 50% 100%);
  }

  /* ==================================================================
     EXHIBIT CANVASES
     A fixed 1128-wide picture made of live text. Every value is literal
     and absolute, read off the Figma frame: absolute coordinates
     reproduce a composition exactly, where flex or grid only approximate
     it. Each canvas sets its own height and background.

     THE 28 RULE (Marina, 20 Sep 2026)
     Every canvas carries 28 of padding on all four sides. The height is
     hand-set per canvas because the content is absolutely positioned and
     nothing can push the box open, so the height IS the bottom padding:
     get it wrong and the canvas gains or loses air at the foot only.

     MEASURE THE 28 FROM THE VISUAL OBJECT, NOT THE ELEMENT BOX. Most of
     the screenshots on these canvases are PNGs exported WITH their drop
     shadow, which means transparent pixels all round — the audit canvas's
     images carry roughly 13 of them at placed scale. An element's
     getBoundingClientRect() includes that transparent margin, so a canvas
     whose artwork sits correctly at 28 measures as though it were at 15.
     Trusting the rect and "correcting" it pushes the artwork to 41 and
     breaks a canvas that was right. Check the image's alpha bounds first.
     ================================================================== */

  .exhibit {
    position: relative;
    width: 1128px;
    background: var(--fill-primary);
    border-radius: 12px;
    overflow: hidden;    /* the iMac bleeds to the bottom edge; the radius clips it */
  }
  .exhibit > * { position: absolute; margin: 0; }

  /* Accessibility + hierarchy, merged (Marina, 9 Oct): replaces .exhibit--a11y (end of II) and
     .exhibit--hierarchy (end of III). iPad removed; tile labels 28 from the top; headings frame
     bottom 1561 → role diagram 180 below (1741); iMac flush to the bottom (1713 + 1596).
     Ground: flat #F3F3F3 over the accessibility part, then the hierarchy's own F3 → ED gradient. */
  .exhibit--a11y-hierarchy {
    height: 3309px;
    background: linear-gradient(180deg, #F3F3F3 0, #F3F3F3 1713px, #EDEDEC 100%);
  }
  .exhibit--account   {
    height: 4794.5px;   /* delete modal card bottom + 28; +1603.5 for the login block; -52.5 for the shorter administrator form (10 Oct) */
    /* Dark at the very top so the iMac's pale top edge reads (Marina, 9 Oct), the same fade as .exhibit--actions:
       #EDEDEC → #F3F3F3 over 1300px (35% of that 3713 canvas), flat after. */
    background: linear-gradient(180deg, #EDEDEC 0, #F3F3F3 1300px);
  }
  .exhibit--details   { height: 1128px; background: var(--fill-secondary); }   /* square frame */
  .exhibit--data      { height: 1549px; background: #F0F3F5; }   /* Marina, 12 Oct: #EDF1F3, then #F0F3F5 (was fill-tertiary #F1F2F6, which the Firstday 02 widths canvas keeps) */
  /* Part 2, end of section I: the dashboard canvas. */
  .exhibit--dash      { height: 2355px; }
  /* Part 2, after III: the width constraint canvas. */
  .exhibit--widths    { height: 1532px; background: var(--fill-tertiary); }
  /* Part 2, after V: the two arrangements (flat --fill-primary). */
  .exhibit--options   { height: 2250px; }
  /* HomeAds — section I canvas. 1128 wide like every other; the width is
     confirmed twice over by the composition: the MacBook at x25 is 1080
     wide (23 to spare on the right) and the two phones at x130 span 998
     (130 to spare) — both centred. */
  .exhibit--ha-audit  { height: 10387.5px; background: var(--fill-primary); }
  /* HomeAds — section III: sitemap above a rule, persona below. */
  .exhibit--ha-ia     { height: 2337px;  background: var(--fill-primary); }
  /* HomeAds — section IV: type scale, grids, component library.  NAME PENDING. */
  .exhibit--ha-ui     { height: 2194px;  background: var(--fill-primary); }
  /* HomeAds — section V: annotated wireframes.  NAME PENDING.
     Two parts of one composition on a single canvas; part 2 starts at 3535. */
  .exhibit--ha-wire   { height: 5165px;  background: var(--fill-primary); }
  /* HomeAds — after Impact: iMac on a white panel, phone + map card, iPad flush to the foot. */
  .exhibit--ha-impact { height: 2769px;  background: var(--fill-primary); }

  /* Formats, errors and anchored columns (Marina, 10 Oct): between the data and login canvases, end of IX. */
  .exhibit--formats   { height: 2368px; background: var(--fill-primary); }
  .exhibit__panel--tertiary { background: var(--fill-tertiary); }   /* #F1F2F6 */
  /* Column tags and the callout chip: Geist Mono 14/16 in a 28-tall chip, 1px edge, measured off her frame. */
  .exhibit-tag { box-sizing: border-box; height: 28px; padding: 8.5px 5px 0; display: block; text-align: left;
                 background: #E7E7E4; box-shadow: inset 0 0 0 1px #C4C4C2; border-radius: 4px; color: var(--label-primary); white-space: nowrap; }
  .exhibit-callout-line { width: 1px; background: var(--label-primary); }
  .exhibit-callout-dot  { width: 8px; height: 8px; border-radius: 50%; background: var(--label-primary); }
  .exhibit--login     { height: 1128px; background: var(--fill-primary); }   /* square; iMac centred */
  .exhibit--actions   {
    height: 3713px;   /* pre-admission iMac merged in; flush to the bottom */
    /* Dark only at the very top, so the iMac's pale body reads against it. */
    background: linear-gradient(180deg, #EDEDEC 0%, #F3F3F3 35%);
  }

  /* ---- Annotation ---------------------------------------------------
     Always 288 wide, hug height, set in .mono-body (14/16) — not
     .caption-1 (14/18), whose extra two points of leading read loose at
     this measure. The braces are real Geist glyphs, inline with the
     prose: they open before the first word and close after the last,
     exactly as in the frame. At 16/16 they sit flush in the 16px line
     box instead of stretching it. */
  /* ANNOTATION-1 — the original annotation: braced prose beside an image,
     no card. Was .exhibit__note; renamed Sep 2026 to sit alongside
     .annotation-2. Type comes from .mono-body. */
  .annotation-1 { width: 288px; color: var(--label-tertiary); }
  .exhibit__brace {
    font-size: 16px;
    line-height: 16px;
    font-weight: 600;
    color: var(--label-primary);
  }

  /* ---- Panel ---------------------------------------------------------
     A tinted card that holds an exhibit and clips whatever bleeds past
     its edge. The stroke is a box-shadow rather than a border because
     Figma draws it OUTSIDE the shape, and a CSS border would sit inside
     and shrink the content box by half a pixel. */
  .exhibit__panel {
    border-radius: 10px;
    box-shadow: 0 0 0 0.3px var(--border-primary);
    overflow: hidden;
  }
  /* Two skins. Geometry is shared; only the fill differs.
     NAMES PROVISIONAL — Marina to confirm --tint / --neutral. */
  .exhibit__panel--tint    { background: var(--fill-lavender); }     /* #F5F8FF */
  .exhibit__panel--neutral { background: var(--fill-secondary); }  /* #EDEDEC */
  .exhibit__panel--white   { background: #FFFFFF; }
  .exhibit__panel > * { position: absolute; margin: 0; }

  /* ---- An image and the mono line that names it --------------------- */
  .exhibit__shot > .mono-body { display: block; margin-bottom: 12px; }
  .exhibit__shot img { display: block; width: 100%; height: auto; }

  /* Images inside an exhibit ship exactly as exported — square corners.
     `.article figure img` rounds every figure image to 12px, which is
     right for a standalone photo and wrong here, so it is cancelled.
     The second selector has to name the figure to outrank it. */
  .exhibit img,
  .article figure .exhibit img { border-radius: 0; }

  /* ---- Canvas reveal (option H, set up by Chrome.reveal) ------------
     Only canvases the script has tagged .rv-canvas are affected.
     Timing, one spec site-wide (Marina, 24 Sep): ground + text 800ms,
     images and tiles 1000ms starting at 150ms, all in by 1150ms. */
  .rv-canvas { background: transparent !important; isolation: isolate; }
  .rv-canvas::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background-color: var(--rv-bg-c);
    background-image: var(--rv-bg-i);
    opacity: 0;
    transition: opacity 800ms cubic-bezier(0.2, 0.6, 0.3, 1);
  }
  .rv-canvas .annotation-1,
  .rv-canvas .annotation-2,
  .rv-canvas .mono-body,
  .rv-canvas .exhibit__panel,
  .rv-canvas .quote-card,
  .rv-canvas .ha-map {
    opacity: 0;
    transition: opacity 800ms cubic-bezier(0.2, 0.6, 0.3, 1);
  }
  /* Code-built samples (hover panel, colour chips) arrive with the images. */
  .rv-canvas img,
  .rv-canvas .tipbig,
  .rv-canvas .cchip {
    opacity: 0;
    transform: scale(0.98);
    transition: opacity 1000ms cubic-bezier(0.2, 0.6, 0.3, 1) 150ms,
                transform 1000ms cubic-bezier(0.2, 0.6, 0.3, 1) 150ms;
  }
  .rv-canvas.is-in::before,
  .rv-canvas.is-in .annotation-1,
  .rv-canvas.is-in .annotation-2,
  .rv-canvas.is-in .mono-body,
  .rv-canvas.is-in .exhibit__panel,
  .rv-canvas.is-in .quote-card,
  .rv-canvas.is-in .ha-map,
  .rv-canvas.is-in img,
  .rv-canvas.is-in .tipbig,
  .rv-canvas.is-in .cchip { opacity: 1; transform: none; }
  /* Applied for one frame while the script hides things, so they vanish
     instantly instead of fading out first. */
  .rv-prep, .rv-prep *, .rv-prep::before { transition: none !important; }
  /* Single images (index thumbnails, article figures): the canvas image
     timing, so the whole site moves at one speed (24 Sep; was 700ms). */
  .rv-img {
    opacity: 0;
    transform: scale(0.98);
    transition: opacity 1000ms cubic-bezier(0.2, 0.6, 0.3, 1) 150ms,
                transform 1000ms cubic-bezier(0.2, 0.6, 0.3, 1) 150ms;
  }
  .rv-img.is-in { opacity: 1; transform: none; }

  /* ---- Glass tile ----------------------------------------------------
     A floating 60px icon plate. Figma's Glass effect is set to
     Light -45deg / 75%, Refraction 1, Depth 17, Dispersion 4, Frost 1.5,
     Splay 0, over a drop shadow of X0 Y3 Blur22 Spread0 at #000 5%.

     Only two of those have CSS equivalents. Frost maps to
     `backdrop-filter` — a blur applied to whatever is painted BEHIND an
     element, like frosted bathroom glass. Light maps to a directional
     inner highlight. Refraction, Depth and Dispersion — the edge bending
     what is behind it and fringing it toward colour — have no CSS
     property at all, so the rest is matched to the rendered frame by
     measurement rather than by reproducing the effect:

       plate centre  L229-238 against a 243 ground  -> a ~4% black tint,
                     strongest at the top-left, easing to the bottom-right
       rim           1px at L254                    -> a near-white inner ring

     Values live in tokens so a second glass surface can reuse them. */
  .exhibit__glass {
    width: 60px;
    height: 60px;
    border-radius: var(--glass-radius);
    display: grid;
    place-items: center;
    background: var(--glass-tint);
    backdrop-filter: blur(var(--glass-frost));
    -webkit-backdrop-filter: blur(var(--glass-frost));
    box-shadow: var(--glass-shadow);
    color: var(--label-primary);
  }
  /* The rim is a 1px ring, not a border, because a border can only be one
     flat colour and this one has to change round the shape. The trick:
     paint the gradient over the whole tile, then mask out everything
     except the 1px padding band — two masks, one clipped to the content
     box, XOR'd so only the frame survives. */
  .exhibit__glass::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    padding: 1px;
    background: var(--glass-rim);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask-composite: exclude;
    pointer-events: none;
  }
  /* 28px icon frame inside the 60px plate (16px padding all round),
     carrying a 24px glyph. The glyph is drawn on the same 16 box as the
     chip icons; stroke-width is divided by the scale factor so the
     rendered weight stays 1.4 instead of scaling up to 2.1. */
  .exhibit__glass svg {
    display: block;
    width: 24px;
    height: 24px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;         /* scales with the glyph: renders as 2.1 at 24px */
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  /* 76 x 76 glass tile over a chip's corner: the part 1 glass recipe
     (tint, conic rim, frost, shadow) at a bigger size and corner. The tile
     is CENTRED on the chip's corner, so it hangs 38 out and covers 38 in.
     Which corner it takes is per chip, as in Marina's frame.
     DEPTH: Figma's glass magnifies what sits under it, which is why the
     chip's corner reads as a soft blob inside the tile. CSS cannot magnify
     a backdrop, so the blob is drawn: a circle of the chip's own colour,
     blurred, sitting where the covered corner is. */
  /* A glass tile exported from Figma, with its real depth baked in.
     Placed by its visible square: the export carries 44 left / 52.5 top of
     transparent margin. */
  .exhibit .glass-shot { z-index: 3; }

  .exhibit__glass--tile { width: 76px; height: 76px; border-radius: 24px; z-index: 3; overflow: hidden; }
  .exhibit__glass--tile::before {
    content: "";
    position: absolute;
    width: 40px; height: 40px;
    border-radius: 100px;
    background: var(--refract);
    opacity: 0.72;
    filter: blur(7px);
  }
  .exhibit__glass--br::before { right: 4px;  bottom: 4px; }   /* chip covered at the tile's bottom-right */
  .exhibit__glass--bl::before { left: 4px;   bottom: 4px; }   /* ... bottom-left */
  /* ==================================================================
     NARROW WINDOWS — the canvas scales, it never reflows.

     A fixed 1128 composition cannot rewrap, so when its column is
     narrower the whole thing shrinks as one unit. (Chosen over sideways
     scrolling, Sep 2026.) Note the column only REACHES 1128 at viewports
     of 1540+; at 1440 the figure is 1028, so the resting scale on a
     common laptop is already 0.91.
     ================================================================== */

  /* The whole canvas shrinks as one unit, exactly like an
     exported image does. `transform` doesn't change layout size, so
     dot-grid.js also sets the wrapper's height to match. */
  .exhibit-frame--scale { overflow: hidden; }
  /* The transform goes on whatever the frame's single child is: one canvas,
     or an .exhibit-stack holding several. Scaling the stack rather than each
     canvas keeps the gap between them proportional as the group shrinks. */
  .exhibit-frame--scale > * {
    transform: scale(var(--s, 1));
    transform-origin: top left;
  }
  .exhibit-stack {
    width: 1128px;
    display: flex;
    flex-direction: column;
    gap: 20px;
  }
