/* Generated by scripts/fetch-fonts.mjs — do not edit by hand.
   Run `npm run fonts` to refresh. Self-hosted so the site has no
   dependency on Google's CDN at runtime. SIL Open Font License. */

/* vietnamese */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 900;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url(/assets/fonts/v25-k3kQo8UDI-1M0wlSfdboLmvDIaK18A.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 900;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url(/assets/fonts/v25-k3kQo8UDI-1M0wlSfdfoLmvDIaK18A.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 900;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url(/assets/fonts/v25-k3kQo8UDI-1M0wlSfdnoLmvDIaI.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Courier Prime';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v11-u-450q2lgwslOqpF_6gQ8kELaw9pWs39pvnRPA.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Courier Prime';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v11-u-450q2lgwslOqpF_6gQ8kELawFpWs39pvk.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Courier Prime';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/assets/fonts/v11-u-4k0q2lgwslOqpF_6gQ8kELY7pMT-7fq9PrNX2-Ag.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Courier Prime';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/assets/fonts/v11-u-4k0q2lgwslOqpF_6gQ8kELY7pMT-Dfq9PrNX0.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v16-aFTU7PB1QTsUX8KYthSQBK6PYK3EXw.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v16-aFTU7PB1QTsUX8KYthqQBK6PYK0.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/v16-aFTR7PB1QTsUX8KYvumzEY2tbYf-Vlh3uA.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/v16-aFTR7PB1QTsUX8KYvumzEYOtbYf-Vlg.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
  font-family: 'EB Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7e8QL9wU6i1hGFJRvzr2Q.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'EB Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7e8QL95U6i1hGFJRvzr2Q.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
  font-family: 'EB Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7e8QL9xU6i1hGFJRvzr2Q.woff2) format('woff2');
  unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
  font-family: 'EB Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7e8QL9-U6i1hGFJRvzr2Q.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'EB Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7e8QL9yU6i1hGFJRvzr2Q.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'EB Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7e8QL9zU6i1hGFJRvzr2Q.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'EB Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7e8QL99U6i1hGFJRvw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR4SDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GRxSDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR5SDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR2SDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR6SDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR7SDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR1SDk_YAPI.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR4SDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GRxSDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR5SDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR2SDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR6SDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR7SDk_YAPIlWk.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/v33-SlGUmQSNjdsmc35JDF1K5GR1SDk_YAPI.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ═══ Base — tokens, elements, shared primitives ════════════════ */
:root {
--ink:      #080706;
--ink-2:    #14110F;   /* raised panels on ink */
--cream:    #F7F3E8;   /* type on ink — never #FFF */
--paper:    #EFE9DA;
--pink:     #F7C4CB;
--red:      #C3231D;

--cream-78: rgba(247, 243, 232, 0.78);
--cream-55: rgba(247, 243, 232, 0.55);
--cream-36: rgba(247, 243, 232, 0.36);
--cream-05: rgba(247, 243, 232, 0.05);
--pink-22:  rgba(247, 196, 203, 0.22);
--cream-28: rgba(247, 243, 232, 0.28);
--cream-12: rgba(247, 243, 232, 0.12);

--display: Archivo, 'Helvetica Neue', sans-serif;
--serif:   'EB Garamond', Georgia, serif;
--mono:    'DM Mono', ui-monospace, monospace;
--struck:  'Courier Prime', 'Courier New', monospace;
--body:    Georgia, 'Times New Roman', serif;

--gut: clamp(1rem, 3.5vw, 2.75rem);
--hair: 1px solid var(--cream-12);
}

*, *::before, *::after { box-sizing: border-box; }
html {
-webkit-text-size-adjust: 100%;
/* Double-tap to zoom goes. It fires by accident constantly on a page this
   dense, and every fixed layer in the stack is sized to the layout viewport,
   so a stray zoom slides the whole texture out from under the content and
   leaves room to pan sideways into nothing. Pinch is deliberately still
   allowed: it is how somebody who needs the type larger reads the page, and
   iOS overrides any attempt to take it away regardless. */
touch-action: manipulation;
}

body {
margin: 0;
background: var(--ink);
color: var(--cream);
font-family: var(--body);
font-size: 17px;
line-height: 1.62;
overflow-x: hidden;
}

/* Grain. Applied once, over everything, fixed — so the whole surface
 reads as exposed stock rather than a flat screen. */
body::after {
content: '';
position: fixed;
/* Height is pinned to the tall viewport rather than tied to top and bottom.
A fixed layer with inset:0 resizes every time iOS collapses or restores the
address bar, and resizing this layer relays out its background tiles, so
the whole texture jumps sideways mid-scroll. Pinning the height means the
bar can come and go without the surface moving under the page. The cost is
a strip of overdraw below the fold on a phone, which is the cheaper of the
two. 100vh first for anything that does not know lvh. */
top: 0;
left: 0;
right: 0;
bottom: auto;
height: 100vh;
height: 100lvh;
pointer-events: none;
z-index: 9999;
opacity: 0.85;
/* Two grain layers and a mask. The fine grain and the coarse mottle jump
   every 450ms (grainshift, below). The density mask that shapes them stands
   still. Grain that arrives at one density everywhere is the single
   strongest tell that a page has a filter over it; real stock varies, and
   the mask is what varies it.

   Until 2 Sep 2026 the mask was baked into the fine tile itself, so it
   jumped with the grain. Measured on 64px blocks, that tile carried about
   300 times the coarse structure of the 180px tile it replaced, and a step
   of coarse structure reads as the whole frame lurching rather than as
   grain re-randomising. That lurch, twice a second, was the "frame by
   frame" choppiness reported on desktop. Split out as a CSS mask, the
   mask never moves and the fine tile is back to a size that only has to
   hide its own repeat, which pure noise does at any size. The mask tile is
   the one that has to be large, because a repeat in it would show. */
background-image:
  url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'380'%20height%3D'380'%3E%3Cfilter%20id%3D'n'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.9'%20numOctaves%3D'4'%20seed%3D'31'%20stitchTiles%3D'stitch'%2F%3E%3CfeColorMatrix%20type%3D'saturate'%20values%3D'0'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'380'%20height%3D'380'%20filter%3D'url%28%23n%29'%20opacity%3D'0.95'%2F%3E%3C%2Fsvg%3E"),
  url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'520'%20height%3D'520'%3E%3Cfilter%20id%3D'c'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.06'%20numOctaves%3D'6'%20stitchTiles%3D'stitch'%2F%3E%3CfeColorMatrix%20type%3D'saturate'%20values%3D'0'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'520'%20height%3D'520'%20filter%3D'url%28%23c%29'%20opacity%3D'0.45'%2F%3E%3C%2Fsvg%3E");
background-size: 380px 380px, 520px 520px;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'760'%20height%3D'760'%3E%3Cfilter%20id%3D'm'%20x%3D'0'%20y%3D'0'%20width%3D'100%25'%20height%3D'100%25'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.005'%20numOctaves%3D'3'%20seed%3D'41'%20stitchTiles%3D'stitch'%20result%3D'mask'%2F%3E%3CfeColorMatrix%20in%3D'mask'%20type%3D'luminanceToAlpha'%20result%3D'ml'%2F%3E%3CfeComponentTransfer%20in%3D'ml'%20result%3D'm'%3E%3CfeFuncA%20type%3D'gamma'%20amplitude%3D'1.5'%20exponent%3D'1.5'%20offset%3D'0.35'%2F%3E%3C%2FfeComponentTransfer%3E%3CfeFlood%20flood-color%3D'white'%20result%3D'w'%2F%3E%3CfeComposite%20in%3D'w'%20in2%3D'm'%20operator%3D'in'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'760'%20height%3D'760'%20filter%3D'url%28%23m%29'%2F%3E%3C%2Fsvg%3E");
        mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'760'%20height%3D'760'%3E%3Cfilter%20id%3D'm'%20x%3D'0'%20y%3D'0'%20width%3D'100%25'%20height%3D'100%25'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.005'%20numOctaves%3D'3'%20seed%3D'41'%20stitchTiles%3D'stitch'%20result%3D'mask'%2F%3E%3CfeColorMatrix%20in%3D'mask'%20type%3D'luminanceToAlpha'%20result%3D'ml'%2F%3E%3CfeComponentTransfer%20in%3D'ml'%20result%3D'm'%3E%3CfeFuncA%20type%3D'gamma'%20amplitude%3D'1.5'%20exponent%3D'1.5'%20offset%3D'0.35'%2F%3E%3C%2FfeComponentTransfer%3E%3CfeFlood%20flood-color%3D'white'%20result%3D'w'%2F%3E%3CfeComposite%20in%3D'w'%20in2%3D'm'%20operator%3D'in'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'760'%20height%3D'760'%20filter%3D'url%28%23m%29'%2F%3E%3C%2Fsvg%3E");
-webkit-mask-size: 760px 760px;
        mask-size: 760px 760px;
mix-blend-mode: overlay;
/* Shift the tile, not the box. An oversized fixed layer doubles overdraw
   and inflates the page's captured bounds for no visual gain. */
/* Slowed from 700ms — grain reads the same and adds far less flicker. */
animation: grainshift 1800ms steps(1) infinite;
}

/* Smaller window of the same mask on a phone, for the reason given on .tex.
   The grain tiles themselves are already small enough to share. */
@media (max-width: 700px) {
  body::after {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'460'%20height%3D'460'%3E%3Cfilter%20id%3D'm'%20x%3D'0'%20y%3D'0'%20width%3D'100%25'%20height%3D'100%25'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.005'%20numOctaves%3D'3'%20seed%3D'41'%20stitchTiles%3D'stitch'%20result%3D'mask'%2F%3E%3CfeColorMatrix%20in%3D'mask'%20type%3D'luminanceToAlpha'%20result%3D'ml'%2F%3E%3CfeComponentTransfer%20in%3D'ml'%20result%3D'm'%3E%3CfeFuncA%20type%3D'gamma'%20amplitude%3D'1.5'%20exponent%3D'1.5'%20offset%3D'0.35'%2F%3E%3C%2FfeComponentTransfer%3E%3CfeFlood%20flood-color%3D'white'%20result%3D'w'%2F%3E%3CfeComposite%20in%3D'w'%20in2%3D'm'%20operator%3D'in'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'460'%20height%3D'460'%20filter%3D'url%28%23m%29'%2F%3E%3C%2Fsvg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'460'%20height%3D'460'%3E%3Cfilter%20id%3D'm'%20x%3D'0'%20y%3D'0'%20width%3D'100%25'%20height%3D'100%25'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.005'%20numOctaves%3D'3'%20seed%3D'41'%20stitchTiles%3D'stitch'%20result%3D'mask'%2F%3E%3CfeColorMatrix%20in%3D'mask'%20type%3D'luminanceToAlpha'%20result%3D'ml'%2F%3E%3CfeComponentTransfer%20in%3D'ml'%20result%3D'm'%3E%3CfeFuncA%20type%3D'gamma'%20amplitude%3D'1.5'%20exponent%3D'1.5'%20offset%3D'0.35'%2F%3E%3C%2FfeComponentTransfer%3E%3CfeFlood%20flood-color%3D'white'%20result%3D'w'%2F%3E%3CfeComposite%20in%3D'w'%20in2%3D'm'%20operator%3D'in'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'460'%20height%3D'460'%20filter%3D'url%28%23m%29'%2F%3E%3C%2Fsvg%3E");
    -webkit-mask-size: 460px 460px;
            mask-size: 460px 460px;
  }
}

@keyframes grainshift {
0%   { background-position: 0 0, 0 0; }
25%  { background-position: -37px 21px, 40px -25px; }
50%  { background-position: 24px -33px, -60px 55px; }
75%  { background-position: -19px -11px, 25px 30px; }
100% { background-position: 13px 29px, -45px -40px; }
}

img, svg { max-width: 100%; height: auto; display: block; }

/* That `height: auto` is why the defs block has to be taken out of flow. ff_deck_defs()
   emits a <svg width="0" height="0"> holding nothing but filters and a clipPath, and it
   is the first child of .shell, directly above the masthead. It carries no viewBox, so
   once height is auto it has no intrinsic size to resolve against and falls back to the
   150px every SVG defaults to. Where that happens the masthead and the wave under it are
   pushed 150px down the page while the mark, the one piece that is position: fixed, stays
   exactly where it is, and the wave ends up drawn across the bird. Reported from an
   iPhone on 5 September 2026 and reproduced by forcing the default height; the geometry
   matched the phone to the pixel, logo at 164.6 and wave at 213.2 against 14.6 and 63.2.
   Absolute positioning settles it in every engine rather than relying on the attribute
   winning the argument. */
.dk-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ---- Type roles ------------------------------------------- */

.wordmark {
font-family: var(--display);
font-variation-settings: 'wdth' 74, 'wght' 800;
text-transform: uppercase;
line-height: 0.9;
letter-spacing: 0.005em;
}

.label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--cream-55);
}

.struck {
font-family: var(--struck);
text-transform: uppercase;
letter-spacing: 0.04em;
}

h1, h2, h3 { font-family: var(--serif); font-weight: 400; line-height: 1.1; margin: 0; }

a { color: inherit; }

.wrap { width: min(100% - (var(--gut) * 2), 1400px); margin-inline: auto; }

.pink { color: var(--pink); }

/* A slab of ground. Content does not sit on a page — it holds a patch of
 ink open against the field, the same way the mark does. Hard-edged: a
 soft gradient halo reads as blur, not as clear space. */
.slab {
background: rgba(8, 7, 6, 0.9);
box-shadow: 0 0 0 1px rgba(247, 243, 232, 0.07);
}

/* Struck-not-set: the log sheet's own control, a field you can press. */
.key {
display: inline-block;
background: rgba(8, 7, 6, 0.9);
font-family: var(--struck);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.12em;
padding: 0.5em 0.9em;
border: 1px solid var(--cream-28);
color: var(--cream);
text-decoration: none;
cursor: pointer;
transition: background 100ms linear, color 100ms linear, border-color 100ms linear;
}
.key:hover, .key:focus-visible, .key[aria-pressed='true'], .key[aria-current='page'] {
background: var(--pink);
border-color: var(--pink);
color: var(--ink);
}

:focus-visible { outline: 2px solid var(--pink); outline-offset: 2px; }

.visually-hidden {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---- Nav tiles -----------------------------------------------------
 GLOBAL because more than one component renders this markup. The Contact
 control builds its own .tile, and a scoped rule in the layout cannot reach
 another component's elements — which rendered the envelope at full size
 across the header. Same trap as .frag and .walker. */
/* No box. The icon carries it, and the name only appears when you go
   looking — the label is always in the DOM for screen readers. */
.tile {
  position: relative;
  display: grid;
  justify-items: center;
  width: 3.3rem;
  padding: 0.35rem 0.2rem;
  text-decoration: none;
  /* Brighter at rest than the labels: the icons are the actual controls and
     were disappearing into the grain, first at 55% and then again at 72% once
     they became solid marks — a filled shape holds less edge against that
     ground than an outline did. Still short of full cream, so hover to pink
     stays a clear step up. */
  color: rgba(247, 243, 232, 0.82);
  transition: color 120ms linear, transform 120ms ease;
}
.tile__icon { width: 2.15rem; height: 2.15rem; }
.tile__label {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  font-size: 9px;
  letter-spacing: 0.14em;
  color: inherit;
  opacity: 0;
  pointer-events: none;
  white-space: nowrap;
  transition: opacity 120ms linear;
}
.tile:hover, .tile:focus-visible { color: var(--pink); transform: translateY(-1px); }
.tile:hover .tile__label, .tile:focus-visible .tile__label { opacity: 1; }
.tile[aria-current='page'] { color: var(--pink); }
@media (max-width: 560px) {
  .tile { width: 3.6rem; }
  .tile__icon { width: 1.95rem; height: 1.95rem; }
}

/* ---- The burger ----------------------------------------------------
 Hidden everywhere but a phone. Three lines that become the close control,
 because the thing that opened the sheet is where a thumb already is. */
.burger {
  display: none;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  background: none;
  color: rgba(247, 243, 232, 0.72);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 120ms linear;
}
.burger:hover, .burger:focus-visible { color: var(--pink); }
.burger__box { position: relative; display: block; width: 20px; height: 14px; }
.burger__box span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1.5px;
  background: currentColor;
  /* Rotating about the middle line is what makes three lines read as becoming
     an x rather than as two separate things happening. */
  transition: transform 200ms ease, top 200ms ease, opacity 120ms linear;
}
.burger__box span:nth-child(1) { top: 0; }
.burger__box span:nth-child(2) { top: 6.25px; }
.burger__box span:nth-child(3) { top: 12.5px; }
.burger[aria-expanded='true'] { color: var(--pink); }
.burger[aria-expanded='true'] .burger__box span:nth-child(1) { top: 6.25px; transform: rotate(45deg); }
.burger[aria-expanded='true'] .burger__box span:nth-child(2) { opacity: 0; }
.burger[aria-expanded='true'] .burger__box span:nth-child(3) { top: 6.25px; transform: rotate(-45deg); }

/* ---- The mark ------------------------------------------------------
 These must be GLOBAL. `.walker` is the class on the <img> inside the Duck
 component, so it carries Duck's scope id, not the page's — scoped rules
 written beside the markup silently never match it.

 The drawing stays clean. Painting damage onto him made him look cheap;
 he sits UNDER the projection instead, so the grain and the lamp fall
 across him the same way they fall across everything else. */
.walker {
animation: brace 3.4s ease-in-out infinite;
transform-origin: 50% 94%;
}

/* Braced against the current, not strolling. */
@keyframes brace {
0%   { transform: rotate(-1.6deg) translateY(0)    scaleY(1); }
28%  { transform: rotate(1.1deg)  translateY(-4px) scaleY(0.99); }
62%  { transform: rotate(-0.7deg) translateY(2px)  scaleY(1.01); }
100% { transform: rotate(-1.6deg) translateY(0)    scaleY(1); }
}

.wave__duck.is-held .walker { animation: hold 900ms ease-out forwards; }
@keyframes hold { to { transform: rotate(0deg) translateY(0) scale(1.03); } }

.wave__duck.is-hit .walker { animation: hit 260ms ease-out; }
@keyframes hit {
0%   { transform: translateX(0)    rotate(0deg)    scale(1); }
30%  { transform: translateX(-5px) rotate(-2.6deg) scale(1.015); }
65%  { transform: translateX(3px)  rotate(1.8deg)  scale(0.995); }
100% { transform: translateX(0)    rotate(0deg)    scale(1); }
}

/* Held: everything but the mark goes down to black. Slow on the way out,
 because the calming is the point; instant on the way back. */
body.is-hushed .field { opacity: 0; transition: opacity 1100ms cubic-bezier(.4,0,.2,1); }
.field { transition: opacity 260ms ease; }
/* Dim the furniture, never an ancestor of the mark — opacity multiplies down,
 so fading `main` would fade the duck with it. */
body.is-hushed .bar,
body.is-hushed .radio,
body.is-hushed .record { opacity: 0.1; transition: opacity 900ms ease; }
.bar, .radio, .record { transition: opacity 420ms ease; }

/* Watching something: the field recedes rather than competing with it. */
body.is-watching .chaos { opacity: 0.1; transition: opacity 420ms ease; }
.chaos { transition: opacity 700ms ease; }

/* …and so does the projection itself. Real footage should not have to fight
   fake film damage on top of it — the grain, tape, scratches and vignette
   settle right down so the picture reads, but never all the way off: a
   whisper stays so the video sits inside the site instead of floating on it
   like a bare embed. Eases back the moment playback stops. */
body.is-watching::after { opacity: 0.14; }
body.is-watching .overlay { opacity: 0.2; }
body::after, .overlay { transition: opacity 600ms ease; }

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

/* TEMPORARY, 27 Aug 2026. Companion to the `?fx=` switches in chaos.js,
   for ruling layers in or out by feel on a real phone. Delete both together. */
body.fx-noov .overlay,
body.fx-noov::after { display: none; }

/* ═══ Component — Overlay ═══════════════════════════════════════ */
.overlay {
  position: fixed;
  /* Pinned height rather than inset:0, for the reason given on body::after. */
  top: 0;
  left: 0;
  right: 0;
  bottom: auto;
  height: 100vh;
  height: 100lvh;
  /* Above the interface, below the page-wide grain. */
  z-index: 90;
  pointer-events: none;
}
.overlay > * { pointer-events: none; }

/* --- Texture stack: dirt, not polish ------------------------------ */

/* Corner falloff. It used to ride inside .tex as a second background layer,
   which meant one opacity governed the dirt and the vignette together and
   the dirt could not be turned down without taking the corners with it.
   Split out 27 Aug 2026 so each is tuned on its own. It sits before .tex at
   the same z-index deliberately: the dirt blends over the falloff, which is
   the order it composited in when the two shared a layer. */
.overlay::before {
  content: '';
  position: absolute; inset: 0; z-index: 8;
  pointer-events: none;
  background-image:
    radial-gradient(ellipse at 50% 50%, transparent 58%, rgba(0,0,0,0.62) 100%);
  mix-blend-mode: overlay;
  opacity: 0.5;
}

.tex {
  position: absolute; inset: 0; z-index: 8;
  pointer-events: none;
  /* Directional wear, not even mottling. Two noise fields: the first is
     anisotropic, 35x lower frequency across the frame than down, so it
     streaks the way tape does; the second is a very coarse turbulence used
     as a mask, so the streaking only happens in some parts of the frame.
     A single fractalNoise arrives at the same density everywhere, and that
     uniformity is what made the old one read as a filter laid on top.
     Replaced the 0.14 / 5-octave field on 27 Aug 2026.

     The streaks jump (dirt, below); the mask does not. Same split as the
     grain above, made 2 Sep 2026 for the same reason: baked into one tile
     the mask jumped with the streaks and carried about 19 times the coarse
     structure of the tile it replaced, so every 800ms step read as the
     frame moving. On its own the streak tile has a third of that, and the
     part that is left is the streaks themselves, which are meant to
     flicker. */
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'600'%20height%3D'600'%3E%3Cfilter%20id%3D'd'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.012%200.42'%20numOctaves%3D'4'%20seed%3D'9'%20stitchTiles%3D'stitch'%2F%3E%3CfeColorMatrix%20type%3D'saturate'%20values%3D'0'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'600'%20height%3D'600'%20filter%3D'url%28%23d%29'%20opacity%3D'0.9'%2F%3E%3C%2Fsvg%3E");
  background-size: 600px 600px;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'900'%20height%3D'900'%3E%3Cfilter%20id%3D'm'%20x%3D'0'%20y%3D'0'%20width%3D'100%25'%20height%3D'100%25'%3E%3CfeTurbulence%20type%3D'turbulence'%20baseFrequency%3D'0.005'%20numOctaves%3D'3'%20seed%3D'21'%20stitchTiles%3D'stitch'%20result%3D'mask'%2F%3E%3CfeColorMatrix%20in%3D'mask'%20type%3D'luminanceToAlpha'%20result%3D'ml'%2F%3E%3CfeComponentTransfer%20in%3D'ml'%20result%3D'm'%3E%3CfeFuncA%20type%3D'gamma'%20amplitude%3D'2.2'%20exponent%3D'2.4'%20offset%3D'-0.04'%2F%3E%3C%2FfeComponentTransfer%3E%3CfeFlood%20flood-color%3D'white'%20result%3D'w'%2F%3E%3CfeComposite%20in%3D'w'%20in2%3D'm'%20operator%3D'in'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'900'%20height%3D'900'%20filter%3D'url%28%23m%29'%2F%3E%3C%2Fsvg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'900'%20height%3D'900'%3E%3Cfilter%20id%3D'm'%20x%3D'0'%20y%3D'0'%20width%3D'100%25'%20height%3D'100%25'%3E%3CfeTurbulence%20type%3D'turbulence'%20baseFrequency%3D'0.005'%20numOctaves%3D'3'%20seed%3D'21'%20stitchTiles%3D'stitch'%20result%3D'mask'%2F%3E%3CfeColorMatrix%20in%3D'mask'%20type%3D'luminanceToAlpha'%20result%3D'ml'%2F%3E%3CfeComponentTransfer%20in%3D'ml'%20result%3D'm'%3E%3CfeFuncA%20type%3D'gamma'%20amplitude%3D'2.2'%20exponent%3D'2.4'%20offset%3D'-0.04'%2F%3E%3C%2FfeComponentTransfer%3E%3CfeFlood%20flood-color%3D'white'%20result%3D'w'%2F%3E%3CfeComposite%20in%3D'w'%20in2%3D'm'%20operator%3D'in'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'900'%20height%3D'900'%20filter%3D'url%28%23m%29'%2F%3E%3C%2Fsvg%3E");
  -webkit-mask-size: 900px 900px;
          mask-size: 900px 900px;
  mix-blend-mode: overlay;
  opacity: 0.28;
  /* Slowed from 900ms: fast stepped texture jitter adds flicker load for
     no visual gain, since the grain reads as grain either way. */
  animation: dirt 2400ms steps(1) infinite;
}

/* A 900px tile holds ~12MB of texture at 2x, and a tile stops repeating once
   it is about as wide as the viewport, so a phone has no use for 900. This
   is deliberate rather than incidental: the field's recycle bug was most
   likely iOS discarding animations under memory pressure, and it would be
   careless to fix that and then hand the same phone three times the texture
   it needs. Same pattern, same scale, smaller window of it. The mask costs a
   tile of its own now, so both are held at 520. */
@media (max-width: 700px) {
  .tex {
    background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'520'%20height%3D'520'%3E%3Cfilter%20id%3D'd'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.012%200.42'%20numOctaves%3D'4'%20seed%3D'9'%20stitchTiles%3D'stitch'%2F%3E%3CfeColorMatrix%20type%3D'saturate'%20values%3D'0'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'520'%20height%3D'520'%20filter%3D'url%28%23d%29'%20opacity%3D'0.9'%2F%3E%3C%2Fsvg%3E");
    background-size: 520px 520px;
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'520'%20height%3D'520'%3E%3Cfilter%20id%3D'm'%20x%3D'0'%20y%3D'0'%20width%3D'100%25'%20height%3D'100%25'%3E%3CfeTurbulence%20type%3D'turbulence'%20baseFrequency%3D'0.005'%20numOctaves%3D'3'%20seed%3D'21'%20stitchTiles%3D'stitch'%20result%3D'mask'%2F%3E%3CfeColorMatrix%20in%3D'mask'%20type%3D'luminanceToAlpha'%20result%3D'ml'%2F%3E%3CfeComponentTransfer%20in%3D'ml'%20result%3D'm'%3E%3CfeFuncA%20type%3D'gamma'%20amplitude%3D'2.2'%20exponent%3D'2.4'%20offset%3D'-0.04'%2F%3E%3C%2FfeComponentTransfer%3E%3CfeFlood%20flood-color%3D'white'%20result%3D'w'%2F%3E%3CfeComposite%20in%3D'w'%20in2%3D'm'%20operator%3D'in'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'520'%20height%3D'520'%20filter%3D'url%28%23m%29'%2F%3E%3C%2Fsvg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'520'%20height%3D'520'%3E%3Cfilter%20id%3D'm'%20x%3D'0'%20y%3D'0'%20width%3D'100%25'%20height%3D'100%25'%3E%3CfeTurbulence%20type%3D'turbulence'%20baseFrequency%3D'0.005'%20numOctaves%3D'3'%20seed%3D'21'%20stitchTiles%3D'stitch'%20result%3D'mask'%2F%3E%3CfeColorMatrix%20in%3D'mask'%20type%3D'luminanceToAlpha'%20result%3D'ml'%2F%3E%3CfeComponentTransfer%20in%3D'ml'%20result%3D'm'%3E%3CfeFuncA%20type%3D'gamma'%20amplitude%3D'2.2'%20exponent%3D'2.4'%20offset%3D'-0.04'%2F%3E%3C%2FfeComponentTransfer%3E%3CfeFlood%20flood-color%3D'white'%20result%3D'w'%2F%3E%3CfeComposite%20in%3D'w'%20in2%3D'm'%20operator%3D'in'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'520'%20height%3D'520'%20filter%3D'url%28%23m%29'%2F%3E%3C%2Fsvg%3E");
    -webkit-mask-size: 520px 520px;
            mask-size: 520px 520px;
  }
}

@keyframes dirt {
  0%   { background-position: 0 0; }
  33%  { background-position: -60px 40px; }
  66%  { background-position: 80px -30px; }
  100% { background-position: -20px -70px; }
}

/* Scratches — sparse vertical damage, like a print that has been run too often. */
.overlay::after {
  content: '';
  position: absolute; inset: 0; z-index: 8;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    to right,
    transparent 0 137px,
    rgba(247,243,232,0.10) 137px 138px,
    transparent 138px 291px,
    rgba(0,0,0,0.5) 291px 292.5px
  );
  opacity: 0.16;
  animation: scratch 4.6s steps(1) infinite;
}
@keyframes scratch {
  0%   { transform: translateX(0);    opacity: 0.5; }
  25%  { transform: translateX(-63px); opacity: 0.22; }
  50%  { transform: translateX(41px);  opacity: 0.6; }
  75%  { transform: translateX(-18px); opacity: 0.3; }
  100% { transform: translateX(77px);  opacity: 0.45; }
}

/* The VHS tape layer was removed on 27 Aug 2026. It carried a tracking band
   drifting up the frame, head-switch noise along the bottom edge and a 3px
   line structure over everything. The line structure was the problem: a fixed
   4px grid across the whole page is the most uniform thing the stack had, and
   it sat in screen blend where it lifted the blacks. Camcorder artifacts now
   come from the dirt layer's directional wear instead, which varies. */

.static {
  position: absolute; inset: 0; z-index: 9;
  pointer-events: none; opacity: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='s'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23s)'/%3E%3C/svg%3E");
  mix-blend-mode: screen;
  /* Ramped, not switched — an instant full-screen luminance jump is the
     part that carries seizure risk. */
  transition: opacity 140ms ease-in-out;
}
.static.is-on { opacity: 0.2; }

/* A phone is held close and fills the visual field, so a burst that reads as
   interference on a monitor across a desk reads as the whole room flashing.
   Measured: at 0.2 in screen blend the burst takes the viewport from about
   0.06 to 0.15 relative luminance, a 2.5x jump, and at p=0.4 every 1700ms it
   arrives every four seconds. Here it is a little over half the amplitude,
   ramped nearly twice as slowly, and chaos.js fires it roughly a third as
   often. Every one of those three moves is downward. Nothing in this block
   raises rate or contrast, which is the direction the seizure note above
   cares about. Desktop keeps the values it has. */
@media (max-width: 700px) {
  .static { transition-duration: 260ms; }
  .static.is-on { opacity: 0.11; }
}

/* The stepped layers all jump rather than tween, which is what makes them
   read as grain rather than as motion. Stacked three deep and uncorrelated,
   at 450ms, 800ms and 1150ms between jumps, the beat between them is the
   "jumpy" the owner reported. Halving each rate on a phone keeps the stepped
   character and halves how often the frame changes under a reader. */
@media (max-width: 700px) {
  body::after { animation-duration: 3600ms; }
  .tex { animation-duration: 4800ms; }
  /* Also off the swing: the keyframes below set opacity as well as transform,
     which overrides the declared 0.16 and drives the layer between 0.22 and
     0.6 on a stepped timer. On a phone that is a visible contrast pulse a bit
     under once a second. This set leaves opacity alone, so the declared value
     is the value, and the throw comes down with it. */
  .overlay::after { animation: scratch-m 9.2s steps(1) infinite; }
}
@keyframes scratch-m {
  0%   { transform: translateX(0); }
  25%  { transform: translateX(-26px); }
  50%  { transform: translateX(17px); }
  75%  { transform: translateX(-8px); }
  100% { transform: translateX(31px); }
}

/* Projector lamp: warm hotspot, falloff to the corners, and a slow
   exposure breath. ~0.13Hz and a few percent, nowhere near a flicker risk.
   It is also the only layer that varies slowly across the whole frame, which
   is why the stack reads as lit rather than as filtered. */
.lamp {
  position: absolute;
  inset: 0;
  z-index: 7;
  pointer-events: none;
  background:
    radial-gradient(ellipse 78% 62% at 50% 46%,
      rgba(255, 240, 214, 0.07) 0%,
      rgba(255, 236, 205, 0.03) 38%,
      rgba(0, 0, 0, 0) 70%),
    radial-gradient(ellipse 96% 88% at 50% 50%,
      rgba(0, 0, 0, 0) 48%,
      rgba(0, 0, 0, 0.72) 100%);
  mix-blend-mode: soft-light;
  animation: breathe 7.4s ease-in-out infinite;
}
@keyframes breathe {
  0%   { opacity: 0.86; }
  50%  { opacity: 1; }
  100% { opacity: 0.86; }
}

/* ═══ Component — Chaos ═════════════════════════════════════════ */
.frag {
  position: absolute;
  white-space: nowrap;
  pointer-events: none;
  color: var(--cream-55);
  will-change: transform, opacity;
  text-shadow: 0 0 18px rgba(15,14,13,0.9);
}
.frag--text  { font-family: var(--mono); letter-spacing: 0.14em; text-transform: uppercase; }
.frag--text, .frag--lit {
  text-shadow:
    0 0 18px rgba(15, 14, 13, 0.9),
    1.1px 0 0 rgba(255, 40, 70, 0.3),
    -1.1px 0 0 rgba(40, 190, 255, 0.27),
    0 0 6px rgba(140, 210, 255, 0.09);
}
.frag--shout {
  font-family: var(--display);
  font-variation-settings: 'wdth' 74, 'wght' 800;
  line-height: 0.82;
  color: transparent;
  -webkit-text-stroke: 1px var(--cream-28);
  opacity: 0.9;
}
.frag--shout.is-filled { color: var(--cream-12); -webkit-text-stroke-width: 0; }
/* Prose keeps the serif and its own case — language, not feed noise. */
.frag--lit {
  font-family: var(--serif);
  line-height: 1.24;
  letter-spacing: 0;
}
/* Gate debris. Sharp, bright, brief. */
.frag--speck { background: var(--cream); opacity: 0.85; }
.frag--hair  { background: var(--cream); opacity: 0.55; }

.frag--block { background: currentColor; opacity: 0.5; }
.frag--tear  { background: var(--cream); mix-blend-mode: difference; opacity: 0.5; }
.frag--img   { object-fit: cover; }
/* Chroma bleeding sideways off the picture, the way tape does. */
.frag--img { box-shadow: 1.5px 0 0 rgba(255,40,70,0.16), -1.5px 0 0 rgba(40,190,255,0.14); }

.is-pink  { color: var(--pink); }
.is-pink.frag--shout { -webkit-text-stroke-color: var(--pink); }

.is-bright { color: var(--cream); }
.is-bright.frag--shout { -webkit-text-stroke-color: var(--cream-55); }


/* Fixed behind the whole site. The chaos is not a panel on the page —
   it is the ground the page sits in. */
.chaos {
  /* Under the plate the field steps back rather than being painted over. A soft
     gradient of the background colour laid over noise is a smudge, which is what
     the old bar was; a mask is the field itself going quiet, the same idea the
     hold-to-hush gesture already uses. */
  position: fixed;
  /* Pinned height rather than inset:0, for the reason given on body::after.
     This one matters twice over: resizing the field changes the box that
     spawn() positions fragments against, so an address bar appearing was
     enough to make the whole field reflow. */
  top: 0;
  left: 0;
  right: 0;
  bottom: auto;
  height: 100vh;
  height: 100lvh;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  background: var(--ink);
}

.field {
  position: absolute;
  inset: 0;
  animation: weave 5.9s steps(1) infinite;
}
@keyframes weave {
  0%   { transform: translate(0, 0); }
  16%  { transform: translate(-1px, 1px); }
  33%  { transform: translate(1px, 0); }
  52%  { transform: translate(0, -1px); }
  71%  { transform: translate(-1px, 0); }
  88%  { transform: translate(1px, 1px); }
  100% { transform: translate(0, 0); }
}

/* ═══ Component — Player ════════════════════════════════════════ */
.fm {
  position: relative;
  width: 100%;
  color: var(--cream-55);
}

/* The wave spans the full width and is the scrub target. */
.fm__wave { display: block; width: 100%; height: 3.6rem; overflow: visible; }
.fm__path {
  opacity: 0.75;
  /* Uneven gaps — a line that has been played over too many times. */
  stroke-dasharray: 34 3 9 2 61 4 17 3;
}
/* The pink fill that shows how far through a tape you are. It was opacity:0 with
   nothing anywhere raising it, so this never rendered once — the script has been
   animating its dash offset every frame into an invisible element. It is drawn
   from a full dash offset, so at zero progress there is nothing to see anyway. */
.fm__played { color: var(--pink); }
/* Hand-inked, not a UI dot: slightly off-round and never quite still. */
.fm__head {
  position: absolute;
  border-radius: 52% 47% 50% 53%;
  animation: fmJitter 1.7s steps(2) infinite;
  left: 0; top: 0;
  width: 11px; height: 11px;
  margin: -5.5px 0 0 -5.5px;
  background: var(--pink);
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 200ms ease;
}
.fm.is-ready .fm__head { opacity: 1; }
@keyframes fmJitter {
  0%   { margin: -5.5px 0 0 -5.5px; }
  50%  { margin: -6.5px 0 0 -5px; }
  100% { margin: -5px 0 0 -6px; }
}

/* A real range input, made invisible and laid over the wave — keyboard
   seeking and screen-reader support for free, without a custom widget. */

/* The wave is no longer a scrub target, so the invisible range input that used to
   lie over it is gone. It is a clock now: its viewBox is 1440 units wide and there
   are 1440 minutes in a day, so the playhead's x is the minute you are looking at,
   one unit to one minute, with no scaling to fudge. The drawing, the dashes, the
   breathing turbulence seed and the pink head are all exactly as they were. */


/* ---- The corner control -------------------------------------------------
   A play mark until a tape is loaded and the cassette itself, turning, once one
   is. It is the only thing in the masthead that says there is a radio at all. */
/* The control is a tile, so it inherits the row's size, spacing, hover and label
   from .tile and adds only what is its own: which mark shows, and the reels turning. */
.tile--fm { background: none; border: 0; padding: 0; font: inherit; cursor: pointer; }
.fmb__mark[hidden] { display: none; }
/* The reel bars are separate marks sitting in windows punched through the shell, which
   is what lets them turn at all — a hole that shares its path with the body cannot be
   rotated on its own. Their origins are written inline, in the icon's own coordinates. */
.ico-reel { transform-box: view-box; will-change: transform; }

/* ---- The shadowbox ------------------------------------------------------ */
.fmbox { position: fixed; inset: 0; z-index: 200; display: grid; place-items: center; }
.fmbox[hidden] { display: none; }
.fmbox__scrim {
  position: absolute; inset: 0; background: rgba(8, 7, 6, 0.93);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  animation: fmboxIn 200ms ease;
}
.fmbox__inner {
  position: relative; display: flex; flex-direction: column;
  /* Sized to the panel, not to the window. At 100% the box was as wide as the screen
     while the deck and the rack stayed packed against the left, so the close control,
     which belongs on the corner of the panel, was flung out to the far edge of the
     display and read as unrelated to anything. max-content is the two columns and the
     gap between them, which is the panel. Phones override this back to 100% below:
     there the box is the screen either way, and sizing to content would let a short
     track title pull the whole dialog in narrower than it should be. */
  width: max-content; max-width: 100%;
  /* svh, not vh. On iOS vh is the height of the screen with the browser chrome
     hidden, which is not the height you can actually see while the address bar and
     the toolbar are up. At 92vh the dialog came out 776px tall inside 645px of
     visible screen, and .fmbox centres it, so 65px went off the top and 65px off
     the bottom. The close button lives at top: 0 of this box, so it sat at -65 and
     could not be reached or hit-tested, and the box covered the scrim edge to edge
     so there was nothing left to tap either. Reported from an iPhone on 8 September
     2026. svh is the small viewport, chrome included, so the box fits what is on
     screen whatever the chrome is doing. vh first for anything that predates svh. */
  max-height: 92vh;
  max-height: 92svh;
  padding: clamp(0.75rem, 2vw, 1.5rem);
  animation: fmboxUp 260ms cubic-bezier(.2, .9, .3, 1);
}
/* The row that does not scroll, which is the whole point of it: the way out stays on
   screen no matter how far down the rack you are. */
.fmbox__head { flex: none; display: flex; justify-content: flex-end; }
/* And the part that does. min-height: 0 because a flex item will not shrink below its
   own content without it, so the box would grow past the cap instead of scrolling and
   the row above would be pushed off the top again, which is the bug this replaced. */
.fmbox__body {
  flex: 1 1 auto; min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 430px) minmax(0, 250px);
  gap: clamp(18px, 3vw, 40px); align-items: start;
  overflow-y: auto; overflow-x: hidden;
}
@keyframes fmboxIn { from { opacity: 0; } }
@keyframes fmboxUp { from { opacity: 0; transform: translateY(12px); } }
.fmbox__x {
  background: none; border: 0; cursor: pointer; padding: 0.4rem 0.6rem;
  font-family: var(--struck); font-size: 20px; line-height: 1; color: var(--cream-36);
}
.fmbox__x:hover, .fmbox__x:focus-visible { color: var(--pink); }
@media (max-width: 780px) {
  .fmbox__body { grid-template-columns: minmax(0, 430px); gap: 24px; }
  .fmbox__inner { width: 100%; }
}

/* ---- The machine --------------------------------------------------------
   The drawing is the layout: the chassis sits in flow and sets the height, and the
   well and the keys are positioned as a percentage of that same box, so there is one
   geometry and nothing can drift out of register with anything else. */
.deck { position: relative; width: 100%; max-width: 430px; }
.chassis { display: block; width: 100%; height: auto; overflow: visible; }
.ch-shell, .ch-well, .ch-ledge, .ch-screw, .ch-spindle, .ch-guide {
  fill: none; stroke-linecap: round;
}
.ch-shell   { stroke: var(--cream-55); stroke-width: 2.6; }
.ch-well    { stroke: var(--cream-36); stroke-width: 1.8; }
.ch-guide   { stroke: var(--cream-36); stroke-width: 1.6; }
.ch-ledge, .ch-screw, .ch-spindle { stroke: var(--cream-28); stroke-width: 1.5; }
.ch-brand, .ch-side {
  font-family: var(--struck); font-size: 10px; letter-spacing: 0.2em;
  text-transform: uppercase; fill: var(--cream-36); stroke: none;
}
/* The FM has not gone anywhere, it is just not spelled out twice: the F and the M
   of FLAMINGO carry it. One step up the weight and one up the value ladder, because
   at 10px through a displacement filter a weight change alone barely registers. */
.ch-em { font-weight: 700; fill: var(--cream-55); }

.well {
  color: var(--cream-78); position: absolute;
  left: 6.9767%; top: 5.7878%;
  width: 86.0465%; height: 60.5785%;
  display: grid; place-items: center;
}
.empty { font-family: var(--struck); font-size: 10px; letter-spacing: 0.2em;
          text-transform: uppercase; color: var(--cream-28); }
.tape { display: block; margin: 0 auto; width: 89.1892%; height: auto; overflow: visible; }
.tape path { vector-effect: non-scaling-stroke; }
.tp-shell  { fill: none; stroke: currentColor; stroke-width: 3.2; }
.tp-label  { fill: none; stroke: var(--pink); stroke-width: 1.5; opacity: 0.75; }
.tp-screw  { fill: none; stroke: var(--cream-36); stroke-width: 1.4; }
.tp-window { fill: none; stroke: var(--cream-55); stroke-width: 2; }
.tp-hub    { fill: var(--ink); stroke: currentColor; stroke-width: 2.2; }
.tp-pack   { fill: var(--pink-22); stroke: var(--pink); stroke-width: 2.2; }
.tp-span   { fill: none; stroke: var(--pink); stroke-width: 2.4; }
/* The title is struck straight onto the label the cassette already has, left aligned
   and bold, rather than typed on a paper patch stuck over it. It is the one thing on
   the machine you are meant to read, so it takes the cream undiluted. */
.tp-ti { font-family: var(--mono); font-weight: 500; font-size: 14px;
         letter-spacing: 0.02em; text-transform: uppercase; fill: var(--pink); }
/* The spokes are the only part that turns, so they sit outside the displacement
   filter: feTurbulence lays its noise in a fixed coordinate space and rotating
   geometry walks through that field and boils. Their hand-drawn quality is baked into
   the path data instead, and the pivot is the hub in the cassette's own coordinates —
   on the spokes' own bounding box it sat 2.4 units high, on a hub of radius 10. */
.tp-spokes { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round;
              transform-box: view-box; will-change: transform; }
.tp-spokes--l { transform-origin: 92px 104px; }
.tp-spokes--r { transform-origin: 208px 104px; }

.keys {
  position: absolute; left: 6.9767%; top: 72.1540%;
  width: 86.0465%; display: flex; gap: 2.1622%;
}
.keys .k {
  position: relative; flex: 1; background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--cream-78); display: grid; justify-items: center;
  transition: transform 70ms ease, color 90ms linear;
}
.kk { display: block; width: 100%; height: auto; overflow: visible; }
/* .7 of the key's own colour, which lands the cap on the same rung as the case. */
.kk-cap { fill: var(--cream-05); stroke: currentColor; stroke-width: 2.2; opacity: 0.7; }
.kk-m   { fill: currentColor; stroke: none; }
.keys .k .kk:nth-of-type(2) { display: none; }
.keys .k.is-on .kk:nth-of-type(1):not(:only-of-type) { display: none; }
.keys .k.is-on .kk:nth-of-type(2) { display: block; }
.keys .k:hover { color: var(--pink); }
.keys .k:active, .keys .k.is-down { transform: translateY(2px); }
.keys .k.is-on { color: var(--pink); }
.keys .k:focus-visible { outline: 2px solid var(--pink); outline-offset: 3px; }
.klab { font-family: var(--struck); font-size: 8.5px; letter-spacing: 0.2em;
         text-transform: uppercase; color: var(--cream-36); margin-top: 6px; }

/* The rack. Its heading is exactly one gutter tall so the first slot's top edge sits
   level with the top edge of the well across the gap. */
.shelf h2 { font-family: var(--struck); font-size: 10px; letter-spacing: 0.2em;
             text-transform: uppercase; font-weight: 400; color: var(--cream-36);
             margin: 0; height: 24px; line-height: 1; }
.shelf ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.sp {
  position: relative; width: 100%; height: 46px; background: none; border: 0;
  padding: 0 15px; cursor: pointer; color: var(--cream-78); font-family: var(--struck);
  display: flex; gap: 12px; align-items: center; text-align: left;
  transform: rotate(var(--t, 0deg));
  transition: transform 110ms, color 110ms, opacity 140ms;
}
.sl { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.sl-f { fill: var(--cream-05); stroke: var(--cream-36); stroke-width: 1.6; }
.sl-spine { fill: none; stroke: var(--pink); stroke-width: 2.6; stroke-linecap: round; }
.sp:hover { transform: rotate(0deg) translateX(3px); color: var(--pink); }
.sp[aria-current="true"] { opacity: 0.26; pointer-events: none; }
.sp__n { position: relative; font-size: 9.5px; letter-spacing: 0.14em; color: var(--cream-36); flex: none; }
.sp__t { position: relative; flex: 1; font-size: 11px; letter-spacing: 0.03em;
          white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The tape's own running time. With this here and the title typed on the cassette's
   label, a readout on the machine would only repeat what the object already says. */
.sp__d { position: relative; font-size: 9.5px; letter-spacing: 0.1em; flex: none;
          color: var(--cream-36); font-variant-numeric: tabular-nums; }

/* ═══ Component — Icon ══════════════════════════════════════════ */
.icon { width: 100%; height: 100%; display: block; overflow: visible; }

/* ═══ Component — VideoPlayer ═══════════════════════════════════ */
.vp {
  position: relative;
  aspect-ratio: 16 / 9;
  width: 100%;
  background: #000;
  box-shadow: 0 0 0 1px var(--cream-28);
  overflow: hidden;
}
/* `:global()` is Astro scoping syntax, not CSS. In a plain stylesheet the
   browser cannot parse the selector and drops the whole rule, which left the
   Vimeo/YouTube iframe at its 300x150 default inside the 16:9 frame. The
   iframe is created at runtime, so it never carried a scope id anyway. */
.vp .vp__el {
  width: 100%;
  height: 100%;
  display: block;
  border: 0;
}

.vp__facade {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 0;
  background: #000;
  border: 0;
  cursor: pointer;
}
.vp__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.55;
  filter: grayscale(0.4) contrast(1.1);
}
.vp__play {
  position: relative;
  display: grid;
  place-items: center;
  width: 4.2rem;
  height: 4.2rem;
  font-size: 1.3rem;
  color: var(--ink);
  background: var(--pink);
  transition: transform 120ms ease;
}
.vp__facade:hover .vp__play { transform: scale(1.08); }
.vp__hint {
  position: absolute;
  left: 0.75rem;
  bottom: 0.6rem;
  color: var(--cream-55);
}

/* ═══ Component — Hud ═══════════════════════════════════════════ */
.hud {
  position: fixed;
  right: 1.15rem;
  bottom: 1rem;
  z-index: 54;
  pointer-events: none;
}
.hud__tc {
  font-family: var(--struck);
  font-size: 10px;
  letter-spacing: 0.18em;
  color: var(--cream-55);
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 10px rgba(15, 14, 13, 0.9);
}
@media (max-width: 700px) { .hud { right: 0.75rem; bottom: 0.7rem; } }

/* ═══ Component — Contact ═══════════════════════════════════════ */
/* Wide enough to hold the address on hover without shoving the nav around:
   the label is absolutely positioned, so the tile itself stays icon-sized. */
.contact { width: 3rem; cursor: copy; }
.contact .contact__label { font-size: 9.5px; letter-spacing: .08em; text-transform: none; }
/* Pull the address back from the right edge so it cannot run off screen. */
.contact .tile__label { left: auto; right: 0; transform: none; }
/* The label is normally hover-only; while confirming, it stays put. */
.contact.is-copied { color: var(--pink); }
.contact.is-copied .contact__label { opacity: 1; text-transform: uppercase; letter-spacing: .14em; }

/* ═══ Component — EmptyState ════════════════════════════════════ */
/* Reached only by direct URL — these sections stay out of the nav until
   they hold something. Stated as a record, not apologised for. */
.empty {
  padding-block: clamp(3rem, 14vh, 7rem);
  border-top: 1px solid var(--cream-28);
  color: var(--cream-55);
}
.empty p { margin: 0.6rem 0 0; font-size: 13px; }

/* ═══ Layout — masthead, shell, radio ═══════════════════════════ */
/* Everything the reader touches floats above the field. */
  .shell { position: relative; z-index: 1; padding-bottom: 2.5rem; }

  /* The bar does not seal the top of the page — it fades into the churn,
     so the nav sits in the chaos rather than capping it. */
  /* The bar was a black gradient fading into the field — a scrim, and it read
     as a smudge because that is what a soft-edged wash of the background colour
     over noise looks like. It is a face now: a flat plate a shade off the ink
     with a hairline under it, so the controls are mounted on something instead
     of floating in a haze. Legibility comes from the plate; the field is handled
     separately, by stepping back rather than by being covered. */
  /* The bar does not seal the top of the page — it fades into the churn, so the
     nav sits in the chaos rather than capping it. The flat plate that replaced
     this read as a black box across the top and it is not coming back. */
  .bar {
    position: sticky; top: 0; z-index: 50;
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem; padding: 0.7rem var(--gut) 1.6rem;
    background: linear-gradient(to bottom, var(--ink) 0%, rgba(8,7,6,0.92) 45%, rgba(8,7,6,0) 100%);
  }
  .bar__left { display: flex; align-items: center; gap: 0.7rem; }
  .bar__id { display: flex; align-items: center; text-decoration: none; }

      /* Keep the player and the tiles on one line; only let them stack once
     the viewport genuinely cannot hold them. */
  .bar__right {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: nowrap;
    justify-content: flex-end;
  }
  @media (max-width: 620px) { .bar__right { flex-wrap: wrap; } }
  .bar__nav { display: flex; align-items: center; gap: 0.15rem; flex-wrap: nowrap; }

  /* Desktop-icon logic: a picture with its name under it. The whole tile is
     the target, and it inverts to pink the way every other control does. */


  /* Full-bleed: the wave is a line drawn across the page, so it should not
     sit inside the content measure. */
  /* Part of the same face as the bar above it, and it carries the hairline
     that closes the plate. */
  /* Full-bleed and unbacked: the wave is a line drawn across the page, not a
     panel, so it should not sit inside the content measure or on a ground. */
  .radio { padding: 0 var(--gut) 0.4rem; margin-top: -1.1rem; position: relative; z-index: 49; }

/* ═══ Page — home (record) ══════════════════════════════════════ */
.hold {
  display: grid;
  place-items: center;
  min-height: clamp(360px, 62vh, 640px);
}
.wave__duck {
  position: relative;
  isolation: isolate;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;   /* holding must not scroll the page */
  /* Without these, a press-and-hold on the mark fires iOS's "Save/Copy Image"
     callout instead of quieting the field — the whole point of the gesture.
     Suppress the callout, the drag and the selection across the button and
     the image it wraps. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
/* On a phone the mark is fixed and sits under the thumb, so it has to let a
   drag through or it becomes a region of the page that cannot be scrolled at
   all. hold.js takes the gesture on a stationary press instead of on contact,
   which is what the label has always said it does. */
@media (max-width: 700px) {
  .wave__duck { touch-action: pan-y; }
}

/* The image must not be the touch target or the drag source; the button owns
   the gesture. */
.wave__duck .walker {
  pointer-events: none;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}
/* Held: he braces harder and stands very still. */



.record { padding-block: clamp(2rem, 7vh, 4rem); }
.record__head {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: 0.6rem 0.75rem; margin-bottom: 4px;
}
.record__count { color: var(--pink); }

.record__sheet { display: grid; gap: 4px; }
.record__row {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr) 8rem 6rem 7rem;
  gap: 1rem; align-items: center;
  padding: 0.8rem 0.75rem;
  text-decoration: none; color: inherit; font-size: 13px;
}
.record__row--blank { min-height: 3rem; color: var(--cream-12); }
.record__row--live { transition: background 90ms linear, transform 90ms linear; }
.record__row--live:hover { background: var(--pink); color: var(--ink); transform: translateX(6px); }
.record__row--live:hover .pink { color: var(--ink); }
/* Struck, not set. The record is a document the machine printed, so the
   title uses the same face as the data and earns its weight through size.
   The serif is reserved for prose in the field — one voice per job. */
.record__title {
  font-family: var(--mono);
  /* Match the row's other cells; bold is what marks it as the title now,
     not size. At 17px it towered over the data and read as mismatched. */
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* One mono voice across the whole sheet: the data cells drop the typewriter
   Courier for the same DM Mono the column headers use, so the record reads as
   machine-set rather than typed. Scoped to .record — `.struck` on the Work
   index, Press, film facts and the player keeps Courier untouched. */
.record .struck { font-family: var(--mono); }

@media (max-width: 760px) {
  .record__row { grid-template-columns: 2.5rem minmax(0, 1fr) 5rem; }
  .record__row > :nth-child(4), .record__row > :nth-child(5) { display: none; }
}

/* ═══ Pages — shared index chrome ═══════════════════════════════ */
.page { padding-block: clamp(1.5rem, 6vh, 3.5rem); }
.page__head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.page__title { font-size: clamp(2rem, 7vw, 4.5rem); font-variation-settings: 'wdth' 74, 'wght' 800; }

/* ═══ Page — work ═══════════════════════════════════════════════ */
.filters { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 1.5rem 0 0; }
.sheet { margin-top: 1.5rem; border-top: 1px solid var(--cream-28); }
.page--work .row {
display: grid; grid-template-columns: 3.5rem minmax(0, 1fr) 9rem 6rem;
gap: 1rem; align-items: center; padding: 0.85rem 0.25rem;
border-bottom: 1px solid var(--cream-12);
text-decoration: none; color: inherit; font-size: 13px;
}
/* Same trap as the tape rack above: `display: grid` outranks the [hidden]
   attribute's UA display:none, so without this the kind filters set hidden
   and every row stays visible anyway. */
.page--work .row[hidden] { display: none; }
.page--work .row--head { border-bottom-color: var(--cream-28); padding-block: 0.5rem; }
.page--work .row--live { transition: background 90ms linear, padding-inline 90ms linear; }
.page--work .row--live:hover { background: var(--pink); color: var(--ink); padding-inline: 0.75rem; }
.page--work .row__title { font-family: var(--struck); font-size: 17px; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase; }
/* Narrow only. Flattening this out of its media query hid the Year column at
   every width and stretched Title across the whole measure. */
@media (max-width: 760px) {
  .page--work .row { grid-template-columns: 2.5rem minmax(0, 1fr) 5rem; }
  .page--work .row > :nth-child(4) { display: none; }
}

/* ═══ Page — press ══════════════════════════════════════════════ */
.sheet { margin-top: 1.5rem; border-top: 1px solid var(--cream-28); }
.page--press .row {
display: grid; grid-template-columns: 3.5rem minmax(0, 1fr) 12rem 7rem;
gap: 1rem; align-items: center; padding: 0.85rem 0.25rem;
border-bottom: 1px solid var(--cream-12);
text-decoration: none; color: inherit; font-size: 13px;
transition: background 90ms linear, padding-inline 90ms linear;
}
.page--press .row:hover { background: var(--pink); color: var(--ink); padding-inline: 0.75rem; }
.page--press .row__title { font-family: var(--struck); font-size: 16px; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase; }
.kit { margin-top: 3rem; border-top: 1px solid var(--cream-28); padding-top: 1.25rem; display: grid; gap: 0.6rem; justify-items: start; }
.kit p { margin: 0; max-width: 58ch; }
/* Narrow only. Same flattening bug as Work, and worse here: it hid both
   Outlet and Date at every width, leaving press as a numbered title list. */
@media (max-width: 760px) {
  .page--press .row { grid-template-columns: 2.5rem minmax(0, 1fr); }
  .page--press .row > :nth-child(3), .page--press .row > :nth-child(4) { display: none; }
}

/* ═══ Page — news ═══════════════════════════════════════════════ */
.posts { margin-top: 1.5rem; display: grid; gap: 2.5rem; }
.post { border-top: 1px solid var(--cream-28); padding-top: 1.1rem; }
.post__title { font-family: var(--serif); font-size: clamp(1.4rem, 3.5vw, 2.2rem); margin: 0.4rem 0 0.6rem; }
.post__body { max-width: 58ch; margin: 0; }

/* ═══ Page — store ══════════════════════════════════════════════ */
.shelf {
list-style: none; margin: 1.5rem 0 0; padding: 0;
display: grid; gap: clamp(1rem, 3vw, 2rem);
grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr));
}
.item { border: 1px solid var(--cream-28); }
.item__meta { padding: 0.85rem 1rem 1.1rem; display: grid; gap: 0.5rem; justify-items: start; border-top: 1px solid var(--cream-28); }
.item__title { font-family: var(--serif); font-size: 19px; }
.item__out { opacity: 0.4; cursor: not-allowed; }

/* ═══ Page — film detail ════════════════════════════════════════ */
.piece { padding-block: clamp(1.5rem, 6vh, 3rem); display: grid; gap: 4px; }
.piece__head { padding: 0.9rem 1rem 1.1rem; }
.piece__title {
  /* The condensed display face, same as the WORK / STORE / PRESS headers and
     the wordmark — so a film title reads as a title card in the site's own
     voice, not a stray serif. */
  font-family: var(--display);
  font-variation-settings: 'wdth' 74, 'wght' 800;
  text-transform: uppercase;
  font-size: clamp(2rem, 7vw, 4.2rem);
  line-height: 1.02;
  letter-spacing: 0.005em;
  margin: 0.35rem 0 0;
}
.piece__still { width: 100%; box-shadow: 0 0 0 1px var(--cream-28); }

.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
  gap: 1rem;
  padding: 0.85rem 1rem;
  margin: 0;
}
.facts__cell dd { margin: 0.25rem 0 0; font-size: 13px; }

.piece__body { padding: 1.1rem 1rem 1.3rem; }
.piece__logline {
  max-width: 58ch;
  margin: 0;
  font-size: 18px;
  line-height: 1.62;
}

.credits { margin: 0; padding: 1rem; display: grid; gap: 0.5rem; }
.credits__row {
  display: grid;
  grid-template-columns: minmax(7rem, 12rem) 1fr;
  gap: 1rem;
  align-items: baseline;
}
.credits dd { margin: 0; font-size: 13px; }

.piece__links { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 1rem 0 0; }
.piece__back { margin: 1.5rem 0 0; }

/* ═══ Page — 404 ════════════════════════════════════════════════ */
.lost { display: grid; justify-items: start; gap: 1rem; padding-block: clamp(3rem, 14vh, 8rem); }
.lost__code { font-size: clamp(3rem, 14vw, 8rem); font-variation-settings: 'wdth' 74, 'wght' 800; }
.lost p { margin: 0; font-size: 13px; color: var(--cream-55); }


/* ═══ Mobile layout ═════════════════════════════════════════════
   Kept at the end of the sheet on purpose. Everything in here is a
   media-query override of a rule declared earlier, and a media query adds no
   specificity: `@media (max-width: 700px) { .record__row { ... } }` loses to a
   plain `.record__row` further down the file, and it loses to the existing
   max-width:760px block for the same reason. Putting these last is what makes
   them apply. Nothing above this line is mobile-only; nothing below it touches
   a viewport wider than 700px. */
/* ── Mobile layout: the column moves, the mark does not ──────────────
   On a phone the record, the radio and later the shop read as one column down
   the left, and the flamingo and the field behind him stay where they are.

   The mark is anchored by top rather than bottom, for the same reason the
   other fixed layers pin their height: a bottom-anchored fixed element moves
   every time iOS collapses the address bar. Pushed a little past the right
   edge so he is a fixture in the frame rather than a picture centred in it,
   and given a z-index so the column passes behind him rather than over him.

   .hold keeps its box in the flow but stops reserving height. Not
   display:none: chaos.js measures the guard disc off this button's rect
   (measureMark), and a hidden element measures zero, which would quietly stop
   the field deflecting around him with nothing visibly broken. */
@media (max-width: 700px) {
  /* The phone home page is the mark, the field and the music. Nothing else.

     Alex, 27 August 2026: "the work is so low at this point it doesn't matter.
     the brand is a vibe." With one film published, a record of a single entry
     is weaker evidence than no record at all, so the work moves into the menu
     and the first screen becomes the thing the brand actually is. There is no
     scroll because there is nothing under the fold to scroll to.

     REVISIT WHEN THERE IS MORE WORK. Undoing this is two declarations: drop
     the `.record { display: none }` below and give `.hold` a height again. The
     column styling underneath is kept intact and dormant for exactly that.
     Condition and reasoning in Company/06-status.md, under Film side. */
  .record { display: none; }
  .hold { min-height: 0; }

  .wave__duck {
    position: fixed;
    /* Centred in the frame now that he is the frame. Optically a little below
       the middle, because the bar and the player hold the top 155px and a
       mark centred on the true midpoint reads as riding high against them. */
    left: 50%;
    right: auto;
    top: 56vh;
    top: 56lvh;
    transform: translate(-50%, -50%);
    /* Above the field, below .bar (50) and the menu sheet (80). */
    z-index: 2;
  }
  /* He carries the page alone, so he is sized to carry it. */
  .hold { --mark-w: 58vw; }

  /* DORMANT while `.record { display: none }` above holds. Kept whole so the
     column can come back in one line once there is work to put in it.

     The record stops being a table.

     Five columns miniaturised to a phone is the desktop language shrunk, and
     shrinking is not designing: it left a 13px title in a 175px cell and threw
     the year and the status away to make room. Every column here becomes a
     line instead, so nothing is dropped and the entry is set at a size worth
     reading and easy to move through with a thumb. Ordered with flex rather
     than markup, because the same rows have to keep working on desktop.

       01  FEATURE          <- eyebrow, the number and the kind
       NIGHT BUS            <- the title, set large
       2026  RELEASED       <- the rest */
  .record__sheet { gap: 0; }

  .record__row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 0.55rem;
    grid-template-columns: none;
    padding: 1.15rem 0.1rem 1.3rem;
    background: none;
    box-shadow: none;
    border-top: 1px solid var(--cream-12);
  }
  /* The 760px block above hides the year and the status. Here they are the
     third line, so they come back. */
  .record__row > :nth-child(3),
  .record__row > :nth-child(4),
  .record__row > :nth-child(5) { display: block; }

  .record__row > :nth-child(1) { order: 1; color: var(--cream-55); }
  .record__row > :nth-child(3) { order: 2; color: var(--cream-55); }
  .record__row > :nth-child(4) { order: 4; color: var(--cream-55); }
  .record__row > :nth-child(5) { order: 5; }
  .record__row > :nth-child(1),
  .record__row > :nth-child(3),
  .record__row > :nth-child(4),
  .record__row > :nth-child(5) {
    font-size: 10px;
    letter-spacing: 0.2em;
    text-transform: uppercase;
  }

  /* Full width forces the line break, so the title always owns its own line. */
  .record__title {
    order: 3;
    flex: 0 0 100%;
    font-family: var(--display);
    font-variation-settings: 'wdth' 74, 'wght' 800;
    font-size: clamp(1.75rem, 9.5vw, 2.9rem);
    line-height: 0.94;
    letter-spacing: -0.005em;
    margin: 0.34rem 0 0.4rem;
  }

  /* Hover is the desktop affordance for a row; a finger gets the press state
     instead, and the pink fill of a whole block is far too loud at this size. */
  .record__row--live:hover { background: none; transform: none; }
  .record__row--live:active { color: var(--pink); }

  /* The ruling that runs past what is filled is a printed-record conceit and
     it reads as a broken list once the rows are set this large. */
  .record__row--blank { display: none; }

  /* Column headings for columns that no longer exist. Each entry now labels
     itself, so the row is noise, and the flex order above scrambled it into
     "TITLE NO. FORMAT YEAR STATUS" besides. */
  .record__row--head { display: none; }

  .record__head {
    flex-wrap: wrap;
    background: none;
    box-shadow: none;
    padding: 0 0.1rem 0.2rem;
  }

  /* Fragment widths are scaled for the phone in spawn(), but an image's
     height is whatever its aspect ratio makes it, and a tall portrait at
     160px wide runs past half the screen before the per-fragment scale of up
     to 1.3 is applied. Capping the height is the only thing holding the
     portraits down. */
  .frag--img { max-height: 24vh; }

  /* ── The menu ──────────────────────────────────────────────────
     The nav stops being a row of icons and becomes a sheet under the bar.
     Same markup and the same elements, so contact.js still binds the one
     .contact it expects and the copy confirmation still lands on the tile
     it was written for. Only the presentation changes. */
  .burger { display: grid; }

  .bar__nav {
    position: absolute;
    top: 100%;
    left: var(--gut);
    right: var(--gut);
    /* Over the tape rack, which is 70 and would otherwise cover the sheet,
       and under .overlay at 90 so the projection still falls across it. */
    z-index: 80;
    display: grid;
    gap: 2px;
    padding: 0.4rem;
    background: rgba(8, 7, 6, 0.94);
    box-shadow: 0 0 0 1px var(--cream-12);
    visibility: hidden;
    opacity: 0;
    transform: translateY(-6px);
    transition: opacity 160ms ease, transform 160ms ease, visibility 0s 160ms;
  }
  .bar.is-menu-open .bar__nav {
    visibility: visible;
    opacity: 1;
    transform: none;
    transition: opacity 160ms ease, transform 160ms ease;
  }
  /* The bar outranks the rack only while it is holding an open sheet. */
  .bar.is-menu-open { z-index: 80; }

  /* Tiles become rows: icon, then the label that used to need a hover. */
  .bar__nav .tile {
    width: auto;
    grid-template-columns: 1.75rem 1fr;
    justify-items: start;
    align-items: center;
    gap: 0.7rem;
    padding: 0.7rem 0.6rem;
    min-height: 2.75rem;
  }
  .bar__nav .tile__label {
    position: static;
    transform: none;
    left: auto;
    right: auto;
    opacity: 1;
    pointer-events: auto;
    font-size: 12px;
    letter-spacing: 0.16em;
  }
  /* The address is longer than a nav word and is not a label to be shouted. */
  .bar__nav .contact .contact__label { font-size: 11.5px; letter-spacing: 0.02em; }

  /* The last stepped layer without a mobile gate. weave jogs the whole field
     by a pixel on seven keyframes over 5.9s, which is six jumps a cycle and
     61 a minute. It was chosen when fragments were full size; scaling them to
     42% above made that fixed 1px throw two and a half times larger relative
     to a glyph, so the same jog now reads as the field twitching. Halving the
     rate, amplitude untouched. */
  .field { animation-duration: 11.8s; }
}
