/* layers.css — the effect layers. Each is a class fx-<name> on <body>; its CSS is gated on that class,
   its knobs are CSS custom properties (defaults declared beside the layer). ?off=<name> removes a layer;
   the bench toggles them live. One block per layer, in commit order. */

/* layer: grid (?off=grid) — the engineering grid, 8px minor / 48px major */
.fx-grid .tube{
  background:
    linear-gradient(rgba(30,42,56,.055) 1px, transparent 1px) 0 0/8px 8px,
    linear-gradient(90deg, rgba(30,42,56,.055) 1px, transparent 1px) 0 0/8px 8px,
    linear-gradient(rgba(30,42,56,.05) 1px, transparent 1px) 0 0/48px 48px,
    linear-gradient(90deg, rgba(30,42,56,.05) 1px, transparent 1px) 0 0/48px 48px,
    var(--paper);
}

/* layer: dots — phosphor dot structure + defocus (?off=dots).
   Every glyph, rule and plotted line inside the tube renders through the mask. */
/* tuned 2026-09-03 from a six-row sweep (receipts/sweep-dots-2026-09-03.png): .58 reads as
   softening, .32 as honest dots; .38 keeps the 11px mono legible. Live knobs: ?dots= ?pitch= ?defocus= */
:root{ --dot-pitch:3px; --dot-floor:.38; --defocus:.4px; }
.fx-dots .tube{
  -webkit-mask-image:radial-gradient(circle, #000 0 38%, rgba(0,0,0,var(--dot-floor)) 62%, rgba(0,0,0,var(--dot-floor)) 100%);
          mask-image:radial-gradient(circle, #000 0 38%, rgba(0,0,0,var(--dot-floor)) 62%, rgba(0,0,0,var(--dot-floor)) 100%);
  -webkit-mask-size:var(--dot-pitch) var(--dot-pitch);
          mask-size:var(--dot-pitch) var(--dot-pitch);
  filter:blur(var(--defocus));
}

/* layer: scan — scanlines (?off=scan). The raster, above the tube; no bezel. */
:root{ --scan:rgba(30,42,56,.14); --scan-pitch:3px; --scan-line:1px; }   /* .11 → .14 in the same sweep; live knob: ?scan= */
/* pitch and line are snapped to whole DEVICE pixels by the switch script (3 CSS px is 3.75 device px at
   125% Windows scaling; a fractional period drifts inside Chrome's raster tiles and the seams walk across
   the screen as a staircase — Bill's screenshot). At DPR 1 the snap is a no-op. */
.crt-scan{ position:fixed; inset:0; pointer-events:none; z-index:5; display:none;
  background:repeating-linear-gradient(0deg, var(--scan) 0 var(--scan-line), transparent var(--scan-line) var(--scan-pitch)); }
.fx-scan .crt-scan{ display:block; }

/* layer: haze — tube falloff + grain + ink bloom (?off=haze). Falloff is the tube's
   light dying toward the edges, not a frame; the grain is inline SVG turbulence. */
:root{ --halo:rgba(30,42,56,.42); }
.fx-haze{ text-shadow:0 0 1.6px var(--halo); }
.crt-haze{ position:fixed; inset:0; pointer-events:none; z-index:6; display:none;
  background:
    radial-gradient(ellipse at 50% 42%, transparent 52%, rgba(30,42,56,.16) 100%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .12 0 0 0 0 .16 0 0 0 0 .22 0 0 0 .07 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
.fx-haze .crt-haze{ display:block; }

/* layer: hum — the mains hum bar (?off=hum). Transform-only; below the scanlines.
   Open: it loops forever, so WCAG 2.2.2 wants a pause that isn't a button. */
:root{ --hum:.3; }   /* band brightness; live knob: ?hum= — below ~.25 the white band is invisible on this ground (Bill, 2026-09-03) */
.crt-hum{ position:fixed; left:0; right:0; bottom:100%; height:18vh; pointer-events:none; z-index:4; display:none;
  background:linear-gradient(180deg, transparent, rgba(255,255,255,var(--hum)) 50%, transparent);
  animation:hum 11s linear infinite; }
.fx-hum .crt-hum{ display:block; }
@keyframes hum{ to{ transform:translateY(calc(100vh + 18vh)); } }
/* honoured by design; fx-motion (?on=motion, review only) forces the preview through the preference */
@media (prefers-reduced-motion:reduce){ .fx-hum:not(.fx-motion) .crt-hum{ display:none; } }

/* layer: led — the dot-matrix readouts (?off=led). Doto on the two large figures only;
   with the layer off they render in the page's own faces at the study's sizes. */
.fx-led .led{ font-family:"Doto","IBM Plex Mono",ui-monospace,monospace; font-weight:700;
  font-variation-settings:"ROND" 100; letter-spacing:.04em; text-transform:none; }
.fx-led .mast .stamp .led{ font-size:24px; line-height:1.1; color:var(--ink); margin-top:2px; }
.fx-led .fc .temp{ font-size:30px; line-height:1; }
.fx-led .fc .temp small{ font-family:"IBM Plex Mono",monospace; font-variation-settings:normal; }

/* layer: type — the beam writes the line; each character lands hot and cools (?off=type).
   After tkmoney's MU/TH/UR 6000 pen; vanilla. */
.fx-type [data-type]{ position:relative; overflow:hidden; }   /* the beam sweeps to translateX(100%); clipped here so it never widens a scroller (scrollbars flashed) */
.fx-type [data-type] .ch{ visibility:hidden; transition:text-shadow .45s ease-out, color .45s ease-out; }
.fx-type [data-type] .ch.on{ visibility:inherit; }   /* inherit, not visible: an explicit visible would show through a hidden screen */
.fx-type [data-type] .ch.hot{ background:var(--ink); color:var(--paper);
  text-shadow:0 0 6px rgba(30,42,56,.95), 0 0 14px rgba(30,42,56,.6); transition:none; }
.fx-type [data-type] .ch.cool{ color:var(--ink);
  text-shadow:0 0 6px rgba(30,42,56,.95), 0 0 14px rgba(30,42,56,.6); transition:none; }
.fx-type [data-type] .beam{ position:absolute; top:0; bottom:0; left:0; width:100%; pointer-events:none;
  background:linear-gradient(90deg, transparent 0, rgba(255,255,255,.55) 25%, #fff 45%, #fff 55%, rgba(255,255,255,.55) 75%, transparent 100%);
  filter:blur(4px); transform:translateX(-100%) scaleY(.6); }

/* layer: screens (?off=screens) — the tube is a fixed monitor; scrolling flips screens. The old
   screen dies as a CRT collapse; the new one paints in top-to-bottom, line by line, left-to-right,
   at --baud lines per second; the figure draws in as a plotter. Gated on .js: with JS off the
   page stays a scrolling page. A screen taller than the monitor scrolls natively; it flips at its edge. */
:root{ --baud:30; --cps:120; }   /* lines/s for the block paint, chars/s for the typing; live knobs ?baud= ?cps= — 2400 baud proper is ~3 lines / 240 chars */
.js.fx-screens .tube{ position:fixed; inset:0; overflow:hidden; min-height:0; }
.js.fx-screens .sheet{ position:relative; height:100%; padding:0; }
.js.fx-screens .screen{ position:absolute; inset:0; overflow-x:hidden; overflow-y:auto; visibility:hidden; overscroll-behavior:contain;
  padding:clamp(20px,4vw,44px) clamp(16px,4vw,40px) 40px; }
.js.fx-screens .screen.cur{ visibility:visible; }
.js.fx-screens .screen:focus{ outline:none; }   /* focused for reading order only — a focus ring here reads as a frame */
.js.fx-screens .screen.off{ visibility:visible; pointer-events:none; animation:crt-off .24s ease-in forwards; }
@keyframes crt-off{
  0%  { clip-path:inset(0 0 0 0); filter:brightness(1); }
  70% { clip-path:inset(49.6% 0 49.6% 0); filter:brightness(1.7); }
  100%{ clip-path:inset(50% 0 50% 0); opacity:0; }
}
.js.fx-screens .pt{ clip-path:inset(0 100% 0 0); }
.js.fx-screens .pt.pd{ animation:crt-line var(--line-ms,80ms) steps(8,end) forwards; }
@keyframes crt-line{ from{ clip-path:inset(0 100% 0 0); } to{ clip-path:inset(0 -2px 0 0); } }
.scr-ind{ display:none; }
.js.fx-screens .scr-ind{ display:block; position:absolute; right:clamp(16px,4vw,40px); bottom:12px;
  font-family:"IBM Plex Mono",monospace; font-size:11px; letter-spacing:.08em; color:var(--ink-2); text-transform:uppercase; }
/* with the layer off, the four screens stack as the study did */
.screen:not(:first-child) .doc-body{ margin-top:0; border-top:0; }

/* layer: glass (?off=glass) — the tube's glass: a broad diagonal sheen, one soft glare high on the
   right, a faint deepening toward the bottom. The outermost surface, above the raster. Knob --glass. */
:root{ --glass:.18; }   /* .12 was near-invisible on this ground in the receipt; .3 reads as glass without shouting */
.crt-glass{ position:fixed; inset:0; pointer-events:none; z-index:7; display:none;
  background:
    linear-gradient(115deg, rgba(255,255,255,0) 6%, rgba(255,255,255,var(--glass)) 26%, rgba(255,255,255,0) 44%),
    radial-gradient(ellipse 34% 20% at 78% 10%, rgba(255,255,255,calc(var(--glass) * 1.1)), rgba(255,255,255,0) 70%),
    linear-gradient(180deg, rgba(30,42,56,0) 62%, rgba(30,42,56,calc(var(--glass) * .45)) 100%); }
.fx-glass .crt-glass{ display:block; }

/* layer: reflect (?off=reflect) — something reflecting in the glass: a window behind the viewer,
   blurred past recognition (four panes, the mullion a faint seam), high on the right; a lamp-glow low
   on the left. It parallaxes against the pointer a few px — the tell that it is a reflection and not a
   smudge — and sways slowly when there is no pointer. Knob --reflect. Still under reduced motion. */
:root{ --reflect:.16; --rblur:14px; --rx:0; --ry:0; }
.crt-reflect{ position:fixed; inset:0; pointer-events:none; z-index:7; display:none; overflow:hidden; }
.fx-reflect .crt-reflect{ display:block; }
/* an IMAGE of the room, if assets/reflection.png exists (the script probes for it): screen-blended, so its
   black is nothing and its light is what reflects; blurred by --rblur; the drawn window then steps aside */
.crt-reflect .img{ position:absolute; inset:-6%; display:none; background:url(assets/reflection.png) center/cover no-repeat;
  mix-blend-mode:screen; filter:blur(var(--rblur)); opacity:var(--reflect);
  transform:translate(calc(var(--rx) * -14px), calc(var(--ry) * -10px)); will-change:transform; }
.crt-reflect.has-img .img{ display:block; }
.crt-reflect.has-img .win{ display:none; }
.fx-reflect.sway .crt-reflect.has-img .img{ animation:reflect-sway-img 18s ease-in-out infinite alternate; }
@keyframes reflect-sway-img{ from{ transform:translate(-6px, 3px); } to{ transform:translate(6px, -3px); } }
@media (prefers-reduced-motion:reduce){ .fx-reflect:not(.fx-motion) .crt-reflect .img{ animation:none; } }
.crt-reflect .win, .crt-reflect .lamp{ position:absolute; will-change:transform; }
.crt-reflect .win{
  top:4vh; right:6vw; width:min(38vw,560px); height:min(30vw,420px);
  display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; gap:min(1.2vw,16px);
  transform:rotate(-7deg) translate(calc(var(--rx) * -14px), calc(var(--ry) * -10px));
  filter:blur(min(2vw,26px)); opacity:var(--reflect); }
.crt-reflect .win i{ display:block; background:linear-gradient(160deg, #fff 0%, rgba(255,255,255,.92) 60%, rgba(255,255,255,.75) 100%); }
/* the wall around the window: a room reflects as a slightly darker, cooler patch with the panes bright inside it —
   on a paper-white ground a purely additive reflection has no headroom, this is what makes it read */
.crt-reflect .win::before{ content:""; position:absolute; inset:-38% -30%; z-index:-1;
  background:radial-gradient(ellipse at 50% 50%, rgba(30,42,56,.55) 30%, rgba(30,42,56,0) 72%); }
.crt-reflect .lamp{
  bottom:-6vh; left:8vw; width:min(26vw,380px); height:min(22vw,320px); border-radius:50%;
  background:radial-gradient(closest-side, rgba(255,246,225,1), rgba(255,246,225,0));
  transform:translate(calc(var(--rx) * -8px), calc(var(--ry) * -6px));
  filter:blur(min(1.6vw,22px)); opacity:calc(var(--reflect) * .7); }
.fx-reflect.sway .crt-reflect .win{ animation:reflect-sway 18s ease-in-out infinite alternate; }
@keyframes reflect-sway{ from{ transform:rotate(-7deg) translate(-6px, 3px); } to{ transform:rotate(-7deg) translate(6px, -3px); } }
@media (prefers-reduced-motion:reduce){ .fx-reflect:not(.fx-motion) .crt-reflect .win{ animation:none; } }

/* @@LAYER-CSS@@ — each element's CSS is appended below this line, one block per commit */
