/* ── Metaphor Lucknow Litfest · Main stylesheet ──────────────────────────────
 *
 * Cascade layers are declared here, once, in the order they cascade. Everything
 * downstream opts into a layer, so specificity fights cannot happen between a
 * component and a utility — the layer order settles it.
 *
 * The build inlines these imports into one file; the @import form is kept so the
 * stylesheet also works unbundled during development.
 */
@layer reset, base, layout, type, components, utils;

/* ── Metaphor Lucknow Litfest · Design Tokens ───────────────────────────────
 *
 * Colour-as-time-of-day: the palette maps one November festival day.
 * data-hour on <body> or any section shifts the local field.
 *
 * paper   → first light, before the city wakes
 * morning → the Residency at 9 am, warm stone
 * noon    → full sun, ideas, argument
 * dusk    → the hour the festival actually starts
 * evening → mehfil, poetry, the lamp lit
 * night   → after the last session, walking back
 * ink     → the page itself
 */

:root {
  /* ── palette ── */
  --c-paper:   #F7F2EA;
  --c-morning: #EDE4D4;   /* ivory */
  --c-noon:    #D8CAB4;   /* warm stone */
  --c-dusk:    #9C362D;   /* red */
  --c-evening: #70334E;   /* pink / plum */
  --c-night:   #422B41;   /* dark aubergine */
  --c-ink:     #171015;   /* near-black */

  /* ── semantic aliases (default: paper ground, ink text) ── */
  --bg:        var(--c-paper);
  --fg:        var(--c-ink);
  --accent:    var(--c-dusk);
  --accent-2:  var(--c-noon);
  --mid:       #886858;   /* warm neutral for metadata, captions */

  /* ── type scale (fluid, clamp-based) ──
   *
   * The steps are deliberately far apart at the ends. An editorial page needs
   * 11px metadata to sit beside 150px display type without anything in between
   * competing — so --t-xs barely grows and --t-hero grows hard. The middle of
   * the scale is where reading happens and is kept calm.
   *
   * Every clamp is checked so the preferred term reaches the maximum before
   * 1920px and never falls below the minimum at 320px.
   */
  --t-xs:   clamp(0.6875rem, 0.66rem + 0.14vw, 0.78rem);
  --t-sm:   clamp(0.8125rem, 0.78rem + 0.17vw, 0.9375rem);
  --t-base: clamp(1rem,      0.96rem + 0.2vw,  1.125rem);
  --t-md:   clamp(1.15rem,   1.05rem + 0.5vw,  1.5rem);
  --t-lg:   clamp(1.5rem,    1.25rem + 1.25vw, 2.5rem);
  --t-xl:   clamp(2.1rem,    1.6rem  + 2.5vw,  4rem);
  --t-2xl:  clamp(2.9rem,    2rem    + 4.5vw,  6.5rem);
  --t-3xl:  clamp(4rem,      2.6rem  + 7vw,    10rem);
  /* Reserved for the one h1 on the site that is a cover. */
  --t-hero: clamp(3.4rem,    1.9rem  + 8.2vw,  11.5rem);

  /* ── spacing ──
   * A 4px base. The gaps between steps widen as the values grow, which is what
   * keeps large vertical rhythm from looking arithmetic.
   */
  --sp-1:  0.25rem;
  --sp-2:  0.5rem;
  --sp-3:  0.75rem;
  --sp-4:  1rem;
  --sp-5:  1.25rem;
  --sp-6:  1.5rem;
  --sp-8:  2rem;
  --sp-10: 2.5rem;
  --sp-12: 3rem;
  --sp-16: 4rem;
  --sp-24: 6rem;
  --sp-32: 8rem;

  /* ── grid ── */
  --gutter:    clamp(1rem, 4vw, 2.5rem);
  --col-max:   1440px;
  --col-prose: 68ch;

  /* ── layering ──
   * One ladder, declared once. Nothing in the stylesheets may use a bare
   * z-index; it names a step here instead. Reading the ladder top to bottom is
   * reading the depth of the page.
   */
  --z-under:   0;   /* colour fields, background typography */
  --z-media:   1;   /* photography and video */
  --z-scrim:   2;   /* the gradient that carries an image into its ground */
  --z-fore:    3;   /* foreground typography sitting over media */
  --z-note:    4;   /* editorial annotations, captions, vertical metadata */
  --z-overlay: 50;  /* the navigation overlay */
  --z-header:  60;  /* the masthead — above the overlay, so the control that
                       opened the menu is also the control that closes it */
  --z-skip:    70;  /* the skip link, above everything */

  /* ── motion ── */
  --ease-out:  cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in:   cubic-bezier(0.7, 0, 0.84, 0);
  --ease-circ: cubic-bezier(0.85, 0, 0.15, 1);
  --dur-fast:  180ms;
  --dur-mid:   380ms;
  --dur-slow:  700ms;
  --dur-xslow: 1200ms;

  /* ── stitch rule (chikankari-derived hairline) ── */
  --stitch: repeating-linear-gradient(
    90deg,
    var(--fg) 0,
    var(--fg) 4px,
    transparent 4px,
    transparent 10px
  );
  --stitch-v: repeating-linear-gradient(
    180deg,
    var(--fg) 0,
    var(--fg) 4px,
    transparent 4px,
    transparent 10px
  );
}

/* ── hour fields ─────────────────────────────────────────────────────────────
 * --mid is hour-bound: no single warm neutral clears WCAG AA 4.5:1 against
 * both the paper and ink grounds. Each value is the original #9B7B6A with its
 * OKLCH lightness moved to threshold; hue 49.4 and chroma 0.047 are held.
 */
[data-hour="paper"],   [data-hour-text="paper"]   { --bg: var(--c-paper);   --fg: var(--c-ink);   --accent: var(--c-dusk);    --mid: #886858; }
[data-hour="morning"], [data-hour-text="morning"] { --bg: var(--c-morning); --fg: var(--c-ink);   --accent: var(--c-evening); --mid: #7F6050; }
[data-hour="noon"],    [data-hour-text="noon"]    { --bg: var(--c-noon);    --fg: var(--c-ink);   --accent: var(--c-evening); --mid: #6E5041; }
[data-hour="dusk"],    [data-hour-text="dusk"]    { --bg: var(--c-dusk);    --fg: var(--c-paper); --accent: var(--c-morning); --mid: #EBC8B5; }
[data-hour="evening"], [data-hour-text="evening"] { --bg: var(--c-evening); --fg: var(--c-paper); --accent: var(--c-noon);    --mid: #D1AE9C; }
[data-hour="night"],   [data-hour-text="night"]   { --bg: var(--c-night);   --fg: var(--c-paper); --accent: var(--c-noon);    --mid: #B59382; }
[data-hour="ink"],     [data-hour-text="ink"]     { --bg: var(--c-ink);     --fg: var(--c-paper); --accent: var(--c-noon);    --mid: #9B7B6A; }

/* An hour is a field of colour, so an element that declares one paints it.
 *
 * This is the load-bearing rule on the site. Without it `data-hour` rebinds the
 * four tokens and then nothing uses `--bg`, so every section inherited the ink
 * ground from <body> while drawing text in some *other* hour's accent — dusk
 * oxblood on near-black, at roughly 2:1. The seven hours were declared, audited
 * in DESIGN.md, and never rendered. The homepage was one flat dark block
 * wearing seven names.
 *
 * <html> is excluded: it would double-paint under <body>, and the root's job is
 * to hold the hour for the browser chrome, not to be a field itself.
 *
 * `data-hour-text` is the other half of the pair: it rebinds the same four
 * tokens but paints no ground. The fixed masthead uses it to pick up the text
 * colour of the field it happens to be over, without laying a solid bar across
 * the photograph underneath.
 */
[data-hour]:not(html) {
  background-color: var(--bg);
  color: var(--fg);
}

[data-hour-text] {
  color: var(--fg);
  background-color: transparent;
}

/* The masthead stays above the navigation overlay so that the button which
 * opened the menu is also the button that closes it. That puts it on an ink
 * ground regardless of the hour it was tracking, so its hour is pinned for as
 * long as the menu is open.
 *
 * This lives here, unlayered, beside the rule it has to beat: `[data-hour-text]`
 * is written by app.js onto the header itself, and a layered rule in
 * components.css loses to any unlayered one no matter how specific.
 */
[data-nav-open] .site-header[data-hour-text] {
  --bg: var(--c-ink);
  --fg: var(--c-paper);
  --accent: var(--c-noon);
  --mid: #9B7B6A;
  color: var(--c-paper);
}

/* ── Metaphor · Base reset + global styles ──────────────────────────────── */
/* Layer order is declared in main.css. */

@layer reset {
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  html { -webkit-text-size-adjust: 100%; tab-size: 4; }
  img, video, svg { display: block; max-width: 100%; }
  button, input, select, textarea { font: inherit; }
  a { color: inherit; }
  ul, ol { list-style: none; }
}

@layer base {
  html {
    font-family: "Playfair Display", Georgia, "Times New Roman", serif;
    font-size: 16px;
  }

  /* Smooth scrolling is scoped to in-page anchors rather than applied to `html`
     globally. On the root it also animates browser-initiated jumps — back/forward
     restoration and skip-links — which turns an instant navigation into a
     multi-second scroll the reader did not ask for. */
  @media (prefers-reduced-motion: no-preference) {
    :has(:target) { scroll-behavior: smooth; }
  }

  /* The masthead is fixed, so an anchor landing at y=0 lands underneath it. */
  :target { scroll-margin-top: 6rem; }

  body {
    background: var(--bg);
    color: var(--fg);
    line-height: 1.6;
    transition: background var(--dur-slow) var(--ease-out),
                color var(--dur-slow) var(--ease-out);
    overflow-x: hidden;
  }

  ::selection {
    background: var(--accent);
    color: var(--bg);
  }

  /* Focus is drawn with two rings — the accent, and a `--bg`-coloured spacer
     behind it. On a photograph or a dark field a single accent ring can fall on
     a colour close to itself and vanish; the spacer guarantees a boundary
     regardless of what is underneath. Never colour alone. */
  :focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 0 4px var(--bg);
  }

  /* Where the element is already a filled block, the ring goes inside so it is
     not clipped by an overflow boundary. */
  .btn:focus-visible,
  .chip:focus-visible {
    outline-offset: -5px;
    box-shadow: none;
  }

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

@layer base {
  /* ── Page transitions ───────────────────────────────────────────────────────
   *
   * The site's motion vocabulary is three verbs and no more:
   *
   *   CUT    the page changes. A short dissolve with a small vertical drift —
   *          the feeling of a page being turned rather than a screen sliding.
   *   WIPE   a fill grows from an edge. Buttons and links use it on hover.
   *   DRIFT  a photograph eases in scale under the cursor. Nothing else moves.
   *
   * There is deliberately no scroll-triggered entrance animation anywhere. A
   * page where every block fades up as it enters the viewport reads as a slide
   * deck, delays the content behind an effect, and makes the composition — which
   * is the actual work — impossible to see until it has finished performing. The
   * compositions are built to be correct as still images, so they are shown as
   * still images.
   */
  @view-transition { navigation: auto; }

  ::view-transition-old(root) {
    animation: cut-out var(--dur-mid) var(--ease-in) both;
  }
  ::view-transition-new(root) {
    animation: cut-in var(--dur-slow) var(--ease-out) both;
  }

  @keyframes cut-out {
    to { opacity: 0; transform: translateY(-0.75rem); }
  }
  @keyframes cut-in {
    from { opacity: 0; transform: translateY(1rem); }
  }

  @media (prefers-reduced-motion: reduce) {
    ::view-transition-old(root),
    ::view-transition-new(root) { animation: none; }
  }
}

/* ── Metaphor · Typography ───────────────────────────────────────────────── */

/*
 * Type system:
 *   Display / editorial  — Playfair Display (serif, variable wght 400–900)
 *   Body / UI            — Archivo (sans, variable wght 300–900, wdth 75–125)
 *   Devanagari           — Halant (subset via &text=)
 *
 * Loaded in <head> via Google Fonts CDN. Fallback stacks below cover the
 * first paint before fonts arrive.
 */

@layer type {

  /* ── families ── */
  .f-display {
    font-family: "Playfair Display", Georgia, "Times New Roman", serif;
    font-optical-sizing: auto;
  }
  .f-hi {
    font-family: "Halant", Georgia, serif;
  }

  /* ── scale ── */
  .t-xs   { font-size: var(--t-xs);   line-height: 1.5; }
  .t-sm   { font-size: var(--t-sm);   line-height: 1.5; }
  .t-base { font-size: var(--t-base); line-height: 1.6; }
  .t-md   { font-size: var(--t-md);   line-height: 1.4; }
  .t-lg   { font-size: var(--t-lg);   line-height: 1.25; }
  .t-xl   { font-size: var(--t-xl);   line-height: 1.1; }
  .t-2xl  { font-size: var(--t-2xl);  line-height: 1.0; }
  .t-3xl  { font-size: var(--t-3xl);  line-height: 0.95; letter-spacing: -0.02em; }

  /* ── weight / style ── */
  .fw-light    { font-weight: 300; }
  .fw-regular  { font-weight: 400; }
  .fw-medium   { font-weight: 500; }
  .fw-semibold { font-weight: 600; }
  .fw-bold     { font-weight: 700; }
  .fw-black    { font-weight: 900; }
  .fi          { font-style: italic; }

  /* ── numeric ── */
  .tabular     { font-variant-numeric: tabular-nums; }

  /* ── small caps for metadata labels ── */
  .caps {
    font-family: "Archivo", "Helvetica Neue", Arial, sans-serif;
    font-size: var(--t-xs);
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    font-variation-settings: "wdth" 85;
  }

  /* ── eyebrow / kicker ── */
  .kicker {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    font-weight: 500;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--accent);
    font-variation-settings: "wdth" 85;
  }

  /* ── metadata / caption ── */
  .meta {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    color: var(--mid);
    line-height: 1.5;
  }

  /* ── prose body ── */
  .prose {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-base);
    line-height: 1.75;
    max-width: var(--col-prose);
  }
  .prose p + p { margin-top: 1.25em; }

  /* ── pull quote ── */
  .pullquote {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-lg);
    font-style: italic;
    line-height: 1.3;
    position: relative;
    padding-left: var(--sp-8);
  }
  .pullquote::before {
    content: "\201C";
    position: absolute;
    left: 0;
    top: -0.1em;
    font-size: 3em;
    line-height: 1;
    color: var(--accent);
    font-style: normal;
  }

  /* ── editorial annotation ── */
  .annotation {
    display: block;
    font-family: "Playfair Display", Georgia, serif;
    font-style: italic;
    font-size: var(--t-sm);
    color: var(--mid);
    line-height: 1.6;
    margin-top: var(--sp-2);
  }
}

/* ── Metaphor · Layout ───────────────────────────────────────────────────── */

@layer layout {

  /* ── page wrapper ── */
  .page {
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
  }

  .main { flex: 1; }

  /* ── grid container ── */
  .container {
    width: 100%;
    max-width: var(--col-max);
    margin-inline: auto;
    padding-inline: var(--gutter);
  }

  .container--narrow {
    max-width: 900px;
    margin-inline: auto;
    padding-inline: var(--gutter);
  }

  /* ── asymmetric editorial grid ──
   * 12 columns. Named areas used by page templates.
   * col-start / col-end span the full 12.
   * col-main spans 7 (left-weighted).
   * col-aside spans 4 (right).
   * col-wide spans 10 (centred, 1 col margin each side).
   */
  .grid {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--gutter);
  }

  /* A grid item is `min-width: auto` by default, so a track holding a 7rem
   * numeral or a long unbroken word grows past its share and pushes the whole
   * row off-screen. This site sets display type inside grid tracks constantly —
   * the archive shelf's "2013" was overflowing the page by 4px and clipping the
   * card — so every direct grid child is allowed to be narrower than its
   * contents. Anything that genuinely needs a floor asks for one.
   */
  .grid > * { min-width: 0; }

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

  .grid--3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .grid--4 {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .grid--2 > *,
  .grid--3 > *,
  .grid--4 > * {
    grid-column: span 1;
  }

  .col-full  { grid-column: 1 / -1; }
  .col-wide  { grid-column: 2 / 12; }
  .col-main  { grid-column: 1 / 8; }
  .col-aside { grid-column: 9 / 13; }
  .col-half  { grid-column: span 6; }
  .col-third { grid-column: span 4; }

  @media (max-width: 1100px) {
    .col-wide,
    .col-main,
    .col-aside { grid-column: 1 / -1; }
    .col-half  { grid-column: span 12; }
    .col-third { grid-column: span 6; }
    .grid--3   { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .grid--4   { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  }

  @media (max-width: 700px) {
    .grid--2,
    .grid--3,
    .grid--4   { grid-template-columns: 1fr; }
  }

  @media (max-width: 600px) {
    .col-third { grid-column: span 12; }
  }

  /* ── section spacing ── */
  .section {
    padding-block: var(--sp-24);
  }

  .section--sm { padding-block: var(--sp-12); }
  .section--lg { padding-block: var(--sp-32); }

  /* ── poster section: full-viewport, colour field ── */
  .poster {
    min-height: 100svh;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: var(--sp-16) var(--gutter);
    background: var(--bg);
    color: var(--fg);
    position: relative;
    overflow: hidden;
  }

  /* ── responsive image ── */
  /* Position is set inline per image from the FOCUS map in media.mjs, so the
     crop follows the subject rather than the centre of the file. */
  .img-cover {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* ── grain overlay ── */
  .grain::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
    background-size: 200px 200px;
    pointer-events: none;
    mix-blend-mode: overlay;
    opacity: 0.5;
  }
}

/* ── Metaphor · Components ───────────────────────────────────────────────── */

@layer components {

  /* ── Site header / nav ──
   *
   * The masthead rides over fields that run from cream to near-black, so it
   * cannot be one fixed colour. It adopts the hour of whatever field is
   * currently beneath it — app.js writes `data-hour-text` as the seams pass.
   *
   * It sits above the navigation overlay (see the z-index ladder in tokens.css)
   * so that the button which opens the menu is the button that closes it.
   */
  .site-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: var(--z-header);
    padding: var(--sp-4) var(--gutter);
    display: flex;
    align-items: center;
    justify-content: space-between;
    pointer-events: none;
    color: var(--fg);
    transition: color var(--dur-mid) var(--ease-out),
                padding var(--dur-mid) var(--ease-out);
  }

  .site-header > * { pointer-events: auto; }

  /* The bar is the answer to text passing beneath a fixed masthead.
   *
   * It used to be a radial gradient behind each item — a soft ellipse of the
   * current hour, which over reading matter read as a greasy smudge with the
   * sentence beneath it still legible through the middle. Every headline that
   * scrolled past the wordmark collided with it.
   *
   * So the masthead earns a real ground instead: nothing at the top of the page,
   * where the cover should be uninterrupted, and on scroll a full-width bar in
   * the hour's own colour, blurred, with a hairline to close it. The seam is
   * deliberate and horizontal, which is a masthead; the smudge was neither.
   */
  .site-header::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: color-mix(in oklab, var(--bg) 82%, transparent);
    backdrop-filter: blur(14px) saturate(1.4);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    border-bottom: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
    opacity: 0;
    transform: translateY(-100%);
    transition: opacity var(--dur-mid) var(--ease-out),
                transform var(--dur-mid) var(--ease-out);
    pointer-events: none;
  }

  [data-scrolled].site-header::before {
    opacity: 1;
    transform: none;
  }

  /* Over footage the composition is the picture, and a translucent bar across it
     is a toolbar laid on a photograph. The hero carries its own gradient. */
  .site-header[data-over-media]::before {
    opacity: 0;
    transform: translateY(-100%);
  }

  /* Tightened once the bar is carrying it, so the masthead reads as a rule
     across the page rather than as a floating pair of controls. */
  [data-scrolled].site-header {
    padding-top: var(--sp-3);
    padding-bottom: var(--sp-3);
  }

  .site-logo,
  .nav-toggle { position: relative; isolation: isolate; }

  .site-logo {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    text-decoration: none;
    color: inherit;
    /* The home link, present on every page — worth a full target. */
    min-height: 44px;
  }

  .site-logo__mark,
  .site-logo__img {
    width: auto;
    height: clamp(3.5rem, 9vh, 5rem);
    flex-shrink: 0;
    object-fit: contain;
    transition: width var(--dur-mid) var(--ease-out),
                height var(--dur-mid) var(--ease-out);
  }

  [data-scrolled] .site-logo__mark,
  [data-scrolled] .site-logo__img {
    height: 4rem;
  }

  /* Two lines at rest, one line once the bar is carrying it. Set as a flex wrap
   * rather than a <br>, so collapsing to a single line keeps the word space
   * between "Metaphor" and "Lucknow" instead of running them together. */
  .site-logo__wordmark {
    display: flex;
    flex-wrap: wrap;
    column-gap: 0.32em;
    max-width: 7.5em;
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-sm);
    font-weight: 700;
    letter-spacing: 0.04em;
    line-height: 1.2;
    text-transform: uppercase;
    transition: font-size var(--dur-mid) var(--ease-out),
                max-width var(--dur-mid) var(--ease-out);
  }

  /* Past the first screen the wordmark sets on one line.
   *
   * It used to disappear entirely, because a fixed two-line logo laid over a
   * page of reading matter is a permanent obstruction. With a bar beneath it
   * that is no longer true, and a masthead that keeps its name is worth more
   * than one that retreats to an unlabelled mark. */
  [data-scrolled] .site-logo__wordmark {
    max-width: none;
    font-size: var(--t-xs);
    letter-spacing: 0.1em;
  }

  /* The only always-visible control on a phone, so it gets a full 44px target.
     The lines stay 2.5rem wide; the padding does the rest, which keeps the mark
     the same size as before while making it comfortably tappable. */
  .nav-toggle {
    background: none;
    border: none;
    cursor: pointer;
    color: inherit;
    padding: var(--sp-3) var(--sp-2);
    margin-right: calc(var(--sp-2) * -1);
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 5px;
    width: 2.75rem;
    min-height: 44px;
  }

  .nav-toggle__line {
    display: block;
    height: 1px;
    background: currentColor;
    transition: transform var(--dur-mid) var(--ease-circ),
                opacity var(--dur-fast) var(--ease-out),
                width var(--dur-mid) var(--ease-out);
  }

  .nav-toggle__line:nth-child(1) { width: 100%; }
  .nav-toggle__line:nth-child(2) { width: 65%; }
  .nav-toggle__line:nth-child(3) { width: 80%; }

  [data-nav-open] .nav-toggle__line:nth-child(1) {
    transform: translateY(6px) rotate(45deg);
    width: 100%;
  }
  [data-nav-open] .nav-toggle__line:nth-child(2) { opacity: 0; }
  [data-nav-open] .nav-toggle__line:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg);
    width: 100%;
  }

  /* ── Full-screen nav overlay ──
   *
   * One of the site's few signature moments, so it is worth more than a fade:
   * the ground arrives first, then the links follow it up in sequence, each one
   * rising out of its own clipped row. `--i` is set per item in the markup.
   *
   * The whole sequence is under 500ms. A menu is a means, not a performance.
   */
  .nav-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    background: var(--c-ink);
    color: var(--c-paper);
    display: grid;
    grid-template-rows: 1fr auto;
    padding: var(--sp-24) var(--gutter) var(--sp-12);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-mid) var(--ease-out);
  }

  [data-nav-open] .nav-overlay {
    opacity: 1;
    pointer-events: auto;
  }

  .nav-overlay__list {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: var(--sp-2);
  }

  /* The row is the mask; the link inside it is what moves. */
  .nav-overlay__item {
    overflow: hidden;
  }

  .nav-overlay__link {
    display: flex;
    align-items: baseline;
    gap: var(--sp-6);
    text-decoration: none;
    color: inherit;
    padding-block: var(--sp-3);
    transition: color var(--dur-fast) var(--ease-out);
  }

  @media (prefers-reduced-motion: no-preference) {
    .nav-overlay__link {
      transform: translateY(105%);
      transition: color var(--dur-fast) var(--ease-out),
                  transform var(--dur-slow) var(--ease-out);
    }

    [data-nav-open] .nav-overlay__link {
      transform: none;
      /* 45ms apart: enough to read as a sequence, quick enough that the last
         item is in place before the hand has left the button. */
      transition-delay: 0ms, calc(60ms + var(--i) * 45ms);
    }
  }

  .nav-overlay__link:hover { color: var(--c-noon); }

  /* The current page is marked in the accent and keeps its rule, so the overlay
     also answers "where am I" — not only "where can I go". */
  .nav-overlay__link[aria-current="page"] {
    color: var(--c-noon);
  }

  .nav-overlay__label {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-xl);
    font-style: italic;
    line-height: 1;
  }

  .nav-overlay__note {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--mid);
    transition: color var(--dur-fast) var(--ease-out);
  }

  .nav-overlay__link:hover .nav-overlay__note { color: var(--c-noon); }

  .nav-overlay__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-top: var(--sp-8);
    border-top: 1px solid color-mix(in oklab, var(--fg) 18%, transparent);
  }

  /* ── Hero ─────────────────────────────────────────────────────────────────
   *
   * A cover, not a banner. The rules that hold it together:
   *
   *   1. The video is the ground and is never dimmed. It carries the palette.
   *   2. Type occupies the margins the dancer does not — bottom-left for the
   *      title, right edge for metadata. Nothing is laid across her.
   *   3. Legibility comes from a gradient that only reaches into the bottom
   *      third, where the footage is already near-black, so it costs no
   *      visible image.
   *   4. The arch is a stroked outline, not a fill.
   */
  .hero {
    position: relative;
    min-height: 100svh;
    display: grid;
    /* The title block sits in the bottom band; the top band is left to the
       photograph. A single row would centre the type over the subject. */
    grid-template-rows: 1fr auto;
    grid-template-columns: minmax(0, 1fr);
    overflow: hidden;
    isolation: isolate;
  }

  .hero__stage {
    position: absolute;
    inset: 0;
    z-index: var(--z-media);
  }

  .hero__video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* The aerial footage is centred on the venue and the Lucknow skyline. */
    object-position: 50% 45%;
  }

  /* Desktop shows city-wide; mobile shows portrait clip. */
  .hero__video--desktop { display: block; }
  .hero__video--mobile  { display: none; }

  /* The arch, centred on the dancer, drawn as architecture around her. A hairline
     at 0.5px was invisible against the spotlight; 1.25px reads as a drawn line at
     hero scale without becoming a graphic device. */
  .hero__arch {
    position: absolute;
    top: 6%;
    left: 50%;
    translate: -50% 0;
    width: min(46vw, 34rem);
    height: 78%;
    z-index: var(--z-scrim);
    color: var(--c-morning);
    opacity: 0.65;
    pointer-events: none;
  }

  /* Full-bleed dark wash over the aerial footage. The city-wide video is a
     daylight/dusk shot — much brighter than the previous near-black kathak
     footage — so a uniform scrim is needed to keep text legible and give the
     hero a cinematic, editorial weight. */
  .hero::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: var(--z-scrim);
    background: color-mix(in oklab, var(--c-ink) 58%, transparent);
    pointer-events: none;
  }

  /* Bottom gradient for extra legibility where title + lede sit. */
  .hero::after {
    content: "";
    position: absolute;
    inset: auto 0 0;
    height: 70%;
    z-index: var(--z-scrim);
    background: linear-gradient(
      to top,
      var(--c-ink) 0%,
      color-mix(in oklab, var(--c-ink) 90%, transparent) 30%,
      color-mix(in oklab, var(--c-ink) 50%, transparent) 60%,
      transparent 100%
    );
    pointer-events: none;
  }

  .hero__frame {
    grid-row: 2;
    position: relative;
    z-index: var(--z-fore);
    padding: 0 var(--gutter) clamp(var(--sp-8), 6vh, var(--sp-16));
    /* The title must stop before the dancer, who stands from about 42% of the
       width rightward. A measure in `ch` would drift with the font size; a
       fraction of the viewport holds the relationship to the subject. */
    max-width: min(48rem, 54vw);
  }

  .hero__eyebrow {
    display: flex;
    align-items: baseline;
    gap: var(--sp-4);
    margin-bottom: var(--sp-6);
    color: var(--c-morning);
  }

  .hero__eyebrow-latin {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
  }

  .hero__eyebrow .f-hi { font-size: 1.05em; opacity: 0.75; }

  /* The title is the largest thing on the site and the only place tracking is
   * pulled this tight. Each line is its own block so `across` can be indented
   * off the axis — the word means displacement, so it is displaced.
   *
   * The line breaks are authored in the markup rather than left to the browser,
   * so the font size can shrink to fit its box instead of the box growing to fit
   * the words. That is what keeps the title clear of the dancer at every width.
   */
  .hero__title {
    font-family: "Playfair Display", Georgia, serif;
    font-size: clamp(2.5rem, min(7vw, 10vh), 5.5rem);
    font-weight: 500;
    line-height: 0.98;
    letter-spacing: -0.025em;
    text-transform: uppercase;
    color: var(--c-paper);
    text-shadow: 0 0.06em 0.5em color-mix(in oklab, var(--c-ink) 55%, transparent);
    text-align: left;
  }

  .hero__title-line {
    display: block;
    white-space: nowrap;
    padding-left: 0;
    margin-left: 0;
  }

  .hero__title-line--turn {
    padding-left: 0;
  }

  .hero__title em {
    font-style: italic;
    font-weight: 400;
    color: var(--c-morning);
  }

  .hero__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    justify-content: space-between;
    gap: var(--sp-6) var(--sp-12);
    margin-top: clamp(var(--sp-6), 4vh, var(--sp-12));
  }

  .hero__lede {
    font-family: "Playfair Display", "Montserrat", Georgia, serif;
    font-size: var(--t-sm);
    letter-spacing: 0.03em;
    line-height: 1.6;
    max-width: 58ch;
    color: color-mix(in oklab, var(--c-paper) 88%, transparent);
  }

  .hero__lede i { font-style: italic; color: var(--c-morning); }

  .hero__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-4) var(--sp-8);
    align-items: center;
  }

  /* The edition line runs up the right margin — the one piece of vertical type
   * on the page, so it stays a deliberate gesture rather than a habit.
   *
   * It lives inside `.hero__frame` in the markup so that on a phone it can
   * simply become the first line of the text block. Here it is lifted out to the
   * hero's own right edge, which is why the inset is `fixed`-like rather than
   * relative to the frame: `.hero` is the containing block.
   */
  .hero__edition {
    position: absolute;
    top: clamp(5rem, 14vh, 9rem);
    right: var(--gutter);
    z-index: var(--z-note);
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    writing-mode: vertical-rl;
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: color-mix(in oklab, var(--c-paper) 60%, transparent);
  }

  .hero__edition-no {
    font-weight: 600;
    color: var(--c-morning);
    letter-spacing: 0.16em;
  }

  .hero__edition-note { font-style: italic; letter-spacing: 0.1em; text-transform: none; }

  .hero__credit {
    position: absolute;
    left: var(--gutter);
    bottom: clamp(var(--sp-3), 2vh, var(--sp-4));
    z-index: var(--z-note);
    font-family: "Archivo", sans-serif;
    font-size: 0.625rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: color-mix(in oklab, var(--c-paper) 38%, transparent);
  }

  /* ── Hero · phone ──
   * A different composition, not a narrower one.
   *
   * The tall cut of the video puts the dancer's face in the upper half, and at
   * 390px there is no margin to set type beside her — so the phone hero is
   * stacked rather than overlaid: her face owns the top 46% of the screen, the
   * type owns the bottom, and the gradient does the join. Nothing crosses her.
   *
   * The rows are explicit for that reason. `1fr auto auto` would let the type
   * grow upward into her face as the lede wraps; a fixed photographic band
   * cannot be encroached on.
   */
  @media (max-width: 700px) {
    .hero {
      grid-template-rows: 46svh auto auto;
    }

    /* Swap to the portrait mobile clip. */
    .hero__video--desktop { display: none; }
    .hero__video--mobile  { display: block; object-position: 50% 40%; }

    .hero__arch {
      width: 78vw;
      height: 40%;
      top: 4%;
      opacity: 0.45;
    }

    /* The gradient starts where the photographic band ends, so it reads as the
       floor the type stands on rather than a veil over the subject. */
    .hero::after {
      height: 66%;
      background: linear-gradient(
        to top,
        var(--c-ink) 0%,
        var(--c-ink) 40%,
        color-mix(in oklab, var(--c-ink) 75%, transparent) 65%,
        transparent 100%
      );
    }

    .hero__frame {
      grid-row: 3;
      max-width: none;
      padding-bottom: clamp(var(--sp-6), 4vh, var(--sp-10));
    }

    .hero__title {
      line-height: 0.94;
      letter-spacing: -0.02em;
    }

    .hero__title-line--turn { padding-left: 0; }

    .hero__eyebrow { margin-bottom: var(--sp-4); }

    .hero__foot {
      display: grid;
      gap: var(--sp-6);
      margin-top: var(--sp-6);
    }

    .hero__lede { max-width: none; }

    .hero__actions {
      display: grid;
      justify-items: start;
      gap: var(--sp-4);
    }

    /* Vertical type is a desktop-margin gesture. On a phone the right edge is
       needed for the thumb and the line would cross the title, so it drops into
       the flow as a normal grid row above the text block — same element, a
       different composition, no duplicated markup. */
    .hero__edition {
      writing-mode: horizontal-tb;
      position: static;
      grid-row: 2;
      align-self: end;
      padding: 0 var(--gutter) var(--sp-4);
      gap: var(--sp-2) var(--sp-3);
      flex-wrap: wrap;
    }

    .hero__credit { display: none; }
  }

  /* ── Link with an arrow ───────────────────────────────────────────────────
   *
   * The site's standard forward link, on any hour. The rule under it is drawn
   * with a background gradient rather than a border so it can animate its width
   * without the text moving, and it starts at partial width — the link looks
   * unfinished until you touch it, which is the whole invitation.
   *
   * Replaces `.hero__cta`, which was hero-specific and then used on two other
   * sections anyway.
   */
  .link-arrow {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-3);
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    text-decoration: none;
    color: inherit;
    padding-bottom: var(--sp-2);
    background-image: linear-gradient(currentColor, currentColor);
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 100% 1px;
    transition: color var(--dur-fast) var(--ease-out);
  }

  .link-arrow svg {
    flex-shrink: 0;
    transition: transform var(--dur-mid) var(--ease-out);
  }

  .link-arrow:hover { color: var(--accent); }
  .link-arrow:hover svg { transform: translateX(0.3rem); }

  /* A 26px-tall inline link is a hard target on a phone. The extra height is
     added above the text so the underline stays where the type sits. */
  @media (hover: none) {
    .link-arrow {
      padding-top: var(--sp-4);
      min-height: 44px;
      align-items: end;
    }
  }

  /* The primary of a pair: the rule is drawn in the accent and sits full width,
     so the hierarchy between two adjacent links is visible without a button. */
  .link-arrow--lead {
    color: var(--accent);
  }

  /* On the hero the accent is morning gold against ink, and the secondary link
     needs to recede without becoming unreadable. */
  .hero .link-arrow { color: color-mix(in oklab, var(--c-paper) 68%, transparent); }
  .hero .link-arrow--lead { color: var(--c-morning); }
  .hero .link-arrow:hover { color: var(--c-paper); }

  /* ── Voice index row (typographic list) ── */
  .voice-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    gap: var(--sp-4);
    padding-block: var(--sp-4);
    border-bottom: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
    text-decoration: none;
    color: inherit;
    transition: color var(--dur-fast) var(--ease-out);
  }

  .voice-row:hover { color: var(--accent); }

  /* Below ~360px the name and its metadata cannot share a line without one of
     them being clipped, so the metadata drops beneath the name. */
  @media (max-width: 380px) {
    .voice-row {
      grid-template-columns: minmax(0, 1fr);
      gap: var(--sp-1);
    }
  }

  .voice-row__name {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-lg);
    font-style: italic;
    line-height: 1.1;
  }

  .voice-row__meta {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--mid);
    white-space: nowrap;
  }

  /* ── Session card (programme) ──
   *
   * Time in a fixed column, the session beside it. The body is held to a reading
   * measure rather than being allowed to run the full field — a blurb set to 120
   * characters is a blurb nobody finishes.
   *
   * The rule is derived from `--fg` so it works on any hour. It used to be
   * `rgba(0,0,0,0.08)` with two hard-coded overrides for the dark hours, which
   * was three rules doing what one can.
   */
  .session {
    display: grid;
    grid-template-columns: 5rem minmax(0, 1fr);
    gap: var(--sp-4);
    padding-block: var(--sp-6);
    border-top: 1px solid color-mix(in oklab, var(--fg) 18%, transparent);
  }

  .session__time {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-sm);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    padding-top: 0.2em;
    color: var(--accent);
  }

  .session__title {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-md);
    font-weight: 500;
    line-height: 1.2;
    text-wrap: balance;
  }

  .session__blurb {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-sm);
    color: var(--mid);
    margin-top: var(--sp-2);
    line-height: 1.6;
    /* A blurb that runs the full width of a wide field is a blurb nobody
       finishes. */
    max-width: 62ch;
  }

  .session__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-2) var(--sp-4);
    margin-top: var(--sp-3);
  }

  /* Metadata, not chips. These were bordered boxes at 50% opacity — a container
     around two words, repeated twice per session, forty times on the programme
     page. The venue and the strand are labels; they are set as labels, separated
     by a rule rather than boxed. */
  .session__tag {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: color-mix(in oklab, var(--fg) 62%, var(--bg));
  }

  .session__tag + .session__tag::before {
    content: "";
    display: inline-block;
    width: 1px;
    height: 0.75em;
    margin-right: var(--sp-4);
    vertical-align: -0.05em;
    background: color-mix(in oklab, var(--fg) 30%, transparent);
  }

  /* The booking tag is the one piece of metadata that is an instruction rather
     than a description, so it is the one that carries the accent. */
  .session__tag--book {
    color: var(--accent);
    font-weight: 600;
  }

  .session--provisional .session__title::after {
    content: " ·";
    color: var(--accent);
    font-style: italic;
  }

  /* ── Button ───────────────────────────────────────────────────────────────
   *
   * The site has exactly one button shape and it is a rectangle with a hairline.
   * No radius, no shadow, no gradient — the arch is the only curve in the design
   * language, and spending it on a CTA would waste it.
   *
   * `--btn-fg`/`--btn-bg` let a button on a photograph override its colours
   * without a second class.
   */
  .btn {
    --btn-fg: var(--fg);
    --btn-bg: transparent;

    display: inline-flex;
    align-items: center;
    gap: var(--sp-3);
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    text-decoration: none;
    padding: var(--sp-4) var(--sp-8);
    border: 1px solid currentColor;
    color: var(--btn-fg);
    background: var(--btn-bg);
    cursor: pointer;
    /* The fill grows from the bottom edge rather than the whole box switching
       colour — a wipe, which is the site's motion verb, not a state flip. */
    background-image: linear-gradient(var(--fg), var(--fg));
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 100% 0;
    transition: background-size var(--dur-mid) var(--ease-out),
                color var(--dur-mid) var(--ease-out);
  }

  .btn svg { transition: transform var(--dur-mid) var(--ease-out); }

  .btn:hover {
    background-size: 100% 100%;
    color: var(--bg);
  }

  .btn:hover svg { transform: translateX(0.3rem); }

  /* The primary action: filled from the start, in `--fg` on the hour's ground.
     Hover wipes the accent up over it, same direction as the outline variant. */
  .btn--fill {
    color: var(--bg);
    border-color: var(--fg);
    background-color: var(--fg);
    background-image: linear-gradient(var(--accent), var(--accent));
    background-size: 100% 0;
  }

  .btn--fill:hover {
    background-size: 100% 100%;
    border-color: var(--accent);
    color: var(--bg);
  }

  /* ── Footer ── */
  .site-footer {
    background: var(--c-ink);
    color: var(--c-paper);
    padding: var(--sp-16) var(--gutter) var(--sp-8);
  }

  .site-footer__grid {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--sp-12);
    padding-bottom: var(--sp-12);
    border-bottom: 1px solid color-mix(in oklab, var(--fg) 15%, transparent);
  }

  @media (max-width: 700px) {
    .site-footer__grid { grid-template-columns: 1fr; gap: var(--sp-8); }
  }

  .site-footer__tagline {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-lg);
    font-style: italic;
    line-height: 1.3;
    margin-bottom: var(--sp-6);
  }

  .site-footer__address {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    color: var(--mid);
    line-height: 1.8;
    font-style: normal;
  }

  .site-footer__heading {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--mid);
    margin-bottom: var(--sp-4);
  }

  .site-footer__links {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
  }

  .site-footer__link {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-sm);
    text-decoration: none;
    color: color-mix(in oklab, var(--fg) 82%, var(--bg));
    transition: color var(--dur-fast) var(--ease-out);
  }

  .site-footer__link:hover { color: var(--c-noon); }

  /* On a touch device the footer's link list is a column of 15px-tall targets.
     Padding them out to 44px is the difference between a usable footer and a
     lottery — and it costs nothing on a pointer device, which keeps the desktop
     list tight. */
  @media (hover: none) {
    .site-footer__links { gap: 0; }
    .site-footer__link {
      display: inline-flex;
      align-items: center;
      min-height: 44px;
    }
  }

  .site-footer__bottom {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-top: var(--sp-8);
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    color: var(--mid);
  }

  /* ── Provisional notice ── */
  .notice {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    letter-spacing: 0.06em;
    padding: var(--sp-3) var(--sp-4);
    border-left: 2px solid var(--accent);
    color: var(--mid);
    margin-bottom: var(--sp-8);
  }

  /* ── Generic card / tile ── */
  .card {
    padding: var(--sp-6);
    background: color-mix(in oklab, var(--fg) 3.5%, transparent);
    border-top: 2px solid color-mix(in oklab, var(--accent) 50%, transparent);
  }

  /* ── Sponsors / Partners Roster ── */
  .sponsors-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 15.5rem), 1fr));
    gap: var(--gutter);
    margin-top: var(--sp-10);
    list-style: none;
    padding: 0;
  }

  .sponsor-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    padding: var(--sp-6) var(--sp-5);
    background: #FFFFFF;
    border: 1px solid color-mix(in oklab, var(--c-ink) 12%, transparent);
    border-top: 3px solid var(--accent);
    border-radius: var(--radius-sm, 4px);
    text-align: left;
    text-decoration: none;
    color: inherit;
    transition: transform var(--dur-mid) var(--ease-out),
                box-shadow var(--dur-mid) var(--ease-out),
                border-color var(--dur-mid) var(--ease-out);
    position: relative;
  }

  .sponsor-card:hover {
    transform: translateY(-3px);
    border-color: color-mix(in oklab, var(--accent) 70%, transparent);
    box-shadow: 0 10px 24px -8px color-mix(in oklab, var(--c-ink) 16%, transparent);
  }

  .sponsor-card--title {
    border-top-color: #B8860B;
    background: linear-gradient(170deg, #FFFDF8 0%, #FFFFFF 100%);
  }

  .sponsor-card--powering {
    border-top-color: #2E6B4F;
    background: linear-gradient(170deg, #F8FBF9 0%, #FFFFFF 100%);
  }

  .sponsor-card--co-sponsor {
    border-top-color: color-mix(in oklab, var(--c-ink) 30%, var(--accent));
  }

  .sponsor-card--partner {
    border-top-color: color-mix(in oklab, var(--accent) 50%, transparent);
  }

  .sponsor-card__tier {
    display: inline-block;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    color: var(--accent);
    padding: 0.2rem 0.5rem;
    background: color-mix(in oklab, var(--accent) 10%, transparent);
    border-radius: 2px;
    margin-bottom: var(--sp-3);
  }

  .sponsor-card--title .sponsor-card__tier {
    color: #8C6200;
    background: rgba(184, 134, 11, 0.12);
  }

  .sponsor-card--powering .sponsor-card__tier {
    color: #1E5338;
    background: rgba(46, 107, 79, 0.12);
  }

  .sponsor-card__name {
    display: block;
    font-size: var(--t-md);
    font-weight: 600;
    line-height: 1.2;
    color: var(--c-ink);
    margin: 0;
  }

  .sponsor-card__sub {
    display: block;
    font-size: var(--t-xs);
    color: color-mix(in oklab, var(--c-ink) 60%, transparent);
    margin-top: var(--sp-1);
  }

  .sponsor-card__desc {
    font-size: var(--t-xs);
    line-height: 1.55;
    color: color-mix(in oklab, var(--c-ink) 75%, transparent);
    margin-top: var(--sp-3);
    margin-bottom: 0;
  }

  @media (max-width: 600px) {
    .sponsors-grid {
      grid-template-columns: 1fr;
      gap: var(--sp-4);
    }
    .sponsor-card {
      padding: var(--sp-5) var(--sp-4);
    }
  }

  /* ── YouTube Section ── */
  .yt-section {
    position: relative;
    overflow: hidden;
  }

  .yt-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
    gap: var(--gutter);
    align-items: start;
  }

  .yt-featured {
    display: flex;
    flex-direction: column;
  }

  .yt-sidebar {
    display: flex;
    flex-direction: column;
  }

  .yt-player-wrap {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    background: #000000;
    border-radius: var(--radius-sm, 4px);
    overflow: hidden;
    border: 1px solid color-mix(in oklab, var(--fg) 14%, transparent);
    box-shadow: 0 16px 36px -12px color-mix(in oklab, var(--c-ink) 35%, transparent);
  }

  .yt-player-wrap iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
  }

  .yt-featured__meta {
    margin-top: var(--sp-4);
  }

  .yt-featured__title {
    font-size: var(--t-md);
    margin-top: var(--sp-1);
    color: var(--fg);
    line-height: 1.3;
  }

  .yt-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
  }

  .yt-list__item {
    display: block;
  }

  .yt-item {
    display: block;
    padding: var(--sp-4);
    background: color-mix(in oklab, var(--fg) 4%, transparent);
    border: 1px solid color-mix(in oklab, var(--fg) 10%, transparent);
    border-radius: var(--radius-sm, 4px);
    text-decoration: none;
    color: inherit;
    transition: background var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out),
                transform var(--dur-fast) var(--ease-out);
  }

  .yt-item:hover {
    background: color-mix(in oklab, var(--fg) 8%, transparent);
    border-color: color-mix(in oklab, var(--accent) 70%, transparent);
    transform: translateX(4px);
  }

  .yt-item__badge {
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    color: var(--accent);
    display: block;
  }

  .yt-item__title {
    font-size: var(--t-sm);
    font-weight: 600;
    margin: var(--sp-1) 0;
    color: var(--fg);
    line-height: 1.3;
  }

  .yt-item__desc {
    font-size: var(--t-xs);
    color: var(--mid);
    margin: 0;
    line-height: 1.45;
  }

  .yt-item__cta {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    font-size: 0.75rem;
    color: var(--accent);
    margin-top: var(--sp-2);
  }

  .yt-item__cta svg {
    width: 0.875rem;
    height: 0.875rem;
  }

  @media (max-width: 900px) {
    .yt-layout {
      grid-template-columns: 1fr;
      gap: var(--sp-6);
    }
  }
}

/* ── motifs ────────────────────────────────────────────────────────────────
 *
 * Decorative, unsemantic PNGs that hang in the margins or behind text to
 * provide texture.
 */
.motif-img {
  position: absolute;
  pointer-events: none;
  user-select: none;
  opacity: 0.12;
  mix-blend-mode: multiply;
  z-index: 0;
}

[data-hour="night"] .motif-img,
[data-hour="ink"] .motif-img,
[data-hour="dusk"] .motif-img {
  mix-blend-mode: screen;
  opacity: 0.1;
}

/* ── Metaphor · Page compositions ────────────────────────────────────────────
 *
 * Everything here is layout for a specific place on a specific page. Components
 * stay generic; this file is where they are arranged.
 */

@layer components {

  /* ── the thesis spread ────────────────────────────────────────────────────
   *
   * The quiet field after the cover. Its job is air: one statement, a narrow
   * column of argument, and the glossary hanging in the margin.
   *
   * The 12-column grid is used as an editorial grid rather than a layout grid —
   * the statement starts at column 1 and stops at 9, the argument sits under it
   * at 1–6, and the lexicon occupies 9–13 and starts lower than both. Nothing is
   * centred and nothing fills the full measure, which is what makes the white
   * space read as intentional rather than as a gap.
   */
  .spread__grid {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: 0 var(--gutter);
    align-items: start;
  }

  .spread__kicker {
    grid-column: 1 / -1;
    margin-bottom: var(--sp-8);
    text-align: center;
  }

  .spread__statement {
    grid-column: 2 / 12;
    text-align: center;
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-xl);
    font-weight: 400;
    line-height: 1.04;
    letter-spacing: -0.02em;
    text-wrap: balance;
    margin-bottom: var(--sp-16);
  }

  .spread__statement em {
    font-style: italic;
    color: var(--accent);
  }

  .spread__body {
    grid-column: 3 / 11;
    margin-inline: auto;
    text-align: center;
  }

  /* The glossary sits in the outer margin alongside the statement, dropped by
     one line so the two do not start on the same baseline. Marginalia, not a
     sidebar: it is set against the statement, which is why it shares that row
     rather than waiting until the prose begins. */
  .spread__lexicon {
    grid-column: 9 / 13;
    grid-row: 2 / span 2;
    margin-top: var(--sp-6);
  }

  @media (max-width: 1100px) {
    .spread__statement { grid-column: 1 / -1; margin-bottom: var(--sp-8); }
    .spread__body { grid-column: 1 / -1; }
    .spread__lexicon { grid-column: 1 / -1; grid-row: auto; margin-top: var(--sp-12); }
  }

  /* ── the ledger ───────────────────────────────────────────────────────────
   *
   * Four numbers. The figures take the largest step on the type scale and the
   * labels the smallest — 11px beside ~90px — which is the contrast the scale
   * exists to make possible.
   *
   * Ruled rather than boxed: a hairline above each item, so the band reads as
   * accounting paper. Stat cards would have been four containers, and a number
   * does not need a container.
   */
  .ledger {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--gutter);
  }

  .ledger__item {
    display: grid;
    gap: var(--sp-3);
    padding-top: var(--sp-4);
    border-top: 1px solid color-mix(in oklab, var(--fg) 30%, transparent);
  }

  .ledger__figure {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-2xl);
    font-weight: 500;
    line-height: 0.9;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums lining-nums;
  }

  .ledger__label {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--mid);
    /* Wide enough that "seats in the Baradari" and "years, since 2013" each set
       on one line at desktop widths. At 16ch they broke after the preposition,
       which reads as a mistake rather than as a line break. */
    max-width: 22ch;
    text-wrap: balance;
  }

  @media (max-width: 700px) {
    .ledger { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: var(--sp-10); }
  }

  /* ── the roster ───────────────────────────────────────────────────────────
   *
   * One portrait at a size where a face reads, and the rest as a list.
   *
   * The previous version was six 200px cards side by side. Most of the archive's
   * photographs are group shots taken from the floor, so at card size you could
   * not tell which person was the one named underneath. One large portrait plus
   * a list is both a stronger composition and a more honest one.
   */
  .roster {
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    gap: var(--sp-12) var(--sp-16);
    align-items: start;
  }

  .roster__intro { grid-column: 1 / -1; }

  .roster__title {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-xl);
    font-weight: 400;
    line-height: 1;
    letter-spacing: -0.02em;
    margin-top: var(--sp-4);
  }

  .roster__title em { font-style: italic; color: var(--accent); }

  .roster__lead {
    display: block;
    text-decoration: none;
    color: inherit;
  }

  .roster__lead-img {
    display: block;
    overflow: hidden;
    background: color-mix(in oklab, var(--fg) 10%, var(--bg));
  }

  .roster__lead-img img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: scale var(--dur-slow) var(--ease-out);
  }

  .roster__lead:hover .roster__lead-img img { scale: 1.03; }

  .roster__lead-body {
    display: grid;
    gap: var(--sp-1);
    padding-top: var(--sp-4);
  }

  .roster__lead-name {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-lg);
    line-height: 1.05;
  }

  .roster__lead-role,
  .roster__row-role {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    color: var(--mid);
  }

  .roster__lead-years,
  .roster__row-years {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    letter-spacing: 0.1em;
    color: var(--accent);
  }

  /* The list is set to the same optical weight as the portrait's caption, so the
     two halves of the section read as one object rather than as a feature plus
     a leftovers column. */
  .roster__list {
    display: grid;
    align-content: start;
  }

  .roster__row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-2) var(--sp-6);
    padding-block: var(--sp-5);
    border-top: 1px solid color-mix(in oklab, var(--fg) 22%, transparent);
    text-decoration: none;
    color: inherit;
    transition: color var(--dur-fast) var(--ease-out),
                padding-left var(--dur-mid) var(--ease-out);
  }

  .roster__row:hover {
    color: var(--accent);
    padding-left: var(--sp-3);
  }

  .roster__row-name {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-md);
    line-height: 1.1;
  }

  .roster__row-meta {
    display: flex;
    align-items: baseline;
    gap: var(--sp-4);
  }

  .roster__more {
    padding-top: var(--sp-8);
    border-top: 1px solid color-mix(in oklab, var(--fg) 22%, transparent);
  }

  @media (max-width: 1100px) {
    .roster { grid-template-columns: minmax(0, 1fr); gap: var(--sp-10); }
  }

  /* ── the schedule ─────────────────────────────────────────────────────────
   *
   * The dense field. Everything else on the page breathes; this one is close-set
   * on purpose, because a schedule is information and should look like it.
   *
   * The title and the standing note share the top band — the note in the outer
   * margin, where a printed programme would put a footnote about running times.
   * The sessions then run the full measure beneath both, because a list of five
   * items squeezed into seven columns leaves a dead column beside it.
   */
  .schedule {
    display: grid;
    grid-template-columns: minmax(0, 7fr) minmax(0, 4fr);
    gap: var(--sp-12) var(--sp-16);
    align-items: end;
  }

  .schedule__head { grid-column: 1; grid-row: 1; }

  .schedule__title {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-xl);
    font-weight: 400;
    line-height: 1;
    letter-spacing: -0.02em;
    margin-top: var(--sp-4);
  }

  .schedule__title em { font-style: italic; color: var(--accent); }

  .schedule__note {
    grid-column: 2;
    grid-row: 1;
    font-family: "Playfair Display", Georgia, serif;
    font-style: italic;
    font-size: var(--t-sm);
    line-height: 1.6;
    max-width: 30ch;
    padding-top: var(--sp-4);
    border-top: 1px solid color-mix(in oklab, var(--fg) 30%, transparent);
    color: color-mix(in oklab, var(--fg) 76%, var(--bg));
  }

  .schedule__body { grid-column: 1 / -1; grid-row: 2; }

  @media (max-width: 1100px) {
    .schedule { grid-template-columns: minmax(0, 1fr); gap: var(--sp-8); }
    .schedule__note { grid-column: 1; grid-row: auto; max-width: none; }
    .schedule__body { grid-column: 1; grid-row: auto; }
  }

  /* ── the years ────────────────────────────────────────────────────────────
   *
   * A chronology, read left to right, running off the right edge of the screen
   * so it is visibly a run rather than a complete set of five.
   *
   * The year sits over the foot of its own photograph, where these images are
   * darkest and least interesting — a stage floor or an empty row of seats. The
   * numeral is the target; the picture tells you what the year looked like.
   */
  .years {
    display: flex;
    gap: var(--gutter);
    overflow-x: auto;
    scrollbar-width: none;
    /* The container's max-width is centred, so the run starts on the same axis
       rather than at the viewport edge.
     *
     * `scroll-snap-type` is deliberately absent. With snapping on, the browser
     * settles the first item against the snap port on load, which scrolls the
     * left padding away and clipped "2024" against the window edge. Free
     * scrolling keeps the indent, and a five-item run does not need snapping to
     * feel controlled. */
    padding-left: max(var(--gutter), calc((100vw - var(--col-max)) / 2 + var(--gutter)));
    padding-right: var(--gutter);
  }

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

  .years__item {
    flex: 0 0 clamp(12rem, 18vw, 17rem);
  }

  .years__title {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-xl);
    font-weight: 400;
    line-height: 1;
    letter-spacing: -0.02em;
    margin-top: var(--sp-4);
  }

  .years__title em { font-style: italic; color: var(--accent); }

  .year {
    display: grid;
    gap: var(--sp-4);
    text-decoration: none;
    color: inherit;
  }

  .year__frame {
    position: relative;
    display: block;
    overflow: hidden;
    background: color-mix(in oklab, var(--fg) 10%, var(--bg));
  }

  .year__img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: saturate(0.85);
    transition: scale var(--dur-slow) var(--ease-out),
                filter var(--dur-slow) var(--ease-out);
  }

  .year:hover .year__img { scale: 1.04; filter: saturate(1); }

  /* Sat on the floor of the frame with a gradient behind it, so the numeral is
     legible over any of the five photographs without dimming the whole image. */
  .year__no {
    position: absolute;
    inset: auto 0 0;
    z-index: var(--z-fore);
    padding: var(--sp-16) var(--sp-4) var(--sp-3);
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-2xl);
    font-weight: 500;
    line-height: 0.85;
    letter-spacing: -0.03em;
    color: var(--c-paper);
    font-variant-numeric: tabular-nums lining-nums;
    background: linear-gradient(
      to top,
      color-mix(in oklab, var(--c-ink) 82%, transparent) 0%,
      transparent 100%
    );
  }

  .year__headline {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    line-height: 1.5;
    color: var(--mid);
    max-width: 24ch;
  }

  /* ── lexicon (the glossary block) ── */
  .lexicon {
    border-top: 1px solid var(--fg);
    padding-top: var(--sp-4);
  }
  .lexicon__row {
    display: grid;
    gap: var(--sp-1);
    padding-block: var(--sp-4);
    border-bottom: 1px solid color-mix(in srgb, var(--fg) 12%, transparent);
  }
  .lexicon__word {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-4);
    font-size: var(--t-md);
    line-height: 1.4;
  }
  .lexicon__latin {
    color: var(--accent);
    font-size: var(--t-sm);
  }
  .lexicon__native {
    font-size: var(--t-md);
    color: var(--fg);
    opacity: 0.75;
  }
  .lexicon__gloss { font-style: italic; }

  .home__aside { align-self: end; }
  .home__aside-note { margin-top: var(--sp-4); font-style: italic; max-width: 32ch; }

  /* ── full-bleed figure ──
   * The one photograph on the page that gets the whole width and nothing on top
   * of it. The caption sits underneath: a picture of a full house is the
   * argument, and covering it with a label would argue against the point.
   */
  .figure-bleed {
    padding-bottom: var(--sp-6);
  }

  .figure-bleed__img { width: 100%; }

  .figure-bleed__caption {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-6);
    align-items: baseline;
    padding-top: var(--sp-4);
  }

  .figure-bleed__caption .caps { color: var(--accent); }

  .figure-bleed__note {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    color: var(--mid);
    max-width: 52ch;
  }

  /* ── stitch rule, as a section marker ── */
  .stitch-run {
    width: 100%;
    height: 8px;
    color: var(--fg);
    opacity: 0.35;
    margin-bottom: var(--sp-12);
  }
  /* ── section header (title left, action right) ── */
  .section-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--sp-8);
    margin-bottom: var(--sp-12);
    flex-wrap: wrap;
  }

  /* ── shelves ── */
  .session-list { margin-block: var(--sp-8); }

  /* ── poster section body ── */
  .poster__media {
    position: absolute;
    inset: 0;
    z-index: var(--z-under);
  }
  .poster__media::after {
    content: "";
    position: absolute;
    inset: 0;
    /* Not a scrim over type — the type sits below it, on solid colour. This only
       carries the photograph down into the field so the seam is not a hard edge. */
    background: linear-gradient(to bottom, transparent 20%, var(--bg) 88%);
  }
  .poster__body { position: relative; z-index: var(--z-fore); }
  .poster__title { margin-block: var(--sp-6) var(--sp-8); }
  .poster__title em { font-style: italic; color: var(--accent); }
  .poster__prose { margin-bottom: var(--sp-8); }

  /* ── closing section ── */
  .closer { text-align: center; }
  .closer__mark {
    width: 80px;
    height: auto;
    display: block;
    margin-inline: auto;
    margin-bottom: var(--sp-6);
  }
  .closer__title { margin-block: var(--sp-4) var(--sp-8); }
  .closer__title em { font-style: italic; color: var(--accent); }
  .closer__prose { margin-bottom: var(--sp-12); }
  .closer__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-4);
    justify-content: center;
  }
  .closer__note { margin-top: var(--sp-8); max-width: 44ch; }

  /* ── nav overlay utility row ── */
  .nav-overlay__utility {
    display: flex;
    gap: var(--sp-6);
    align-items: center;
  }
  .nav-overlay__utility-link {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--c-paper);
    transition: color var(--dur-fast) var(--ease-out);
  }
  .nav-overlay__utility-link:hover { color: var(--c-noon); }

  /* ── shared page furniture ──────────────────────────────────────────────
   * Every page except the home page and the archive doors opens the same way:
   * a colour field, a kicker, one enormous line, and a lede that stops well
   * short of the measure. Consistency here is what lets the individual pages
   * be strange further down.
   */
  .page-head {
    background: var(--bg);
    color: var(--fg);
    padding-block: calc(var(--sp-32) + 4rem) var(--sp-24);
  }

  .page-head__title {
    margin-block: var(--sp-4) var(--sp-8);
    max-width: 18ch;
    text-wrap: balance;
  }
  .page-head__title em { font-style: italic; color: var(--accent); }

  .page-head__lede {
    max-width: 54ch;
    font-size: var(--t-md);
    line-height: 1.5;
    opacity: 0.85;
  }

  /* A notice that has a column to itself rather than sitting beside prose. */
  .notice--wide {
    max-width: 60ch;
    font-size: var(--t-sm);
    letter-spacing: 0.02em;
    line-height: 1.6;
  }

  /* Stitch-rule variants. --short is a rule that stops; --demo is the one on
     the Lucknow page, blown up so the stitch is legible as a stitch. */
  .stitch-run--short { max-width: 12rem; }
  .stitch-run--demo {
    height: 1.5rem;
    margin-block: var(--sp-8) var(--sp-3);
    opacity: 1;
  }

  /* ── home page roster grid ──────────────────────────────────────────────────
   * 16 columns by 4 rows. Far right can bleed off screen.
   */
  .voice-grid--home {
    /* 16 columns, each a fixed width so they don't squish */
    grid-template-columns: repeat(16, minmax(12rem, 1fr));
    gap: var(--sp-8) var(--sp-4);
    
    /* Allow it to bleed off the right edge */
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: var(--sp-4);
    
    /* Optional: make it scroll smoothly like a carousel */
    scroll-snap-type: x mandatory;
    scrollbar-width: thin;
  }
  
  .voice-grid--home .voice-tile {
    scroll-snap-align: start;
  }
}

/* Per-page compositions. Each one only ever styles classes that appear on its
   own pages, so they can be read — and deleted — independently. */

/* ── Metaphor · Voices ───────────────────────────────────────────────────────
 *
 * Two pages: the index and the person.
 *
 * The index is one grid holding the whole roster, so the answer to "who has
 * spoken here" is a single screen of faces rather than twenty separate visits.
 * Half the roster has no photograph, and those tiles become colour plates in the
 * hour of the strand the person spoke in — the same box and the same weight,
 * carrying a letter instead of a face.
 *
 * The person page is one photograph, one claim, and the trail back into the
 * archive. Nothing on it is invented: where the record is thin, the page says
 * so rather than filling the space.
 */

@layer components {

  /* ── index: search ── */
  .voice-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--sp-6);
    margin-bottom: var(--sp-12);
  }

  .voice-controls__count {
    color: var(--mid);
    font-variant-numeric: tabular-nums;
    padding-bottom: var(--sp-2);
  }

  /* ── index: the grid ──
   * Four across on a wide screen, two on a phone. The columns are wide enough
   * that a face reads as a face — the failure of the old contact sheet was
   * 200px thumbnails of group photographs where you could not tell which person
   * was being named.
   */
  .voice-grid {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(15rem, 42vw), 1fr));
    gap: var(--sp-12) var(--sp-6);
  }

  .voice-tile__link {
    display: grid;
    grid-template-rows: auto 1fr;
    gap: var(--sp-4);
    text-decoration: none;
    color: inherit;
  }

  /* The frame is the constant: photograph or colour plate, the box is identical,
     so the grid holds its rhythm regardless of what the archive happens to have. */
  .voice-tile__frame {
    position: relative;
    display: block;
    aspect-ratio: 3/4;
    overflow: hidden;
    background: color-mix(in oklab, var(--fg) 6%, transparent);
  }

  .voice-tile__img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--dur-slow) var(--ease-out);
  }

  @media (prefers-reduced-motion: no-preference) {
    .voice-tile__link:hover .voice-tile__img { transform: scale(1.03); }
  }

  /* No photograph is a state, not an error: the plate paints the strand's own
     hour and carries the initial, so the tile is a designed object rather than a
     grey rectangle apologising for itself. */
  .voice-tile__plate {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    position: relative;
  }

  .voice-tile__initial {
    font-size: clamp(4rem, 9vw, 7rem);
    font-style: italic;
    line-height: 1;
    color: var(--accent);
  }

  .voice-tile__plate-mark {
    position: absolute;
    width: 38%;
    height: auto;
    color: var(--fg);
    opacity: 0.12;
  }

  /* The catalogue number, set in the corner of the frame — the device that makes
   * the grid read as an archive rather than as a team page.
   *
   * It sits on its own small scrim rather than using `mix-blend-mode: difference`,
   * which inverted against warm skin tones and printed the numerals cyan on half
   * the photographs. A blend mode here is a way of not choosing a colour. */
  .voice-tile__index {
    position: absolute;
    top: 0;
    left: 0;
    z-index: var(--z-note);
    padding: var(--sp-3) var(--sp-4);
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    font-weight: 600;
    letter-spacing: 0.1em;
    color: var(--c-paper);
    background: linear-gradient(
      135deg,
      color-mix(in oklab, var(--c-ink) 55%, transparent) 0%,
      transparent 100%
    );
  }

  /* The body is a column so the rule and its metadata pin to the bottom of the
     tile: a two-line name must not push its own footer out of line with the
     footers either side of it. */
  .voice-tile__body {
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
  }

  .voice-tile__name {
    font-size: var(--t-md);
    line-height: 1.15;
    text-wrap: balance;
    transition: color var(--dur-fast) var(--ease-out);
  }
  .voice-tile__link:hover .voice-tile__name { color: var(--accent); }

  .voice-tile__role {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    line-height: 1.4;
    color: var(--mid);
  }

  .voice-tile__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-2);
    margin-top: auto;
    padding-top: var(--sp-3);
    border-top: 1px solid color-mix(in oklab, var(--fg) 15%, transparent);
  }

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

  .voice-tile__years {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    color: var(--mid);
    letter-spacing: 0.08em;
  }

  .voice-grid__note,
  .voice-grid__empty {
    margin-top: var(--sp-16);
    max-width: 52ch;
  }
  .voice-grid__empty { margin-top: var(--sp-12); }

  @media (max-width: 640px) {
    .voice-grid { gap: var(--sp-8) var(--sp-4); }
    .voice-tile__name { font-size: var(--t-base); }
  }

  /* ── person: header ── */
  .voice__head {
    background: var(--bg);
    color: var(--fg);
    padding-block: calc(var(--sp-32) + 2rem) var(--sp-24);
  }

  .voice__intro { align-self: end; }

  .voice__back {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid currentColor;
    padding-bottom: 0.1em;
  }
  .voice__back:hover { color: var(--accent); }

  .voice__name {
    margin-block: var(--sp-4) var(--sp-6);
    text-wrap: balance;
  }

  .voice__role {
    color: var(--accent);
    font-size: var(--t-sm);
  }

  .voice__years {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-sm);
    letter-spacing: 0.14em;
    opacity: 0.6;
    margin-top: var(--sp-3);
  }

  .voice__portrait { margin: 0; }

  /* The portrait is a portrait: rectangular, uncropped by any decorative shape.
   *
   * It used to carry `.aperture`, which clipped it to the arch — and because the
   * arch narrows sharply at the shoulders, that cut the subject's face in half on
   * every voice page. The mask was destroying the one asset the page exists to
   * show. Faces stay whole; the arch does its work at the mark and on the
   * no-photograph state, where there is no face to lose. */
  .voice__portrait-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* No photograph is a state, not an error. The arch stands in for the person
     and the caption says exactly why it is there. */
  .voice__noportrait {
    display: grid;
    justify-items: center;
    align-content: center;
    gap: var(--sp-6);
    aspect-ratio: 3/4;
    padding: var(--sp-8);
    text-align: center;
    background: color-mix(in oklab, var(--fg) 7%, transparent);
  }
  .voice__noportrait-mark {
    width: 4rem;
    height: auto;
    opacity: 0.7;
  }
  .voice__noportrait .meta { max-width: 22ch; }

  /* ── person: body ── */
  .voice__note {
    font-size: var(--t-md);
    line-height: 1.45;
    max-width: 46ch;
  }

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

  .voice__cite {
    display: block;
    margin-top: var(--sp-6);
    color: var(--mid);
    font-style: normal;
  }

  .voice__subhead {
    color: var(--mid);
    padding-bottom: var(--sp-3);
    margin-bottom: var(--sp-2);
    border-bottom: 1px solid color-mix(in oklab, var(--fg) 18%, transparent);
  }

  .voice__editions { list-style: none; }

  .voice__edition {
    display: grid;
    grid-template-columns: 4.5rem 1fr;
    gap: var(--sp-3);
    align-items: baseline;
    padding-block: var(--sp-3);
    text-decoration: none;
    color: inherit;
    border-bottom: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
    transition: color var(--dur-fast) var(--ease-out);
  }
  .voice__edition:hover { color: var(--accent); }

  .voice__edition-year {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-sm);
    font-weight: 600;
    color: var(--accent);
  }

  .voice__edition-title {
    font-family: "Playfair Display", Georgia, serif;
    font-style: italic;
    font-size: var(--t-base);
  }

  /* ── prev / next ──
   * Shared with the archive. Two halves of one band, split by a stitched
   * vertical rule, each half reaching for its own edge on hover.
   */
  .voice-nav {
    display: grid;
    grid-template-columns: 1fr 1fr;
    background: var(--bg);
    color: var(--fg);
  }

  .voice-nav__link {
    display: grid;
    gap: var(--sp-2);
    padding: var(--sp-16) var(--gutter);
    text-decoration: none;
    color: inherit;
    transition: background var(--dur-mid) var(--ease-out);
  }
  .voice-nav__link .caps { color: var(--accent); }

  .voice-nav__link--next {
    text-align: right;
    justify-items: end;
    border-left: 1px solid color-mix(in oklab, var(--fg) 18%, transparent);
  }

  .voice-nav__link:hover { background: color-mix(in oklab, var(--fg) 8%, transparent); }
  .voice-nav__link--empty { opacity: 0.4; }
  .voice-nav__link--empty:hover { background: none; }

  .voice-nav__name {
    font-size: var(--t-md);
    display: inline-flex;
    align-items: center;
    gap: var(--sp-3);
  }
  .voice-nav__link svg { transition: transform var(--dur-mid) var(--ease-out); }
  .voice-nav__link:hover svg { transform: translateX(0.35rem); }

  @media (max-width: 600px) {
    .voice-nav { grid-template-columns: 1fr; }
    .voice-nav__link { padding-block: var(--sp-8); }
    .voice-nav__link--next {
      border-left: 0;
      border-top: 1px solid color-mix(in oklab, var(--fg) 18%, transparent);
    }
  }
}

/* ── Metaphor · Programme ────────────────────────────────────────────────────
 *
 * The schedule is the hardest thing on the site to lay out honestly, because
 * four rooms run at once and the reader's only real question is "which of these
 * two". So: time in the left margin, parallel sessions side by side in the
 * right, and no attempt at a calendar grid — a calendar would need to invent a
 * duration for every session, and half of these do not have one.
 */

@layer components {

  /* ── filters ──
   * Ships hidden, unhidden by app.js. A control that cannot work is worse than
   * no control, so with JS off the whole bar stays gone and the list is
   * complete.
   */
  .filter-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-8);
    padding-block: var(--sp-6) var(--sp-12);
    border-bottom: 1px solid color-mix(in oklab, var(--fg) 15%, transparent);
    margin-bottom: var(--sp-12);
  }

  .filter-set {
    border: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
  }

  .filter-set__legend {
    float: left;
    width: 100%;
    color: var(--mid);
    margin-bottom: var(--sp-3);
  }

  .chip {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    font-weight: 500;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    /* The programme's filters are the controls someone uses standing outside a
       venue looking for the next session, so they are sized for a thumb. */
    min-height: 44px;
    padding: var(--sp-2) var(--sp-4);
    background: none;
    border: 1px solid color-mix(in oklab, var(--fg) 25%, transparent);
    color: inherit;
    cursor: pointer;
    border-radius: 0;
    transition:
      background var(--dur-fast) var(--ease-out),
      border-color var(--dur-fast) var(--ease-out),
      color var(--dur-fast) var(--ease-out);
  }
  .chip:hover { border-color: var(--fg); }

  .chip[aria-pressed="true"] {
    background: var(--fg);
    border-color: var(--fg);
    color: var(--bg);
  }

  /* ── day ── */
  .day + .day { margin-top: var(--sp-24); }

  .day__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-4);
    padding-bottom: var(--sp-4);
    border-bottom: 2px solid var(--fg);
    margin-bottom: var(--sp-8);
  }

  .day__label {
    font-size: var(--t-lg);
    line-height: 1;
  }

  /* ── slot ──
   * The time is a hanging marginal. Sessions inside a slot sit in equal columns
   * so two parallel rooms read as a choice rather than a sequence.
   */
  .slot {
    display: grid;
    grid-template-columns: 7rem 1fr;
    gap: var(--sp-6);
    padding-block: var(--sp-8);
    border-top: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
  }
  .slot:first-of-type { border-top: 0; }

  .slot__time {
    font-family: "Archivo", sans-serif;
    font-weight: 600;
    display: grid;
    gap: var(--sp-2);
    align-content: start;
    position: sticky;
    top: var(--sp-16);
    height: max-content;
  }

  .slot__clock {
    font-size: var(--t-md);
    line-height: 1;
    color: var(--accent);
  }

  .slot__parallel {
    font-size: var(--t-xs);
    color: var(--mid);
    letter-spacing: 0.1em;
  }

  .slot__body {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
    gap: var(--gutter);
  }

  .slot__session {
    padding-left: var(--sp-4);
    border-left: 2px solid color-mix(in oklab, var(--accent) 55%, transparent);
  }

  .slot__title {
    font-size: var(--t-md);
    line-height: 1.2;
    margin-bottom: var(--sp-2);
  }

  /* The native-script line sits directly under the title it belongs to. */
  .slot__native {
    font-size: var(--t-sm);
    color: var(--mid);
    margin-bottom: var(--sp-2);
    width: fit-content;
    margin-inline-end: auto;
  }

  @media (max-width: 700px) {
    .slot {
      grid-template-columns: 1fr;
      gap: var(--sp-4);
    }
    .slot__time {
      position: static;
      display: flex;
      align-items: baseline;
      gap: var(--sp-4);
    }
  }

  /* ── the rooms ── */
  .venue-list {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
    gap: 1px;
    background: color-mix(in oklab, var(--fg) 18%, transparent);
    border: 1px solid color-mix(in oklab, var(--fg) 18%, transparent);
    margin-top: var(--sp-12);
  }

  .venue {
    background: var(--bg);
    padding: var(--sp-6) var(--sp-4);
    display: grid;
    gap: var(--sp-2);
    align-content: start;
  }

  .venue__name {
    font-size: var(--t-md);
    line-height: 1.1;
  }

  .venue__note {
    color: var(--accent);
    font-size: var(--t-xs);
  }

  .venue__cap { color: var(--mid); }

  /* ── held sessions ──
   * Evidence, so it gets photographs. Horizontal scroll on narrow screens
   * rather than a stack, because it is a footnote and should not out-length the
   * draft above it.
   */
  .held-list {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(16rem, 80vw), 1fr));
    gap: var(--gutter);
    margin-top: var(--sp-12);
  }

  .held {
    display: grid;
    gap: var(--sp-4);
    align-content: start;
  }

  .held__img {
    display: block;
    overflow: hidden;
  }
  .held__img img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: grayscale(0.15);
    transition: filter var(--dur-slow) var(--ease-out);
  }
  .held:hover .held__img img { filter: grayscale(0); }

  .held__body {
    display: grid;
    gap: var(--sp-2);
  }

  .held__year {
    color: var(--accent);
    font-size: var(--t-xs);
  }

  .held__title {
    font-size: var(--t-base);
    line-height: 1.25;
  }

  /* ── strand cards ── */
  .strand-card {
    padding: var(--sp-6);
    background: color-mix(in oklab, var(--fg) 3.5%, transparent);
    border-left: 3px solid color-mix(in oklab, var(--accent) 55%, transparent);
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    transition: background var(--dur-mid) var(--ease-out),
                border-left-color var(--dur-mid) var(--ease-out);
  }

  .strand-card:hover {
    background: color-mix(in oklab, var(--fg) 6%, transparent);
    border-left-color: var(--accent);
  }

  .strand-card__title {
    font-size: var(--t-md);
    line-height: 1.2;
    margin: 0;
  }

  .strand-card__body {
    font-size: var(--t-sm);
    line-height: 1.6;
    color: var(--fg);
    margin: 0;
  }
}

/* ── Metaphor · Archive ──────────────────────────────────────────────────────
 *
 * Two compositions live here.
 *
 * 1. The index: a stack of doors. Each edition is a full-width band in its own
 *    hour, so scrolling from 2024 back to 2013 walks the palette from noon to
 *    ink. The year is the largest type on the site — bigger than the headline,
 *    bigger than the h1 above it — because on this page the year *is* the
 *    subject. No cards, no shadows, no hover-lift. A door either opens or it
 *    doesn't.
 *
 * 2. One edition: a full-bleed cover, a two-column read, a facts list set like
 *    a colophon, and a mosaic of whatever photographs survived.
 *
 * Reused from elsewhere, never redefined here:
 *   .page-head*, .stitch-run   → pages.css
 *   .voice-nav*, .voice__back  → voices.css
 *   .voice-row*, .session*     → components.css
 *
 * Every .door__* child is a <span> inside an <a> on real editions, and a <p>
 * inside a <div> on the forthcoming band, so each rule sets its own display and
 * none of them assume a block-level element.
 */

@layer components {

  /* ── the stack ─────────────────────────────────────────────────────────── */

  .doors {
    display: flex;
    flex-direction: column;
  }

  /* Each door paints its own hour edge-to-edge. The 1px inset shadow of the
     next band is a hairline, not a border, so the colour fields touch. */
  .door {
    position: relative;
    display: grid;
    grid-template-columns: 46fr 54fr;
    align-items: stretch;
    gap: 0;
    background: var(--bg);
    color: var(--fg);
    text-decoration: none;
    overflow: hidden;
  }

  /* Mirror alternate doors so the media wall zig-zags down the page instead of
     forming a left-hand column. */
  .door:nth-child(even) > .door__media { order: 2; }

  .door__media {
    display: block;
    position: relative;
    min-height: 26rem;
    background: color-mix(in oklab, var(--fg) 10%, var(--bg));
  }

  .door__media img,
  .door__media picture {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .door__body {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: var(--sp-4);
    padding: var(--sp-24) clamp(var(--sp-6), 5vw, var(--sp-24));
  }

  .door__edition {
    display: block;
    font-size: var(--t-xs);
    letter-spacing: 0.12em;
    color: var(--accent);
    margin: 0 0 calc(-1 * var(--sp-2));
  }

  .door__year {
    display: block;
    font-size: var(--t-3xl);
    line-height: 0.78;
    letter-spacing: -0.03em;
    margin: 0;
  }

  .door__headline {
    display: block;
    font-size: var(--t-md);
    font-style: italic;
    line-height: 1.15;
    color: var(--accent);
    margin: 0;
    max-width: 24ch;
  }

  .door__lede {
    display: block;
    font-size: var(--t-sm);
    line-height: 1.6;
    margin: 0;
    max-width: 46ch;
    /* 78% measured 4.49:1 — a hair under AA at this size. 85% clears it and is
       still visibly quieter than the headline above. */
    color: color-mix(in oklab, var(--fg) 85%, var(--bg));
  }

  .door__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-2) var(--sp-4);
    margin: 0;
    padding-top: var(--sp-2);
  }

  .door__meta::before {
    content: "";
    display: block;
    width: 3rem;
    height: 4px;
    margin-bottom: var(--sp-2);
    background-image: var(--stitch);
    opacity: 0.4;
    flex-basis: 100%;
  }

  .door__cta {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    margin-top: var(--sp-4);
    padding-bottom: 2px;
    border-bottom: 1px solid color-mix(in oklab, currentColor 40%, transparent);
    font-size: var(--t-xs);
  }

  .door__cta svg { transition: transform var(--dur-fast) var(--ease-out); }

  /* The whole band is the target, so the only hover state that matters is the
     one that says "this is a link": the rule under the words extends. */
  @media (prefers-reduced-motion: no-preference) {
    .door__media img { transition: transform var(--dur-slow) var(--ease-out); }
    .door:hover .door__media img { transform: scale(1.02); }
    .door:hover .door__cta svg { transform: translateX(0.35rem); }
  }

  .door:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: -6px;
  }

  /* ── the forthcoming band ──────────────────────────────────────────────── */

  /* No photograph exists for a festival that hasn't happened, so this band has
     no media column and says so with space rather than a placeholder. */
  .door--forthcoming {
    grid-template-columns: 1fr;
    border-bottom: 4px solid var(--accent);
  }

  .door--forthcoming .door__body {
    max-width: 68rem;
    margin-inline: auto;
    width: 100%;
    padding-block: var(--sp-24);
  }

  /* The forthcoming year is deliberately ghosted — the edition has not happened,
     so its numeral has not fully arrived. At 30% it measured 2.16:1, under the
     3:1 floor for large text; 45% keeps it clearly the faintest thing on the
     page while staying legible. The idea survives, the reading improves. */
  .door--forthcoming .door__year { color: color-mix(in oklab, var(--fg) 45%, var(--bg)); }

  /* .link-arrow is paper-on-dark by default; this band is paper-coloured. */
  .door--forthcoming .link-arrow {
    color: inherit;
    border-bottom-color: currentColor;
  }

  .door--forthcoming .link-arrow:hover { color: var(--accent); }

  @media (max-width: 1100px) {
    .door,
    .door:nth-child(even) { grid-template-columns: 1fr; }

    .door:nth-child(even) > .door__media { order: 0; }

    .door__media { min-height: 0; aspect-ratio: 4 / 3; }

    .door__body { padding-block: var(--sp-16); }
  }

  /* ── one edition: the cover ────────────────────────────────────────────── */

  .edition__head {
    position: relative;
    background: var(--bg);
    color: var(--fg);
    padding-top: var(--sp-32);
    padding-bottom: var(--sp-16);
    overflow: hidden;
    isolation: isolate;
  }

  /* The photograph sits behind the type and fades into the colour field rather
     than under a flat scrim, so the year reads on solid ground. */
  .edition__media {
    position: absolute;
    inset: 0;
    z-index: -1;
  }

  .edition__media img { opacity: 0.55; }

  .edition__media::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(
      to bottom,
      color-mix(in oklab, var(--bg) 55%, transparent) 0%,
      color-mix(in oklab, var(--bg) 80%, transparent) 55%,
      var(--bg) 100%
    );
  }

  .edition__intro { position: relative; }

  .edition__year {
    font-size: var(--t-3xl);
    line-height: 0.8;
    letter-spacing: -0.035em;
    margin: var(--sp-4) 0 0;
  }

  .edition__title {
    font-size: var(--t-xl);
    font-style: italic;
    line-height: 1.05;
    margin: var(--sp-2) 0 0;
    max-width: 28ch;
    color: var(--accent);
  }

  .edition__native {
    font-size: var(--t-md);
    line-height: 1.4;
    margin: var(--sp-4) 0 0;
    opacity: 0.8;
  }

  /* ── one edition: the read ─────────────────────────────────────────────── */

  .edition__headline {
    margin: 0 0 var(--sp-6);
    max-width: 34ch;
    text-wrap: balance;
  }

  .edition__subhead {
    margin: 0 0 var(--sp-8);
    padding-bottom: var(--sp-3);
    border-bottom: 1px solid color-mix(in oklab, currentColor 20%, transparent);
    color: var(--accent);
  }

  /* ── facts: set like a colophon ────────────────────────────────────────── */

  .facts {
    margin: 0;
    border-top: 2px solid currentColor;
  }

  .facts__row {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--sp-1);
    padding-block: var(--sp-3);
    border-bottom: 1px solid color-mix(in oklab, currentColor 18%, transparent);
  }

  .facts__key {
    margin: 0;
    font-size: var(--t-xs);
    color: var(--mid);
  }

  .facts__value {
    margin: 0;
    font-size: var(--t-sm);
    line-height: 1.5;
  }

  [data-hour="night"] .facts__key,
  [data-hour="ink"] .facts__key { color: color-mix(in oklab, var(--fg) 60%, var(--bg)); }

  /* ── who spoke ─────────────────────────────────────────────────────────── */

  .edition__voices {
    list-style: none;
    margin: 0;
    padding: 0;
    columns: 2;
    column-gap: var(--gutter);
  }

  .edition__voices > li { break-inside: avoid; }

  @media (max-width: 700px) {
    .edition__voices { columns: 1; }
  }

  /* ── gallery: a mosaic on a five-step rhythm ───────────────────────────── */

  /* Twelve columns, and each of the five item variants takes a different span
     and aspect. The pattern repeats every five photographs, which never lands
     on the same row twice — so it composes instead of tiling. */
  .gallery {
    list-style: none;
    margin: var(--sp-8) 0 0;
    padding-inline: var(--gutter);
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--sp-3);
  }

  .gallery__item {
    position: relative;
    overflow: hidden;
    background: color-mix(in oklab, var(--fg) 12%, var(--bg));
  }

  .gallery__item img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .gallery__item--1 { grid-column: span 7; aspect-ratio: 3 / 2; }
  .gallery__item--2 { grid-column: span 5; aspect-ratio: 4 / 5; align-self: end; }
  .gallery__item--3 { grid-column: span 4; aspect-ratio: 1; }
  .gallery__item--4 { grid-column: span 8; aspect-ratio: 16 / 9; }
  .gallery__item--5 { grid-column: span 12; aspect-ratio: 21 / 9; }

  @media (prefers-reduced-motion: no-preference) {
    .gallery__item img { transition: transform var(--dur-slow) var(--ease-out); }
    .gallery__item:hover img { transform: scale(1.03); }
  }

  @media (max-width: 1100px) {
    .gallery__item--1 { grid-column: span 12; }
    .gallery__item--2 { grid-column: span 6; }
    .gallery__item--3 { grid-column: span 6; }
    .gallery__item--4 { grid-column: span 12; }
  }

  @media (max-width: 700px) {
    .gallery { gap: var(--sp-2); }
    .gallery__item--2,
    .gallery__item--3 { grid-column: span 12; }
    .gallery__item--5 { aspect-ratio: 16 / 9; }
  }
}

/* ── Metaphor · Featured conversations ───────────────────────────────────────
 *
 * A row of cards on the home page, each one a link out to a recorded
 * conversation on Instagram.
 *
 * The composition is an exhibition wall, not a video platform: artwork in a
 * plate, a name and a role set beneath it on the ground, and a great deal of
 * air between them. There is deliberately no play triangle, no runtime and no
 * scrim over the artwork — none of those are true of a link, and each of them
 * would sit on top of a design somebody has already finished.
 *
 * Reused from elsewhere, never redefined here:
 *   .section, .container, .img-cover   → layout.css
 *   .section-head                      → pages.css
 *   .kicker                            → type.css
 *   .u-sr                              → utils.css
 */

@layer components {

  /* The one rounded thing on the site.
   *
   * Everything else here is square-cornered on purpose — forms.css and
   * programme.css both explicitly reset `border-radius: 0`, and no radius token
   * exists in tokens.css. The brief asks for rounded plates, so the radius is
   * declared once, locally, and used only by this section. Keeping it out of
   * tokens.css is the point: the departure stays contained, and stays legible
   * as a departure to whoever reads this next. */
  .convos-section {
    --convo-radius: 10px;

    /* The seam. `archive` above is night, this field is ink, and the closer
       below is ink as well — so the boundary into the closer would carry no
       change of colour at all and the two would read as one very long dark
       block. A hairline in the accent is enough to say there are two fields
       here without drawing a border across the page. */
    border-bottom: 1px solid color-mix(in oklab, var(--accent) 12%, transparent);
  }

  .convos__title {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-xl);
    font-weight: 400;
    line-height: 1.05;
    letter-spacing: -0.02em;
    margin-top: var(--sp-4);
  }

  .convos__title em { font-style: italic; color: var(--accent); }

  .convos__sub {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-sm);
    line-height: 1.6;
    color: var(--mid);
    margin-top: var(--sp-4);
    max-width: 42ch;
  }

  /* ── the arrows ────────────────────────────────────────────────────────── */

  /* Shipped `hidden` in the markup and revealed by app.js, so a reader without
     JavaScript never meets a control that does nothing. */
  .convos__nav {
    display: flex;
    gap: var(--sp-2);
  }

  .convos__arrow {
    display: grid;
    place-items: center;
    width: 3rem;
    height: 3rem;
    padding: 0;
    background: none;
    color: var(--fg);
    border: 1px solid color-mix(in oklab, var(--fg) 28%, transparent);
    cursor: pointer;
    transition: color var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out);
  }

  /* No new iconography: the same arrow the rest of the page uses, mirrored. */
  .convos__arrow[data-dir="-1"] svg { scale: -1 1; }

  .convos__arrow:hover:not(:disabled) {
    color: var(--accent);
    border-color: var(--accent);
  }

  /* At an end of the track the arrow is disabled rather than hidden, so the row
     does not reflow every time you reach the edge of it. */
  .convos__arrow:disabled {
    opacity: 0.3;
    cursor: default;
  }

  /* ── the track ─────────────────────────────────────────────────────────── */

  /* The same mechanism as `.years`, for the same reason: the row starts on the
     container's axis and runs off the right edge of the window.
     *
     * `scroll-snap-type` is deliberately absent, as it is there. With snapping
     * on, the browser settles the first item against the snap port on load,
     * which scrolls the left indent away and clips the first card against the
     * window edge. */
  .convos {
    display: flex;
    gap: var(--gutter);
    overflow-x: auto;
    scrollbar-width: none;
    padding-left: max(var(--gutter), calc((100vw - var(--col-max)) / 2 + var(--gutter)));
    padding-right: var(--gutter);
    list-style: none;
    margin: 0;
  }

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

  /* Three to four cards inside the 1440px measure — enough that the row reads as
     a wall, few enough that each card is still a picture rather than a stamp. */
  .convos__item {
    flex: 0 0 clamp(16rem, 26vw, 21rem);
  }

  .convo {
    display: grid;
    gap: var(--sp-4);
    text-decoration: none;
    color: inherit;
  }

  /* ── the plate ─────────────────────────────────────────────────────────── */

  .convo__plate {
    position: relative;
    display: block;
    overflow: hidden;
    border-radius: var(--convo-radius);
    background: color-mix(in oklab, var(--fg) 10%, var(--bg));
    transition: box-shadow var(--dur-mid) var(--ease-out);
  }

  .convo__img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
  }

  /* The mark that says where the card goes. A decorative span, not a nested
     button: the whole card is already one link, and a <button> inside an <a> is
     both invalid and unreachable from the keyboard. */
  .convo__mark {
    position: absolute;
    inset: auto var(--sp-3) var(--sp-3) auto;
    z-index: var(--z-fore);
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    color: var(--c-paper);
    opacity: 0.45;
    transition: opacity var(--dur-mid) var(--ease-out);
  }

  .convo__mark svg { width: 1.25rem; height: 1.25rem; }

  /* The line under the plate. It exists at rest as nothing — a 1px accent rule
     scaled to zero — and draws itself in from the left on hover, which is the
     whole of the card's "this is a link" signal. Pulled back against the plate
     it belongs to, so it reads as the plate's own edge rather than as a third
     element floating in the stack. */
  .convo__rule {
    display: block;
    height: 1px;
    margin-top: calc(var(--sp-4) * -1);
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--dur-mid) var(--ease-out);
  }

  /* ── the label ─────────────────────────────────────────────────────────── */

  .convo__caption {
    display: grid;
    gap: var(--sp-1);
  }

  .convo__name {
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-md);
    line-height: 1.15;
    color: var(--c-paper);
    transition: color var(--dur-fast) var(--ease-out);
  }

  .convo__role {
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    line-height: 1.5;
    color: var(--mid);
  }

  /* ── hover ─────────────────────────────────────────────────────────────── */

  /* Behind `hover: hover` so a touch device never latches into a hover state it
     has no way of leaving.
     *
     * Colour is separated from movement here on purpose. Under reduced motion
     * the name still turns, the rule still draws and the mark still comes up —
     * the card keeps every affordance that says it is a link — while the lift
     * and the zoom, which are motion and nothing else, simply do not happen. */
  @media (hover: hover) {
    /* A drop shadow alone barely registers against an ink ground, so the lift is
       carried by a hairline ring in the accent as much as by the shadow. */
    .convo:hover .convo__plate {
      box-shadow: 0 20px 44px -20px rgb(0 0 0 / 0.7),
                  inset 0 0 0 1px color-mix(in oklab, var(--accent) 18%, transparent);
    }

    .convo:hover .convo__name { color: var(--accent); }
    .convo:hover .convo__rule { transform: scaleX(1); }
    .convo:hover .convo__mark { opacity: 1; }
  }

  @media (hover: hover) and (prefers-reduced-motion: no-preference) {
    .convo { transition: translate var(--dur-mid) var(--ease-out); }
    .convo__img { transition: scale var(--dur-slow) var(--ease-out); }

    .convo:hover { translate: 0 -6px; }
    .convo:hover .convo__img { scale: 1.03; }
  }

  /* ── the reveal ────────────────────────────────────────────────────────── */

  /* The hidden start state applies only under [data-reveal], which app.js sets
     on the track at init. With no JavaScript the cards are simply visible —
     they can never be stranded at opacity 0 waiting for an observer that is
     never going to run. */
  @media (prefers-reduced-motion: no-preference) {
    [data-reveal] .convos__item {
      opacity: 0;
      translate: 0 1.25rem;
      transition: opacity var(--dur-slow) var(--ease-out),
                  translate var(--dur-slow) var(--ease-out);
      transition-delay: calc(var(--i) * 80ms);
    }

    [data-reveal].is-in .convos__item {
      opacity: 1;
      translate: 0 0;
    }
  }

  /* While a drag is in progress the cursor says so, and nothing inside the track
     is selectable or draggable — otherwise a mouse drag across a card picks up
     the image as a native drag ghost, or paints a text selection across three
     names.
     *
     * Note there is no `scroll-behavior: smooth` anywhere on this track. It
     * would apply to every programmatic scroll, including the per-frame
     * `scrollLeft` writes a drag makes — so the row would lag a whole
     * animation behind the pointer. app.js asks for smoothness explicitly, per
     * scroll, where it actually wants it. */
  .convos.is-dragging {
    cursor: grabbing;
    user-select: none;
  }

  .convos.is-dragging img { pointer-events: none; }

  @media (max-width: 700px) {
    /* One dominant card with a sliver of the next in view, so the row reads as
       something to push rather than as a stranded item. */
    .convos__item { flex: 0 0 78vw; }
  }
}

/* ── Metaphor · Editorial pages ───────────────────────────────────────────────
 *
 * The two long-read pages: /festival/ and /lucknow/. Neither has a product to
 * sell, so both are set as reading matter — hanging numerals, colour ladders,
 * photographs that carry a caption rather than a caption that carries a
 * photograph.
 *
 * Reused, never redefined here:
 *   .poster, .poster__media, .poster__title  → layout.css, pages.css
 *   .section-head, .lexicon*, .home__aside*  → pages.css
 *   .stitch-run, .stitch-run--demo           → pages.css
 *   .btn, .ledger*                           → components.css, pages.css
 */

@layer components {

  /* ── festival · four principles ────────────────────────────────────────── */

  /* An ordered list that keeps its numbers. They hang outside the text column
     in the accent colour, large enough to read as structure rather than
     decoration — the list is an argument in four parts, and it should look it. */
  .principles {
    list-style: none;
    counter-reset: principle;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-12) var(--gutter);
  }

  .principle {
    counter-increment: principle;
    display: grid;
    grid-template-columns: 4.5rem 1fr;
    align-items: start;
    gap: 0 var(--sp-4);
    padding-top: var(--sp-4);
    border-top: 2px solid color-mix(in oklab, currentColor 25%, transparent);
  }

  .principle::before {
    content: counter(principle, decimal-leading-zero);
    grid-row: 1 / span 2;
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-lg);
    font-style: italic;
    line-height: 0.9;
    color: var(--accent);
  }

  .principle__title {
    margin: 0 0 var(--sp-2);
    font-size: var(--t-md);
    line-height: 1.15;
    text-wrap: balance;
  }

  .principle__body {
    margin: 0;
    font-size: var(--t-sm);
    line-height: 1.65;
    max-width: 42ch;
    color: color-mix(in oklab, currentColor 82%, var(--bg));
  }

  @media (max-width: 1100px) {
    .principles { grid-template-columns: 1fr; gap: var(--sp-8); }
    .principle { grid-template-columns: 3rem 1fr; }
  }

  /* ── festival · the eight strands ──────────────────────────────────────── */

  /* Each strand owns an hour, so the list is also the palette: eight bands read
     top to bottom as a colour ladder. Deliberately not a card grid — the
     strands are a scale, and a scale is a stack. */
  .strand-list {
    list-style: none;
    margin: 0;
    padding: 0;
    border-top: 1px solid color-mix(in oklab, currentColor 20%, transparent);
  }

  .strand {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-2) var(--sp-6);
    padding: var(--sp-4) clamp(var(--sp-4), 3vw, var(--sp-12));
    background: var(--bg);
    color: var(--fg);
  }

  .strand__label {
    font-size: var(--t-md);
    line-height: 1.1;
  }

  /* 65% of --fg fell to 3.62:1 on the lighter strand grounds. --mid is the
     token that already exists for exactly this register, and it is audited per
     hour, so the count uses it rather than a fresh blend. */
  .strand__count {
    font-size: var(--t-xs);
    color: var(--mid);
  }

  /* ── lucknow · the poster ──────────────────────────────────────────────── */

  .lucknow__poster { min-height: 100svh; }

  .lucknow__poster .poster__title em {
    font-style: italic;
    color: var(--accent);
  }

  /* ── lucknow · three places ────────────────────────────────────────────── */

  /* One photograph exists for the Residency and none for the other two. Rather
     than filling the gap with stock imagery, the two unillustrated places are
     set as text blocks in their own column — the asymmetry is the layout. */
  .places {
    display: grid;
    grid-template-columns: 7fr 5fr;
    gap: var(--sp-12) var(--gutter);
    align-items: start;
    margin-top: var(--sp-12);
  }

  .place--wide {
    grid-column: 1;
    grid-row: 1 / span 2;
  }

  .place__media {
    display: block;
    position: relative;
    margin-bottom: var(--sp-6);
    background: color-mix(in oklab, var(--fg) 12%, var(--bg));
  }

  .place__media img {
    display: block;
    width: 100%;
    height: auto;
  }

  /* Text-only places get the stitch as a header rule, which is what the
     photograph would otherwise have been doing: marking a beginning. */
  .place:not(.place--wide) .place__body {
    padding-top: var(--sp-4);
    background-image: var(--stitch);
    background-repeat: no-repeat;
    background-size: 100% 4px;
    background-position: 0 0;
  }

  .place__name {
    margin: 0 0 var(--sp-3);
    font-size: var(--t-lg);
    line-height: 1;
    letter-spacing: -0.01em;
  }

  .place--wide .place__name { font-size: var(--t-xl); }

  .place__note {
    margin: 0;
    font-size: var(--t-sm);
    line-height: 1.7;
    max-width: 52ch;
    color: color-mix(in oklab, currentColor 85%, var(--bg));
  }

  @media (max-width: 1100px) {
    .places { grid-template-columns: 1fr; gap: var(--sp-8); }
    .place--wide { grid-column: 1; grid-row: auto; }
  }

  /* ── lucknow · the chikankari figure ───────────────────────────────────── */

  /* The kurta is the citation for every hairline on the site, so it stays in
     view while the paragraph explaining it scrolls past. */
  .stitch-figure {
    position: sticky;
    top: var(--sp-24);
    background: color-mix(in oklab, var(--fg) 8%, var(--bg));
  }

  .stitch-figure img {
    display: block;
    width: 100%;
    height: auto;
  }

  @media (max-width: 1100px) {
    .stitch-figure { position: static; margin-bottom: var(--sp-8); }
  }
}

/* ── Metaphor · Forms, Visit, and the 404 ─────────────────────────────────────
 *
 * One file for the pages that are mostly interface: /join/ (two real forms),
 * /visit/ (facts, routes, questions, contacts) and /404.html. They sit together
 * because they share one idea — a form on this site is set like the rest of the
 * site, not like a form. No pills, no drop shadows, no filled boxes. A field is
 * a label, a ruled line, and the space above the next one.
 *
 * Reused, never redefined here:
 *   .btn, .btn--fill, .notice        → components.css
 *   .notice--wide, .closer*          → pages.css
 *   .venue*                          → programme.css (shared with /programme/)
 *   .link-arrow, .stitch-run          → components.css, pages.css
 */

@layer components {

  /* ── the form itself ───────────────────────────────────────────────────── */

  .form { margin-top: var(--sp-8); }

  .form__grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-8) var(--gutter);
  }

  .form__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-4) var(--sp-6);
    margin-top: var(--sp-12);
    padding-top: var(--sp-6);
    background-image: var(--stitch);
    background-repeat: no-repeat;
    background-size: 100% 4px;
    background-position: 0 0;
  }

  @media (max-width: 700px) {
    .form__grid { grid-template-columns: 1fr; gap: var(--sp-6); }
  }

  /* ── one field ─────────────────────────────────────────────────────────── */

  .field {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    min-width: 0;
    border: 0;
  }

  .field--full { grid-column: 1 / -1; }

  .field--search { max-width: 26rem; }

  .field__label {
    display: block;
    color: color-mix(in oklab, currentColor 70%, var(--bg));
  }

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

  /* A ruled line, not a box. The line thickens and takes the accent colour on
     focus, which is the only state change the control needs. */
  .field__input {
    width: 100%;
    /* 44px minimum, so the field is a comfortable target on a phone without
       becoming a box — the rule under it stays the whole visual treatment. */
    min-height: 44px;
    padding: var(--sp-3) 0;
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-base);
    color: inherit;
    background: transparent;
    border: 0;
    border-bottom: 1px solid color-mix(in oklab, currentColor 40%, transparent);
    border-radius: 0;
    transition: border-color var(--dur-fast) var(--ease-out);
  }

  .field__input::placeholder {
    color: color-mix(in oklab, currentColor 45%, var(--bg));
    font-style: italic;
  }

  .field__input:hover { border-bottom-color: color-mix(in oklab, currentColor 65%, transparent); }

  .field__input:focus {
    outline: none;
    border-bottom: 2px solid var(--accent);
    padding-bottom: calc(var(--sp-2) - 1px);
  }

  /* Keyboard users still get the ring; the rule alone is not enough contrast. */
  .field__input:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 4px;
  }

  .field__input:user-invalid {
    border-bottom-color: var(--c-dusk);
  }

  .field__input--area {
    resize: vertical;
    min-height: 5.5rem;
    line-height: 1.6;
  }

  /* The native arrow is replaced by a drawn one so the control matches the
     ruled fields either side of it. */
  .field__input--select {
    appearance: none;
    padding-right: var(--sp-6);
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-size: 6px 6px, 6px 6px;
    background-position: right 12px center, right 6px center;
    background-repeat: no-repeat;
    cursor: pointer;
  }

  .field__help {
    margin: 0;
    max-width: 44ch;
  }

  [data-hour="night"] .field__help,
  [data-hour="ink"] .field__help,
  [data-hour="evening"] .field__help { color: color-mix(in oklab, var(--fg) 60%, var(--bg)); }

  /* ── checkboxes ────────────────────────────────────────────────────────── */

  .check-group { gap: var(--sp-4); }

  .check-group__items {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
    gap: var(--sp-3) var(--gutter);
    margin-top: var(--sp-2);
  }

  /* The label wraps the input, so the whole row is the hit area — the 1.1rem box
     is only what you see. The minimum height is what makes it tappable. */
  .check {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    min-height: 44px;
    cursor: pointer;
  }

  .check__box {
    appearance: none;
    flex: 0 0 auto;
    width: 1.1rem;
    height: 1.1rem;
    margin: 0;
    background: transparent;
    border: 1px solid color-mix(in oklab, currentColor 55%, transparent);
    border-radius: 0;
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out);
  }

  .check__box:checked {
    background: var(--accent);
    border-color: var(--accent);
    box-shadow: inset 0 0 0 3px var(--bg);
  }

  .check:hover .check__box { border-color: currentColor; }

  .check__label {
    font-size: var(--t-sm);
    line-height: 1.4;
  }

  /* ── join · three other ways in ────────────────────────────────────────── */

  .ways {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--gutter);
    margin-top: var(--sp-12);
    text-align: left;
  }

  .way {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-3);
    padding-top: var(--sp-4);
    border-top: 2px solid color-mix(in oklab, currentColor 30%, transparent);
  }

  .way__title {
    margin: 0;
    font-size: var(--t-md);
    line-height: 1.15;
  }

  .way__body {
    margin: 0;
    font-size: var(--t-sm);
    line-height: 1.6;
    color: color-mix(in oklab, currentColor 78%, var(--bg));
  }

  .way .link-arrow { margin-top: auto; }

  @media (max-width: 800px) {
    .ways { grid-template-columns: 1fr; gap: var(--sp-8); }
  }

  /* ── visit · the head ──────────────────────────────────────────────────── */

  /* The city clip runs behind the facts. It is ambient, muted and secondary to
     the four numbers in front of it, so it sits at low opacity under a gradient
     rather than under a flat black scrim. */
  .visit-head {
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    min-height: 88svh;
    padding-block: var(--sp-32) var(--sp-16);
    background: var(--bg);
    color: var(--fg);
    overflow: hidden;
    isolation: isolate;
  }

  .visit-head__media {
    position: absolute;
    inset: 0;
    z-index: -1;
  }

  .visit-head__video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0.4;
  }

  .visit-head__media::after {
    content: "";
    position: absolute;
    inset: 0;
    /* The clip has a printed backdrop in it — large stencilled lettering that
       reads as competing type behind the headline. The gradient is weighted to
       the upper half where the title sits, so the footage stays legible as
       texture without arguing with the words in front of it. */
    background: linear-gradient(
      to bottom,
      color-mix(in oklab, var(--bg) 72%, transparent) 0%,
      color-mix(in oklab, var(--bg) 78%, transparent) 55%,
      var(--bg) 100%
    );
  }

  .visit-head__body { position: relative; }

  .visit-head__title {
    margin: var(--sp-4) 0 var(--sp-12);
    max-width: 22ch;
    text-wrap: balance;
  }

  /* Four facts on one rule. Tabular figures so the values line up as a table
     even though they are set as display type. */
  .visit-head__facts {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--sp-6) var(--gutter);
    padding-top: var(--sp-6);
    background-image: var(--stitch);
    background-repeat: no-repeat;
    background-size: 100% 4px;
    background-position: 0 0;
  }

  .visit-fact {
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
  }

  .visit-fact__key { color: color-mix(in oklab, var(--fg) 60%, var(--bg)); }

  .visit-fact__value {
    font-size: var(--t-md);
    line-height: 1.2;
  }

  @media (max-width: 800px) {
    .visit-head { min-height: 0; padding-block: var(--sp-32) var(--sp-12); }
    .visit-head__facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  }

  /* ── visit · getting here ──────────────────────────────────────────────── */

  /* Four modes of arrival, numbered in the markup because they genuinely are
     ordered — nearest first. The number hangs in the margin with the mode, so
     the eye can skip to "by train" without reading the paragraphs. */
  .routes {
    display: grid;
    gap: 0;
    margin-top: var(--sp-12);
    border-top: 1px solid color-mix(in oklab, currentColor 20%, transparent);
  }

  .route {
    display: grid;
    grid-template-columns: 12rem 1fr;
    gap: var(--sp-4) var(--gutter);
    padding-block: var(--sp-8);
    border-bottom: 1px solid color-mix(in oklab, currentColor 20%, transparent);
  }

  .route__mode {
    margin: 0;
    color: var(--accent);
    align-self: start;
    position: sticky;
    top: var(--sp-16);
    height: max-content;
  }

  .route__head {
    margin: 0 0 var(--sp-2);
    font-size: var(--t-md);
    line-height: 1.15;
  }

  .route__body {
    margin: 0;
    font-size: var(--t-sm);
    line-height: 1.7;
    max-width: 56ch;
    color: color-mix(in oklab, currentColor 82%, var(--bg));
  }

  @media (max-width: 700px) {
    .route {
      grid-template-columns: 1fr;
      gap: var(--sp-2);
      padding-block: var(--sp-6);
    }
    .route__mode { position: static; }
  }

  /* ── visit · questions ─────────────────────────────────────────────────── */

  /* A definition list set as a two-column read: question left, answer right.
     Not an accordion — there are six of them and hiding an answer behind a
     click to save six centimetres is not a trade worth making. */
  .qa {
    margin-top: var(--sp-12);
    border-top: 2px solid currentColor;
  }

  .qa__row {
    display: grid;
    grid-template-columns: 5fr 7fr;
    gap: var(--sp-2) var(--gutter);
    padding-block: var(--sp-6);
    border-bottom: 1px solid color-mix(in oklab, currentColor 15%, transparent);
  }

  .qa__q {
    margin: 0;
    font-size: var(--t-md);
    line-height: 1.2;
    max-width: 24ch;
    text-wrap: balance;
  }

  .qa__a {
    margin: 0;
    font-family: "Playfair Display", Georgia, serif;
    font-size: var(--t-base);
    line-height: 1.7;
    max-width: 54ch;
  }

  @media (max-width: 800px) {
    .qa__row { grid-template-columns: 1fr; gap: var(--sp-3); }
  }

  /* ── visit · contacts ──────────────────────────────────────────────────── */

  .contact-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-8) var(--gutter);
    margin-top: var(--sp-12);
  }

  .contact {
    flex: 1 1 340px;
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    padding: var(--sp-6);
    background-color: color-mix(in oklab, var(--fg) 4%, transparent);
    border-radius: var(--radius-md);
  }

  .contact__key {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    color: color-mix(in oklab, var(--fg) 60%, var(--bg));
  }
  
  .contact__key svg {
    width: 1.25em;
    height: 1.25em;
  }

  /* The phone number and the email are the two most useful strings on the page,
     so they are set at display size and are the link, with no surrounding
     button to find first. Which means the link itself has to be tappable. */
  .contact__value {
    font-size: var(--t-md);
    line-height: 1.25;
    text-decoration: none;
    color: inherit;
    transition: color var(--dur-fast) var(--ease-out);
  }

  a.contact__value {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  a.contact__value:hover { color: var(--accent); }

  .contact__value--address {
    font-size: var(--t-base);
    line-height: 1.5;
  }

  /* ── 404 ───────────────────────────────────────────────────────────────── */

  /* A missing page is the one screen with the reader's full attention and
     nothing to sell them, so the whole site is handed back as a list. Each row
     carries the destination's own hour: the nav becomes the palette, and the
     page is the only place on the site where all seven fields are visible at
     once.
     */
  .lost { text-align: left; }

  .lost__inner {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
  }

  .lost__mark {
    width: 3rem;
    color: var(--accent);
    margin-bottom: var(--sp-8);
  }

  .lost__title {
    margin-block: var(--sp-4) var(--sp-8);
    text-wrap: balance;
  }

  .lost__lede {
    max-width: 48ch;
    margin-bottom: var(--sp-12);
  }

  .lost__nav {
    width: 100%;
    display: grid;
    gap: 1px;
    margin-bottom: var(--sp-12);
  }

  .lost__item {
    background: var(--bg);
    color: var(--fg);
  }

  .lost__link {
    display: grid;
    grid-template-columns: 1fr auto 2rem;
    align-items: baseline;
    gap: var(--sp-4);
    padding: var(--sp-4) var(--sp-6);
    text-decoration: none;
    color: inherit;
  }

  .lost__label {
    font-size: var(--t-md);
    line-height: 1.1;
  }

  .lost__note {
    color: color-mix(in oklab, var(--fg) 65%, var(--bg));
    text-align: right;
  }

  .lost__arrow {
    justify-self: end;
    color: var(--accent);
  }

  .lost__arrow svg { transition: transform var(--dur-fast) var(--ease-out); }

  @media (prefers-reduced-motion: no-preference) {
    .lost__link:hover .lost__arrow svg { transform: translateX(0.35rem); }
  }

  .lost__link:hover .lost__label { font-style: italic; }

  .lost__aside {
    max-width: 54ch;
    margin-bottom: var(--sp-12);
  }

  .lost__aside a {
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px solid color-mix(in oklab, var(--accent) 45%, transparent);
  }

  .lost__aside a:hover { border-bottom-color: currentColor; }

  /* The 404 is the one page with no header offset to fight, so it centres its
     own column instead. */
  .page--404 .lost { display: flex; align-items: center; }

  .page--404 .closer__actions { justify-content: flex-start; }

  @media (max-width: 700px) {
    .lost__link {
      grid-template-columns: 1fr 1.5rem;
      row-gap: var(--sp-1);
    }
    .lost__note {
      grid-column: 1;
      text-align: left;
    }
    .lost__arrow { grid-row: 1; }
  }
}

/* ── Metaphor · Utilities ────────────────────────────────────────────────── */

@layer utils {

  /* `hidden` has to beat the display value a component sets on itself. */
  [hidden] { display: none !important; }

  /* Visually hidden, still read aloud. */
  .u-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* Skip link: off-screen until focused, then a real button in the corner. */
  .u-skip {
    position: fixed;
    top: var(--sp-2);
    left: var(--sp-2);
    z-index: var(--z-skip);
    transform: translateY(-200%);
    background: var(--c-ink);
    color: var(--c-paper);
    padding: var(--sp-3) var(--sp-4);
    font-family: "Archivo", sans-serif;
    font-size: var(--t-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
    transition: transform var(--dur-fast) var(--ease-out);
  }
  .u-skip:focus { transform: none; }

  .u-mt-2  { margin-top: var(--sp-2); }
  .u-mt-4  { margin-top: var(--sp-4); }
  .u-mt-6  { margin-top: var(--sp-6); }
  .u-mt-8  { margin-top: var(--sp-8); }
  .u-mt-12 { margin-top: var(--sp-12); }
  .u-mb-4  { margin-bottom: var(--sp-4); }
  .u-mb-6  { margin-bottom: var(--sp-6); }
  .u-mb-8  { margin-bottom: var(--sp-8); }

  .u-measure { max-width: var(--col-prose); }

}

