    /* ---- SELF-HOSTED FONTS (no third-party request, works offline) ----
       Space Grotesk = body / headings / nav (geometric, techy, very legible).
       IBM Plex Mono  = terminal chrome only: title bars, prompt lines, prices,
       buttons, the 404 heading. Both OFL, served from /fonts, cached one week
       by the /fonts rule in _headers. Space Grotesk ships as ONE variable file
       (300-700 axis); Plex Mono as two static weights. */
    @font-face {
      font-family: 'Space Grotesk';
      font-style: normal;
      font-weight: 300 700;
      font-display: swap;
      src: url('/fonts/SpaceGrotesk-Variable.woff2') format('woff2');
    }
    @font-face {
      font-family: 'IBM Plex Mono';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url('/fonts/IBMPlexMono-Regular.woff2') format('woff2');
    }
    @font-face {
      font-family: 'IBM Plex Mono';
      font-style: normal;
      font-weight: 600;
      font-display: swap;
      src: url('/fonts/IBMPlexMono-SemiBold.woff2') format('woff2');
    }

    html {
      background-color: #000;
      background-image:
        repeating-linear-gradient(0deg, rgba(255,255,255,.028) 0 1px, rgba(0,0,0,0) 1px 4px),
        repeating-linear-gradient(90deg, rgba(255,255,255,.028) 0 1px, rgba(0,0,0,0) 1px 4px);
      /* pixelated keeps the 4px background grid's hairlines crisp. It is an
         INHERITED property, so it must NOT be allowed to reach photos — the
         img rule below resets that. */
      image-rendering: pixelated;
    }
    /* Photos (merch cards, item gallery, About headshot, project tiles, the
       Patreon SVG) must use the browser's smooth scaler, not the inherited
       nearest-neighbour from html. Without this every <img> is downscaled
       pixelated and looks like garbage. The inline-SVG logo/sigil are vector
       and unaffected either way. */
    img { image-rendering: auto; }
    body {
      margin: 0; color: #e6e6e6;
      font-family: 'Space Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
      font-size: 16px; font-weight: 400; line-height: 1.45; letter-spacing: 0;
      -webkit-font-smoothing: antialiased;
    }
    /* Terminal chrome runs on the mono face. Everything else inherits the
       body's Space Grotesk. Prices (.rbtext / .term-rendered) and buttons live
       here so numbers and CTAs read as terminal type, not body copy. */
    .tbar, .term, .buy, .rbtext {
      font-family: 'IBM Plex Mono', 'Courier New', monospace;
    }
    a { color: #fff; text-decoration: underline; }
    a:hover { color: #000; background: #fff; }
    .px   { letter-spacing: .02em; }
    .term { font-weight: 400; }
    .dim { color: #9a9a9a; }
    .faint { color: #6a6a6a; }
    h1,h2,h3,h4 { margin: 0; font-weight: 500; letter-spacing: 0; }

    /* ---- RAINBOW EDGE ----
       Four strips, one per side. Lengths are real pixels measured along the
       perimeter, so a wide box and a tall box get identical rainbow runs.
       Two runs, half a perimeter apart, both starting from the top-left corner
       going clockwise — so every box on the site is in the same phase.
       Slides with scroll position. */
    .rbox { position: relative; padding: 3px; background: #000; border-radius: 0; }
    .rbox > .e { position: absolute; pointer-events: none; }
    .rbox > .et { top: 0; left: 0; height: 3px; }
    .rbox > .eb { bottom: 0; left: 0; height: 3px; }
    .rbox > .el { top: 0; left: 0; width: 3px; }
    .rbox > .er { top: 0; right: 0; width: 3px; }
    .rbox > .win { position: relative; z-index: 1; }

    /* OS window */
    .win { background: #000; border: 1px solid #3a3a3a; }
    .tbar {
      background: #262626; color: #d8d8d8; border-bottom: 1px solid #3a3a3a;
      padding: 5px 5px 5px 9px; display: flex; align-items: center; justify-content: space-between; gap: 8px;
      font-size: 12px; letter-spacing: .04em; font-weight: 600;
    }
    /* Long "NAME // LABEL" titles must not shove the window buttons off the
       bar or overflow on a phone — mono glyphs are wider than the old pixel
       font. Let the label truncate; the buttons keep their slot. */
    .tbar b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .wbtn { display: flex; gap: 3px; }
    .wbtn i { width: 14px; height: 12px; border: 1px solid #5a5a5a; background: #3a3a3a;
      display: block; position: relative; }
    .wbtn i::before, .wbtn i::after { content: ""; position: absolute; background: #d8d8d8; }
    /* minimise */
    .wbtn i:nth-child(1)::after { left: 3px; right: 3px; bottom: 2px; height: 1px; }
    /* maximise */
    .wbtn i:nth-child(2)::after { left: 3px; right: 3px; top: 2px; bottom: 2px;
      background: none; border: 1px solid #d8d8d8; border-top-width: 2px; }
    /* close */
    .wbtn i:nth-child(3)::before, .wbtn i:nth-child(3)::after {
      left: 2px; right: 2px; top: 50%; height: 1px; margin-top: -1px; }
    .wbtn i:nth-child(3)::before { transform: rotate(45deg); }
    .wbtn i:nth-child(3)::after  { transform: rotate(-45deg); }
    /* a panel with a single button gets a close, not a minimise */
    .wbtn i:only-child::before, .wbtn i:only-child::after {
      left: 2px; right: 2px; top: 50%; bottom: auto; height: 1px; margin-top: -1px;
      background: #d8d8d8; border: 0; }
    .wbtn i:only-child::before { transform: rotate(45deg); }
    .wbtn i:only-child::after  { transform: rotate(-45deg); }
    .pad { padding: 14px 16px; }
    .inset { border: 1px solid #333; background: #060606; }
    /* SM-7: plugin index cards + home deck tiles click through to plugin detail
       (wired by render.js wireCardNav). Show the pointer so they read as links. */
    [data-render="plugins"] .inset[data-slug],
    [data-render="deck"] .inset[data-slug] { cursor: pointer; }
    /* AG-10: these cards are keyboard-reachable now (deck tile = real <a>; plugins
       card = role="link" tabindex="0"). Show a clear focus ring so a keyboard user
       can see which card they're on — same cyan as the edit-mode focus outline. */
    [data-render="plugins"] .inset[data-slug]:focus-visible,
    [data-render="deck"] .inset[data-slug]:focus-visible {
      outline: 2px solid #2fd8ff; outline-offset: 2px; }

    hr.rule { border: 0; height: 5px; margin: 14px 0;
      background: repeating-linear-gradient(90deg, #4a4a4a 0 7px, #000 7px 14px); }
    .hazard { background: repeating-linear-gradient(135deg, #262626 0 9px, #000 9px 18px); }

    .buy { display: inline-block; border: 1px solid #fff; background: #fff; color: #000;
      padding: 8px 18px; font-size: 18px; letter-spacing: .05em; }
    .buy.alt { background: #000; color: #fff; }
    .buy.off { background: #0a0a0a; color: #6a6a6a; border-color: #3a3a3a; }

    .rbtext {
      display: inline-block;
      /* SM-33 — never let a long rainbow heading (e.g. the workshop box's
         "VJ PLUGINS FOR RESOLUME!") spill past its container; wrap instead. */
      max-width: 100%; overflow-wrap: break-word;
      background: linear-gradient(90deg,#ff2d2d,#ff9a1f,#ffe83d,#4dff5a,#2fd8ff,#6a5cff,#ff4dd2);
      -webkit-background-clip: text; background-clip: text; color: transparent;
    }
    .rbline { height: 3px; background: linear-gradient(90deg,
      #000, #ff2d2d, #ff9a1f, #ffe83d, #4dff5a, #2fd8ff, #6a5cff, #ff4dd2, #000); }

    /* progress bars */
    .bar { height: 14px; border: 1px solid #3a3a3a; background: #060606; position: relative; }
    .bar > span { display: block; height: 100%;
      background: repeating-linear-gradient(90deg, #e8e8e8 0 4px, #9a9a9a 4px 7px); }
    .barrow { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; }
    .barrow .nm { width: 120px; font-size: 18px; color: #fff; }
    .barrow .bar { flex-grow: 1; }
    .barrow .pc { width: 50px; text-align: right; color: #9a9a9a; font-size: 18px; }

    .logo { color: #fff; display: block; }
    .logo svg { display: block; width: 100%; height: auto; }

    /* Centre the whole page column in the viewport at every width above the
       breakpoint. The outer frame is the inline 1100px div; it sets width +
       padding inline but NOT margin, so auto side-margins apply with no
       !important. Ben's first look: the column hugged the left edge. Every
       page (index, plugins, plugin, merch) uses the same 1100px frame, so
       this one rule centres them all. */
    [style*="width:1100px"] { margin-left: auto; margin-right: auto; }

    ::-webkit-scrollbar { width: 15px; }
    ::-webkit-scrollbar-track { background: #000; border: 1px solid #3a3a3a; }
    ::-webkit-scrollbar-thumb { background: #3a3a3a; border: 1px solid #5a5a5a; }

    /* ================================================================
       RESPONSIVE — one breakpoint (story 1-3)
       Ben's traffic is Instagram → phone. Above 720px NOTHING here
       applies; the desktop look is the mockup, untouched. These
       selectors match the mockup's own inline layout values, so every
       page built from that markup inherits mobile behaviour for free.
       ================================================================ */
    @media (max-width: 720px) {
      /* Fixed-width containers become fluid. max-width caps the inline
         px width without needing !important. Widths enumerated are the
         mockup's real column widths (frame, content, home split,
         detail-page split). */
      [style*="width:1100px"], [style*="width:1000px"],
      [style*="width:676px"],  [style*="width:600px"],
      [style*="width:384px"],  [style*="width:308px"],
      [style*="width:300px"] { max-width: 100%; }

      /* SM-34 — home right column (STATUS + VISITORS): the inline width:308px is
         NARROWER than the phone viewport, so max-width:100% above never stretches
         it and both boxes sat short of the full content width (unlike the
         welcome/Patreon boxes in the 676px left column, which DO fill because 676
         caps to 100%). Force the column to the full width so STATUS and VISITORS
         span it; the flex column stretches both .rbox children to match. Desktop
         (>720px) keeps the 676/308 two-column split untouched. The 676 left
         column is forced to 100% in the SAME breakpoint so that between 676px and
         720px it matches the content width too (max-width:100% alone leaves it at
         676 while the right column fills) — so STATUS/VISITORS line up with the
         welcome/Patreon boxes at every width ≤720. */
      [style*="width:308px"], [style*="width:676px"] { width: 100% !important; }

      /* The page frame's inline padding is "22px 0 34px" (no side gutter),
         which lets content touch the phone's edge. Force a side gutter and
         border-box so the padding fits inside the fluid width. */
      [style*="width:1100px"] {
        box-sizing: border-box !important;
        padding-left: 14px !important;
        padding-right: 14px !important;
      }

      /* Two-column layouts stack. The mockup's splits are flex rows
         aligned to the top (home: 676/308; detail pages: 600/384).
         flex-direction is not set inline, so no !important needed. */
      [style*="align-items:flex-start"] { flex-direction: column; }

      /* Deck / card grids drop to 2-across. grid-template-columns IS
         set inline, so this override needs !important. */
      [style*="grid-template-columns:repeat(4"],
      [style*="grid-template-columns:repeat(3"] {
        grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
      }

      /* Nav / header rows wrap instead of overflowing the viewport. */
      nav, [style*="justify-content:space-between"] { flex-wrap: wrap; }

      /* Nothing exceeds the viewport width. border-box keeps a box's
         padding + border inside its (grid/flex) track so tiles don't bleed. */
      .win, .inset { box-sizing: border-box; max-width: 100%; }
      .logo { max-width: 100%; }
      img, svg { max-width: 100%; height: auto; }

      /* Flex children default to min-width:auto, which refuses to shrink below
         their content — that is what pushes the banner's fixed-width logo lockup
         and the nav's fixed cells past the phone edge. Let flex children shrink
         so the max-width caps above actually take effect, cap the one remaining
         fixed inline width (the 380px lockup), and let the nav row wrap instead
         of overflow. (Story 1-6; scoped to ≤720px, desktop is untouched.) */
      [style*="display:flex"] > * { min-width: 0; }
      [style*="width:380px"] { max-width: 100%; }
      .win[style*="display:flex"] { flex-wrap: wrap; }
      /* SM-34 — the grey secondary sigil (.sigil-mark) is DELETED from every page
         (markup + CSS). The old ≤720px `display:none` hid it on phone but it still
         flashed on refresh before this rule applied; removing the markup kills it
         everywhere, so there is no rule to keep here. */
    }

    /* ================================================================
       STORY 1-17 — home: split banner, wide Patreon box
       (SM-21: 88x31 buttons box removed — useless/confusing + carried a NO-DRM
       button that breaks the cut-anti-DRM copy rule)
       ================================================================ */
    /* Wide Patreon / Workshop box: official mark on the left, the workshop
       copy + JOIN ON PATREON button on the right. The box itself spans the
       full content column (it is a direct child of the home column, not the
       old 676/308 split), so it is full width on desktop and phone. */
    .patreon { display: flex; align-items: center; gap: 24px; }
    .patreon .pmark { flex: 0 0 auto; width: 104px; }
    .patreon .pmark img { display: block; width: 100%; height: auto; }
    .patreon .pcopy { flex: 1 1 0; min-width: 0; }

    /* SM-36 — the header wrapper (wordmark + "> Ben Cooper // VJ - Creator -
       Artist" line). One class in the shared stylesheet, NOT five inline styles,
       so the SM-29 router (which swaps only the content region, never the <head>)
       carries it across a nav with nothing jumping. DESKTOP (>720px): left-aligned
       — align-items:flex-start + text-align:left. The ≤720px rule below re-centres
       for phones. padding/gap unchanged so the banner box height is identical. */
    .banner-head {
      padding: 24px 20px;
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 10px;
      text-align: left;
    }

    /* AC5: desktop keeps the banner on one line
       ("Ben Cooper // VJ - Creator - Artist"). The two-line phone form is in
       the media query below. */
    .banner .banner-name, .banner .banner-role { display: inline; }

    @media (max-width: 720px) {
      /* AC5: at ≤720px the banner is centred on two lines — "Ben Cooper" then
         "VJ - Creator - Artist" — with the prompt and the "//" separator
         dropped. The spans go block so each sits on its own line. */
      /* SM-36 — phone stays exactly as before: the header wrapper re-centres
         (desktop left-align above is overridden here). */
      .banner-head { align-items: center; text-align: center; }
      .banner { text-align: center; }
      .banner .banner-prompt, .banner .banner-sep { display: none; }
      .banner .banner-name, .banner .banner-role { display: block; }

      /* The wide Patreon box stacks: mark on top, copy below, both centred.
         SM-33 — the copy column must take the FULL width here; without it the
         centred column shrinks to its widest child (the inline-block rainbow
         heading, which won't wrap on its own) and the long "VJ PLUGINS FOR
         RESOLUME!" heading blows out past the box edge at 390px. */
      .patreon { flex-direction: column; align-items: center; text-align: center; gap: 14px; }
      .patreon .pmark { width: 72px; }
      .patreon .pcopy { width: 100%; }
    }

    /* ================================================================
       STORY 1-21 / 1-22 — the shop (merch grid, item page, cart)
       Black-and-white chrome only. The one place colour appears is the
       product's own swatch chip (.swatch .chip), which IS product data
       straight from the Fourthwall API — the story asks for it.
       ================================================================ */
    /* SM-35 — the merch grid lives HERE in the shared stylesheet, NOT inline in
       merch.html. The SM-29 client router swaps only the content region (never the
       <head>), so a nav from Home to /merch used to inject the grid markup WITHOUT
       the grid-template rule that sat in merch.html's inline <style>: the cards fell
       back to full-width blocks and the square photos rendered HUGE. A hard reload is
       a real document load that carries the inline <style>, which is why reloading
       "fixed" it. Moving the rule into site.css (in every page's head, persistent
       across router swaps) fixes BOTH the direct load and the router-nav path. Kept
       as a CLASS (not an inline grid-template) so the global repeat(3)->repeat(2)
       phone override above does not catch it; this grid goes straight to ONE column
       at <=720. */
    .merch-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
    @media (max-width: 720px) { .merch-grid { grid-template-columns: minmax(0, 1fr); } }

    /* merch grid card: the whole card is a link; show the pointer + a hover lift
       the same way the plugin cards read as clickable. min-width:0 lets the grid
       item shrink to its track so its content (a wide photo/canvas) can never push
       it past the column (SM-35). */
    .shop-card { cursor: pointer; min-width: 0; }
    .shop-card:hover { border-color: #5a5a5a; }
    .shop-card:hover, .shop-card:hover * { color: inherit; background: inherit; }
    .soldout { color: #6a6a6a; font-size: 16px; letter-spacing: .05em; }

    /* SM-31 — merch grid card image: a SQUARE box on #000 so every card is the
       same shape at every width; the photo FILLS the square via object-fit:cover
       (centre-crop, no black side bars). shop.js replaces the <img> with a square
       <canvas> holding a stepped-halved centre-crop downscale of the 1920px source
       so it doesn't alias when drawn small in one pass (worst in WebKit); the
       <img> is the plain fallback (also cover) when JS is off or the canvas path
       fails. The item-page gallery (.itemmain below) is unchanged — it stays
       contain in its fixed frame. */
    .card-photo { aspect-ratio: 1 / 1; background: #000; border: 1px solid #3a3a3a;
      overflow: hidden; display: flex; align-items: center; justify-content: center; }
    .card-photo img, .card-photo canvas { width: 100%; height: 100%;
      max-width: 100%; max-height: 100%;
      object-fit: cover; object-position: center; display: block; }

    /* item page gallery: one big main image, a row of square thumbnails under it.
       SM-28 — STABLE GALLERY: the main frame keeps ONE fixed aspect ratio for the
       whole product (shop.js sets `aspect-ratio` inline = the tallest image across
       every colour), so switching photo or colour never resizes it and the thumb
       row below never jumps. The image sits object-fit:contain, centred, with the
       page background (#000, same as .win) showing in the letterbox. The frame
       size is set before the image loads, so there is no reflow. */
    .itemmain { border: 1px solid #3a3a3a; background: transparent; aspect-ratio: 1 / 1.3333; }
    .itemmain img { display: block; width: 100%; height: 100%; object-fit: contain; object-position: center; }
    .thumbrow { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
    .thumb { width: 64px; height: 64px; padding: 0; border: 1px solid #3a3a3a; background: #0a0a0a;
      cursor: pointer; overflow: hidden; }
    /* SM-28 — thumb quality: the <img> is the plain fallback (no JS / canvas fails);
       shop.js replaces it with a <canvas> holding a stepped-halved downscale so the
       1920px source doesn't alias when squeezed to 64px in one step (worst in WebKit). */
    .thumb img, .thumb canvas { width: 100%; height: 100%; object-fit: cover; display: block; }
    .thumb.on { border-color: #fff; outline: 1px solid #fff; }
    .thumb:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }

    /* colour swatches: a chip in the product's real colour + the colour name. */
    .swatchrow, .sizerow { display: flex; flex-wrap: wrap; gap: 8px; }
    .swatch { display: inline-flex; align-items: center; gap: 8px; padding: 6px 10px;
      border: 1px solid #3a3a3a; background: #0a0a0a; color: #cfcfcf; cursor: pointer;
      font-family: 'IBM Plex Mono', 'Courier New', monospace; font-size: 14px; }
    .swatch .chip { width: 16px; height: 16px; border: 1px solid #5a5a5a; display: inline-block; }
    .swatch.on { border-color: #fff; color: #fff; background: #1a1a1a; }
    .swatch.out { opacity: .55; }
    .swatch:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }

    /* size picker: plain mono buttons; a sold-out size is disabled + greyed.
       SM-32 — each button stacks the size name over its remaining-stock line
       ("3 left", or a greyed "SOLD OUT" when the variant is out). */
    .sizebtn { min-width: 52px; padding: 7px 12px; border: 1px solid #3a3a3a; background: #0a0a0a;
      color: #e6e6e6; cursor: pointer; font-family: 'IBM Plex Mono', 'Courier New', monospace;
      font-size: 15px; letter-spacing: .03em; display: inline-flex; flex-direction: column;
      align-items: center; gap: 3px; line-height: 1.1; }
    .sizebtn .sz-name { font-size: 15px; }
    .sizebtn .sz-stock { font-size: 11px; color: #9a9a9a; letter-spacing: 0; }
    .sizebtn.on { background: #fff; color: #000; border-color: #fff; }
    .sizebtn.on .sz-stock { color: #444; }
    .sizebtn.off { color: #5a5a5a; border-color: #2a2a2a; background: #060606; cursor: not-allowed; }
    .sizebtn.off .sz-stock { color: #5a5a5a; }
    .sizebtn:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }

    /* SM-32 — "N left" shown next to the price for a no-size product (the sticker). */
    .stocknote { font-size: 14px; color: #9a9a9a; margin-left: 12px; letter-spacing: .02em; }

    /* ADD TO CART button that can't be used yet (no size picked / sold out). */
    .buy.off { cursor: default; }

    /* the description block — keep the safe formatting tags readable, dim. */
    .shopdesc p { margin: 0 0 10px; }
    .shopdesc ul, .shopdesc ol { margin: 0 0 10px; padding-left: 20px; }
    .shopdesc li { margin: 2px 0; }

    /* cart lines: name | qty stepper | line price | remove. */
    .cartlines { display: flex; flex-direction: column; gap: 10px; }
    .cartline { display: grid; grid-template-columns: 1fr auto auto auto; align-items: center;
      gap: 12px; border: 1px solid #2a2a2a; background: #060606; padding: 8px 10px; }
    .cl-name { min-width: 0; display: flex; align-items: center; gap: 10px; }
    /* SM-33 — square photo of the line item (chosen colour's first photo; sticker =
       product photo). shop.js fills the plain <img> then upgrades it to a stepped
       downscale canvas so it stays sharp at this small size. */
    .cl-thumb { flex: 0 0 auto; width: 48px; height: 48px; background: #000;
      border: 1px solid #3a3a3a; overflow: hidden; display: block; }
    .cl-thumb img, .cl-thumb canvas { width: 100%; height: 100%;
      object-fit: cover; object-position: center; display: block; }
    .cl-thumb:empty { display: none; }
    .cl-meta { min-width: 0; }
    .cl-qty { display: inline-flex; align-items: center; gap: 6px; }
    .cl-price { white-space: nowrap; }
    .qtybtn { width: 28px; height: 28px; line-height: 1; border: 1px solid #3a3a3a; background: #0a0a0a;
      color: #fff; cursor: pointer; font-family: 'IBM Plex Mono', 'Courier New', monospace; font-size: 16px; }
    .qtybtn:hover { background: #fff; color: #000; }
    .qtybtn[disabled] { opacity: .5; cursor: default; }
    .cl-rm { color: #cfcfcf; }
    .qn { min-width: 20px; text-align: center; color: #fff;
      font-family: 'IBM Plex Mono', 'Courier New', monospace; }

    @media (max-width: 720px) {
      /* the item page's 600/384 split stacks via the shared align-items:flex-start
         rule above; the gallery just goes full width. Cart line: let the name take
         the top row, the controls wrap under it. */
      .cartline { grid-template-columns: 1fr auto; row-gap: 8px; }
      .cl-name { grid-column: 1 / -1; }
      .thumb { width: 54px; height: 54px; }
    }

    /* ================================================================
       SM-27 — no-blank nav + smooth page swaps, footer links, About
       paragraph spacing, item quantity stepper
       ================================================================ */
    /* (3) NO page-navigation cross-fade. SM-27 added a cross-document page fade
       here (the CSS navigation-transition rule); Ben did not like the fade (SM-29),
       so it is removed entirely — there is NO fade anywhere on navigation. Instant
       swaps now come from the client-side router (js/router.js): it keeps the shell
       (banner + nav + rainbow edges) mounted and swaps only the content region, so
       nothing fades, blanks, or resets. With JS off, links do a plain instant load
       (progressive enhancement) — still no fade. */

    /* (4) Footer link bar: real <a>s (INSTAGRAM / PATREON / TWITCH / EMAIL),
       centred in the bar, wrapping onto two lines on a phone. Hrefs come from
       content.json site.links (render.js fillLinks); the static markup carries
       them too so nothing blanks. */
    .footlinks { display: flex; flex-wrap: wrap; justify-content: center;
      gap: 12px 24px; font-size: 16px; }
    .footlinks a { color: #cfcfcf; text-decoration: none; letter-spacing: .04em; }
    .footlinks a:hover { color: #000; background: #fff; }

    /* (7) About copy: each "\n"-split line is its own paragraph with a real
       blank-line gap, so the copy reads as paragraphs, not one wall of text. */
    .apara { margin: 0 0 14px; }
    .apara:last-child { margin-bottom: 0; }

    /* SM-40 — "WHAT I MAKE" row markers ([1]/[2]/[3]). Each row is a flex box:
       marker + heading/body. The global ≤720px rule above
       ([style*="display:flex"] > * { min-width: 0; }) resets every flex
       child's automatic min-width to 0, which lets the marker (default
       flex-shrink:1) get squashed by its long sibling text down to a few px —
       the "[1]" glyphs then overflow the squashed box and paint over the
       heading. SM-38/39 only added margin-right, which starts from the
       squashed box's edge and so had no visible effect. flex:none is the real
       fix: it takes the marker OUT of shrinking entirely, regardless of the
       min-width:0 override, so it always renders at its full text width. */
    .wim-n { flex: none; margin-right: .5em; color: #fff; }

    /* (9) Item page quantity stepper, sitting left of ADD TO CART. Reuses the
       cart's .qtybtn / .qn chrome. Disabled (greyed) until a buyable variant is
       picked; min 1, max = that colour×size stock. */
    .addrow { display: flex; gap: 10px; align-items: stretch; }
    .addrow .buy { display: flex; align-items: center; justify-content: center; }
    .qtypick { display: inline-flex; align-items: center; gap: 6px;
      border: 1px solid #3a3a3a; background: #0a0a0a; padding: 4px 8px; }
    .qtypick.off { opacity: .5; }
