/* board.css - the board's entire stylesheet.

   Extracted verbatim from web/index.html lines 12-541. Two contracts now span
   files and nothing but this comment enforces them:

   1. JS writes five custom properties this sheet reads, in drawHero():
        --hero-cols  --hero-gap  --hero-stroke  --hero-type  --hero-plus
   2. Every class in board.js's render templates must have a selector here -
      job-card__*, axis-item__*, chip*, hero__cell--*, tag--*, badge, flash,
      marker--busy, marker--err, empty. tools/verify_board.js checks this.
*/
  /* Palette, type and spacing lifted from the reference system: warm paper
     ground, near-black ink, one electric accent, hairline rules, no radii.
     The lime is the second mark colour - logo tiles and live counts only. */
  :root {
    --color-background: #ffffff;
    --color-foreground: #121210;
    --color-foreground-rgb: 18, 18, 16;
    --color-body: #3a3a36;
    --color-muted: #706e63;
    /* Two faints, because they answer different questions.
       --color-faint is DECORATION: the · after a nav label, the / in a section
       label, the · between card meta fields. WCAG 1.4.3 exempts decorative
       text, and this tone is what carries the hierarchy - 2.06:1 on white.
       --color-faint-text is anything a reader has to READ: nav numbers, the
       search placeholder, muted chips. 4.65:1, which clears AA.
       Putting a nav number in --color-faint is the mistake this split exists
       to make visible. */
    --color-faint: #b6b5ab;
    --color-faint-text: #767569;
    --color-soft: rgba(var(--color-foreground-rgb), .64);
    --color-hairline: rgba(var(--color-foreground-rgb), .12);
    /* the blue is the one in web/brand/logo.svg and Favicon.svg - the mark is
       the source of truth, and the stylesheet had drifted a shade off it */
    --color-accent: #133AFF;
    --color-accent-rgb: 19, 58, 255;
    --color-accent-line: rgba(var(--color-accent-rgb), .4);
    --color-accent-foreground: #f6f6f4;
    /* one gap for the whole top stack: logo -> glyph grid -> nav -> filters.
       Each band carries it as top padding only, so the three are the same
       number by construction rather than by two halves adding up. */
    --stack-gap: 3.5rem;
    --icon-x: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M1.5 1.5 10.5 10.5M10.5 1.5 1.5 10.5' stroke='%23000' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E");
    --color-warn: #a8641b;
    --color-danger: #c0341d;
    --color-danger-strong: #8f2415;
    /* lime is the live-count highlight in the nav, and only that */
    --color-mark: #c9f24d;
    --color-mark-ink: #121210;
    /* Logo grounds. Nothing paints these any more - the marks sit straight on
       the page by request. Kept because 9_fetch_logos.js still measures which
       ground each mark WOULD need, so putting the tiles back is two
       declarations rather than a re-crawl. */
    --color-tile: rgba(0, 0, 0, .04);
    --color-tile-dark: rgba(0, 0, 0, .8);

    --font-sans: "Geist", "Helvetica Neue", Helvetica, Arial, sans-serif;
    --font-mono: "Geist Mono", ui-monospace, "SFMono-Regular", "SF Mono", Consolas, Menlo, monospace;

    --site-gutter: clamp(1.5rem, calc(.607rem + 3.214vw), 3.5rem);
    /* 100rem, not the 150rem it was. At 150rem a 1920px monitor rendered
       full-bleed: each job card came out 848px wide with the title at one end
       and [ Site ] at the other, and Sort sat 500px from the filter it belongs
       with. Capping here is what makes a wide screen look deliberately centred
       rather than merely stretched - below 1600px nothing changes at all. */
    --site-container-max: 100rem;
    --section-padding-block: clamp(2.25rem, 5dvh, 3rem);

    --transition-duration: .35s;
    --ease-out: cubic-bezier(.2, .7, .2, 1);
    /* quick hover feedback - the bracket kick and the chip fade */
    --dur-quick: .18s;

    /* One focus ring, applied in one place (see "focus" near the bottom).
       2px because WCAG 2.2 SC 2.4.11 wants an indicator with real area and 3:1
       against what is behind it - the 1px underline colour change this replaces
       had neither. --focus-ring-color is overridden inside the accent panel,
       where an accent-blue ring would be invisible on accent blue. */
    --focus-ring-color: var(--color-accent);
    --focus-ring: 2px solid var(--focus-ring-color);
    --focus-ring-offset: .25rem;

    --axis-x: 1.75rem;
    --mono-tracking: .06em;
    --hairline-width: 1px;

    /* The mono type scale. Every label, meta line, badge and chip on the board
       is one of these four sizes; nothing else is. */
    --type-mono-2xs: .5625rem;
    --type-mono-xs: .625rem;
    --type-mono-sm: .6875rem;
    --type-mono-md: .8125rem;

    /* Job cards. --card-pad-y is one decision in two places: the card's own
       padding and the inset of the centre divider, which has to stop exactly
       where the content does or the segments stop lining up with the rules. */
    --card-pad-y: 2rem;
    --col-gap: 7rem;          /* 56px of clear space either side of the divider */
    --logo-size: 2.75rem;
    --logo-inset: 68%;        /* unused since the tile went; see .job-card__logo */
    --recede-opacity: .64;    /* unhovered cards, while another card is hovered */

    /* 104px between nav items is right on a desktop and absurd on a phone -
       it was the row-gap too, so three wrapped items ate 366px of a 320px
       screen. Solved for 24px at 390 and 104px at 1280. */
    --nav-gap: clamp(1.5rem, calc(-11.06px + 8.99vw), 6.5rem);

    /* The hero field. --hero-tone-min/max are the two ends of the breathing
       animation AND the static weave under reduced motion, so they are one
       decision. --hero-fill-scale is a SCALE, not an opacity - it sizes the
       solid squares, and it stayed at .56 when the opacities went to .64
       precisely because the two were never the same decision. Same for
       --hero-glyph, which is a size in percent. --hero-beat is duplicated as
       HERO_BEAT in board.js, which spreads the per-cell delays across it. */
    --hero-glyph: 56%;
    --hero-tone-min: .16;
    --hero-tone-max: .64;
    --hero-fill-scale: .56;
    --hero-beat: 2s;

    --icon-x-scale: 133.33%;  /* the x mark's ink fills 3/4 of its own viewBox */
  }

  * { box-sizing: border-box; }
  html { -webkit-text-size-adjust: 100%; }
  body {
    min-height: 100%; margin: 0; overflow-x: clip;
    background: var(--color-background); color: var(--color-foreground);
    font-family: var(--font-sans); font-size: 1rem; line-height: 1.56;
    font-synthesis-weight: none; text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
  }
  a { color: inherit; text-decoration: none; }
  /* In the accessibility tree, off the screen. Not display:none and not
     visibility:hidden - both of those remove it from the tree too, which is the
     opposite of the point. clip-path over the old clip:rect() because it does
     not need the absolute positioning hack to work. */
  .visually-hidden {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
  }
  ::selection { background: var(--color-accent); color: var(--color-accent-foreground); }

  .site-container { width: min(100%, var(--site-container-max)); margin-inline: auto; padding-inline: var(--site-gutter); }
  .section-spacing { padding-block: var(--section-padding-block); }
  /* the board is the last band of the top stack, so it takes the same gap */
  #board { padding-top: var(--stack-gap); }

  /* ---- header -------------------------------------------------------- */
  .site-header {
    position: relative; z-index: 3; display: flex; gap: 1.5rem;
    align-items: center; justify-content: space-between; padding-block: 1.25rem 0;
  }
  /* no rule under the header - it sits on open background by request */
  .site-header__brand {
    display: inline-flex; align-items: center; gap: .5rem;
    font-family: var(--font-mono); font-size: var(--type-mono-md); font-weight: 500;
    letter-spacing: .14em; text-transform: uppercase;
  }
  /* the mark is dropped into web/brand/ by hand; until it exists the logo mark
     stands in, so the header is never empty and never a broken image */
  .site-header__mark { display: block; width: auto; height: 2rem; }
  /* ---- status bar ------------------------------------------------------
     Used twice - top of the page and in the footer - and the two have to be
     indistinguishable. So they are the same two classes rather than two rules
     carrying the same numbers, which is what let the footer drift to a 1.5rem
     dot gap while the header sat at .75rem. Only the alignment differs, and
     that is the one thing spelled out per site. */
  .status-bar { display: flex; flex-wrap: wrap; gap: .75rem 1.5rem; align-items: center; }
  .status-bar--end { justify-content: flex-end; }
  .status {
    display: flex; gap: .75rem; align-items: center; min-width: 0; margin: 0;
    font-family: var(--font-mono); font-size: var(--type-mono-xs); letter-spacing: var(--mono-tracking);
    text-transform: uppercase; color: var(--color-muted);
  }
  /* the status line is the only thing here that can grow without bound, so it
     is what gives way rather than pushing the buttons off the page */
  .status span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  @media (max-width: 48rem) {
    .site-header { flex-wrap: wrap; row-gap: .75rem; }
    .site-header .status { order: 3; flex-basis: 100%; }
    /* Breathing room where the browser chrome crowds it. 20px above the mark
       is fine under a desktop title bar and tight under a phone's status bar
       and notch; the extra 8px at the foot keeps the last line off the home
       indicator. Desktop keeps the values the design was tuned at. */
    .site-header { padding-block: 2rem 0; }
    .site-footer { padding-block: var(--stack-gap) 4rem; }
  }
  .marker { flex: none; width: .5rem; height: .5rem; background: var(--color-accent); }
  .marker--busy { animation: marker-pulse 1.05s ease-in-out infinite; }
  .marker--err { background: var(--color-danger); }
  @keyframes marker-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .25 } }

  /* ---- bracket button ------------------------------------------------ */
  .site-button {
    /* every bracket button is bold, whatever its size */
    --site-button-font-size: 1rem; --site-button-weight: 700;
    --bracket-open-x: -.07em; --bracket-close-x: .07em; --bracket-scale: 1.07;
    /* WCAG 2.2 SC 2.5.8 wants a 24px target; the bracket buttons paint at 14px
       high by design. The padding grows the hit area and the equal negative
       margin gives the space straight back to the layout, so the box a finger
       hits is 24px and the box the page lays out is unchanged. Measured both. */
    appearance: none; display: inline-flex; align-items: center; border: 0;
    /* padding-inline: 0 is not redundant. Dropping the old `padding: 0`
       shorthand let Chrome's UA `padding: 1px 6px` for <button> back in on the
       inline axis, and every bracket button silently grew 12px wider. */
    padding-block: .3125rem; padding-inline: 0; margin-block: -.3125rem;
    background: transparent; color: inherit; font-family: var(--font-sans);
    font-size: var(--site-button-font-size); font-weight: var(--site-button-weight);
    letter-spacing: -.02em; line-height: 1; cursor: pointer;
  }
  /* Same fight as .site-nav__link[hidden] below: display:inline-flex above has
     the same specificity as the UA's [hidden] rule and sits later in the
     cascade, so it wins and the button stays visible. This is exactly what let
     Refresh/Re-crawl show up on the published static site, where board.js sets
     hidden=true on both because there is no server for them to talk to. */
  .site-button[hidden] { display: none; }
  /* a token, not a constant: the brackets were hardcoded accent blue, which is
     invisible on the accent-blue contact panel */
  .site-button:before, .site-button:after {
    display: inline-block; color: var(--bracket-color, var(--color-accent));
    transition: transform var(--dur-quick) var(--ease-out); transform-origin: center;
  }
  .site-button:before { content: "["; margin-right: .25em; }
  .site-button:after { content: "]"; margin-left: .25em; }
  .site-button:hover:before, .site-button:focus-visible:before { transform: translate(var(--bracket-open-x), 0) scale(var(--bracket-scale)); }
  .site-button:hover:after,  .site-button:focus-visible:after  { transform: translate(var(--bracket-close-x), 0) scale(var(--bracket-scale)); }
  /* the ring itself is in the focus block near the bottom, with every other
     focusable thing - only the bracket kick belongs to the button */
  .site-button[disabled] { color: rgba(var(--color-foreground-rgb), .4); cursor: not-allowed; }
  .site-button[disabled]:before, .site-button[disabled]:after { color: currentColor; transform: none; }
  .site-button--sm { --site-button-font-size: .875rem; }

  /* ---- hero: character field ----------------------------------------- */
  /* A field of drifting glyphs with the live counts stamped into it. The
     numbers are the point - they are redrawn whenever data lands, so the hero
     is never showing yesterday's total. */
  .hero { position: relative; overflow: clip; padding-block: var(--stack-gap) 0; }
  /* Rows of plus signs, every cell square, columns fitted to the width.
     The counts and the logo mark are stamped into the grid a character per cell,
     so they line up with the pluses whatever the font does. */
  /* The gap is 1.6x spacing: pitch stays width/cols, so the glyph shrinks to
     make room and the grid still meets both edges. */
  .hero__grid {
    display: grid; grid-template-columns: repeat(var(--hero-cols, 40), minmax(0, 1fr));
    gap: var(--hero-gap, 0px);
    margin: 0; user-select: none; -webkit-user-select: none;
  }
  .hero__cell { aspect-ratio: 1; display: grid; place-items: center; }
  /* drawn, not an image file: two gradients stay crisp at any cell size and
     cost nothing next to 480 inline SVGs */
  .hero__cell--plus {
    background:
      linear-gradient(var(--color-accent) 0 0) 50% 50% / var(--hero-glyph) var(--hero-stroke, 1px) no-repeat,
      linear-gradient(var(--color-accent) 0 0) 50% 50% / var(--hero-stroke, 1px) var(--hero-glyph) no-repeat;
    opacity: var(--hero-tone-max);
    animation: hero-breathe var(--hero-beat) ease-in-out infinite;
  }
  /* the pale half of the field runs the same cycle backwards, so the two tones
     trade places instead of the whole grid pulsing together */
  .hero__cell--dim { opacity: var(--hero-tone-min); animation-direction: reverse; }
  @keyframes hero-breathe { from, to { opacity: var(--hero-tone-max) } 50% { opacity: var(--hero-tone-min) } }
  /* Drop a plus at web/brand/plus.svg and the grid uses that shape instead of
     the drawn one. It is applied as a MASK, not an image, so the accent colour
     and the per-cell opacity still come from here - the file supplies geometry
     only and can be any colour. */
  .hero__grid--svg .hero__cell--plus {
    background: var(--color-accent);
    -webkit-mask: var(--hero-plus) 50% 50% / var(--hero-glyph) var(--hero-glyph) no-repeat;
    mask: var(--hero-plus) 50% 50% / var(--hero-glyph) var(--hero-glyph) no-repeat;
  }
  /* the solid squares pulse too, but harder than the pluses - .16 to full -
     and in sequence rather than together, so the mark reads as one travelling
     pulse. The order and the per-square delay come from the draw loop. */
  .hero__cell--fill {
    background: var(--color-accent); opacity: var(--hero-tone-min); transform: scale(var(--hero-fill-scale));
    animation: hero-pulse var(--hero-beat) ease-in-out infinite;
  }
  @keyframes hero-pulse { from, to { opacity: var(--hero-tone-min) } 50% { opacity: 1 } }
  /* The global reduce rule only shortens durations, which would freeze every
     cell on one tone - kill the animation outright and keep the static weave.
     Must sit AFTER both cell rules: a media query adds no specificity, so
     declared earlier it simply loses to them. */
  @media (prefers-reduced-motion: reduce) {
    .hero__cell--plus, .hero__cell--fill { animation: none; }
  }
  .hero__cell--char {
    color: var(--color-accent); font-family: "Jersey 10", var(--font-mono);
    font-size: var(--hero-type, 1rem); line-height: 1; font-weight: 400;
  }
  .hero__error { margin: .75rem 0 0; color: var(--color-danger); font-family: var(--font-mono); font-size: .75rem; letter-spacing: var(--mono-tracking); }
  .hero__error:empty { display: none; }

  /* ---- nav ----------------------------------------------------------- */
  .site-nav {
    position: relative; display: flex; flex-wrap: wrap; gap: var(--nav-gap);
    align-items: center; padding-block: var(--stack-gap) 0;
  }
  /* no rules on the nav at all - it sits on open background by request */
  /* two-line item: the ordinal sits above the label, and the label carries the
     count as a filled badge on the same baseline */
  .site-nav__link {
    position: relative;
    display: inline-flex; flex-direction: column; gap: .3rem; align-items: flex-start;
    border: 0; padding: 0; background: transparent; cursor: pointer;
    transition: color var(--transition-duration) var(--ease-out);
  }
  /* beats the UA [hidden] rule losing to display:inline-flex above - without
     this the Failures tab stays visible with SHOW_EXTRAS off */
  .site-nav__link[hidden] { display: none; }
  /* hairline between items, drawn in the gap before each one after the first */
  .site-nav__link + .site-nav__link:before {
    position: absolute; top: 50%; left: calc(var(--nav-gap) / -2);
    width: 1px; height: 1.25rem; translate: 0 -50%;
    background: var(--color-hairline); content: "";
  }
  /* The hairline is drawn into the gap BEFORE each item, which is right on one
     row and wrong the moment the row wraps - it ends up floating in mid-air to
     the left of the first item on the second line. Below 30rem the nav becomes
     an explicit two-column grid and the rules go: deterministic, rather than
     depending on where the flex row happens to break. */
  @media (max-width: 30rem) {
    .site-nav { display: grid; grid-template-columns: 1fr 1fr; gap: 1.75rem 1.5rem; }
    .site-nav__link + .site-nav__link:before { content: none; }
  }
  .site-nav__num {
    color: var(--color-faint-text); font-family: var(--font-mono); font-size: var(--type-mono-xs);
    letter-spacing: var(--mono-tracking);
  }
  /* centred, not baseline: the badge is 12px against a 16px label, so sitting
     it on the shared baseline hung the box low */
  .site-nav__row { display: inline-flex; gap: .45rem; align-items: center; }
  .site-nav__label {
    color: var(--color-muted); font-size: 1rem; font-weight: 400; line-height: 1;
    text-transform: uppercase;
    transition: color var(--transition-duration) var(--ease-out);
  }
  .site-nav__link:hover .site-nav__label { color: var(--color-foreground); }
  .site-nav__link[aria-current] .site-nav__label { color: var(--color-foreground); font-weight: 700; }
  .site-nav__count {
    padding: .1em .3em; background: rgba(var(--color-accent-rgb), .65); color: #fff;
    font-family: var(--font-mono); font-size: .75rem; font-weight: 700; line-height: 1.15;
  }
  .site-nav__link[aria-current] .site-nav__count { background: var(--color-accent); }
  /* separator lives on the label so it stays outside the badge fill, and
     disappears with the badge while the counts are still loading */
  .site-nav__label:after { margin-left: .45rem; color: var(--color-faint); content: "·"; }
  .site-nav__row:has(.site-nav__count:empty) .site-nav__label:after { content: none; }
  .site-nav__count:empty { display: none; }

  /* ---- section header (kept, hidden by SHOW_EXTRAS) ------------------- */
  .section-label {
    margin: 0; color: var(--color-body); font-family: var(--font-mono);
    font-size: var(--type-mono-md); letter-spacing: var(--mono-tracking); line-height: 1.2;
  }
  .section-label__number { color: var(--color-accent); }
  .section-label__slash { margin: 0 .5rem; color: var(--color-faint); }
  .section-header { position: relative; z-index: 1; display: grid; gap: 1.375rem; margin-bottom: 1.5rem; }
  .section-header[hidden] { display: none; }
  .section-header__label-row { display: flex; gap: 1rem; align-items: flex-start; justify-content: space-between; }
  .section-header__label { font-family: var(--font-mono); font-size: .75rem; letter-spacing: var(--mono-tracking); text-transform: uppercase; }
  .section-header__body {
    display: grid; gap: clamp(1.25rem, 1.611rem + 1.85vw, 3rem);
    grid-template-columns: minmax(0, 27.5rem) minmax(13.75rem, 22.5rem);
    align-items: end; justify-content: space-between;
  }
  .section-header__title { margin: 0; font-size: 1.75rem; font-weight: 700; line-height: 1.1; }
  .section-header__description { max-width: 20rem; margin: 0; color: var(--color-body); font-size: .875rem; line-height: 1.5; }
  @media (max-width: 60rem) { .section-header__body { grid-template-columns: minmax(0, 1fr); } }

  /* ---- controls ------------------------------------------------------ */
  /* six controls on one line at desktop width, which is what the design calls
     for - hence the tighter gap and the shorter basis on the search box */
  .controls {
    display: flex; flex-wrap: wrap; gap: 2.5rem;
    align-items: flex-end;
  }
  .field { display: grid; gap: .375rem; min-width: 0; flex: 0 1 auto; }
  /* every select is the same width, so the row reads as one rank of controls
     rather than each one sized to its own longest option */
  .field select.field__control { min-width: 8.5rem; max-width: 8.5rem; }
  /* search is 0.8x its old rendered width, and fixed rather than growing - it
     used to absorb all the slack, which is now the gap before Sort */
  .field--grow { flex: 0 0 18.375rem; }
  /* Sort sits apart on the right; everything else clusters left */
  #f-sort { margin-left: auto; }
  .field__label {
    color: var(--color-muted); font-family: var(--font-mono); font-size: var(--type-mono-xs);
    letter-spacing: var(--mono-tracking); text-transform: uppercase;
  }
  .field__control {
    appearance: none; width: 100%; padding: .375rem 0; border: 0;
    border-bottom: var(--hairline-width) solid var(--color-hairline); border-radius: 0; background: transparent;
    color: var(--color-foreground); font-family: var(--font-sans); font-size: .875rem;
    line-height: 1.4; transition: border-color var(--transition-duration) var(--ease-out);
  }
  select.field__control { padding-right: 1.25rem; background:
    linear-gradient(45deg, transparent 50%, var(--color-muted) 50%) calc(100% - 5px) calc(50% + 1px) / 5px 5px no-repeat,
    linear-gradient(135deg, var(--color-muted) 50%, transparent 50%) calc(100% - 1px) calc(50% + 1px) / 5px 5px no-repeat;
    cursor: pointer; }
  .field__control:focus { outline: 0; border-bottom-color: var(--color-accent); }
  .field__control::placeholder { color: var(--color-faint-text); }
  /* the native clear button cannot be recoloured, so it is replaced: the glyph
     is a mask and the colour is ours. Chromium/Safari only - Firefox draws no
     clear button at all, which is why this degrades to nothing rather than
     leaving a grey one behind. */
  .field__control[type="search"]::-webkit-search-cancel-button {
    -webkit-appearance: none; appearance: none;
    width: .73125rem; height: .73125rem; cursor: pointer; background: var(--color-accent);
    -webkit-mask: var(--icon-x) 50% 50% / contain no-repeat;
    mask: var(--icon-x) 50% 50% / contain no-repeat;
  }
  /* Chromium already withholds the cancel button on an empty field, but that is
     UA behaviour rather than a guarantee - and the contact x next to it is
     spelled out, so this one is too. Same rule, same place, one thing to read. */
  .field__control[type="search"]:placeholder-shown::-webkit-search-cancel-button { display: none; }
  /* Below the width where all six controls fit on one line, a flex row is the
     wrong tool: `margin-left: auto` on Sort stops meaning "push it to the far
     end of the row" and starts meaning "strand it alone on a row of its own",
     which is what it was doing at every width under 1280. A grid instead -
     every field the same width, search across the top, no orphan. */
  @media (max-width: 78rem) {
    .controls {
      /* 7.5rem, so two columns still fit inside a 320px screen: 272px of usable
         width minus the 28px gap leaves 122px a column. At 9.5rem they dropped
         to one column and the filter block was 445px tall on a phone. */
      display: grid; grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
      gap: 1.5rem 1.75rem; align-items: end;
    }
    .field--grow { flex: none; grid-column: 1 / -1; }
    /* the fixed select width exists to make one rank of controls read evenly;
       inside a grid the grid already does that */
    .field select.field__control { min-width: 0; max-width: none; }
    #f-sort { margin-left: 0; }
  }

  .field--check { align-self: flex-end; padding-bottom: .5rem; }
  .check {
    display: inline-flex; gap: .625rem; align-items: center; cursor: pointer;
    color: var(--color-muted); font-family: var(--font-mono); font-size: var(--type-mono-xs);
    letter-spacing: var(--mono-tracking); text-transform: uppercase;
  }
  .check input { width: .875rem; height: .875rem; margin: 0; accent-color: var(--color-accent); border-radius: 0; }

  /* ---- result bar + filter chips ------------------------------------- */
  .resultbar {
    display: flex; flex-wrap: wrap; gap: .75rem 1.25rem; align-items: center;
    justify-content: space-between; margin-top: clamp(1.25rem, 2.5dvh, 1.75rem);
  }
  .resultbar__count {
    margin: 0; color: var(--color-muted); font-family: var(--font-mono); font-size: var(--type-mono-sm);
    letter-spacing: var(--mono-tracking); line-height: 1.5; text-transform: uppercase;
  }
  .resultbar__count b { color: var(--color-foreground); font-weight: 500; }
  .chips { display: flex; flex-wrap: wrap; gap: .375rem; align-items: center; justify-content: flex-end; }
  /* Same 24px target rule as .site-button, but chips have a painted box, so
     padding would be visible. An invisible centred overlay instead - it works
     here because nothing else uses the chip's pseudo-elements. */
  .chip::after {
    content: ""; position: absolute; top: 50%; left: 50%;
    width: 100%; height: 1.5rem; transform: translate(-50%, -50%);
  }
  .chip {
    position: relative;
    appearance: none; display: inline-flex; gap: .4em; align-items: center; height: 1.375rem;
    padding: 0 .45rem; border: 0; border-radius: 0;
    background: var(--color-accent); color: var(--color-accent-foreground);
    font-family: var(--font-mono); font-size: .75rem; font-weight: 700;
    letter-spacing: var(--mono-tracking);
    text-transform: uppercase; white-space: nowrap; cursor: pointer;
    transition: opacity var(--dur-quick) var(--ease-out);
  }
  .chip:hover { opacity: .78; }
  /* The text glyph is replaced by a masked mark. Geist Mono's multiplication
     sign carries 9.72 of ink above the baseline and -1.61 below it, so its ink
     centre sits 0.84px under the box centre however the box is aligned - no
     amount of flex centring fixes that. The mask is symmetric in its own
     viewBox, so it is centred by construction.
     The box is the mark, with no dead space around it, so the chip's own
     .45rem padding is the real inset on the right and matches the left. The
     mask is oversized to 133% because the icon's ink fills 3/4 of its viewBox. */
  .chip__x {
    display: inline-block; width: .6em; height: .6em;
    color: transparent; background: var(--color-accent-foreground);
    -webkit-mask: var(--icon-x) 50% 50% / var(--icon-x-scale) var(--icon-x-scale) no-repeat;
    mask: var(--icon-x) 50% 50% / var(--icon-x-scale) var(--icon-x-scale) no-repeat;
  }
  /* red: it is the one control here that throws work away */
  .chip--clear {
    height: auto; padding: 0; background: transparent; color: var(--color-danger);
    text-decoration: underline; text-underline-offset: .25em;
  }
  .chip--clear:hover { opacity: 1; color: var(--color-danger-strong); }

  /* ---- job cards ----------------------------------------------------- */
  /* One hairline down the middle of the grid, drawn on the container so it does
     not break when a row has an odd number of cards. */
  .job-grid {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--col-gap); padding: 0; margin: var(--stack-gap) 0 0; list-style: none;
    border-bottom: var(--hairline-width) solid var(--color-hairline);
  }
  /* The centre divider is drawn per card rather than as one line down the
     container, so it breaks at every row boundary instead of running through
     the horizontal rules. Inset top and bottom is what makes the gap. */
  .job-grid > .job-card:nth-child(odd):before {
    position: absolute; top: var(--card-pad-y); right: calc(var(--col-gap) / -2); bottom: var(--card-pad-y);
    width: 0; border-left: var(--hairline-width) solid var(--color-hairline); content: "";
  }
  .job-grid[hidden] { display: none; }
  @media (max-width: 60rem) {
    .job-grid { grid-template-columns: minmax(0, 1fr); }
    .job-grid > .job-card:nth-child(odd):before { content: none; }
  }

  .job-card {
    position: relative; display: grid; gap: .5rem 1rem;
    grid-template-columns: var(--logo-size) minmax(0, 1fr) auto;
    align-items: start; padding: var(--card-pad-y) 0;
    border-top: var(--hairline-width) solid var(--color-hairline);
    color: var(--color-soft); transition: color var(--transition-duration) var(--ease-out);
  }
  /* same underline sweep the list rows use */
  /* the rules separate rows, so the top row has nothing to separate from -
     two cards at two columns, one when the grid collapses */
  .job-grid > .job-card:nth-child(-n+2) { border-top: 0; }
  @media (max-width: 60rem) {
    .job-grid > .job-card:nth-child(2) { border-top: var(--hairline-width) solid var(--color-hairline); }
  }
  .job-card:after {
    position: absolute; right: 0; bottom: -1px; left: 0; z-index: 2; display: block; height: 0;
    border-top: 2px solid var(--color-accent); content: ""; opacity: 0;
    pointer-events: none; transform: scaleX(0); transform-origin: 0 50%;
    transition: transform .22s cubic-bezier(.4, 0, .2, 1), opacity 80ms ease-out .18s;
  }
  .job-card:hover { color: var(--color-foreground); }
  /* hovering one card recedes the rest. :has, not .job-grid:hover, so the gaps
     and the divider between columns do not trigger it */
  .job-card { transition: color var(--transition-duration) var(--ease-out), opacity var(--transition-duration) var(--ease-out); }
  .job-grid:has(.job-card:hover) > .job-card:not(:hover) { opacity: var(--recede-opacity); }
  .job-card:hover:after { opacity: 1; transform: scaleX(1); transition: transform .36s cubic-bezier(.22, .61, .36, 1), opacity 80ms ease-out; }

  /* No tile of ours. The image sits on the page exactly as Dealroom holds it.
     The pale and dark grounds are gone by request - the dark one in particular
     was what made a row of cards read as a row of black boxes, because a mark
     drawn in white ink gets classified light and handed a dark ground to sit on.
     Whatever background a mark now shows is its OWN, baked into the source file;
     9b_fetch_dealroom_logos.js writes those bytes unaltered on purpose. */
  .job-card__logo {
    display: grid; width: var(--logo-size); height: var(--logo-size); place-items: center;
    background: none; overflow: hidden;
  }
  /* Kept as hooks only. board.js still reads _dark_tile from the manifest, and
     that list is empty now, so neither class is emitted. Give either of these a
     background and the grounds come straight back for whatever the list holds. */
  .job-card__logo--dark, .job-card__logo--os-dark { background: none; }
  /* contain, so a non-square source keeps its aspect ratio rather than stretching */
  .job-card__logo img { width: 100%; height: 100%; object-fit: contain; }
  .job-card__logo span {
    color: var(--color-mark-ink); font-family: var(--font-mono); font-size: 1.0625rem;
    font-weight: 500; line-height: 1; text-transform: uppercase;
  }
  .job-card__body { grid-column: 2; display: grid; gap: .375rem; min-width: 0; }
  .job-card__head { display: flex; flex-wrap: wrap; gap: .5rem .625rem; align-items: center; }
  .job-card__title {
    /* full strength on every card, not just the hovered one */
    margin: 0; min-width: 0; overflow-wrap: break-word; color: var(--color-foreground);
    font-size: clamp(.9375rem, calc(.9rem + .15vw), 1.0625rem); font-weight: 600;
    letter-spacing: -.005em; line-height: 1.2; text-transform: uppercase;
  }
  .job-card__title a:hover { color: var(--color-accent); }
  .job-card__meta {
    display: flex; flex-wrap: wrap; gap: .25rem .5rem; align-items: center;
    color: var(--color-muted); font-family: var(--font-mono); font-size: var(--type-mono-sm);
    letter-spacing: var(--mono-tracking); text-transform: uppercase;
  }
  .job-card__meta .strong { color: var(--color-body); }
  .job-card__meta .sep { color: var(--color-faint); }
  .job-card__tags { display: flex; flex-wrap: wrap; gap: .3rem; align-items: center; }
  .job-card__tags:empty { display: none; }
  .job-card__site { grid-column: 3; align-self: start; margin-top: .1rem; color: var(--color-foreground); }
  @media (max-width: 40rem) {
    /* one token, not two rules: the tile and the grid column it sits in are
       the same measurement, and they drifted apart the last time they were
       written separately. (The 40rem itself stays a literal - a custom
       property is illegal in a media condition and the query would silently
       evaporate.) */
    .job-card { --logo-size: 2.25rem; grid-template-columns: var(--logo-size) minmax(0, 1fr); }
    .job-card__site { grid-column: 2; }
  }

  /* outlined, not filled. Height comes from the padding rather than a fixed
     value - .1em .3em, the same as the nav count - so the box tracks the type. */
  .badge {
    display: inline-flex; align-items: center; padding: .1em .3em;
    border: 1px solid var(--color-accent); background: transparent; color: var(--color-accent);
    font-family: var(--font-mono); font-size: var(--type-mono-2xs); font-weight: 700; line-height: 1.15;
    letter-spacing: var(--mono-tracking); text-transform: uppercase; white-space: nowrap;
  }

  /* ---- axis list: companies, people, failures ------------------------ */
  .axis-list {
    position: relative; display: flex; flex-direction: column; padding: 0;
    margin: var(--stack-gap) 0 0; border-bottom: var(--hairline-width) solid var(--color-hairline); list-style: none;
  }
  .axis-list[hidden] { display: none; }

  /* ---- people lock ----------------------------------------------------
     display is set explicitly here for the same reason .axis-list[hidden]
     is above it and .site-button[hidden] is near the top of this file: a
     class rule with equal specificity to [hidden] that comes later in the
     cascade wins, so [hidden] alone is not enough to trust. */
  .people-lock {
    display: flex; flex-direction: column; gap: 1.25rem; align-items: flex-start;
    margin: var(--stack-gap) 0 0; padding-bottom: 2rem;
    border-bottom: var(--hairline-width) solid var(--color-hairline);
  }
  .people-lock[hidden] { display: none; }
  .people-lock__note { max-width: 34rem; color: var(--color-muted); font-family: var(--font-mono); font-size: var(--type-mono-sm); line-height: 1.5; margin: 0; }
  .people-lock__form { display: flex; align-items: flex-end; gap: 1rem; flex-wrap: wrap; }
  .people-lock__says { font-family: var(--font-mono); font-size: var(--type-mono-xs); color: var(--color-danger); }
  .people-lock__says:empty { display: none; }
  .axis-list:before {
    position: absolute; top: 0; bottom: 0; left: var(--axis-x); width: 0;
    border-left: var(--hairline-width) solid var(--color-hairline); content: "";
  }
  .axis-item {
    position: relative; z-index: 1; display: grid;
    grid-template-columns: 3.5rem minmax(0, 1fr); align-items: start;
    padding: clamp(.875rem, 2dvh, 1.125rem) .25rem clamp(.875rem, 2dvh, 1.125rem) 0;
    border-top: var(--hairline-width) solid var(--color-hairline); color: var(--color-soft);
    transition: color var(--transition-duration) var(--ease-out);
  }
  .axis-list > .axis-item:first-child { border-top: 0; }
  .axis-item:after {
    position: absolute; right: 0; bottom: -1px; left: 0; z-index: 2; display: block; height: 0;
    border-top: 1px solid rgba(var(--color-accent-rgb), .5); content: ""; opacity: 0;
    pointer-events: none; transform: scaleX(0); transform-origin: var(--axis-x) 50%;
    transition: transform .22s cubic-bezier(.4, 0, .2, 1), opacity 80ms ease-out .18s;
  }
  .axis-item:hover { color: var(--color-foreground); }
  .axis-item:hover:after { opacity: 1; transform: scaleX(1); transition: transform .36s cubic-bezier(.22, .61, .36, 1), opacity 80ms ease-out; }
  .axis-item__chip {
    display: inline-flex; width: max-content; min-width: 2.25rem; height: 1.5rem;
    align-items: center; justify-content: center; justify-self: center; align-self: start;
    margin-top: .125rem; padding: 0 .5rem; border: 1px solid var(--color-accent-line);
    background: var(--color-background); color: var(--color-accent);
    font-family: var(--font-mono); font-size: var(--type-mono-sm); font-weight: 400; line-height: 1;
  }
  .axis-item__chip--solid { border-color: var(--color-accent); background: var(--color-accent); color: var(--color-accent-foreground); font-weight: 500; }
  .axis-item__chip--mute { border-color: var(--color-hairline); color: var(--color-faint-text); }
  .axis-item__body { grid-column: 2; display: grid; gap: .375rem; min-width: 0; }
  .axis-item__name { display: flex; gap: .75rem; align-items: baseline; justify-content: space-between; }
  .axis-item__name-text {
    min-width: 0; overflow-wrap: break-word; color: currentColor;
    font-size: clamp(1rem, calc(.95rem + .2vw), 1.125rem); font-weight: 600;
    letter-spacing: -.01em; line-height: 1.15;
  }
  .axis-item__name-text a:hover { color: var(--color-accent); }
  .axis-item__aside {
    flex: none; color: var(--color-muted); font-family: var(--font-mono); font-size: var(--type-mono-sm);
    letter-spacing: var(--mono-tracking); text-transform: uppercase; text-align: right;
  }
  .axis-item__meta {
    display: flex; flex-wrap: wrap; gap: .375rem .75rem; align-items: center;
    color: var(--color-muted); font-family: var(--font-mono); font-size: var(--type-mono-sm);
    letter-spacing: var(--mono-tracking); text-transform: uppercase;
  }
  .axis-item__meta .sep { color: var(--color-faint); }
  .axis-item__meta .strong { color: var(--color-body); }
  /* The Dealroom facts - ranking, technologies, revenue, investors, LinkedIn.
     A line of its own inside the meta block, one step quieter than the line
     above it: that one says where the jobs are, this one says what the company
     is, and the first is why a reader is here. flex-basis 100% is what breaks
     it onto its own row inside the wrapping flex parent. */
  .axis-item__dr {
    flex-basis: 100%; display: flex; flex-wrap: wrap; gap: .375rem .625rem; align-items: center;
    margin-top: .125rem; color: var(--color-faint-text); font-size: var(--type-mono-xs);
  }
  .axis-item__dr .sep { color: var(--color-faint); }
  .axis-item__dr .link { color: inherit; text-decoration: underline; text-underline-offset: .2em; }
  .axis-item__desc { max-width: 46rem; color: var(--color-muted); font-size: .875rem; line-height: 1.5; }

  .tag {
    display: inline-flex; align-items: center; height: 1.125rem; padding: 0 .375rem;
    border: var(--hairline-width) solid var(--color-hairline); color: var(--color-muted);
    font-family: var(--font-mono); font-size: var(--type-mono-xs); letter-spacing: var(--mono-tracking);
    text-transform: uppercase; white-space: nowrap;
  }
  .tag--accent { border-color: var(--color-accent-line); color: var(--color-accent); }
  .tag--warn { border-color: rgba(168, 100, 27, .45); color: var(--color-warn); }
  .link { color: var(--color-accent); }
  .link:hover { text-decoration: underline; text-underline-offset: .2em; }
  .muted { color: var(--color-faint-text); }

  .empty { padding: 4rem 0; color: var(--color-muted); font-family: var(--font-mono); font-size: var(--type-mono-md); letter-spacing: var(--mono-tracking); text-transform: uppercase; }
  .more {
    display: flex; flex-direction: column; gap: .75rem;
    align-items: center; justify-content: center; padding: 1.75rem 0 0;
  }
  .more[hidden] { display: none; }
  /* .64 of the button it sits under. Deliberately its own literal and not
     --recede-opacity: that token is the card hover, a different decision that
     happens to land on the same number.
     At .56 this measured 4.28:1 and failed AA by a hair; .64 measures 5.84:1.
     --type-mono-xs is the same token the filter and contact labels use, so
     every small mono label on the page is one number. */
  .more__left {
    opacity: .64; font-family: var(--font-mono); font-size: var(--type-mono-xs);
    letter-spacing: var(--mono-tracking); text-transform: uppercase;
  }
  .more__note { padding: 1.25rem 0 0; color: var(--color-muted); font-family: var(--font-mono); font-size: var(--type-mono-sm); letter-spacing: var(--mono-tracking); text-align: center; text-transform: uppercase; }

  /* ---- footer -------------------------------------------------------- */
  /* Two columns: the plus field with the logo mark stamped into its top-left and
     the logo beneath it, against a solid accent panel carrying the contact form
     with the live status underneath. */
  .site-footer {
    position: relative;
    /* 42:50 and the panel bleeding right, measured off the drawing */
    display: grid; grid-template-columns: 44fr 48fr; align-items: start;
    gap: var(--stack-gap);
    padding-block: var(--stack-gap);
  }
  /* the field sits lower than the panel's top edge, as drawn */
  .site-footer__left { display: flex; flex-direction: column; justify-content: space-between; gap: var(--stack-gap); min-height: 100%; padding-top: 3.75rem; }
  /* the field inherits every hero token - same plus, same beat, same tones */
  .site-footer__field { margin: 0; }
  .site-footer__brand { display: inline-flex; align-items: center; }
  .site-footer__mark { display: block; width: auto; height: 4rem; }
  #footer-text {
    font-family: var(--font-mono); font-size: var(--type-mono-md); font-weight: 500;
    letter-spacing: .14em; text-transform: uppercase;
  }

  .site-footer__right { display: flex; flex-direction: column; gap: var(--stack-gap); }
  /* nothing here about type or gaps - see .status-bar / .status */

  /* ---- contact panel ------------------------------------------------- */
  .contact {
    /* bleeds through the container gutter to the viewport edge, as drawn */
    margin-right: calc(var(--site-gutter) * -1);
    padding: 3.5rem;
    background: var(--color-accent); color: var(--color-accent-foreground);
  }
  .contact .site-button { --bracket-color: currentColor; }
  .contact__title {
    margin: 0 0 1.5rem; font-size: clamp(1.25rem, calc(.9rem + .9vw), 1.75rem);
    font-weight: 700; letter-spacing: -.02em; line-height: 1.1; text-transform: uppercase;
  }
  .contact__form { display: grid; gap: 1.25rem; }
  .contact__field { display: grid; gap: .5rem; }
  .contact__label {
    font-family: var(--font-mono); font-size: var(--type-mono-xs);
    letter-spacing: var(--mono-tracking); text-transform: uppercase;
    color: #ffffff; opacity: 1;
  }
  .contact__control {
    /* Still .75rem. The clear button's negative margin pulls its border box
       .35625rem left, and the mark sits .35625rem inside that box, so the two
       cancel exactly and the visible gap is the gap. I widened this first and
       measured 17.7px where 12 was wanted - the arithmetic only looks like it
       needs compensating. */
    display: flex; gap: .75rem; align-items: center;
    border-bottom: var(--hairline-width) solid rgba(255, 255, 255, .16);
    transition: border-color var(--dur-quick) var(--ease-out);
  }
  .contact__control:focus-within { border-bottom-color: var(--color-accent-foreground); }
  /* deliberately the same type as .field__control in the filter row - same
     family, same .875rem, same line-height, same padding. The two are the only
     text inputs on the page and they should read as one control. */
  .contact__input {
    appearance: none; flex: 1 1 auto; width: 100%; padding: .375rem 0; border: 0;
    background: transparent; color: var(--color-accent-foreground);
    font-family: var(--font-sans); font-size: .875rem; line-height: 1.4;
  }
  .contact__input:focus { outline: 0; }
  /* Matched to the search placeholder by CONTRAST, not by alpha - the two
     backgrounds are nowhere near each other in luminance, so equal alpha would
     not have looked equal. --color-faint on white measures 2.06:1; .375 white
     on #133AFF measures 2.07:1. Retune this if the accent ever moves again. */
  .contact__input::placeholder { color: rgba(255, 255, 255, .375); }
  /* same masked mark as the chips and the search field, so there is one x on
     the page and its weight is set in exactly one place */
  /* 24px hit area, 12.6px mark - the third shape of the same SC 2.5.8 fix.
     The overlay trick used on .chip is no good here: the mask applies to the
     element AND its pseudo-elements, so an invisible ::after would be masked
     into an x-shape and stop being a target. So the button itself grows, the
     MARK moves to ::after at its designed size, and a negative margin returns
     the space. The parent's gap is widened by the same amount it eats, which
     is why .contact__control reads 1.106rem rather than .75rem. */
  .contact__x {
    position: relative;
    appearance: none; flex: none; width: 1.5rem; height: 1.5rem; margin: -.35625rem; padding: 0; border: 0;
    background: none; cursor: pointer;
    opacity: .85; transition: opacity var(--dur-quick) var(--ease-out);
  }
  .contact__x:after {
    content: ""; position: absolute; inset: 0; margin: auto;
    width: .7875rem; height: .7875rem;
    background: var(--color-accent-foreground);
    -webkit-mask: var(--icon-x) 50% 50% / contain no-repeat;
    mask: var(--icon-x) 50% 50% / contain no-repeat;
  }
  .contact__x:hover { opacity: 1; }
  /* visibility, not display: the x keeps its box, so the rule the input sits on
     does not change length the moment the first character is typed. Both inputs
     carry a placeholder, which is what makes :placeholder-shown mean "empty". */
  .contact__input:placeholder-shown + .contact__x { visibility: hidden; }
  /* off-screen rather than display:none - some bots skip anything a browser
     would not render, and the point is that they DO fill this one in */
  .contact__hp {
    position: absolute; left: -9999px; width: 1px; height: 1px;
    opacity: 0; pointer-events: none;
  }
  .contact__actions { display: flex; gap: 1rem; align-items: center; justify-content: flex-end; }
  .contact__says {
    margin-right: auto;
    font-family: var(--font-mono); font-size: var(--type-mono-xs);
    letter-spacing: var(--mono-tracking); text-transform: uppercase; opacity: .9;
  }
  .contact__note {
    margin: 2.25rem 0 0;
    font-family: var(--font-mono); font-size: var(--type-mono-xs); line-height: 1.7;
  }

  @media (max-width: 60rem) {
    .site-footer { grid-template-columns: minmax(0, 1fr); }
    .site-footer__left { order: 2; }
    .site-footer .status-bar { justify-content: flex-start; }
  }

  /* ---- focus ----------------------------------------------------------
     Every focusable thing on the page, in one block, deliberately last so
     nothing earlier can win by source order. Six controls used to sit at
     `outline: 0` with a 1px underline colour change standing in for a ring -
     measured by tabbing, stops 6 through 11 all reported `outline: none`.

     :focus-visible, not :focus - a mouse click on a select should not paint a
     ring, and the browser already knows the difference. */
  .site-button:focus-visible,
  .site-header__brand:focus-visible,
  .site-footer__brand:focus-visible,
  .site-nav__link:focus-visible,
  .field__control:focus-visible,
  .chip:focus-visible,
  .contact__input:focus-visible,
  .contact__x:focus-visible,
  .job-card__title a:focus-visible,
  .axis-item__name a:focus-visible {
    outline: var(--focus-ring); outline-offset: var(--focus-ring-offset);
  }
  /* accent blue on accent blue is not a ring */
  .contact { --focus-ring-color: var(--color-accent-foreground); }

  .flash { animation: flash 1.4s ease-out; }
  @keyframes flash { from { background: rgba(var(--color-accent-rgb), .07) } to { background: transparent } }

  @media (prefers-reduced-motion: reduce) { * { animation-duration: .01ms !important; transition-duration: .01ms !important; } }
