/* ═══════════════════════════════════════════════════════════════════════════
   STITCH OBSESSION — "OBSESSION PRESS" DESIGN SYSTEM
   v1 · 2026-08-12 · vintage poster / mid-century advertising

   The reference world: a 1950s CARAMELS candy-box advert, an ornate GIN TONIC
   label, a RETRO TOASTER pop advert, a RETRO COFFEE SHOP sign. Aged paper,
   heavy slab display with a hard offset shadow, scalloped candy-box edges,
   filigree corners, starbursts, roundel price badges, a starred ribbon banner
   and sunburst rays. Loud on purpose. Restraint is not the brief.

   ── WHAT THIS FILE IS NOT ───────────────────────────────────────────────
   Nothing here descends from the template this repo was cloned from. The
   chain-stitch rule, the shell-stitch ornament set, the V-stitch fabric
   field, the Silkscreen pixel face and the rose/sage/amber palette are all
   gone. Some of that system's TOKEN NAMES survive in §2 as a deliberate
   compatibility shim, remapped onto this palette, because article.css and
   the raw-HTML bodies inside build_static_pages.py still read them. Those
   are aliases only — new work uses the §1 names.

   ── ORDER ───────────────────────────────────────────────────────────────
    1 Tokens            7 Layout primitives   13 Newsletter + forms
    2 Legacy aliases    8 Buttons + controls  14 Ribbon banner
    3 Reset + base      9 Masthead + nav      15 Footer
    4 Accessibility    10 Hero               16 Listings + search
    5 Ornament engine  11 Cards + grid       17 Static pages
    6 Typography       12 Stats + features   18 404 · 19 News · 20 Utilities
                                             21 Responsive · 22 Motion · Print

   ── CONTRAST (real relative-luminance math, WCAG 2.1) ───────────────────
   Every ratio quoted beside a token below was computed, not estimated.
   Body/UI needs ≥ 4.5:1; ≥ 3:1 is allowed ONLY at ≥ 24px display sizes.
   MUSTARD IS ORNAMENT ONLY — 1.74:1 on paper. It is a ground, never a
   foreground, except on ink (7.19:1).

   ── LOAD ORDER ──────────────────────────────────────────────────────────
   Pages link article.css BEFORE stitchobsession.css, so on an equal-specificity
   tie this file wins. Bump the ?v= query on every page when this changes.
   ═══════════════════════════════════════════════════════════════════════ */

/* Alfa Slab One (display) · Caveat (script) · Oswald (condensed caps) ·
   Bitter (body slab serif).
   ⚠ This @import MUST stay byte-identical to site.json →
     fingerprint.font_url, or the SaaS deploy re-pins a different font.

   Only the weights a rule actually renders are requested. Measured with
   getComputedStyle over every element (and ::before/::after) on twelve
   routes: Bitter 400 · 700 · 800 + italic 400 (em/i in prose) · Caveat 400
   and 600 · Oswald 400 · 500 · 600, plus 700 because <strong> inside a caps
   run asks for 800 and 700 is the heaviest Oswald cuts. The seven weights the
   first import also asked for — Bitter 500, 600, italic 600; Caveat 500, 700;
   Oswald 300 — matched nothing and cost 11 KB of render-blocking CSS. If you
   add a rule with a new weight, add it here AND in site.json. */
@import url('https://fonts.googleapis.com/css2?family=Alfa+Slab+One&family=Bitter:ital,wght@0,400;0,700;0,800;1,400&family=Caveat:wght@400;600&family=Oswald:wght@400;500;600;700&display=swap');

/* ═══════════════════════════════════════════════════════════════════════════
   1 · TOKENS
   ═══════════════════════════════════════════════════════════════════════ */
:root{

  /* ── 1.1 Palette · aged paper, never pure white, never pure black ──── */
  --paper:        #f4e4c1;  /* page ground — aged cream                    */
  --paper-light:  #faf0da;  /* card stock — panels, cards                  */
  --cream-deep:   #e8d3a9;  /* second band tone, recessed panels           */
  --ink:          #2b2118;  /* warm near-black · 12.53:1 on paper          */
  --red:          #c8332b;  /* poster red — DISPLAY ≥24px only (4.22:1)    */
  --teal:         #1d6a6d;  /* deep teal · 5.01:1 on paper — AA as text    */
  --mustard:      #e0a52e;  /* ORNAMENT ONLY on paper · 1.74:1 — fails AA.
                               On INK it is 7.19:1 and is the footer's display
                               voice (headings, tagline, credit) — the rule is
                               "never a foreground on paper", not "never text". */

  /* ── 1.2 Text-safe derivations (computed, not guessed) ─────────────── */
  --red-d:        #a2231d;  /* 5.98 paper · 6.64 paper-light · 5.13 cream-deep — the TEXT red */
  --red-deep:     #8f1f18;  /* 7.03 paper — AAA emphasis, focus ring on cream                */
  --teal-d:       #155052;  /* 7.26 paper · 6.22 cream-deep — text teal on tinted grounds    */
  /* The only mustard hue allowed to carry small text. Was #8a5c07 at 4.62 on
     paper — which the .06 grain took to 4.28, under AA, on the Important Notes
     labels. Now 6.19 paper · 6.87 paper-light · 5.31 cream-deep, and still
     5.74 / 4.94 with the grain composited. */
  --mustard-d:    #704b06;
  --ink-2:        #5c4a38;  /* 6.71 paper · 7.44 paper-light — muted body                    */
  --ink-3:        #6b5744;  /* 5.44 paper · 4.66 cream-deep — meta, captions (still AA)      */

  /* ── 1.3 Foregrounds for coloured grounds ──────────────────────────── */
  --on-red:       #fdf7ea;  /* 4.96:1 on --red  ✓ AA (paper-light is only 4.68 — too thin)   */
  --on-teal:      #fdf7ea;  /* 5.90:1 on --teal ✓ AA                                        */
  --on-mustard:   #2b2118;  /* 7.19:1 on --mustard ✓ AAA                                    */
  --on-ink:       #f4e4c1;  /* 12.53:1 on --ink ✓ AAA                                       */
  --on-ink-mut:   #d9c39a;  /*  9.17:1 on --ink ✓ AAA — footer body + footer links          */
  --on-cream:     #2b2118;  /* 10.74:1 on --cream-deep ✓ AAA                                */

  /* ── 1.4 Semantic roles ────────────────────────────────────────────── */
  --bg-page:      var(--paper);
  --bg-sunk:      var(--cream-deep);
  --bg-raised:    var(--paper-light);
  --fg:           var(--ink);
  --fg-mut:       var(--ink-2);
  --fg-faint:     var(--ink-3);
  --link:         var(--red-d);
  --link-hover:   var(--red-deep);
  --line:         var(--ink);     /* every drawn border is ink — poster rule */
  --line-soft:    #cbb289;        /* 7.70:1 against ink — hairlines on cream */
  --focus:        var(--red-deep);/* redeclared to --on-red on dark grounds  */

  /* ── 1.5 Type ──────────────────────────────────────────────────────── */
  --display:  'Alfa Slab One', 'Rockwell', 'Clarendon', Georgia, serif;
  --script:   'Caveat', 'Segoe Script', 'Brush Script MT', cursive;
  --caps:     'Oswald', 'Haettenschweiler', 'Arial Narrow', system-ui, sans-serif;
  --font:     'Bitter', 'Bookman Old Style', Georgia, 'Times New Roman', serif;

  /* 17.5px body floor — this audience skews older (see §22 of the spec). */
  --fs-micro:  .72rem;
  --fs-xs:     .82rem;
  --fs-sm:     .95rem;
  --fs-body:   1.09375rem;   /* 17.5px                                   */
  --fs-lg:     1.2rem;
  --fs-xl:     1.4rem;
  --fs-h3:     clamp(1.15rem, 1.7vw, 1.4rem);
  --fs-h2:     clamp(1.9rem, 3.6vw, 2.9rem);
  /* The brief asked for clamp(2.6rem, 7vw, 5rem). Screenshot review cut it
     back: at 7vw/5rem the homepage headline alone filled a 1366×768 laptop
     top to bottom — no lede, no buttons, no stats above the fold, which is
     the one thing a poster must not do. This scale still reads as a poster
     (66px on a laptop, 36px on a phone) and leaves the call to action on
     screen. Verified by screenshot at 1440, 1366 and 380. */
  --fs-h1:     clamp(2.25rem, 5.4vw, 4.15rem);
  --fs-mega:   clamp(4rem, 16vw, 11rem);   /* 404 numeral                 */

  /* Display leading. NOT .95: Alfa Slab One has a tall cap height and every
     display class carries a HARD (unblurred) drop shadow, so at .95 the shadow
     of one line landed on the caps of the next — every wrapped headline read
     as a smudge, worst at 380px where headlines wrap most.

     A RATIO cannot solve this. The shadow offset is an absolute length, so the
     leading it needs is absolute too: at the old 1.03 a 66px desktop headline
     had 2.0px of leading against a 4px shadow, and a 39px phone headline had
     1.17px against 3px — measured on the seven-line article H1 at 390px, where
     the ink shadow physically sat on the caps below. The smaller the type, the
     worse a fixed ratio does, which is backwards.

     So express it as the shadow plus a fixed gap. `1em` in line-height is the
     element's own font-size, and --hard-y is substituted at the use site, so
     .sp-display--sm (which drops the shadow to 2px) automatically gets tighter
     leading, and the ≤760px block (3px) gets its own. Every display line now
     clears its own shadow by 3px at every size. */
  --lh-poster: calc(1em + var(--hard-y) + 3px);
  --lh-tight:  1.08;
  --lh-snug:   1.3;
  --lh-body:   1.72;  /* the brief's body leading                         */
  --lh-loose:  1.85;

  --fw-body:    400;
  --fw-med:     500;
  --fw-bold:    700;
  --fw-black:   800;
  --fw-display: 400;  /* Alfa Slab One ships one weight and it is heavy   */

  --track-caps:  .08em;   /* Oswald condensed caps — the brief's value    */
  --track-wide:  .16em;   /* ribbon + eyebrow chrome                      */
  --track-tight: -.01em;  /* Alfa Slab display                            */

  /* ── 1.6 Spacing rhythm (4px base, poster-generous at the top) ─────── */
  --sp-1:  4px;
  --sp-2:  8px;
  --sp-3:  12px;
  --sp-4:  18px;
  --sp-5:  26px;
  --sp-6:  36px;
  --sp-7:  52px;
  --sp-8:  76px;
  --sp-9:  104px;

  /* ── 1.7 Radii — poster stock is mostly square; curves are ornament ── */
  --r-xs:   3px;
  --r-sm:   6px;
  --r:      10px;
  --r-lg:   18px;
  --r-pill: 999px;

  /* ── 1.8 Borders — heavy ink keylines, the letterpress signature ───── */
  --bw-hair:  1.5px;
  --bw:       2.5px;
  --bw-thick: 4px;
  --bw-slab:  6px;

  /* ── 1.9 Shadows — hard offsets only. No blur anywhere in this system. */
  --pop-sm:  3px 3px 0 var(--line);
  --pop:     5px 5px 0 var(--line);
  --pop-lg:  8px 8px 0 var(--line);
  --pop-xl: 12px 12px 0 var(--line);
  --pop-in:  0 0 0 0 var(--line);   /* pressed = flat                     */
  --press:   5px;                    /* travel of a pressed control       */
  --hard-x:  4px;                    /* display text-shadow offset        */
  --hard-y:  4px;

  /* ── 1.10 Motion ───────────────────────────────────────────────────── */
  --t-fast: .12s;
  --t:      .22s;
  --t-slow: .4s;
  --ease:   cubic-bezier(.2,.7,.3,1);
  --ease-out: cubic-bezier(.16,1,.3,1);

  /* ── 1.11 Layout ───────────────────────────────────────────────────── */
  --container: 1200px;
  --measure:   66ch;
  --gutter:    26px;
  --nav-h:     76px;
  --scallop-base: 16px;  /* .sp-scallop--lg is derived from this, so the  */
  --scallop-h: 16px;   /* --lg edge shrinks with the rest on a phone      */
  --tape-h:    12px;

  /* ── 1.12 Ornament sources (data-URIs, recoloured via mask-color) ────
     Everything decorative in this system is an inline SVG mask or a
     gradient. No image files, no extra HTTP requests, no icon font. */

  /* aged paper grain — feTurbulence, alpha baked to .06 so the flat-blend
     worst case darkens paper by ~3.5%: ink 11.74:1, red-d 5.60:1,
     teal 4.70:1, ink-2 6.29:1 — every one still AA. Do not raise it. */
  --tex-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='.06'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23g)'/%3E%3C/svg%3E");

  /* filigree corner, four rotations of one drawing (GIN TONIC label) */
  --m-fil-tl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cg fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M2 16V7a5 5 0 0 1 5-5h9'/%3E%3Cpath d='M8 21v-9a4 4 0 0 1 4-4h9'/%3E%3Cpath d='M14 27c0-4 3-7 7-7s7 3 7 7c0 3-2.2 5-4.6 5-2 0-3.2-1.4-3.2-2.9 0-1.3 1-2.4 2.3-2.4'/%3E%3C/g%3E%3Cpath d='M27 33q7-2.4 11-11-8.6 3-11 11z' fill='%23000'/%3E%3Ccircle cx='35' cy='7' r='2.1' fill='%23000'/%3E%3Ccircle cx='7' cy='35' r='2.1' fill='%23000'/%3E%3C/svg%3E");
  --m-fil-tr: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cg transform='rotate(90 24 24)'%3E%3Cg fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M2 16V7a5 5 0 0 1 5-5h9'/%3E%3Cpath d='M8 21v-9a4 4 0 0 1 4-4h9'/%3E%3Cpath d='M14 27c0-4 3-7 7-7s7 3 7 7c0 3-2.2 5-4.6 5-2 0-3.2-1.4-3.2-2.9 0-1.3 1-2.4 2.3-2.4'/%3E%3C/g%3E%3Cpath d='M27 33q7-2.4 11-11-8.6 3-11 11z' fill='%23000'/%3E%3Ccircle cx='35' cy='7' r='2.1' fill='%23000'/%3E%3Ccircle cx='7' cy='35' r='2.1' fill='%23000'/%3E%3C/g%3E%3C/svg%3E");
  --m-fil-br: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cg transform='rotate(180 24 24)'%3E%3Cg fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M2 16V7a5 5 0 0 1 5-5h9'/%3E%3Cpath d='M8 21v-9a4 4 0 0 1 4-4h9'/%3E%3Cpath d='M14 27c0-4 3-7 7-7s7 3 7 7c0 3-2.2 5-4.6 5-2 0-3.2-1.4-3.2-2.9 0-1.3 1-2.4 2.3-2.4'/%3E%3C/g%3E%3Cpath d='M27 33q7-2.4 11-11-8.6 3-11 11z' fill='%23000'/%3E%3Ccircle cx='35' cy='7' r='2.1' fill='%23000'/%3E%3Ccircle cx='7' cy='35' r='2.1' fill='%23000'/%3E%3C/g%3E%3C/svg%3E");
  --m-fil-bl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cg transform='rotate(270 24 24)'%3E%3Cg fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M2 16V7a5 5 0 0 1 5-5h9'/%3E%3Cpath d='M8 21v-9a4 4 0 0 1 4-4h9'/%3E%3Cpath d='M14 27c0-4 3-7 7-7s7 3 7 7c0 3-2.2 5-4.6 5-2 0-3.2-1.4-3.2-2.9 0-1.3 1-2.4 2.3-2.4'/%3E%3C/g%3E%3Cpath d='M27 33q7-2.4 11-11-8.6 3-11 11z' fill='%23000'/%3E%3Ccircle cx='35' cy='7' r='2.1' fill='%23000'/%3E%3Ccircle cx='7' cy='35' r='2.1' fill='%23000'/%3E%3C/g%3E%3C/svg%3E");

  /* stars: a 4-point sparkle, a 12-point coffee-sign star, a 16-point burst */
  --m-sparkle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M50 2 L56.36 43.64 L98 50 L56.36 56.36 L50 98 L43.64 56.36 L2 50 L43.64 43.64 Z' fill='%23000'/%3E%3C/svg%3E");
  --m-star12: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M50 2 L54.92 31.65 L74 8.43 L63.44 36.56 L91.57 26 L68.35 45.08 L98 50 L68.35 54.92 L91.57 74 L63.44 63.44 L74 91.57 L54.92 68.35 L50 98 L45.08 68.35 L26 91.57 L36.56 63.44 L8.43 74 L31.65 54.92 L2 50 L31.65 45.08 L8.43 26 L36.56 36.56 L26 8.43 L45.08 31.65 Z' fill='%23000'/%3E%3C/svg%3E");
  --m-burst16: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M50 1 L53.9 30.38 L68.75 4.73 L61.11 33.37 L84.65 15.35 L66.63 38.89 L95.27 31.25 L69.62 46.1 L99 50 L69.62 53.9 L95.27 68.75 L66.63 61.11 L84.65 84.65 L61.11 66.63 L68.75 95.27 L53.9 69.62 L50 99 L46.1 69.62 L31.25 95.27 L38.89 66.63 L15.35 84.65 L33.37 61.11 L4.73 68.75 L30.38 53.9 L1 50 L30.38 46.1 L4.73 31.25 L33.37 38.89 L15.35 15.35 L38.89 33.37 L31.25 4.73 L46.1 30.38 Z' fill='%23000'/%3E%3C/svg%3E");

  /* the candy-box scalloped edge, as a mask for the div-flavoured variant */
  --m-scallop: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='16' viewBox='0 0 32 16'%3E%3Cpath d='M0 16 A16 16 0 0 1 32 16 Z' fill='%23000'/%3E%3C/svg%3E");

  /* diamond-and-dot rule — this system's section divider (replaces the
     chain-stitch rule the clone shipped with) */
  --m-rule: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='42' height='14' viewBox='0 0 42 14'%3E%3Cpath d='M21 1 L27 7 L21 13 L15 7 Z' fill='%23000'/%3E%3Ccircle cx='6' cy='7' r='2.4' fill='%23000'/%3E%3Ccircle cx='36' cy='7' r='2.4' fill='%23000'/%3E%3C/svg%3E");

  /* fine ticking field — the faint printed texture on panel grounds
     (replaces the clone's V-stitch fabric) */
  --m-ticking: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.4' stroke-linecap='round'%3E%3Cpath d='M4 2v6M16 2v6M10 14v6M22 14v6'/%3E%3C/g%3E%3C/svg%3E");

  /* hook + yarn doodles for hero float slots (aria-hidden decoration) */
  --m-hook: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%23000' stroke-width='7' stroke-linecap='round'%3E%3Cpath d='M50 92V38'/%3E%3Cpath d='M50 38c0-13 22-13 22 0 0 10-12 12-16 6'/%3E%3C/g%3E%3C/svg%3E");
  --m-yarnball: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%23000' stroke-width='6'%3E%3Ccircle cx='50' cy='50' r='40'/%3E%3Cpath d='M22 22c18 10 34 26 44 44M12 46c14 8 26 20 34 34M46 12c14 8 27 20 34 34M79 27C65 41 45 62 32 76'/%3E%3C/g%3E%3C/svg%3E");
  /* the masthead lens — replaces the 🔍 emoji, see §5.5b */
  --m-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%23000' stroke-width='10' stroke-linecap='round'%3E%3Ccircle cx='43' cy='43' r='30'/%3E%3Cpath d='M65 65 L90 90'/%3E%3C/g%3E%3C/svg%3E");
  /* the error face of the newsletter popup icon */
  --m-cross: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%23000' stroke-width='13' stroke-linecap='round'%3E%3Cpath d='M22 22 78 78M78 22 22 78'/%3E%3C/g%3E%3C/svg%3E");
  --m-scissors: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%23000' stroke-width='6' stroke-linecap='round'%3E%3Ccircle cx='24' cy='78' r='13'/%3E%3Ccircle cx='76' cy='78' r='13'/%3E%3Cpath d='M32 68 74 12M68 68 26 12'/%3E%3C/g%3E%3C/svg%3E");

  /* ── 1.13 Brand aliases the SaaS branding refresh writes to ─────────── */
  --primary:     var(--red);
  --primary-d:   var(--red-d);
  --secondary:   var(--teal);
  --secondary-d: var(--teal-d);
  --accent:      var(--mustard);
  --accent-d:    var(--mustard-d);
  --bg:          var(--paper);
  --surface:     var(--paper-light);
  --text:        var(--ink);
  --text-soft:   var(--ink-2);
  --shadow:      var(--pop-sm);
  --shadow-lg:   var(--pop);
  --shadow-xl:   var(--pop-lg);
}

/* ═══════════════════════════════════════════════════════════════════════════
   2 · LEGACY TOKEN ALIASES  (compatibility shim — do not use in new work)

   The cloned system's token names. The audit grepped every one of them
   across both stylesheets, site/js, _templates, the four hand-written pages
   and the build scripts: sixteen had NO reader left (--paper-2, the -t tint
   grounds, the sage and amber families, --pixel, --track-pixel and the five
   repointed masks) and were deleted, along with a duplicate --r / --t that
   restated §1.7 and §1.10 with the same values.

   The five below are still read by the cookie banner, which site-extras.js
   builds with INLINE styles (`var(--card)`, `var(--rose)`, `var(--rose-d)`,
   `var(--on-rose)`, `var(--amber)`). Each is an alias of a live §1 token —
   never a second value — so a palette change reaches the banner too. When
   the banner is rewritten onto the §1 names, delete this block.
   ═══════════════════════════════════════════════════════════════════════ */
:root{
  --card:      var(--paper-light);
  --rose:      var(--red);      /* display ≥24px only */
  --rose-d:    var(--red-d);    /* 5.98:1 — the text red */
  --amber:     var(--mustard);
  --on-rose:   var(--on-red);
}

/* ═══════════════════════════════════════════════════════════════════════════
   3 · RESET + BASE
   ═══════════════════════════════════════════════════════════════════════ */
*,*::before,*::after{ box-sizing:border-box; }

html{
  -webkit-text-size-adjust:100%;
  scroll-behavior:smooth;
  /* +40, not +24: the masthead's triangle tape (.navbar::after) hangs ~8px
     BELOW the nav's own height, so 24px of clearance left a jumped-to section
     eyebrow pressed right up under the triangles. */
  scroll-padding-top:calc(var(--nav-h) + 40px);
}

body{
  margin:0;
  font-family:var(--font);
  font-size:var(--fs-body);
  line-height:var(--lh-body);
  font-weight:var(--fw-body);
  color:var(--fg);
  background-color:var(--bg-page);
  /* aged paper: grain, then a faint vignette. Both are fixed so they never
     travel with the scroll and never tile visibly against a section edge. */
  background-image:
    var(--tex-grain),
    radial-gradient(ellipse 120% 85% at 50% 38%, transparent 52%, rgba(43,33,24,.06) 100%);
  background-repeat:repeat, no-repeat;
  background-size:220px 220px, 100% 100%;
  background-attachment:fixed, fixed;
  overflow-x:hidden;
  text-rendering:optimizeLegibility;
  -webkit-font-smoothing:antialiased;
}

img,svg,video,canvas{ max-width:100%; height:auto; display:block; }
img{ background-color:var(--cream-deep); }

a{ color:var(--link); text-decoration-thickness:2px; text-underline-offset:3px; }
a:hover{ color:var(--link-hover); }

p{ margin:0 0 var(--sp-4); }
ul,ol{ margin:0 0 var(--sp-4); padding-left:1.4em; }
li{ margin-bottom:var(--sp-2); }
li::marker{ color:var(--red-d); }

strong,b{ font-weight:var(--fw-black); color:var(--fg); }
em,i{ font-style:italic; }
small{ font-size:var(--fs-sm); }

hr{
  border:0; height:0; margin:var(--sp-6) 0;
  border-top:var(--bw) solid var(--line);
}

blockquote{
  margin:var(--sp-5) 0;
  padding:var(--sp-4) var(--sp-5);
  background:var(--paper-light);
  border:var(--bw) solid var(--line);
  border-left-width:var(--bw-slab);
  box-shadow:var(--pop-sm);
  font-size:var(--fs-lg);
}

code,kbd,samp{
  font-family:ui-monospace,'Cascadia Mono','Courier New',monospace;
  font-size:.92em;
  background:var(--cream-deep);
  border:var(--bw-hair) solid var(--line-soft);
  border-radius:var(--r-xs);
  padding:.1em .38em;
}

table{ border-collapse:collapse; width:100%; }

::selection{ background:var(--mustard); color:var(--on-mustard); }

input,select,textarea,button{ font:inherit; color:inherit; }

/* ═══════════════════════════════════════════════════════════════════════════
   4 · ACCESSIBILITY PRIMITIVES
   ═══════════════════════════════════════════════════════════════════════ */

/* Focus is a first-class state and is never removed. --focus is redeclared
   to a cream on every dark ground (§14 ribbon, §15 footer, §5.7 bands), so
   the ring always clears 3:1 against whatever sits behind it. */
:focus{ outline:none; }
:focus-visible{
  outline:3px solid var(--focus);
  outline-offset:3px;
  border-radius:var(--r-xs);
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible{
  outline:3px solid var(--focus);
  outline-offset:3px;
}

.sr-only{
  position:absolute!important; width:1px; height:1px;
  padding:0; margin:-1px; overflow:hidden;
  clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
}

.skip-link{
  position:absolute; left:-9999px; top:0; z-index:999;
  background:var(--ink); color:var(--on-ink);
  font-family:var(--caps); font-weight:600; text-transform:uppercase;
  letter-spacing:var(--track-caps);
  padding:var(--sp-3) var(--sp-5);
  border:var(--bw) solid var(--mustard);
  text-decoration:none;
}
.skip-link:focus{ left:var(--sp-4); top:var(--sp-4); color:var(--on-ink); }

/* LOAD-BEARING: article.js toggles .hidden to swap the loading state for
   the rendered article. Never repurpose this class. */
.hidden{ display:none!important; }
[hidden]{ display:none!important; }

/* Minimum tap target, applied to every control this system ships. */
.sp-tap{ min-height:48px; min-width:48px; }

/* ═══════════════════════════════════════════════════════════════════════════
   5 · THE ORNAMENT ENGINE — the ten signature devices

   All ten are reusable classes. Everything is CSS or an inline-SVG data-URI
   recoloured from a token: no image files, no extra HTTP requests, no icon
   font. Each device names the token it recolours from so a palette change
   reaches the ornament automatically.

     5.1  .sp-display          hard-shadow display type
     5.2  .sp-section--scallop / .sp-scallop / .scalloped-border  candy edges
     5.3  .sp-framed           SVG filigree frame corners
     5.4  .sp-star .sp-burst   starbursts
     5.5  .sp-roundel          roundel badges
     5.6  .sp-ribbon           starred ribbon banner  (full component in §14)
     5.7  .sp-rays             sunburst rays
     5.8  .sp-grain            aged-paper noise texture
     5.9  .sp-tape             dotted / triangle border tape
     5.10 .sp-sticker          sticker rotation
   ═══════════════════════════════════════════════════════════════════════ */

/* ── 5.1 · HARD-SHADOW DISPLAY TYPE ──────────────────────────────────────
   The loudest signal of the reference posters: a solid offset shadow with
   no blur. Three variants —
     .sp-display              red on cream, ink shadow          (default)
     .sp-display--cream       cream on a colour band, ink shadow
     .sp-display--outline     cream fill, ink outline + shadow (poster cut-out)
     .sp-display--ink         ink on cream, mustard shadow
   CONTRAST RULE: the default fill is --red at 4.22:1 on paper, which is
   legal ONLY at ≥ 24px. .sp-display sets a 1.6rem floor and .sp-display--sm
   swaps the fill to --red-d (5.98:1) for anything smaller. Do not override
   the fill back to --red at small sizes.                                   */
.sp-display{
  font-family:var(--display);
  font-weight:var(--fw-display);
  font-size:max(1.6rem, 1em);
  line-height:var(--lh-poster);
  letter-spacing:var(--track-tight);
  text-transform:uppercase;
  color:var(--red);
  text-shadow:var(--hard-x) var(--hard-y) 0 var(--ink);
  text-wrap:balance;
}
.sp-display--sm{                       /* < 24px: fill must clear 4.5:1 */
  font-size:1em;
  color:var(--red-d);
  --hard-x:2px; --hard-y:2px;
}
.sp-display--cream{ color:var(--on-red); }
.sp-display--ink{ color:var(--ink); text-shadow:var(--hard-x) var(--hard-y) 0 var(--mustard); }
.sp-display--teal{ color:var(--teal); }
.sp-display--mustard{                  /* mustard fill needs an ink outline
                                          to be legible — see .sp-display--outline */
  color:var(--mustard);
  text-shadow:
    -2px -2px 0 var(--ink),  2px -2px 0 var(--ink),
    -2px  2px 0 var(--ink),  2px  2px 0 var(--ink),
    var(--hard-x) var(--hard-y) 0 var(--ink);
}
/* Cut-out variant: cream fill, ink outline, then the hard offset. Layered
   shadows rather than -webkit-text-stroke, which thins the glyph inward and
   erodes the slab. */
.sp-display--outline{
  color:var(--on-red);
  text-shadow:
    -2px -2px 0 var(--ink),  0 -2px 0 var(--ink),  2px -2px 0 var(--ink),
    -2px  0    0 var(--ink),                        2px  0    0 var(--ink),
    -2px  2px 0 var(--ink),  0  2px 0 var(--ink),  2px  2px 0 var(--ink),
    calc(var(--hard-x) + 2px) calc(var(--hard-y) + 2px) 0 var(--ink);
}
.sp-display--tight{ --hard-x:2px; --hard-y:2px; }
.sp-display--loose{ --hard-x:6px; --hard-y:6px; }

/* ── 5.2 · SCALLOPED SECTION EDGES ───────────────────────────────────────
   The cut candy-box edge. REQUIRED between every major section.

   Three flavours, same geometry:
     (a) .sp-section--scallop     zero extra markup; the section grows its
                                  own bumps upward out of its top edge
     (b) .sp-scallop              a standalone strip you place between two
                                  sections (set --scallop-fill to the colour
                                  of the band BELOW it)
     (c) .scalloped-border > .scallop
                                  the div-per-bump structure, for markup that
                                  wants each bump addressable

   Colour: set --scallop-fill, or let it inherit --section-bg from the band
   modifiers in §7. The bumps are always the colour of the band below, so
   the boundary reads as one sheet cut over another.

   TWO THINGS THE FIRST VERSION GOT WRONG (both caught by screenshot):

   1. A standalone strip's GAPS are transparent, so they show whatever is
      behind the strip — which is the page background, NOT the section
      above it. Between the featured band (cream-deep) and the section
      below (paper) that meant paper bumps on a paper ground: the strip
      rendered, measured 16px, and was completely invisible. The boundary
      read as a ruled line. --scallop-bg paints the colour of the band
      ABOVE into the gaps, so set BOTH properties on every strip that sits
      between two coloured bands. The --on-* modifiers below do it by name.

   2. Two of this palette's papers are four percent apart in luminance. A
      fill-only scallop between them is technically present and visually
      absent. So the disc is now STROKED: an ink arc rides its rim, which
      is also what the reference posters do — a die-cut edge on a printed
      box is a drawn line, not a tonal step. The stroke is what makes the
      device read at every boundary, including paper-on-paper.             */
.sp-section--scallop,
.sp-section--scallop-b{ position:relative; }

.sp-section--scallop::before,
.sp-section--scallop-b::after,
.sp-scallop{
  --scallop-fill:var(--section-bg, var(--paper));
  --scallop-edge:var(--ink);
  --scallop-sw:2px;                     /* the drawn rim */
  height:var(--scallop-h);
  background-color:var(--scallop-bg, transparent);
  /* top layer = the disc; bottom layer = the same disc 2px bigger in the
     rim colour, so exactly --scallop-sw of it survives as a stroke. */
  background-image:
    radial-gradient(circle at 50% 100%,
      var(--scallop-fill) 0 calc(var(--scallop-h) - var(--scallop-sw) - .5px),
      rgba(0,0,0,0)       calc(var(--scallop-h) - var(--scallop-sw) - .5px)),
    radial-gradient(circle at 50% 100%,
      var(--scallop-edge) 0 calc(var(--scallop-h) - .5px),
      rgba(0,0,0,0)       calc(var(--scallop-h) - .5px));
  background-size:calc(var(--scallop-h) * 2) var(--scallop-h);
  background-repeat:repeat-x;
  background-position:center bottom;
  pointer-events:none;
}
.sp-section--scallop::before{
  content:'';
  position:absolute; left:0; right:0;
  top:calc(-1 * var(--scallop-h) + 1px);
  z-index:3;
}
/* Bottom edge: the same disc flipped, so the bumps hang down into the next
   section instead of rising into the previous one. */
.sp-section--scallop-b::after{
  content:'';
  position:absolute; left:0; right:0;
  bottom:calc(-1 * var(--scallop-h) + 1px);
  z-index:3;
  background-image:
    radial-gradient(circle at 50% 0,
      var(--scallop-fill) 0 calc(var(--scallop-h) - var(--scallop-sw) - .5px),
      rgba(0,0,0,0)       calc(var(--scallop-h) - var(--scallop-sw) - .5px)),
    radial-gradient(circle at 50% 0,
      var(--scallop-edge) 0 calc(var(--scallop-h) - .5px),
      rgba(0,0,0,0)       calc(var(--scallop-h) - .5px));
  background-position:center top;
}
.sp-scallop{ display:block; width:100%; border:0; margin:0; }

/* the band BELOW (what the bumps are cut from) */
.sp-scallop--paper{       --scallop-fill:var(--paper); }
.sp-scallop--paper-light{ --scallop-fill:var(--paper-light); }
.sp-scallop--cream{       --scallop-fill:var(--cream-deep); }
.sp-scallop--teal{        --scallop-fill:var(--teal); }
.sp-scallop--red{         --scallop-fill:var(--red); }
.sp-scallop--ink{         --scallop-fill:var(--ink); }
.sp-scallop--mustard{     --scallop-fill:var(--mustard); }

/* the band ABOVE (what shows through the valleys). Pair one of these with
   a fill modifier on every strip, or the valleys leak the page ground. */
.sp-scallop--on-paper{       --scallop-bg:var(--paper); }
.sp-scallop--on-paper-light{ --scallop-bg:var(--paper-light); }
.sp-scallop--on-cream{       --scallop-bg:var(--cream-deep); }
.sp-scallop--on-teal{        --scallop-bg:var(--teal); }
.sp-scallop--on-red{         --scallop-bg:var(--red); }
.sp-scallop--on-ink{         --scallop-bg:var(--ink); }
/* Relative, not absolute: this hard 22px ignored the ≤760px block that drops
   --scallop-h to 12px, so on a phone one scallop was 83% taller than the
   seven others on the same page. */
.sp-scallop--lg{ --scallop-h:calc(var(--scallop-base, 16px) * 1.375); }
.sp-scallop--sm{ --scallop-h:11px; }

/* (c) the div-per-bump flavour */
.scalloped-border{
  display:flex; width:100%; height:var(--scallop-h);
  line-height:0; pointer-events:none;
}
.scalloped-border .scallop{
  flex:1 1 auto;
  min-width:calc(var(--scallop-h) * 2);
  background:var(--scallop-fill, var(--section-bg, var(--paper)));
  border-radius:50% 50% 0 0 / 100% 100% 0 0;
}
.scalloped-border--down .scallop{ border-radius:0 0 50% 50% / 0 0 100% 100%; }

/* ── 5.3 · FILIGREE FRAME CORNERS ────────────────────────────────────────
   From the GIN TONIC label. One drawing, four rotations, painted as a mask
   so the colour comes from a token. Sits on an overlay layer above the
   panel's ground and below its content, and never intercepts clicks.       */
.sp-framed{ position:relative; }
.sp-framed::before{
  content:'';
  position:absolute; inset:var(--fil-inset, 7px);
  z-index:4; pointer-events:none;
  background-color:var(--fil-color, var(--red));
  -webkit-mask-image:var(--m-fil-tl), var(--m-fil-tr), var(--m-fil-bl), var(--m-fil-br);
          mask-image:var(--m-fil-tl), var(--m-fil-tr), var(--m-fil-bl), var(--m-fil-br);
  -webkit-mask-position:top left, top right, bottom left, bottom right;
          mask-position:top left, top right, bottom left, bottom right;
  -webkit-mask-repeat:no-repeat;
          mask-repeat:no-repeat;
  -webkit-mask-size:var(--fil-size, 46px) var(--fil-size, 46px);
          mask-size:var(--fil-size, 46px) var(--fil-size, 46px);
}
/* WHICH COLOUR A CORNER TAKES — the corner names the panel's ROLE, and one
   page never mixes roles inside a family. The audit counted four colours on
   eight panels of one article with no rule behind them; this is the rule:
     red      the one object the page is about — the pattern card, the 404
              "try one of these" panel
     ink      reading panels — the article's numbered content cards, the
              mistakes card (default when nothing below applies)
     teal     utility / aside panels — the sidebar promo, Q&A, side blocks
     mustard  chrome plates that frame a whole band — the hero plate, the
              newsletter card, the footer card (ornament ground, never text)
   The homepage level row is the one deliberate triad (teal / mustard / red =
   beginner / intermediate / advanced): there the colour IS the information.
   article.css enforces the article roles with scoped overrides, because the
   renderer hands out these modifier classes per card. */
.sp-framed--teal{    --fil-color:var(--teal); }
.sp-framed--mustard{ --fil-color:var(--mustard); }
.sp-framed--ink{     --fil-color:var(--ink); }
.sp-framed--cream{   --fil-color:var(--on-red); }
.sp-framed--lg{ --fil-size:64px; --fil-inset:10px; }
.sp-framed--sm{ --fil-size:32px; --fil-inset:5px; }

/* ── 5.4 · STARBURSTS ────────────────────────────────────────────────────
   The coffee-shop sign's stars. Decorative only — every instance must carry
   aria-hidden="true" in the markup. Mustard by default (ornament, never
   text). Three sizes of two shapes plus the 16-point burst plate.          */
.sp-star,
.sp-burst{
  display:inline-block; flex:none;
  width:var(--star-size, 22px); height:var(--star-size, 22px);
  background-color:var(--star-color, var(--mustard));
  -webkit-mask-image:var(--m-star12); mask-image:var(--m-star12);
  -webkit-mask-size:contain;          mask-size:contain;
  -webkit-mask-repeat:no-repeat;      mask-repeat:no-repeat;
  -webkit-mask-position:center;       mask-position:center;
  vertical-align:middle;
}
.sp-star--sparkle{ -webkit-mask-image:var(--m-sparkle); mask-image:var(--m-sparkle); }
.sp-burst{ -webkit-mask-image:var(--m-burst16); mask-image:var(--m-burst16); }
.sp-star--sm{ --star-size:14px; }
.sp-star--lg{ --star-size:34px; }
.sp-star--xl{ --star-size:58px; }
.sp-star--red{   --star-color:var(--red); }
.sp-star--teal{  --star-color:var(--teal); }
.sp-star--ink{   --star-color:var(--ink); }
.sp-star--cream{ --star-color:var(--on-red); }

/* A burst sitting BEHIND something — a badge, a heading initial. Absolute,
   centred, always below the content it decorates. */
.sp-star-behind{ position:relative; isolation:isolate; }
.sp-star-behind::before{
  content:'';
  position:absolute; z-index:-1;
  top:50%; left:50%; translate:-50% -50%;
  width:var(--behind-size, 150%); aspect-ratio:1;
  background-color:var(--behind-color, var(--mustard));
  opacity:var(--behind-opacity, .3);
  -webkit-mask-image:var(--m-burst16); mask-image:var(--m-burst16);
  -webkit-mask-size:contain;           mask-size:contain;
  -webkit-mask-repeat:no-repeat;       mask-repeat:no-repeat;
  -webkit-mask-position:center;        mask-position:center;
  animation:sp-spin 44s linear infinite;
}
@keyframes sp-spin{ to{ rotate:360deg; } }

/* A corner burst for sticker cards — the mustard flash on the featured grid.
   It sits INSIDE the card's box: the host clips (overflow:hidden keeps the
   photo's corners rounded), so a burst hung off the corner at a negative
   offset showed three-quarters of a star on every card. Same fix as
   .featured-card::after in §11. */
.sp-corner-burst{ position:relative; overflow:hidden; }
.sp-corner-burst::after{
  content:'';
  position:absolute; z-index:2;
  top:8px; right:8px; width:56px; height:56px;
  background-color:var(--mustard);
  -webkit-mask-image:var(--m-burst16); mask-image:var(--m-burst16);
  -webkit-mask-size:contain;           mask-size:contain;
  -webkit-mask-repeat:no-repeat;       mask-repeat:no-repeat;
  pointer-events:none;
}

/* ── 5.5 · ROUNDEL BADGES ────────────────────────────────────────────────
   The "Only 25¢ A POUND" device: a circular red plate, cream double ring,
   script over slab, rotated off-axis. Used for "100% FREE", skill level and
   the newsletter "NO SPAM EVER" seal.
   Markup:
     <span class="sp-roundel" aria-label="One hundred percent free">
       <span class="sp-roundel__script">only</span>
       <span class="sp-roundel__big">100%</span>
       <span class="sp-roundel__sub">free</span>
     </span>
   CONTRAST: --on-red on --red = 4.96:1 ✓ AA for the small type too.        */
.sp-roundel{
  --roundel-size:112px;
  display:inline-flex; flex-direction:column;
  align-items:center; justify-content:center;
  gap:1px;
  width:var(--roundel-size); height:var(--roundel-size);
  padding:var(--sp-2);
  border-radius:50%;
  background:var(--roundel-bg, var(--red));
  color:var(--roundel-fg, var(--on-red));
  text-align:center; line-height:1;
  rotate:var(--roundel-tilt, -8deg);
  box-shadow:
    0 0 0 4px var(--roundel-ring, var(--paper-light)),
    0 0 0 7px var(--ink),
    var(--pop-sm);
  transition:rotate var(--t) var(--ease), scale var(--t) var(--ease);
}
.sp-roundel__script{
  font-family:var(--script);
  font-size:calc(var(--roundel-size) * .2);
  font-weight:600;
  line-height:1;
}
/* The slab word has to stay INSIDE the disc. These ratios are the NO-SCRIPT
   baseline only: site-extras.js measures every badge against the chord of its
   own disc and sets the real size, because Alfa Slab One's advance width runs
   from .727em per character ("FUSSY") to .822em ("SPAM") and a character count
   cannot tell those apart — .27 was tuned on "100%", whose digits are the
   narrowest glyphs in the face, and it pushed SPAM 5.8px out of its padding
   box on 239 pages. The default is .24 rather than .27 so that even a
   four-letter word of all M's and W's stays on the plate with no JS at all;
   the fitter scales the roomy ones back up. Do not add a --w4. */
.sp-roundel__big{
  font-family:var(--display);
  font-size:calc(var(--roundel-size) * var(--roundel-big, .24));
  line-height:.9;
  letter-spacing:var(--track-tight);
  text-transform:uppercase;
  /* Never let a long word stretch the disc into an oval or spill past the
     ring, even with scripting off. */
  max-width:100%;
}
.sp-roundel--w5 .sp-roundel__big{ --roundel-big:.215; }
.sp-roundel--w6 .sp-roundel__big{ --roundel-big:.180; }
/* The sub-label had no length guard at all, and on the patterns and category
   stamps its string is data-driven — "118 PATTERNS" was already touching the
   ring, and the count only grows. Same treatment: a conservative baseline,
   then the fitter. */
.sp-roundel__sub{
  font-family:var(--caps);
  font-size:calc(var(--roundel-size) * var(--roundel-sub, .105));
  font-weight:600;
  text-transform:uppercase;
  letter-spacing:var(--track-caps);
  max-width:100%;
}
.sp-roundel--teal{    --roundel-bg:var(--teal);    --roundel-fg:var(--on-teal); }
.sp-roundel--ink{     --roundel-bg:var(--ink);     --roundel-fg:var(--on-ink); }
.sp-roundel--mustard{ --roundel-bg:var(--mustard); --roundel-fg:var(--on-mustard); }
.sp-roundel--sm{ --roundel-size:78px; }
.sp-roundel--lg{ --roundel-size:150px; }
.sp-roundel--right{ --roundel-tilt:8deg; }
a:hover > .sp-roundel,
.sp-roundel:hover{ rotate:0deg; scale:1.04; }

/* ── 5.5b · THE DRAWN MARKS (colour emoji, retired) ──────────────────────
   Screenshot review, and it was the loudest failure on the site: every
   decorative 🧶 renders from Segoe UI Emoji as a BRIGHT BLUE ball with a
   gold thread, and 🔍 as a blue lens. Blue appears nowhere in this palette.
   The masthead one sat beside the wordmark on all 130 pages, so the first
   thing a reader saw on a 1950s poster was a Windows emoji — exactly the
   "assembled from a UI kit" read the whole brief exists to avoid.

   The glyph is not deleted from the markup. It is zeroed and the system's
   own --m-yarnball / --m-search masks are painted in its place, tinted
   from currentColor. Doing it here rather than in six templates means the
   118 already-baked slug pages are fixed by the stylesheet alone, and a
   future rebrand recolours the mark with everything else.

   These are MASKS painted from a token — not the inherited `.logo-icon`
   background-image PNG rule, which stays deleted.                         */
.logo-icon,
.footer-logo-icon,
.newsletter-illustration,
.author-portrait--mark,
.sp-loading-mark,
.completion-modal-emoji,
.nl-popup-icon,
.sp-nlpop-icon,
.empty-icon,
.blog-img-placeholder,
#thumbnailPlaceholder,
.sp-mark-yarn,
.search-icon{
  /* !important, deliberately: every one of these elements has a component
     rule LATER in this file (or in article.css, which cascades ahead of it)
     that sets a font-size to scale the emoji. Same specificity, later wins,
     and the first pass shipped the blue ball printing underneath the mask.
     Nothing here has text content worth a size, so the flag is the honest
     fix rather than shadowing six component rules. */
  font-size:0 !important;      /* kills the glyph, keeps the box */
  color:var(--mark-color, inherit);
}
.logo-icon::before,
.footer-logo-icon::before,
.newsletter-illustration::before,
.author-portrait--mark::before,
.sp-loading-mark::before,
.completion-modal-emoji::before,
.nl-popup-icon::before,
.sp-nlpop-icon::before,
.empty-icon::before,
.blog-img-placeholder::before,
#thumbnailPlaceholder::before,
.sp-mark-yarn::before,
.search-icon::before{
  content:'';
  display:block; flex:none;
  width:var(--mark-size, 26px); height:var(--mark-size, 26px);
  background-color:currentColor;
  -webkit-mask-image:var(--m-yarnball); mask-image:var(--m-yarnball);
  -webkit-mask-size:contain;            mask-size:contain;
  -webkit-mask-repeat:no-repeat;        mask-repeat:no-repeat;
  -webkit-mask-position:center;         mask-position:center;
}
.logo-icon{ --mark-size:27px; }
.footer-logo-icon{ --mark-size:26px; }
/* the portrait disc is 78px; at the 26px default the mark read as a speck
   floating in an empty plate (screenshot) */
.author-portrait--mark{ --mark-size:44px; }
/* These three have no fixed box of their own — the emoji's font-size was
   doing the sizing, so they need explicit dimensions now. */
.newsletter-illustration{
  --mark-size:60px; width:60px; height:60px;
  color:var(--on-teal);
}
.sp-loading-mark{ --mark-size:52px; width:52px; height:52px; color:var(--red); }
.completion-modal-emoji{
  --mark-size:54px; width:54px; height:54px;
  margin-inline:auto; color:var(--red);
}
.nl-popup-icon,
.sp-nlpop-icon{ --mark-size:30px; width:30px; height:30px; }
.nl-popup-icon--error::before,
.sp-nlpop-icon--error::before{
  -webkit-mask-image:var(--m-cross); mask-image:var(--m-cross);
}
/* empty-state / missing-image placeholders */
.empty-icon{ --mark-size:66px; width:66px; height:66px; margin-inline:auto; color:var(--ink-3); }
.blog-img-placeholder,
#thumbnailPlaceholder{
  --mark-size:44px;
  display:grid; place-items:center;
  color:var(--ink-3);
}
/* the inline flavour: a mark that sits in a run of links or buttons */
.sp-mark-yarn{ --mark-size:19px; display:inline-grid; place-items:center; }
.search-icon{
  --mark-size:19px; width:19px; height:19px;
  color:var(--ink-2);
  -webkit-mask-image:none; mask-image:none;
}
.search-icon::before{
  -webkit-mask-image:var(--m-search); mask-image:var(--m-search);
}

/* ── 5.6 · RIBBON BANNER ─────────────────────────────────────────────────
   The starred "FRESH EVERY DAY ★ ALWAYS THE FINEST" bar. Full component,
   including the tape edges, lives in §14; the device hook is here so any
   band can borrow it.                                                       */

/* ── 5.7 · SUNBURST RAYS ─────────────────────────────────────────────────
   Radiating cream lines behind the hero image. Deliberately very low
   contrast — the rays must never sit between text and its ground. The mask
   fades them out before they reach the edges of the block.                  */
.sp-rays{ position:relative; isolation:isolate; }
.sp-rays::before{
  content:'';
  position:absolute; z-index:-1;
  top:50%; left:50%; translate:-50% -50%;
  width:var(--rays-size, 165%); aspect-ratio:1;
  background:repeating-conic-gradient(
    from 0deg at 50% 50%,
    var(--rays-color, var(--paper-light)) 0deg 5deg,
    transparent 5deg 13deg);
  opacity:var(--rays-opacity, .5);
  -webkit-mask-image:radial-gradient(closest-side, #000 25%, rgba(0,0,0,.35) 60%, transparent 80%);
          mask-image:radial-gradient(closest-side, #000 25%, rgba(0,0,0,.35) 60%, transparent 80%);
  pointer-events:none;
}
.sp-rays--mustard{ --rays-color:var(--mustard); --rays-opacity:.22; }
.sp-rays--ink{     --rays-color:var(--ink);     --rays-opacity:.07; }
.sp-rays--cream{   --rays-color:var(--paper-light); }
.sp-rays--spin::before{ animation:sp-spin 120s linear infinite; }

/* ── 5.8 · AGED PAPER TEXTURE ────────────────────────────────────────────
   The page ground carries it permanently (§3, body). .sp-grain adds it to
   any panel or band. Alpha is capped at .06 in the SVG itself: the flat
   worst-case blend darkens paper by ~3.5%, which leaves ink at 11.74:1,
   red-d at 5.60:1 and teal at 4.70:1 — all still AA. Raising it breaks AA.  */
.sp-grain{ position:relative; isolation:isolate; }
.sp-grain::after{
  content:'';
  position:absolute; inset:0; z-index:1;
  background-image:var(--tex-grain);
  background-size:220px 220px;
  opacity:var(--grain-opacity, 1);
  mix-blend-mode:multiply;
  pointer-events:none;
}
.sp-grain > *{ position:relative; z-index:2; }
.sp-grain--soft{ --grain-opacity:.55; }

/* Vignette, for panels that want the print-edge falloff on their own. */
.sp-vignette{
  background-image:radial-gradient(ellipse 118% 90% at 50% 40%, transparent 55%, rgba(43,33,24,.08) 100%);
}

/* ── 5.9 · DOTTED / TRIANGLE BORDER TAPE ─────────────────────────────────
   The GIN TONIC edge treatment. A strip element, or a modifier that grafts
   the strip onto the top/bottom of any block.                              */
.sp-tape{
  display:block; width:100%; height:var(--tape-h);
  background-repeat:repeat-x;
  background-position:center;
  pointer-events:none;
  --tape-c:var(--ink);
}
.sp-tape--dots{
  background-image:radial-gradient(circle at 50% 50%, var(--tape-c) 0 2.6px, transparent 2.8px);
  background-size:15px var(--tape-h);
}
.sp-tape--tri{
  background-image:conic-gradient(from 135deg at 50% 0, var(--tape-c) 0 90deg, transparent 90deg);
  background-size:22px var(--tape-h);
}
.sp-tape--tri-up{
  background-image:conic-gradient(from -45deg at 50% 100%, var(--tape-c) 0 90deg, transparent 90deg);
  background-size:22px var(--tape-h);
}
.sp-tape--dash{
  background-image:linear-gradient(90deg, var(--tape-c) 0 14px, transparent 14px 24px);
  background-size:24px var(--tape-h);
}
.sp-tape--diamond{
  background-image:var(--m-rule);
  background-image:none;
  background-color:transparent;
  -webkit-mask-image:var(--m-rule); mask-image:var(--m-rule);
  -webkit-mask-size:42px 14px;      mask-size:42px 14px;
  -webkit-mask-repeat:repeat-x;     mask-repeat:repeat-x;
  -webkit-mask-position:center;     mask-position:center;
  background-color:var(--tape-c);
  height:14px;
}
.sp-tape--mustard{ --tape-c:var(--mustard); }
.sp-tape--red{     --tape-c:var(--red); }
.sp-tape--teal{    --tape-c:var(--teal); }
.sp-tape--cream{   --tape-c:var(--paper-light); }
.sp-tape--thin{ --tape-h:8px; }

/* Grafted flavour: put tape on the edge of a section without extra markup */
.sp-taped-top{ position:relative; }
.sp-taped-top::before{
  content:''; position:absolute; left:0; right:0; top:0; height:var(--tape-h);
  z-index:2; pointer-events:none;
  background-image:conic-gradient(from 135deg at 50% 0, var(--tape-c, var(--ink)) 0 90deg, transparent 90deg);
  background-size:22px var(--tape-h);
  background-repeat:repeat-x;
}
.sp-taped-bottom{ position:relative; }
.sp-taped-bottom::after{
  content:''; position:absolute; left:0; right:0; bottom:0; height:var(--tape-h);
  z-index:2; pointer-events:none;
  background-image:radial-gradient(circle at 50% 50%, var(--tape-c, var(--ink)) 0 2.6px, transparent 2.8px);
  background-size:15px var(--tape-h);
  background-repeat:repeat-x;
}

/* ── 5.10 · STICKER ROTATION ─────────────────────────────────────────────
   Cards and badges sit slightly off-axis and straighten on hover, the way a
   pasted-on label does. Alternate the tilt so a grid never reads as a
   uniform skew.                                                             */
.sp-sticker{
  rotate:var(--tilt, -1.2deg);
  transition:rotate var(--t) var(--ease),
             translate var(--t) var(--ease),
             box-shadow var(--t) var(--ease);
}
.sp-sticker:hover,
.sp-sticker:focus-within{ rotate:0deg; translate:0 -4px; }
.sp-sticker--r{ --tilt:1.2deg; }
.sp-sticker--hard{ --tilt:-2deg; }
.sp-sticker--hard-r{ --tilt:2deg; }
/* Grid auto-alternation: every other child leans the other way. */
.sp-sticker-grid > *:nth-child(3n+1){ --tilt:-1.1deg; }
.sp-sticker-grid > *:nth-child(3n+2){ --tilt:.9deg; }
.sp-sticker-grid > *:nth-child(3n+3){ --tilt:-.6deg; }

/* ═══════════════════════════════════════════════════════════════════════════
   6 · TYPOGRAPHY
   ═══════════════════════════════════════════════════════════════════════ */

/* Headings default to the slab display voice. Every h1 inside an <article>
   or <section> is given an explicit size here, so a nested h1 can never
   fall back to the browser's shrunken UA size. */
h1,h2,h3,h4,h5,h6{
  font-family:var(--display);
  font-weight:var(--fw-display);
  line-height:var(--lh-tight);
  letter-spacing:var(--track-tight);
  color:var(--fg);
  margin:0 0 var(--sp-4);
  text-wrap:balance;
}
h1,
article h1, section h1, aside h1, nav h1{ font-size:var(--fs-h1); line-height:var(--lh-poster); text-transform:uppercase; }
h2{ font-size:var(--fs-h2); text-transform:uppercase; }
h3{ font-size:var(--fs-h3); }
h4{ font-size:1.1rem; }
h5,h6{ font-size:1rem; font-family:var(--caps); font-weight:600; text-transform:uppercase; letter-spacing:var(--track-caps); }

/* The four voices, as utilities. */
.sp-script{
  font-family:var(--script);
  font-weight:600;
  font-size:var(--fs-xl);
  line-height:1.25;
  letter-spacing:0;
  text-transform:none;
  color:var(--teal);      /* 5.01:1 on paper ✓ AA */
}
.sp-script--red{  color:var(--red-d); }    /* 5.98:1 ✓ */
.sp-script--ink{  color:var(--ink-2); }    /* 6.71:1 ✓ */
.sp-script--cream{ color:var(--on-red); }
.sp-script--lg{ font-size:clamp(1.7rem, 3.4vw, 2.6rem); }

.sp-caps{
  font-family:var(--caps);
  font-weight:600;
  text-transform:uppercase;
  letter-spacing:var(--track-caps);
  line-height:1.25;
}
.sp-caps--wide{ letter-spacing:var(--track-wide); }
.sp-caps--sm{ font-size:var(--fs-sm); }
.sp-caps--xs{ font-size:var(--fs-xs); }

.sp-slab{ font-family:var(--display); font-weight:var(--fw-display); letter-spacing:var(--track-tight); }
.sp-body{ font-family:var(--font); font-size:var(--fs-body); line-height:var(--lh-body); }

/* Eyebrow / kicker: condensed caps over a diamond rule. */
.sp-eyebrow,
.patterns-kicker{
  display:inline-flex; align-items:center; gap:var(--sp-2);
  font-family:var(--caps); font-weight:600; font-size:var(--fs-xs);
  text-transform:uppercase; letter-spacing:var(--track-wide);
  color:var(--teal-d);              /* 7.26:1 ✓ AAA */
  margin:0 0 var(--sp-3);
}
.sp-eyebrow::before,
.patterns-kicker::before{
  content:''; width:16px; height:16px; flex:none;
  background-color:var(--mustard);
  -webkit-mask-image:var(--m-star12); mask-image:var(--m-star12);
  -webkit-mask-size:contain;          mask-size:contain;
  -webkit-mask-repeat:no-repeat;      mask-repeat:no-repeat;
}

/* Lede / standfirst */
.sp-lede,
.patterns-lede{
  font-size:var(--fs-lg);
  line-height:var(--lh-loose);
  color:var(--ink-2);              /* 6.71:1 ✓ */
  max-width:var(--measure);
  margin:0 0 var(--sp-5);
}

/* Pull quote — the poster aside, set in script */
.sp-pull{
  font-family:var(--script);
  font-size:clamp(1.5rem, 3vw, 2.2rem);
  line-height:1.3;
  color:var(--red-d);
  border-left:var(--bw-slab) solid var(--mustard);
  padding-left:var(--sp-4);
  margin:var(--sp-6) 0;
}

/* Highlighter mark — mustard ground, ink text (7.19:1 ✓ AAA) */
.sp-mark,
mark{
  background:var(--mustard);
  color:var(--on-mustard);
  padding:.05em .3em;
  border-radius:var(--r-xs);
  box-decoration-break:clone;
  -webkit-box-decoration-break:clone;
}

/* Section heading + its diamond rule flourish. */
.sp-h2,
.section-title,
.content-title,
.qa-title,
.recommendations-title,
.newsletter-title,
.patterns-h1,
.sp-h1,
.sp-h3{ font-family:var(--display); font-weight:var(--fw-display); letter-spacing:var(--track-tight); }

.sp-h2{
  font-size:var(--fs-h2);
  /* Was inheriting --lh-tight (1.08) from the bare h2 rule while carrying a
     3px shadow: 2.18px of leading at 27.3px on a phone, so the shadow landed
     inside the next line box. Four homepage H2s wrap at 390px. */
  line-height:var(--lh-poster);
  text-transform:uppercase;
  color:var(--red);                 /* ≥24px display → 4.22:1 ≥ 3:1 ✓ */
  text-shadow:3px 3px 0 var(--ink);
  margin:0 0 var(--sp-5);
}
.sp-h2--teal{ color:var(--teal); }
.sp-h2--ink{ color:var(--ink); text-shadow:3px 3px 0 var(--mustard); }
.sp-h2--cream{ color:var(--on-red); }

.sp-rule{
  display:block; height:14px; width:126px;   /* exactly 3 diamond repeats */
  margin:var(--sp-3) 0 var(--sp-5);
  background-color:var(--mustard);
  -webkit-mask-image:var(--m-rule); mask-image:var(--m-rule);
  -webkit-mask-size:42px 14px;      mask-size:42px 14px;
  -webkit-mask-repeat:repeat-x;     mask-repeat:repeat-x;
}
.sp-rule--center{ margin-left:auto; margin-right:auto; }
.sp-rule--full{ width:100%; }
.sp-rule--ink{ background-color:var(--ink); }
.sp-rule--cream{ background-color:var(--on-red); }

/* ═══════════════════════════════════════════════════════════════════════════
   7 · LAYOUT PRIMITIVES
   ═══════════════════════════════════════════════════════════════════════ */
.container,
.sp-wrap{
  width:100%; max-width:var(--container);
  margin-inline:auto;
  padding-inline:var(--gutter);
}
.sp-wrap--narrow,
.sp-wrap-narrow{ max-width:860px; }
.sp-wrap--prose{ max-width:min(100%, 78ch); }

.sp-section{
  position:relative;
  padding-block:var(--sp-8);
  background-color:var(--section-bg, transparent);
}
.sp-section--tight,
.sp-section-tight{ padding-block:var(--sp-6); }
.sp-section--tall{ padding-block:var(--sp-9); }

/* Bands. Each sets --section-bg so the scallop device in §5.2 picks up the
   right fill with no extra class, and redeclares --focus + --link when the
   ground is dark. */
.sp-band--paper       { --section-bg:var(--paper);       background-color:var(--paper); }
.sp-band--paper-light { --section-bg:var(--paper-light); background-color:var(--paper-light); }
.sp-band--cream       { --section-bg:var(--cream-deep);  background-color:var(--cream-deep); }

.sp-band--teal{
  --section-bg:var(--teal);
  background-color:var(--teal);
  color:var(--on-teal);          /* 5.90:1 ✓ AA */
  --fg:var(--on-teal);
  --fg-mut:var(--on-teal);
  --fg-faint:#e2d8c0;            /* 4.79:1 on teal ✓ AA */
  --link:var(--on-teal);
  --link-hover:var(--mustard);
  --line:var(--on-red);
  --focus:var(--on-red);         /* 5.90:1 against the band ✓ */
}
.sp-band--red{
  --section-bg:var(--red);
  background-color:var(--red);
  color:var(--on-red);           /* 4.96:1 ✓ AA */
  --fg:var(--on-red);
  --fg-mut:var(--on-red);
  --fg-faint:var(--on-red);
  --link:var(--on-red);
  --link-hover:var(--mustard);
  --line:var(--ink);
  --focus:var(--on-red);         /* 4.96:1 against the band ✓ */
}
.sp-band--ink{
  --section-bg:var(--ink);
  background-color:var(--ink);
  color:var(--on-ink);           /* 12.53:1 ✓ AAA */
  --fg:var(--on-ink);
  --fg-mut:var(--on-ink-mut);    /*  9.17:1 ✓ AAA */
  --fg-faint:var(--on-ink-mut);
  --link:var(--mustard);         /*  7.19:1 ✓ AAA */
  --link-hover:var(--on-ink);
  --line:var(--on-ink);
  --focus:var(--mustard);
}
.sp-band--teal *, .sp-band--red *, .sp-band--ink *{ border-color:currentColor; }
.sp-band--teal h1,.sp-band--teal h2,.sp-band--teal h3,
.sp-band--red  h1,.sp-band--red  h2,.sp-band--red  h3,
.sp-band--ink  h1,.sp-band--ink  h2,.sp-band--ink  h3{ color:inherit; }

/* Panels — the card stock every content block is printed on. */
.sp-panel{
  position:relative;
  background:var(--paper-light);
  border:var(--bw) solid var(--line);
  border-radius:var(--r);
  box-shadow:var(--pop);
  padding:var(--sp-6);
}
.sp-panel--flat{ box-shadow:none; }
.sp-panel--deep{ box-shadow:var(--pop-lg); }
.sp-panel--cream{ background:var(--cream-deep); }
.sp-panel--teal{ background:var(--teal); color:var(--on-teal); --focus:var(--on-red); --link:var(--on-teal); }
.sp-panel--red{  background:var(--red);  color:var(--on-red);  --focus:var(--on-red); --link:var(--on-red); }
.sp-panel--slab{ border-width:var(--bw-thick); }

.sp-panel-side{ background:var(--cream-deep); border:var(--bw) solid var(--line); border-radius:var(--r); padding:var(--sp-5); box-shadow:var(--pop-sm); }

/* NB: the bare `.sp-grid` name is deliberately NOT a generic utility — after
   the prefix rename it belongs to the pattern grid in §11. Column helpers
   carry their own display:grid. */
.sp-cols-2{ display:grid; gap:var(--sp-5); grid-template-columns:repeat(2, minmax(0,1fr)); }
.sp-cols-3{ display:grid; gap:var(--sp-5); grid-template-columns:repeat(3, minmax(0,1fr)); }
.sp-cols-4{ display:grid; gap:var(--sp-5); grid-template-columns:repeat(4, minmax(0,1fr)); }
.sp-stack{ display:flex; flex-direction:column; gap:var(--sp-5); }
.sp-row{ display:flex; flex-wrap:wrap; align-items:center; gap:var(--sp-3); }

/* ═══════════════════════════════════════════════════════════════════════════
   8 · BUTTONS + CONTROLS

   Physics: a button sits proud on a hard offset shadow. On :hover it lifts
   (the shadow grows). On :active it presses INTO the page — the element
   translates by --press and the shadow collapses to flat. Focus is a 3px
   ring at 3px offset in --focus, which every band redeclares.
   CONTRAST: primary = --on-red on --red 4.96:1 ✓ · secondary = --on-teal on
   --teal 5.90:1 ✓ · ghost = --ink on --paper-light 13.90:1 ✓ · mustard =
   --ink on --mustard 7.19:1 ✓.
   ═══════════════════════════════════════════════════════════════════════ */
.sp-btn,
.sp-btn,
.cta-button,
.submit-button,
.btn-secondary,
.pattern-btn,
.page-btn,
.footer-btn,
.back-btn,
.blog-read-more{
  display:inline-flex; align-items:center; justify-content:center;
  gap:var(--sp-2);
  min-height:52px;
  padding:14px var(--sp-5);
  font-family:var(--caps);
  font-weight:600;
  font-size:1rem;
  text-transform:uppercase;
  letter-spacing:var(--track-caps);
  text-decoration:none;
  text-align:center;
  cursor:pointer;
  color:var(--ink);
  background:var(--paper-light);
  border:var(--bw) solid var(--line);
  border-radius:var(--r-sm);
  box-shadow:var(--pop);
  transition:translate var(--t-fast) var(--ease),
             box-shadow var(--t-fast) var(--ease),
             background-color var(--t-fast) var(--ease),
             color var(--t-fast) var(--ease);
}
.sp-btn:hover,
.cta-button:hover,
.submit-button:hover,
.btn-secondary:hover,
.pattern-btn:hover,
.page-btn:hover,
.footer-btn:hover,
.back-btn:hover,
.blog-read-more:hover{
  translate:-2px -2px;
  box-shadow:var(--pop-lg);
}
.sp-btn:active,
.cta-button:active,
.submit-button:active,
.btn-secondary:active,
.pattern-btn:active,
.page-btn:active,
.footer-btn:active,
.back-btn:active,
.blog-read-more:active{
  translate:var(--press) var(--press);
  box-shadow:var(--pop-in);
}
.sp-btn:focus-visible,
.cta-button:focus-visible,
.submit-button:focus-visible,
.btn-secondary:focus-visible,
.pattern-btn:focus-visible,
.page-btn:focus-visible,
.footer-btn:focus-visible,
.back-btn:focus-visible{ outline:3px solid var(--focus); outline-offset:3px; }

.sp-btn--primary,
.sp-btn-primary,
.cta-button,
.submit-button{ background:var(--red);  color:var(--on-red); }
.sp-btn--primary:hover,
.sp-btn-primary:hover,
.cta-button:hover,
.submit-button:hover{ background:var(--red-d); color:var(--on-red); }

.sp-btn--secondary,
.btn-secondary{ background:var(--teal); color:var(--on-teal); }
.sp-btn--secondary:hover,
.btn-secondary:hover{ background:var(--teal-d); color:var(--on-teal); }

.sp-btn--mustard{ background:var(--mustard); color:var(--on-mustard); }
.sp-btn--mustard:hover{ background:#cf9524; color:var(--on-mustard); }

.sp-btn--ink,
.footer-btn-accent{ background:var(--ink); color:var(--on-ink); }
.sp-btn--ink:hover,
.footer-btn-accent:hover{ background:#3c2e21; color:var(--on-ink); }

.sp-btn--ghost,
.sp-btn-ghost,
.footer-btn-plain{ background:transparent; color:var(--fg); box-shadow:var(--pop-sm); }
.sp-btn--ghost:hover,
.sp-btn-ghost:hover,
.footer-btn-plain:hover{ background:var(--paper-light); color:var(--fg); box-shadow:var(--pop); }

.sp-btn--lg{ min-height:60px; padding:18px var(--sp-6); font-size:1.1rem; }
.sp-btn--sm{ min-height:44px; padding:10px var(--sp-4); font-size:var(--fs-sm); }
.sp-btn--block{ width:100%; }

.sp-btn[disabled],
.sp-btn.disabled,
button[disabled],
.submit-button[disabled],
.page-btn.disabled{
  opacity:.55; cursor:not-allowed;
  translate:3px 3px; box-shadow:var(--pop-in);
  pointer-events:none;
}

.btn-row,
.footer-actions{ display:flex; flex-wrap:wrap; gap:var(--sp-3); }
.btn-row-mt{ margin-top:var(--sp-5); }
.back-row{ margin-bottom:var(--sp-5); }

/* ═══════════════════════════════════════════════════════════════════════════
   9 · MASTHEAD + NAV
   The shop sign: cream stock, heavy ink keyline, triangle tape underneath.
   ═══════════════════════════════════════════════════════════════════════ */
.navbar{
  position:sticky; top:0; z-index:100;
  background:var(--paper-light);
  border-bottom:var(--bw-thick) solid var(--line);
  box-shadow:0 4px 0 rgba(43,33,24,.14);
}
/* the GIN TONIC edge treatment under the masthead.

   The strip must be OPAQUE. The masthead is position:sticky, and this
   pseudo-element hangs 16px BELOW the navbar's own painted box — so the page
   scrolled underneath it and showed through the transparent gaps between the
   triangles. On a long article you could read the body copy in the sawtooth.
   Painting the strip in the masthead's own paper makes the whole thing read as
   one sticky block with a cut edge, which is what it always looked like at the
   top of the page anyway.

   The element now covers the 4px gap under the border as well (that gap leaked
   too); background-position keeps the triangles below it. */
.navbar::after{
  content:''; position:absolute; left:0; right:0;
  bottom:calc(-1 * var(--tape-h) - 4px);
  height:calc(var(--tape-h) + 4px);
  pointer-events:none; z-index:1;
  background-color:var(--paper-light);
  background-image:conic-gradient(from 135deg at 50% 0, var(--red) 0 90deg, transparent 90deg);
  background-size:22px var(--tape-h);
  background-position:0 4px;
  background-repeat:repeat-x;
}
.navbar-inner{
  max-width:var(--container); margin-inline:auto;
  padding:var(--sp-3) var(--gutter);
  min-height:var(--nav-h);
  display:flex; align-items:center; justify-content:space-between; gap:var(--sp-4);
}

.logo{ display:inline-flex; align-items:center; gap:var(--sp-3); text-decoration:none; color:var(--ink); }
.logo-icon,
.footer-logo-icon{
  display:grid; place-items:center; flex:none;
  width:46px; height:46px;
  font-size:1.45rem; line-height:1;
  background:var(--mustard);
  color:var(--on-mustard);
  border:var(--bw) solid var(--line);
  border-radius:50%;
  box-shadow:var(--pop-sm);
  rotate:-6deg;
  transition:rotate var(--t) var(--ease);
}
.logo:hover .logo-icon{ rotate:6deg; }
.logo-text{ display:flex; flex-direction:column; line-height:1; }
.logo-name,
.footer-brand-name{
  font-family:var(--display); font-weight:var(--fw-display);
  font-size:1.5rem; letter-spacing:var(--track-tight);
  text-transform:uppercase; color:var(--ink);
  text-shadow:2px 2px 0 var(--mustard);
}
.logo-sub{
  font-family:var(--script); font-size:1rem; font-weight:600;
  color:var(--teal-d);            /* 7.26:1 ✓ AAA */
  margin-top:2px;
}
.logo:focus-visible{ outline:3px solid var(--focus); outline-offset:4px; }

.nav-links{ display:flex; align-items:center; gap:var(--sp-2); list-style:none; margin:0; padding:0; }
.nav-links li{ margin:0; }
.nav-link{
  display:inline-flex; align-items:center;
  min-height:44px; padding:8px var(--sp-3);
  font-family:var(--caps); font-weight:500; font-size:1rem;
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--ink);                /* 13.90:1 on the cream masthead ✓ AAA */
  text-decoration:none;
  border:var(--bw) solid transparent;
  border-radius:var(--r-sm);
  transition:background-color var(--t-fast) var(--ease),
             color var(--t-fast) var(--ease),
             border-color var(--t-fast) var(--ease);
}
.nav-link:hover{ background:var(--cream-deep); color:var(--ink); border-color:var(--line); }
.nav-link:focus-visible{ outline:3px solid var(--focus); outline-offset:2px; }
.nav-link.active,
.nav-link[aria-current="page"]{
  background:var(--red); color:var(--on-red);   /* 4.96:1 ✓ AA */
  border-color:var(--line);
  box-shadow:var(--pop-sm);
}

.menu-toggle{
  display:none;
  width:48px; height:48px;
  /* It is a flex item in .navbar-inner, so without this it is allowed to
     shrink below the 44px WCAG 2.5.8 target once the wordmark grows — measured
     at 38.6px on a 320px screen, the width where a tap target matters most. */
  flex:0 0 auto; min-width:48px;
  flex-direction:column;
  align-items:center; justify-content:center;
  gap:5px;
  padding:0;
  background:var(--paper-light); color:var(--ink);
  border:var(--bw) solid var(--line); border-radius:var(--r-sm);
  box-shadow:var(--pop-sm); cursor:pointer;
}
/* The three bars. Every page's markup is <button><span/><span/><span/></button>
   and the spans had no rule at all, so the toggle shipped as an empty outlined
   square — a mobile menu with no icon in it. Caught by screenshot at 552px. */
.menu-toggle span{
  display:block; width:24px; height:3px;
  background:var(--ink);
  border-radius:2px;
  transition:translate var(--t) var(--ease), rotate var(--t) var(--ease), opacity var(--t) var(--ease);
}
.menu-toggle[aria-expanded="true"] span:nth-child(1){ translate:0 8px; rotate:45deg; }
.menu-toggle[aria-expanded="true"] span:nth-child(2){ opacity:0; }
.menu-toggle[aria-expanded="true"] span:nth-child(3){ translate:0 -8px; rotate:-45deg; }
.menu-toggle:active{ translate:3px 3px; box-shadow:var(--pop-in); }

/* The closed drawer must be HIDDEN, not merely pushed off-screen. Translated
   alone it stays visible:visible, so all eight links keep their place in the
   tab order and in the accessibility tree at every width — a keyboard user
   tabbed through eight invisible duplicates of the nav, with the focus ring
   painted 340px off the right edge, before reaching the page. `visibility` is
   in the transition list so the slide still animates out (it flips at the end
   of the transition, not at the start). */
.mobile-nav{
  position:fixed; top:0; right:0; z-index:130;
  width:min(86vw, 340px); height:100dvh;
  padding:var(--sp-7) var(--sp-5);
  padding-bottom:max(var(--sp-7), env(safe-area-inset-bottom));
  background:var(--paper-light);
  border-left:var(--bw-thick) solid var(--line);
  translate:100% 0;
  visibility:hidden;
  /* Asymmetric on purpose. `visibility` is a discrete property: transitioned
     normally it still computes to `hidden` at progress 0, so a .focus() call
     in the same tick as the class change is a no-op and the drawer opens with
     focus left behind on the page. Opening flips it with no delay (see the
     .open rule); closing delays the flip until the slide has finished, so the
     panel does not vanish mid-animation. */
  transition:translate var(--t-slow) var(--ease-out),
             visibility 0s linear var(--t-slow);
  overflow-y:auto;
  overscroll-behavior:contain;
  display:flex; flex-direction:column; gap:var(--sp-2);
}
.mobile-nav.is-active,
.mobile-nav.active,
.mobile-nav.open{
  translate:0 0;
  visibility:visible;
  transition:translate var(--t-slow) var(--ease-out),
             visibility 0s linear 0s;
}
/* Belt and braces for the rotation case: an iPad that opens the drawer in
   portrait and turns to landscape crosses the 900px masthead breakpoint, which
   hides .menu-toggle — leaving an open panel with no control left to close it.
   The JS controller force-closes on the matchMedia change; this makes the
   panel unpaintable next to the desktop link row even if that never runs. */
@media (min-width:961px){
  .mobile-nav{ display:none; }
}
.mobile-nav-link{
  display:block; padding:var(--sp-3) var(--sp-4);
  min-height:52px;
  font-family:var(--caps); font-weight:600; font-size:1.15rem;
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--ink); text-decoration:none;
  border-bottom:var(--bw-hair) solid var(--line-soft);
}
.mobile-nav-link:hover{ background:var(--cream-deep); color:var(--ink); }
/* The drawer is the keyboard user's whole nav below 960px, and its links had
   a :hover state only — no ring, no pressed state. Inset ring, because the
   panel clips at its left border and an outset ring would lose an edge. */
.mobile-nav-link:focus-visible{
  outline:3px solid var(--focus); outline-offset:-3px;
  background:var(--cream-deep); color:var(--ink);
}
.mobile-nav-link:active{ background:var(--mustard); color:var(--on-mustard); }   /* 7.19:1 ✓ */
.mobile-overlay{
  position:fixed; inset:0; z-index:120;
  background:rgba(43,33,24,.55);
  opacity:0; visibility:hidden;
  transition:opacity var(--t) var(--ease), visibility var(--t) var(--ease);
}
.mobile-overlay.is-active,
.mobile-overlay.active{ opacity:1; visibility:visible; }

/* Masthead search box */
.search-box,
.sp-searchbar{
  display:flex; align-items:center; gap:var(--sp-2);
  background:var(--paper);
  border:var(--bw) solid var(--line);
  border-radius:var(--r-sm);
  box-shadow:var(--pop-sm);
  padding:0 var(--sp-3);
}
.search-box:focus-within,
.sp-searchbar:focus-within{ outline:3px solid var(--focus); outline-offset:2px; }
.search-icon{ flex:none; color:var(--ink-2); font-size:1.05rem; }
.search-input{
  border:0; background:transparent; outline:none;
  min-height:46px; min-width:0; width:100%;
  font-family:var(--font); font-size:var(--fs-body); color:var(--ink);
}
.search-input::placeholder{ color:var(--ink-3); opacity:1; }   /* 5.44:1 ✓ */

/* ═══════════════════════════════════════════════════════════════════════════
   10 · HERO
   ═══════════════════════════════════════════════════════════════════════ */
.hero-section,
.sp-hero{
  position:relative; isolation:isolate;
  --section-bg:var(--paper);
  /* Was --sp-8/--sp-9 (76/104). 180px of padding plus a poster headline
     pushed the call to action off a 768px-tall laptop. */
  padding-block:var(--sp-7) var(--sp-8);
  overflow:hidden;
}
.hero-title,
.sp-h1{
  font-family:var(--display); font-weight:var(--fw-display);
  font-size:var(--fs-h1);
  line-height:var(--lh-poster);
  letter-spacing:var(--track-tight);
  text-transform:uppercase;
  color:var(--red);                      /* ≥24px display → 4.22:1 ≥ 3:1 ✓ */
  text-shadow:var(--hard-x) var(--hard-y) 0 var(--ink);
  margin:0 0 var(--sp-5);
  /* 16ch forced the homepage headline onto five lines. 19ch lets it set in
     three without letting it run past a comfortable display measure. */
  max-width:19ch;
}
.sp-sub,
.article-desc{
  font-size:var(--fs-lg); line-height:var(--lh-loose);
  color:var(--ink-2);                    /* 6.71:1 ✓ AA */
  max-width:var(--measure);
  margin:0 0 var(--sp-6);
}
.sp-hero-actions{ display:flex; flex-wrap:wrap; gap:var(--sp-3); margin-bottom:var(--sp-6); }
.sp-hero-grid{
  display:grid; gap:var(--sp-7);
  /* Copy column widened from 1.05fr: the headline is the loudest thing on
     the page and it needs room to set in three lines, not five. */
  grid-template-columns:minmax(0, 1.15fr) minmax(0, .85fr);
  align-items:center;
}
/* min-width:0 is a guard, not tidiness. A grid item's automatic minimum size
   is content-based, and for an item holding a replaced element the browser can
   take that from the image's INTRINSIC width — 900px for the hero photograph.
   The declared track is minmax(0,1fr) so the track itself is already immune;
   this stops the ITEM from overflowing its own track if the plate ever gains a
   wider child (a table, a long unbroken URL, a second image). Measured: with
   both in place, document.body.scrollWidth equals the viewport width at every
   width from 511px to 1440px. */
.sp-hero-grid > *{ min-width:0; }
.sp-hero-art{ position:relative; }
.sp-hero-card,
.sp-hero-card-1,
.sp-hero-card-2{
  background:var(--paper-light);
  border:var(--bw) solid var(--line);
  border-radius:var(--r);
  box-shadow:var(--pop-lg);
  overflow:hidden;
}
.sp-hero-card-1{ rotate:-2deg; }
.sp-hero-card-2{ rotate:2.5deg; }
.sp-mosaic{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--sp-4); }

/* Floating doodles. Decoration only — every one needs aria-hidden="true" in
   the markup, and the whole layer is hidden below 1100px. */
.floating-elements,
.decorative-elements{ position:absolute; inset:0; z-index:0; pointer-events:none; }
.float-item{
  position:absolute;
  width:var(--float-size, 74px); height:var(--float-size, 74px);
  background-color:var(--float-color, var(--teal));
  opacity:.24;
  -webkit-mask-size:contain;     mask-size:contain;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-position:center;  mask-position:center;
  animation:sp-float 9s ease-in-out infinite;
}
.float-1{ top:9%;  left:4%;  --float-color:var(--red);
  -webkit-mask-image:var(--m-yarnball); mask-image:var(--m-yarnball); }
.float-2{ top:22%; right:5%; --float-color:var(--teal); --float-size:60px; animation-delay:-2.4s;
  -webkit-mask-image:var(--m-hook);     mask-image:var(--m-hook); }
.float-3{ bottom:14%; left:8%; --float-color:var(--mustard); --float-size:52px; animation-delay:-4.8s;
  -webkit-mask-image:var(--m-scissors); mask-image:var(--m-scissors); }
.float-4{ bottom:9%; right:11%; --float-color:var(--teal); --float-size:44px; animation-delay:-6.5s;
  -webkit-mask-image:var(--m-star12);   mask-image:var(--m-star12); }
@keyframes sp-float{
  0%,100%{ translate:0 0;     rotate:-4deg; }
  50%    { translate:0 -14px; rotate:4deg; }
}

/* Hero decorative tag pills (🧶 Beautiful / ✨ Detailed …) */
.tag{
  display:inline-flex; align-items:center; gap:6px;
  padding:7px var(--sp-3);
  font-family:var(--caps); font-weight:600; font-size:var(--fs-xs);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  background:var(--paper-light); color:var(--ink);   /* 13.90:1 ✓ AAA */
  border:var(--bw) solid var(--line);
  border-radius:var(--r-pill);
  box-shadow:var(--pop-sm);
  rotate:-1.5deg;
}
.tag-1{ background:var(--mustard); color:var(--on-mustard); rotate:-2deg; }  /* 7.19:1 ✓ */
.tag-2{ background:var(--teal);    color:var(--on-teal);    rotate:1.5deg; } /* 5.90:1 ✓ */
.tag-3{ background:var(--red);     color:var(--on-red);     rotate:-1deg; }  /* 4.96:1 ✓ */

/* ── 10b · THE HOMEPAGE POSTER ───────────────────────────────────────────
   The hero photo sits on a card-stock plate with filigree corners, sunburst
   rays behind it, a rotated "100% FREE" roundel hanging off the top-right,
   and two script asides pinned to its edges. Everything is absolutely
   positioned against .sp-hero-art, and .sp-hero clips (overflow:hidden), so
   an aside that hangs past the plate is cut by the section edge rather than
   widening the page.                                                        */
.sp-hero-kicker{ margin:0 0 var(--sp-2); }

.sp-hero-plate{
  position:relative; z-index:1;
  margin:0;
  padding:var(--sp-3);
}
.sp-hero-plate img{
  width:100%; height:auto;
  border:var(--bw) solid var(--line);
  border-radius:var(--r-sm);
  background:var(--cream-deep);
}
.sp-hero-caption{
  margin:var(--sp-3) 0 var(--sp-1);
  text-align:center;
  font-size:1.25rem;
  color:var(--teal-d);                      /* 7.26:1 on card stock ✓ AAA */
}
.sp-hero-roundel{
  position:absolute; z-index:5;
  top:calc(-1 * var(--sp-5)); right:calc(-1 * var(--sp-4));
  pointer-events:none;
}
/* The two pinned asides. §10 already gives .sp-hero-card its card surface
   and the -1/-2 modifiers their tilt; these rules place and set them. */
.sp-hero-card{
  position:absolute; z-index:5;
  margin:0;
  padding:var(--sp-3) var(--sp-4);
  max-width:190px;
  font-family:var(--script);
  font-size:1.3rem; line-height:1.15;
  color:var(--ink);                         /* 13.90:1 on card stock ✓ AAA */
  text-align:center;
}
.sp-hero-card-1{ left:calc(-1 * var(--sp-5)); bottom:16%; }
.sp-hero-card-2{ right:calc(-1 * var(--sp-4)); bottom:calc(-1 * var(--sp-4)); }

/* Skill-level cards. Each carries a roundel badge, so the anchor is a column
   and the "→" link sits on the baseline of the tallest card in the row. */
.sp-level-row{ align-items:stretch; }
.sp-level{
  display:flex; flex-direction:column; align-items:flex-start;
  gap:var(--sp-3);
  text-decoration:none;
  color:var(--ink);
  transition:translate var(--t) var(--ease), box-shadow var(--t) var(--ease), rotate var(--t) var(--ease);
}
.sp-level:hover{ color:var(--ink); box-shadow:var(--pop-lg); }
.sp-level-title{
  font-family:var(--display); font-size:1.45rem;
  text-transform:uppercase; letter-spacing:var(--track-tight);
  margin:0;
}
.sp-level-desc{
  font-size:var(--fs-sm); line-height:1.62;
  color:var(--ink-2);                       /* 6.71:1 on cream stock ✓ AA */
  margin:0;
}
.sp-level .sp-more{ margin-top:auto; }

/* Shared form furniture. These four are duplicated in article.css §9 with
   identical values, for the same reason as .footer-legal in §15: article
   pages load article.css FIRST, so this file wins an equal-specificity tie
   and a drift here would silently restyle the article newsletter. */
.newsletter-kicker{ margin:0 0 var(--sp-2); }
.newsletter-seal{ align-self:flex-start; margin-top:var(--sp-5); }
.req{ color:var(--red-d); }                 /* 6.64:1 on card stock ✓ AA */
.nl-live{ margin:0; }
.newsletter-form .submit-button{ width:100%; }

/* ═══════════════════════════════════════════════════════════════════════════
   11 · CARDS + GRIDS
   The homepage featured grid is emitted by compile.py :: card_html(). Those
   class names are a build contract — restyle them, never rename them.
   ═══════════════════════════════════════════════════════════════════════ */
.featured-grid,
.sp-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(230px, 1fr));
  gap:var(--sp-5);
}
.featured-card{
  position:relative; display:flex; flex-direction:column;
  background:var(--paper-light);
  border:var(--bw) solid var(--line);
  border-radius:var(--r);
  box-shadow:var(--pop);
  overflow:hidden;
  text-decoration:none; color:var(--ink);
  rotate:var(--tilt, -1deg);
  transition:rotate var(--t) var(--ease),
             translate var(--t) var(--ease),
             box-shadow var(--t) var(--ease);
}
.featured-grid > .featured-card:nth-child(3n+2){ --tilt:1deg; }
.featured-grid > .featured-card:nth-child(3n+3){ --tilt:-.5deg; }
.featured-card:hover{ rotate:0deg; translate:0 -5px; box-shadow:var(--pop-lg); color:var(--ink); }
.featured-card:focus-visible{ outline:3px solid var(--focus); outline-offset:3px; rotate:0deg; }
/* mustard corner starburst — the brief's sticker flash.
   Inside the card's box, not hung off its corner: the card keeps
   overflow:hidden so the photo's top corners follow the 10px radius, which
   means anything at a negative offset is clipped — the first version showed
   three-quarters of a star on all twelve cards. A 50px burst 10px in sits on
   the top-right of the photograph, whole, like a sticker pressed on a print. */
.featured-card::after{
  content:''; position:absolute; z-index:3;
  top:10px; right:10px; width:50px; height:50px;
  background-color:var(--mustard);
  -webkit-mask-image:var(--m-burst16); mask-image:var(--m-burst16);
  -webkit-mask-size:contain;           mask-size:contain;
  -webkit-mask-repeat:no-repeat;       mask-repeat:no-repeat;
  pointer-events:none;
}
/* NB: .featured-img is the WRAPPER div, not the <img> — that is the shape
   compile.py :: card_html() and search.js :: cardHTML() both emit. The
   aspect-ratio therefore has to be enforced on the child, or a photo whose
   intrinsic ratio differs (search.js declares 400x400, compile.py 400x300)
   sets the box height from its own min-content and the grid goes ragged. */
.featured-img{
  display:block; width:100%; aspect-ratio:4/3;
  overflow:hidden;
  border-bottom:var(--bw) solid var(--line);
  background:var(--cream-deep);
}
.featured-img > img{
  display:block; width:100%; height:100%;
  object-fit:cover; object-position:center;
}
.featured-body{ display:flex; flex-direction:column; gap:var(--sp-2); padding:var(--sp-4); flex:1; }
.featured-ribbon{
  position:absolute; top:var(--sp-3); left:0; z-index:4;
  padding:5px var(--sp-3) 5px var(--sp-2);
  font-family:var(--caps); font-weight:600; font-size:var(--fs-xs);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  background:var(--red); color:var(--on-red);       /* 4.96:1 ✓ AA */
  border:var(--bw) solid var(--line); border-left:0;
  border-radius:0 var(--r-sm) var(--r-sm) 0;
  box-shadow:var(--pop-sm);
}
.featured-skill,
.skill-level,
.category-badge,
.blog-category,
.article-category-badge,
.news-tag,
.sp-chip{
  display:inline-flex; align-items:center; gap:6px;
  max-width:100%;
  padding:5px var(--sp-3);
  font-family:var(--caps); font-weight:600; font-size:var(--fs-xs);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  background:var(--cream-deep); color:var(--on-cream);   /* 10.74:1 ✓ AAA */
  border:var(--bw-hair) solid var(--line);
  border-radius:var(--r-pill);
  text-decoration:none;
  align-self:flex-start;
}
.featured-title{
  font-family:var(--display); font-weight:var(--fw-display);
  font-size:1.02rem; line-height:1.2; letter-spacing:var(--track-tight);
  color:var(--ink); margin:0;
  display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
}
.featured-desc{
  font-size:var(--fs-sm); line-height:1.55; color:var(--ink-2);   /* 7.44:1 ✓ */
  margin:0;
  display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
}
.featured-cta-inline{
  margin-top:auto;
  font-family:var(--caps); font-weight:600; font-size:var(--fs-xs);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--red-d);                                   /* 6.64:1 ✓ AA */
}
.featured-card:hover .featured-cta-inline{ color:var(--red-deep); }

/* Category cards */
.sp-cats{ display:grid; grid-template-columns:repeat(auto-fill, minmax(240px,1fr)); gap:var(--sp-5); }
.sp-cat{
  display:flex; flex-direction:column;
  background:var(--paper-light); border:var(--bw) solid var(--line);
  border-radius:var(--r); box-shadow:var(--pop); overflow:hidden;
  text-decoration:none; color:var(--ink);
  transition:translate var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.sp-cat:hover{ translate:0 -4px; box-shadow:var(--pop-lg); color:var(--ink); }
.sp-cat-img{ width:100%; aspect-ratio:16/10; object-fit:cover; border-bottom:var(--bw) solid var(--line); }
.sp-cat-body{ padding:var(--sp-4); display:flex; flex-direction:column; gap:var(--sp-2); }
.sp-cat-title{ font-family:var(--display); font-size:1.15rem; letter-spacing:var(--track-tight); margin:0; }
.sp-cat-desc{ font-size:var(--fs-sm); color:var(--ink-2); margin:0; }
.sp-more{
  display:inline-flex; align-items:center; gap:6px;
  font-family:var(--caps); font-weight:600; font-size:var(--fs-xs);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--red-d); text-decoration:none;
}

/* Recommendation cards (sidebar + article foot) */
.recommendations{ display:grid; gap:var(--sp-4); }
.recommendations-title{ font-size:var(--fs-h3); text-transform:uppercase; margin:0 0 var(--sp-3); }
.rec-card{
  display:flex; gap:var(--sp-3); align-items:center;
  padding:var(--sp-3);
  background:var(--paper-light); border:var(--bw) solid var(--line);
  border-radius:var(--r-sm); box-shadow:var(--pop-sm);
  text-decoration:none; color:var(--ink);
  transition:translate var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.rec-card:hover{ translate:-2px -2px; box-shadow:var(--pop); color:var(--ink); }
.rec-card img{ width:76px; height:76px; object-fit:cover; flex:none; border:var(--bw-hair) solid var(--line); border-radius:var(--r-xs); }

/* Blog / listing cards */
.blog-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(280px,1fr)); gap:var(--sp-5); }
.blog-card,
.blog-item{
  display:flex; flex-direction:column;
  background:var(--paper-light); border:var(--bw) solid var(--line);
  border-radius:var(--r); box-shadow:var(--pop); overflow:hidden;
  transition:translate var(--t) var(--ease), box-shadow var(--t) var(--ease), rotate var(--t) var(--ease);
  rotate:var(--tilt, -.7deg);
}
.blog-grid > *:nth-child(2n){ --tilt:.7deg; }
.blog-card:hover,
.blog-item:hover{ rotate:0deg; translate:0 -5px; box-shadow:var(--pop-lg); }
.blog-image,
.blog-img{ width:100%; aspect-ratio:16/10; object-fit:cover; border-bottom:var(--bw) solid var(--line); }
.blog-img-placeholder{ display:grid; place-items:center; aspect-ratio:16/10; background:var(--cream-deep); font-size:2.4rem; }
.blog-content{ padding:var(--sp-4); display:flex; flex-direction:column; gap:var(--sp-2); flex:1; }
/* text-transform:none is load-bearing. .blog-title is an <h2>, and the base
   h2 rule in §6 uppercases every heading — which is right for a four-word
   section rubric and wrong for a fifty-character pattern title. Set in caps
   the listing became a wall of shouting slab. */
.blog-title{ font-family:var(--display); font-size:1.15rem; line-height:1.22; letter-spacing:var(--track-tight); text-transform:none; margin:0; }
.blog-title a{ color:var(--ink); text-decoration:none; }
.blog-title a:hover{ color:var(--red-d); text-decoration:underline; }
.blog-excerpt{ font-size:var(--fs-sm); line-height:1.6; color:var(--ink-2); margin:0; }
.blog-meta{
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--sp-2);
  font-family:var(--caps); font-size:var(--fs-xs); text-transform:uppercase;
  letter-spacing:var(--track-caps); color:var(--ink-3);   /* 6.04:1 ✓ AA */
}
.blog-read-more{ margin-top:auto; align-self:flex-start; }
.blog-section{ padding-block:var(--sp-8); }

/* Content cards — shared by the article page and the static pages */
.content-card{
  position:relative;
  background:var(--paper-light);
  border:var(--bw) solid var(--line);
  border-radius:var(--r);
  box-shadow:var(--pop);
  padding:var(--sp-6);
  margin-bottom:var(--sp-6);
}
.content-card-mt{ margin-top:var(--sp-6); }
.content-title{
  font-family:var(--display); font-size:var(--fs-h3);
  text-transform:uppercase; letter-spacing:var(--track-tight);
  color:var(--teal);                                  /* 5.56:1 on card ✓ AA */
  margin:0 0 var(--sp-4);
  display:flex; align-items:center; gap:var(--sp-2);
}
.content-featured-image{
  width:100%; border:var(--bw) solid var(--line); border-radius:var(--r-sm);
  box-shadow:var(--pop-sm); margin-bottom:var(--sp-4);
}
.content-grid{ display:grid; gap:var(--sp-6); grid-template-columns:minmax(0,2.1fr) minmax(0,1fr); align-items:start; }
.content-section{ margin-bottom:var(--sp-6); }

/* ═══════════════════════════════════════════════════════════════════════════
   12 · STATS + FEATURE CARDS + STEPS
   ═══════════════════════════════════════════════════════════════════════ */
.sp-stats,
.hero-stats{
  display:flex; flex-wrap:wrap; gap:var(--sp-5);
  padding:var(--sp-4) 0;
}
.sp-stat,
.hero-stats .stat{
  display:flex; flex-direction:column; gap:2px; min-width:110px;
  padding-left:var(--sp-4);
  border-left:var(--bw-thick) solid var(--mustard);
}
.sp-stat-num,
.stat-number{
  font-family:var(--display); font-size:clamp(1.7rem, 3vw, 2.4rem);
  line-height:1; letter-spacing:var(--track-tight);
  color:var(--red);                     /* ≥24px display → 4.22:1 ≥ 3:1 ✓ */
  text-shadow:2px 2px 0 var(--ink);
}
.sp-stat-label,
.stat-label{
  font-family:var(--caps); font-weight:500; font-size:var(--fs-xs);
  text-transform:uppercase; letter-spacing:var(--track-wide);
  color:var(--ink-2);                   /* 6.71:1 ✓ AA */
}

.sp-features,
.intro-grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(250px,1fr)); gap:var(--sp-5); }
.intro-section{ padding-block:var(--sp-7); }
.sp-feature,
.feature-card{
  position:relative;
  background:var(--paper-light);
  border:var(--bw) solid var(--line);
  border-radius:var(--r);
  box-shadow:var(--pop);
  padding:var(--sp-5);
  rotate:var(--tilt, -.8deg);
  transition:rotate var(--t) var(--ease), translate var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.sp-features > *:nth-child(2n),
.intro-grid > *:nth-child(2n){ --tilt:.8deg; }
.sp-feature:hover,
.feature-card:hover{ rotate:0deg; translate:0 -4px; box-shadow:var(--pop-lg); }
/* The disc on a feature card carries a NUMERAL, not a pictogram.
   It used to print whatever emoji the markup or the article JSON supplied
   (🧮 🔤 🎯 ⏱ …). Screenshot: a rainbow abacus and a blue "abc" tile
   inside a mustard roundel on a 1950s poster — the single clearest tell
   that a page was assembled rather than drawn. A leading-zero numeral is
   the same device the pattern card already uses for materials and
   abbreviations, so the page now counts in one voice throughout.
   The counter also means the article renderer can keep passing its emoji;
   nothing downstream has to change and no data is edited. */
.sp-features,
.intro-grid,
#featureCards{ counter-reset:sp-feat; }
.sp-feature,
.feature-card{ counter-increment:sp-feat; }

.sp-feature-icon,
.feature-icon{
  display:grid; place-items:center;
  width:56px; height:56px; margin-bottom:var(--sp-3);
  font-size:0;                                   /* hides the source glyph */
  line-height:1;
  background:var(--mustard); color:var(--on-mustard);
  border:var(--bw) solid var(--line); border-radius:50%;
  box-shadow:var(--pop-sm);
}
.sp-feature-icon::before,
.feature-icon::before{
  content:counter(sp-feat, decimal-leading-zero);
  font-family:var(--display);
  font-size:1.22rem; line-height:1;
  letter-spacing:var(--track-tight);
}
.sp-feature-title{ font-family:var(--display); font-size:1.12rem; letter-spacing:var(--track-tight); margin:0 0 var(--sp-2);
  text-wrap:balance;  /* 3 of the 4 house-rules headings ended in a widow */ }
.sp-feature-desc{ font-size:var(--fs-sm); line-height:1.62; color:var(--ink-2); margin:0; }

.sp-steps{ display:grid; grid-template-columns:repeat(auto-fit, minmax(230px,1fr)); gap:var(--sp-5); }
.sp-step{ position:relative; padding:var(--sp-5); background:var(--cream-deep); border:var(--bw) solid var(--line); border-radius:var(--r); box-shadow:var(--pop-sm); }
.sp-step-num{
  position:absolute; top:calc(-1 * var(--sp-4)); left:var(--sp-4);
  display:grid; place-items:center; width:44px; height:44px;
  font-family:var(--display); font-size:1.2rem;
  background:var(--red); color:var(--on-red);
  border:var(--bw) solid var(--line); border-radius:50%;
  box-shadow:var(--pop-sm);
}
.sp-step-icon{ font-size:1.8rem; margin-bottom:var(--sp-2); }
.sp-step-title{ font-family:var(--display); font-size:1.08rem; margin:var(--sp-3) 0 var(--sp-2); }
.sp-step-desc{ font-size:var(--fs-sm); color:var(--ink-2); margin:0; }

/* ═══════════════════════════════════════════════════════════════════════════
   13 · NEWSLETTER + FORMS
   ═══════════════════════════════════════════════════════════════════════ */
.newsletter-section{ position:relative; padding-block:var(--sp-8); }
.newsletter-card{
  position:relative;
  background:var(--paper-light);
  border:var(--bw-thick) solid var(--line);
  border-radius:var(--r-lg);
  box-shadow:var(--pop-xl);
  overflow:hidden;
}
.newsletter-content{
  display:grid; grid-template-columns:minmax(0,.95fr) minmax(0,1.05fr);
  gap:0;
}
.newsletter-left{
  justify-content:center;   /* 312px of dead ray-field sat below the seal */
  padding:var(--sp-7) var(--sp-6);
  background:var(--teal); color:var(--on-teal);   /* 5.90:1 ✓ AA */
  --focus:var(--on-red);
  --link:var(--on-teal);
  position:relative; isolation:isolate;
}
.newsletter-illustration{ font-size:3.2rem; line-height:1; margin-bottom:var(--sp-3); }
.newsletter-title{
  font-family:var(--display);
  /* Was clamp(1.7rem,3vw,2.4rem) — 38.4px against the 46.4px its five
     sibling section titles render at, and 2.4rem is not a scale step. */
  font-size:var(--fs-h2);
  text-transform:uppercase; letter-spacing:var(--track-tight);
  line-height:var(--lh-poster);
  color:var(--on-teal);
  text-shadow:3px 3px 0 var(--ink);
  margin:0 0 var(--sp-3);
}
.newsletter-subtitle{ font-size:var(--fs-body); line-height:var(--lh-body); color:var(--on-teal); margin:0; }
.newsletter-right{ padding:var(--sp-7) var(--sp-6); background:var(--paper-light); }

/* .newsletter-form is a BEHAVIOUR selector as well as a style one — the
   opt-in snippet binds every form carrying it and posts to /subscribe.php.
   Any form that is not a newsletter signup (the contact composer, for one)
   must use .sp-form instead: identical layout, no handler. */
.newsletter-form,
.sp-form{ display:flex; flex-direction:column; gap:var(--sp-4); }
.sp-form .submit-button{ width:100%; }
.form-group{ display:flex; flex-direction:column; gap:6px; }
.form-group label{
  font-family:var(--caps); font-weight:600; font-size:var(--fs-sm);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--ink);
}
.form-group input,
.form-group select,
.form-group textarea,
.newsletter-form input,
.newsletter-form select,
.newsletter-form textarea,
.sp-form input,
.sp-form select,
.sp-form textarea{
  width:100%; min-height:52px;
  padding:12px var(--sp-4);
  font-family:var(--font); font-size:var(--fs-body); color:var(--ink);
  background:var(--paper);
  border:var(--bw) solid var(--line);
  border-radius:var(--r-sm);
  box-shadow:inset 2px 2px 0 rgba(43,33,24,.09);
  transition:box-shadow var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease);
}
.form-group textarea,
.newsletter-form textarea{ min-height:104px; resize:vertical; line-height:var(--lh-body); }
.form-group input::placeholder,
.newsletter-form input::placeholder,
.newsletter-form textarea::placeholder{ color:var(--ink-3); opacity:1; }  /* 5.44:1 ✓ */
.form-group input:hover,
.form-group select:hover,
.form-group textarea:hover{ background:var(--paper-light); }
.form-group input:focus-visible,
.form-group select:focus-visible,
.form-group textarea:focus-visible,
.newsletter-form input:focus-visible,
.newsletter-form select:focus-visible,
.newsletter-form textarea:focus-visible{
  outline:3px solid var(--focus); outline-offset:2px;
  background:var(--paper-light);
}
/* Helper text under a field is instruction, not decoration, and this
   audience skews older — --fs-xs (13px) was too small to be doing that job. */
.form-text{ font-size:var(--fs-sm); line-height:1.5; color:var(--ink-2); }  /* 7.44:1 on card ✓ */
/* Left-aligned like the three .form-text helpers above it — it was the one
   centred line in a column of left-set helper copy. */
.privacy-note{
  font-size:var(--fs-sm); line-height:1.5; color:var(--ink-2); text-align:start; margin:0;
}
.privacy-note a{ color:var(--red-d); font-weight:var(--fw-bold); }
/* injected by the opt-in snippet on failure */
.nl-err{
  font-family:var(--caps); font-weight:600; font-size:var(--fs-sm);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--red-deep);                                     /* 7.80:1 ✓ AAA */
  background:var(--paper); border:var(--bw) solid var(--red-deep);
  border-radius:var(--r-sm); padding:var(--sp-2) var(--sp-3); margin-top:var(--sp-2);
}

/* ═══════════════════════════════════════════════════════════════════════════
   14 · RIBBON BANNER
   The starred "FRESH EVERY DAY ★ ALWAYS THE FINEST" bar. Teal band, mustard
   stars, Oswald caps. Once on the homepage, once above the footer.
   CONTRAST: --on-teal on --teal = 5.90:1 ✓ AA. The stars are ornament and
   carry no meaning, so their 2.88:1 is fine — they must be aria-hidden.
   ═══════════════════════════════════════════════════════════════════════ */
.sp-ribbon,
.sp-strip{
  position:relative;
  display:flex; flex-wrap:wrap; align-items:center; justify-content:center;
  gap:var(--sp-4);
  padding:var(--sp-4) var(--gutter);
  background:var(--teal);
  color:var(--on-teal);
  border-block:var(--bw-thick) solid var(--ink);
  font-family:var(--caps); font-weight:600;
  font-size:clamp(.92rem, 1.5vw, 1.15rem);
  text-transform:uppercase; letter-spacing:var(--track-wide);
  text-align:center;
  --focus:var(--on-red);
}
.sp-ribbon a,
.sp-strip a{ color:var(--on-teal); text-decoration-thickness:2px; }
.sp-ribbon a:hover{ color:var(--mustard); }
.sp-ribbon__item{ white-space:nowrap; }
.sp-ribbon__star{
  display:inline-block; flex:none;
  width:18px; height:18px;
  background-color:var(--mustard);
  -webkit-mask-image:var(--m-star12); mask-image:var(--m-star12);
  -webkit-mask-size:contain;          mask-size:contain;
  -webkit-mask-repeat:no-repeat;      mask-repeat:no-repeat;
}
.sp-ribbon--red{ background:var(--red); color:var(--on-red); }
.sp-ribbon--red a{ color:var(--on-red); }
.sp-ribbon--ink{ background:var(--ink); color:var(--on-ink); --focus:var(--mustard); }
.sp-ribbon--ink a{ color:var(--mustard); }
/* dotted tape on both edges of the ribbon */
.sp-ribbon::before,
.sp-ribbon::after{
  content:''; position:absolute; left:0; right:0; height:8px;
  background-image:radial-gradient(circle at 50% 50%, var(--mustard) 0 2px, transparent 2.2px);
  background-size:14px 8px; background-repeat:repeat-x;
  pointer-events:none;
}
.sp-ribbon::before{ top:4px; }
.sp-ribbon::after{ bottom:4px; }

/* ═══════════════════════════════════════════════════════════════════════════
   15 · FOOTER
   Ink ground. Every foreground here was computed against #2b2118:
   cream body 12.53:1 · muted links 9.17:1 · mustard headings 7.19:1.
   ═══════════════════════════════════════════════════════════════════════ */
.footer-section{
  position:relative;
  background:var(--ink);
  color:var(--on-ink);
  padding:var(--sp-8) 0 var(--sp-5);
  border-top:var(--bw-thick) solid var(--mustard);
  /* The footer is a dark ground and must redeclare the same four tokens the
     §7 band modifiers do. Without this, --fg and --line still resolve to
     --ink: the ghost "write to us" button rendered ink-on-ink (1:1, i.e.
     invisible) with an ink border and an ink drop shadow, and the mustard
     logo disc lost its keyline. Caught by screenshot, not by reading. */
  --fg:var(--on-ink);            /* 12.53:1 on ink ✓ AAA */
  --fg-mut:var(--on-ink-mut);    /*  9.17:1 on ink ✓ AAA */
  --fg-faint:var(--on-ink-mut);
  --line:var(--on-ink-mut);      /*  9.17:1 — borders read as a drawn keyline */
  --link:var(--mustard);         /*  7.19:1 on ink ✓ AAA */
  --link-hover:var(--on-ink);
  --focus:var(--mustard);
}
/* The hard offset shadow is ink by default, which is the footer's own ground.
   On this band the shadow has to be the cream keyline colour or the controls
   look flat and unpressable. */
.footer-section .footer-btn,
.footer-section .sp-btn{ box-shadow:3px 3px 0 var(--on-ink-mut); }
.footer-section .footer-btn:hover,
.footer-section .sp-btn:hover{ box-shadow:5px 5px 0 var(--on-ink-mut); }
.footer-section .footer-btn:active,
.footer-section .sp-btn:active{ box-shadow:0 0 0 0 var(--on-ink-mut); }
/* triangle tape above the footer */
.footer-section::before{
  content:''; position:absolute; left:0; right:0; top:calc(-1 * var(--tape-h));
  height:var(--tape-h); pointer-events:none;
  background-image:conic-gradient(from -45deg at 50% 100%, var(--ink) 0 90deg, transparent 90deg);
  background-size:22px var(--tape-h); background-repeat:repeat-x;
}
.footer-content{ display:grid; grid-template-columns:repeat(auto-fit, minmax(210px,1fr)); gap:var(--sp-6); }
.footer-card{
  background:rgba(244,228,193,.06);
  border:var(--bw) solid var(--on-ink-mut);
  border-radius:var(--r);
  padding:var(--sp-6);
}
.footer-column{ display:flex; flex-direction:column; gap:var(--sp-2); }
.footer-logo-row{ display:flex; align-items:center; gap:var(--sp-3); margin-bottom:var(--sp-2); }
.footer-brand-name{ color:var(--on-ink); text-shadow:2px 2px 0 var(--red);
  line-height:var(--lh-poster);   /* was inheriting body leading, 1.72 */ }
.footer-tagline{ font-family:var(--script); font-size:1.25rem; line-height:1.25; color:var(--mustard); margin:0 0 var(--sp-3); }
.footer-heading{
  font-family:var(--caps); font-weight:600; font-size:var(--fs-sm);
  text-transform:uppercase; letter-spacing:var(--track-wide);
  color:var(--mustard);                     /* 7.19:1 on ink ✓ AAA */
  margin:0 0 var(--sp-2);
  display:flex; align-items:center; gap:var(--sp-2);
}
.footer-links{ display:flex; flex-direction:column; gap:2px; }
/* 44px tall, not 42.1: WCAG 2.5.8 measured against the rendered box, and
   8px + 26.1px of line box + 8px missed it by 1.9px on every footer link.
   inline-flex + align-items keeps the text where it was. */
.footer-link,
.footer-contact-email{
  display:inline-flex; align-items:center;
  min-height:44px; padding:8px 0;
  color:var(--on-ink-mut);                  /* 9.17:1 on ink ✓ AAA */
  text-decoration:none;
  font-size:var(--fs-sm);
  transition:color var(--t-fast) var(--ease), translate var(--t-fast) var(--ease);
}
.footer-link:hover,
.footer-contact-email:hover{ color:var(--mustard); translate:4px 0; text-decoration:underline; }
.footer-link:focus-visible,
.footer-contact-email:focus-visible{ outline:3px solid var(--mustard); outline-offset:3px; }

.social-links{ display:flex; flex-wrap:wrap; gap:var(--sp-2); margin-top:var(--sp-2); }
.social-icon{
  display:grid; place-items:center; width:46px; height:46px;
  font-size:1.2rem;
  color:var(--on-ink);
  background:transparent;
  border:var(--bw) solid var(--on-ink-mut);
  border-radius:50%;
  text-decoration:none;
  transition:background-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), rotate var(--t) var(--ease);
}
.social-icon:hover{ background:var(--mustard); color:var(--on-mustard); rotate:-8deg; }

.footer-bottom{
  display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between;
  gap:var(--sp-3);
  margin-top:var(--sp-6); padding-top:var(--sp-4);
  border-top:var(--bw-hair) solid var(--on-ink-mut);
  font-size:var(--fs-sm);
  color:var(--on-ink-mut);                  /* 9.17:1 ✓ AAA */
}
.footer-copy,
.footer-credit-right{ margin:0; }
.footer-credit-right{ font-family:var(--script); font-size:1.1rem; line-height:1.25; color:var(--mustard); }

/* The five legal links live in .footer-bottom and run as a ROW, not as a
   column like every other .footer-links group.
   These four declarations are duplicated in article.css §13 with identical
   values. They must stay identical: article pages load article.css BEFORE
   this file, so on an equal-specificity tie this file wins, and a drift here
   would silently restyle the article footer. */
.footer-legal{
  flex-direction:row; flex-wrap:wrap; gap:var(--sp-2) var(--sp-4);
  justify-content:center;
}
/* The row keeps its 38px rhythm; the HIT box is 44px. Negative block margins
   pull the taller box back into the line so the legal row does not grow. */
.footer-legal .footer-link{ padding:0; min-height:44px; margin-block:-3px; }
.footer-content--3{ grid-template-columns:repeat(3, minmax(0,1fr)); }
.footer-note{
  font-size:var(--fs-xs); line-height:1.6;
  color:var(--on-ink-mut);                  /* 9.17:1 on ink ✓ AAA */
  margin:var(--sp-2) 0 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   16 · LISTINGS · PAGINATION · SEARCH · BREADCRUMBS
   ═══════════════════════════════════════════════════════════════════════ */
.patterns-hero{
  position:relative;
  --section-bg:var(--cream-deep);
  background:var(--cream-deep);
  padding-block:var(--sp-8) var(--sp-7);
  border-bottom:var(--bw-thick) solid var(--line);
}
/* The listing hero's price stamp. Absolute so it cannot push the copy
   around, and gone below 1100px where the copy already fills the width. */
.patterns-hero--stamped .container{ position:relative; }
.patterns-hero-stamp{ display:none; }
@media (min-width:1100px){
  .patterns-hero--stamped .container{ padding-right:230px; }
  .patterns-hero-stamp{
    display:inline-flex;
    /* 12px, not 8: at exactly 1100px the gutter is still the ≤1100 block's
       20px, and the tilted 150px disc's rotated box reached 0.58px past the
       viewport — one pixel of sideways scroll on the listing pages. */
    position:absolute; top:50%; right:12px; translate:0 -50%;
    --roundel-tilt:7deg;
  }
}
.patterns-h1{
  font-size:clamp(2.2rem, 5.5vw, 3.8rem);
  text-transform:uppercase; color:var(--red);
  text-shadow:var(--hard-x) var(--hard-y) 0 var(--ink);
  margin:0 0 var(--sp-4);
}
.patterns-lede{ color:var(--ink-2); }        /* 5.75:1 on cream-deep ✓ AA */

.patterns-breadcrumb,
.article-breadcrumb{
  display:flex; flex-wrap:wrap; align-items:center; gap:6px;
  font-family:var(--caps); font-size:var(--fs-xs);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--ink-2);
  margin-bottom:var(--sp-4);
}
/* A breadcrumb link's text box is 22.6px tall — half the 44px target. The
   hit box is padded out to 44px and pulled back into the line with matching
   negative margins, so the trail keeps its height and the page keeps its
   rhythm; only the tappable area grows. */
.patterns-breadcrumb a,
.article-breadcrumb a{
  display:inline-flex; align-items:center;
  min-height:44px; margin-block:-10.7px;
  color:var(--red-d); text-decoration:none;
}
.patterns-breadcrumb a:hover,
.article-breadcrumb a:hover{ color:var(--red-deep); text-decoration:underline; }
/* Was --ink-3: 4.66:1 on the cream-deep listing hero, and 4.34:1 once that
   band's grain is composited. --ink-2 is 5.75 / 5.35 on the same ground. */
.sep{ color:var(--ink-2); }

.filter-controls,
.sp-facets,
.filter-buttons,
.sp-chips,
.sp-chips-browse{ display:flex; flex-wrap:wrap; gap:var(--sp-2); align-items:center;
  margin-bottom:var(--sp-6);  /* was 0 — the sticker-rotated cards below
     overlapped this row by 2px while every neighbouring step is 12-36px */ }
.sp-chip{
  background:var(--paper-light); color:var(--ink);
  box-shadow:var(--pop-sm);
  transition:translate var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
  min-height:44px;   /* was 40 — a chip is a link or a facet button, so it is a tap target */
}
.sp-chip:hover{ background:var(--mustard); color:var(--on-mustard); translate:-1px -1px; box-shadow:var(--pop); }
.sp-chip:focus-visible{ outline:3px solid var(--focus); outline-offset:3px; }
.sp-chip-n{
  font-size:var(--fs-micro); font-weight:500;
  color:var(--ink-2); opacity:.9;
}
.sp-chip:hover .sp-chip-n{ color:var(--on-mustard); }
/* search.js toggles .is-on on the facet chip it has applied. */
.sp-chip-facet{ cursor:pointer; }
.sp-chip-facet.is-on{
  background:var(--teal); color:var(--on-teal);       /* 5.90:1 ✓ AA */
  border-color:var(--line);
}
.sp-chip-facet.is-on .sp-chip-n{ color:var(--on-teal); }
/* The search page's three facet columns. .sp-facets on its own is a chip row;
   this variant lays the three labelled groups side by side and lets each one
   wrap its chips independently. */
.sp-search-facets{ align-items:flex-start; gap:var(--sp-5); }
.sp-search-facets > *{ flex:1 1 220px; min-width:0; }
.sp-search-facets .sp-side-label{ margin:0 0 var(--sp-2); color:var(--ink-2); }
.sp-facet,
.sp-seg{
  display:inline-flex; align-items:center; min-height:44px;
  padding:8px var(--sp-4);
  font-family:var(--caps); font-weight:600; font-size:var(--fs-sm);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  background:var(--paper-light); color:var(--ink);
  border:var(--bw) solid var(--line); border-radius:var(--r-sm);
  box-shadow:var(--pop-sm); text-decoration:none; cursor:pointer;
}
.sp-facet:hover,
.sp-seg:hover{ background:var(--cream-deep); color:var(--ink); }
.sp-facet.active,
.sp-facet[aria-current="page"],
.sp-seg.active{ background:var(--teal); color:var(--on-teal); }

.results-bar,
.sp-resultbar{
  display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between;
  gap:var(--sp-3);
  padding:var(--sp-3) 0 var(--sp-4);
  border-bottom:var(--bw-hair) solid var(--line-soft);
  margin-bottom:var(--sp-5);
}
.results-count,
.sp-result-count{
  font-family:var(--caps); font-size:var(--fs-sm);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--ink-2);
}
/* The active-filter readout shares the results bar with the count and is set
   in the same condensed caps. It used to inherit the body face at weight 600
   — the ONE place Bitter 600 was ever rendered, and the font import no
   longer carries that cut. */
.sp-result-active{
  font-family:var(--caps); font-weight:600; font-size:var(--fs-sm);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--red-d);
}

.pagination-section{ padding-block:var(--sp-7); }
.pagination-container{ display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:var(--sp-2); }
.page-btn{ min-width:52px; min-height:52px; padding:10px var(--sp-3); }
.page-btn[aria-current="page"],
.page-btn.active{ background:var(--red); color:var(--on-red); }
/* A disabled Prev/Next is a label, not a control, and §8's generic disabled
   rule fades it with opacity:.55 — ink at 55% over card stock is #887e6f,
   3.52:1. Paint the state with a real colour instead: --ink-2 on
   --paper-light is 7.44:1, and the flat shadow + press-in still say "off". */
.page-btn.disabled{
  opacity:1;
  color:var(--ink-2); background:var(--paper-light);
  border-color:var(--line-soft);
}
.page-dots{ padding:0 var(--sp-2); color:var(--ink-3); font-family:var(--caps); }

/* ONE empty state. The site had three and they did not look related: the
   search "nothing matched" heading was 2.6x smaller than the other two, its
   mark 3.5x smaller, and it was the only one with no way out. The heading is
   now .sp-h2 like the others and every empty state ends in a button. */
.empty-state{
  display:grid; justify-items:center; gap:var(--sp-2);
  text-align:center; padding:var(--sp-8) var(--gutter);
  background:var(--paper-light);
  border:var(--bw) dashed var(--line);
  border-radius:var(--r);
}
.empty-state .sp-btn{ margin-top:var(--sp-3); }
.empty-icon{ font-size:3.4rem; line-height:1; margin-bottom:var(--sp-3); }

/* "Show more" sits under the result grid, clear of the last row. */
.sp-showmore-wrap{ display:flex; justify-content:center; margin-top:var(--sp-6); }
.sp-showmore-wrap [hidden]{ display:none; }

/* ═══════════════════════════════════════════════════════════════════════════
   17 · STATIC PAGES · PROSE · TABLES · GLOSSARY · FAQ
   ═══════════════════════════════════════════════════════════════════════ */
.page-prose,
.sp-prose{
  max-width:var(--measure);
  font-size:var(--fs-body); line-height:var(--lh-loose);
  color:var(--ink);
}
.page-prose-flush,
.sp-prose-section{ max-width:none; }
.page-prose h2,
.sp-prose h2,
.page-h2{
  font-family:var(--display); font-size:clamp(1.35rem, 2.4vw, 1.75rem);
  text-transform:uppercase; letter-spacing:var(--track-tight);
  color:var(--teal);                         /* 5.56:1 on card ✓ AA */
  margin:var(--sp-7) 0 var(--sp-3);
}
.page-h2-flush{ margin-top:0; }
.page-prose h3,
.sp-prose h3{ font-size:1.2rem; margin:var(--sp-5) 0 var(--sp-2); }
.page-prose a,
.sp-prose a{ color:var(--red-d); font-weight:var(--fw-bold); }   /* 6.64:1 ✓ */
.page-list,
.sp-prose ul,
.sp-prose ol{ line-height:var(--lh-loose); color:var(--ink-2); }
.sp-prose-grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(260px,1fr)); gap:var(--sp-5); }
.sp-prose-figure{ margin:var(--sp-5) 0; }
.sp-prose-figure img{ border:var(--bw) solid var(--line); border-radius:var(--r-sm); box-shadow:var(--pop-sm); }
/* Same object as .sp-hero-caption — a Caveat caption under a photograph.
   It was 17.6px/1.72 here and 20px/1.25 there, and neither is a scale step. */
.sp-prose-figure figcaption{ font-family:var(--script); font-size:var(--fs-lg); line-height:1.25; color:var(--ink-2); margin-top:var(--sp-2); }

.answer-summary,
.news-answer-summary{
  position:relative;
  background:var(--cream-deep);
  border:var(--bw) solid var(--line);
  border-left-width:var(--bw-slab);
  border-radius:var(--r-sm);
  box-shadow:var(--pop-sm);
  padding:var(--sp-4) var(--sp-5);
  margin:0 0 var(--sp-5);
  font-size:var(--fs-lg); line-height:var(--lh-body);
  color:var(--on-cream);                     /* 10.74:1 ✓ AAA */
}

.table-scroll{ overflow-x:auto; -webkit-overflow-scrolling:touch; margin:var(--sp-5) 0; border:var(--bw) solid var(--line); border-radius:var(--r-sm); }
.sp-table{ min-width:520px; font-size:var(--fs-sm); background:var(--paper-light); }
.sp-table th,
.sp-table td{ padding:var(--sp-3) var(--sp-4); text-align:left; border-bottom:var(--bw-hair) solid var(--line-soft); }
.sp-table thead th{
  font-family:var(--caps); font-weight:600; text-transform:uppercase; letter-spacing:var(--track-caps);
  background:var(--teal); color:var(--on-teal);     /* 5.90:1 ✓ AA */
  border-bottom:var(--bw) solid var(--line);
}
.sp-table tbody tr:nth-child(2n){ background:var(--paper); }

.link-card-grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(240px,1fr)); gap:var(--sp-4); margin:var(--sp-5) 0; }
.link-card{
  display:block; padding:var(--sp-4);
  background:var(--paper-light); border:var(--bw) solid var(--line);
  border-radius:var(--r-sm); box-shadow:var(--pop-sm);
  text-decoration:none; color:var(--ink);
  transition:translate var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.link-card:hover{ translate:-2px -2px; box-shadow:var(--pop); color:var(--ink); }
.link-card-title{ font-family:var(--display); font-size:1.05rem; letter-spacing:var(--track-tight); margin:0 0 4px; }
.link-card-note{ font-size:var(--fs-xs); color:var(--ink-2); margin:0; }

.glossary-list{ display:grid; gap:var(--sp-4); list-style:none; padding:0; margin:0; }
.glossary-item{
  display:grid; grid-template-columns:auto 1fr; gap:var(--sp-4); align-items:start;
  padding:var(--sp-4);
  background:var(--paper-light); border:var(--bw) solid var(--line);
  border-radius:var(--r-sm); box-shadow:var(--pop-sm);
}
.glossary-abbr{
  display:grid; place-items:center; min-width:64px; padding:var(--sp-2) var(--sp-3);
  font-family:var(--display); font-size:1.05rem;
  background:var(--mustard); color:var(--on-mustard);     /* 7.19:1 ✓ AAA */
  border:var(--bw) solid var(--line); border-radius:var(--r-xs);
}
.glossary-name{ font-family:var(--caps); font-weight:600; text-transform:uppercase; letter-spacing:var(--track-caps); }
.glossary-def{ font-size:var(--fs-sm); color:var(--ink-2); margin:4px 0 0; }
.glossary-uk{ font-size:var(--fs-xs); color:var(--ink-3); font-style:italic; }

/* FAQ / Q&A — works as native <details> or as a JS accordion */
.qa-section{ padding-block:var(--sp-8); }
.qa-header{ text-align:center; margin-bottom:var(--sp-6); }
.qa-title{ font-size:var(--fs-h2); text-transform:uppercase; color:var(--red); text-shadow:3px 3px 0 var(--ink); margin:0 0 var(--sp-2); }
.qa-list,
.sp-faq{ display:grid; gap:var(--sp-3); }
.sp-faq details,
.qa-list details,
.news-faq-item{
  background:var(--paper-light);
  border:var(--bw) solid var(--line);
  border-radius:var(--r-sm);
  box-shadow:var(--pop-sm);
  overflow:hidden;
}
.sp-faq summary,
.qa-list summary,
.qa-question{
  display:flex; align-items:center; justify-content:space-between; gap:var(--sp-3);
  padding:var(--sp-4) var(--sp-5);
  min-height:56px;
  font-family:var(--caps); font-weight:600; font-size:var(--fs-lg);
  text-transform:none; letter-spacing:.01em;
  color:var(--ink);
  cursor:pointer; list-style:none;
}
.sp-faq summary::-webkit-details-marker,
.qa-list summary::-webkit-details-marker{ display:none; }
.sp-faq summary:hover,
.qa-list summary:hover,
.qa-question:hover{ background:var(--cream-deep); }
.sp-faq summary:focus-visible,
.qa-list summary:focus-visible,
.qa-question:focus-visible{ outline:3px solid var(--focus); outline-offset:-3px; }
.qa-icon,
.sp-faq summary::after,
.qa-list summary::after{
  content:'+'; flex:none;
  display:grid; place-items:center; width:34px; height:34px;
  /* weight is explicit: the glyph inherits 600 from the caps summary, and
     Alfa Slab One ships one cut, so the browser was smearing a faux bold */
  font-family:var(--display); font-weight:var(--fw-display); font-size:1.3rem; line-height:1;
  background:var(--mustard); color:var(--on-mustard);
  border:var(--bw-hair) solid var(--line); border-radius:50%;
  transition:rotate var(--t) var(--ease);
}
.sp-faq details[open] summary::after,
.qa-list details[open] summary::after{ content:'+'; rotate:45deg; }
.qa-answer,
.sp-faq details > *:not(summary),
.qa-list details > *:not(summary){
  padding:0 var(--sp-5) var(--sp-4);
  color:var(--ink-2); line-height:var(--lh-loose);
}
.qa-question.open + .qa-answer,
.qa-answer.open{ display:block; }

/* Author card + attribution */
.author-card{
  display:flex; gap:var(--sp-4); align-items:flex-start;
  padding:var(--sp-5);
  background:var(--cream-deep);
  border:var(--bw) solid var(--line); border-radius:var(--r);
  box-shadow:var(--pop-sm);
}
.author-portrait{
  width:82px; height:82px; flex:none; object-fit:cover;
  border:var(--bw) solid var(--line); border-radius:50%;
  box-shadow:var(--pop-sm); rotate:-4deg; background:var(--paper-light);
}
.author-name{ font-family:var(--display); font-size:1.2rem; letter-spacing:var(--track-tight); margin:0 0 4px; }
.author-bio{ font-size:var(--fs-sm); line-height:1.65; color:var(--ink-2); margin:0; }
.source-attribution{
  font-size:var(--fs-xs); color:var(--ink-3); margin-top:var(--sp-4);
}

/* ═══════════════════════════════════════════════════════════════════════════
   18 · 404
   ═══════════════════════════════════════════════════════════════════════ */
.sp-404{ position:relative; isolation:isolate; overflow:hidden; text-align:center; padding-block:var(--sp-9); }
.sp-404__numeral{
  font-family:var(--display); font-size:var(--fs-mega);
  line-height:.85; letter-spacing:-.03em;
  color:var(--red);                       /* enormous display → 3:1 rule ✓ */
  text-shadow:8px 8px 0 var(--ink);
  margin:0 0 var(--sp-4);
}
.sp-404 .sp-script{ margin-bottom:var(--sp-2); }
.sp-404-actions{ justify-content:center; margin-top:var(--sp-5); }
.sp-404-panel{ margin-top:var(--sp-8); text-align:left; }
.sp-404-panel .sp-chips{ justify-content:flex-start; }

/* ═══════════════════════════════════════════════════════════════════════════
   19 · NEWS
   ═══════════════════════════════════════════════════════════════════════ */
.news-hero{ --section-bg:var(--cream-deep); background:var(--cream-deep); padding-block:var(--sp-8) var(--sp-7); border-bottom:var(--bw-thick) solid var(--line); }
.news-h1{ font-size:clamp(2rem, 4.6vw, 3.2rem); text-transform:uppercase; color:var(--red); text-shadow:var(--hard-x) var(--hard-y) 0 var(--ink); margin:0 0 var(--sp-3); }
.news-dek{ font-size:var(--fs-lg); line-height:var(--lh-loose); color:var(--ink-2); max-width:var(--measure); }
.news-measure{ max-width:var(--measure); }
.news-article{ padding-block:var(--sp-7); }
.news-article-header{ margin-bottom:var(--sp-5); }
.news-hero-figure img{ border:var(--bw) solid var(--line); border-radius:var(--r-sm); box-shadow:var(--pop); }
.news-meta,
.news-byline{
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--sp-2);
  font-family:var(--caps); font-size:var(--fs-xs);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--ink-2);
}
.news-meta-sep{ color:var(--ink-3); }
.news-body{ font-size:var(--fs-body); line-height:var(--lh-loose); }
.news-body h2{ font-size:clamp(1.3rem,2.3vw,1.7rem); color:var(--teal); text-transform:uppercase; margin:var(--sp-6) 0 var(--sp-3); }
.news-tags{ display:flex; flex-wrap:wrap; gap:var(--sp-2); margin:var(--sp-5) 0; }
.news-faq{ margin:var(--sp-6) 0; }
.news-faq-title{ font-size:var(--fs-h3); text-transform:uppercase; margin:0 0 var(--sp-3); }
.news-faq-list{ display:grid; gap:var(--sp-3); }
.news-source-card{
  background:var(--cream-deep); border:var(--bw) solid var(--line);
  border-radius:var(--r-sm); padding:var(--sp-4); margin:var(--sp-5) 0;
}
.news-source-text{ font-size:var(--fs-sm); color:var(--ink-2); margin:0; }
.news-related-bottom{ padding-block:var(--sp-7); }
.news-related-title{ font-size:var(--fs-h3); text-transform:uppercase; margin:0 0 var(--sp-4); }
.news-related-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:var(--sp-4); }
.news-related-card{
  display:block; padding:var(--sp-4);
  background:var(--paper-light); border:var(--bw) solid var(--line);
  border-radius:var(--r-sm); box-shadow:var(--pop-sm);
  text-decoration:none; color:var(--ink);
}
.news-related-card:hover{ translate:-2px -2px; box-shadow:var(--pop); color:var(--ink); }
.news-disclosure-wrap{ font-size:var(--fs-xs); color:var(--ink-3); margin:var(--sp-4) 0; }
.news-amazon{ margin:var(--sp-6) 0; }
.news-amazon-title{ font-size:var(--fs-h3); text-transform:uppercase; margin:0 0 var(--sp-2); }
.news-amazon-sub{ font-size:var(--fs-sm); color:var(--ink-2); }
.news-amazon-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(210px,1fr)); gap:var(--sp-4); }
.news-amazon-card{
  display:flex; flex-direction:column; gap:var(--sp-2); padding:var(--sp-3);
  background:var(--paper-light); border:var(--bw) solid var(--line);
  border-radius:var(--r-sm); box-shadow:var(--pop-sm);
  text-decoration:none; color:var(--ink);
}
.news-amazon-thumb{ width:100%; aspect-ratio:1; object-fit:contain; background:var(--paper); }
.news-amazon-name{ font-size:var(--fs-sm); font-weight:var(--fw-bold); line-height:1.35; }
.news-amazon-price{ font-family:var(--display); color:var(--red-d); }
.news-amazon-body{ display:flex; flex-direction:column; gap:4px; }

/* ═══════════════════════════════════════════════════════════════════════════
   19b · NEWSLETTER RESULT POPUP  (site-wide — every page except the article)

   The article page has its own popup, styled in article.css under
   `.sp-modal` / `.nl-popup` and driven by SPArticleUI. article.css does not
   ship on any other page, so the shared opt-in snippet
   (_templates/_optin_snippet.html) builds THIS one instead.

   The class names are deliberately different (`sp-nlpop*`, not `sp-modal*`).
   Reusing the article names here would be a live hazard: pages link
   article.css BEFORE stitchobsession.css, so identical selectors in this file
   would win the tie and silently restyle the article page's modals.
   The declarations are the same, so the two read as one component.
   ═══════════════════════════════════════════════════════════════════════ */
.sp-nlpop{
  position:fixed; inset:0; z-index:200;
  display:grid; place-items:center;
  padding:var(--gutter);
  background:rgba(43,33,24,.72);
  opacity:0;
  transition:opacity var(--t) var(--ease);
}
.sp-nlpop.open{ opacity:1; }
.sp-nlpop[hidden]{ display:none; }
.sp-nlpop-card{
  position:relative;
  width:min(100%, 480px);
  /* dvh, like the nav drawer: 100vh on a phone includes the height under the
     browser chrome, so the card's bottom — and its close affordances — could
     sit below the visible edge. The vh line is the fallback. */
  max-height:calc(100vh - var(--sp-7));
  max-height:calc(100dvh - var(--sp-7));
  overflow:auto;
  text-align:center;
  background:var(--paper-light);
  border:var(--bw-thick) solid var(--line);
  border-radius:var(--r-lg);
  box-shadow:var(--pop-xl);
  scale:.96;
  transition:scale var(--t) var(--ease-out);
}
.sp-nlpop.open .sp-nlpop-card{ scale:1; }
.sp-nlpop-close{
  position:absolute; z-index:6; top:var(--sp-3); right:var(--sp-3);
  display:grid; place-items:center;
  width:44px; height:44px;
  font-family:var(--display); font-size:1.5rem; line-height:1;
  color:var(--on-red); background:var(--red);            /* 4.96:1 ✓ AA */
  border:var(--bw) solid var(--line);
  border-radius:50%;
  box-shadow:var(--pop-sm);
  cursor:pointer;
}
.sp-nlpop-close:hover{ background:var(--red-d); color:var(--on-red); }
.sp-nlpop-close:focus-visible{ outline:3px solid var(--mustard); outline-offset:3px; }
.sp-nlpop-head{
  padding:var(--sp-6) var(--sp-5) var(--sp-5);
  background:var(--teal); color:var(--on-teal);          /* 5.90:1 ✓ AA */
  border-bottom:var(--bw-thick) solid var(--line);
  border-radius:calc(var(--r-lg) - 4px) calc(var(--r-lg) - 4px) 0 0;
}
.sp-nlpop.is-error .sp-nlpop-head{ background:var(--red); color:var(--on-red); }
.sp-nlpop-icon{
  display:grid; place-items:center;
  width:64px; height:64px; margin:0 auto var(--sp-3);
  font-size:2rem; line-height:1;
  background:var(--paper-light); color:var(--ink);
  border:var(--bw) solid var(--line); border-radius:50%;
  box-shadow:var(--pop-sm);
  rotate:-6deg;
}
.sp-nlpop-title{
  font-family:var(--display); font-size:1.5rem;
  text-transform:uppercase; letter-spacing:var(--track-tight);
  color:inherit; margin:0;
  text-shadow:2px 2px 0 var(--ink);
}
.sp-nlpop-body{ padding:var(--sp-5); }
.sp-nlpop-body p{
  font-size:var(--fs-body); line-height:var(--lh-body);
  color:var(--ink-2); margin:0 0 var(--sp-5);
}
.sp-nlpop-actions{ display:flex; flex-wrap:wrap; gap:var(--sp-3); justify-content:center; }

/* ═══════════════════════════════════════════════════════════════════════════
   20 · UTILITIES
   ═══════════════════════════════════════════════════════════════════════ */
.sp-center{ text-align:center; }
.sp-center-x{ margin-inline:auto; }
.sp-mt-0{ margin-top:0; }
.sp-mt-4{ margin-top:var(--sp-4); }
.sp-mt-6{ margin-top:var(--sp-6); }
.sp-mb-0{ margin-bottom:0; }
.sp-mb-4{ margin-bottom:var(--sp-4); }
.sp-mb-6{ margin-bottom:var(--sp-6); }
.sp-measure{ max-width:var(--measure); }
.sp-nowrap{ white-space:nowrap; }
.sp-side-label,
.sp-side-label-mt{ display:block; }
.sp-side-label-mt{ margin-top:var(--sp-5); }
.sp-side-list{ display:flex; flex-direction:column; gap:var(--sp-2); list-style:none; padding:0; margin:0; }
.sp-side-row{ display:flex; align-items:center; justify-content:space-between; gap:var(--sp-3); }
.sp-side-opt{
  display:flex; align-items:center; gap:var(--sp-2);
  min-height:44px; padding:6px var(--sp-3);
  font-family:var(--caps); font-size:var(--fs-sm);
  text-transform:uppercase; letter-spacing:var(--track-caps);
  color:var(--ink); text-decoration:none;
  border:var(--bw-hair) solid transparent; border-radius:var(--r-sm);
}
.sp-side-opt:hover{ background:var(--cream-deep); border-color:var(--line); color:var(--ink); }
.sp-side-cta{ margin-top:var(--sp-4); }

/* ═══════════════════════════════════════════════════════════════════════════
   21 · RESPONSIVE — breakpoints at 1100px and 760px
   ═══════════════════════════════════════════════════════════════════════ */
@media (max-width:1100px){
  :root{
    --gutter:20px;
    --sp-8:60px;
    --sp-9:80px;
    --hard-x:3px;
    --hard-y:3px;
  }
  /* minmax(0,1fr), never a bare 1fr — a bare 1fr is minmax(auto,1fr) and the
     auto minimum is content-based, which is exactly the blow-out documented
     on .sp-hero-grid above. */
  .sp-hero-grid{ grid-template-columns:minmax(0,1fr); gap:var(--sp-6); }
  .content-grid{ grid-template-columns:minmax(0,1fr); }
  .newsletter-content{ grid-template-columns:minmax(0,1fr); }
  /* Floating doodles are decoration; they go first when space is tight. */
  .floating-elements,
  .decorative-elements .float-item,
  .float-item{ display:none; }
  .featured-grid,
  .sp-grid{ grid-template-columns:repeat(auto-fill, minmax(210px,1fr)); }
  /* Masthead: logo + seven condensed-caps links + the search box need
     ~1080px. Below that the box goes first — the nav still carries every
     destination, and /search/ is one tap away in the drawer. */
  .navbar .search-box{ display:none; }
}

/* The masthead's OWN breakpoint. Dropping the search box is only half the
   room: the wordmark plus seven condensed-caps links still measure ~880px,
   so between 761px (where the drawer used to appear) and ~880px the link
   row ran off the right edge and took the whole document with it — a
   measured 886px scrollWidth in a 768px viewport, i.e. every page scrolled
   sideways on a tablet and on a phone held landscape. Hand the masthead to
   the drawer at 900px, well clear of the measurement, instead of tying it
   to the 760px layout breakpoint. */
@media (max-width:960px){
  .navbar .nav-links{ display:none; }
  .navbar .menu-toggle{ display:flex; }
}

@media (max-width:760px){
  :root{
    --gutter:16px;
    --fs-h1:clamp(2.1rem, 10vw, 3rem);
    --fs-h2:clamp(1.6rem, 7vw, 2.1rem);
    --sp-7:40px;
    --sp-8:48px;
    --sp-9:60px;
    --scallop-base:12px;
    --scallop-h:12px;
    --hard-x:3px;
    --hard-y:3px;
  }
  /* Filigree corners are sized for a desktop panel. Left at 64px they sat on
     top of the first line of the pattern-card title on a 380px screen — the
     ornament was reading as damage to the type. Shrink the corners rather
     than pad the card, so the copy keeps its full width.

     The first pass at this only shrank them PART of the way. The corner motif
     occupies `--fil-inset + --fil-size` measured in from the panel's edge, and
     it paints at z-index 4 OVER the content — so it clears the copy only when
     that sum is no greater than the panel's padding. Every framed panel on this
     site pads 18px at mobile, and 7 + 30 = 37 left every one of them 19px
     short: measured on one article page, 15 separate text runs sat underneath a
     corner curl — all four numbered discs (01/02/03/04) at the top-left of
     their cards, body copy in the bottom corners, and the newsletter seal.

     Pulled tight into the corner and sized to the padding: 2 + 14 = 16 ≤ 18,
     which leaves 2px of daylight. Keep that sum under the padding if you ever
     retune these, and re-run the collision check rather than eyeballing one
     card — the shortfall was invisible on three cards and obvious on the
     fourth purely because of what happened to sit in the corner. */
  .sp-framed,
  .sp-framed--lg,
  .sp-framed--sm{ --fil-inset:2px; --fil-size:14px; }
  .nav-links,
  .navbar .search-box{ display:none; }
  .menu-toggle{ display:flex; }
  /* minmax(200px) here, and a fixed two-up below 480px (see the block after
     this one). The old minmax(160px) had a dead zone: at 360px the grid box
     is 328px and two 160px tracks plus the 12px gap need 332, so the grid
     fell to ONE column from 340px to 371px while showing two at 375 — the
     most common phone widths got the worst layout. 200px also means any
     card that shows its description is at least ~200px wide. */
  .featured-grid,
  .sp-grid{ grid-template-columns:repeat(auto-fill, minmax(200px,1fr)); gap:var(--sp-3); }
  .featured-body{ padding:var(--sp-3); }
  .featured-card,
  .blog-card,
  .sp-feature,
  .feature-card{ rotate:0deg; }           /* tilt reads as broken on narrow screens */
  .featured-card::after{ width:36px; height:36px; top:8px; right:8px; }
  /* The pinned hero asides need the plate to have room around it; at this
     width they would land on top of the photograph. The roundel stays — it
     is the one badge that carries information. */
  .sp-hero-card{ display:none; }
  .sp-hero-roundel{ top:calc(-1 * var(--sp-4)); right:calc(-1 * var(--sp-2)); }
  .sp-cols-2,.sp-cols-3,.sp-cols-4{ grid-template-columns:1fr; }
  .content-card,
  .sp-panel{ padding:var(--sp-4); }
  .newsletter-left,
  .newsletter-right{ padding:var(--sp-5) var(--sp-4); }
  .footer-content{ grid-template-columns:1fr; gap:var(--sp-5); }
  .footer-content--3{ grid-template-columns:1fr; }
  .footer-card{ padding:var(--sp-4); }
  .footer-bottom{ flex-direction:column; align-items:flex-start; }
  .footer-legal{ justify-content:flex-start; }
  .sp-stats,
  .hero-stats{ gap:var(--sp-4); }
  .sp-ribbon,
  .sp-strip{ gap:var(--sp-2); font-size:.86rem; letter-spacing:var(--track-caps); }
  .author-card{ flex-direction:column; }
  .glossary-item{ grid-template-columns:1fr; }
  /* The phone roundel scale. ORDER MATTERS: every rule here is 0,1,0, so the
     base size has to come first and the modifiers after it, or the base
     cancels them — which is exactly what shipped: --sm, --lg AND the hero's
     own 74px all rendered at 88px below 760px, and the badge hierarchy the
     roundel exists to express (seal < badge < price stamp) was flat. The
     ratios are the desktop ones (78 : 112 : 150) scaled to the 88px base. */
  .sp-roundel{ --roundel-size:88px; }
  .sp-roundel--sm{ --roundel-size:64px; }
  .sp-roundel--lg{ --roundel-size:118px; }
  .sp-hero-roundel{ --roundel-size:74px; }
  .table-scroll{ margin-inline:calc(-1 * var(--gutter)); border-radius:0; border-inline:0; }
}

/* Phones in portrait: the featured grid is a fixed two-up (auto-fill would
   drop to one column under 412px), the card description goes once a card is
   narrower than ~200px, and the three hero stats stop wrapping 2 + 1 — each
   becomes one line, number then label, down the left rule. */
@media (max-width:480px){
  .featured-grid,
  .sp-grid{ grid-template-columns:repeat(2, minmax(0,1fr)); }
  .sp-stats{ display:grid; grid-template-columns:minmax(0,1fr); gap:var(--sp-2); padding-block:var(--sp-3); }
  .sp-stat{
    flex-direction:row; align-items:baseline; flex-wrap:wrap;
    gap:0 var(--sp-3); min-width:0; padding-block:2px;
  }
}
@media (max-width:440px){
  .featured-desc{ display:none; }         /* three clamped lines at ≤ 20 characters each is not a description */
}

/* ═══════════════════════════════════════════════════════════════════════════
   22 · REDUCED MOTION + PRINT

   Everything ornamental is animation. None of it carries meaning, so
   prefers-reduced-motion switches all of it off — the spinning bursts, the
   floating doodles, the sticker straightening, the drawer slide, the smooth
   scroll. State changes stay instant and visible; only the movement goes.
   ═══════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion:reduce){
  html{ scroll-behavior:auto; }
  *,*::before,*::after{
    animation-duration:.001ms!important;
    animation-iteration-count:1!important;
    transition-duration:.001ms!important;
    scroll-behavior:auto!important;
  }
  .float-item{ animation:none; }
  .sp-star-behind::before,
  .sp-rays--spin::before{ animation:none; }
  /* No off-axis tilting and no lift: the ornament stays, the movement goes. */
  .sp-sticker,
  .sp-sticker:hover,
  .featured-card,
  .featured-card:hover,
  .blog-card,
  .blog-item,
  .sp-feature,
  .feature-card,
  .sp-roundel,
  .sp-roundel:hover,
  .logo-icon,
  .author-portrait,
  .tag,.tag-1,.tag-2,.tag-3,
  .sp-hero-card-1,.sp-hero-card-2{ rotate:0deg!important; translate:0 0!important; }
  .mobile-nav{ transition:none; }
  /* The hamburger's open state is a STATE, not decoration — it must still
     change shape, it just does it instantly. */
  .menu-toggle span{ transition:none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   §15  ABOVE THE FOLD — both hero calls to action visible without scrolling

   Measured in a same-origin iframe at the real layout viewport (headless
   Chrome clamps its own viewport near 526px, so a --window-size shot cannot
   answer this). Before these rules the second button sat 76px below the fold
   on a 1366x768 laptop and 107px below on a 360x800 phone, and on a 1440x800
   laptop it cleared the fold by only 56px — less than the cookie banner is
   tall, so the banner covered it anyway.

   The queries key off HEIGHT, not width: a short window is the problem, and a
   tall phone or a large desktop should keep the full poster. Nothing here
   shrinks the display type below its 3:1 contrast floor, and the sentence the
   short-screen rule drops is supplementary — the promise the page is making
   still reads in full without it.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-height:900px){
  .sp-hero{ padding-block:var(--sp-5) var(--sp-6); }
  /* 24ch instead of 19ch sets the headline in three lines rather than four at
     laptop widths — one whole line of poster type reclaimed without touching
     the font size. */
  .sp-hero .sp-h1{
    font-size:clamp(2rem, 4.4vw, 3.35rem);
    max-width:24ch;
    margin-bottom:var(--sp-4);
  }
  .sp-hero .sp-eyebrow{ margin-bottom:var(--sp-2); }
  .sp-hero-kicker{ margin-bottom:var(--sp-2); }
  .sp-hero .sp-lede{ margin-bottom:var(--sp-4); }
  .sp-hero-actions{ margin-bottom:var(--sp-4); }
  .sp-stats{ margin-top:var(--sp-3); }
}

/* Genuinely short: 768px-tall laptops and most phones in portrait with the
   browser chrome taken off. */
@media (max-height:800px){
  .sp-hero{ padding-block:var(--sp-4) var(--sp-5); }
  .sp-lede-tail{ display:none; }
  .sp-hero .sp-h1{ font-size:clamp(1.9rem, 4vw, 2.9rem); }
  .sp-hero .sp-lede{ font-size:var(--fs-body); }   /* was var(--fs-base), a token that does not exist — the declaration was being thrown away */
}

/* Phones. The stacked buttons cost ~90px of the fold on their own, so they go
   side by side and the eyebrow — pure decoration — steps aside. */
@media (max-width:760px) and (max-height:820px){
  .sp-hero .sp-eyebrow{ display:none; }
  .sp-hero .sp-h1{ font-size:clamp(1.75rem, 8.2vw, 2.4rem); }
  .sp-hero-actions{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:var(--sp-2);
  }
  .sp-hero-actions .sp-btn{
    width:100%;
    padding-inline:var(--sp-3);
    font-size:.82rem;
    /* Two condensed-caps labels of different lengths sit on one row: let the
       longer one wrap to two lines inside its own button rather than pushing
       the row taller than the shorter button. */
    display:flex; align-items:center; justify-content:center; text-align:center;
    min-height:52px;                     /* still a comfortable tap target */
  }
}

/* Very short phones (360x800 with chrome, and landscape). */
@media (max-width:760px) and (max-height:720px){
  .sp-hero{ padding-block:var(--sp-3) var(--sp-4); }
  .sp-hero-kicker{ font-size:1.15rem; margin-bottom:var(--sp-1, 4px); }
  .sp-hero .sp-lede{ margin-bottom:var(--sp-3); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   §20  AUDIT FIXES — 2026-08-12

   Each rule below closes a finding from the full-site audit and says which.
   They live at the end of the file on purpose: every one of them has to beat
   an equal-specificity rule declared earlier (and article.css cascades ahead
   of this file entirely), so position is what makes them win.
   ═══════════════════════════════════════════════════════════════════════ */

/* 132 · The --measure token was declared and never applied to prose. Body copy
   ran 84-91 characters per line on the legal pages, the about page and the
   glossary — for an elder-skewing audience with a 17.5px body size, that is
   the single worst readability defect on the site. The PANEL keeps its width;
   only the text column is bounded. */
.page-prose p,
.page-prose li,
.page-prose blockquote,
.glossary-def{ max-width:var(--measure); }
/* Ledes are set larger, so bound them in their own em, not the body's. */
.page-prose .lede,
.patterns-lede,
.sp-lede{ max-width:60ch; }

/* 133 · Stacked call-to-action buttons measured 234.88px and 348.28px on a
   390px hero — a 113px spread on two buttons sharing a left edge, neither
   filling the column. When they stack, they match. */
@media (max-width:760px){
  .sp-hero-actions{ display:grid; grid-template-columns:1fr; gap:var(--sp-3); }
  .sp-hero-actions .sp-btn{ width:100%; justify-content:center; text-align:center; }
}
.footer-btn-row,
.footer-actions{ display:grid; gap:var(--sp-2); justify-items:stretch; }
.footer-btn-row .footer-btn,
.footer-actions .footer-btn{ width:100%; justify-content:center; text-align:center; }

/* 134 · The ribbon's ★ separators became line-terminal and line-initial
   punctuation the moment the band wrapped — "Free forever ★" ending a line,
   "★ US crochet terms" starting the next. A separator that ends a line is not
   a separator. Below the wrap point the stars go and the band centres. */
@media (max-width:760px){
  .sp-ribbon{ justify-content:center; text-align:center; row-gap:var(--sp-1, 4px); }
  .sp-ribbon__star{ display:none; }
  .sp-ribbon__item:not(:last-child)::after{
    content:'·';
    margin-inline:var(--sp-2);
    color:currentColor; opacity:.55;
  }
}

/* 139 · The newsletter's experience dropdown kept Chrome's native chrome while
   its three sibling controls were painted by the design system — a system
   select in a hand-drawn form. Paint it, and draw the chevron from ink. */
.newsletter-form select,
.sp-form select,
select.form-input{
  appearance:none; -webkit-appearance:none; -moz-appearance:none;
  padding-right:46px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink) 50%),
    linear-gradient(135deg, var(--ink) 50%, transparent 50%);
  background-position:
    calc(100% - 24px) calc(50% + 2px),
    calc(100% - 18px) calc(50% + 2px);
  background-size:6px 6px, 6px 6px;
  background-repeat:no-repeat;
}

/* 141 · The four hero chips had four fills, TWO border widths (1.25px on the
   category badge, 2.5px on the other three) and four heights — 38.6 / 44.8 /
   44 / 43px, a 6.2px spread that left both rows visibly ragged on a phone.
   The ±1-2deg sticker tilt is the design and stays; the ragged baseline was
   not. One chip component, one height, one border weight.

   The container is `.hero-tags` (inside `.decorative-elements`). An earlier
   pass at this wrote `.hero-meta`, which does not exist on the page, so the
   rule silently did nothing — check the class against the rendered DOM, not
   against what the component is called in prose. */
.hero-tags .article-category-badge,
.hero-tags .tag{
  display:inline-flex; align-items:center; justify-content:center;
  min-height:44px;                 /* also the WCAG 2.5.8 target size */
  padding:0 var(--sp-3);
  border-width:var(--bw);
  border-style:solid;
  line-height:1.1;
}
.hero-tags{ align-items:stretch; row-gap:var(--sp-2); }

/* 147 · A "variations" panel whose single list item ran 23 rendered lines is
   not a list — the lone marker read as a typo. Bound the measure at least, so
   the block sets as prose rather than as one enormous bullet. */
.variations li,
.mistake-item{ max-width:var(--measure); }
.variations li:only-child{ list-style:none; margin-left:0; }
.variations li:only-child::marker{ content:''; }

/* 68 · No safe-area insets anywhere. On a notched phone the footer and every
   fixed bottom control landed inside the home-indicator strip, and in
   landscape the gutters ran under the notch. viewport-fit=cover is what makes
   env() non-zero; it is set in the <meta viewport> of every page system. */
body{
  padding-left:env(safe-area-inset-left);
  padding-right:env(safe-area-inset-right);
}
.footer-section{ padding-bottom:calc(var(--sp-7) + env(safe-area-inset-bottom)); }

/* 66 · A 740x360 landscape phone gave 48% of the viewport to chrome, so the
   hero call to action was almost entirely below the fold before the cookie
   banner even appeared. Tighten the vertical chrome when height is scarce. */
@media (max-height:480px) and (orientation:landscape){
  .sp-hero{ padding-block:var(--sp-3) var(--sp-4); }
  .sp-hero .sp-eyebrow,
  .sp-hero-kicker{ display:none; }
  .sp-hero .sp-h1{ font-size:clamp(1.5rem, 5.2vw, 2rem); }
  .sp-hero .sp-lede{ margin-bottom:var(--sp-2); font-size:var(--fs-sm); }
  .sp-hero-actions{ grid-template-columns:1fr 1fr; }
  .navbar{ --nav-h:56px; }
}

/* One unbreakable display word can take the whole document sideways: at 320px
   the glossary H1's "Abbreviations" measured 325px against a 269px box and put
   40px of horizontal scroll on the page. break-word only acts when the word
   genuinely cannot fit, so normal wrapping is untouched. */
.sp-display,.sp-h1,.sp-h2,.hero-title,.patterns-h1,
h1,h2,h3{ overflow-wrap:break-word; }

/* The footer card is the one .sp-framed panel with CONTENT in its corners.
   Its 30px filigree motifs overlapped the script credit by 9.5px in both axes
   at 390px — the "K" of "Kept by Ruby, one round at a time." sat underneath
   the bottom-left squiggle — and the right-aligned credit meets the
   bottom-right motif the same way on desktop. Measured: motif box x 16-46 /
   y 773.9-803.9 against a credit box starting at x 36.5 and ending at y 783.4.
   Shrink the ornament on this panel and give the last row room beneath it. */
.footer-card{ --fil-size:18px; }   /* desktop; the ≤760px block overrides it */
.footer-card .footer-bottom{ padding-bottom:var(--sp-3); }

@media print{
  body{ background:#fff; color:#000; }
  .navbar,.mobile-nav,.mobile-overlay,.footer-section,.sp-ribbon,.sp-strip,
  .newsletter-section,.floating-elements,.float-item,.skip-link,.sp-nlpop,
  .pagination-section,.sp-tape,.sp-scallop,.scalloped-border{ display:none!important; }
  .sp-display,.hero-title,.sp-h1,.sp-h2,.patterns-h1{ color:#000; text-shadow:none; }
  .content-card,.sp-panel{ box-shadow:none; border:1px solid #000; break-inside:avoid; }
  a[href^="http"]::after{ content:" (" attr(href) ")"; font-size:.8em; }
}
