/**
 * THE HALL — live teasers.
 *
 * Two parts:
 *   1. THE SLOT (below) — the fixed 240x400 stage every teaser is built to, and
 *      how it sizes across the wide layout and the stacked one. Owned here so
 *      the four teasers never have to agree about pixels: they draw into one
 *      coordinate space and this file decides how big that space renders.
 *   2. THE TEASERS — one block per app, appended below, each scoped under
 *      `.bay[data-app="<slug>"]`. Written by the teaser modules; nothing in a
 *      teaser block may reach outside its own bay.
 *
 * The slot replaces the decorative `.spine` ornament. Bays with no teaser keep
 * the ornament (see parts/apps-body.php), so the two coexist during rollout and
 * a fifth app dropped into catalogue.json still renders something.
 *
 * @package Aaron25
 */

/* ==========================================================================
 *  THE SLOT
 * ========================================================================== */
.tz-slot {
	/* THE STAGE IS A FLEX CHILD NOW.
	 *
	 * It used to be pinned at `left:50%; top:5%` with `max-height:58%`, which
	 * worked only because the plaque underneath it was collapsed to zero height
	 * until the bay was hovered. The hall's LOW BEAM pass put that plaque
	 * permanently in flow, so a fixed 58% band and a plaque of unknown height
	 * would sit on top of each other. As a flex child the stage takes exactly
	 * the height the plaque leaves it, at every viewport, for any length of
	 * copy — the overlap is not merely unlikely, it is unrepresentable.
	 *
	 * `position: relative` is kept for the reason `absolute` was there: it is
	 * the containing block the NORMALIZER below positions `.tz` against.
	 */
	position: relative;
	flex: 1 1 0;
	align-self: center;
	min-height: 130px;

	/* The stage is always 240x400 in its own coordinates; only the rendered
	   size changes. Nothing it draws can be stretched: every teaser's root SVG
	   carries `preserveAspectRatio="xMidYMid meet"`, so the artwork fits itself
	   into whatever box these two values describe and is never scaled past 1:1.
	   That is precisely what the old `max-height: 58%` was already doing on a
	   short viewport — the clamp is the same, the number it clamps to is now
	   the real space available instead of a guess. */
	--tz-w:      min(240px, 92%);
	--tz-w-open: min(300px, 54%);
	width: var(--tz-w);
	max-height: 400px;
	transition:
		width 620ms var(--ease),
		max-height 620ms var(--ease);

	pointer-events: none;
	z-index: -1;
	/* Deliberately no opacity here — presence is the teaser's own business, and
	   two of them animate their root opacity by state. A slot opacity would
	   multiply with theirs and quietly wash them out. */
}

/* AN OPEN BAY GIVES ITS TEASER MORE ROOM.
 *
 * The old numbers here (`74%` closed, `42%` open) were not a growth — they were
 * a compensation. A bay nobody was pointing at shrank to about two-thirds of a
 * column, where 74% was a small box; the hovered bay grew to two and a half
 * columns, where 42% was a slightly larger one. Read against their own bays the
 * two percentages came out nearly the same rendered width, and the teaser was
 * mostly just refusing to change size.
 *
 * These are a real growth, because the stage is height-driven now: at rest the
 * artwork sits at exactly 1:1 in its 240x400 coordinate space, and an open bay
 * takes it to about 1.15x. Both stops move — `--tz-w-open` so a wider column is
 * allowed to be used, `max-height` so the extra height is what actually lands,
 * since height is the binding axis in a bay this tall. Upscaling is safe in a
 * way downscaling never was: every teaser is vector, and the ~2px minimum
 * detail the spec asks for only gets easier to see.
 *
 * The state machine is untouched. assets/teasers.js still promotes a teaser to
 * `live` on :hover, :focus-within and .is-open — the same three states that
 * grow the box, so a teaser wakes up and gets its room in one motion. */
.bays .bay:hover .tz-slot,
.bays .bay:focus-within .tz-slot,
.bays .bay.is-open .tz-slot {
	width: var(--tz-w-open);
	max-height: 460px;
}

/* ==========================================================================
 *  BALANCE PASS
 *
 *  Each teaser was built alone, so each is correctly weighted against nothing.
 *  Standing in a row they are not: Anagrammaton sets seven lit capitals nearly
 *  edge to edge and carries roughly twice the ink of its neighbours, while
 *  Cursed Words spends most of its loop as one word with air around it and goes
 *  quiet next to three dense siblings.
 *
 *  Corrected here rather than inside the teaser blocks, because it is a fact
 *  about the row, not about any teaser — re-delivering one of them shouldn't
 *  silently discard the row's balance, and a fifth bay would change these
 *  numbers again. Both dials are ones the teasers already exposed.
 * ========================================================================== */

/* Pulled in ~10%: those caps are the loudest ink in the hall.
 *
 * Expressed as a scale rather than as a width cap, because the stage's binding
 * dimension is now its height — the space the plaque leaves — and a width cap
 * would quietly stop doing anything the moment height became the constraint.
 * A transform also leaves the slot's own box the size of its siblings', so the
 * four plaques stay on one line across the row; only the painted teaser moves.
 *
 * Wide layout only. Stacked, every slot is a flat 100px in the corner and the
 * row this balances doesn't exist. */
@media (min-width: 981px) {
	.a25-hall .bay[data-app="anagrammaton"] .tz-slot {
		transform: scale(0.9);
	}
}

/* Its own dial for how present the etymology read-out is at rest. At .58 the
   root chain is texture; at .78 it holds its own beside the ledger and the
   spectrum without pre-empting the payoff, which stays live-only. */
.a25-hall .bay[data-app="etim"] .tz--etim {
	--tz-dim: 0.78;
}

/* NORMALIZER — the host owns placement, the teaser owns content.
 *
 * The brief was ambiguous about who positions the stage, and the four teasers
 * split two-two: half fill their container, half place themselves absolutely at
 * `left:50%; top:5%; width:min(240px,74%)` against the bay. Nested inside the
 * slot, that second reading resolves its percentages against the slot instead of
 * the bay and comes out small and off-centre.
 *
 * Rather than send two of them back, placement is overridden here — one rule,
 * one place to look, and every future teaser lands the same way whichever
 * reading its author takes. The extra `.a25-hall` is there purely to reach
 * (0,4,0) so this beats `.bay[data-app="…"] .tz--…` at (0,3,0) regardless of
 * where the integration script appends the teaser blocks.
 *
 * transform:none is safe — the only root transforms in play are static
 * translateX(-50%) centring, which the slot now does instead.
 */
.a25-hall .bay .tz-slot .tz {
	position: absolute;
	/* All four offsets are pinned, not just the near edges: one teaser also
	   ships its own `@media (max-width: 980px)` re-placement anchored with
	   `right`, and resetting only left/top would leave that `right` live — a box
	   with both horizontal edges set stretches instead of filling. Media queries
	   carry no specificity of their own, so this one rule outranks those blocks
	   too. Content-level rules inside them (hiding a column that would be 4px of
	   noise on a phone, say) still apply, which is the half worth keeping. */
	left: 0;
	top: 0;
	right: auto;
	bottom: auto;
	width: 100%;
	height: 100%;
	max-height: none;
	aspect-ratio: auto;
	transform: none;
	overflow: visible;
}
.tz > svg {
	width: 100%;
	height: 100%;
	display: block;
	overflow: visible;
}

/* Interaction is opt-in per teaser: the bay itself is an <a>, so a teaser that
   wants a pointer has to say so, and its own handlers must stop propagation. */
.tz-slot.is-interactive {
	pointer-events: auto;
}

/* ==========================================================================
 *  RESPONSIVE
 * ========================================================================== */
@media (max-width: 980px) {
	/* Stacked: the teaser moves to the bay's top-right corner and shrinks hard.
	   Everything a module draws has to survive this — roughly 100x167 — which is
	   why the spec bans detail finer than ~2px in 240-space. */
	/* All four selectors, because the open-state rule above is (0,3,0) and
	   would otherwise widen the corner teaser to `--tz-w-open` the moment a
	   touch device reported a hover. Stacked, there is no accordion to answer —
	   every bay is full width already, so every slot is the same 100px. */
	.tz-slot,
	.bays .bay:hover .tz-slot,
	.bays .bay:focus-within .tz-slot,
	.bays .bay.is-open .tz-slot {
		/* Back out of the flow: stacked bays are content-height, so there is
		   no free space for a flexible stage to take. (parts/apps-body.php
		   does the same for the fallback ornament.) */
		position: absolute;
		flex: 0 0 auto;
		right: 12px;
		top: 10px;
		width: 100px;
		min-height: 0;
		max-height: none;
		opacity: 0.9;
	}

	/* RESERVE THE CORNER.
	 *
	 * Stacked, every plaque is open, so the bay's own text occupies the same
	 * band the teaser sits in: measured, the slot took x222..330 / y13..193 and
	 * the tagline ran x41..322 / y162..222. They collided in a 100x31 patch, and
	 * because a teaser carries real marks — a running total, a word — the
	 * overlap read as damage rather than as texture the way the old ornament did.
	 *
	 * So the top-right corner is reserved: the short lines stop short of it, and
	 * the plaque starts below it. Applied to every bay, not just teaser-bearing
	 * ones, because the fallback ornament occupies exactly the same corner.
	 *
	 * Specificity is deliberate — the bay's own rules live in an inline <style>
	 * inside the body, which is later in document order than this stylesheet, so
	 * matching specificity would lose.
	 */
	.a25-hall .bays .bay .bay__name,
	.a25-hall .bays .bay .bay__motto,
	.a25-hall .bays .bay .bay__blurb {
		padding-right: 108px;
	}
}

@media (prefers-reduced-motion: reduce) {
	/* The controller pins data-state to idle; modules render a composed static
	   frame. Belt and braces: nothing inside a teaser may animate. */
	.tz, .tz * {
		animation: none !important;
		transition: none !important;
	}
}

/* ==========================================================================
 *  TEASERS — one block per app, appended below this line.
 * ========================================================================== */

/* >>> teaser:etim — css */
/* ── etim · Cursed Words ──────────────────────────────────────────
   A word is set whole, in the reader's own language. A deep-blue fracture
   descends through it; the word breaks at its true seam. Its roots are
   read out beneath, and the literal sense rises, lit. Three words,
   6s each, forever. Pure CSS — no JS in either state.            */

.bay[data-app="etim"] .tz--etim{
  position:absolute; left:50%; top:5%; transform:translateX(-50%);
  width:min(240px,74%); aspect-ratio:240/400; height:auto;
  display:block; pointer-events:none; z-index:-1;
  opacity:.9; transition:opacity 620ms var(--ease);
  /* state dials — see [data-state="live"] below */
  --tz-dim:.58;  /* how present the etymology read-out is        */
  --tz-live:0;   /* the sense gloss: live only                   */
}
.bay[data-app="etim"] .tz--etim[data-state="live"],
.bay[data-app="etim"]:hover .tz--etim,
.bay[data-app="etim"]:focus-within .tz--etim,
.bay[data-app="etim"].is-open .tz--etim{
  opacity:1; --tz-dim:1; --tz-live:1;
}

.bay[data-app="etim"] .tz-svg{
  display:block; width:100%; height:100%; overflow:visible;
}

/* ---- the plate ---------------------------------------------------- */
.bay[data-app="etim"] .tz-rule{
  stroke:var(--neon-deep); stroke-opacity:.72; stroke-width:1;
  vector-effect:non-scaling-stroke;
}
.bay[data-app="etim"] .tz-tick{
  stroke:var(--a1); stroke-width:1.6; stroke-linecap:butt;
  vector-effect:non-scaling-stroke; opacity:.15;
  animation:tzEtTick 18s linear infinite both;
  animation-delay:calc(var(--i) * -6s);
}

/* ---- per-word card ------------------------------------------------ */
.bay[data-app="etim"] .tz-card{
  opacity:0;
  animation:tzEtCard 18s linear infinite both;
  animation-delay:calc(var(--i) * -6s);
}

/* the word, whole and ordinary */
.bay[data-app="etim"] .tz-word{
  font-family:var(--sans); font-weight:600; font-size:var(--ws,44px);
  letter-spacing:-.03em;
  fill:var(--a1);
}
.bay[data-app="etim"] .tz-hL,
.bay[data-app="etim"] .tz-hR{
  animation-duration:18s; animation-iteration-count:infinite;
  animation-fill-mode:both; animation-timing-function:var(--ease);
  animation-delay:calc(var(--i) * -6s);
}
.bay[data-app="etim"] .tz-hL{animation-name:tzEtHL}
.bay[data-app="etim"] .tz-hR{animation-name:tzEtHR}

/* the fracture — the one saturated thing on an otherwise pale plate */
.bay[data-app="etim"] .tz-crack{
  stroke:var(--a3); stroke-width:1.5; fill:none; stroke-linecap:round;
  vector-effect:non-scaling-stroke;
  transform-box:view-box; transform-origin:120px 127px;
  opacity:0;
  animation:tzEtCrack 18s var(--ease) infinite both;
  animation-delay:calc(var(--i) * -6s);
}

/* the read-out: source language, roots, senses */
.bay[data-app="etim"] .tz-glossw{
  opacity:0;
  animation:tzEtRoot 18s linear infinite both;
  animation-delay:calc(var(--i) * -6s);
}
.bay[data-app="etim"] .tz-dim{opacity:var(--tz-dim); transition:opacity 620ms var(--ease)}
.bay[data-app="etim"] .tz-onlive{opacity:var(--tz-live); transition:opacity 620ms var(--ease)}
.bay[data-app="etim"] .tz-lang{
  font-family:var(--mono); font-weight:400; font-size:10.5px;
  letter-spacing:2.6px; fill:var(--a2); fill-opacity:.8;
}
.bay[data-app="etim"] .tz-root{
  font-family:var(--mono); font-weight:400; font-size:15px;
  letter-spacing:.02em;
  fill:var(--a2);
}
.bay[data-app="etim"] .tz-sense{
  font-family:var(--mono); font-weight:400; font-size:9.5px;
  letter-spacing:1.5px; fill:var(--a2); fill-opacity:.72;
}
.bay[data-app="etim"] .tz-join{fill-opacity:.36}

/* ∴ — therefore */
.bay[data-app="etim"] .tz-dot{fill:var(--a3); opacity:.92}

/* the payoff */
.bay[data-app="etim"] .tz-litw{
  opacity:0;
  animation:tzEtLit 18s var(--ease) infinite both;
  animation-delay:calc(var(--i) * -6s);
}
.bay[data-app="etim"] .tz-lit{
  font-family:var(--sans); font-weight:600; font-size:27px;
  letter-spacing:-.025em;
  fill:var(--a1);
  filter:drop-shadow(0 0 6px rgba(76, 122, 232,.46));
  filter:drop-shadow(0 0 6px color-mix(in srgb, var(--a3) 48%, transparent));
}

/* ---- choreography · 18s = 3 words × 6s ---------------------------- */
/*  0.0s word whole  ·  1.7s fracture  ·  2.4s roots  ·  3.2s literal
    5.5s hold, fade  ·  next word                                     */
@keyframes tzEtCard{
  0%{opacity:0} 2.2%{opacity:1} 30.6%{opacity:1} 33.2%{opacity:0} 100%{opacity:0}
}
@keyframes tzEtHL{
  0%,9.4%{transform:translateX(0)}
  15%,33.2%{transform:translateX(-11px)}
  33.3%,100%{transform:translateX(0)}
}
@keyframes tzEtHR{
  0%,9.4%{transform:translateX(0)}
  15%,33.2%{transform:translateX(11px)}
  33.3%,100%{transform:translateX(0)}
}
@keyframes tzEtCrack{
  0%,8.9%{transform:scaleY(0); opacity:0}
  9.6%{opacity:1}
  15%{transform:scaleY(1)}
  33.2%{transform:scaleY(1); opacity:1}
  33.3%,100%{transform:scaleY(0); opacity:0}
}
@keyframes tzEtRoot{
  0%,13.3%{opacity:0}
  18.9%,33.2%{opacity:1}
  33.3%,100%{opacity:0}
}
@keyframes tzEtLit{
  0%,17.8%{opacity:0; transform:translateY(9px)}
  24.4%,33.2%{opacity:1; transform:translateY(0)}
  33.3%,100%{opacity:0; transform:translateY(9px)}
}
@keyframes tzEtTick{
  0%{opacity:.15} 2.2%,30.6%{opacity:1} 33.2%,100%{opacity:.15}
}

/* ---- reduced motion: one composed frame, mechanic intact ---------- */
@media (prefers-reduced-motion:reduce){
  .bay[data-app="etim"] .tz--etim *{animation:none !important; transition:none !important}
  .bay[data-app="etim"] .tz-card{opacity:0}
  .bay[data-app="etim"] .tz-c1{opacity:1}
  .bay[data-app="etim"] .tz-c1 .tz-hL{transform:translateX(-11px)}
  .bay[data-app="etim"] .tz-c1 .tz-hR{transform:translateX(11px)}
  .bay[data-app="etim"] .tz-c1 .tz-crack{transform:scaleY(1); opacity:1}
  .bay[data-app="etim"] .tz-c1 .tz-glossw{opacity:1}
  .bay[data-app="etim"] .tz-c1 .tz-litw{opacity:1; transform:translateY(0)}
  .bay[data-app="etim"] .tz-tick{opacity:.15}
  .bay[data-app="etim"] .tz-k1{opacity:1}
}
/* <<< teaser:etim */

/* >>> teaser:anagrammaton — css */
/* ============================================================
 *  ANAGRAMMATON — "every letter sworn to a seat"
 *  FUNERAL -> REAL FUN, demonstrated: the seats are cut and
 *  labelled before anything moves, every letter's sworn path is
 *  drawn as a thread before it is walked, and each glyph carries
 *  its origin index all the way to its seat. Nothing is added,
 *  nothing is thrown away -- the letters never leave the frame,
 *  they only change seats, and the run rewinds rather than resets.
 *  Pure CSS. No JS in either state.
 * ============================================================ */

.bay[data-app="anagrammaton"] .tz--anagrammaton{
  display:block; width:100%; height:100%;
  pointer-events:none;
  /* one soft glow, on the matter only -- derived from the bay accent */
  --tz-glow:color-mix(in srgb, var(--a1) 50%, transparent);
  --tz-cycle:11s;
}
.bay[data-app="anagrammaton"] .tz-svg{ display:block; width:100%; height:100%; overflow:visible; }

/* ---- carved stone: rules, ticks, section marks ---- */
.bay[data-app="anagrammaton"] .tz-rule{ stroke:var(--a3); stroke-width:1; opacity:.6; }
.bay[data-app="anagrammaton"] .tz-tick{ stroke:var(--a3); stroke-width:1; opacity:.75; }
.bay[data-app="anagrammaton"] .tz-sec{
  font-family:var(--mono); font-size:5.5px; font-weight:400;
  letter-spacing:.22em; fill:var(--ink-faint); opacity:.75;
  transition:opacity .5s var(--ease);
}

/* ---- the machine's declarations, in the app's second light ---- */
.bay[data-app="anagrammaton"] .tz-th{ stroke:var(--a2); stroke-width:1.4; opacity:.22; }
.bay[data-app="anagrammaton"] .tz-seat{ fill:none; stroke:var(--a2); stroke-width:1.2; opacity:.36; }
.bay[data-app="anagrammaton"] .tz-slabel{
  font-family:var(--mono); font-size:5.5px; letter-spacing:.06em;
  text-anchor:middle; fill:var(--a2); opacity:.36;
}
.bay[data-app="anagrammaton"] .tz-assay{
  font-family:var(--mono); font-size:6px; letter-spacing:.08em;
  text-anchor:middle; fill:var(--a2); opacity:0;
  transition:opacity .55s var(--ease);
}

/* ---- the matter itself. Monospaced on purpose: a letter loom seats one
       glyph per column, and the type that already means "one cell, one
       character" is the type that should sit in them. ---- */
.bay[data-app="anagrammaton"] .tz-ghost{
  font-family:var(--mono); font-size:27px; font-weight:500;
  text-anchor:middle; fill:var(--a1); opacity:.22;
  transition:opacity .5s var(--ease);
}
.bay[data-app="anagrammaton"] .tz-glyph{
  font-family:var(--mono); font-size:27px; font-weight:500;
  text-anchor:middle; fill:var(--a1);
  filter:drop-shadow(0 0 3px var(--tz-glow));
}
.bay[data-app="anagrammaton"] .tz-chip{
  font-family:var(--mono); font-size:5.5px; letter-spacing:.06em;
  text-anchor:middle; fill:var(--a1); opacity:.45;
  transition:opacity .5s var(--ease);
}
/* the finished word, lit. one soft wash, nothing else glows. */
.bay[data-app="anagrammaton"] .tz-wash{ fill:var(--a1); fill-opacity:.035; opacity:0; }

/* ---- the receipt ---- */
.bay[data-app="anagrammaton"] .tz-proof{
  font-family:var(--mono); font-size:6.5px; letter-spacing:.16em;
  text-anchor:middle; fill:var(--ink-faint); opacity:.8;
  transition:opacity .5s var(--ease);
}
.bay[data-app="anagrammaton"] .tz-num{ fill:var(--a2); }
.bay[data-app="anagrammaton"] .tz-dot{ fill:var(--a3); }

/* ============================================================
 *  The run. One shared 11s timeline; three waves.
 *  A: R,E cross.  B: A,L slide.  C: F,U,N travel as a block --
 *  FUN survives FUNERAL intact, which is the joke and the proof.
 * ============================================================ */
@keyframes tz-ana-fly-a{
  0%,8%{transform:translate(0,0);animation-timing-function:var(--ease);}
  26%{transform:translate(var(--dx),var(--dy));}
  84%{transform:translate(var(--dx),var(--dy));animation-timing-function:ease-in-out;}
  92%,100%{transform:translate(0,0);}
}
@keyframes tz-ana-fly-b{
  0%,22%{transform:translate(0,0);animation-timing-function:var(--ease);}
  40%{transform:translate(var(--dx),var(--dy));}
  81%{transform:translate(var(--dx),var(--dy));animation-timing-function:ease-in-out;}
  89%,100%{transform:translate(0,0);}
}
@keyframes tz-ana-fly-c{
  0%,36%{transform:translate(0,0);animation-timing-function:var(--ease);}
  55%{transform:translate(var(--dx),var(--dy));}
  78%{transform:translate(var(--dx),var(--dy));animation-timing-function:ease-in-out;}
  86%,100%{transform:translate(0,0);}
}
@keyframes tz-ana-th-a{
  0%,6%{opacity:.22;}
  11%,26%{opacity:.95;}
  33%,84%{opacity:.34;}
  92%,100%{opacity:.22;}
}
@keyframes tz-ana-th-b{
  0%,20%{opacity:.22;}
  25%,40%{opacity:.95;}
  47%,81%{opacity:.34;}
  89%,100%{opacity:.22;}
}
@keyframes tz-ana-th-c{
  0%,34%{opacity:.22;}
  39%,55%{opacity:.95;}
  62%,78%{opacity:.34;}
  86%,100%{opacity:.22;}
}
@keyframes tz-ana-seat-a{
  0%,23%{opacity:.36;}
  26%,84%{opacity:.95;}
  92%,100%{opacity:.36;}
}
@keyframes tz-ana-seat-b{
  0%,37%{opacity:.36;}
  40%,81%{opacity:.95;}
  89%,100%{opacity:.36;}
}
@keyframes tz-ana-seat-c{
  0%,52%{opacity:.36;}
  55%,78%{opacity:.95;}
  86%,100%{opacity:.36;}
}
@keyframes tz-ana-wash{
  0%,51%{opacity:0;}
  58%,78%{opacity:1;}
  85%,100%{opacity:0;}
}

.bay[data-app="anagrammaton"] .tz-fly{ animation:tz-ana-fly-a var(--tz-cycle) infinite; }
.bay[data-app="anagrammaton"] .tz-fly.tz-wb{ animation-name:tz-ana-fly-b; }
.bay[data-app="anagrammaton"] .tz-fly.tz-wc{ animation-name:tz-ana-fly-c; }

.bay[data-app="anagrammaton"] .tz-th{ animation:tz-ana-th-a var(--tz-cycle) infinite; }
.bay[data-app="anagrammaton"] .tz-th.tz-wb{ animation-name:tz-ana-th-b; }
.bay[data-app="anagrammaton"] .tz-th.tz-wc{ animation-name:tz-ana-th-c; }

.bay[data-app="anagrammaton"] .tz-seat,
.bay[data-app="anagrammaton"] .tz-slabel{ animation:tz-ana-seat-a var(--tz-cycle) infinite; }
.bay[data-app="anagrammaton"] .tz-seat.tz-wb,
.bay[data-app="anagrammaton"] .tz-slabel.tz-wb{ animation-name:tz-ana-seat-b; }
.bay[data-app="anagrammaton"] .tz-seat.tz-wc,
.bay[data-app="anagrammaton"] .tz-slabel.tz-wc{ animation-name:tz-ana-seat-c; }

.bay[data-app="anagrammaton"] .tz-wash{ animation:tz-ana-wash var(--tz-cycle) infinite; opacity:0; }

/* ============================================================
 *  LIVE -- the same run, instrumented. Phase and speed do not
 *  change on hover (so nothing ever jumps); what changes is that
 *  the evidence becomes readable -- every origin index, the
 *  assay, the full receipt.
 * ============================================================ */
.bay[data-app="anagrammaton"] .tz--anagrammaton[data-state="live"] .tz-chip{ opacity:.92; }
.bay[data-app="anagrammaton"] .tz--anagrammaton[data-state="live"] .tz-sec{ opacity:1; }
.bay[data-app="anagrammaton"] .tz--anagrammaton[data-state="live"] .tz-ghost{ opacity:.26; }
.bay[data-app="anagrammaton"] .tz--anagrammaton[data-state="live"] .tz-proof{ opacity:1; }
.bay[data-app="anagrammaton"] .tz--anagrammaton[data-state="live"] .tz-assay{ opacity:.8; }

/* ============================================================
 *  REDUCED MOTION -- one composed frame that still proves it:
 *  ghost plate above, the whole permutation drawn as threads,
 *  REAL FUN seated with every receipt showing.
 * ============================================================ */
@media (prefers-reduced-motion: reduce){
  .bay[data-app="anagrammaton"] .tz-fly,
  .bay[data-app="anagrammaton"] .tz-th,
  .bay[data-app="anagrammaton"] .tz-seat,
  .bay[data-app="anagrammaton"] .tz-slabel,
  .bay[data-app="anagrammaton"] .tz-wash{ animation:none !important; }
  .bay[data-app="anagrammaton"] .tz-fly{ transform:translate(var(--dx),var(--dy)); }
  .bay[data-app="anagrammaton"] .tz-th{ opacity:.5; }
  .bay[data-app="anagrammaton"] .tz-seat{ opacity:.9; }
  .bay[data-app="anagrammaton"] .tz-slabel{ opacity:.85; }
  .bay[data-app="anagrammaton"] .tz-chip{ opacity:.9; }
  .bay[data-app="anagrammaton"] .tz-wash{ opacity:1; }
  .bay[data-app="anagrammaton"] .tz-assay{ opacity:.8; }
}
/* <<< teaser:anagrammaton */

/* >>> teaser:chill-streaks — css */
/* ── chill-streaks · Chill Streaks ─────────────────────────────────
   A month, kept. Two weeks stand filled in the app's own pink; this
   week lands one square at a time. Thursday is missed: its square is
   drawn hollow and never filled — the app doesn't backfill days you
   weren't there for. The streak under the calendar dims, dips, and
   HOLDS at 39; then the days simply keep landing — 40, 41, 42, 43.
   The number never returns to zero, which is the entire product
   (`forgiving` mode's `by()` has no reset branch at all).

   12s loop. Pure CSS — no JS in either state.                        */

.bay[data-app="chill-streaks"] .tz--chill-streaks{
  display:block; width:100%; height:100%; pointer-events:none;
  opacity:.9; transition:opacity 620ms var(--ease);
  /* state dial — see [data-state="live"] below */
  --tz-live:0;   /* the argument, spelled out: live only */
}
.bay[data-app="chill-streaks"] .tz--chill-streaks[data-state="live"],
.bay[data-app="chill-streaks"]:hover .tz--chill-streaks,
.bay[data-app="chill-streaks"]:focus-within .tz--chill-streaks,
.bay[data-app="chill-streaks"].is-open .tz--chill-streaks{
  opacity:1; --tz-live:1;
}

/* ---- the plate ---------------------------------------------------- */
.bay[data-app="chill-streaks"] .tz-rule{
  stroke:var(--neon-deep); stroke-opacity:.72; stroke-width:1;
  vector-effect:non-scaling-stroke;
}
.bay[data-app="chill-streaks"] .tz-wd{
  font-family:var(--mono); font-weight:400; font-size:7.5px;
  letter-spacing:.06em; text-anchor:middle;
  fill:var(--ink); fill-opacity:.34;
  transition:fill-opacity 620ms var(--ease);
}
.bay[data-app="chill-streaks"] .tz--chill-streaks[data-state="live"] .tz-wd{
  fill-opacity:.6;
}

/* ---- a day kept: a square of the app's own light ------------------- */
.bay[data-app="chill-streaks"] .tz-day{
  fill:var(--a1); fill-opacity:.17;
  stroke:var(--a1); stroke-opacity:.55; stroke-width:1.2;
  vector-effect:non-scaling-stroke;
}
/* the fortnight already banked reads a shade quieter than this week */
.bay[data-app="chill-streaks"] .tz-kept .tz-day{
  fill-opacity:.13; stroke-opacity:.38;
}
/* this week lands day by day */
.bay[data-app="chill-streaks"] .tz-f{
  animation-duration:12s; animation-iteration-count:infinite;
  animation-timing-function:var(--spring);
}
/* the day after the miss carries its own light — the recovery */
.bay[data-app="chill-streaks"] .tz-rec{
  fill-opacity:.3; stroke-opacity:.95;
  filter:drop-shadow(0 0 6px rgba(109, 222, 255,.5));
  filter:drop-shadow(0 0 6px color-mix(in srgb, var(--a1) 50%, transparent));
}
/* ---- the day that was missed. It is never filled in. --------------- */
.bay[data-app="chill-streaks"] .tz-hole{
  fill:none;
  stroke:var(--ink); stroke-opacity:.42; stroke-width:1.2;
  stroke-dasharray:3.5 3.5;
  vector-effect:non-scaling-stroke;
  animation:tzCsHole 12s var(--ease) infinite;
}
/* the days still ahead */
.bay[data-app="chill-streaks"] .tz-ghostd{
  fill:none; stroke:var(--ink-ghost); stroke-width:1;
  vector-effect:non-scaling-stroke;
}
/* the whole week flinches when the day is dropped, then re-lights */
.bay[data-app="chill-streaks"] .tz-week{
  animation:tzCsDim 12s var(--ease) infinite;
}

/* ---- the streak that does not reset -------------------------------- */
.bay[data-app="chill-streaks"] .tz-count{
  animation:tzCsCount 12s var(--spring) infinite;
}
/* The payoff reads as a neon sign: white-hot core, coloured bloom. It is the
   one number in the bay and should be the brightest thing in it. */
.bay[data-app="chill-streaks"] .tz-num{
  font-family:var(--mono); font-weight:500; font-size:40px;
  fill:var(--ink); text-anchor:middle;
  filter:drop-shadow(0 0 9px rgba(109, 222, 255,.55));
  filter:drop-shadow(0 0 9px color-mix(in srgb, var(--a1) 60%, transparent));
}
.bay[data-app="chill-streaks"] .tz-unit{
  font-family:var(--mono); font-weight:400; font-size:8.5px;
  letter-spacing:1.7px; fill:var(--ink); fill-opacity:.42;
  text-anchor:middle;
}
/* exactly one glyph per column is lit at a time */
.bay[data-app="chill-streaks"] .tz-u,
.bay[data-app="chill-streaks"] .tz-t{
  opacity:0;
  animation-duration:12s; animation-iteration-count:infinite;
  animation-timing-function:steps(1,end);
}
/* the resolved frame — and prefers-reduced-motion — reads 43 */
.bay[data-app="chill-streaks"] .tz-ulast,
.bay[data-app="chill-streaks"] .tz-tlast{opacity:1}

/* ---- the caption: dim texture at rest, the thesis in live ---------- */
.bay[data-app="chill-streaks"] .tz-cap{
  font-family:var(--mono); font-weight:400; font-size:7.5px;
  letter-spacing:.18em; text-anchor:middle;
  fill:var(--a2);
  opacity:calc(.34 + .58 * var(--tz-live));
  transition:opacity 620ms var(--ease);
}

/* ---- choreography · 12s ───────────────────────────────────────────
    0.0–0.5s  plate and streak fade in at 36
    0.8–2.4s  Mon, Tue, Wed land: 37, 38, 39
    2.9–3.2s  the rhythm stops. Thursday never lands.
    3.2–4.3s  its square is drawn hollow; the week and the streak dim,
              the streak dips — and HOLDS at 39. It was never 0.
    4.8–7.2s  Fri, Sat, Sun, Mon land: 40, 41, 42, 43
    7.2–11s   hold — the frame most visitors will only ever see       */

@keyframes tzCsF0{0%,5.3%{opacity:0;transform:translateY(-5px)}6.7%,90%{opacity:1;transform:translateY(0)}95%,100%{opacity:0;transform:translateY(0)}}
@keyframes tzCsF1{0%,12%{opacity:0;transform:translateY(-5px)}13.3%,90%{opacity:1;transform:translateY(0)}95%,100%{opacity:0;transform:translateY(0)}}
@keyframes tzCsF2{0%,18.7%{opacity:0;transform:translateY(-5px)}20%,90%{opacity:1;transform:translateY(0)}95%,100%{opacity:0;transform:translateY(0)}}
@keyframes tzCsF3{0%,38.7%{opacity:0;transform:translateY(-5px)}40%,90%{opacity:1;transform:translateY(0)}95%,100%{opacity:0;transform:translateY(0)}}
@keyframes tzCsF4{0%,45.4%{opacity:0;transform:translateY(-5px)}46.7%,90%{opacity:1;transform:translateY(0)}95%,100%{opacity:0;transform:translateY(0)}}
@keyframes tzCsF5{0%,52%{opacity:0;transform:translateY(-5px)}53.3%,90%{opacity:1;transform:translateY(0)}95%,100%{opacity:0;transform:translateY(0)}}
@keyframes tzCsF6{0%,58.7%{opacity:0;transform:translateY(-5px)}60%,90%{opacity:1;transform:translateY(0)}95%,100%{opacity:0;transform:translateY(0)}}

@keyframes tzCsHole{0%,24%{opacity:0}27%,90%{opacity:1}95%,100%{opacity:0}}
@keyframes tzCsDim{0%,26%{opacity:1}29%,36%{opacity:.4}41%,100%{opacity:1}}
@keyframes tzCsCount{
  0%,3.4%{opacity:0;transform:translateY(0)}
  5%,26%{opacity:1;transform:translateY(0)}
  29%,36%{opacity:.3;transform:translateY(7px)}
  41%,90%{opacity:1;transform:translateY(0)}
  95%,100%{opacity:0;transform:translateY(0)}
}

@keyframes tzCsU0{0%,4%{opacity:0}4.1%,6.6%{opacity:1}6.7%,100%{opacity:0}}
@keyframes tzCsU1{0%,6.6%{opacity:0}6.7%,13.2%{opacity:1}13.3%,100%{opacity:0}}
@keyframes tzCsU2{0%,13.2%{opacity:0}13.3%,19.9%{opacity:1}20%,100%{opacity:0}}
@keyframes tzCsU3{0%,19.9%{opacity:0}20%,39.9%{opacity:1}40%,100%{opacity:0}}
@keyframes tzCsU4{0%,39.9%{opacity:0}40%,46.6%{opacity:1}46.7%,100%{opacity:0}}
@keyframes tzCsU5{0%,46.6%{opacity:0}46.7%,53.2%{opacity:1}53.3%,100%{opacity:0}}
@keyframes tzCsU6{0%,53.2%{opacity:0}53.3%,59.9%{opacity:1}60%,100%{opacity:0}}
@keyframes tzCsU7{0%,59.9%{opacity:0}60%,100%{opacity:1}}
@keyframes tzCsT0{0%,4%{opacity:0}4.1%,39.9%{opacity:1}40%,100%{opacity:0}}
@keyframes tzCsT1{0%,39.9%{opacity:0}40%,100%{opacity:1}}

/* ---- stacked bays --------------------------------------------------
   The slot moves to the bay's top-right at ~100px wide; the caption is
   1px of noise there and the calendar tells the story alone. */
@media (max-width:980px){
  .bay[data-app="chill-streaks"] .tz-cap{display:none}
}

/* ---- reduced motion: one composed frame, mechanic intact ---------- */
@media (prefers-reduced-motion:reduce){
  .bay[data-app="chill-streaks"] .tz--chill-streaks *{animation:none !important; transition:none !important}
  /* the base styles already ARE the resolved frame: the month filled,
     Thursday hollow, the recovery lit, the streak reading 43 */
}
/* <<< teaser:chill-streaks */

/* >>> teaser:mic-rain — css */
/* ══ SOUND BUBBLE (mic-rain) · "the bubble, literally" ════════════════════
   One circle of the app's own light, with you (one warm point) at its
   centre. The
   room's noise arrives as jagged bolts from every direction; each breaks
   against the rim in a soft pink flare — shards deflect OUTWARD, nothing
   crosses. Inside, the only motion is a slow calm ripple and your own
   breathing halo. Live grows two more bolts, covered exactly the same
   way, and names the two sides: NOISE outside, QUIET inside.

   7.2s loop, one impact roughly every 1.8s. Pure CSS — no JS, no audio.
   Motion is transform and opacity only, inside prefers-reduced-motion:
   no-preference; `reduce` falls back to the authored frame: one bolt
   mid-flight, one flare lit, its shards deflected.                       */

.bay[data-app="mic-rain"] .tz--mic-rain{
  display:block; width:100%; height:100%; pointer-events:none;
  opacity:.9; transition:opacity 620ms var(--ease);
  --tz-T:7.2s;
  --tz-lab:.38;  /* how present the labels are at rest */
}
.bay[data-app="mic-rain"] .tz--mic-rain[data-state="live"],
.bay[data-app="mic-rain"]:hover .tz--mic-rain,
.bay[data-app="mic-rain"]:focus-within .tz--mic-rain,
.bay[data-app="mic-rain"].is-open .tz--mic-rain{
  opacity:1; --tz-lab:.9;
}
.bay[data-app="mic-rain"] .tz-svg{display:block;width:100%;height:100%;
  overflow:visible;stroke-linecap:round;stroke-linejoin:round}

/* ── the two sides, named ─────────────────────────────────────────────── */
.bay[data-app="mic-rain"] .tz-lab{
  font-family:var(--mono);font-weight:400;font-size:8px;
  letter-spacing:.32em;stroke:none;
  opacity:var(--tz-lab);transition:opacity 620ms var(--ease);
}
.bay[data-app="mic-rain"] .tz-lab--noise{fill:var(--ink-soft)}
.bay[data-app="mic-rain"] .tz-lab--quiet{fill:var(--a2)}

/* ── the room: bolts of noise ─────────────────────────────────────────── */
.bay[data-app="mic-rain"] .tz-bolt{stroke:var(--ink);stroke-width:1.8}
.bay[data-app="mic-rain"] .tz-brun{opacity:.6;transform:translateX(26px)}
/* the rim answers, exactly where it was hit */
.bay[data-app="mic-rain"] .tz-flare{
  stroke:var(--a1);stroke-width:5;opacity:0;
  filter:drop-shadow(0 0 6px rgba(93, 156, 255,.55));
  filter:drop-shadow(0 0 6px color-mix(in srgb, var(--a1) 55%, transparent));
}
/* and the noise breaks up — outward, never inward */
.bay[data-app="mic-rain"] .tz-shards line{stroke:var(--ink-soft);stroke-width:1.2}
.bay[data-app="mic-rain"] .tz-shards{opacity:0}

/* ── the bubble ───────────────────────────────────────────────────────── */
.bay[data-app="mic-rain"] .tz-mem-halo{stroke:var(--a1);stroke-width:11;opacity:.055}
.bay[data-app="mic-rain"] .tz-mem-soft{stroke:var(--a1);stroke-width:4.5;opacity:.11}
.bay[data-app="mic-rain"] .tz-mem{stroke:var(--a1);stroke-width:1.5;opacity:.6}
.bay[data-app="mic-rain"] .tz-mem-in{stroke:var(--a2);stroke-width:1;opacity:.15}
.bay[data-app="mic-rain"] .tz-bub{transform-box:fill-box;transform-origin:50% 50%}

/* ── inside: the same room, gentled ───────────────────────────────────── */
.bay[data-app="mic-rain"] .tz-rip{
  stroke:var(--a2);stroke-width:1.4;opacity:.2;
  transform-box:fill-box;transform-origin:50% 50%;transform:scale(.9);
}
/* You are one of only two warm marks anywhere in the hall — the route is a
   single blue hue now, and this and the MAX star are the whole of the
   exceptions. One amber dot in all that cold reads as a person without
   needing a label. */
.bay[data-app="mic-rain"] .tz-you-halo{stroke:var(--warm);stroke-opacity:.22;stroke-width:1;opacity:.6;
  transform-box:fill-box;transform-origin:50% 50%}
.bay[data-app="mic-rain"] .tz-you-ring{stroke:var(--warm);stroke-width:1;opacity:.4}
.bay[data-app="mic-rain"] .tz-you-dot{fill:var(--warm);stroke:none;
  filter:drop-shadow(0 0 5px rgba(255,193,74,.7))}

/* ── live: the room grows two more bolts — covered exactly the same way ─ */
.bay[data-app="mic-rain"] .tz-x{display:none}
.bay[data-app="mic-rain"] .tz--mic-rain[data-state="live"] .tz-x{display:inline}
.bay[data-app="mic-rain"] .tz--mic-rain[data-state="live"] .tz-mem{opacity:.78}
.bay[data-app="mic-rain"] .tz--mic-rain[data-state="live"] .tz-mem-soft{opacity:.17}
.bay[data-app="mic-rain"] .tz--mic-rain[data-state="live"] .tz-mem-halo{opacity:.085}

@media (prefers-reduced-motion: no-preference){
  .bay[data-app="mic-rain"] .tz-brun{
    animation:tz-sb-run var(--tz-T) var(--ease) var(--dl,0s) infinite both}
  .bay[data-app="mic-rain"] .tz-flare{
    animation:tz-sb-flare var(--tz-T) var(--ease) var(--dl,0s) infinite both}
  .bay[data-app="mic-rain"] .tz-shards{
    animation:tz-sb-shards var(--tz-T) var(--ease) var(--dl,0s) infinite both}
  .bay[data-app="mic-rain"] .tz-rip{
    animation:tz-sb-rip 6s linear var(--dl,0s) infinite both}
  .bay[data-app="mic-rain"] .tz-bub{
    animation:tz-sb-breathe 9s ease-in-out infinite both}
  .bay[data-app="mic-rain"] .tz-you-halo{
    animation:tz-sb-you 9s ease-in-out infinite both}
}

/* a bolt crosses the room and dies on the rim */
@keyframes tz-sb-run{
  0%,3%  {transform:translateX(0);opacity:0}
  7%     {opacity:.7}
  16%    {opacity:.7}
  19%    {transform:translateX(36px);opacity:.6}
  23%,100%{transform:translateX(36px);opacity:0}
}
/* the rim flares at the point of impact, then settles */
@keyframes tz-sb-flare{
  0%,18% {opacity:0}
  22%    {opacity:1}
  30%    {opacity:.4}
  44%,100%{opacity:0}
}
/* the debris is thrown back the way it came */
@keyframes tz-sb-shards{
  0%,19% {transform:translateX(0);opacity:0}
  22%    {opacity:.85}
  34%,100%{transform:translateX(-11px);opacity:0}
}
/* inside, the event arrives as one wide slow ring */
@keyframes tz-sb-rip{
  0%   {transform:scale(.45);opacity:0}
  12%  {opacity:.38}
  75%  {opacity:.09}
  100% {transform:scale(1.55);opacity:0}
}
@keyframes tz-sb-breathe{
  0%,100%{transform:scale(1)}
  50%    {transform:scale(1.012)}
}
@keyframes tz-sb-you{
  0%,100%{transform:scale(1);opacity:.5}
  50%    {transform:scale(1.18);opacity:.9}
}

/* ── stacked bays: labels are unreadable at 100px — the picture carries ─ */
@media (max-width:980px){
  .bay[data-app="mic-rain"] .tz-lab{display:none}
}

/* ── reduced motion: one composed frame, mechanic intact ──────────────── */
@media (prefers-reduced-motion:reduce){
  .bay[data-app="mic-rain"] .tz--mic-rain *{animation:none !important; transition:none !important}
  /* base styles compose the frame: the first bolt frozen just short of the
     rim (.tz-brun's base transform/opacity), the second bolt's flare lit
     with its shards deflected. */
  .bay[data-app="mic-rain"] .tz-b:nth-of-type(2) .tz-brun{opacity:0}
  .bay[data-app="mic-rain"] .tz-b:nth-of-type(2) .tz-flare{opacity:.85}
  .bay[data-app="mic-rain"] .tz-b:nth-of-type(2) .tz-shards{opacity:.7;transform:translateX(-6px)}
  .bay[data-app="mic-rain"] .tz-b:nth-of-type(3) .tz-brun{opacity:0}
  .bay[data-app="mic-rain"] .tz-b:nth-of-type(4) .tz-brun{opacity:0}
  .bay[data-app="mic-rain"] .tz-rip{transform:scale(1);opacity:.12}
}
/* <<< teaser:mic-rain */
