/* ============================================================
   John McDonald — Cybersecurity / AI Security Portfolio
   assets/css/style.css

   Design direction: "ZINE / TORN EDITORIAL" on a near-black wall.
   The page reads like a stapled, photocopied zine cut from the
   poster references. The ground is near-black almost everywhere —
   exactly ONE section (About) is warm paper, torn top and bottom;
   paper appears elsewhere only as collage scraps (cards, the hero
   subject card). Display type is enormous Bebas Neue with one
   outlined word per headline; section marks are Old London
   blackletter (paper ground) or outlined Bebas roman numerals
   (black grounds) — never both on the same head. Pull quotes are
   Playfair italic; everything else (nav, labels, data, body copy)
   is Space Mono. Tiny letter-spaced caps captions pin the corners
   of every section and always knock out their ground so the film
   grain never softens them.

   Palette: black / paper / gray dominant. Accents are rationed:
     gold  #d9a839 — THE page accent (nav, marks, stamps, sparkles,
           hatch). On paper: #a3781f display / #7a5915 small text.
     blue  #7cc3e0 — Featured Projects ONLY (the full-bleed liquid
           texture plus that section's status labels — all three
           project badges are blue; the status WORD carries the
           difference, not a second fill colour).
     teal  #1c4b49 — the Contact trapezoid ONLY, on every page.
           .badge-green and .badge-accent are still spelled that way by
           projects.html and labs.js, but both now paint themselves with
           --accent-solid, so a badge is whatever the SECTION's one
           accent is: blue inside .accent-blue (unchanged on the home
           page's Featured Projects), gold everywhere else.
     red   #e0281c — reserved for at most one alert mark; unused here.

   Ground-relative tokens: every component reads --ground / --ink /
   --line / --accent etc. Those default to the BLACK ground on
   :root; a `.sec-paper` wrapper flips them to the PAPER ground, so
   the same component markup works on either. THREE things carry a
   ground, and each one flips the whole token set: `.sec-paper`
   (paper), `.card` / `.subject-card` (a paper scrap pasted on any
   ground) and `.thumb` (a black window cut back into a scrap).
   Nesting them therefore composes correctly in either direction.

   Every <section> — with or without `.sec` — is the clip boundary
   for its own decoration (position:relative + overflow:hidden), so
   a tilted scrap, a ghost word or a halftone patch can bleed past
   the text column without putting the page into horizontal scroll.

   To build a page:

     <section class="sec sec-black">…</section>
     <section class="sec sec-paper tear-1">…</section>   (torn top+bottom)
     <section class="sec sec-black">…</section>

   SECTION RECIPES (the home page's compositions, documented so
   projects.html / labs.html / 404.html / projects/*.html can reuse
   them without re-inventing anything):

     Poster hero      .sec.sec-black.hero > .hero-tex (full-bleed
                      texture + veil) + .container > .poster
                      (dashed frame, .dev-crease, four .dev-cap,
                      .dev-vtext) > .hero-grid > .hero-copy +
                      .collage > .subject-card.
     Subject card     .subject-card > .dev-tape + .subject-head +
                      dl.field-list + .subject-foot + .stamp.stamp-lg.
                      A torn paper scrap for shell-style key/value data.
     Section head     .section-head.with-num > div(eyebrow + h2) +
                      .dev-num  — black grounds. On paper use
                      .section-head.with-mark + .dev-mark instead.
     Eyebrow mark     <p class="eyebrow"><span class="mark">[02]</span> …</p>
                      (markup, not a CSS counter, so screen readers
                      and textContent stay clean).
     Textured band    .sec-black > .projects-tex (or .hero-tex): a
                      full-bleed WebP under a ≥0.8-alpha black veil
                      wherever real text sits.
     Card on black    .card > .dev-halftone (L-shaped offset dot
                      shadow that never covers the paper) +
                      .card-tag caption + optional .thumb.
     Thumbnail        .thumb.thumb-blue|thumb-mono[.invert]|thumb-silk
                      — halftone-screened texture strip with a boxed
                      .dev-cap and a .dev-barcode.
     Exhibit panel    .panel.record-panel > four .corner + .record-grid
                      > .record-meta (micro caps, hatch, tall barcode)
                      + copy. The dashed evidence frame.
     Framed exhibit   .exhibit — halftoned texture panel with a ring,
                      .flip-text and a bordered .exhibit-tag.
     Mirrored word    .proof-mark > .dev-rect ×2 + .mirror-word +
                      .mirror-word.reflection (Playfair, ref 14).
     Stacked word     .dev-stack — AT MOST TWO PER PAGE.
     Footer           .dev-checker strip + .site-footer > .footer-wm
                      watermark + note + links + .footer-meta micro row.

   PAGE RECIPES (section 6b) — the same idiom for the five interior
   pages. Each one is documented in full, with markup, where it is
   defined; this is the index:

     Page furniture   .page-caps         four .dev-cap corners pinned to
                                         the whole document, not a section
                      .masthead          interior-page head: eyebrow +
                        + .masthead-grid / -side / -stack
                      .backlink          "back to all projects" chip,
                        + .backlink-row  with a dashed rule to the margin
     Project detail   .case-file         case-file masthead section
                      .case-head / .case-title / .case-title-row / .case-stack
                      .case-meta         the dossier meta row (CASE NO. /
                                         STATUS / FILED / SITE)
                      .section-head.sub  h2 + outlined roman numeral,
                                         one step under the case h1
                      .case-body         narrative column + .case-margin,
                        + .case-wide     .case-wide spans both tracks
                      .split-body        the same split inside a .findings box
                      .case-rail         the tall device stack beside an
                        + .rail-fill     inline case scrap (projects.html)
                      .panel.metric-strip + .panel-tag   metric band
                      .chip-row.chip-block               tech stack
                      .findings          key-findings box, h2 on the border
                      .callout.pull      Playfair pull quote
     Catalog          .filter-bar        re-cut as a contact-sheet rail
                        + .filter-btn[.active]  (contract unchanged)
                      .lab-grid .card    dense scrap, single box-shadow
                      .lab-detail        the summary-first disclosure
                      .catalog-count     the live "showing N of 44" line
     Error            .err + .err-tex + .err-stack + .h1-mono + .err-poster
     Device           .dev-target + .target-row  registration mark

   ACCENT BUDGET on the new pages: still ONE per section, and the gold
   #d9a839 is the page accent everywhere in section 6b — the three
   case-file pages included. Blue stays Featured Projects on the home
   page and teal stays the Contact trapezoid, and they mean nothing if
   an interior page wears them as its own colour: teal is what tells
   you you have reached Contact on every page. .badge-green paints
   itself with --accent-solid for exactly that reason — the status
   WORD carries the difference, never a second fill colour.

   External requests: the single Google Fonts stylesheet (+ two
   preconnects). Old London is self-hosted (Dieter Steffmann, free).
   Textures are generated locally (assets/img/*.webp, halftone.svg).

   Structure of this file:
     1.  Design tokens (fonts, palette, ground tokens, type, spacing)
     1b. Tear polygons (generated)
     2.  Reset
     3.  Base typography
     4.  Layout primitives + grounds (sections, tears, container, grid)
     5.  Components (nav, hero poster, subject card, buttons, badges,
         chips, cards, thumbnails, panels, metrics, skills grid,
         numbered list, filter bar, callout, stamp, footer)
     6.  Decorative devices (.dev-* — always aria-hidden in markup)
     6b. Page recipes (masthead, case file, catalog, error poster) —
         compositions built from 1-6, plus their own responsive band
     7.  Utilities
     8.  Responsive breakpoints
   ============================================================ */

/* ---------- 1. Design tokens ---------- */

/* Old London — blackletter for section marks, drop caps, watermarks.
   Self-hosted; swap so text never waits on it. */
@font-face {
  font-family: 'Old London';
  src: url('/assets/fonts/OldLondon.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* Bebas Neue / Playfair Display / Space Mono — self-hosted, latin subset only
   (the site is English-only, so the other Google-served subsets are dead
   weight). Same five cuts Google's own CSS would have served for the exact
   family/weight/style combinations this stylesheet actually uses below
   (--font-display, --font-quote normal + italic, --font-mono 400 + 700) —
   pulled once from fonts.gstatic.com and vendored here so the site makes
   zero requests to Google at runtime, matching the README's "no external
   font requests" claim instead of contradicting it. */
@font-face {
  font-family: 'Bebas Neue';
  src: url('/assets/fonts/bebasneue-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Playfair Display';
  src: url('/assets/fonts/playfairdisplay-600-normal.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Playfair Display';
  src: url('/assets/fonts/playfairdisplay-600-italic.woff2') format('woff2');
  font-weight: 600;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: 'Space Mono';
  src: url('/assets/fonts/spacemono-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Space Mono';
  src: url('/assets/fonts/spacemono-700-normal.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  /* -- Fixed palette -- */
  --black: #0b0b0c;
  --black-2: #141415;
  --white: #f3f1ec;
  --muted: #b8b6ae;
  --dim: #8a8880;
  --paper: #e8e6e1;
  --paper-2: #f4f2ed;
  --paper-3: #dcd9d2;
  --paper-ink: #17150f;
  --paper-ink-2: #3a3830;
  --paper-ink-3: #5c5a52;

  --gold: #d9a839;
  --gold-paper: #a3781f;       /* on paper: 3.2:1 — display (24px+) and decoration only */
  --gold-paper-text: #7a5915;  /* on paper: 5.2:1 — small text */
  --blue: #7cc3e0;
  --blue-paper: #1d5670;       /* on paper: 6.4:1 */
  --teal: #1c4b49;             /* a FILL — paper-white on it is 8.7:1 */
  --teal-paper: #1c4b49;       /* on paper as text: 7.8:1 */
  --red: #e0281c;              /* reserved: one alert mark per page, unused on index */

  /* -- Ground-relative tokens: BLACK ground by default -- */
  --ground: var(--black);
  --ground-2: var(--black-2);
  --ink: var(--white);          /* 17.4:1 on black */
  --ink-2: var(--muted);        /*  9.7:1 */
  --ink-3: var(--dim);          /*  5.5:1 */
  --line: rgba(243, 241, 236, 0.22);
  --line-strong: rgba(243, 241, 236, 0.55);
  --accent: var(--gold);        /* decoration + display-size text */
  --accent-text: var(--gold);   /* small text that must pass AA */
  --accent-solid: var(--gold);  /* fills that carry --on-accent text */
  --on-accent: var(--paper-ink);
  --scrap: var(--paper);        /* paper scraps (cards) on this ground */
  --scrap-2: var(--paper-2);
  --scrap-ink: var(--paper-ink);
  --scrap-ink-2: var(--paper-ink-2);
  --scrap-ink-3: var(--paper-ink-3);
  --scrap-line: rgba(23, 21, 15, 0.35);
  --scrap-shadow: 0 0 0 transparent;               /* a paper scrap on black casts no visible shadow */
  --tape: rgba(243, 241, 236, 0.32);
  --focus: var(--gold);

  /* -- Legacy aliases so the not-yet-restyled pages keep rendering -- */
  --bg: var(--ground);
  --bg-raised: var(--ground-2);
  --bg-panel: var(--ground-2);
  --border: var(--line);
  --border-strong: var(--line-strong);
  --text: var(--ink);
  --text-muted: var(--ink-2);
  --text-dim: var(--ink-3);
  /* The retired palette's aliases (--pink/--violet/--magenta/--coral/--orange/
     --amber/--green) are GONE. The last markup that passed them into `.disc`
     inline styles is gone with them, and a var()-reference scan of all seven
     pages, style.css and both JS files finds zero reads — so nothing can
     silently resolve a retired hue to gold any more. Do not reintroduce them. */

  /* -- Type -- */
  --font-display: 'Bebas Neue', 'Oswald', Impact, 'Arial Narrow', sans-serif;
  --font-quote: 'Playfair Display', 'Georgia', 'Times New Roman', serif;
  --font-mono: 'Space Mono', ui-monospace, 'Cascadia Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
  --font-black: 'Old London', 'UnifrakturMaguntia', 'Georgia', serif;
  --font-sans: var(--font-mono); /* body copy is monospace on purpose: photocopied typewriter zine */

  /* -- Spacing scale -- */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-24: 6rem;
  --sec-pad: clamp(4rem, 8vw, 7rem);
  --tear-h: 34px;              /* height of the torn band (matches the generator) */

  --radius: 0;                 /* corners are square. pills are gone. */
  --radius-sm: 0;
  --max-width: 1200px;

  --grain: url('/assets/img/grain.webp');

  color-scheme: dark;
}

/* ---------- 1b. Tear polygons (generated once by gen-textures.js; x in %, y in px so the
   tear height stays constant at every viewport width) ---------- */
:root {
/* about */
  --tear-about: polygon(0% 9.7px, 2.27% 2.6px, 4.55% 21px, 6.82% 12px, 9.09% 19px, 11.36% 15.7px, 13.64% 16.3px, 15.91% 14.3px, 18.18% 18.3px, 20.45% 10.4px, 22.73% 10.1px, 25% 4px, 27.27% 4.3px, 29.55% 15.8px, 31.82% 20.7px, 34.09% 10.7px, 36.36% 7px, 38.64% 16.5px, 40.91% 12.2px, 43.18% 18.4px, 45.45% 21.3px, 47.73% 22.2px, 50% 19.3px, 52.27% 21.2px, 54.55% 11px, 56.82% 12.1px, 59.09% 11.2px, 61.36% 8px, 63.64% 2.7px, 65.91% 5.6px, 68.18% 3.6px, 70.45% 11.2px, 72.73% 6.4px, 75% 2.5px, 77.27% 14.8px, 79.55% 19.3px, 81.82% 9.1px, 84.09% 8.2px, 86.36% 6.9px, 88.64% 18.8px, 90.91% 22.6px, 93.18% 11.8px, 95.45% 10.5px, 97.73% 9.6px, 100% 11px, 100% calc(100% - 3.9px), 97.73% calc(100% - 11px), 95.45% calc(100% - 10.6px), 93.18% calc(100% - 15.6px), 90.91% calc(100% - 22.6px), 88.64% calc(100% - 25.4px), 86.36% calc(100% - 32.7px), 84.09% calc(100% - 31.7px), 81.82% calc(100% - 24.8px), 79.55% calc(100% - 20.3px), 77.27% calc(100% - 19.8px), 75% calc(100% - 15px), 72.73% calc(100% - 20.1px), 70.45% calc(100% - 8.8px), 68.18% calc(100% - 6.3px), 65.91% calc(100% - 16.3px), 63.64% calc(100% - 19.6px), 61.36% calc(100% - 29.7px), 59.09% calc(100% - 22.1px), 56.82% calc(100% - 18.2px), 54.55% calc(100% - 28.9px), 52.27% calc(100% - 23.6px), 50% calc(100% - 26.1px), 47.73% calc(100% - 21.7px), 45.45% calc(100% - 27.3px), 43.18% calc(100% - 17.5px), 40.91% calc(100% - 22.6px), 38.64% calc(100% - 16.3px), 36.36% calc(100% - 28.2px), 34.09% calc(100% - 26.2px), 31.82% calc(100% - 24.7px), 29.55% calc(100% - 16.9px), 27.27% calc(100% - 15.8px), 25% calc(100% - 7.1px), 22.73% calc(100% - 2.6px), 20.45% calc(100% - 8.7px), 18.18% calc(100% - 11.8px), 15.91% calc(100% - 15.9px), 13.64% calc(100% - 15.7px), 11.36% calc(100% - 6.7px), 9.09% calc(100% - 8.1px), 6.82% calc(100% - 12.4px), 4.55% calc(100% - 8.1px), 2.27% calc(100% - 5.2px), 0% calc(100% - 5px));
  --fiber-about-top: polygon(0 0, 100% 0, 100% 13.6px, 97.73% 14.6px, 95.45% 16.0px, 93.18% 18.0px, 90.91% 27.3px, 88.64% 24.9px, 86.36% 12.4px, 84.09% 12.3px, 81.82% 14.5px, 79.55% 21.6px, 77.27% 17.3px, 75% 7.9px, 72.73% 9.9px, 70.45% 14.4px, 68.18% 8.4px, 65.91% 10.9px, 63.64% 6.4px, 61.36% 13.8px, 59.09% 15.7px, 56.82% 15.6px, 54.55% 13.2px, 52.27% 24.2px, 50% 25.3px, 47.73% 24.3px, 45.45% 24.5px, 43.18% 20.8px, 40.91% 16.8px, 38.64% 23.5px, 36.36% 12.5px, 34.09% 14.0px, 31.82% 24.5px, 29.55% 20.5px, 27.27% 6.5px, 25% 9.8px, 22.73% 12.9px, 20.45% 17.2px, 18.18% 25.2px, 15.91% 18.0px, 13.64% 19.2px, 11.36% 20.8px, 9.09% 22.8px, 6.82% 17.9px, 4.55% 27.1px, 2.27% 6.3px, 0% 13.1px);
  --fiber-about-bot: polygon(0% calc(100% - 8.4px), 2.27% calc(100% - 9.4px), 4.55% calc(100% - 12.6px), 6.82% calc(100% - 18.5px), 9.09% calc(100% - 10.2px), 11.36% calc(100% - 10.5px), 13.64% calc(100% - 20.8px), 15.91% calc(100% - 19.1px), 18.18% calc(100% - 16.2px), 20.45% calc(100% - 10.8px), 22.73% calc(100% - 7.7px), 25% calc(100% - 11.3px), 27.27% calc(100% - 17.9px), 29.55% calc(100% - 21.3px), 31.82% calc(100% - 29.6px), 34.09% calc(100% - 31.8px), 36.36% calc(100% - 34.5px), 38.64% calc(100% - 18.4px), 40.91% calc(100% - 28.7px), 43.18% calc(100% - 24.3px), 45.45% calc(100% - 33.9px), 47.73% calc(100% - 26.6px), 50% calc(100% - 30.4px), 52.27% calc(100% - 27.0px), 54.55% calc(100% - 31.7px), 56.82% calc(100% - 23.9px), 59.09% calc(100% - 24.6px), 61.36% calc(100% - 34.3px), 63.64% calc(100% - 22.4px), 65.91% calc(100% - 19.7px), 68.18% calc(100% - 9.1px), 70.45% calc(100% - 12.8px), 72.73% calc(100% - 23.4px), 75% calc(100% - 18.5px), 77.27% calc(100% - 24.3px), 79.55% calc(100% - 27.2px), 81.82% calc(100% - 29.5px), 84.09% calc(100% - 34.9px), 86.36% calc(100% - 37.7px), 88.64% calc(100% - 32.3px), 90.91% calc(100% - 25.9px), 93.18% calc(100% - 21.2px), 95.45% calc(100% - 13.9px), 97.73% calc(100% - 13.8px), 100% calc(100% - 9.5px), 100% 100%, 0 100%);

/* skills */
  --tear-skills: polygon(0% 7.8px, 2.27% 8.7px, 4.55% 7px, 6.82% 16.9px, 9.09% 22.8px, 11.36% 27.7px, 13.64% 31.5px, 15.91% 29px, 18.18% 29.5px, 20.45% 17.3px, 22.73% 16.8px, 25% 16.4px, 27.27% 17.9px, 29.55% 23.7px, 31.82% 17.8px, 34.09% 9.7px, 36.36% 1.1px, 38.64% 3.3px, 40.91% 9.2px, 43.18% 16.4px, 45.45% 16.6px, 47.73% 9.4px, 50% 1.2px, 52.27% 13.9px, 54.55% 8.8px, 56.82% 18px, 59.09% 12.7px, 61.36% 10.3px, 63.64% 2.3px, 65.91% 15.3px, 68.18% 13px, 70.45% 16.3px, 72.73% 10.7px, 75% 16px, 77.27% 23.2px, 79.55% 26.3px, 81.82% 15.3px, 84.09% 6.8px, 86.36% 11.7px, 88.64% 7.5px, 90.91% 18px, 93.18% 15.4px, 95.45% 23.8px, 97.73% 21px, 100% 15.2px, 100% calc(100% - 11.6px), 97.73% calc(100% - 21.3px), 95.45% calc(100% - 12.2px), 93.18% calc(100% - 20.3px), 90.91% calc(100% - 11.7px), 88.64% calc(100% - 11px), 86.36% calc(100% - 1.3px), 84.09% calc(100% - 1.4px), 81.82% calc(100% - 2.3px), 79.55% calc(100% - 1.7px), 77.27% calc(100% - 5.1px), 75% calc(100% - 20.1px), 72.73% calc(100% - 26.2px), 70.45% calc(100% - 32.9px), 68.18% calc(100% - 27.2px), 65.91% calc(100% - 21px), 63.64% calc(100% - 15.4px), 61.36% calc(100% - 5.2px), 59.09% calc(100% - 22px), 56.82% calc(100% - 16.6px), 54.55% calc(100% - 12.5px), 52.27% calc(100% - 11.7px), 50% calc(100% - 14.4px), 47.73% calc(100% - 18.7px), 45.45% calc(100% - 12.5px), 43.18% calc(100% - 20.5px), 40.91% calc(100% - 17.1px), 38.64% calc(100% - 21.3px), 36.36% calc(100% - 16.2px), 34.09% calc(100% - 17.2px), 31.82% calc(100% - 11.1px), 29.55% calc(100% - 7.8px), 27.27% calc(100% - 11.2px), 25% calc(100% - 15.6px), 22.73% calc(100% - 23.9px), 20.45% calc(100% - 23.4px), 18.18% calc(100% - 23.2px), 15.91% calc(100% - 2.1px), 13.64% calc(100% - 6.1px), 11.36% calc(100% - 15.3px), 9.09% calc(100% - 25.2px), 6.82% calc(100% - 20.2px), 4.55% calc(100% - 15.9px), 2.27% calc(100% - 7.1px), 0% calc(100% - 9.7px));
  --fiber-skills-top: polygon(0 0, 100% 0, 100% 18.0px, 97.73% 23.8px, 95.45% 27.3px, 93.18% 21.1px, 90.91% 24.3px, 88.64% 11.0px, 86.36% 17.1px, 84.09% 12.0px, 81.82% 18.6px, 79.55% 29.7px, 77.27% 25.6px, 75% 21.8px, 72.73% 17.4px, 70.45% 22.3px, 68.18% 17.2px, 65.91% 21.4px, 63.64% 6.5px, 61.36% 14.4px, 59.09% 17.2px, 56.82% 23.6px, 54.55% 11.9px, 52.27% 20.5px, 50% 5.7px, 47.73% 14.6px, 45.45% 19.3px, 43.18% 23.1px, 40.91% 13.8px, 38.64% 6.1px, 36.36% 4.3px, 34.09% 16.3px, 31.82% 23.0px, 29.55% 26.0px, 27.27% 24.0px, 25% 21.4px, 22.73% 22.3px, 20.45% 22.0px, 18.18% 31.8px, 15.91% 33.5px, 13.64% 36.0px, 11.36% 30.2px, 9.09% 24.9px, 6.82% 19.6px, 4.55% 12.3px, 2.27% 12.0px, 0% 11.2px);
  --fiber-skills-bot: polygon(0% calc(100% - 13.1px), 2.27% calc(100% - 10.8px), 4.55% calc(100% - 19.7px), 6.82% calc(100% - 23.1px), 9.09% calc(100% - 30.7px), 11.36% calc(100% - 21.5px), 13.64% calc(100% - 12.8px), 15.91% calc(100% - 6.2px), 18.18% calc(100% - 28.1px), 20.45% calc(100% - 28.4px), 22.73% calc(100% - 26.7px), 25% calc(100% - 19.0px), 27.27% calc(100% - 17.2px), 29.55% calc(100% - 9.8px), 31.82% calc(100% - 17.4px), 34.09% calc(100% - 21.1px), 36.36% calc(100% - 20.2px), 38.64% calc(100% - 24.1px), 40.91% calc(100% - 23.2px), 43.18% calc(100% - 26.7px), 45.45% calc(100% - 18.7px), 47.73% calc(100% - 21.7px), 50% calc(100% - 17.2px), 52.27% calc(100% - 18.7px), 54.55% calc(100% - 16.2px), 56.82% calc(100% - 19.4px), 59.09% calc(100% - 24.5px), 61.36% calc(100% - 8.2px), 63.64% calc(100% - 18.7px), 65.91% calc(100% - 25.2px), 68.18% calc(100% - 29.4px), 70.45% calc(100% - 39.6px), 72.73% calc(100% - 32.7px), 75% calc(100% - 23.9px), 77.27% calc(100% - 9.5px), 79.55% calc(100% - 4.7px), 81.82% calc(100% - 4.9px), 84.09% calc(100% - 5.7px), 86.36% calc(100% - 6.2px), 88.64% calc(100% - 15.2px), 90.91% calc(100% - 16.7px), 93.18% calc(100% - 25.4px), 95.45% calc(100% - 18.5px), 97.73% calc(100% - 26.9px), 100% calc(100% - 17.4px), 100% 100%, 0 100%);

/* certs */
  --tear-certs: polygon(0% 17.8px, 2.27% 8.3px, 4.55% 6.1px, 6.82% 5.7px, 9.09% 17.7px, 11.36% 32.7px, 13.64% 28.3px, 15.91% 20.5px, 18.18% 29.7px, 20.45% 26.3px, 22.73% 32.3px, 25% 26.7px, 27.27% 19.9px, 29.55% 26px, 31.82% 17.4px, 34.09% 11.8px, 36.36% 16.1px, 38.64% 8.3px, 40.91% 13.5px, 43.18% 5.5px, 45.45% 1.4px, 47.73% 3.4px, 50% 18px, 52.27% 17.1px, 54.55% 19.4px, 56.82% 15.6px, 59.09% 16.1px, 61.36% 10.6px, 63.64% 4.5px, 65.91% 7.9px, 68.18% 16.3px, 70.45% 10.4px, 72.73% 15.8px, 75% 10.8px, 77.27% 16px, 79.55% 13.6px, 81.82% 7px, 84.09% 9.2px, 86.36% 7.1px, 88.64% 1.4px, 90.91% 10.2px, 93.18% 21.4px, 95.45% 12.6px, 97.73% 16.2px, 100% 26.2px, 100% calc(100% - 15.3px), 97.73% calc(100% - 3.4px), 95.45% calc(100% - 3.2px), 93.18% calc(100% - 4.3px), 90.91% calc(100% - 5.8px), 88.64% calc(100% - 9.8px), 86.36% calc(100% - 16.3px), 84.09% calc(100% - 9.7px), 81.82% calc(100% - 6.4px), 79.55% calc(100% - 12.6px), 77.27% calc(100% - 11.3px), 75% calc(100% - 21.8px), 72.73% calc(100% - 20.8px), 70.45% calc(100% - 25.6px), 68.18% calc(100% - 25.4px), 65.91% calc(100% - 23.4px), 63.64% calc(100% - 15.9px), 61.36% calc(100% - 13.1px), 59.09% calc(100% - 21.7px), 56.82% calc(100% - 21.8px), 54.55% calc(100% - 16px), 52.27% calc(100% - 16.7px), 50% calc(100% - 12.1px), 47.73% calc(100% - 1.6px), 45.45% calc(100% - 5.8px), 43.18% calc(100% - 12.1px), 40.91% calc(100% - 7.5px), 38.64% calc(100% - 3.4px), 36.36% calc(100% - 6px), 34.09% calc(100% - 3.5px), 31.82% calc(100% - 17.3px), 29.55% calc(100% - 25.3px), 27.27% calc(100% - 13.9px), 25% calc(100% - 21.9px), 22.73% calc(100% - 25.8px), 20.45% calc(100% - 27.2px), 18.18% calc(100% - 28.8px), 15.91% calc(100% - 24.8px), 13.64% calc(100% - 24.1px), 11.36% calc(100% - 31px), 9.09% calc(100% - 25.2px), 6.82% calc(100% - 32.3px), 4.55% calc(100% - 30.9px), 2.27% calc(100% - 17px), 0% calc(100% - 7.8px));
  --fiber-certs-top: polygon(0 0, 100% 0, 100% 29.2px, 97.73% 21.8px, 95.45% 19.1px, 93.18% 26.5px, 90.91% 13.1px, 88.64% 7.2px, 86.36% 12.5px, 84.09% 15.5px, 81.82% 13.1px, 79.55% 18.1px, 77.27% 18.2px, 75% 16.9px, 72.73% 20.6px, 70.45% 14.1px, 68.18% 19.9px, 65.91% 9.9px, 63.64% 9.2px, 61.36% 13.1px, 59.09% 20.6px, 56.82% 18.3px, 54.55% 23.4px, 52.27% 22.3px, 50% 21.0px, 47.73% 6.8px, 45.45% 3.7px, 43.18% 11.6px, 40.91% 18.0px, 38.64% 11.8px, 36.36% 22.0px, 34.09% 16.7px, 31.82% 24.0px, 29.55% 30.9px, 27.27% 24.8px, 25% 30.9px, 22.73% 35.5px, 20.45% 28.8px, 18.18% 32.4px, 15.91% 25.8px, 13.64% 34.3px, 11.36% 37.6px, 9.09% 23.2px, 6.82% 10.3px, 4.55% 10.7px, 2.27% 11.1px, 0% 21.2px);
  --fiber-certs-bot: polygon(0% calc(100% - 11.2px), 2.27% calc(100% - 20.3px), 4.55% calc(100% - 33.9px), 6.82% calc(100% - 37.1px), 9.09% calc(100% - 29.0px), 11.36% calc(100% - 34.6px), 13.64% calc(100% - 27.3px), 15.91% calc(100% - 29.7px), 18.18% calc(100% - 34.1px), 20.45% calc(100% - 30.0px), 22.73% calc(100% - 31.2px), 25% calc(100% - 24.6px), 27.27% calc(100% - 18.7px), 29.55% calc(100% - 29.9px), 31.82% calc(100% - 20.0px), 34.09% calc(100% - 5.7px), 36.36% calc(100% - 12.6px), 38.64% calc(100% - 7.0px), 40.91% calc(100% - 13.5px), 43.18% calc(100% - 17.6px), 45.45% calc(100% - 11.5px), 47.73% calc(100% - 7.8px), 50% calc(100% - 18.5px), 52.27% calc(100% - 22.3px), 54.55% calc(100% - 20.5px), 56.82% calc(100% - 26.7px), 59.09% calc(100% - 24.2px), 61.36% calc(100% - 19.4px), 63.64% calc(100% - 19.7px), 65.91% calc(100% - 28.5px), 68.18% calc(100% - 31.9px), 70.45% calc(100% - 30.1px), 72.73% calc(100% - 25.4px), 75% calc(100% - 25.9px), 77.27% calc(100% - 15.5px), 79.55% calc(100% - 16.7px), 81.82% calc(100% - 11.8px), 84.09% calc(100% - 15.1px), 86.36% calc(100% - 21.2px), 88.64% calc(100% - 16.3px), 90.91% calc(100% - 12.4px), 93.18% calc(100% - 8.9px), 95.45% calc(100% - 7.5px), 97.73% calc(100% - 6.8px), 100% calc(100% - 21.3px), 100% 100%, 0 100%);

/* projects */
  --tear-projects: polygon(0% 24.5px, 2.27% 16.4px, 4.55% 1.1px, 6.82% 1.8px, 9.09% 3.1px, 11.36% 9.5px, 13.64% 8.5px, 15.91% 20.3px, 18.18% 30.4px, 20.45% 29.9px, 22.73% 20.7px, 25% 17.4px, 27.27% 8.4px, 29.55% 9.1px, 31.82% 8.5px, 34.09% 5px, 36.36% 6.8px, 38.64% 16.7px, 40.91% 18.2px, 43.18% 19.2px, 45.45% 21.5px, 47.73% 12.9px, 50% 21px, 52.27% 7.3px, 54.55% 19px, 56.82% 19.4px, 59.09% 28.9px, 61.36% 27.6px, 63.64% 29.2px, 65.91% 18.9px, 68.18% 24.2px, 70.45% 14px, 72.73% 23.6px, 75% 21px, 77.27% 25.5px, 79.55% 2.6px, 81.82% 2.8px, 84.09% 22.3px, 86.36% 22.6px, 88.64% 11.4px, 90.91% 9.9px, 93.18% 2.2px, 95.45% 12.6px, 97.73% 10.7px, 100% 12.5px, 100% calc(100% - 32.7px), 97.73% calc(100% - 25.4px), 95.45% calc(100% - 24.1px), 93.18% calc(100% - 19.6px), 90.91% calc(100% - 7.6px), 88.64% calc(100% - 3.9px), 86.36% calc(100% - 20.6px), 84.09% calc(100% - 16.5px), 81.82% calc(100% - 6.4px), 79.55% calc(100% - 10.5px), 77.27% calc(100% - 9px), 75% calc(100% - 4px), 72.73% calc(100% - 10.3px), 70.45% calc(100% - 18.4px), 68.18% calc(100% - 13.3px), 65.91% calc(100% - 4.3px), 63.64% calc(100% - 11px), 61.36% calc(100% - 19.1px), 59.09% calc(100% - 23.9px), 56.82% calc(100% - 16.1px), 54.55% calc(100% - 4px), 52.27% calc(100% - 4.6px), 50% calc(100% - 17.8px), 47.73% calc(100% - 20.9px), 45.45% calc(100% - 19.5px), 43.18% calc(100% - 8.7px), 40.91% calc(100% - 22.5px), 38.64% calc(100% - 18.3px), 36.36% calc(100% - 13.6px), 34.09% calc(100% - 15.7px), 31.82% calc(100% - 6.3px), 29.55% calc(100% - 2.3px), 27.27% calc(100% - 22px), 25% calc(100% - 32.4px), 22.73% calc(100% - 26.7px), 20.45% calc(100% - 28.1px), 18.18% calc(100% - 30px), 15.91% calc(100% - 30.3px), 13.64% calc(100% - 23.2px), 11.36% calc(100% - 15.4px), 9.09% calc(100% - 15px), 6.82% calc(100% - 1.5px), 4.55% calc(100% - 5.8px), 2.27% calc(100% - 2.7px), 0% calc(100% - 6px));
  --fiber-projects-top: polygon(0 0, 100% 0, 100% 15.7px, 97.73% 14.2px, 95.45% 17.1px, 93.18% 6.8px, 90.91% 14.4px, 88.64% 14.5px, 86.36% 27.9px, 84.09% 24.7px, 81.82% 6.7px, 79.55% 8.3px, 77.27% 27.6px, 75% 27.4px, 72.73% 26.6px, 70.45% 20.5px, 68.18% 27.2px, 65.91% 21.8px, 63.64% 34.4px, 61.36% 33.4px, 59.09% 33.3px, 56.82% 24.2px, 54.55% 23.8px, 52.27% 11.1px, 50% 27.5px, 47.73% 19.4px, 45.45% 28.3px, 43.18% 24.6px, 40.91% 22.6px, 38.64% 21.0px, 36.36% 10.3px, 34.09% 8.2px, 31.82% 11.5px, 29.55% 11.6px, 27.27% 12.2px, 25% 20.9px, 22.73% 26.6px, 20.45% 35.3px, 18.18% 33.6px, 15.91% 26.4px, 13.64% 11.1px, 11.36% 11.7px, 9.09% 6.9px, 6.82% 8.2px, 4.55% 4.9px, 2.27% 18.7px, 0% 28.0px);
  --fiber-projects-bot: polygon(0% calc(100% - 9.5px), 2.27% calc(100% - 5.5px), 4.55% calc(100% - 8.0px), 6.82% calc(100% - 8.1px), 9.09% calc(100% - 17.1px), 11.36% calc(100% - 21.4px), 13.64% calc(100% - 28.0px), 15.91% calc(100% - 36.0px), 18.18% calc(100% - 35.7px), 20.45% calc(100% - 33.8px), 22.73% calc(100% - 29.8px), 25% calc(100% - 39.3px), 27.27% calc(100% - 25.6px), 29.55% calc(100% - 4.5px), 31.82% calc(100% - 10.4px), 34.09% calc(100% - 21.2px), 36.36% calc(100% - 17.9px), 38.64% calc(100% - 22.7px), 40.91% calc(100% - 28.4px), 43.18% calc(100% - 13.5px), 45.45% calc(100% - 24.8px), 47.73% calc(100% - 25.2px), 50% calc(100% - 22.7px), 52.27% calc(100% - 8.7px), 54.55% calc(100% - 9.4px), 56.82% calc(100% - 18.1px), 59.09% calc(100% - 26.3px), 61.36% calc(100% - 23.8px), 63.64% calc(100% - 15.3px), 65.91% calc(100% - 10.3px), 68.18% calc(100% - 19.2px), 70.45% calc(100% - 20.6px), 72.73% calc(100% - 13.1px), 75% calc(100% - 8.4px), 77.27% calc(100% - 13.1px), 79.55% calc(100% - 15.8px), 81.82% calc(100% - 9.6px), 84.09% calc(100% - 23.0px), 86.36% calc(100% - 25.4px), 88.64% calc(100% - 7.8px), 90.91% calc(100% - 10.8px), 93.18% calc(100% - 23.7px), 95.45% calc(100% - 26.4px), 97.73% calc(100% - 31.6px), 100% calc(100% - 38.9px), 100% 100%, 0 100%);

/* course */
  --tear-course: polygon(0% 22.7px, 2.27% 22.6px, 4.55% 26.5px, 6.82% 21.7px, 9.09% 20.9px, 11.36% 23.3px, 13.64% 20.5px, 15.91% 15.3px, 18.18% 7.3px, 20.45% 17px, 22.73% 11.8px, 25% 21.2px, 27.27% 26.3px, 29.55% 29px, 31.82% 28.8px, 34.09% 31px, 36.36% 23.9px, 38.64% 26.2px, 40.91% 15.9px, 43.18% 6.7px, 45.45% 5.1px, 47.73% 9.6px, 50% 6.6px, 52.27% 10.4px, 54.55% 1.9px, 56.82% 1.4px, 59.09% 6.5px, 61.36% 4.2px, 63.64% 3.1px, 65.91% 3.5px, 68.18% 5.3px, 70.45% 4.7px, 72.73% 10.9px, 75% 4px, 77.27% 1.4px, 79.55% 10.8px, 81.82% 20.4px, 84.09% 12.5px, 86.36% 14px, 88.64% 14.7px, 90.91% 17.7px, 93.18% 7.2px, 95.45% 6.2px, 97.73% 5.3px, 100% 11.5px, 100% calc(100% - 22.5px), 97.73% calc(100% - 12.4px), 95.45% calc(100% - 13.3px), 93.18% calc(100% - 8px), 90.91% calc(100% - 10.9px), 88.64% calc(100% - 22.2px), 86.36% calc(100% - 31.9px), 84.09% calc(100% - 29.6px), 81.82% calc(100% - 22.6px), 79.55% calc(100% - 22.4px), 77.27% calc(100% - 21.7px), 75% calc(100% - 22.9px), 72.73% calc(100% - 29.7px), 70.45% calc(100% - 32.9px), 68.18% calc(100% - 27.2px), 65.91% calc(100% - 25.9px), 63.64% calc(100% - 12.4px), 61.36% calc(100% - 22.2px), 59.09% calc(100% - 27.6px), 56.82% calc(100% - 18.6px), 54.55% calc(100% - 31.2px), 52.27% calc(100% - 26.3px), 50% calc(100% - 16px), 47.73% calc(100% - 26.6px), 45.45% calc(100% - 16.7px), 43.18% calc(100% - 29.3px), 40.91% calc(100% - 21.7px), 38.64% calc(100% - 18px), 36.36% calc(100% - 23.1px), 34.09% calc(100% - 19.7px), 31.82% calc(100% - 31.8px), 29.55% calc(100% - 25.2px), 27.27% calc(100% - 18.9px), 25% calc(100% - 17.7px), 22.73% calc(100% - 27.1px), 20.45% calc(100% - 29.9px), 18.18% calc(100% - 25.5px), 15.91% calc(100% - 21.2px), 13.64% calc(100% - 28.5px), 11.36% calc(100% - 21.8px), 9.09% calc(100% - 25.5px), 6.82% calc(100% - 30.7px), 4.55% calc(100% - 28.4px), 2.27% calc(100% - 24.9px), 0% calc(100% - 24.5px));
  --fiber-course-top: polygon(0 0, 100% 0, 100% 14.9px, 97.73% 11.6px, 95.45% 8.7px, 93.18% 11.3px, 90.91% 23.9px, 88.64% 20.1px, 86.36% 19.2px, 84.09% 16.0px, 81.82% 27.2px, 79.55% 17.6px, 77.27% 8.4px, 75% 10.7px, 72.73% 17.1px, 70.45% 8.9px, 68.18% 7.7px, 65.91% 7.2px, 63.64% 8.8px, 61.36% 8.4px, 59.09% 10.9px, 56.82% 8.3px, 54.55% 7.6px, 52.27% 12.8px, 50% 11.6px, 47.73% 14.2px, 45.45% 11.5px, 43.18% 11.4px, 40.91% 20.3px, 38.64% 31.2px, 36.36% 30.1px, 34.09% 37.5px, 31.82% 33.2px, 29.55% 34.0px, 27.27% 28.9px, 25% 23.9px, 22.73% 15.3px, 20.45% 20.3px, 18.18% 10.9px, 15.91% 22.2px, 13.64% 24.7px, 11.36% 27.9px, 9.09% 23.0px, 6.82% 25.0px, 4.55% 29.6px, 2.27% 29.5px, 0% 26.2px);
  --fiber-course-bot: polygon(0% calc(100% - 28.0px), 2.27% calc(100% - 27.2px), 4.55% calc(100% - 34.9px), 6.82% calc(100% - 34.2px), 9.09% calc(100% - 30.9px), 11.36% calc(100% - 25.2px), 13.64% calc(100% - 34.8px), 15.91% calc(100% - 27.7px), 18.18% calc(100% - 31.7px), 20.45% calc(100% - 33.5px), 22.73% calc(100% - 32.9px), 25% calc(100% - 23.8px), 27.27% calc(100% - 21.4px), 29.55% calc(100% - 30.0px), 31.82% calc(100% - 37.4px), 34.09% calc(100% - 23.5px), 36.36% calc(100% - 30.0px), 38.64% calc(100% - 23.1px), 40.91% calc(100% - 27.5px), 43.18% calc(100% - 33.5px), 45.45% calc(100% - 21.6px), 47.73% calc(100% - 29.1px), 50% calc(100% - 19.4px), 52.27% calc(100% - 29.0px), 54.55% calc(100% - 37.5px), 56.82% calc(100% - 22.7px), 59.09% calc(100% - 30.0px), 61.36% calc(100% - 25.2px), 63.64% calc(100% - 17.2px), 65.91% calc(100% - 32.7px), 68.18% calc(100% - 32.5px), 70.45% calc(100% - 37.9px), 72.73% calc(100% - 35.7px), 75% calc(100% - 27.6px), 77.27% calc(100% - 25.7px), 79.55% calc(100% - 28.8px), 81.82% calc(100% - 28.6px), 84.09% calc(100% - 32.2px), 86.36% calc(100% - 36.6px), 88.64% calc(100% - 28.4px), 90.91% calc(100% - 15.7px), 93.18% calc(100% - 11.5px), 95.45% calc(100% - 18.6px), 97.73% calc(100% - 16.5px), 100% calc(100% - 28.9px), 100% 100%, 0 100%);

/* contact */
  --tear-contact: polygon(0% 20.8px, 2.27% 18.6px, 4.55% 21.1px, 6.82% 20px, 9.09% 19.6px, 11.36% 28.1px, 13.64% 25px, 15.91% 25.2px, 18.18% 27.1px, 20.45% 20.5px, 22.73% 13.1px, 25% 14.8px, 27.27% 22.5px, 29.55% 14.9px, 31.82% 7.9px, 34.09% 4.4px, 36.36% 14.5px, 38.64% 20.8px, 40.91% 28.3px, 43.18% 30.5px, 45.45% 29.1px, 47.73% 30.3px, 50% 19.6px, 52.27% 21.2px, 54.55% 12.7px, 56.82% 17.6px, 59.09% 26px, 61.36% 31px, 63.64% 13.2px, 65.91% 9.3px, 68.18% 13.3px, 70.45% 28.6px, 72.73% 21.9px, 75% 28.7px, 77.27% 25.1px, 79.55% 22.5px, 81.82% 19.8px, 84.09% 26.3px, 86.36% 32.7px, 88.64% 30px, 90.91% 32.1px, 93.18% 30.5px, 95.45% 20.6px, 97.73% 26.1px, 100% 32.8px, 100% calc(100% - 11px), 97.73% calc(100% - 19.6px), 95.45% calc(100% - 18.4px), 93.18% calc(100% - 27.7px), 90.91% calc(100% - 20.4px), 88.64% calc(100% - 11px), 86.36% calc(100% - 1.9px), 84.09% calc(100% - 1.9px), 81.82% calc(100% - 3.3px), 79.55% calc(100% - 1px), 77.27% calc(100% - 7.8px), 75% calc(100% - 10.1px), 72.73% calc(100% - 20.7px), 70.45% calc(100% - 20.4px), 68.18% calc(100% - 13.3px), 65.91% calc(100% - 6.4px), 63.64% calc(100% - 2.1px), 61.36% calc(100% - 3.1px), 59.09% calc(100% - 13.9px), 56.82% calc(100% - 13.4px), 54.55% calc(100% - 12.8px), 52.27% calc(100% - 1.1px), 50% calc(100% - 8.9px), 47.73% calc(100% - 8.9px), 45.45% calc(100% - 8.4px), 43.18% calc(100% - 16.2px), 40.91% calc(100% - 20.6px), 38.64% calc(100% - 28.3px), 36.36% calc(100% - 27.6px), 34.09% calc(100% - 16.3px), 31.82% calc(100% - 12.7px), 29.55% calc(100% - 22px), 27.27% calc(100% - 16.3px), 25% calc(100% - 11.8px), 22.73% calc(100% - 22px), 20.45% calc(100% - 18.3px), 18.18% calc(100% - 20.3px), 15.91% calc(100% - 18px), 13.64% calc(100% - 24.7px), 11.36% calc(100% - 29.2px), 9.09% calc(100% - 22.2px), 6.82% calc(100% - 31.6px), 4.55% calc(100% - 30.6px), 2.27% calc(100% - 30.9px), 0% calc(100% - 22.7px));
  --fiber-contact-top: polygon(0 0, 100% 0, 100% 36.4px, 97.73% 30.2px, 95.45% 26.0px, 93.18% 34.1px, 90.91% 34.9px, 88.64% 32.8px, 86.36% 37.8px, 84.09% 30.9px, 81.82% 24.4px, 79.55% 25.4px, 77.27% 31.9px, 75% 30.7px, 72.73% 26.2px, 70.45% 35.6px, 68.18% 20.0px, 65.91% 13.9px, 63.64% 19.4px, 61.36% 33.6px, 59.09% 30.4px, 56.82% 21.6px, 54.55% 19.3px, 52.27% 27.2px, 50% 23.1px, 47.73% 33.1px, 45.45% 35.0px, 43.18% 34.6px, 40.91% 32.6px, 38.64% 26.6px, 36.36% 18.3px, 34.09% 9.2px, 31.82% 13.7px, 29.55% 17.5px, 27.27% 28.9px, 25% 21.8px, 22.73% 19.3px, 20.45% 26.7px, 18.18% 31.2px, 15.91% 27.9px, 13.64% 30.7px, 11.36% 30.1px, 9.09% 25.0px, 6.82% 25.1px, 4.55% 23.4px, 2.27% 25.0px, 0% 24.3px);
  --fiber-contact-bot: polygon(0% calc(100% - 26.2px), 2.27% calc(100% - 37.8px), 4.55% calc(100% - 36.3px), 6.82% calc(100% - 36.9px), 9.09% calc(100% - 26.0px), 11.36% calc(100% - 35.0px), 13.64% calc(100% - 27.6px), 15.91% calc(100% - 20.3px), 18.18% calc(100% - 26.9px), 20.45% calc(100% - 24.7px), 22.73% calc(100% - 25.5px), 25% calc(100% - 17.2px), 27.27% calc(100% - 22.6px), 29.55% calc(100% - 24.4px), 31.82% calc(100% - 19.7px), 34.09% calc(100% - 18.4px), 36.36% calc(100% - 32.2px), 38.64% calc(100% - 34.2px), 40.91% calc(100% - 26.4px), 43.18% calc(100% - 19.7px), 45.45% calc(100% - 12.8px), 47.73% calc(100% - 14.5px), 50% calc(100% - 15.8px), 52.27% calc(100% - 7.4px), 54.55% calc(100% - 15.0px), 56.82% calc(100% - 19.6px), 59.09% calc(100% - 16.3px), 61.36% calc(100% - 9.5px), 63.64% calc(100% - 7.3px), 65.91% calc(100% - 9.1px), 68.18% calc(100% - 18.0px), 70.45% calc(100% - 23.2px), 72.73% calc(100% - 24.8px), 75% calc(100% - 15.2px), 77.27% calc(100% - 11.6px), 79.55% calc(100% - 3.6px), 81.82% calc(100% - 7.2px), 84.09% calc(100% - 5.6px), 86.36% calc(100% - 6.5px), 88.64% calc(100% - 14.5px), 90.91% calc(100% - 26.9px), 93.18% calc(100% - 30.7px), 95.45% calc(100% - 21.6px), 97.73% calc(100% - 26.5px), 100% calc(100% - 17.6px), 100% 100%, 0 100%);

/* footer */
  --tear-footer: polygon(0% 19px, 2.27% 14.4px, 4.55% 19.2px, 6.82% 22.5px, 9.09% 19.3px, 11.36% 28.4px, 13.64% 20.2px, 15.91% 21.1px, 18.18% 21.9px, 20.45% 22.7px, 22.73% 31.4px, 25% 25.1px, 27.27% 25px, 29.55% 15.3px, 31.82% 21.1px, 34.09% 11.4px, 36.36% 13.4px, 38.64% 23.7px, 40.91% 17.1px, 43.18% 13.7px, 45.45% 6.3px, 47.73% 1.3px, 50% 8px, 52.27% 4px, 54.55% 16px, 56.82% 22.4px, 59.09% 22.5px, 61.36% 17.7px, 63.64% 9.7px, 65.91% 25.9px, 68.18% 25.1px, 70.45% 27.1px, 72.73% 21.5px, 75% 31.6px, 77.27% 18.4px, 79.55% 22.3px, 81.82% 13.1px, 84.09% 5.4px, 86.36% 6.9px, 88.64% 17.4px, 90.91% 1.3px, 93.18% 9.3px, 95.45% 6.3px, 97.73% 15px, 100% 17.5px, 100% calc(100% - 17.5px), 97.73% calc(100% - 20.3px), 95.45% calc(100% - 28.6px), 93.18% calc(100% - 32.5px), 90.91% calc(100% - 28.8px), 88.64% calc(100% - 22.9px), 86.36% calc(100% - 13px), 84.09% calc(100% - 22.5px), 81.82% calc(100% - 24.8px), 79.55% calc(100% - 20.6px), 77.27% calc(100% - 22.5px), 75% calc(100% - 25.5px), 72.73% calc(100% - 26.3px), 70.45% calc(100% - 24.1px), 68.18% calc(100% - 29.7px), 65.91% calc(100% - 22.5px), 63.64% calc(100% - 21.2px), 61.36% calc(100% - 13.5px), 59.09% calc(100% - 16.9px), 56.82% calc(100% - 5.5px), 54.55% calc(100% - 2.2px), 52.27% calc(100% - 2.6px), 50% calc(100% - 5.8px), 47.73% calc(100% - 15.4px), 45.45% calc(100% - 5.8px), 43.18% calc(100% - 13.1px), 40.91% calc(100% - 1.8px), 38.64% calc(100% - 7px), 36.36% calc(100% - 14.2px), 34.09% calc(100% - 17.2px), 31.82% calc(100% - 28.7px), 29.55% calc(100% - 21.1px), 27.27% calc(100% - 18.3px), 25% calc(100% - 11.8px), 22.73% calc(100% - 5px), 20.45% calc(100% - 4px), 18.18% calc(100% - 11.7px), 15.91% calc(100% - 10.4px), 13.64% calc(100% - 11px), 11.36% calc(100% - 20.4px), 9.09% calc(100% - 23.6px), 6.82% calc(100% - 4.8px), 4.55% calc(100% - 11px), 2.27% calc(100% - 12.4px), 0% calc(100% - 14.6px));
  --fiber-footer-top: polygon(0 0, 100% 0, 100% 21.3px, 97.73% 21.9px, 95.45% 9.7px, 93.18% 12.4px, 90.91% 5.7px, 88.64% 22.5px, 86.36% 12.0px, 84.09% 11.1px, 81.82% 15.5px, 79.55% 26.4px, 77.27% 25.1px, 75% 33.9px, 72.73% 24.0px, 70.45% 31.8px, 68.18% 31.2px, 65.91% 31.4px, 63.64% 16.4px, 61.36% 23.6px, 59.09% 26.8px, 56.82% 28.5px, 54.55% 18.4px, 52.27% 8.5px, 50% 10.1px, 47.73% 7.2px, 45.45% 11.8px, 43.18% 17.1px, 40.91% 21.3px, 38.64% 30.3px, 36.36% 19.9px, 34.09% 14.5px, 31.82% 23.4px, 29.55% 20.5px, 27.27% 30.3px, 25% 31.3px, 22.73% 35.3px, 20.45% 26.7px, 18.18% 26.4px, 15.91% 24.6px, 13.64% 22.5px, 11.36% 32.8px, 9.09% 23.0px, 6.82% 29.5px, 4.55% 25.7px, 2.27% 20.3px, 0% 22.5px);
  --fiber-footer-bot: polygon(0% calc(100% - 18.1px), 2.27% calc(100% - 18.8px), 4.55% calc(100% - 15.9px), 6.82% calc(100% - 7.0px), 9.09% calc(100% - 25.7px), 11.36% calc(100% - 23.5px), 13.64% calc(100% - 15.4px), 15.91% calc(100% - 13.5px), 18.18% calc(100% - 13.7px), 20.45% calc(100% - 8.3px), 22.73% calc(100% - 11.2px), 25% calc(100% - 16.4px), 27.27% calc(100% - 23.5px), 29.55% calc(100% - 26.1px), 31.82% calc(100% - 32.1px), 34.09% calc(100% - 22.6px), 36.36% calc(100% - 16.5px), 38.64% calc(100% - 13.6px), 40.91% calc(100% - 7.5px), 43.18% calc(100% - 16.0px), 45.45% calc(100% - 9.8px), 47.73% calc(100% - 19.1px), 50% calc(100% - 11.2px), 52.27% calc(100% - 7.5px), 54.55% calc(100% - 5.2px), 56.82% calc(100% - 8.8px), 59.09% calc(100% - 19.2px), 61.36% calc(100% - 18.2px), 63.64% calc(100% - 26.9px), 65.91% calc(100% - 26.1px), 68.18% calc(100% - 33.8px), 70.45% calc(100% - 29.6px), 72.73% calc(100% - 28.6px), 75% calc(100% - 30.9px), 77.27% calc(100% - 26.2px), 79.55% calc(100% - 24.3px), 81.82% calc(100% - 31.5px), 84.09% calc(100% - 27.3px), 86.36% calc(100% - 17.5px), 88.64% calc(100% - 28.7px), 90.91% calc(100% - 31.9px), 93.18% calc(100% - 35.0px), 95.45% calc(100% - 34.8px), 97.73% calc(100% - 25.0px), 100% calc(100% - 24.3px), 100% 100%, 0 100%);

  --tear-card-1: polygon(0% 6.3px, 3.85% 3.1px, 7.69% 7.4px, 11.54% 9.8px, 15.38% 6px, 19.23% 5.3px, 23.08% 3.1px, 26.92% 1.5px, 30.77% 1.6px, 34.62% 6px, 38.46% 10.1px, 42.31% 9.6px, 46.15% 11.5px, 50% 8.1px, 53.85% 5.3px, 57.69% 3.7px, 61.54% 1.1px, 65.38% 1.5px, 69.23% 3.4px, 73.08% 10.4px, 76.92% 9.4px, 80.77% 12.2px, 84.62% 8.2px, 88.46% 10.4px, 92.31% 5.8px, 96.15% 5.1px, 100% 4.8px, 100% 100%, 0 100%);
  --tear-card-2: polygon(0% 1.6px, 3.85% 3.3px, 7.69% 3.4px, 11.54% 2.3px, 15.38% 9.7px, 19.23% 8.7px, 23.08% 9.5px, 26.92% 10.6px, 30.77% 6.7px, 34.62% 9.2px, 38.46% 10.6px, 42.31% 6px, 46.15% 2.4px, 50% 2.5px, 53.85% 7.3px, 57.69% 7.4px, 61.54% 4.9px, 65.38% 2.5px, 69.23% 5.9px, 73.08% 11.5px, 76.92% 8.7px, 80.77% 10.5px, 84.62% 10.1px, 88.46% 6.7px, 92.31% 5.8px, 96.15% 8.2px, 100% 10.9px, 100% 100%, 0 100%);
  --tear-card-3: polygon(0% 7.8px, 3.85% 5px, 7.69% 4.6px, 11.54% 6.1px, 15.38% 4.2px, 19.23% 6.9px, 23.08% 3.6px, 26.92% 1.3px, 30.77% 1.7px, 34.62% 4.2px, 38.46% 7.3px, 42.31% 9.8px, 46.15% 13px, 50% 10.2px, 53.85% 9.1px, 57.69% 8.7px, 61.54% 8.1px, 65.38% 11.6px, 69.23% 6.7px, 73.08% 5.2px, 76.92% 7.1px, 80.77% 9.6px, 84.62% 12.9px, 88.46% 7.8px, 92.31% 6.8px, 96.15% 2.7px, 100% 5.2px, 100% 100%, 0 100%);
  --tear-card-4: polygon(0% 8.6px, 3.85% 5.9px, 7.69% 3.2px, 11.54% 4.3px, 15.38% 3.3px, 19.23% 3.6px, 23.08% 1.4px, 26.92% 3.5px, 30.77% 3.5px, 34.62% 4.7px, 38.46% 9.8px, 42.31% 12.9px, 46.15% 12px, 50% 7.3px, 53.85% 8.6px, 57.69% 10.4px, 61.54% 8.4px, 65.38% 6.1px, 69.23% 7.6px, 73.08% 9.5px, 76.92% 7.2px, 80.77% 3.6px, 84.62% 1.3px, 88.46% 1.5px, 92.31% 1.9px, 96.15% 5.2px, 100% 4px, 100% 100%, 0 100%);
  --tear-scrap: polygon(0% 9px, 3.33% 12.4px, 6.67% 11.7px, 10% 12.5px, 13.33% 10.3px, 16.67% 11.6px, 20% 13.6px, 23.33% 15.9px, 26.67% 11.8px, 30% 11.6px, 33.33% 7.1px, 36.67% 5.9px, 40% 5.3px, 43.33% 3.1px, 46.67% 1.3px, 50% 1.1px, 53.33% 3.9px, 56.67% 4.4px, 60% 4.4px, 63.33% 7.7px, 66.67% 2.9px, 70% 8.2px, 73.33% 9.3px, 76.67% 4.8px, 80% 15.1px, 83.33% 15.5px, 86.67% 15.3px, 90% 14.7px, 93.33% 8.3px, 96.67% 5.4px, 100% 11.7px, 100% calc(100% - 3.2px), 96.67% calc(100% - 8.3px), 93.33% calc(100% - 7.3px), 90% calc(100% - 9.5px), 86.67% calc(100% - 4.1px), 83.33% calc(100% - 3.5px), 80% calc(100% - 7px), 76.67% calc(100% - 10.4px), 73.33% calc(100% - 14.2px), 70% calc(100% - 9px), 66.67% calc(100% - 9.4px), 63.33% calc(100% - 8.2px), 60% calc(100% - 8.9px), 56.67% calc(100% - 11.5px), 53.33% calc(100% - 15px), 50% calc(100% - 9.8px), 46.67% calc(100% - 12.9px), 43.33% calc(100% - 9.9px), 40% calc(100% - 16.7px), 36.67% calc(100% - 12.2px), 33.33% calc(100% - 13.6px), 30% calc(100% - 10.6px), 26.67% calc(100% - 14.7px), 23.33% calc(100% - 15.5px), 20% calc(100% - 14.8px), 16.67% calc(100% - 15.7px), 13.33% calc(100% - 15.9px), 10% calc(100% - 16.2px), 6.67% calc(100% - 11.8px), 3.33% calc(100% - 13.5px), 0% calc(100% - 10px));

/* sidebar (2026-09-08): the same tear/fiber technique as .sec-paper's top/bottom
   edges, rotated 90deg — a jagged RIGHT edge instead of a jagged top or bottom,
   since the sidebar is a vertical strip torn off and pinned to the side rather
   than a horizontal band torn top and bottom. Two independently-random polygons
   of similar depth, same reason as everywhere else: one clean cut would look
   too regular, two overlapping ragged lines read as real torn paper. */
  --tear-sidebar: polygon(0% 0%, calc(100% - 19.5px) 0%, calc(100% - 12.2px) 2.27%, calc(100% - 18.4px) 4.55%, calc(100% - 22.0px) 6.82%, calc(100% - 20.9px) 9.09%, calc(100% - 24.3px) 11.36%, calc(100% - 19.3px) 13.64%, calc(100% - 10.7px) 15.91%, calc(100% - 9.4px) 18.18%, calc(100% - 11.4px) 20.45%, calc(100% - 8.2px) 22.73%, calc(100% - 9.0px) 25.00%, calc(100% - 8.0px) 27.27%, calc(100% - 16.5px) 29.55%, calc(100% - 21.9px) 31.82%, calc(100% - 13.0px) 34.09%, calc(100% - 13.1px) 36.36%, calc(100% - 16.1px) 38.64%, calc(100% - 14.0px) 40.91%, calc(100% - 12.4px) 43.18%, calc(100% - 18.2px) 45.45%, calc(100% - 14.0px) 47.73%, calc(100% - 9.9px) 50.00%, calc(100% - 11.9px) 52.27%, calc(100% - 7.7px) 54.55%, calc(100% - 6.4px) 56.82%, calc(100% - 7.3px) 59.09%, calc(100% - 7.8px) 61.36%, calc(100% - 15.7px) 63.64%, calc(100% - 10.6px) 65.91%, calc(100% - 18.0px) 68.18%, calc(100% - 18.9px) 70.45%, calc(100% - 11.1px) 72.73%, calc(100% - 14.9px) 75.00%, calc(100% - 10.5px) 77.27%, calc(100% - 12.1px) 79.55%, calc(100% - 8.9px) 81.82%, calc(100% - 15.8px) 84.09%, calc(100% - 13.9px) 86.36%, calc(100% - 16.6px) 88.64%, calc(100% - 14.3px) 90.91%, calc(100% - 17.3px) 93.18%, calc(100% - 11.0px) 95.45%, calc(100% - 17.6px) 97.73%, calc(100% - 15.8px) 100%, 0% 100%);
  --fiber-sidebar: polygon(100% 0%, calc(100% - 5.6px) 0%, calc(100% - 5.1px) 2.27%, calc(100% - 10.9px) 4.55%, calc(100% - 14.0px) 6.82%, calc(100% - 11.7px) 9.09%, calc(100% - 18.2px) 11.36%, calc(100% - 19.1px) 13.64%, calc(100% - 15.6px) 15.91%, calc(100% - 8.9px) 18.18%, calc(100% - 12.4px) 20.45%, calc(100% - 13.9px) 22.73%, calc(100% - 11.5px) 25.00%, calc(100% - 18.0px) 27.27%, calc(100% - 19.7px) 29.55%, calc(100% - 20.5px) 31.82%, calc(100% - 19.6px) 34.09%, calc(100% - 17.6px) 36.36%, calc(100% - 16.9px) 38.64%, calc(100% - 10.8px) 40.91%, calc(100% - 17.9px) 43.18%, calc(100% - 19.2px) 45.45%, calc(100% - 15.0px) 47.73%, calc(100% - 19.6px) 50.00%, calc(100% - 19.9px) 52.27%, calc(100% - 21.9px) 54.55%, calc(100% - 13.1px) 56.82%, calc(100% - 11.8px) 59.09%, calc(100% - 7.0px) 61.36%, calc(100% - 4.6px) 63.64%, calc(100% - 12.8px) 65.91%, calc(100% - 17.6px) 68.18%, calc(100% - 13.6px) 70.45%, calc(100% - 7.5px) 72.73%, calc(100% - 15.8px) 75.00%, calc(100% - 20.3px) 77.27%, calc(100% - 11.0px) 79.55%, calc(100% - 8.5px) 81.82%, calc(100% - 13.1px) 84.09%, calc(100% - 11.8px) 86.36%, calc(100% - 16.1px) 88.64%, calc(100% - 11.2px) 90.91%, calc(100% - 12.0px) 93.18%, calc(100% - 17.1px) 95.45%, calc(100% - 13.4px) 97.73%, calc(100% - 7.9px) 100%, 100% 100%);
}



/* ---------- 2. Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
ul, ol { margin: 0; padding: 0; list-style: none; }
dl, dt, dd { margin: 0; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
h1, h2, h3, h4, p, figure { margin: 0; }

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

/* ---------- 3. Base typography ---------- */
body {
  background: var(--black);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  /* Slashed compound tokens in the project copy ("[ESTABLISHED]/[CONTESTED]/
     [FOLKLORE]", "confidentiality/integrity/availability") are longer than a
     phone column. `break-word` only breaks a word that would otherwise overflow
     and, unlike `anywhere`, leaves min-content sizing alone - so no grid track
     and no flex line changes width anywhere on the site. */
  overflow-wrap: break-word;
}

/* ONE grain layer per ground - deliberate, per John (2026-09-04).
   There used to be a second, site-wide photocopy veil on body::after at 0.12
   sitting on top of this. Because that veil was `fixed` while the per-section
   layers scroll, two identical 320px noise tiles slid across each other and
   shimmered - worst on the black sections, which are 23 of the 24 grounds.
   Each ground now carries its own single grain layer instead:
     .sec-black::before    grain + scan lines, 0.34
     .sec-paper            grain in its own background stack, ~0.12
     .site-footer::before  grain + scan lines, 0.34 (a black ground too)
   Never reintroduce a global veil on top of these. */

h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 400; text-transform: uppercase; }
h1 {
  /* The 4.5rem (72px) floor was a real overflow below ~460px: "COURSEWORK," set
     in Bebas at 72px is wider than a 342px phone column, and only `.hero h1`
     carried a phone override - every other page's h1 ran off the viewport. The
     vw term and the cap are untouched, so every width from ~483px up (the home
     page included, which overrides below 900 anyway) renders exactly as before. */
  font-size: clamp(2.9rem, 9.6vw, 10.5rem);
  line-height: 0.84;
  letter-spacing: 0.005em;
  color: var(--ink);
}
/* Headline lines are block spans, not <br>, so textContent reads as a
   sentence and the outlined word can carry its own decoration. */
.h1-line { display: block; }
h2 {
  font-size: clamp(2.7rem, 5.8vw, 5.6rem);
  line-height: 0.9;
  letter-spacing: 0.005em;
  color: var(--ink);
}
h3 { font-size: 1.65rem; line-height: 1; letter-spacing: 0.03em; color: var(--ink); }
h4 { font-family: var(--font-mono); font-size: 0.74rem; font-weight: 700; letter-spacing: 0.2em; color: var(--ink-3); }

/* One outlined word per headline (refs 10, 12). Falls back to muted solid. */
.outline { color: var(--ink-2); position: relative; }
.dev-sparkle.in-word { position: absolute; right: -0.3em; top: -0.1em; width: 0.24em; height: 0.24em; } /* sparkle tucked into the outlined word (ref 9) */
@supports (-webkit-text-stroke: 1px #000) {
  .outline {
    color: transparent;
    -webkit-text-stroke: 0.045em var(--ink);
    paint-order: stroke fill;
  }
}

/* Pull quote / lead — Playfair italic */
.lead, blockquote, .quote {
  font-family: var(--font-quote);
  font-style: italic;
  font-weight: 600;
  font-size: clamp(1.15rem, 1.45vw, 1.5rem);
  line-height: 1.4;
  color: var(--ink);
  max-width: 34ch;
}
blockquote { margin: 0; padding-left: 1.2rem; border-left: 2px solid var(--accent); }

p { color: var(--ink-2); max-width: 66ch; }
p a, li a, dd a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--accent);
  text-underline-offset: 3px;
}
p a:hover, li a:hover, dd a:hover { background: var(--ink); color: var(--ground); text-decoration: none; }
strong { color: var(--ink); }
/* Body copy is already Space Mono, so a bare <code> was indistinguishable from
   the prose around it and `POST /evaluate` wrapped mid-token at 390. A hairline
   box and a held line make it read as a literal again. */
code, .mono { font-family: var(--font-mono); }
code {
  padding: 0.1em 0.36em;
  border: 1px solid var(--line);
  background: var(--ground-2);
  font-size: 0.92em;
  white-space: nowrap;
}

/* Blackletter drop cap for the opening paragraph of a section */
.dropcap::first-letter {
  font-family: var(--font-black);
  font-size: 4.3em;
  line-height: 0.72;
  float: left;
  margin: 0.06em 0.14em 0 0;
  color: var(--ink);
  font-style: normal;
}

/* Keyboard focus — always visible, ground-aware */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

/* -- Eyebrow: "[01] section label" -- */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.75em;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-2);
  max-width: none;
}
.eyebrow::before { content: ""; width: 28px; height: 1px; background: currentColor; flex: none; }
/* The bracketed index is real markup (<span class="mark" aria-hidden="true">[02]</span>)
   rather than a CSS counter, so textContent and the a11y tree stay clean.
   When a mark is present it replaces the rule. */
.eyebrow:has(.mark)::before { content: none; }
/* When the label wraps (AFRL's two-line eyebrow at 390), `align-items:center`
   floated the bracketed mark down beside the SECOND line and read as a broken
   hanging indent. Baseline pins it to the first line. The mark shares the
   eyebrow's font and size, so on the single-line eyebrows everywhere else this
   computes to exactly the same box the centred rule produced. */
.eyebrow:has(.mark) { align-items: baseline; }
.mark {
  font-family: var(--font-mono);
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--accent-text);
  flex: none;
}

/* ---------- 4. Layout primitives + grounds ---------- */
.container { max-width: var(--max-width); margin: 0 auto; padding: 0 var(--space-6); }
.stack { display: flex; flex-direction: column; }
.grid { display: grid; gap: var(--space-6); }
.two-col-60-40 { grid-template-columns: 1.15fr 1fr; }
.two-col-40-60 { grid-template-columns: 1fr 1.4fr; }

/* CONTAINMENT CONTRACT: a section is the clip boundary for its own decoration -
   tilted paper scraps, halftone patches, ghost words, corner captions. `.sec`
   always had it; a bare <section> did not, which is why every interior page
   scrolled sideways at 390 while the home page never did. It lives on the
   element now, so both spellings behave identically. */
section { padding: var(--sec-pad) 0; position: relative; overflow: hidden; }
section.tight { padding: calc(var(--sec-pad) * 0.7) 0; }
/* Positioning context for .page-caps (page-level, not section-level, captions).
   Nothing inside index.html's <main> is positioned against the initial
   containing block, so this changes no existing layout. */
main { position: relative; }

/* -- Sections carry their ground. .sec is the base (position + clip). -- */
.sec { position: relative; overflow: hidden; }

/* BLACK ground: heavier grain + faint scan lines (refs 8, 15). */
.sec-black {
  background: var(--black);
  color: var(--ink);
  z-index: 1;
}
.sec-black::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(to bottom, rgba(243, 241, 236, 0.045) 0 1px, transparent 1px 3px),
    var(--grain) 0 0 / 320px 320px repeat;
  opacity: 0.34;
  z-index: 0;
}
.sec-black > * { position: relative; z-index: 1; }

/* PAPER ground: flips every ground token, then tears its own top and
   bottom (clip-path) and overlaps the black sections around it. */
.sec-paper {
  --ground: var(--paper);
  --ground-2: var(--paper-2);
  --ink: var(--paper-ink);
  --ink-2: var(--paper-ink-2);
  --ink-3: var(--paper-ink-3);
  --line: rgba(23, 21, 15, 0.28);
  --line-strong: rgba(23, 21, 15, 0.7);
  --accent: var(--gold-paper);
  --accent-text: var(--gold-paper-text);
  --accent-solid: var(--gold);
  --on-accent: var(--paper-ink);
  --scrap: var(--paper-2);
  --scrap-2: #fbfaf7;
  --scrap-ink: var(--paper-ink);
  --scrap-ink-2: var(--paper-ink-2);
  --scrap-ink-3: var(--paper-ink-3);
  --scrap-line: rgba(23, 21, 15, 0.35);
  --scrap-shadow: 5px 6px 0 rgba(23, 21, 15, 0.6);   /* paste-up shadow on paper */
  --tape: rgba(23, 21, 15, 0.14);
  --focus: var(--paper-ink);
  /* Legacy aliases, restated. See the note in .card: `--text: var(--ink)` is
     substituted where it is DECLARED (:root), so redefining --ink here would
     otherwise leave `style="color:var(--text)"` white on paper. */
  --text: var(--paper-ink);
  --text-muted: var(--paper-ink-2);
  --text-dim: var(--paper-ink-3);
  --bg: var(--paper);
  --bg-raised: var(--paper-2);
  --bg-panel: var(--paper-2);
  --border: rgba(23, 21, 15, 0.28);
  --border-strong: rgba(23, 21, 15, 0.7);
  color-scheme: light;

  /* Paper's single grain layer. It cannot use a pseudo-element - ::before and
     ::after are both spent on the fibrous tear edges - so the grain rides in
     the background stack: an 88%-opaque paper wash over the noise tile mutes
     it to the ~0.12 the veil used to supply, flat paper colour beneath. */
  background:
    linear-gradient(rgba(232, 230, 225, 0.88), rgba(232, 230, 225, 0.88)),
    var(--grain) 0 0 / 320px 320px repeat,
    var(--paper);
  color: var(--ink);
  z-index: 2;
  margin-top: calc(var(--tear-h) * -1);
  margin-bottom: calc(var(--tear-h) * -1);
  padding-top: calc(var(--sec-pad) + var(--tear-h));
  padding-bottom: calc(var(--sec-pad) + var(--tear-h));
  clip-path: var(--tear, var(--tear-about));
}
.sec-paper.tight { padding-top: calc(var(--sec-pad) * 0.7 + var(--tear-h)); padding-bottom: calc(var(--sec-pad) * 0.7 + var(--tear-h)); }
/* Fibrous lighter edge along both tears */
.sec-paper::before, .sec-paper::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: calc(var(--tear-h) + 14px);
  background: #fbfaf7;
  pointer-events: none;
  z-index: 0;
}
.sec-paper::before { top: 0; clip-path: var(--fiber-top, var(--fiber-about-top)); }
.sec-paper::after { bottom: 0; clip-path: var(--fiber-bot, var(--fiber-about-bot)); }
.sec-paper > * { position: relative; z-index: 1; }

/* Four tear variants so consecutive paper sections never repeat an edge */
.tear-1 { --tear: var(--tear-about);    --fiber-top: var(--fiber-about-top);    --fiber-bot: var(--fiber-about-bot); }
.tear-2 { --tear: var(--tear-certs);    --fiber-top: var(--fiber-certs-top);    --fiber-bot: var(--fiber-certs-bot); }
.tear-3 { --tear: var(--tear-course);   --fiber-top: var(--fiber-course-top);   --fiber-bot: var(--fiber-course-bot); }
.tear-4 { --tear: var(--tear-skills);   --fiber-top: var(--fiber-skills-top);   --fiber-bot: var(--fiber-skills-bot); }

/* Per-section accent (one per composition) */
.accent-gold { --accent: var(--gold); --accent-text: var(--gold); --accent-solid: var(--gold); --on-accent: var(--paper-ink); }
.accent-blue { --accent: var(--blue); --accent-text: var(--blue); --accent-solid: var(--blue); --on-accent: var(--paper-ink); }
.accent-teal { --accent: #9fcfcb; --accent-text: #9fcfcb; --accent-solid: var(--teal); --on-accent: var(--white); }
.sec-paper.accent-gold { --accent: var(--gold-paper); --accent-text: var(--gold-paper-text); --accent-solid: var(--gold); --on-accent: var(--paper-ink); }
.sec-paper.accent-blue { --accent: var(--blue-paper); --accent-text: var(--blue-paper); --accent-solid: var(--blue-paper); --on-accent: var(--white); }
.sec-paper.accent-teal { --accent: var(--teal-paper); --accent-text: var(--teal-paper); --accent-solid: var(--teal); --on-accent: var(--white); }

/* -- Section head: eyebrow + h2. A dashed rule runs across the top of every
      head (the zine's page rule). Two numeral variants, one per head:
        .with-mark + .dev-mark   → blackletter numeral, PAPER grounds
        .with-num  + .dev-num    → outlined Bebas numeral, BLACK grounds
      Never show both for the same section. -- */
.section-head {
  max-width: 760px;
  margin-bottom: var(--space-12);
  position: relative;
  padding-top: var(--space-5);
  border-top: 1px dashed var(--line);
}
.section-head h2 { margin-top: var(--space-3); }
.section-head.with-mark {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--space-6);
  align-items: start;
}
.section-head.with-mark .dev-mark { grid-row: 1 / span 2; margin-top: -0.2em; }
.section-head.with-num {
  max-width: none;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-8);
}
.section-head.with-num > div { max-width: 760px; }
.h2-sm { font-size: clamp(1.5rem, 2.4vw, 2.1rem); line-height: 1.05; max-width: 26ch; }

/* -- Hatch strip under the nav (ref 8's ////// bar), one gold segment -- */
.stripe {
  height: 8px;
  background:
    linear-gradient(var(--gold), var(--gold)) 0 0 / 140px 100% no-repeat,
    repeating-linear-gradient(-45deg, rgba(243, 241, 236, 0.7) 0 1px, transparent 1px 7px);
}

/* ---------- 5. Components ---------- */

/* -- Nav: sticky, black, dashed rule; brand in Bebas; links bracketed when active -- */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(11, 11, 12, 0.94);
  backdrop-filter: blur(6px);
  border-bottom: 1px dashed var(--line-strong);
}
.site-nav .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 64px;
}
.brand {
  font-family: var(--font-display);
  font-size: 1.55rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--white);
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  line-height: 1;
  min-height: 44px;      /* the wordmark is also the home link — give it a target */
}
.brand .cursor {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--gold);
  border: 1px solid var(--gold);
  padding: 0.25em 0.4em 0.15em;
  line-height: 1;
}
.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}
.nav-links a { color: var(--muted); transition: color 0.15s ease; }
.nav-links a:hover { color: var(--white); }
.nav-links a.active { color: var(--white); }
/* The brackets are DECORATION, not part of the link's name: without the ""
   alt string Chromium folds the generated characters into the accessible name
   and the current page announces as "[ Home ]". Same for the footer's hover
   pair below. */
.nav-links a.active::before { content: "[ " / ""; color: var(--gold); }
.nav-links a.active::after { content: " ]" / ""; color: var(--gold); }
/* WCAG 2.2 target size (2.5.8) at EVERY width, not just on the phone: a 19px
   link box becomes 27px. The nav container's height is fixed at 64px, so
   nothing above or beside it moves. */
.nav-links a { display: inline-block; padding: 4px 0; }
/* Nav toggle: a 44x44 target (WCAG 2.5.8) whose three bars animate into an X.
   main.js mirrors the state in aria-expanded and closes on Escape. */
.nav-toggle {
  display: none;
  position: relative;
  width: 44px;
  height: 44px;
  padding: 0;
  margin-right: -10px;
  flex: none;
}
.nav-toggle span {
  position: absolute;
  left: 10px;
  width: 24px;
  height: 2px;
  background: var(--white);
  display: block;
  transition: transform 0.18s ease, opacity 0.12s ease;
}
.nav-toggle span:nth-child(1) { top: 15px; }
.nav-toggle span:nth-child(2) { top: 21px; }
.nav-toggle span:nth-child(3) { top: 27px; }
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

/* -- Hero poster (2026-09-08: now the "Home" section, a single-column case-file
      masthead for the flagship project, not the old two-column layout — the
      SUBJECT FILE card that used to live in .collage is a permanent sidebar
      fixture now, so this section doesn't carry personal identity anymore).
      The dashed frame with fold creases, four knockout corner caps, the
      vertical edge caption and the silk-ribbon texture ground still apply. -- */
.hero { padding: var(--space-6) 0 calc(var(--space-12) + var(--tear-h)); }
/* Home only: fill (at least) the first viewport, so the flagship project is
   the whole first screen — .poster's own flex column centers its content
   inside whatever height this gives it. */
.home-feature { min-height: 100vh; display: flex; align-items: center; }
.home-feature .container { width: 100%; }

/* Full-bleed texture ground. z-index -1 puts it under .sec-black::before's
   grain + scan lines but over the section's own black. The veil keeps every
   run of real text on ≥0.9-alpha black. */
.hero-tex {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--black) url('/assets/img/silk-ribbon.webp') right center / cover no-repeat;
  pointer-events: none;
}
.hero-tex::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg, rgba(11, 11, 12, 0.98) 0%, rgba(11, 11, 12, 0.955) 42%, rgba(11, 11, 12, 0.62) 62%, rgba(11, 11, 12, 0.2) 100%),
    linear-gradient(180deg, rgba(11, 11, 12, 0.85) 0%, transparent 20%, transparent 66%, var(--black) 100%);
}

.poster {
  position: relative;
  border: 1px dashed rgba(243, 241, 236, 0.6);
  padding: clamp(1.8rem, 3.2vw, 3rem) clamp(1.2rem, 3vw, 2.8rem) clamp(2rem, 3.6vw, 3.4rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* The 1.75:1 split is what keeps "under attack" on ONE line at 1440 — the
   headline is three lines by construction, not by a <br>. */
.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 3.5vw, 3.5rem);
  align-items: center;
  position: relative;
  z-index: 2;
}
.hero h1 { margin-top: var(--space-4); position: relative; }
.hero .lead { margin-top: var(--space-5); max-width: 30ch; }
.hero .lede { max-width: 50ch; margin-top: var(--space-4); font-size: 0.92rem; }

/* Collage column: rings + text ring behind the subject card */
.collage { position: relative; min-height: 460px; display: flex; align-items: center; }
.collage .dev-ring.r1 { position: absolute; left: -12%; top: 26%; width: 118%; height: 44%; transform: rotate(-14deg); }
.collage .dev-ring.r2 { position: absolute; left: 2%; top: 34%; width: 96%; height: 30%; transform: rotate(-10deg); opacity: 0.5; }
.collage .dev-sparkle.s1 { position: absolute; top: 2%; left: -2%; width: 30px; height: 30px; }
.collage .dev-sparkle.s2 { position: absolute; bottom: 6%; right: 2%; width: 18px; height: 18px; }

/* -- Subject card: the whoami.sh shell lines, typed on the sidebar's sheet of
      paper. Since 2026-09-13 the card paints no paper of its own: the whole
      .sidebar column IS the sheet (John: "I want the left side of the screen to
      be the entire white thing"), so the card is just the content laid out on
      it - no rotation, no tear, no tape, no drop shadow. The shell prompts are
      real text in the markup, not CSS content. -- */
/* The sidebar's grain (alpha 0.28) drops the paper's luminance from .792 to
   ~.645, so --gold-paper-text (#7a5915, a true 5.15:1 on clean #e8e6e1) only
   measures ~4.3:1 HERE. The shell prompts are small text, so the token is
   darkened one step for this card only - the texture stays untouched. */
.subject-card {
  --gold-paper-text: #6b4d12;   /* 5.1–5.2:1 on the grained paper, measured in-page */
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  width: 100%;
  color: var(--paper-ink);
}
.subject-card > * { position: relative; z-index: 1; }
.subject-head {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  padding-bottom: 0.5rem;
  border-bottom: 1px solid rgba(23, 21, 15, 0.55);
}
.subject-card .micro { color: var(--paper-ink-2); background: none; }
.field-list { display: grid; gap: 0.75rem; margin-top: 0.85rem; font-family: var(--font-mono); }
/* NO text-transform here: these are shell commands ("$ ls", "$ cat"), and an
   uppercase `LS` is not one. The caps system stops at the card's captions. */
.field-list dt {
  font-size: 0.68rem;   /* 9.6px was below comfortable reading size for the one line a hiring manager most needs */
  font-weight: 700;
  letter-spacing: 0.2em;
  color: var(--gold-paper-text);
}
.field-list dd {
  font-size: 0.8rem;
  line-height: 1.45;
  white-space: break-spaces;   /* the double spaces between the three paths are in the source; keep them visible */
  color: var(--paper-ink-2);
  border-bottom: 1px dashed rgba(23, 21, 15, 0.3);
  padding-bottom: 0.4rem;
  margin-top: 0.2rem;
}
.subject-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-top: auto; padding-top: 1.4rem; }   /* margin-top:auto = pinned to the foot of the sheet */
.subject-foot .micro { display: inline-flex; align-items: center; gap: 0.5em; }
.subject-foot .dev-barcode { width: 90px; height: 20px; color: var(--paper-ink-2); }
/* Stamped at the foot of the sheet, beside the social links, where nothing is
   typed - at the old top-right spot it sat on the whoami line once the card
   stopped being a narrow scrap. */
.subject-card .stamp { position: absolute; right: 10px; bottom: -4px; z-index: 3; }

/* -- Home page shell: brittanychiang.com's structural pattern (a fixed sidebar,
      scrolling resume-style content) rebuilt in this site's own paper/tape idiom —
      own copy, own gold accent, not her navy/teal look. The sidebar's whole content
      IS .subject-card, just pinned here permanently instead of living once in a
      hero; .side-nav and .side-social are the two things it didn't already have. -- */
.shell { display: flex; align-items: flex-start; }
/* The sidebar is ONE full-height sheet of paper (2026-09-13): the paper that
   used to be a 400px scrap floating on a black column now fills the column edge
   to edge, torn along its right side. The card inside is content only. */
.sidebar {
  position: relative;
  flex: none;
  width: min(360px, 34vw);
  padding: var(--space-8) var(--space-8) var(--space-8) var(--space-6);
  background: var(--paper);
  clip-path: var(--tear-sidebar);
}
/* Grain over the sidebar's own torn shape, same recipe as every other
   paper/card surface, so it doesn't read as the one flat, textureless
   surface on the page. */
.sidebar::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--grain) 0 0 / 320px 320px repeat;
  opacity: 0.28;   /* the one grain layer on this ground; the card no longer adds its own */
  clip-path: var(--tear-sidebar);
  pointer-events: none;
}
/* Fiber band: an independently-jagged near-white sliver painted over the
   torn edge's own zone, the same trick as .sec-paper's ::before/::after —
   two similarly-scaled but non-matching ragged lines read as real torn
   paper; one clean cut does not. */
.sidebar::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: 48px;
  background: #fbfaf7;
  clip-path: var(--fiber-sidebar);
  pointer-events: none;
}
@media (min-width: 900px) {
  .sidebar { position: sticky; top: 0; height: 100vh; display: flex; align-items: stretch; overflow-y: auto; }
}
.sidebar .subject-card { width: 100%; min-height: 100%; margin: 0 auto; position: relative; z-index: 1; }
.side-nav { display: grid; gap: 0.55rem; margin-top: 0.9rem; font-family: var(--font-mono); }
.side-nav a { font-size: 0.76rem; color: var(--paper-ink-2); }
.side-nav a:hover, .side-nav a:focus-visible, .side-nav a.active { color: var(--gold-paper-text); }
.side-social { display: flex; flex-wrap: wrap; gap: 0.5em 1rem; margin-top: 0.9rem; font-family: var(--font-mono); font-size: 0.7rem; }
.side-social a { color: var(--paper-ink-2); }
.side-social a:hover, .side-social a:focus-visible { color: var(--gold-paper-text); }

@media (max-width: 899px) {
  .shell { flex-direction: column; }
  /* The torn edge is a seam against the content beside it — on the stacked
     mobile layout the sidebar sits above the content instead, so a torn
     RIGHT edge has nothing to explain. Plain rectangle here. */
  .sidebar { width: 100%; clip-path: none; }
  .sidebar::before, .sidebar::after { clip-path: none; }
  .sidebar::after { display: none; }
  .sidebar .subject-card { max-width: 420px; }
}

/* -- Buttons: square, mono caps; primary is the ground's inverse -- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.85em 1.5em;
  border: 1px solid var(--ink);
  border-radius: 0;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  background: transparent;
  line-height: 1;
  white-space: nowrap;
  transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease, color 0.12s ease;
}
.btn:hover { transform: translate(-2px, -2px); box-shadow: 3px 3px 0 var(--accent-solid); }
.btn-primary { background: var(--ink); color: var(--ground); }
.btn-primary:hover { background: var(--ink); color: var(--ground); }
.btn-ghost { background: transparent; }
.btn-ghost:hover { background: var(--ink); color: var(--ground); }
.btn-row { display: flex; flex-wrap: wrap; gap: var(--space-3); }

/* -- Badges: solid label blocks (ref 8's "Savior of the world" bar). Text is always
      dark-on-light or light-on-dark, so they pass AA on both grounds. -- */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  padding: 0.38em 0.7em 0.3em;
  border-radius: 0;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1;
  border: 1px solid transparent;
  white-space: nowrap;
}
.badge::before { content: ""; width: 6px; height: 6px; background: currentColor; flex: none; }
/* The status WORD carries the difference, not a second fill colour - the rule
   the home page's Certifications section already follows. --teal goes back to
   being reserved for the Contact trapezoid. 8.4:1 on the gold fill. */
.badge-green { background: var(--accent-solid); color: var(--on-accent); }
.badge-amber { background: var(--gold); color: var(--paper-ink); }
.badge-accent { background: var(--accent-solid); color: var(--on-accent); }
.badge-muted { background: transparent; color: var(--ink-2); border-color: var(--line-strong); }
.badge-muted::before { display: none; }
/* on a paper scrap (card) the muted badge borrows the scrap's ink */
.card .badge-muted { color: var(--scrap-ink-2); border-color: var(--scrap-line); }

/* -- Chips: outlined mono tags, square -- */
.chip {
  display: inline-flex;
  align-items: center;
  padding: 0.3em 0.6em 0.22em;
  border: 1px solid var(--line-strong);
  border-radius: 0;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--ink-2);
  line-height: 1.3;
  background: transparent;
}
.chip-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.card .chip { color: var(--scrap-ink-2); border-color: var(--scrap-line); }

/* -- Cards: paper scraps with a torn top, taped down. The paper lives on ::before
      (so the tape on ::after can sit above the tear); the card's own filter draws
      the fibrous edge highlight and, on paper grounds, a hard paste-up shadow.
      Rotation comes from --tilt (set per grid position below). -- */
.card {
  /* Same correction as .subject-card and for the same reason: the card's own
     grain (::before, alpha 0.4) drops the paper's luminance, so the #7a5915
     token measures ~4.65:1 on .card-tag instead of its nominal 5.2. One step
     darker restores the margin with no visible change to the caption. */
  --gold-paper-text: #6b4d12;
  /* A .card is a paper scrap pasted onto whatever ground it sits on, so it has
     to flip the ground-relative tokens the way .sec-paper does. Without this,
     anything reading --ink/--ink-2/--ink-3 (directly or through the legacy
     --text/--text-muted/--text-dim aliases) stayed on the BLACK ground's
     palette while sitting on paper: <strong style="color:var(--text)"> rendered
     #f3f1ec on #f4f2ed - 1.05:1, which is why projects.html's "The problem." /
     "The approach." / "Team." labels were invisible - and .text-dim measured
     2.9:1. The explicit --scrap-* declarations further down are unchanged and
     still win wherever they exist. */
  --ink: var(--scrap-ink);
  --ink-2: var(--scrap-ink-2);
  --ink-3: var(--scrap-ink-3);
  --line: rgba(23, 21, 15, 0.22);
  --line-strong: var(--scrap-line);
  --ground: var(--scrap);
  --ground-2: var(--scrap-2);
  --accent: var(--gold-paper);
  --accent-text: var(--gold-paper-text);
  /* The legacy aliases have to be restated, not inherited: `--text: var(--ink)`
     is declared on :root, and a var() inside a custom property is substituted
     at computed-value time WHERE IT IS DECLARED. So --text had already frozen
     to #f3f1ec before it ever reached this scrap, and redefining --ink above
     could not reach it. This is the line that makes projects.html's
     `<strong style="color:var(--text)">The problem.</strong>` visible. */
  --text: var(--scrap-ink);
  --text-muted: var(--scrap-ink-2);
  --text-dim: var(--scrap-ink-3);
  --bg: var(--scrap);
  --bg-raised: var(--scrap-2);
  --bg-panel: var(--scrap-2);
  --border: rgba(23, 21, 15, 0.22);
  --border-strong: var(--scrap-line);
  --tilt: 0deg;
  --tape-x: 50%;
  --tear-card: var(--tear-card-1);
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 2.3rem 1.5rem 1.5rem;
  color: var(--scrap-ink);
  transform: rotate(var(--tilt));
  filter: drop-shadow(0 -1px 0 rgba(255, 255, 255, 0.55)) drop-shadow(var(--scrap-shadow));
  transition: transform 0.18s ease;
}
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--scrap);
  clip-path: var(--tear-card);
  z-index: 0;
}
.card::after { /* tape strip */
  content: "";
  position: absolute;
  top: -9px;
  left: var(--tape-x);
  width: 86px; height: 24px;
  background:
    repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.32) 0 2px, transparent 2px 6px),
    var(--tape);
  transform: translateX(-50%) rotate(-4deg);
  z-index: 2;
}
.card > * { position: relative; z-index: 1; }
a.card:hover, .card.hoverable:hover { transform: rotate(0deg) translateY(-3px); }
/* Focus inside a card lands on PAPER, not on the section's ground, so --ink is
   remapped to the scrap's ink for the ring (the ground's gold would vanish on
   #e8e6e1). The card's own ring stays the ground colour and clears the tear. */
.card :focus-visible { --ink: var(--scrap-ink); outline-color: var(--ink); }
a.card:focus-visible { outline-offset: 6px; }
.card h3 { color: var(--scrap-ink); margin-bottom: var(--space-2); }
.card p { color: var(--scrap-ink-2); font-size: 0.9rem; margin-bottom: var(--space-4); max-width: none; }
.card p a, .card li a { color: var(--scrap-ink); text-decoration-color: var(--accent-solid); }
.card .chip-row { margin-top: auto; }
.card .btn { align-self: flex-start; border-color: var(--scrap-ink); color: var(--scrap-ink); }
.card .btn:hover { background: var(--scrap-ink); color: var(--scrap); }
.card .btn-primary { background: var(--scrap-ink); color: var(--scrap); }
.card .eyebrow { color: var(--scrap-ink-3); }
.card-head {
  display: flex;
  /* A title plus a nowrap status block on one unwrappable flex line is what
     pushed labs.html and all three project pages off the right edge at 390:
     neither child can shrink below its own min-content, so the row's
     min-content became the card's, and the card's became the grid track's.
     Wrapping costs nothing where both fit and fixes it where they do not. */
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
/* The two certification scraps keep their badge on the title's line: those two
   titles fit beside it at every width, and index.html is the approved
   composition - this is the one place the default wrap is opted out of. */
.cert-cards .card-head { flex-wrap: nowrap; }
.card-head h3 { margin-bottom: 0; }
.card .stamp { position: absolute; top: 0.55rem; right: 0.9rem; z-index: 3; } /* sits in the torn margin above the card-head */
/* A card's badge is a label block, not a full-width bar: in a flex column
   (a.card) it would otherwise stretch to the card's width. */
.card .badge { align-self: flex-start; }

/* Caption line above a card's head: "CASE NO. 01 — <context>". Restates
   context already in the copy; always aria-hidden. */
.card-tag {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--gold-paper-text);
  border-bottom: 1px solid rgba(23, 21, 15, 0.35);
  padding-bottom: 0.5rem;
  margin-bottom: var(--space-3);
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.2rem 0.75rem;
}
.card-tag .status { color: var(--scrap-ink-3); font-weight: 400; }

/* Affordance line at the foot of a card that IS a link. Three of the six cards
   on projects.html open a full case study and three do not, and they rendered
   identically — the only cue was a 3px hover lift, which does not exist on a
   phone. This is the cue, and it is real text, so it reads to everyone.
   --gold-paper-text on the grained scrap: 6.25:1. */
.card .card-cta {
  margin: var(--space-4) 0 0;
  padding-top: 0.7rem;
  border-top: 1px dashed var(--scrap-line);
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--gold-paper-text);
}
a.card:hover .card-cta, a.card:focus-visible .card-cta { color: var(--scrap-ink); }

/* The card's shadow on a black ground is an L-shaped halftone strip offset
   down and right, clipped so it never lies over the paper itself (ref 8).
   z-index:-1 keeps it behind ::before's paper inside the card's own stacking
   context (the card has transform + filter, so it makes one). */
.card > .dev-halftone {
  position: absolute;
  inset: 0;
  z-index: -1;
  width: auto; height: auto;
  opacity: 1;
  transform: translate(12px, 14px);
  background: radial-gradient(circle, var(--halftone-ink, rgba(243, 241, 236, 0.55)) 1.1px, transparent 1.6px) 0 0 / 5px 5px;
  -webkit-mask: none;
  mask: none;
  clip-path: polygon(calc(100% - 12px) 0, 100% 0, 100% 100%, 0 100%, 0 calc(100% - 14px), calc(100% - 12px) calc(100% - 14px));
}
.sec-paper .card > .dev-halftone { --halftone-ink: rgba(23, 21, 15, 0.5); }

/* Photo-treated texture thumbnail across the top of a card (ref 8). The image
   lives on ::before so an invert filter never reaches the caption or barcode. */
.thumb {
  position: relative;
  aspect-ratio: 16 / 7;
  overflow: hidden;
  background-color: var(--black);
  border-bottom: 1px solid var(--scrap-line);
  margin: -0.6rem -0.4rem var(--space-4);
}
.thumb::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--thumb-img);
  background-size: cover;
  background-position: center;
  filter: var(--thumb-filter, none);
}
.thumb::after { /* halftone screen */
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, rgba(11, 11, 12, 0.85) 1px, transparent 1.55px);
  background-size: 5px 5px;
  mix-blend-mode: multiply;
  opacity: 0.6;
}
.thumb > * { position: relative; z-index: 1; }
.thumb-blue { --thumb-img: url('/assets/img/liquid-blue.webp'); }
.thumb-mono { --thumb-img: url('/assets/img/silk-ribbon.webp'); --thumb-filter: contrast(1.12); }
.thumb-mono.invert { --thumb-filter: invert(1) contrast(1.12); }
.thumb-silk { --thumb-img: url('/assets/img/silk-mono.webp'); }
.thumb .dev-cap { color: var(--white); background: var(--black); border: 1px solid var(--line-strong); padding: 3px 6px; top: 9px; left: 9px; }
.thumb .dev-barcode { position: absolute; right: 10px; bottom: 9px; z-index: 1; width: 64px; height: 18px; color: var(--white); opacity: 0.75; }

/* Flagship: thick ink bar down the left of the scrap */
.card-flagship::before { box-shadow: inset 7px 0 0 var(--accent-solid); }
.card-flagship { padding-left: 1.9rem; }

/* Grids of scraps: alternate tilt, tape position and tear shape */
.project-grid { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: var(--space-8) var(--space-6); align-items: stretch; }
/* THE PASTE-UP WALL. This was `columns: 17rem 4` — multi-column flow, chosen
   because a grid row sizes to its tallest cell and the 44 fully-expanded cards
   ran from a one-line kata to a DevSecOps capstone. But column flow runs DOWN:
   scanning across the top of the wall read LAB NO. 01 → 15 → 29, so the
   catalogue's own numbering looked random, and reading one column properly
   meant a 3,600px scroll and a scroll back up.
   The cards are summary-first now (see `.lab-detail` below) so their heights
   are close to uniform, which is what makes a real grid affordable: numbering
   runs in reading order, and `align-items:start` keeps a short card short
   instead of stretching it to the row. */
.lab-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr));
  gap: var(--space-8) var(--space-6);
  align-items: start;
}
/* labs.html paints 44 of these at once: a two-pass drop-shadow filter on each
   one is the page's most expensive paint. Swap it for a single box-shadow —
   the ground-relative token is transparent on black, so nothing is lost there
   and paper grounds still get the hard paste-up offset. */
.lab-grid .card { filter: none; box-shadow: var(--scrap-shadow); }

/* ---- CATALOG INTERSTITIAL BAND --------------------------------------------
   The catalog was the one place on the site that stopped composing and started
   listing: an unbroken 11x4 grid running ~5,000px with no device in it, on a
   page that changes gear every ~800px everywhere else. labs.js cuts one of
   these across the full grid after every twelfth VISIBLE scrap (so it re-places
   itself on every filter change and never lands under a hidden card):

     <div class="lab-band" aria-hidden="true">
       <span class="dev-num">II</span>
       <span class="lab-band-mark"><span class="dev-label">…</span>
                                   <span class="dev-hazard"></span></span>
       <span class="lab-band-ghost">…discipline of the run below…</span>
     </div>

   Decoration only, and every string in it restates a caption already on the
   page (the Sec. V page cap, a discipline already filed on the scraps).  ---- */
.lab-band {
  grid-column: 1 / -1;
  position: relative;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: clamp(0.9rem, 2.4vw, 2rem);
  margin: var(--space-6) 0 var(--space-2);
  padding-top: var(--space-6);
  border-top: 1px dashed var(--line-strong);
  overflow: hidden;
  pointer-events: none;
  user-select: none;
}
.lab-band > .dev-num { font-size: clamp(2.4rem, 5vw, 4rem); }
.lab-band-mark { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3); flex: 1 1 auto; min-width: 0; }
.lab-band-mark .dev-hazard { width: min(300px, 100%); }
.lab-band-ghost {
  flex: none;
  font-family: var(--font-display);
  font-size: clamp(1.55rem, 5vw, 4rem);
  line-height: 0.82;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  white-space: nowrap;
  color: var(--ink);
  opacity: 0.14;
}
@supports (-webkit-text-stroke: 1px #000) {
  .lab-band-ghost {
    color: transparent;
    -webkit-text-stroke: 1px var(--ink);
    opacity: 0.34;
  }
}
.project-grid > .card:nth-child(4n+1), .lab-grid > .card:nth-child(4n+1), .grid > .card:nth-child(4n+1) { --tilt: -1.1deg; --tape-x: 26%; --tear-card: var(--tear-card-1); }
.project-grid > .card:nth-child(4n+2), .lab-grid > .card:nth-child(4n+2), .grid > .card:nth-child(4n+2) { --tilt: 0.8deg;  --tape-x: 44%; --tear-card: var(--tear-card-2); }
.project-grid > .card:nth-child(4n+3), .lab-grid > .card:nth-child(4n+3), .grid > .card:nth-child(4n+3) { --tilt: -0.5deg; --tape-x: 56%; --tear-card: var(--tear-card-3); }
.project-grid > .card:nth-child(4n+4), .lab-grid > .card:nth-child(4n+4), .grid > .card:nth-child(4n+4) { --tilt: 1.2deg;  --tape-x: 34%; --tear-card: var(--tear-card-4); }

/* -- Lab pages (/labs/*.html): a transcribed coursework deliverable. The prose
      column is narrower than the case files' because these are step-by-step lab
      notes rather than a narrative, and the screenshots are the evidence. -- */
.lab-head { padding-bottom: var(--space-10); }
.lab-head .lab-wm {
  /* Same signing gesture as the footer watermark, held well under the headline
     so a long ghost word (a CVE id, say) never competes with the h1. */
  right: 2%;
  top: 8%;
  font-size: clamp(3.4rem, 9vw, 8rem);
  opacity: 0.05;
  white-space: nowrap;
}
.lab-doc { max-width: 78ch; }
.lab-doc h3 {
  margin: var(--space-10) 0 var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px dashed var(--line);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.lab-doc h3:first-child { margin-top: 0; border-top: 0; padding-top: 0; }
.lab-doc p { margin: 0 0 var(--space-4); color: var(--ink-2); }
.lab-doc ul { margin: 0 0 var(--space-4); padding-left: 1.1rem; color: var(--ink-2); }
.lab-doc li { margin: 0 0 var(--space-2); }
.lab-doc li::marker { color: var(--accent-text); }

/* A series page stacks several deliverables, so each one gets a heavier mark
   than the h3 steps inside it: poster type on a full rule, the way a section
   opens elsewhere on the site. */
.lab-part {
  margin: var(--space-12) 0 var(--space-6);
  padding-top: var(--space-5);
  border-top: 2px solid var(--accent);
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 3.4vw, 2.3rem);
  font-weight: 400;
  letter-spacing: 0.02em;
  line-height: 1.05;
  text-transform: uppercase;
  color: var(--ink);
}
.lab-part:first-child { margin-top: 0; }

/* Commands and policy JSON are literals: they keep their own line breaks and
   scroll sideways rather than reflowing as prose. The inline <code> rule sets
   white-space:nowrap, which would defeat a block, so it is reset here. */
.lab-doc pre {
  margin: 0 0 var(--space-4);
  padding: var(--space-4);
  overflow-x: auto;
  border: 1px solid var(--line);
  background: var(--ground-2);
  font-size: 0.82rem;
  line-height: 1.6;
}
.lab-doc pre code {
  padding: 0;
  border: 0;
  background: none;
  font-size: inherit;
  white-space: pre;
}
/* A long literal (an IAM action list runs to dozens of lines) is evidence
   rather than reading, so it stays folded until asked for. */
.lab-code { margin: 0 0 var(--space-4); }
.lab-code > summary {
  cursor: pointer;
  padding: 0.45rem 0.7rem;
  border: 1px dashed var(--line);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.lab-code > summary:hover { background: var(--ground-2); }
.lab-code[open] > summary { margin-bottom: var(--space-3); }
.lab-code pre { margin-bottom: 0; }

/* A screenshot is a taped-in exhibit, not a bare image: paper mount, slight
   tilt, and the same paste-up shadow the cards use. The image itself is left
   untouched - no grain, no filter - because it is evidence. */
.lab-shot {
  --tilt: -0.6deg;
  margin: var(--space-6) 0 var(--space-8);
  padding: 10px 10px 12px;
  background: var(--scrap);
  clip-path: var(--tear-card-2);
  transform: rotate(var(--tilt));
  filter: drop-shadow(0 -1px 0 rgba(255, 255, 255, 0.5)) drop-shadow(var(--scrap-shadow));
}
.lab-shot:nth-of-type(even) { --tilt: 0.7deg; clip-path: var(--tear-card-4); }
.lab-shot img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid rgba(23, 21, 15, 0.35);
}
@media (max-width: 700px) {
  .lab-shot { transform: none; clip-path: none; padding: 6px; }
}
@media (prefers-reduced-motion: reduce) {
  .lab-shot { transform: none; }
}

/* -- Panel: thin-bordered poster panel (ref 3's grid) with registration-mark corners -- */
.panel {
  position: relative;
  background: transparent;
  border: 1px solid var(--line-strong);
  border-radius: 0;
  padding: var(--space-6);
}
.panel .corner {
  position: absolute;
  width: 13px; height: 13px;
  background: none;
  border-radius: 0;
}
.panel .corner::before, .panel .corner::after { content: ""; position: absolute; background: var(--ink); }
.panel .corner::before { left: 6px; top: 0; width: 1px; height: 13px; }
.panel .corner::after { top: 6px; left: 0; width: 13px; height: 1px; }
.panel .corner.tl { top: -7px; left: -7px; }
.panel .corner.tr { top: -7px; right: -7px; }
.panel .corner.bl { bottom: -7px; left: -7px; }
.panel .corner.br { bottom: -7px; right: -7px; }

/* -- Metric: big Bebas number in a bordered label box -- */
/* `min(200px, 100%)` rather than a bare 160px: a fixed track minimum wider than
   its container is itself an overflow, and a 160px cell cannot hold
   "54.9-76.2%" at display size. Four cells still fit a 1200px panel; a phone
   gets one full-width cell per metric. */
.metric-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr)); gap: var(--space-4); }
.metric {
  position: relative;
  border: 1px solid var(--line-strong);
  padding: var(--space-4) var(--space-5);
  background: transparent;
}
.metric::before {
  content: "";
  position: absolute;
  top: 6px; right: 6px;
  width: 10px; height: 10px;
  border: 1px solid var(--line-strong);
}
.metric .value {
  font-family: var(--font-display);
  /* Ranged values ("7.5-39.3%", "0 / 1,536") are the widest strings on the
     project pages; 2.6rem is kept as the cap, so 1440 is unchanged. 1.9rem
     (30px) stays display size, so --accent's 3.6:1 on paper still passes. */
  font-size: clamp(1.9rem, 4.6vw, 2.6rem);
  line-height: 0.95;
  color: var(--accent);
  letter-spacing: 0.02em;
  overflow-wrap: anywhere;
}
.metric .label {
  font-family: var(--font-mono);
  overflow-wrap: anywhere;
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-top: var(--space-2);
}

/* -- Skills matrix: ref 3's grid of thin-bordered panels, each with a bracketed index
      and a small pictogram in its corner -- */
.skill-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--line-strong);
  border: 1px solid var(--line-strong);
}
.skill-group {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--ground);
  padding: 1.6rem 1.4rem 1.5rem;
  min-height: 210px;
}
/* Grid rows stretch to the tallest cell, so a 4-tag panel next to a 6-tag one
   ends with slack. Bottom-aligning the tags moved that slack from the foot of
   the cell to its MIDDLE, where a ~100px band between the discipline title and
   its own tags read as a hole and detached the heading from its content. The
   tags go back under their heading; the slack goes back to the foot, where a
   hairline ruled across every cell at the same height makes it deliberate. */
.skill-group .chip-row { margin-bottom: var(--space-6); }
.skill-group::before {
  content: "";
  position: absolute;
  left: 1.4rem; right: 1.4rem; bottom: 1.5rem;
  height: 1px;
  background: var(--line);
}
/* The [01]–[06] index is markup (<span class="mark" aria-hidden="true">),
   not a counter, so it never reaches the accessibility tree or textContent. */
.skill-group > .mark { display: block; font-size: 0.66rem; letter-spacing: 0.14em; }
.skill-group::after { /* pictogram: sparkle on odd cells, ring on even */
  content: "";
  position: absolute;
  top: 1.2rem; right: 1.2rem;
  width: 18px; height: 18px;
  background: var(--ink-3);
  -webkit-mask: var(--sparkle) center / contain no-repeat;
  mask: var(--sparkle) center / contain no-repeat;
}
.skill-group:nth-child(even)::after {
  background: none;
  border: 1px solid var(--ink-3);
  border-radius: 50%;
  -webkit-mask: none;
  mask: none;
}
.skill-group:nth-child(3n)::after {
  background: none;
  border: 1px solid var(--ink-3);
  border-radius: 50%;
  -webkit-mask: none;
  mask: none;
  width: 22px; height: 12px;
  transform: rotate(-20deg);
}
.skill-group h3 {
  font-family: var(--font-display);
  font-size: 1.6rem;
  letter-spacing: 0.04em;
  color: var(--ink);
  margin: 0.4rem 0 1rem;
  text-transform: uppercase;
}
.skill-group .chip-row { gap: 0.4rem; }

/* -- Numbered list (project pages): dashed rules, Bebas ordinals --
   The ordinal is MARKUP (<span class="li-num" aria-hidden="true">01</span>),
   not a CSS counter, for the same reason .eyebrow .mark is: generated content
   is announced. On an <ol> the counter fired alongside the list's own native
   numbering and a screen reader read "item 1 ... oh-one ... Build and secure".
   aria-hidden markup leaves the native numbering as the only thing announced. */
.numbered-list { display: flex; flex-direction: column; }
.numbered-list li {
  display: flex;
  /* The row is a flex container and its text is an anonymous flex item, whose
     automatic minimum size is its longest word - "(Elasticsearch/Kibana/
     Filebeat)" is wider than a phone column. `anywhere`, not `break-word`, is
     what actually lowers that minimum. */
  overflow-wrap: anywhere;
  gap: var(--space-5);
  padding: var(--space-4) 0;
  border-top: 1px dashed var(--line-strong);
  background: transparent;
  color: var(--ink-2);
}
.numbered-list li:last-child { border-bottom: 1px dashed var(--line-strong); }
.numbered-list .li-num {
  font-family: var(--font-display);
  font-size: 1.7rem;
  line-height: 1;
  color: var(--accent);
  flex: none;
  min-width: 2ch;
  user-select: none;
}

/* -- Filter bar (labs page): square mono toggles; active = ink block -- */
.filter-bar { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-8); }
.filter-btn {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 0.6em 1em 0.5em;
  border: 1px solid var(--line-strong);
  border-radius: 0;
  color: var(--ink-2);
  line-height: 1;
}
.filter-btn:hover { color: var(--ink); border-color: var(--ink); }
.filter-btn.active { color: var(--ground); background: var(--ink); border-color: var(--ink); }

/* -- Callout: the dashed quote box from ref 2. .info gets a solid teal bar. -- */
.callout {
  border: 1px dashed var(--line-strong);
  background: transparent;
  padding: var(--space-5) var(--space-6);
  font-size: 0.92rem;
  position: relative;
  /* The project pages spell this as <p class="callout info">, so the colour has
     to live on the box itself and not only on a child <p>. */
  color: var(--ink);
}
/* The bar follows the section's one accent instead of bringing teal onto a gold
   page as a second hue. */
.callout.info { border-style: solid; box-shadow: inset 5px 0 0 var(--accent-solid); padding-left: calc(var(--space-6) + 5px); }
.callout p { color: var(--ink); margin: 0; max-width: none; }
.callout strong { color: inherit; }

/* -- Stamp: rotated double-ruled rubber stamp -- */
.stamp {
  display: inline-flex;
  align-items: center;
  transform: rotate(-6deg);
  border: 2px solid currentColor;
  box-shadow: inset 0 0 0 2px transparent, inset 0 0 0 3px currentColor;
  border-radius: 0;
  padding: 0.45em 0.8em 0.35em;
  background: transparent;
  color: var(--accent-text);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.66rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  line-height: 1;
  opacity: 0.92;
}
.card .stamp, .subject-card .stamp { color: var(--gold-paper-text); }
/* Display-size stamp (Bebas) for the hero subject card's "ON FILE" mark */
.stamp-lg {
  font-family: var(--font-display);
  font-size: 1.15rem;
  letter-spacing: 0.16em;
  padding: 0.2em 0.55em 0.1em;
  color: var(--gold-paper);
}

/* -- Footer: black, checker divider above it, an Old London watermark of the
      full name behind the small print, and a micro row of end-of-file data -- */
.site-footer {
  position: relative;
  overflow: hidden;
  border-top: 1px dashed var(--line-strong);
  padding: var(--space-12) 0 var(--space-16);   /* ~64px of clearance under the watermark so it signs rather than smears */
  margin-top: 0;
  background: var(--black);
}
/* The footer is a black ground like .sec-black, so it carries the same single
   grain + scan-line layer. It used to get its texture from the global veil. */
.site-footer::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(to bottom, rgba(243, 241, 236, 0.045) 0 1px, transparent 1px 3px),
    var(--grain) 0 0 / 320px 320px repeat;
  opacity: 0.34;
  z-index: 0;
}
/* Specificity note: this has to out-rank `.dev-mark.wm` (which is declared later
   in the devices section at 22rem). At that size the full name is ~2030px wide
   and gets clipped at BOTH ends plus the bottom, so it reads as a smear; at
   13vw it fits the page and signs it. line-height 1.5 keeps the "J" descender
   inside the box, and the footer's padding-bottom gives it ~64px of clearance. */
.site-footer .dev-mark.footer-wm {
  position: absolute;
  left: 50%;
  bottom: 6px;
  line-height: 1.5;
  transform: translateX(-50%);
  font-size: clamp(4rem, 13vw, 12rem);
  color: var(--white);
  opacity: 0.05;
  white-space: nowrap;
  z-index: 0;
}
.site-footer .container {
  position: relative;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
}
.footer-note { font-family: var(--font-mono); font-size: 0.72rem; color: var(--dim); max-width: 60ch; line-height: 1.6; }
.footer-links { display: flex; gap: var(--space-6); font-family: var(--font-mono); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; }
.footer-links a { color: var(--muted); }
.footer-links a:hover { color: var(--white); }
.footer-links a:hover::before { content: "[ " / ""; color: var(--gold); }
.footer-links a:hover::after { content: " ]" / ""; color: var(--gold); }
/* 18px tall -> 26px: WCAG 2.2 target size on desktop as well as on the phone. */
.footer-links a { display: inline-flex; align-items: center; min-height: 44px; }
.footer-meta { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-4); width: 100%; justify-content: space-between; border-top: 1px dashed var(--line); padding-top: var(--space-5); }
.footer-meta .dev-barcode { width: 96px; height: 22px; }

/* Micro caption: the smallest text on the page. Always knocks out its ground
   so neither the film grain nor a texture underneath softens it. */
.micro {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-3);
  background: var(--ground);
  line-height: 1.4;
}

/* ---------- 6. Decorative devices (mark them aria-hidden="true" in markup) ---------- */
:root {
  --sparkle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 0C12 7.5 16.5 12 24 12 16.5 12 12 16.5 12 24 12 16.5 7.5 12 0 12 7.5 12 12 7.5 12 0Z'/%3E%3C/svg%3E");
}

/* Corner captions (ref 2: TIME / TAKES / US / ALL; ref 13) */
.dev-cap {
  position: absolute;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.85;
  white-space: nowrap;
  z-index: 3;
  pointer-events: none;
}
.dev-cap.tl { top: 14px; left: 18px; }
.dev-cap.tr { top: 14px; right: 18px; }
.dev-cap.bl { bottom: 14px; left: 18px; }
.dev-cap.br { bottom: 14px; right: 18px; }
/* section-level caps sit inside the section padding, offset for paper tears.
   The bare-element selectors let an un-classed <section> carry the same four
   captions; `.sec >` is more specific, so the ground-aware offsets still win. */
section > .dev-cap.tl, section > .dev-cap.tr { top: calc(var(--tear-h) + 12px); }
section > .dev-cap.bl, section > .dev-cap.br { bottom: calc(var(--tear-h) + 12px); }
.sec > .dev-cap.tl, .sec > .dev-cap.tr { top: calc(var(--tear-h) + 12px); }
.sec > .dev-cap.bl, .sec > .dev-cap.br { bottom: calc(var(--tear-h) + 12px); }
.sec-black > .dev-cap.tl, .sec-black > .dev-cap.tr { top: calc(var(--tear-h) + 14px); } /* clear the paper section torn over it */
.sec-black > .dev-cap.bl, .sec-black > .dev-cap.br { bottom: calc(var(--tear-h) + 14px); }
/* Every caption at this size knocks out its ground, so the film grain, a
   halftone patch or a texture underneath can never soften it. */
.dev-cap { background: var(--ground); padding: 3px 6px; }

/* Vertical rotated text along an edge (refs 3, 8) */
.dev-vtext {
  position: absolute;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--ink-3);
  white-space: nowrap;
  pointer-events: none;
  z-index: 3;
  background: var(--ground);
  padding: 6px 3px;
}
.dev-vtext.right { right: 18px; top: 50%; transform: rotate(180deg) translateY(50%); }
.dev-vtext.left { left: 18px; top: 50%; transform: rotate(180deg) translateY(50%); }
.dev-vtext.up { transform: none; }

/* Stacked / repeated word watermark (refs 10, 12): first line solid, then outlined, fading */
.dev-stack {
  position: absolute;
  font-family: var(--font-display);
  text-transform: uppercase;
  line-height: 0.82;
  pointer-events: none;
  user-select: none;
  z-index: 0;
  color: var(--ink);
  -webkit-mask-image: linear-gradient(to bottom, #000 30%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 30%, transparent 100%);
}
.dev-stack span { display: block; }
.dev-stack span:nth-child(1) { opacity: 0.16; }
.dev-stack span:nth-child(2) { opacity: 0.45; color: transparent; -webkit-text-stroke: 1px var(--ink); }
.dev-stack span:nth-child(3) { opacity: 0.09; }
.dev-stack span:nth-child(4) { opacity: 0.22; color: transparent; -webkit-text-stroke: 1px var(--ink); }

/* Mirrored reflection under a display line (ref 14) */
.dev-reflect {
  display: block;
  font-family: var(--font-display);
  text-transform: uppercase;
  line-height: 0.86;
  transform: scaleY(-1);
  color: var(--ink);
  opacity: 0.32;
  -webkit-mask-image: linear-gradient(to top, #000 0%, transparent 85%);
  mask-image: linear-gradient(to top, #000 0%, transparent 85%);
  pointer-events: none;
  user-select: none;
  margin-top: -0.08em;
}

/* Thin rings (refs 2, 8) */
.dev-ring { position: absolute; border: 1px solid var(--line-strong); border-radius: 50%; pointer-events: none; }

/* Four-point sparkle (refs 4, 8, 12) */
.dev-sparkle {
  position: absolute;
  width: 20px; height: 20px;
  background: var(--accent);
  -webkit-mask: var(--sparkle) center / contain no-repeat;
  mask: var(--sparkle) center / contain no-repeat;
  pointer-events: none;
}

/* Crosshair */
.dev-xhair { position: absolute; width: 22px; height: 22px; pointer-events: none; }
.dev-xhair::before, .dev-xhair::after { content: ""; position: absolute; background: var(--line-strong); }
.dev-xhair::before { left: 50%; top: 0; width: 1px; height: 100%; }
.dev-xhair::after { top: 50%; left: 0; height: 1px; width: 100%; }

/* Barcode (refs 3, 13): three layered stripe rhythms read as varied bars */
.dev-barcode {
  display: inline-block;
  width: 118px; height: 30px;
  background:
    repeating-linear-gradient(90deg, currentColor 0 2px, transparent 2px 5px),
    repeating-linear-gradient(90deg, currentColor 0 1px, transparent 1px 8px),
    repeating-linear-gradient(90deg, currentColor 0 3px, transparent 3px 13px);
  color: var(--ink-2);
  opacity: 0.9;
}
.dev-barcode.xs { width: 44px; height: 12px; }
.dev-barcode.tall { width: 118px; height: 46px; }

/* Hatch strip (ref 8's ////////) */
.dev-hatch {
  display: inline-block;
  width: 120px; height: 10px;
  background: repeating-linear-gradient(-55deg, currentColor 0 2px, transparent 2px 7px);
  color: var(--accent);
}

/* Halftone dot patch (refs 3, 8): fades from the top-right corner */
.dev-halftone {
  position: absolute;
  width: 260px; height: 260px;
  background: var(--ink);
  -webkit-mask: url('/assets/img/halftone.svg') center / contain no-repeat;
  mask: url('/assets/img/halftone.svg') center / contain no-repeat;
  pointer-events: none;
  opacity: 0.5;
}
.dev-halftone.flip { transform: scaleX(-1); }
.dev-halftone.flip-y { transform: scaleY(-1); }
.dev-halftone.flip-xy { transform: scale(-1, -1); }

/* Colour-swatch strip (ref 3): gray steps + one accent */
.dev-swatch { display: inline-flex; gap: 4px; }
.dev-swatch i { width: 14px; height: 14px; display: block; border: 1px solid var(--line-strong); }
.dev-swatch i:nth-child(1) { background: var(--white); }
.dev-swatch i:nth-child(2) { background: var(--muted); }
.dev-swatch i:nth-child(3) { background: var(--dim); }
.dev-swatch i:nth-child(4) { background: #3a3a3c; }
.dev-swatch i:nth-child(5) { background: var(--black); }
.dev-swatch i:nth-child(6) { background: var(--accent-solid); }

/* Fold creases across the poster frame (ref 2) */
.dev-crease { position: absolute; pointer-events: none; z-index: 1; }
.dev-crease.v { top: 0; bottom: 0; left: 50%; width: 3px; background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.13), transparent); }
.dev-crease.h { left: 0; right: 0; top: 48%; height: 3px; background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.11), transparent); }

/* Bordered label box (ref 8's "John 19:30", ref 12's "ROMANS 8:17") */
.dev-label {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink);
  border: 1px solid var(--ink);
  padding: 0.45em 0.8em 0.35em;
  background: var(--ground);
  line-height: 1;
  white-space: nowrap;
}
.dev-label.pill { border-radius: 999px; } /* the one allowed pill: ref 12's ROMANS 8:17 ellipse. decoration only */

/* Ring drawn around a word (ref 11's "LUKE 22:42") — wrap the word in .ringed */
.ringed { position: relative; display: inline-block; }
.ringed::after {
  content: "";
  position: absolute;
  /* The vertical radius has to clear the x-height or the stroke crosses the stems. */
  inset: -0.78em -0.7em -0.72em -0.6em;
  border: 1.5px solid var(--accent);
  border-radius: 50%;
  transform: rotate(-3deg);
  pointer-events: none;
}

/* Checkered strip divider (refs 7, 8) — sits above the footer. Two offset rows,
   not one: a single row of squares reads as a dashed rule, not a checkerboard. */
/* Full-bleed, this strip was the brightest object on the lower half of the page
   and pulled the eye onto pure decoration right before the sign-off. Inset to
   the text column and run at ~36% of it from the left margin, it reads as a
   filing device on the same axis as the footer note instead of as a band. */
.dev-checker {
  height: 24px;
  width: min(38%, 420px);
  margin-left: max(var(--space-6), calc((100% - var(--max-width)) / 2 + var(--space-6)));
  background: repeating-conic-gradient(var(--white) 0 25%, transparent 0 50%) 0 0 / 24px 24px;
  opacity: 0.55;
}

/* Thin overlapping rectangle outlines (ref 4) */
.dev-rect { position: absolute; border: 1px solid var(--line-strong); pointer-events: none; }

/* Redaction bar (ref 13) — a struck-out reference number, decoration only */
.redact { display: inline-block; height: 0.75em; width: 3.4em; background: var(--paper-ink); vertical-align: middle; }

/* Outlined Bebas roman numeral at the right of a section head (black grounds).
   Pairs with — never appears alongside — the blackletter .dev-mark. */
.dev-num {
  position: relative;
  font-family: var(--font-display);
  font-size: clamp(3rem, 6.4vw, 6rem);
  line-height: 0.78;
  letter-spacing: 0.14em;   /* roman numerals need air or III reads as bars */
  color: var(--ink-3);
  flex: none;
  user-select: none;
  pointer-events: none;
  transform: translateY(-0.06em);
  padding: 0.16em 0.1em 0.14em 0;
  text-shadow: 0 0 24px var(--ground), 0 0 9px var(--ground);
}
/* Hairline top/bottom brackets. Without them an outlined "II" reads as a pause
   glyph and "III" as three stray verticals; bracketed, they read as a count. */
.dev-num::before, .dev-num::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 1px;
  background: var(--line-strong);
}
.dev-num::before { top: 0; }
.dev-num::after { bottom: 0; }
@supports (-webkit-text-stroke: 1px #000) {
  .dev-num { color: transparent; -webkit-text-stroke: 1px var(--line-strong); }
}

/* Arched text on a circle segment (ref 10) — inline SVG over a display line.
   The path's radius is deliberately much larger than half its chord so the
   arc stays a shallow rainbow; textLength matches the arc length so the
   letters spread across the whole span instead of climbing one side. */
/* The arch is sized from the panel it sits on (--trap-w), never from the
   viewport: 0.86 of the panel leaves both of its ends clear of the trapezoid's
   slanted sides at every width, and it is capped at 720 so the desktop
   composition is unchanged. Tying it to the viewport instead is what let the
   panel shrink toward a fixed-width arch between 1200 and 1360. */
.dev-arch { width: min(720px, calc(var(--trap-w, 720px) * 0.86)); margin: 0 auto var(--space-4); overflow: visible; }
.dev-arch text {
  font-family: var(--font-display);
  font-size: 34px;
  letter-spacing: 4px;
  fill: var(--ink-2);
  text-transform: uppercase;
}

/* Old London section mark: blackletter numeral / initial (refs 11, 13) */
.dev-mark {
  font-family: var(--font-black);
  font-size: clamp(3.6rem, 6vw, 6rem);
  line-height: 0.8;
  color: var(--accent);
  user-select: none;
  pointer-events: none;
}
.dev-mark.wm { position: absolute; font-size: clamp(10rem, 24vw, 22rem); color: var(--ink); opacity: 0.06; z-index: 0; }

/* Teal trapezoid panel (ref 14's red trapezoid) */
.dev-trap {
  position: absolute;
  background: var(--teal);
  clip-path: polygon(4% 0, 100% 3%, 94% 100%, 0 96%);
  pointer-events: none;
  z-index: 0;
}
.dev-trap::after { content: ""; position: absolute; inset: 0; background: var(--grain) 0 0 / 320px 320px; opacity: 0.35; }

/* Widely spaced words (ref 14: "Fear   is   a   liar."). The two spans let
   the line break at the sentence boundary instead of mid-sentence. */
.dev-spaced {
  font-family: var(--font-quote);
  font-style: italic;
  font-size: clamp(1rem, 1.6vw, 1.3rem);
  color: var(--ink-2);
  word-spacing: 1.6em;
  letter-spacing: 0.04em;
  pointer-events: none;
}
.dev-spaced span { display: inline-block; }

/* Tape strip on its own */
.dev-tape {
  position: absolute;
  width: 90px; height: 24px;
  background:
    repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.32) 0 2px, transparent 2px 6px),
    var(--tape);
  transform: rotate(-4deg);
  pointer-events: none;
  z-index: 2;
}

/* -- Compositions used by index.html (documented so other pages can borrow them) -- */

/* Only two stacked-word devices exist on the page: ATTACK behind the hero
   (wide screens only) and CASE FILES behind Featured Projects. Never a third. */
/* Held clear of the MMXXVI corner chip (left of it, and low enough that the top
   repetition starts INSIDE the dashed frame), with more stroke contrast and a
   later mask fade so it reads as a repeated word, not a smudge on the ribbon. */
.hero-stack {
  right: 9%; top: 9%;
  font-size: clamp(5rem, 8.4vw, 8.4rem);
  text-align: right;
  line-height: 0.8;
  opacity: 1;
  -webkit-mask-image: linear-gradient(to bottom, #000 58%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 58%, transparent 100%);
}
.hero-stack span:nth-child(1) { opacity: 0.2; }
.hero-stack span:nth-child(2) { opacity: 0.62; }
.hero-stack span:nth-child(3) { opacity: 0.13; }
/* Starts below the SEC. IV / P. 5 / 7 caption row so the caption's knockout box
   no longer punches a hole through the first ghost line. */
.projects-stack { right: 0.5%; top: 6.5%; font-size: clamp(3.6rem, 8vw, 8rem); text-align: right; }
.sec > .dev-halftone.ht-tr { top: calc(var(--tear-h) - 10px); right: -30px; }
.sec > .dev-halftone.ht-bl { bottom: calc(var(--tear-h) - 10px); left: -30px; }
.sec-black > .dev-halftone.ht-tr { top: -10px; }
.sec-black > .dev-halftone.ht-bl { bottom: -10px; }
.dev-strip { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-4); margin-top: var(--space-6); }

/* Column anchor: a bordered label over a tall barcode, a hatch bar and one micro
   caption. Drops into the SHORT column of a two-column section so the bottom-left
   quadrant reads as composed rather than unfinished (Certifications, Coursework).
   Decoration only — every line restates a caption already on the page, and the
   whole block is aria-hidden in the markup. Hidden once the columns stack. */
.dev-anchor {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  margin-top: var(--space-10);
  padding-top: var(--space-6);
  border-top: 1px dashed var(--line);
  max-width: 280px;
  pointer-events: none;
  user-select: none;
}
.dev-anchor .dev-barcode.tall { width: 152px; height: 52px; }
.dev-anchor .dev-hatch { width: 152px; height: 12px; }
/* The knockout stays: this is the one .micro that sits on grain AND on the
   halftone dot field, and without --ground behind it the caption measured
   4.26:1 — the only AA shortfall on the page, and the muddiest caption on it. */
.dev-anchor .micro { line-height: 1.6; padding: 1px 4px; margin-left: -4px; }

/* About — the page's one paper section. Mirrored PROOF over two thin
   rectangle outlines (refs 14 + 4), then the framed exhibit. */
.proof-mark {
  position: relative;
  margin: var(--space-8) 0 var(--space-10);
  padding: var(--space-8) 0 var(--space-4);
  font-family: var(--font-quote);
  font-weight: 600;
  font-size: clamp(3rem, 6.4vw, 5.6rem);
  line-height: 0.84;
  text-transform: uppercase;
  letter-spacing: -0.01em;
  pointer-events: none;
  user-select: none;
}
.proof-mark .dev-rect.r1 { left: -6%; top: 0; width: 62%; height: 88%; }
.proof-mark .dev-rect.r2 { left: 22%; top: 26%; width: 58%; height: 84%; }
.mirror-word { position: relative; display: block; color: var(--ink); }
.mirror-word.reflection {
  transform: scaleY(-1);
  opacity: 0.4;
  filter: blur(1px);
  margin-top: -0.05em;
  -webkit-mask-image: linear-gradient(to top, rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0.2) 48%, transparent 80%);
  mask-image: linear-gradient(to top, rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0.2) 48%, transparent 80%);
}

/* Framed exhibit: a halftone-screened texture panel pasted onto the paper,
   with a ring, flipped micro-text and a bordered coordinate + barcode tag. */
.exhibit {
  position: relative;
  border: 1px solid var(--line-strong);
  min-height: 300px;
  overflow: hidden;
  background: var(--black-2) url('/assets/img/silk-mono.webp') center / cover no-repeat;
}
.exhibit::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, rgba(11, 11, 12, 0.9) 1px, transparent 1.6px);
  background-size: 5px 5px;
  mix-blend-mode: multiply;
  opacity: 0.5;
}
.exhibit > * { z-index: 1; }
.exhibit .dev-cap { color: var(--white); background: rgba(11, 11, 12, 0.9); }
.exhibit .dev-cap.tl { top: 12px; left: 12px; }
.exhibit .dev-cap.tr { top: 12px; right: 12px; }
.exhibit .dev-ring { width: 68%; height: 30%; left: 16%; top: 34%; transform: rotate(-18deg); border-color: rgba(243, 241, 236, 0.6); }
.exhibit .dev-sparkle { left: 12%; bottom: 22%; width: 26px; height: 26px; background: var(--gold); }
.exhibit .flip-text {
  position: absolute;
  right: 16px; top: 40%;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--white);
  opacity: 0.75;
  transform: scale(-1, -1);
  max-width: 190px;
  z-index: 1;
}
.exhibit .exhibit-tag {
  position: absolute;
  left: 14px; bottom: 14px;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0.45em 0.7em;
  border: 1px solid var(--white);
  background: var(--black);
  color: var(--white);
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.exhibit .dev-barcode { color: var(--white); }

/* Certifications — gold blackletter "Earned" over the head, two taped scraps */
.blackletter {
  position: relative;
  display: inline-block;
  margin: var(--space-4) 0 var(--space-3);
  font-family: var(--font-black);
  font-size: clamp(3.4rem, 7.4vw, 6.4rem);
  line-height: 0.92;
  color: var(--gold);
  text-transform: none;
  letter-spacing: 0;
  user-select: none;
  pointer-events: none;
}
.blackletter .dev-sparkle { right: -1.1rem; top: 0.1em; width: 22px; height: 22px; }
.certs-grid { align-items: start; }
.cert-cards { gap: var(--space-8); }
.certs .dev-ring.cert-ring { left: -200px; top: 14%; width: 480px; height: 480px; border-style: dashed; opacity: 0.75; }

/* Featured projects — the page's single coloured moment. The blue liquid runs
   full-bleed under a black veil that stays ≥0.86 alpha wherever real text sits
   and only opens up across the band the opaque paper cards cover. */
.projects-tex {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: #08131b url('/assets/img/liquid-blue.webp') center / cover no-repeat;
  pointer-events: none;
}
.projects-tex::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    var(--black) 0%,
    rgba(11, 11, 12, 0.93) 22%,
    rgba(11, 11, 12, 0.86) 31%,
    rgba(11, 11, 12, 0.26) 44%,
    rgba(11, 11, 12, 0.26) 76%,
    rgba(11, 11, 12, 0.9) 87%,
    var(--black) 100%);
}
.projects .card { --halftone-ink: rgba(11, 11, 12, 0.75); }
/* The head is held to the left so the CASE FILES stack owns the right corner */
.projects .section-head.with-num { max-width: 760px; }
.see-all a {
  display: inline-block;
  padding: 0.7rem 1rem;
  border: 1px solid var(--line-strong);
  background: rgba(11, 11, 12, 0.92);
  font-size: 0.8rem;
  text-decoration: none;
}
.see-all a:hover { border-color: var(--ink); background: var(--ink); color: var(--ground); }

/* Coursework — head on the left, the dashed EXHIBIT record panel on the right */
.course-grid { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: clamp(2rem, 5vw, 4.5rem); align-items: start; }
/* text-wrap:balance stops "…COURSEWORK, BY" hanging a two-letter word at the
   end of line 2 at poster scale. */
.course-grid h2 { font-size: clamp(2.2rem, 3.9vw, 3.7rem); text-wrap: balance; }
.record-panel { position: relative; border-style: dashed; padding: var(--space-8) var(--space-6); background: rgba(17, 17, 19, 0.55); }
.record-grid { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-6); align-items: start; }
.record-meta { display: flex; flex-direction: column; gap: var(--space-4); align-items: flex-start; }
.record-meta .dev-hatch { width: 84px; }
.record-panel p { font-size: 0.88rem; }

/* Contact — arched name over the mirrored LET'S TALK on the teal trapezoid.

   GEOMETRY, one rule for every width. The panel is a child of .contact-stage,
   the block that holds the arch, the eyebrow, the headline and its reflection,
   so it is measured against that block instead of against the section:
     top    — 14px ABOVE the arch's own box, so the crown of the arc can never
              reach the slanted top edge however the arch is scaled;
     bottom — the foot of the reflection, so the slanted bottom edge can never
              cross the headline however large the headline is;
     width  — min(920px, 64vw), the width it always had, with the arch scaled
              from it rather than from the viewport.
   The two blocking defects (arch sliced 1200-1330, headline sliced 601-700)
   were both a fixed length on one element meeting a proportional length on the
   other; nothing here is proportional to anything but the content it wraps. */
.contact {
  text-align: center;
  --trap-w: min(920px, 64vw);
  --trap-bias: 2vw;              /* the panel's rightward bias, as before */
}
.contact-stage { position: relative; }
.contact .dev-trap {
  left: calc(50% - var(--trap-w) / 2 + var(--trap-bias));
  right: auto;
  width: var(--trap-w);
  top: -14px;
  bottom: 0;
  height: auto;
}
/* The panel is a positioned z-index:0 box inside the container's stacking
   context, so the two unpositioned children have to be lifted over it. */
.contact-stage > .dev-arch, .contact-stage > .eyebrow { position: relative; z-index: 2; }
.contact-title { font-size: clamp(4.2rem, 12.4vw, 12.4rem); line-height: 0.84; position: relative; z-index: 2; }
/* The reflection has to die INSIDE the teal — a mirror that stops dead on the
   panel's slanted edge reads as a clip, not as a reflection. The mask ends at
   68% of the mirrored line, ~30px of clear teal above the edge at every width. */
.contact-reflect {
  font-size: clamp(4.2rem, 12.4vw, 12.4rem);
  margin: -0.06em auto 0;
  position: relative;
  z-index: 1;
  -webkit-mask-image: linear-gradient(to top, #000 0%, transparent 68%);
  mask-image: linear-gradient(to top, #000 0%, transparent 68%);
}
.contact .eyebrow { justify-content: center; background: var(--ground); padding: 4px 10px; } /* knockout: the trapezoid runs behind it */
/* Contact has no .section-head to hang a numeral on, so VI is pinned to the
   section's right margin instead — same device, same rules. It must sit fully
   on ONE ground: below 700px the trapezoid is too wide for that, so it goes. */
.contact-num { position: absolute; right: 3.5%; top: 30%; z-index: 1; }
/* The panel now lives inside the container's stacking context, so the section's
   own marks — which are placed to land ON the teal — have to be lifted over it
   or they vanish behind it. Verified clear of the arch's ink and of the
   headline at 1440 / 1200 / 900 / 768 / 700 / 390 before raising them. */
.contact > .dev-sparkle, .contact > .dev-xhair { z-index: 3; }
/* Positions live here, not in inline styles, for the same reason the sparkles'
   do: xh-a rides the panel's upper right, clear of the arched name's last
   letters at every width down to 701; below that the panel goes near-full-bleed
   and there is no clear teal for it, so it goes the way VI does. */
.contact .dev-xhair.xh-a { right: 20%; top: 16%; }
.contact .dev-xhair.xh-b { left: 22%; bottom: 20%; }
/* Sparkle positions live here, not in inline styles, so the phone layout can
   move the first one off the arched name. */
.contact .dev-sparkle.sp-a { left: 14%; top: 18%; width: 26px; height: 26px; }
.contact .dev-sparkle.sp-b { right: 12%; bottom: 22%; width: 16px; height: 16px; }
.contact .dev-spaced { margin-left: auto; margin-right: auto; max-width: none; }
.contact .container { position: relative; z-index: 2; }
/* One ramp instead of three per-band overrides: the closing paragraph keeps the
   same air under the panel's bottom edge at every width (56px at 1440 down to
   24px at 390). Two-class specificity so it beats the .mt-4 utility. */
.contact .contact-copy { position: relative; z-index: 2; margin-top: clamp(1.5rem, 4vw, 3.5rem); }


/* ============================================================
   6b. PAGE RECIPES — the compositions the interior pages are
   built from. Everything here is assembled from the tokens and
   the .dev-* devices above; nothing introduces a new hue, a new
   font or a new external request, and every decorative element
   is meant to be marked aria-hidden="true" in the markup.

   Four page types are covered:
     PAGE FURNITURE  .page-caps, .masthead, .backlink
     PROJECT DETAIL  .case-file, .case-head, .case-meta,
                     .section-head.sub, .case-body + .case-margin +
                     .case-wide, .split-body, .case-rail + .rail-fill,
                     .metric-strip, .panel-tag, .chip-block,
                     .findings, .callout.pull
     CATALOG         .filter-bar (re-cut as a rail), .lab-grid cards,
                     .lab-detail, .catalog-count
     ERROR           .err, .err-tex, .err-stack, .h1-mono
   ============================================================ */

/* ---- A black window cut into a paper scrap (thumbnails). The scrap flips the
   ground tokens to PAPER for its copy; anything drawn INSIDE the photo strip is
   back on black and has to say so, or its hairlines vanish. ---- */
.thumb {
  --ground: var(--black);
  --ground-2: var(--black-2);
  --ink: var(--white);
  --ink-2: var(--muted);
  --ink-3: var(--dim);
  --line: rgba(243, 241, 236, 0.22);
  --line-strong: rgba(243, 241, 236, 0.55);
  --text: var(--white);
  --text-muted: var(--muted);
  --text-dim: var(--dim);
  --bg: var(--black);
  --bg-raised: var(--black-2);
  --bg-panel: var(--black-2);
  --border: rgba(243, 241, 236, 0.22);
  --border-strong: rgba(243, 241, 236, 0.55);
}

/* ---- PAGE-LEVEL CORNER CAPTIONS ------------------------------------------
   The four .dev-cap corners pinned to the whole DOCUMENT rather than to one
   section: top pair at the top of <main>, bottom pair at the very bottom of it.
   Drop it in as the FIRST child of <main>, aria-hidden, four .dev-cap children:

     <div class="page-caps" aria-hidden="true">
       <span class="dev-cap tl">Case no. 01</span>
       <span class="dev-cap tr">MMXXVI</span>
       <span class="dev-cap bl">40.7128° N</span>
       <span class="dev-cap br">74.0060° W</span>
     </div>

   The captions knock out --ground, which at page level is always the BLACK
   ground, so only use it on a page whose first and last sections are black.
   The bottom pair inherits the ≤780px `display:none` from .dev-cap. ---- */
.page-caps {
  position: absolute;
  inset: 0;
  pointer-events: none;
  user-select: none;
  z-index: 6;
}
.page-caps > .dev-cap.tl { top: 20px; left: 20px; }
.page-caps > .dev-cap.tr { top: 20px; right: 20px; }
.page-caps > .dev-cap.bl { bottom: 20px; left: 20px; }
.page-caps > .dev-cap.br { bottom: 20px; right: 20px; }

/* ---- INTERIOR-PAGE MASTHEAD ----------------------------------------------
   The head of projects.html / labs.html: eyebrow, poster h1 with exactly one
   .outline word, a lead paragraph, and a decoration column on the right. It is
   deliberately a size smaller than the home hero — an index page should not
   out-shout the front page — and it closes on a full-bleed dashed page rule.

     <section class="sec sec-black accent-gold masthead">
       <div class="dev-stack masthead-stack" aria-hidden="true">…×3</div>
       <span class="dev-vtext right" aria-hidden="true">…</span>
       <div class="container">
         <div class="masthead-grid">
           <div>
             <p class="eyebrow"><span class="mark" aria-hidden="true">[01]</span> projects</p>
             <h1>Six things, each with a status you <span class="outline">can trust</span>.</h1>
             <p class="mt-6">…lead…</p>
           </div>
           <div class="masthead-side" aria-hidden="true"><div class="dev-anchor">…</div></div>
         </div>
       </div>
     </section>                                                          ---- */
.masthead {
  /* The 3.4rem floor is clearance for .page-caps: its top pair sits 20px down
     and is ~17px tall, and the eyebrow must not run into it in the tablet band.
     The cap is unchanged, so 1200+ is unaffected. */
  padding-top: clamp(3.4rem, 4.6vw, 4rem);
  padding-bottom: clamp(3rem, 6vw, 5rem);
  border-bottom: 1px dashed var(--line-strong);
}
.masthead-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: end;
}
.masthead h1 {
  margin-top: var(--space-4);
  /* One step under the home hero at every width. */
  font-size: clamp(2.5rem, 7.2vw, 6.4rem);
  max-width: 16ch;
}
.masthead .eyebrow { margin-bottom: var(--space-2); }
.masthead p { max-width: 60ch; font-size: 0.94rem; }
.masthead .lead { max-width: 40ch; }
/* The decoration column is exactly that: it carries a .dev-anchor (bordered
   label over a tall barcode and a hatch bar) and nothing a reader needs. */
.masthead-side { justify-self: end; }
.masthead-side .dev-anchor { margin-top: 0; border-top: none; padding-top: 0; }
/* Ghost word behind the head. AT MOST ONE per page (the .dev-stack budget from
   the home page still applies), anchored to the top-right corner. */
.masthead-stack {
  /* Clear of .dev-vtext.right, which sits at right:18px, is ~22px wide and
     knocks out its ground at z-index 3 — at right:0.5% it sliced the last two
     letters off every repetition. */
  right: 3.6rem;
  top: 8%;
  font-size: clamp(3.2rem, 7.4vw, 7.4rem);
  text-align: right;
}
/* The two catalog mastheads (projects.html, labs.html) were built from exactly
   the same parts and read as twins side by side. ONE axis is varied on the
   Coursework page: the ghost word is set larger and dropped lower, and the
   filing anchor hangs from the FOOT of its column rather than the head. */
.masthead-stack.stack-low { top: 26%; font-size: clamp(3.6rem, 9.2vw, 9.4rem); }
.masthead-side.side-foot { align-self: end; }

/* ---- BACK-TO-INDEX DEVICE -------------------------------------------------
   A bordered mono chip with a dashed rule running off to the right margin —
   the filing-tab you tab back through, not a bare underlined link.

     <p class="backlink-row"><a href="/projects.html" class="backlink">← All projects</a></p>

   The arrow stays in the markup (it is part of the link's text), so nothing is
   generated and the accessible name is unchanged. 30px tall: WCAG 2.5.8. ---- */
.backlink-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-bottom: var(--space-8);
  max-width: none;
}
.backlink-row::after {
  content: "";
  flex: 1 1 auto;
  height: 0;
  border-top: 1px dashed var(--line);
}
.backlink {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  /* 44px and 11.2px, not the old 30/9.9: this is the one way back out of a
     case file and it was the smallest target on the page. */
  min-height: 44px;
  padding: 0.6em 1.1em 0.5em;
  border: 1px solid var(--line-strong);
  background: var(--ground);
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease, transform 0.12s ease;
}
.backlink:hover {
  background: var(--ink);
  color: var(--ground);
  border-color: var(--ink);
  transform: translateX(-3px);
  text-decoration: none;
}

/* ---- PROJECT DETAIL: THE CASE-FILE MASTHEAD -------------------------------
   Eyebrow → poster h1 with ONE outlined word → status stamp on the title's
   baseline → lead → the dossier meta row. The section closes on a page rule,
   the way the .masthead does, so the metric strip below reads as the next
   exhibit rather than as more of the same block.

     <section class="sec sec-black accent-gold case-file">
       <span class="dev-vtext right" aria-hidden="true">…</span>
       <div class="dev-stack case-stack" aria-hidden="true">…×3</div>
       <div class="container">
         <p class="backlink-row"><a href="/projects.html" class="backlink">← All projects</a></p>
         <div class="case-head">
           <p class="eyebrow"><span class="mark" aria-hidden="true">[01]</span> team research — afrl rome research site</p>
           <div class="case-title-row">
             <h1 class="case-title">AFRL_<span class="outline">AIAI</span></h1>
             <span class="stamp stamp-lg">PA cleared</span>
           </div>
           <p class="lead">…</p>
           <dl class="case-meta">…</dl>
         </div>
       </div>
     </section>                                                          ---- */
.case-file {
  padding-top: clamp(3rem, 4vw, 3.4rem);   /* .page-caps clearance — see .masthead */
  padding-bottom: clamp(2.6rem, 5vw, 4.2rem);
  border-bottom: 1px dashed var(--line-strong);
}
.case-head { position: relative; }
.case-title-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3) var(--space-6);
  margin-top: var(--space-4);
}
/* A case number is not a page title: the case h1 runs one step under the
   .masthead h1, which runs one step under the home hero. */
.case-title {
  font-size: clamp(2.3rem, 6.4vw, 5.4rem);
  line-height: 0.86;
  overflow-wrap: anywhere;   /* "S.H.I.E.L.D." and "AFRL_AIAI" are single tokens */
}
.case-file .lead { margin-top: var(--space-5); max-width: 46ch; }
.case-file .stamp { flex: none; }
/* Ghost word behind the case head — one per page, same budget as .dev-stack. */
.case-stack {
  right: 3.6rem;   /* clear of .dev-vtext.right — see .masthead-stack */
  top: 12%;
  font-size: clamp(3rem, 7vw, 7rem);
  text-align: right;
}

/* ---- THE DOSSIER META ROW -------------------------------------------------
   Hairline-connected data cells: CASE NO. / STATUS / FILED / SITE. Same
   construction as the home page's .skill-grid (1px gaps over a --line ground),
   so it reads as one instrument rather than four boxes. Give the cell that
   carries the project's real status `class="is-accent"` on its <dd> — it is the
   one word on the row that should catch the eye, and it must be the status
   VERBATIM (never softened, never strengthened).

     <dl class="case-meta">
       <div><dt>Case no.</dt><dd>01</dd></div>
       <div><dt>Status</dt><dd class="is-accent">In progress</dd></div>
       <div><dt>Filed</dt><dd>MMXXVI</dd></div>
       <div><dt>Site</dt><dd><span class="nb">40.7128° N / 74.0060° W</span></dd></div>
     </dl>

   Wrap a coordinate pair in `.nb` as above, or the cell breaks it across two
   lines in the tablet band.                                                               ---- */
.case-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line-strong);
  margin-top: var(--space-8);
}
.case-meta > div {
  position: relative;
  background: var(--ground);
  padding: var(--space-4) var(--space-5) var(--space-4);
}
.case-meta > div::after {   /* registration square, top-right of each cell */
  content: "";
  position: absolute;
  top: 7px; right: 7px;
  width: 7px; height: 7px;
  border: 1px solid var(--line-strong);
}
.case-meta dt {
  font-family: var(--font-mono);
  font-size: 0.66rem;   /* was 0.58rem / 9.3px — a label a recruiter reads */
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-3);          /* 5.5:1 on black */
  line-height: 1.4;
}
.case-meta dd {
  margin-top: 0.4rem;
  padding-right: 14px;          /* clear of the registration square */
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--ink);            /* 17.4:1 */
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.case-meta dd.is-accent { color: var(--accent-text); }   /* gold: 9:1 on black */

/* ---- SUB-HEAD: an h2 with an outlined roman numeral ------------------------
   The home page's .section-head.with-num at chapter scale. `.sub` steps the
   headline and the numeral down so an in-page h2 sits under the case h1
   instead of competing with it. Numeral markup is unchanged:

     <div class="section-head with-num sub">
       <div><h2>The <span class="outline">problem</span></h2></div>
       <span class="dev-num" aria-hidden="true">I</span>
     </div>                                                              ---- */
.section-head.sub {
  max-width: none;
  margin-bottom: var(--space-5);
  padding-top: var(--space-4);
  align-items: center;
}
.section-head.sub > div { max-width: 46ch; }
.section-head.sub h2 { margin-top: 0; font-size: clamp(1.7rem, 3.1vw, 2.7rem); }
.section-head.sub .dev-num { font-size: clamp(1.9rem, 3.3vw, 3.1rem); }

/* ---- CASE BODY: the narrative column AND ITS MARGIN ------------------------
   The case body used to be one 820px column in a 1152px container, which left
   ~356px of bare ground running the whole second half of every dossier page —
   62% of each page's height with nothing in it but a ghosted numeral. It is a
   TWO-track grid now: the narrative column and the margin beside it, the same
   shape the home page's About section and projects.html's case sections use.

     <div class="case-body">
       <div>…narrative block…</div>                  → track 1
       <aside class="case-margin" aria-hidden="true">…devices…</aside>
                                                     → track 2, SAME ROW
       <div class="chip-row chip-block case-wide">…</div>
                                                     → both tracks, full 1152
     </div>

   Auto-placement does the work: a `.case-margin` written straight after a
   narrative block lands beside it, and a `.case-wide` starts a fresh row. Put
   at least two `.case-wide` blocks on a page so the grid never looks like it
   changed its mind halfway down. A `.case-margin` that is aria-hidden is pure
   decoration and disappears when the page goes single-column; one carrying
   real copy (an AI-use note on a paper scrap, a pull quote) stacks instead. */
.case-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, clamp(240px, 24vw, 330px));
  column-gap: clamp(1.6rem, 3.4vw, 3.2rem);
  row-gap: clamp(2.4rem, 5vw, 3.6rem);
  max-width: none;
  align-items: start;
}
.case-body > * { grid-column: 1; min-width: 0; }
.case-body > .case-wide { grid-column: 1 / -1; }
.case-body > .case-margin { grid-column: 2; }
.case-body > div { position: relative; }
.case-body h2 { font-size: clamp(1.7rem, 3.1vw, 2.7rem); }
/* The 70ch monospace measure is already at the top of the comfortable range,
   so the narrative NEVER widens — only the furniture around it does. */
.case-body p { max-width: 70ch; }

/* The margin column: a stack of devices (or one paper scrap) beside a block. */
/* The margin stretches to the row so its last child can take up the slack: a
   long narrative block (SHIELD's six-phase lifecycle) is otherwise 700px taller
   than the plate beside it and the gutter goes dead again below the plate. */
.case-margin {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  align-self: stretch;
  gap: var(--space-6);
}
.case-margin > * { flex: none; }
.case-margin > .rail-fill { flex: 1 1 auto; }
.case-margin > .dev-anchor { margin-top: 0; border-top: none; padding-top: 0; max-width: none; }
.case-margin > .exhibit { min-height: clamp(220px, 26vw, 320px); }
/* A margin whose ROW is set by a short narrative block: a fixed 320px plate
   beside a five-line paragraph drove a ~270px void down the text column next to
   it (ChuckSpot, THE PROBLEM). Here the plate is the elastic member instead, so
   the row closes at the paragraph's own depth. */
.case-margin.margin-fit { justify-content: stretch; }
.case-margin.margin-fit > .exhibit { flex: 1 1 auto; min-height: clamp(140px, 13vw, 168px); }
.case-margin .card { padding: 2rem 1.2rem 1.2rem; align-self: flex-start; }
.case-margin .card p { font-size: 0.82rem; line-height: 1.62; }
/* A halftone patch is absolutely positioned by default; in the margin it is a
   block of its own, clipped to the track so it cannot widen the page. */
.case-margin > .dev-plate {
  position: relative;
  height: clamp(130px, 15vw, 180px);
  overflow: hidden;
  border-top: 1px dashed var(--line);
  padding-top: var(--space-4);
}
.case-margin > .dev-plate > .dev-halftone { top: -46px; right: -84px; width: 300px; height: 300px; opacity: 0.4; }
.case-margin > .dev-plate > .dev-barcode { position: relative; z-index: 1; }
.case-margin > .dev-plate > .micro { position: absolute; left: 0; bottom: 0; z-index: 1; }

/* A full-measure findings box splits into narrative + margin internally, so a
   1152px exhibit never contains a 643px paragraph and 500px of nothing. */
.split-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, clamp(240px, 24vw, 330px));
  column-gap: clamp(1.6rem, 3.4vw, 3.2rem);
  row-gap: var(--space-8);
  align-items: start;
}
.split-body > * { min-width: 0; }
.split-body > .split-margin { display: flex; flex-direction: column; gap: var(--space-5); align-items: stretch; }
.split-body > .split-margin > .dev-anchor { margin-top: 0; border-top: none; padding-top: 0; max-width: none; }

/* ---- THE CASE RAIL (projects.html's inline case sections) -----------------
   Those three sections put a tall paper scrap in a 1.15fr column and ONE short
   device in the 1fr column beside it, so the right ~45% of the viewport went
   dead for 480–1,040px of continuous scroll each. The rail is the fix: the
   black column now carries a stack that runs the full height of the scrap.

     <div class="grid two-col-60-40 case-split">
       <div class="card">…the case…</div>
       <div class="case-rail" aria-hidden="true">
         <div class="exhibit">…</div>
         <div class="dev-anchor">…</div>
         <div class="rail-fill"><span class="dev-halftone"></span>…</div>
       </div>
     </div>

   `.rail-fill` is the elastic member: it takes whatever height is left over so
   the rail always reaches the foot of the scrap, however long the copy runs.
   The scrap itself opts out of the row stretch — a paper scrap grown taller
   than its copy is just blank paper.                                     ---- */
.case-split { align-items: stretch; }
.case-split > .card { align-self: start; }
.case-rail {
  display: flex;
  flex-direction: column;
  gap: clamp(1.4rem, 3vw, 2.4rem);
  align-self: stretch;
  min-width: 0;
}
.case-rail > * { flex: none; }
.case-rail > .dev-anchor { margin-top: 0; border-top: 1px dashed var(--line); max-width: none; }
.case-rail > .exhibit { flex: none; }
.rail-fill {
  position: relative;
  flex: 1 1 auto;
  min-height: 150px;
  overflow: hidden;
  border-left: 1px dashed var(--line);   /* the spine down the empty ground */
}
/* The dot field that fills whatever height is left over, rather than a
   fixed-size patch that leaves the bottom of a tall rail bare. It is MASKED,
   never painted over the section: an opaque ground laid over .sec-black::before
   covers the film grain and the whole rail reads as a darker rectangle with a
   hard edge. The spine is a border and the barcode is ::after, so neither is
   caught by the mask. */
.rail-fill::before {
  content: "";
  position: absolute;
  inset: 0 0 34px 0;
  background-image: radial-gradient(circle, var(--ink) 1.05px, transparent 1.7px);
  background-size: 7px 7px;
  opacity: 0.5;
  -webkit-mask-image: linear-gradient(196deg, #000 0%, rgba(0, 0, 0, 0.5) 38%, transparent 74%);
  mask-image: linear-gradient(196deg, #000 0%, rgba(0, 0, 0, 0.5) 38%, transparent 74%);
  pointer-events: none;
}
/* A rotated edge caption riding the spine — the one mark with weight in the
   middle of a long rail, so the elastic member is not dots alone. */
/* Centred, not pinned at 44%: once the rail carries real payload the elastic
   member is ~230-310px tall, and a caption anchored near the top ran off the
   bottom edge and lost its first half to the clip. */
.rail-fill > .dev-vtext {
  left: var(--space-5);
  right: auto;
  top: 50%;
  transform: translateY(-50%) rotate(180deg);
}
/* Swatch strip filed in the bottom corner: the mark that closes the rail, so
   the foot of a long case does not fade out into dots alone. */
.rail-fill > .dev-swatch { position: absolute; right: 2px; bottom: 26%; }
.rail-fill::after {    /* a barcode filed in the foot of the rail, right-hand
                          side: the spine already holds the left edge, and a
                          mark out at the far side is what actually closes the
                          gutter rather than hugging the text column. */
  content: "";
  position: absolute;
  right: 0; bottom: 0;
  width: 92px; height: 22px;
  background:
    repeating-linear-gradient(90deg, currentColor 0 2px, transparent 2px 5px),
    repeating-linear-gradient(90deg, currentColor 0 1px, transparent 1px 8px);
  color: var(--ink-3);
  opacity: 0.85;
}
.rail-fill > .dev-xhair { left: -10px; bottom: 46%; }
.rail-fill > .micro { position: absolute; left: var(--space-5); top: 0; }
.rail-fill > .dev-xhair { left: -11px; }

/* RAIL PAYLOAD. `.rail-fill` is elastic, so a rail whose fixed furniture is
   short leaves the whole second column to the dot field: projects.html §III and
   §IV measured 631px and 765px of it — half the screen, empty, for most of a
   laptop viewport of scroll. The cure is furniture, not a taller patch, so the
   two sections put their REAL instrument in the rail (the verification-pass
   metric band; the tech stack) instead of a full-width strip above the case.
   Both are fielded for a ~330px track: one metric per row, chips wrapping. */
.case-rail > .metric-strip,
.case-rail > .chip-block { flex: none; }
.case-rail .metric-strip .metric-row { grid-template-columns: minmax(0, 1fr); }
.case-rail .metric-strip .metric { padding: var(--space-5) var(--space-5) var(--space-4); }
.case-rail .metric-strip .metric .value { font-size: clamp(1.7rem, 2.6vw, 2.2rem); }
.case-rail > .chip-block { padding: var(--space-4) var(--space-4) var(--space-3); }

/* MIRRORED CASE. One section in the run flips so the scrap sits right and the
   rail left; the DOM order stays narrative-then-instrument, and the swap is
   reset the moment the grid is a single column (below 780px) so a phone still
   reads the case before its margin. */
.case-split.case-mirror { grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); }
.case-split.case-mirror > .card { order: 2; }
.case-split.case-mirror > .case-rail { order: 1; }
/* Mirrored, the spine belongs on the side that faces the scrap. */
.case-split.case-mirror .rail-fill { border-left: none; border-right: 1px dashed var(--line); }
.case-split.case-mirror .rail-fill > .dev-vtext { left: auto; right: var(--space-5); }
.case-split.case-mirror .rail-fill > .micro { left: auto; right: var(--space-5); }
.case-split.case-mirror .rail-fill > .dev-xhair { left: auto; right: -11px; }
.case-split.case-mirror .rail-fill > .dev-swatch { right: auto; left: 2px; }
.case-split.case-mirror .rail-fill::after { right: auto; left: 0; }

/* ---- METRIC STRIP ---------------------------------------------------------
   .metric-row / .metric are unchanged in markup. Two dressings:

     <div class="panel">…<div class="metric-row">…      separate data cells
     <div class="panel metric-strip">…<div class="metric-row">…   one band

   `.metric-strip` collapses the panel's padding and hairlines the cells
   together, so four numbers read as one instrument across the page. Add a
   `<span class="panel-tag" aria-hidden="true">Exhibit A</span>` to sit the
   exhibit letter on the panel's top border.                            ---- */
/* Accent tick on the top-left corner of every cell — the one accent mark this
   band gets. Decoration only; no generated text. */
.metric::after {
  content: "";
  position: absolute;
  top: -1px; left: -1px;
  width: 26px; height: 3px;
  background: var(--accent-solid);
}
.metric .label {
  border-top: 1px dashed var(--line);
  padding-top: var(--space-3);
}
.panel.metric-strip { padding: 0; border-color: var(--line-strong); }
/* .metric-strip has no padding for a tab to straddle, so the exhibit label
   becomes the band's header rail. Put it BEFORE .metric-row in the markup. */
.metric-strip > .panel-tag {
  position: static;
  display: block;
  transform: none;
  background: none;
  padding: var(--space-3) var(--space-5) calc(var(--space-3) - 1px);
  border-bottom: 1px solid var(--line-strong);
}
.metric-strip .metric-row { gap: 1px; background: var(--line-strong); }
.metric-strip .metric {
  border: none;
  background: var(--ground);
  padding: var(--space-6) var(--space-5) var(--space-5);
}
.metric-strip .metric::before { top: 10px; right: 10px; }

/* Exhibit label straddling a .panel's top border (the file-folder tab). */
.panel-tag {
  position: absolute;
  top: 0;
  left: var(--space-6);
  transform: translateY(-50%);
  background: var(--ground);
  padding: 0 0.6em;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-3);
  line-height: 1.2;
  white-space: nowrap;
  pointer-events: none;
}

/* ---- TECH-STACK CHIP BLOCK ------------------------------------------------
   The same .chip-row, fielded: a hairline box with the section's accent down
   its left edge and a hatch tick in the top-right corner, so a stack list reads
   as a specimen sheet instead of a loose row of tags.

     <div class="chip-row chip-block"> <span class="chip">…</span> … </div>  */
.chip-block {
  position: relative;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-5) var(--space-4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent-solid);
  background: var(--ground-2);
}
.chip-block::after {
  content: "";
  position: absolute;
  top: 8px; right: 8px;
  width: 46px; height: 9px;
  background: repeating-linear-gradient(-55deg, var(--line-strong) 0 2px, transparent 2px 7px);
  pointer-events: none;
}
.chip-block .chip { border-color: var(--line-strong); background: var(--ground); }

/* ---- KEY-FINDINGS BLOCK ---------------------------------------------------
   A hairline evidence box whose h2 straddles the top border and knocks it out —
   the legend on a case exhibit. Takes prose, a .numbered-list, or both.

     <div class="findings">
       <h2>Key <span class="outline">finding</span></h2>
       <p>…</p>
       <ul class="numbered-list mt-4">…</ul>
     </div>                                                              ---- */
.findings {
  position: relative;
  border: 1px solid var(--line-strong);
  padding: 0 clamp(1.1rem, 2.6vw, 2rem) clamp(1.4rem, 2.8vw, 2rem);
}
.findings > h2 {
  display: inline-block;
  margin: -0.42em 0 var(--space-5);
  padding-right: 0.55em;
  background: var(--ground);
  font-size: clamp(1.6rem, 2.9vw, 2.4rem);
  line-height: 1;
}
.findings > h2 + * { margin-top: 0; }
.findings::after {   /* crosshair in the bottom-right corner */
  content: "";
  position: absolute;
  right: 9px; bottom: 9px;
  width: 13px; height: 13px;
  background:
    linear-gradient(var(--line-strong), var(--line-strong)) 50% 0 / 1px 100% no-repeat,
    linear-gradient(var(--line-strong), var(--line-strong)) 0 50% / 100% 1px no-repeat;
  pointer-events: none;
}
.findings .numbered-list li:last-child { border-bottom: none; padding-bottom: 0; }

/* ---- CALLOUT / QUOTE ------------------------------------------------------
   .callout          dashed evidence box (default)
   .callout.info     solid box with the section's accent down the left edge
   .callout.pull     the Playfair pull quote: no box, an accent rule and a
                     four-point sparkle. Use it at most once per page.     ---- */
.callout::after {
  content: "";
  position: absolute;
  right: 9px; bottom: 9px;
  width: 12px; height: 12px;
  background:
    linear-gradient(var(--line-strong), var(--line-strong)) 50% 0 / 1px 100% no-repeat,
    linear-gradient(var(--line-strong), var(--line-strong)) 0 50% / 100% 1px no-repeat;
  opacity: 0.85;
  pointer-events: none;
}
.callout.pull {
  border: none;
  border-left: 2px solid var(--accent-solid);
  padding: var(--space-2) var(--space-8) var(--space-2) var(--space-6);
  font-family: var(--font-quote);
  font-style: italic;
  font-weight: 600;
  font-size: clamp(1.15rem, 2vw, 1.6rem);
  line-height: 1.35;
  color: var(--ink);
  max-width: 34ch;
}
.callout.pull p { font: inherit; color: inherit; max-width: none; }
.callout.pull::after {
  right: 0; bottom: auto; top: 0.1em;
  width: 18px; height: 18px;
  background: var(--accent);
  -webkit-mask: var(--sparkle) center / contain no-repeat;
  mask: var(--sparkle) center / contain no-repeat;
  opacity: 1;
}

/* ---- CATALOG: THE FILTER RAIL ---------------------------------------------
   Same contract as before — labs.js writes `.filter-btn` buttons into
   `.filter-bar` and toggles `.active` — but the row is cut as a contact-sheet
   rail rather than a strip of buttons: a hazard tick at the left of the top
   rule, cells sharing single hairlines (negative margins collapse the borders),
   the live cell knocked out to solid ink with a gold register mark, and a
   barcode pinned above the right end. Every cell is ≥44px.   ---- */
.filter-bar {
  position: relative;
  gap: 0;
  margin: var(--space-10) 0 var(--space-10);
  padding: var(--space-5) 0 0;
  border-top: 1px solid var(--line-strong);
}
.filter-bar::before {   /* hazard tick riding the top rule */
  content: "";
  position: absolute;
  top: -2px; left: 0;
  width: 96px; height: 4px;
  background: var(--accent-solid);
}
.filter-bar::after {    /* barcode above the right end of the rail */
  content: "";
  position: absolute;
  right: 0; top: -22px;
  width: 88px; height: 14px;
  background:
    repeating-linear-gradient(90deg, currentColor 0 2px, transparent 2px 5px),
    repeating-linear-gradient(90deg, currentColor 0 1px, transparent 1px 8px);
  color: var(--ink-3);
  opacity: 0.8;
  pointer-events: none;
}
.filter-btn {
  display: inline-flex;
  align-items: center;
  min-height: 44px;           /* WCAG 2.5.8 floor is 24px; 44 is the comfort size */
  margin: 0 -1px -1px 0;      /* collapse adjacent hairlines into one */
  padding: 0.5em 0.95em 0.4em;
  border: 1px solid var(--line);
  background: transparent;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.filter-btn:hover { color: var(--ink); border-color: var(--ink); background: var(--ground-2); position: relative; z-index: 1; }
.filter-btn.active { position: relative; z-index: 2; }
.filter-btn.active::before {   /* the live cell's register mark */
  content: "";
  width: 6px; height: 6px;
  margin-right: 0.7em;
  background: var(--accent-solid);
  flex: none;
}
.filter-btn:focus-visible { position: relative; z-index: 3; }

/* ---- CATALOG: THE DENSE CARD ----------------------------------------------
   ~44 scraps on one page. Two rules keep it cheap: no `filter:` (the two-pass
   drop-shadow was the page's most expensive paint — one box-shadow instead,
   already above), and no blend modes. Everything else is a tightening pass:
   smaller measure, smaller title, the discipline chip re-cut as a filing tag,
   and a status that reads as a stamp rather than a coloured bar.         ---- */
.lab-grid .card { padding: 2rem 1.2rem 1.2rem; }
.lab-grid .card-head { gap: var(--space-2) var(--space-3); margin-bottom: var(--space-2); }
.lab-grid .card h3 { font-size: 1.25rem; line-height: 1.02; letter-spacing: 0.02em; }
.lab-grid .card p { font-size: 0.82rem; line-height: 1.62; margin-bottom: var(--space-3); }
.lab-grid .card .chip-row { gap: 0.34rem; }
.lab-grid .card .chip { font-size: 0.62rem; padding: 0.26em 0.5em 0.18em; }
.lab-grid .card .btn { font-size: 0.62rem; padding: 0.7em 1.1em; }
/* The discipline line labs.js emits as `<p class="chip mt-2">` becomes the
   card's filing tag: accent ink, letter-spaced caps, no fill. 6.3:1. */
.lab-grid .card > .chip {
  border-color: var(--gold-paper-text);
  color: var(--gold-paper-text);
  font-weight: 700;
  font-size: 0.58rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 0.3em 0.5em 0.22em;
}
/* 44 identical "Complete" labels should not shout: on the catalog the status is
   an outlined stamp, not a filled bar. The WORD is untouched. */
.lab-grid .badge-green {
  background: transparent;
  color: var(--scrap-ink-2);        /* 10.5:1 on the scrap */
  border-color: var(--scrap-line);
}
.lab-grid .badge-green::before { background: var(--accent-solid); }
/* The flagship bar reads as a marked file edge rather than a stripe. */
.lab-grid .card-flagship { padding-left: 1.6rem; }
/* The chips and the repo button were 9.3–9.9px — the two layers a recruiter
   actually scans a catalogue by. One step up, and the button clears 44px. */
.lab-grid .card > .chip { font-size: 0.66rem; }
.lab-grid .card .chip { font-size: 0.68rem; }
.lab-grid .card .btn { font-size: 0.68rem; min-height: 44px; padding: 0.7em 1.1em; }

/* ---- CATALOG: THE SUMMARY-FIRST SCRAP -------------------------------------
   Every one of the 44 scraps used to render fully expanded — two to three
   paragraphs each, ~150 words under a 20px title — so the catalogue was
   10,880px at 1440 and 29,764px at 390: roughly 35 phone screens for a
   coursework list, with no collapsed state and nothing scannable above the
   prose. The card is filed in two layers now: LAB NO. + title + discipline +
   status + the one-line result stay open; the write-up goes behind a native
   <details>, so the scan layer is the whole default view and nothing is lost.

   The disclosure marker is a CSS plus that becomes a minus. Drawn, not
   generated text: a `content: "+"` would be announced.                  ---- */
.lab-detail { margin-top: var(--space-4); }
.lab-detail > summary {
  display: flex;
  align-items: center;
  gap: 0.7em;
  min-height: 44px;
  padding: 0.55rem 0 0;
  border-top: 1px dashed var(--scrap-line);
  cursor: pointer;
  list-style: none;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--scrap-ink-2);          /* 10.5:1 on the scrap */
}
.lab-detail > summary::-webkit-details-marker { display: none; }
.lab-detail > summary::after {
  content: "";
  margin-left: auto;
  flex: none;
  width: 11px; height: 11px;
  background:
    linear-gradient(currentColor, currentColor) 50% 0 / 1px 100% no-repeat,
    linear-gradient(currentColor, currentColor) 0 50% / 100% 1px no-repeat;
}
.lab-detail[open] > summary::after {
  background: linear-gradient(currentColor, currentColor) 0 50% / 100% 1px no-repeat;
}
.lab-detail > summary:hover { color: var(--scrap-ink); }
.lab-detail[open] > summary { margin-bottom: var(--space-3); }
.lab-detail > :not(summary):last-child { margin-bottom: 0; }

/* The catalogue's running scale, and the live count the filter rewrites. */
.catalog-count {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-2);               /* 9.7:1 on black */
  margin: calc(var(--space-8) * -1) 0 var(--space-10);
  max-width: none;
}

/* ---- REGISTRATION TARGET ---------------------------------------------------
   Two hairline rings under a crosshair — the register mark beside a case
   title. This is the last survivor of the retired `.disc-card` / `.idx-grid` /
   `.disc` block, respelled as a .dev-* device; the rest of that block matched
   nothing on any page and is gone with the palette it was built for.

     <div class="target-row"><span class="dev-target" aria-hidden="true"></span>…</div>  */
.target-row { display: flex; align-items: center; gap: var(--space-4); }
.target-row h3 { font-size: 1.35rem; }
.dev-target {
  position: relative;
  flex: none;
  width: 40px; height: 40px;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  pointer-events: none;
}
.dev-target::before {
  content: "";
  position: absolute;
  inset: 7px;
  border: 1px solid var(--line);
  border-radius: 50%;
}
.dev-target::after {
  content: "";
  position: absolute;
  inset: -4px;
  background:
    linear-gradient(var(--line-strong), var(--line-strong)) 50% 0 / 1px 100% no-repeat,
    linear-gradient(var(--line-strong), var(--line-strong)) 0 50% / 100% 1px no-repeat;
}

/* ---- ERROR PAGE: THE FULL-BLEED POSTER ------------------------------------
   One screen, one poster. The silk-mono texture already on the site runs
   full-bleed under a veil that never drops below 0.88 alpha where the type
   sits; a 404 ghost-word stack sits behind the dashed .poster frame; the h1
   stays in Space Mono, because "$ curl: page not found" is a shell command and
   a shell command set in Bebas caps stops being one.

     <section class="sec sec-black accent-gold err text-center">
       <div class="err-tex" aria-hidden="true"></div>
       <div class="dev-stack err-stack" aria-hidden="true">×4</div>
       <span class="dev-vtext left"  aria-hidden="true">…</span>
       <span class="dev-vtext right" aria-hidden="true">…</span>
       <div class="container">
         <div class="poster err-poster">
           <span class="dev-cap tl|tr|bl|br" aria-hidden="true">…</span>
           <span class="dev-crease v" aria-hidden="true"></span>
           <p class="eyebrow center-x"><span class="mark" aria-hidden="true">[00]</span> 404 / not_found</p>
           <h1 class="h1-mono">$ curl: page not found</h1>
           <span class="dev-hazard err-hazard" aria-hidden="true"></span>
           <p class="mt-4 mx-auto err-copy">…</p>
           <div class="btn-row mt-8 center-x">…</div>
         </div>
       </div>
     </section>                                                          ---- */
.err {
  display: grid;
  align-content: center;
  min-height: calc(100svh - 5rem);
  padding: clamp(2.5rem, 6vw, 5rem) 0 clamp(3rem, 7vw, 6rem);
}
.err-tex {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--black) url('/assets/img/silk-mono.webp') center / cover no-repeat;
  pointer-events: none;
}
.err-tex::after {
  content: "";
  position: absolute;
  inset: 0;
  /* ≥0.88 alpha across the whole middle band, where every run of real text
     sits; it only opens up toward the outer corners. */
  background:
    radial-gradient(118% 74% at 50% 50%,
      rgba(11, 11, 12, 0.93) 0%,
      rgba(11, 11, 12, 0.89) 56%,
      rgba(11, 11, 12, 0.34) 100%),
    linear-gradient(180deg, var(--black) 0%, rgba(11, 11, 12, 0.22) 24%, rgba(11, 11, 12, 0.22) 76%, var(--black) 100%);
}
.err-stack {
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  font-size: clamp(7rem, 26vw, 22rem);
  line-height: 0.76;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 26%, #000 62%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 26%, #000 62%, transparent 100%);
}
.err-poster { padding: clamp(2.4rem, 5vw, 4rem) clamp(1.2rem, 3vw, 3rem) clamp(2.6rem, 5vw, 4rem); }
/* .poster is a flex column, which blockifies an inline-flex child: without
   align-self the eyebrow's knockout background painted the full poster width
   instead of hugging the label. */
.err .eyebrow { align-self: center; justify-content: center; background: var(--ground); padding: 4px 10px; }
/* The filing cluster under the poster frame: it turns the band between the
   frame and the checker strip into poster margin instead of leftover height. */
.err-foot { margin-top: clamp(1.6rem, 3.4vw, 2.6rem); gap: var(--space-5); }
.err-foot > .micro { text-align: left; }
/* A display-scale line that must stay monospaced. 5.2:1 minimum is the ink
   token's 17.4:1 — the veil under it never drops below 0.88 alpha. */
.h1-mono {
  font-family: var(--font-mono);
  font-weight: 700;
  text-transform: none;
  letter-spacing: -0.01em;
  font-size: clamp(1.3rem, 4.4vw, 3rem);
  line-height: 1.15;
  margin-top: var(--space-5);
  overflow-wrap: anywhere;
}
.err-hazard { display: block; width: min(280px, 62%); margin: var(--space-6) auto 0; }
.err-copy { max-width: 52ch; }
.err .dev-xhair.err-xh-a { left: 8%; top: 16%; }
.err .dev-sparkle.err-sp-a { right: 9%; bottom: 18%; width: 24px; height: 24px; }
.err .dev-spaced { margin-left: auto; margin-right: auto; max-width: none; }

/* Hazard-stripe band (the ///// bar from ref 8, as a reusable device). */
.dev-hazard {
  display: inline-block;
  height: 10px;
  background: repeating-linear-gradient(-45deg, var(--accent-solid) 0 7px, transparent 7px 15px);
  pointer-events: none;
}
/* A labelled hairline: <p class="dev-rule"><span class="dev-label">Exhibit C</span></p> */
.dev-rule { display: flex; align-items: center; gap: var(--space-4); max-width: none; }
.dev-rule::after { content: ""; flex: 1 1 auto; height: 0; border-top: 1px dashed var(--line); }

/* ---- 6b RESPONSIVE --------------------------------------------------------
   The decoration columns and ghost words exist to fill a second column; once
   the page is one column they have nothing to fill and they go. ---- */
@media (max-width: 1000px) {
  .masthead-grid { grid-template-columns: 1fr; gap: var(--space-8); align-items: start; }
  .masthead-side { display: none; }
  .masthead h1 { max-width: none; }
  /* One column: the narrative takes the width, the decoration-only margins go
     (they exist to fill a second track), and the ones carrying real copy stack
     under the block they were beside. */
  .case-body, .split-body { grid-template-columns: minmax(0, 1fr); }
  .case-body > .case-wide, .case-body > .case-margin { grid-column: 1; }
  .case-body > .case-margin[aria-hidden="true"],
  .split-body > .split-margin[aria-hidden="true"] { display: none; }
  .case-margin > .exhibit { min-height: 220px; }
}
@media (max-width: 900px) {
  .section-head.sub { align-items: flex-start; }
  .case-meta { grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr)); }
  .metric-strip .metric { padding: var(--space-5) var(--space-4); }
}
@media (max-width: 780px) {
  /* One ghost word per page was already the budget; on a phone there is no
     corner to put it in, exactly as .hero-stack does on the home page. */
  .masthead-stack, .case-stack { display: none; }
  .filter-bar::after { display: none; }
  .filter-bar { margin-top: var(--space-8); }
  .backlink-row::after { display: none; }
  /* A rail's elastic member exists to fill a SECOND column; with one column
     there is nothing left over to fill, and 1,000px of decorative spine under
     the scrap is 1,000px of phone scroll. The exhibit plate and the record
     panel stay: those are texture, and they were on the phone before. */
  .rail-fill { display: none; }
  .case-rail { gap: var(--space-8); }
  /* One column: the band has no room for the ghost word beside the label, so
     it drops to a line of its own under it. */
  .lab-band { gap: var(--space-4); }
  .lab-band-ghost { flex: 1 0 100%; }
  .lab-band-mark .dev-hazard { width: min(220px, 100%); }
  /* One column: the mirror has nothing to mirror, and the case must come before
     its margin. Higher specificity than .two-col-60-40, so it resets by name. */
  .case-split.case-mirror { grid-template-columns: minmax(0, 1fr); }
  .case-split.case-mirror > .card,
  .case-split.case-mirror > .case-rail { order: 0; }
  .case-split.case-mirror .rail-fill { border-right: none; }
  .findings > h2 { margin-top: -0.34em; }
  .callout.pull { padding-right: var(--space-6); }
  .callout.pull::after { position: static; display: block; margin-bottom: var(--space-3); }
  .err-poster { padding: 2.2rem 1.1rem 2.4rem; }
  /* .dev-stack's phone size is !important, so the 404 ghost word has to answer
     in kind or the poster's one big gesture shrinks to 67px on a phone. */
  .dev-stack.err-stack { font-size: clamp(5.5rem, 30vw, 9rem) !important; }
}
@media (max-width: 600px) {
  .case-title { font-size: clamp(2.1rem, 11vw, 3.4rem); }
  .masthead h1 { font-size: clamp(2.3rem, 11vw, 3.6rem); }
  .findings > h2, .case-body h2, .section-head.sub h2 { font-size: 1.55rem; }
  .lab-grid .card { padding: 1.9rem 1rem 1.1rem; }
}

/* ---------- 7. Utilities ---------- */
.text-center { text-align: center; }
.mt-2 { margin-top: var(--space-2); }
.mt-4 { margin-top: var(--space-4); }
.mt-6 { margin-top: var(--space-6); }
.mt-8 { margin-top: var(--space-8); }
.mt-12 { margin-top: var(--space-12); }
.mb-4 { margin-bottom: var(--space-4); }
.mb-6 { margin-bottom: var(--space-6); }
.text-muted { color: var(--ink-2); }
.text-dim { color: var(--ink-3); }
.hidden { display: none !important; }
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--gold); color: var(--paper-ink);
  padding: var(--space-3) var(--space-5);
  z-index: 1000;
  font-family: var(--font-mono); font-size: 0.74rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase;
}
.skip-link:focus { left: var(--space-4); top: var(--space-4); outline: 2px solid var(--paper-ink); outline-offset: 2px; }
.mx-auto { margin-left: auto; margin-right: auto; }
.center-x { justify-content: center; }
.nb { white-space: nowrap; }   /* keep a coordinate pair / unit from orphaning a token */
.balance { text-wrap: balance; }

/* ---------- 8. Responsive ---------- */
/* ---- Contact type scale below 1200. The panel/arch GEOMETRY is width-agnostic
   now (see .contact-stage in section 6) — all that is left to do per band is
   hold the arched line at display scale: textLength is fixed in viewBox units,
   so a narrower arch shrinks the glyphs and the size has to step up. ---- */
@media (max-width: 1199px) {
  .dev-arch text { font-size: 42px; letter-spacing: 3px; }
}
@media (max-width: 1080px) {
  .skill-grid { grid-template-columns: repeat(2, 1fr); }
  .course-grid { grid-template-columns: 1fr; gap: var(--space-10); }
  /* Column anchors only exist to fill a two-column section's short column. */
  .course-grid .dev-anchor { display: none; }
}
@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; gap: var(--space-8); }
  /* The grid collapses to one column here, so the TYPE has to take the freed
     width — otherwise 768–899px leaves the hero's right half bare black. */
  .hero h1 { font-size: clamp(3.6rem, 14.5vw, 8.5rem); }
  .hero .lead { max-width: 48ch; }
  .hero .lede { max-width: 70ch; font-size: 0.95rem; }
  .poster { padding: clamp(1.8rem, 3.2vw, 3rem) clamp(1.2rem, 2vw, 1.8rem) clamp(2rem, 3.6vw, 3.4rem); }
  /* Two repeats, starting at the h2 rather than over the eyebrow. Anchored to
     the LEFT margin here, not the right: right-aligned, the ghost words began
     mid-headline and the half-overlap read as an accident. From the left margin
     they run under the whole headline block the way refs 10 and 12 do. */
  .projects-stack { top: 5.5%; right: auto; left: 0; text-align: left; }
  .projects-stack span:nth-child(n+3) { display: none; }
  .hero-tex::after {
    background:
      linear-gradient(180deg, rgba(11, 11, 12, 0.9) 0%, rgba(11, 11, 12, 0.86) 46%, rgba(11, 11, 12, 0.92) 100%),
      linear-gradient(90deg, rgba(11, 11, 12, 0.6), rgba(11, 11, 12, 0.2));
  }
  .collage { min-height: 0; padding-top: var(--space-6); }
  .collage .dev-ring { display: none; }
  .subject-card { margin: 0 auto; }
  .dev-vtext { display: none; }
  /* The stacked ATTACK watermark collides with the corner caps and the
     eyebrow once the hero goes single-column — drop it entirely. */
  .hero-stack { display: none; }
  .section-head.with-mark { grid-template-columns: 1fr; }
  .section-head.with-mark .dev-mark { grid-row: auto; margin-bottom: var(--space-2); font-size: 3rem; }
  .section-head.with-num { flex-direction: row; align-items: flex-start; }
  .dev-num { font-size: 3rem; }
  .record-grid { grid-template-columns: 1fr; }
  .record-meta { flex-direction: row; align-items: center; flex-wrap: wrap; gap: var(--space-4); }
  .record-meta .dev-barcode.tall { height: 24px; }
}
/* Tablet-portrait hero (the 768–900 band, iPad portrait included). The grid is
   one column here, so the ATTACK stack no longer has a second column to fight
   for the top-right corner — it comes back to occupy it, tucked under the
   MMXXVI chip and clear of every headline line at 701px and up. Below 701 it
   stays hidden: there is no spare corner on a phone. */
@media (min-width: 701px) and (max-width: 900px) {
  .hero-stack { display: block; top: 4%; right: 4%; }
}
@media (max-width: 780px) {
  .two-col-60-40, .two-col-40-60 { grid-template-columns: 1fr; }
  .nav-links {
    position: absolute;
    top: 64px; left: 0; right: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-4);
    padding: var(--space-6);
    background: var(--black);
    border-bottom: 1px dashed var(--line-strong);
    display: none;
  }
  .nav-links.open { display: flex; }
  /* The open panel had no visible foot: page copy resumed straight under
     "Contact" with nothing separating the two layers. A solid rule, a drop
     shadow and the site's own checker strip seal it. */
  .nav-links { border-bottom: 1px solid var(--line-strong); box-shadow: 0 16px 26px -12px rgba(0, 0, 0, 0.95); }
  .nav-links::after {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: -7px;
    height: 6px;
    background: repeating-linear-gradient(90deg, var(--line-strong) 0 7px, transparent 7px 14px);
    pointer-events: none;
  }
  .nav-toggle { display: flex; }
  .sec { --sec-pad: 4.8rem; }
  .dev-cap { font-size: 0.55rem; letter-spacing: 0.2em; }
  .dev-cap.bl, .dev-cap.br { display: none; }
  .card { --tilt: 0deg; }
  .card:nth-child(odd) { --tilt: -0.6deg; }
  .card:nth-child(even) { --tilt: 0.5deg; }
  .dev-stack { font-size: 4.2rem !important; }
  .hero-stack { top: 4%; right: 8%; }
  .dev-mark.wm { display: none; }
  .dev-anchor { display: none; }
  /* Once the About columns stack, r1's -6% bleeds past the viewport's left edge
     and .sec{overflow:hidden} silently clips its left side off. Align it to the
     text column from here down, not just below 600. */
  .proof-mark .dev-rect.r1 { left: 0; width: 72%; }
  /* The stacked phone menu can afford the full 44px comfort target; the desktop
     rule stays at 4px because the nav bar's height is fixed at 64px. */
  .nav-links a { display: flex; align-items: center; min-height: 44px; padding: 0; }
  /* The only place a decorative field sat under long-form body copy. */
  #about > .dev-halftone.ht-bl { display: none; }
}
@media (max-width: 600px) {
  .skill-grid { grid-template-columns: 1fr; }
  .skill-group { min-height: 0; }
  .poster { padding: 2.2rem 1.1rem 2.4rem; }
  .hero h1 { font-size: clamp(3.4rem, 15.4vw, 5rem); }
  h2 { font-size: clamp(2.5rem, 11vw, 3.6rem); }
  .course-grid h2 { font-size: clamp(2.2rem, 10vw, 3.2rem); }
  .h2-sm { font-size: 1.4rem; }
  .dropcap::first-letter { font-size: 3.4em; }
  .btn { white-space: normal; text-align: center; }
  .site-footer .container { align-items: flex-start; }
  .blackletter { font-size: 3.4rem; }
  .proof-mark { font-size: 3rem; margin-bottom: var(--space-8); }
  .proof-mark .dev-rect.r2 { display: none; }
  /* With r2 gone, the surviving rectangle bleeds 20px left of the text column
     and reads as an accident. Align it to the column instead. */
  .proof-mark .dev-rect.r1 { left: 0; width: 80%; }
  .footer-meta { flex-direction: column; align-items: flex-start; }
  .thumb { aspect-ratio: 16 / 6; }
}

/* ---- Contact, below 700px. Three elements used to collide here: the arched
   name straddled the trapezoid's top edge (crown on black, ends on teal, the
   edge slicing letterforms mid-word), a sparkle landed on the "MC" of
   MCDONALD, and the outlined VI straddled the slanted right edge while
   overlapping the h2. The panel's own top/bottom anchoring now lives in the
   base rule, so all the phone needs is a near-full-bleed panel (and therefore a
   near-full-bleed arch), the sparkle moved to the panel's left flank, and VI
   dropped. The 252px fixed panel height that used to live here was what sliced
   the headline between 601 and 700, where the type had not yet shrunk.
   Placed after the 600px block on purpose: these rules must win at 390 too. ---- */
@media (max-width: 700px) {
  .contact { --trap-w: 96vw; --trap-bias: 0px; }
  .dev-arch text { font-size: 52px; letter-spacing: 2px; }
  .contact .dev-sparkle.sp-a { left: 7%; top: 26%; width: 20px; height: 20px; }
  .contact .dev-xhair.xh-a { display: none; }
  .contact-num { display: none; }
}
