/* ============================================================
   Chewtoy's Dugout — ornament & motion · "Night Pitch" (2026-09-03)
   Requires tokens.css. Loads on every v3 page.

   ONE COMPONENT, PLACED BY MODIFIER. `.ctd-orn` wraps an inline SVG
   from assets/ornaments/; where it goes and how big it is comes from a
   `--corner-tl` / `--cap` / `--rake` modifier, never from the SVG. The
   art is all `currentColor`, so a gem is bone in a section heading and
   gold on a chosen card without a second file — the same trick the
   colour-stop variables play in the reference build.

   Every ornament is decorative: `aria-hidden` on the span, and nothing
   in here is ever the only way to perceive state.

   MOTION BUDGET
   -------------
   Ambient loops (glint, breathe, drift) are slow enough that you notice
   them only in the corner of the eye, and they never run over body copy.
   Pointer responses (trace, rake, lift) are 200-600ms. Everything stops
   under `prefers-reduced-motion` — see the block at the foot of the file.
   ============================================================ */

/* --ctd-trace-angle has to be a registered property or the conic sweep in
   --fx-trace cannot be animated at all (unregistered custom properties
   interpolate discretely). Browsers without @property just get a still
   gradient, which is a fine resting state. */
@property --ctd-trace-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

/* ---------- the component ---------- */
.ctd-orn { position: absolute; display: block; pointer-events: none; user-select: none; color: var(--text-3); z-index: 2; }
.ctd-orn svg { display: block; width: 100%; height: 100%; }

/* Corner gussets. The SVG is drawn top-left; the other three are flips, so one
   file serves all four and the bracket always hugs the outside of the box.
   The flip lives on this wrapper and never on anything inside the SVG, so the
   two can never fight over the same transform. */
.ctd-orn--corner { width: var(--orn-corner); height: var(--orn-corner); }
.ctd-orn--corner-tl { top: -1px; left: -1px; }
.ctd-orn--corner-tr { top: -1px; right: -1px; transform: scaleX(-1); }
.ctd-orn--corner-bl { bottom: -1px; left: -1px; transform: scaleY(-1); }
.ctd-orn--corner-br { bottom: -1px; right: -1px; transform: scale(-1, -1); }
.ctd-orn--sm { width: var(--orn-corner-sm); height: var(--orn-corner-sm); }

/* `.ctd-orn--bone` lived here: a `color` override for an ornament drawn in
   currentColor. Nothing has emitted it since the corners stopped being
   currentColor art, and the last currentColor ornament left is the claw
   underline, which takes --accent. Removed 2026-09-06 rather than left as an
   orphan — see the two cheap post-effects checks in DESIGN-SYSTEM.md. */

/* ---------- the iron ring ----------
   All that is left of it. It used to be the corner ornament — a shackle ring
   bolted to a rivetted strap with a drive chain running over its rim, round
   all four sides of a framed panel. That was a fine piece of ironmongery from
   the wrong building: this site is a steel-framed stand, and a chain belt is
   not how anything in it is held together (2026-09-06, owner). The frame
   corners are gussets now; the ring survives only as the cap at each end of a
   ctd_rule() divider.

   Iron in a room is still until something disturbs it, so it rattles: a short
   burst of decaying shake at the end of a long cycle, stillness the rest of
   the time. */
.ctd-orn__ring { animation: ctd-rattle var(--t-rattle) var(--ease) infinite; }

/* Decaying and asymmetric — a real rattle does not swing evenly about centre. */
@keyframes ctd-rattle {
  0%, 84%, 100% { transform: rotate(0deg); }
  85.5% { transform: rotate(-3.2deg); }
  87%   { transform: rotate(2.4deg); }
  88.5% { transform: rotate(-1.8deg); }
  90%   { transform: rotate(1.2deg); }
  91.5% { transform: rotate(-0.7deg); }
  93%   { transform: rotate(0.35deg); }
  94.5% { transform: rotate(-0.15deg); }
}

/* ---------- corner gussets ----------
   A framed panel is a plate BOLTED INTO the frame, held at each corner by a
   gusset cut from the same steel js/ctd-steel.js rolls the girders and cross
   members out of. That is the whole idea, and it is why this replaced the ring
   and its chain: every other joint in this room is a plate, a bolt or a strap,
   and the frame corners are now made of the same three things.

   THE METAL IS SET HERE, NOT IN THE SVG. assets/ornaments/corner-gusset.svg
   shades itself through --orn-metal* and --orn-bolt* and never touches
   `color`, so one file is steel on one card and bronze on the next. The
   defaults in tokens.css are the iron the divider ring wants; a corner asks
   for the frame's own steel. */
.ctd-orn--corner {
  --orn-metal:      var(--girder-face);
  --orn-metal-hi:   var(--girder-lit);
  --orn-metal-lo:   var(--girder-lo);
  --orn-metal-spec: var(--girder-spec);
  --orn-bolt:       var(--girder-rivet);
  --orn-bolt-hi:    var(--girder-rivet-hi);
  --orn-bolt-lo:    var(--girder-rivet-lo);
}

/* Bronze marks the chosen thing, and it is the SAME bronze as the tee straps
   at the frame's joints and the nameplate over every section — so "this is the
   one" is said in a material the page has already taught the eye to read,
   rather than in a colour invented for the occasion.

   The modifier keeps the name `gold` on purpose: it is the ROLE (the upsell,
   the chosen card), and renaming it to `bronze` would tie the class to
   whatever alloy it happens to be this month. Same reasoning as
   ctd_corner_ornaments() keeping its name through five different motifs. */
.ctd-orn--gold {
  --orn-metal:      var(--joint-face);
  --orn-metal-hi:   var(--joint-lit);
  --orn-metal-lo:   var(--joint-lo);
  --orn-metal-spec: var(--joint-spec);
  --orn-bolt:       var(--joint-bolt);
  --orn-bolt-hi:    var(--joint-bolt-hi);
  --orn-bolt-lo:    var(--joint-bolt-lo);
}

/* NOTHING HAPPENS ON HOVER, and that is deliberate. The ring turned gold under
   the pointer, which worked while the corners were jewellery; a bolted-on
   gusset that changes alloy when you look at it is the one thing real hardware
   never does — and on the compare cards it broke the rule above outright, by
   turning the Standard card's steel into the bronze that means "chosen". The
   card still answers the pointer through .ctd-trace and the rim tokens. */

/* The glint is the same path as the gusset's lit edge, dashed to a short
   segment and walked along it — the lamp finding a machined edge, which is the
   only thing that should still move out here now the chain has gone.
   pathLength="100" in the SVG makes the offset a percentage regardless of the
   real path length, so all four corners stay in step. */
.ctd-orn__glint {
  opacity: 0;
  animation: ctd-glint var(--t-glint) var(--ease) infinite;
}
.ctd-orn--corner-tr .ctd-orn__glint { animation-delay: calc(var(--t-glint) / -4); }
.ctd-orn--corner-br .ctd-orn__glint { animation-delay: calc(var(--t-glint) / -2); }
.ctd-orn--corner-bl .ctd-orn__glint { animation-delay: calc(var(--t-glint) * -0.75); }

@keyframes ctd-glint {
  0%           { stroke-dashoffset: 30; opacity: 0; }
  12%          { opacity: 0.85; }
  38%          { opacity: 0; }
  38.01%, 100% { stroke-dashoffset: -100; opacity: 0; }
}

/* ---------- chain ----------
   assets/ornaments/chain-h.svg holds ONE tile of real interlocking links in an
   SVG <pattern>, which tiles for free at any length, so a run is a single
   <rect> however wide it needs to be.

   THE ONLY CHAIN LEFT IS IN A DIVIDER, between a ctd_rule() label and its two
   ring caps — see the section-divider block below, which owns the placement.
   What used to live here was a belt round all four sides of a framed panel,
   with its runs sitting tangent to the corner rings and a quarter wrap at each
   corner; that geometry went out with the ring (2026-09-06), along with
   --orn-cog-c and --orn-cog-r, which existed only to place it.

   Motion. The pattern is resolved in the element's user space, so translating
   an ANCESTOR group drags the painted links with it — one pitch per step,
   which loops seamlessly. The rect overhangs by a pitch at both ends so the
   travel never exposes an edge. */
@keyframes ctd-chain-h { to { transform: translateX(var(--orn-pitch)); } }

/* Below this the full-size gussets crowd the copy on a framed panel, so they
   come down to card scale. The corner is the only ornament on a frame now, so
   this is the whole responsive story for one. */
@media (max-width: 900px) {
  .ctd-ornframe .ctd-orn--corner { width: var(--orn-corner-sm); height: var(--orn-corner-sm); }
}

/* ---------- section divider ----------
   Each end is a small iron ring on its rattle cycle. --cap is in flow, unlike
   every other ornament placement, so it overrides the component's absolute
   positioning.

   THIS IS THE LAST CHAIN IN THE THEME, and it is on borrowed time: the frame
   corners went steel in 2026-09 and a divider on the builders hub and the shop
   archive is now the only place a visitor meets the old ironmongery. Converting
   it is a small job (a bolted flat bar, or ctd_plate() where the divider is
   really a heading) and it was left alone only because the brief was the home
   page, which carries no ctd_rule(). */
.ctd-orn--cap {
  position: relative; flex: none;
  width: var(--orn-cap); height: var(--orn-cap);
  color: var(--accent);
}

/* The divider's lines are chain runs between its two rings, at the pitch the
   ring's rim is derived from. Masked directionally so the chain is solid where
   it meets its ring and dissolves toward the label; the symmetric
   --fx-fade-line would fade it at the ring end too, exactly where it should be
   biting. */
.ctd-rule__line {
  position: relative; flex: 1; height: var(--orn-chain);
  color: var(--bone-500); overflow: hidden;
  mask-image: var(--fx-fade-e); -webkit-mask-image: var(--fx-fade-e);
}
.ctd-rule__line .ctd-orn__chain-run { animation: ctd-chain-h var(--t-chain) linear infinite; }

/* The run on the far side of the label. Targeted by sibling, NOT by
   :first-of-type / :nth-of-type — every child of .ctd-rule is a <span>, so
   "first span of type" is the cog wrapper and a type selector silently matches
   nothing at all. The CHAIN keeps one direction across both runs: a belt between
   two sprockets moves uniformly, and reversing half of it reads as two chains
   fighting over the label. */
.ctd-rule__label ~ .ctd-rule__line {
  mask-image: var(--fx-fade-w); -webkit-mask-image: var(--fx-fade-w);
}

/* ---------- the wall ----------
   A masonry ground for a hero, a footer or a section that wants to feel like a
   room rather than a page. Decorative: aria-hidden, pointer-events none, and
   NEVER the only way to perceive anything.

   ctd_wall() in inc/theme-chrome.php emits it with the static tile inlined,
   and js/ctd-wall.js replaces that svg on load with a wall built to the
   element's own size and no repeat in it. So this component has to look right
   BOTH ways, which is why nothing in here knows or cares which one it holds.

   AMPLITUDE IS THE WHOLE DISCIPLINE. --wall-amp is a real luminance shift, so
   the rule from DESIGN-SYSTEM.md §2 bites harder here than for the blooms: no
   body copy over the wall at any amplitude. Display type is fine over
   .ctd-wall--scrim; reading copy sits on a card or on the plain ground.

   The parent needs position:relative and its content needs a z-index above 0. */
.ctd-wall {
  position: absolute; inset: 0; z-index: 0;
  overflow: hidden; pointer-events: none;
  opacity: var(--wall-amp);
  mask-image: var(--fx-wall-fade); -webkit-mask-image: var(--fx-wall-fade);
}
.ctd-wall svg {
  display: block;
  /* transform scales what the svg PAINTS; the size compensation keeps the
     layer covered. Scaling the svg box alone would do nothing — the geometry
     is in userSpaceOnUse px and the svg has no viewBox, by design, so a stone
     is never stretched to fit a container. */
  width: calc(100% / var(--wall-scale)); height: calc(100% / var(--wall-scale));
  transform: scale(var(--wall-scale)); transform-origin: 0 0;
}

@media (max-width: 700px) { .ctd-wall { --wall-scale: 0.68; } }

/* Lit from above: a pool of torchlight, dark at the edges. ::after rather than
   a second background layer, so the mask above still governs the lot. */
.ctd-wall::after {
  content: ""; position: absolute; inset: 0;
  background-image: var(--fx-wall-torch);
}

/* The claw decal. Placed ONCE, by the template, wherever it looks like
   something came through the wall — never inside the tile, or it repeats and
   gives the period away. Sized by --wall-claw, positioned by the caller. */
.ctd-wall__mark { position: absolute; width: var(--wall-claw); height: calc(var(--wall-claw) / 0.85); opacity: 0.9; }

/* ---------- water ----------
   The one thing in the wall that moves. js/ctd-wall.js draws a gleam down the
   lit side of every drip and puts it in three phase groups; this dashes those
   paths down to a short segment and walks the segment along them, so a runnel
   of light runs down the drip and the wall reads as actively wet rather than
   just stained.

   Each group is a separate <path>, which is the only reason the delays work: a
   dash pattern belongs to an element, so one path for every drip in the wall
   would run them all in lockstep — a machine, not weather.

   Mostly REST. The runnel takes about a third of the cycle and the rest is
   nothing, the same shape as the ring rattle. Water that trickles continuously
   is a fountain. */
.ctd-wall__wet {
  stroke-dasharray: 30 3000;   /* one segment per drip, then a long gap */
  stroke-dashoffset: 30;       /* parked just above the top of the drip */
  animation: ctd-trickle var(--t-trickle) linear infinite;
}
.ctd-wall__wet--2 { animation-delay: calc(var(--t-trickle) / -3); }
.ctd-wall__wet--3 { animation-delay: calc(var(--t-trickle) * -0.66); }

@keyframes ctd-trickle {
  0%            { stroke-dashoffset: 30; opacity: 0; }
  4%            { opacity: 1; }
  30%           { stroke-dashoffset: -300; opacity: 1; }
  36%           { opacity: 0; }
  36.01%, 100%  { stroke-dashoffset: 30; opacity: 0; }
}

/* OPT-IN: the torch gutters. The pool of light breathes and the wall breathes
   with it — the only motion in here, and it moves LIGHT, never stone. Kept to
   the ambient budget: slow, low amplitude, and something you should only
   notice out of the corner of your eye. Irregular keyframes, like the ring
   rattle, because an even fade reads as a pulsing div. */
.ctd-wall--torchlit::after { animation: ctd-gutter-flame var(--t-torch) var(--ease) infinite; }
@keyframes ctd-gutter-flame {
  0%, 100% { opacity: 1; }
  18%      { opacity: 0.9; }
  26%      { opacity: 1; }
  44%      { opacity: 0.84; }
  52%      { opacity: 0.97; }
  71%      { opacity: 0.88; }
  86%      { opacity: 1; }
}

/* ---------- the gutter walls ----------
   Stone down both sides of the page, floor to ceiling, with the content column
   running between them like a corridor. This is the placement the amplitude
   rule wanted all along: the gutters are OUTSIDE the measure, so there is no
   copy to sit over and the wall can run at full strength without a scrim.

   FIXED, not absolute. A wall that scrolls has to be as tall as the document
   and would be regenerated every time the page grew; a fixed pair is one
   viewport-sized wall each, painted once, and reads as a room the page moves
   through rather than a background that travels with it.

   Wide screens only, at the same 1280 breakpoint as the floodlight rails —
   below it the gutter is a few pixels and stone in it would just crowd the
   copy. The gutter runs from the viewport edge to the content column, so its
   width is whatever is left over, and on a 1920 screen that is 360px of wall. */
.ctd-wall--gutter { display: none; }

@media (min-width: 1280px) {
  .ctd-wall--gutter {
    display: block; position: fixed; top: 0; bottom: 0; inset-inline: auto;
    width: calc(50% - var(--wrap) / 2);
    /* Behind the floodlight rails and everything else, but above the page
       ground. The rails sit at z-index 0 and are drawn later in the markup. */
    z-index: 0;
    /* Full strength: nothing reads over it. The fade instead pulls the stone
       back toward the content column, so the corridor is darkest where it
       meets the text and the join is never a hard edge. */
    opacity: 1;
  }
  .ctd-wall--gutter-l {
    left: 0;
    mask-image: var(--fx-wall-gutter-l); -webkit-mask-image: var(--fx-wall-gutter-l);
  }
  .ctd-wall--gutter-r {
    right: 0;
    mask-image: var(--fx-wall-gutter-r); -webkit-mask-image: var(--fx-wall-gutter-r);
  }
  /* The torch pool is a hero device — a tall thin gutter wants light from the
     side it faces, so the ::after carries the corridor falloff instead. */
  .ctd-wall--gutter-l::after { background-image: var(--fx-wall-corridor-l); }
  .ctd-wall--gutter-r::after { background-image: var(--fx-wall-corridor-r); }
}

.ctd-wall--footer { mask-image: var(--fx-wall-fade-up); -webkit-mask-image: var(--fx-wall-fade-up); }
.ctd-wall--full { mask-image: var(--fx-wall-all); -webkit-mask-image: var(--fx-wall-all); }
.ctd-wall--scrim::after { background-image: var(--fx-wall-torch), var(--fx-wall-scrim); }

/* ---------- the pitch ----------
   The floor of the room the gutter walls are the sides of: flagstones with
   grass, moss and weeds in the joints, fixed behind the entire page.

   WHERE IT SITS IN THE STACK, and why every layer is where it is. body::before
   (the blooms) and body::after (grain + vignette) are both z-index -1; this is
   -2, so the order out from the canvas is

       body background → the pitch → the blooms → grain and vignette

   which is the only order that makes physical sense: the blooms are light
   falling ON the floor, so they cannot be under it, and the grain and the
   vignette are the air in the room, so they go over the lot. A -1 here would
   put the floor above the blooms and light the room from behind its own walls.

   FIXED, for the two reasons the gutter walls are fixed: a floor that scrolled
   would have to be as tall as the document and be regenerated every time the
   page grew, and a fixed layer cannot contribute scrollable overflow — the trap
   body::before documents. One viewport, painted once.

   READ THROUGH, not read over. Unlike the wall this surface has copy on it, so
   the discipline is in the tokens (see the block comment in tokens.css: every
   value is roughly half the wall's) and in --ground-amp, not in a scrim. There
   is deliberately no `.ctd-ground--scrim`: a floor that needs one is a floor
   turned up too far.

   NOTHING IN HERE MOVES. The grass used to, in three phase groups on a slow
   translate, and it went out with the grass itself: this layer is a background
   and a background that moves is a background you look at. There is no
   reduced-motion entry for it any more, because there is nothing to cancel. */
.ctd-ground {
  position: fixed; inset: 0; z-index: -2;
  overflow: hidden; pointer-events: none;
  opacity: var(--ground-amp);
}
.ctd-ground svg {
  display: block;
  /* transform scales what the svg PAINTS; the size compensation keeps the
     layer covered. Scaling the svg box alone would do nothing — the geometry
     is in userSpaceOnUse px and the svg has no viewBox, by design, so a flag
     is never stretched to fit a container.
     `max-width: none` is load-bearing: base.css sets `img, svg { max-width:
     100% }` globally, which silently clamped the width back to the layer while
     the height (nothing caps that) grew as intended. The result was a floor
     20% short down its right-hand side and a pitch laid out against a box
     narrower than the one it is centred in — visible only on phones, because
     --ground-scale is 1 everywhere else and the bug cannot fire. */
  width: calc(100% / var(--ground-scale)); height: calc(100% / var(--ground-scale));
  max-width: none;
  transform: scale(var(--ground-scale)); transform-origin: 0 0;
}

/* A slab is 176px, which on a 390px phone is two and a bit across the screen —
   at that point it has stopped being paving and become three large rectangles.
   Scaling the layer down gives a phone about four slabs across, which still
   reads as a floor. This is the lever that exists for exactly this: the
   geometry is in px, so it does not adapt on its own. */
@media (max-width: 700px) { .ctd-ground { --ground-scale: 0.55; } }

/* Floodlit from the near edge, falling away at the far corners. Same job
   --fx-wall-torch does for the wall, and the same reason: without a light
   source in it, an even field of texture is wallpaper. */
.ctd-ground::after {
  content: ""; position: absolute; inset: 0;
  background-image: var(--fx-ground-pool);
}

/* ---------- the boarding ----------
   Timber lining a section. The third surface in the room and the first that is
   not stone: the wall in the gutters and the floor behind the page are cut
   from one masonry kernel, and this is the boarding a dugout is lined with.
   Everything it LOOKS like is in --wood-* / --fx-wood-* (tokens.css); this
   component only places it.

   ctd_planks() in inc/theme-chrome.php emits it with the static tile inlined,
   and js/ctd-plank.js replaces that svg on load with boarding built to the
   element's own box, with no repeat in it. So this has to look right BOTH
   ways, which is why nothing in here knows or cares which one it is holding.

   IT HAS COPY ON IT, which is the whole difference from .ctd-wall. The wall
   lives in a 113px gutter and can run its amplitude up; a section is where the
   reading happens, so the discipline is in the tokens (every value roughly the
   floor's rather than the wall's) and in --plank-amp, and there is deliberately
   no .ctd-planks--scrim — a surface that needs one is turned up too far.

   NO MASK BY DEFAULT. A section is a room with boarded walls, not a plank
   floating on a page, so the boarding runs to the edges of the box and the
   section's own border finishes it. `--fade` is for the placement that wants
   the boarding to die out instead.

   THE BOX IS THE BAY, NOT THE PAGE (2026-09-05, owner: the card backgrounds
   "currently stretch the whole screen"). The boarding lines the inside of the
   steel frame and stops on the girders' inner faces, so what shows outside the
   frame is the pitch paving the whole page stands on — the gutter is floor,
   the bay is a boarded room. --bay-gutter is 0 below the girder gate, so this
   one declaration is also the old full-bleed behaviour at the widths where
   there is no frame to line.

   NOTHING IN HERE MOVES, for the reason nothing in .ctd-ground moves: this is
   a background, and a background that moves is a background you look at. There
   is no reduced-motion entry for it because there is nothing to cancel.

   The parent needs position:relative and its content needs a z-index above 0 —
   .ctd-section--wood in components.css does both. */
.ctd-planks {
  position: absolute; inset: 0;
  left: var(--bay-gutter); right: var(--bay-gutter);
  z-index: 0;
  overflow: hidden; pointer-events: none;
  opacity: var(--plank-amp);
}
.ctd-planks svg {
  display: block;
  /* transform scales what the svg PAINTS; the size compensation keeps the
     layer covered. Scaling the svg box alone would do nothing — the geometry
     is in userSpaceOnUse px and the svg has no viewBox, by design, so a board
     is never stretched to fit a container.
     `max-width: none` is load-bearing: base.css sets `img, svg { max-width:
     100% }` globally, which silently clamps the width back to the layer while
     the height grows as intended. That was a real bug in .ctd-ground and it
     would be the same one here. */
  width: calc(100% / var(--plank-scale)); height: calc(100% / var(--plank-scale));
  max-width: none;
  transform: scale(var(--plank-scale)); transform-origin: 0 0;
}

/* A board is ~76px, which on a 390px phone is five across and a section only
   two or three courses tall. Scaling down gives back the number of boards the
   surface was tuned for — and unlike the floor there is no grid to lose, so
   this can go further than --ground-scale does. */
@media (max-width: 700px) { .ctd-planks { --plank-scale: 0.72; } }

/* The lantern above. ::after rather than another background layer on the
   element, so anything masking the component still governs the lot.

   There is no second layer for damp any more. It was a violet bloom low in the
   box and it did to the whole surface what the damp BOARD did to one plank —
   put the room's light inside the material instead of on it. The page's own
   blooms already drop violet on everything, which is the right way round. */
.ctd-planks::after {
  content: ""; position: absolute; inset: 0;
  background-image: var(--fx-wood-lamp);
}

.ctd-planks--fade { mask-image: var(--fx-wood-fade); -webkit-mask-image: var(--fx-wood-fade); }

/* ---------- the pitch ----------
   Grass, for a placement that wants the pitch under it: a hero, a product bay,
   a masthead. The fourth surface in the room after the wall, the paving and
   the boarding — and the only one that moves. Everything it LOOKS like is in
   --turf-* / --grass-* / --fx-turf-* (tokens.css); this component only places
   it and runs the wind.

   ctd_turf() in inc/theme-chrome.php emits it with the static tile inlined,
   and js/ctd-turf.js replaces that svg on load with a pitch built to the
   element's own box, with no repeat in it and with the wind cells in it. So
   this has to look right BOTH ways, and the tile — which has no cells — simply
   finds nothing to animate.

   NO BODY COPY OVER THIS, at any amplitude. It is the one surface in the room
   with a real signal in it: --turf-amp is the highest of the three and the
   blades move, which is exactly what a reader's eye is built to catch. Display
   type is fine; a paragraph goes on a card.

   There is deliberately no --still modifier. A placement that needs the pitch
   held still is a placement that wants the paving — the same argument
   .ctd-planks makes for having no scrim.

   The parent needs position:relative and its content needs a z-index above 0. */
.ctd-turf {
  position: absolute; inset: 0; z-index: 0;
  overflow: hidden; pointer-events: none;
  opacity: var(--turf-amp);
}
.ctd-turf svg {
  display: block;
  /* transform scales what the svg PAINTS; the size compensation keeps the
     layer covered. Scaling the svg box alone would do nothing — the geometry
     is in userSpaceOnUse px and the svg has no viewBox, by design, so a blade
     is never stretched to fit a container.
     `max-width: none` is load-bearing: base.css sets `img, svg { max-width:
     100% }` globally, which silently clamps the width back to the layer while
     the height grows as intended. That was a real bug in .ctd-ground. */
  width: calc(100% / var(--turf-scale)); height: calc(100% / var(--turf-scale));
  max-width: none;
  transform: scale(var(--turf-scale)); transform-origin: 0 0;
}

/* A clump is ~30px and a phone is 390 wide. Scaled less than the boarding is:
   the blades are the smallest marks on any surface in the room and they go to
   mush before the layer runs out of room. */
@media (max-width: 700px) { .ctd-turf { --turf-scale: 0.82; } }

/* THE WIND. One group per CLUMP, skewed about that clump's own base: skewX
   moves the top of a box and leaves the bottom where it is, which is exactly
   what a blade of grass does.

   Per clump, not per cell of the surface, and the difference is the whole
   feature. A cell held fifty clumps and leaned them all by the same angle at
   the same moment, so nothing moved relative to anything beside it — and the
   angle had to stay under three degrees or clumps near the top of the cell
   were dragged sideways at their bases. One pixel of tip travel, in lockstep,
   which reads as a layer sliding rather than as grass. With the origin at a
   clump's own base the only thing above it is its own blades, so --turf-lean
   is nine degrees and a tip travels ten. See js/ctd-turf.js.

   Only some clumps move (MOVE_P there); the still ones carry no group and no
   animation at all. That is both what a field looks like in a light wind and
   where the cost went.

   The generator supplies each group's transform-origin, a phase (--p), an
   amplitude (--a) and its own PERIOD (--s). The period matters: identical
   durations stay in lockstep forever however the phases are spread, so the
   whole verge would breathe as one thing. The angle, the base duration and the
   resting state are here, so the block at the foot of this file cancels them.

   The drawn geometry is the MIDDLE of the sway — the keyframes run from -lean
   to +lean — so a cancelled animation, a printed page and the fallback tile
   all show grass standing where it was drawn rather than blown over. */
.ctd-turf__w {
  animation: ctd-turf-sway calc(var(--t-wind) * var(--s, 1)) var(--ease-wind) infinite alternate;
  animation-delay: calc(var(--p, 0) * var(--t-wind) * -1);
}
@keyframes ctd-turf-sway {
  from { transform: skewX(calc(var(--a, 1) * var(--turf-lean) * -1)); }
  to   { transform: skewX(calc(var(--a, 1) * var(--turf-lean))); }
}

/* Floodlit from the near edge. ::before rather than another background layer
   on the element, so anything masking the component still governs the lot. */
.ctd-turf::before {
  content: ""; position: absolute; inset: 0;
  background-image: var(--fx-turf-pool);
}

/* There WAS a second motion here: a wide, weak band of light that crossed the
   pitch on a slow translate, meant as the half of the wind that reads from
   across the room. It is gone, at the owner's word — with the blades barely
   moving it was the only motion you could actually see, so the surface read as
   a filter sweeping over a still picture of grass. Fixing the blades removed
   its job, and a light sweep on top of moving grass is a second weather
   system. --fx-turf-gust and --t-gust went with it. */

.ctd-turf--fade { mask-image: var(--fx-turf-fade); -webkit-mask-image: var(--fx-turf-fade); }

/* ---------- the mist gutters ----------
   Violet air turning over on itself down both sides of the page, with the
   content column running between and the steel girders standing at their inner
   edge. Generated by js/ctd-mist.js; everything it looks like is in the
   --mist-* block in tokens.css.

   IT REPLACED THE GRASS HERE on 2026-09-07, at the owner's word. The grass is
   not gone — index.php still runs it in the home teams bay — and this is not
   a correction of the 2026-09-05 decision to put it here. What that decision
   got right was the PLACEMENT: the gutters are outside the measure, so nothing
   reads over them and a surface there can move. What it got wrong was that
   grass is a thing you can name. Four more named things were tried after it
   (a crowd, a wood, a fence, stained glass) and all four went the same way,
   which is how the rule got stated: a gutter must have no subject, because
   anything nameable in the corner of the eye gets looked at every time.

   FIXED, not absolute, for the two reasons the retired stone walls were fixed
   and the paving still is: a layer that scrolled would have to be as tall as
   the document and be regenerated every time the page grew, and a fixed layer
   cannot contribute scrollable overflow. One viewport each, painted once — and
   it reads as weather the page travels through rather than a background that
   travels with it.

   GATED AT THE GIRDERS' BREAKPOINT, not the stone walls' 1280. The steel is
   what makes the gutter read as the edge of something rather than as a margin,
   and below 1360 there is no steel. At 1360 the gutter is 80px, at 1920 it is
   360; the generator sizes everything off the box it is given, so a wide
   gutter gets bigger, slower weather rather than more of the same.

   THE RESTING STATE IS THE GROUND GRADIENT. Alone of the six surfaces this one
   ships no static fallback tile, because it does not need one: with JS off,
   --fx-mist-ground is a perfectly good gutter. */
.ctd-mist { display: none; }

@media (min-width: 1360px) {
  .ctd-mist {
    display: block; position: fixed; top: 0; bottom: 0; inset-inline: auto;
    width: calc(50% - var(--wrap) / 2);
    overflow: hidden; pointer-events: none;
    /* Above the paving (z-index -2) and behind the girders and everything
       else. The girders sit at z-index 0 and are drawn later in the markup. */
    z-index: 0;
    background-image: var(--fx-mist-ground);
  }
  /* The dark the copy sits against, over the whole surface. ::after rather
     than another generated layer, so it cannot be lost to a rebuild. */
  .ctd-mist::after {
    content: ""; position: absolute; inset: 0;
    background-image: var(--fx-mist-edge);
  }
  .ctd-mist--gutter-l {
    left: 0;
    mask-image: var(--fx-mist-gutter-l); -webkit-mask-image: var(--fx-mist-gutter-l);
  }
  .ctd-mist--gutter-r {
    right: 0;
    mask-image: var(--fx-mist-gutter-r); -webkit-mask-image: var(--fx-mist-gutter-r);
  }
}

/* Every child the generator emits is an <i>, absolutely positioned. The
   element is a span of nothing — it carries no text and no meaning — and <i>
   is the shortest tag that is valid inside one. */
.ctd-mist > i, .ctd-mist i { position: absolute; display: block; }
.ctd-mist svg { display: block; max-width: none; }

/* A drifting sheet of noise. It carries an exact whole number of tiles and is
   twice as tall as it needs to be, so translating it by half its own height
   lands the pattern back on itself and the loop has no seam in it.

   This is the one thing left in the surface that loops, and it is only safe
   because no two sheets share a tile height — see TILES in js/ctd-mist.js.
   NOT EVERYTHING FALLS: two rise and one falls, at speeds that are not
   multiples of each other, because a surface where every layer travels the
   same way is a scroll however slowly it goes. */
.ctd-mist__sheet {
  left: -8%; right: -8%; top: 0; background-repeat: repeat;
  will-change: transform;
  animation: ctd-mist-fall var(--dur, 90s) linear infinite;
}
.ctd-mist__sheet.is-up { animation-name: ctd-mist-rise; }
@keyframes ctd-mist-fall { from { transform: translateY(-50%); } to { transform: translateY(0); } }
@keyframes ctd-mist-rise { from { transform: translateY(0); } to { transform: translateY(-50%); } }

/* A PARTICLE. It crosses the gutter once, fading in and out at the ends, then
   starts again from a position no one saw it leave. Each carries its own
   duration and its own negative delay, so at any instant the set is at a dozen
   unrelated points in a dozen unrelated cycles — which is what makes the
   surface read as random rather than as a loop, and what lets the eddies avoid
   being drawn twice the way a looping layer's contents must be. */
.ctd-mist__drop { animation: ctd-mist-drop var(--fdur, 120s) linear infinite; }
.ctd-mist__drop.is-up { animation-name: ctd-mist-drop-up; }
@keyframes ctd-mist-drop {
  0%   { transform: translateY(0); opacity: 0; }
  12%  { opacity: 1; }
  84%  { opacity: 1; }
  100% { transform: translateY(var(--fall, 1000px)); opacity: 0; }
}
@keyframes ctd-mist-drop-up {
  0%   { transform: translateY(0); opacity: 0; }
  12%  { opacity: 1; }
  84%  { opacity: 1; }
  100% { transform: translateY(calc(var(--fall, 1000px) * -1)); opacity: 0; }
}

/* THE SWAY IS NOT A SINE. A clean back-and-forth at one period is a metronome
   and the eye locks onto it within seconds; five uneven stops at uneven times
   never repeats visibly even though it loops exactly. The scale rides along
   with it so the bank billows as it goes — that is the difference between fog
   and a sliding photograph of fog.

   Two transforms cannot live on one element, so lateral movement is always a
   parent's job and rotation is always a child's. */
.ctd-mist__sway { inset: 0; animation: ctd-mist-sway var(--sdur, 27s) ease-in-out infinite; }
@keyframes ctd-mist-sway {
  0%   { transform: translateX(calc(var(--sx, 10px) * -1)) scale(1); }
  23%  { transform: translateX(calc(var(--sx, 10px) * .42)) scale(1.05); }
  41%  { transform: translateX(calc(var(--sx, 10px) * .95)) scale(1.02); }
  64%  { transform: translateX(calc(var(--sx, 10px) * -.34)) scale(1.07); }
  86%  { transform: translateX(calc(var(--sx, 10px) * .6)) scale(1.03); }
  100% { transform: translateX(calc(var(--sx, 10px) * -1)) scale(1); }
}

/* A NET-ZERO WANDER, and the net-zero part is what makes it usable: it ends
   exactly where it began, so it can sit inside something that is travelling or
   looping and never disturb it. This is what stops the eddies moving in
   convoy — they share a direction, but each takes its own path through it. */
.ctd-mist__wander { inset: 0; animation: ctd-mist-wander var(--wdur, 44s) ease-in-out infinite; }
@keyframes ctd-mist-wander {
  0%   { transform: translate(0, 0); }
  17%  { transform: translate(var(--wx, 14px), calc(var(--wy, 26px) * -.62)); }
  38%  { transform: translate(calc(var(--wx, 14px) * -.72), calc(var(--wy, 26px) * .84)); }
  59%  { transform: translate(calc(var(--wx, 14px) * .48), var(--wy, 26px)); }
  81%  { transform: translate(calc(var(--wx, 14px) * -.9), calc(var(--wy, 26px) * -.28)); }
  100% { transform: translate(0, 0); }
}

/* The eddy turning on the spot. `will-change` is deliberate here and not
   cargo: the element carries a blur, and without a promoted layer the browser
   re-rasterises that blur on every frame of the rotation. Its size is capped
   in js/ctd-mist.js for the other half of the same bargain — a promoted
   blurred layer costs GPU memory by area. */
.ctd-mist__spin {
  inset: 0; will-change: transform;
  animation: ctd-mist-spin var(--rdur, 60s) linear infinite;
}
.ctd-mist__spin.is-ccw { animation-direction: reverse; }
@keyframes ctd-mist-spin { to { transform: rotate(360deg); } }

.ctd-mist__bloom { inset: 0; animation: ctd-mist-breathe var(--bdur, 17s) ease-in-out infinite; }
@keyframes ctd-mist-breathe { 0%, 100% { opacity: .42; } 50% { opacity: 1; } }

/* A wisp thickens and thins rather than simply fading, so it reads as
   something passing rather than something switching on. */
.ctd-mist__ghost { inset: 0; animation: ctd-mist-ghost var(--gdur, 21s) ease-in-out infinite; }
@keyframes ctd-mist-ghost {
  0%, 100% { opacity: .18; transform: scaleX(.86); }
  30% { opacity: .9; }
  44% { opacity: .55; }
  62% { opacity: 1; transform: scaleX(1.08); }
  82% { opacity: .4; }
}

/* Everything stops, and what is left still looks like a gutter: the sheets
   hold whatever offset they were at, the eddies sit where they are. The one
   thing that needs saying explicitly is the particles' opacity — their
   envelope starts and ends at 0, so frozen at frame zero they would all be
   invisible. */
@media (prefers-reduced-motion: reduce) {
  /* The .is-* variants are listed too, and not for tidiness: they set
     animation-name at (0,2,0), so a bare (0,1,0) reset here loses to them and
     the reversed layers keep running. Matching the weight is what makes this
     work without !important, which this stylesheet does not allow. */
  .ctd-mist__sheet, .ctd-mist__sheet.is-up,
  .ctd-mist__drop, .ctd-mist__drop.is-up,
  .ctd-mist__spin, .ctd-mist__spin.is-ccw,
  .ctd-mist__sway, .ctd-mist__wander,
  .ctd-mist__bloom, .ctd-mist__ghost { animation: none; }
  .ctd-mist__drop, .ctd-mist__drop.is-up { opacity: .85; }
}

/* ---------- the steel frame ----------
   A rivetted plate girder down the inner edge of each gutter wall, and the
   same girder laid flat across the page at every boundary between one section
   and the next. The content column runs between the girders; the cross members
   separate the sections it runs through, and a section's nameplate (.ctd-plate,
   below) bolts to the member above it. Everything the steel looks like is in
   --fx-girder / --fx-girder-t (tokens.css); this component only places it.

   THE WRAPPER EXISTS FOR THE CROSS MEMBERS. They have to span the page, and a
   child of a girder can only reach as far as that 40px box — its own `50%` is
   40px, not the page. So both members hang in one full-width layer instead,
   and the cross member is simply `left: 0; right: 0`. It also settles the
   overlap for free: the wrapper puts the cross members after the girders in
   paint order, so the member laps OVER the girders it crosses.

   It SCROLLS with the page. That is the one thing it does differently from the
   stone floor under it, and it is what the cross members require: they mark
   real section boundaries, so the frame has to travel with those boundaries.
   The floor stays fixed because it is generated markup and a document-tall one
   would be regenerated every time the page grew — this frame is gradient
   layers on empty spans, so document height costs it nothing.

   TWO GATES, NOT ONE. The frame and its cross members show at every width — a
   horizontal member needs no gutter, and the nameplates hang from it — while
   the VERTICAL girders keep a gate higher than the floor's 1280: a 40px member
   needs 40px of gutter and then some floor still showing outside it, and the
   gutter is 50% - wrap/2 of the CONTENT box, so at a 1280 viewport a 15px
   scrollbar leaves 32.5px and the girder overhung the screen, losing its lit
   flange off the left edge. 1360 is the first width that holds the steel plus
   about 30px of floor. */

/* The frame's containing block. `top: 0; bottom: 0` has to resolve against
   something document-tall, and body is it — v3 pages only, so a legacy route
   keeps whatever it had. An absolutely positioned child of a flex container
   is out of flow, so this never joins the column. */
html.ctd-v3 body { position: relative; }

.ctd-girders {
  position: absolute; inset: 0;
  z-index: 0; pointer-events: none;
}

/* The cross members. One per section boundary, as deep as the girder is wide
   and carrying its rivet run. js/ctd-girder.js places them and owns the `top`;
   without JS there are no members and a nameplate hangs from nothing — still a
   rivetted plaque, which is a resting state and not a broken one. Same bargain
   as ctd-reveal.js.

   IT STOPS ON THE GIRDERS' INNER FACES rather than running off the screen
   (2026-09-05, owner: "the bars should only hit the edge of the other bar, so
   they look like squares"). --bay-gutter is 0 below the girder gate and the
   girder's inner face above it, so one declaration covers both widths: the
   member is the full width where there is nothing to stop against and exactly
   --wrap across where the frame is closed. Two verticals and two horizontals
   then bound a rectangular bay, and the crossing is a T with a joint on it
   instead of an X with the beam lapped over the column.

   Centred ON the boundary, so half the member sits in each section's block
   padding. A positioned z-index:0 layer paints ABOVE in-flow content and there
   is nothing else stopping it, so that padding is the constraint: the tightest
   section on the site (the trust bands) carries 32px against a 20px half.
   Re-check with elementFromPoint before growing --girder-w. */
.ctd-girders__t {
  position: absolute; left: var(--bay-gutter); right: var(--bay-gutter);
  height: var(--girder-t-h);
  background: var(--fx-girder-t);
  box-shadow: var(--fx-girder-t-edge);
}

/* ---------- the bronze T joint ----------
   The joining brace at each end of a cross member: a bronze tee over the
   corner, its band lying down the girder and its arm out along the member,
   with one bolt through the band. What it is MADE of is --joint-* /
   --fx-joint (tokens.css) with JS off and js/ctd-steel.js with JS on; this
   only places it.

   REAL ELEMENTS, WHERE THEY WERE THE MEMBER'S TWO PSEUDOS. The old note here
   argued the tees should not be in the script's hands, because a joint's
   existence is purely a consequence of where a member ends — and that held
   right up until they had to carry generated bronze, because a pseudo-element
   cannot hold an <svg>. js/ctd-girder.js creates them with the member and
   nowhere else, so the consequence still follows the member around.

   FLUSH WITH BOTH BARS, which is what makes it a brace rather than a badge:
   the band is exactly --girder-w and the arm exactly --girder-t-h, so no steel
   shows beside the fitting on either member. What shows instead is
   --joint-reach — how far it runs past the crossing — and that is the only
   number that decides how big the thing looks.

   ONLY WHERE THERE IS A GIRDER TO STRAP, so the same 1360 gate: below it the
   member runs the full width with nothing at its ends, and a bracket bolted to
   the edge of the screen is a bracket holding air. The gate is a `display`,
   which also means the painter never generates one: a hidden element measures
   zero and js/ctd-steel.js declines a box it cannot see.

   Unlike the member, it cannot foul the copy. It hangs --joint-reach into the
   section padding at each end, but both ends are in the gutter, outside the
   content column — so it costs no section padding and is not part of the
   --girder-w clearance sum above. */
.ctd-joint { display: none; }

@media (min-width: 1360px) {
  .ctd-joint {
    display: block; position: absolute; top: 50%;
    width: var(--joint-w); height: var(--joint-h);
    transform: translateY(-50%);
    background: var(--fx-joint);
    box-shadow: var(--fx-joint-edge);
  }
  /* The one declaration that mirrors the bolt. Its four layers are a band
     wide, so `left` and `right` move them from one end of the fitting to the
     other; the plate layers are the full width, which leaves them no
     positioning slack, so the same value passes through them unchanged. It
     has to live here rather than in --fx-joint — see the note there. */
  .ctd-joint--l { background-position-x: left; }
  .ctd-joint--r { background-position-x: right; }
  /* HUNG OFF THE MEMBER'S EDGE BY THE BAND'S WIDTH, not centred on it. The
     band has to finish exactly where the member starts or the tee's inner
     corner is not the frame's corner, and `left: -stem` puts it there without
     the box's width entering into it — so --joint-arm-w can change on its own.

     Two polygons rather than one plus scaleX(-1): mirroring the element would
     mirror its lighting with it, and both joints are lit from the left like
     everything else in this room. */
  .ctd-joint--l {
    left: calc(var(--joint-stem) * -1);
    clip-path: polygon(
      var(--joint-chamfer) 0,
      var(--joint-stem) 0,
      var(--joint-stem) calc(50% - var(--joint-arm) / 2),
      calc(100% - var(--joint-chamfer)) calc(50% - var(--joint-arm) / 2),
      100% calc(50% - var(--joint-arm) / 2 + var(--joint-chamfer)),
      100% calc(50% + var(--joint-arm) / 2 - var(--joint-chamfer)),
      calc(100% - var(--joint-chamfer)) calc(50% + var(--joint-arm) / 2),
      var(--joint-stem) calc(50% + var(--joint-arm) / 2),
      var(--joint-stem) 100%,
      var(--joint-chamfer) 100%,
      0 calc(100% - var(--joint-chamfer)),
      0 var(--joint-chamfer));
  }
  .ctd-joint--r {
    right: calc(var(--joint-stem) * -1);
    clip-path: polygon(
      calc(100% - var(--joint-chamfer)) 0,
      calc(100% - var(--joint-stem)) 0,
      calc(100% - var(--joint-stem)) calc(50% - var(--joint-arm) / 2),
      var(--joint-chamfer) calc(50% - var(--joint-arm) / 2),
      0 calc(50% - var(--joint-arm) / 2 + var(--joint-chamfer)),
      0 calc(50% + var(--joint-arm) / 2 - var(--joint-chamfer)),
      var(--joint-chamfer) calc(50% + var(--joint-arm) / 2),
      calc(100% - var(--joint-stem)) calc(50% + var(--joint-arm) / 2),
      calc(100% - var(--joint-stem)) 100%,
      calc(100% - var(--joint-chamfer)) 100%,
      100% calc(100% - var(--joint-chamfer)),
      100% var(--joint-chamfer));
  }
}

/* ---------- generated steel ----------
   js/ctd-steel.js paints a girder, a cross member or a tee into its element as
   an <svg> and then sets [data-steel] on it. These rules are what that
   attribute buys: everything the CSS was drawing gets out of the way, because
   the same picture is now in the markup and drawn better.

   THE ATTRIBUTE IS SET LAST, after the markup is in. So a module that fails to
   load, a browser that will not run it, a crawler and a thrown exception all
   leave the gradient girder above exactly as it was — the switch is one
   attribute and it is only ever set by something that has already succeeded.

   THE SHADOW BECOMES A FILTER, and it has to. The generated plate's edges are
   CHEWED — nicks bitten out of both long sides — so a box-shadow, which
   follows the border box, would cast a crisp rectangle off a silhouette that
   is not one. drop-shadow follows the alpha, so the cast runs into every nick
   with the metal. The value differs by axis for the reason --fx-girder-edge
   and --fx-girder-t-edge differ: light in this room comes from the upper left,
   so a column casts to the right and a beam casts downward.

   The tee keeps no cast of its own (see --fx-joint-edge on why) and loses its
   clip-path with its background: the generated fitting draws its own
   silhouette, and clipping a shape to a polygon it already is only costs a
   composite layer. */
.ctd-girders svg { display: block; width: 100%; height: 100%; }
.ctd-girder[data-steel],
.ctd-girders__t[data-steel],
.ctd-joint[data-steel] {
  background: none;
  box-shadow: none;
}
.ctd-joint[data-steel] { clip-path: none; }
.ctd-girder[data-steel="v"] { filter: var(--fx-steel-cast-v); }
.ctd-girders__t[data-steel="h"] { filter: var(--fx-steel-cast-h); }

.ctd-girder { display: none; }

@media (min-width: 1360px) {
  .ctd-girder {
    display: block; position: absolute; top: 0; bottom: 0;
    width: var(--girder-w);
    background: var(--fx-girder);
    box-shadow: var(--fx-girder-edge);
  }
  /* Both sides take the same offset: the girder sits in the gutter with its
     inner face on the content column's edge. Fixed inline offsets and no
     transform, so — unlike the hero bloom base.css warns about — the frame
     cannot push the document's scroll width. */
  .ctd-girder--l { left: calc(50% - var(--wrap) / 2 - var(--girder-w)); }
  .ctd-girder--r { right: calc(50% - var(--wrap) / 2 - var(--girder-w)); }

  /* The content keeps clear of the girders. Inside the measure rather than
     outside it, so the girders stay put and the full-bleed section grounds
     still run the whole width — only the copy moves. */
  .ctd-wrap, .ctd-wrap--narrow { padding-inline: var(--girder-pad); }
}

/* ---------- the nameplate ----------
   A section's title on a bronze plaque bolted to the cross member above it.
   ctd_plate() in inc/theme-chrome.php emits the plate with the heading INSIDE
   it, so the plate is the section's real <h2> — not a decoration beside a
   screen-reader one, which is what the chain divider was. Everything it is
   MADE of — the bronze, the border cut into it and its rivets — is in
   --plate-* / --fx-plate* (tokens.css); this places it and cuts its corners.

   HOW IT MEETS THE BAR WITHOUT JS KNOWING ABOUT IT. The section that holds a
   plate carries .ctd-section--plated (components.css), which zeroes the
   section's top padding, so the plate's box starts exactly on the boundary the
   member is centred on. `translateY(-50%)` then lifts it by half its own
   height, which puts the plaque's centre line and the bar's centre line on the
   same pixel — the plate straddles the steel. Both are anchored to the
   section's top edge, so that holds at any width, at any section padding, with
   the title on one line or two, and the script still never learns the plate
   exists. --plate-tuck (tokens.css) settles the flow box the transform left
   behind.

   THE Z-INDEX IS LOAD-BEARING NOW, not tidiness. The bar lives in the
   body-level frame at z-index 0 and the plate is in flow, so `position:
   relative; z-index: 1` is what puts the plaque in FRONT of the steel it
   straddles — the same rule .ctd-home__hero-grid needed over the wall. Lose it
   and the bar cuts across the middle of the title, which is the lap this
   design has already been told off for twice. The plate's top half also
   overhangs into the previous section's bottom padding; that section is
   `position: relative` with no z-index of its own, so it opens no stacking
   context and the plaque paints over its ground as well.

   The chamfer is a clip-path on the face, and a clip-path cuts a box-shadow
   off with the corners; so the face carries only the inset bevel and the cast
   shadow is a drop-shadow FILTER on the plate, which follows the clipped
   silhouette. The four rivets are empty elements rather than sixteen more
   background layers: one --fx-rivet recipe, placed four times. */
.ctd-plate {
  position: relative; z-index: 1;
  align-self: center; margin-inline: auto;
  width: fit-content; min-width: min(var(--plate-min-w), 100%); max-width: 100%;
  margin-bottom: var(--plate-tuck);
  transform: translateY(-50%);
  padding: var(--plate-pad-top) var(--plate-pad-x) var(--plate-pad-bottom);
  text-align: center;
  filter: var(--fx-plate-cast);
}
.ctd-plate::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: var(--fx-plate);
  box-shadow: var(--fx-joint-edge);
  clip-path: polygon(
    var(--plate-cut-x) 0, calc(100% - var(--plate-cut-x)) 0,
    100% var(--plate-cut-y), 100% calc(100% - var(--plate-cut-y)),
    calc(100% - var(--plate-cut-x)) 100%, var(--plate-cut-x) 100%,
    0 calc(100% - var(--plate-cut-y)), 0 var(--plate-cut-y));
}
/* The border: the field cut into the face, inset by the rim the rivets sit on.
   Square, where the plate is chamfered — a panel offset inward from a 45° cut
   by more than the cut is deep has no chamfer left to inherit, and faking one
   would put four gaps in the keyline where the clip trims it. Same z-index as
   the face and later in the tree, so it lands on top of it and under the
   rivets, which are real elements. */
.ctd-plate::after {
  content: ""; position: absolute; inset: var(--plate-frame); z-index: -1;
  background: var(--fx-plate-field);
}
/* A rivet's box is 4r square, so parking it against an edge puts the dome's
   centre 2r (--plate-inset) in from that edge — on the rim, outside the field,
   where a plaque's fixings belong. It sits further in from the ENDS than from
   the top and bottom, because the ends are cut away: --plate-rivet-x is the
   centre it wants and the box is offset by half itself to get there. */
.ctd-plate__rivet {
  position: absolute; width: var(--rivet-box); height: var(--rivet-box);
  background: var(--fx-rivet); pointer-events: none;
}
.ctd-plate__rivet--tl { top: 0; left: var(--plate-rivet-off); }
.ctd-plate__rivet--tr { top: 0; right: var(--plate-rivet-off); }
.ctd-plate__rivet--bl { bottom: 0; left: var(--plate-rivet-off); }
.ctd-plate__rivet--br { bottom: 0; right: var(--plate-rivet-off); }
/* FLUID, not stepped (2026-09-05, owner: "if we can make it expand/contract
   depending on the size, that would be good"). --plate-fs runs from .ctd-h2's
   mobile size to a step under its desktop one, so the plate is a plaque around
   a title at every width instead of a slab with a label on it at one width and
   a wrapped headline at another. It is the only thing here that needed a
   media query, and it no longer has one. */
/* The plaque is a nameplate, so it is lettered like one: --font-varsity is the
   face the real base nameplates are printed in. Tracking drops from 0.08em to
   0.04em — Varsity is a wider fit than Oswald and the old value pushed the
   longest label ("How Blitz Bases work") past the plate. */
.ctd-plate__title {
  font-family: var(--font-varsity); font-size: var(--plate-fs); font-weight: 600;
  line-height: var(--lh-tight); letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--text); text-shadow: var(--fx-plate-stamp); text-wrap: balance;
}

/* ---------- border trace ----------
   A conic sweep masked down to the 1px border. Runs only while hovered or
   focused, so nothing loops on a resting page. */
.ctd-trace { position: relative; }
.ctd-trace::after {
  content: ""; position: absolute; inset: 0; z-index: 3;
  border-radius: inherit; padding: 1px; pointer-events: none;
  background: var(--fx-trace);
  mask: var(--fx-ring-mask); -webkit-mask: var(--fx-ring-mask);
  mask-composite: exclude; -webkit-mask-composite: xor;
  opacity: 0; transition: opacity var(--t-base) var(--ease);
}
.ctd-trace:hover::after, .ctd-trace:focus-within::after {
  opacity: 1;
  animation: ctd-trace-spin var(--t-trace) linear infinite;
}
@keyframes ctd-trace-spin { to { --ctd-trace-angle: 360deg; } }

/* ---------- lit section edge ----------
   Replaces the rivet seam, which read as a row of little bones rather than
   as hardware. Now the top edge of an alt section is a hairline that glows
   brightest in the middle, with a highlight travelling along it. */
.ctd-edge { position: relative; }
.ctd-edge::before,
.ctd-edge::after {
  content: ""; position: absolute; left: 0; right: 0; height: 1px;
  pointer-events: none;
  background-image: var(--fx-shimmer), var(--fx-edge-line);
  background-size: 34% 100%, 100% 100%;
  background-repeat: no-repeat, no-repeat;
  animation: ctd-shimmer var(--t-shimmer) linear infinite;
}
.ctd-edge::before { top: -1px; }
/* The bottom edge runs the other way, so the two do not look like a convoy. */
.ctd-edge::after { bottom: -1px; animation-direction: reverse; animation-delay: calc(var(--t-shimmer) / -3); }

@keyframes ctd-shimmer {
  from { background-position: -40% 0, 0 0; }
  to   { background-position: 140% 0, 0 0; }
}

/* ---------- the moon breathes ---------- */
.ctd-home__hero::before { animation: ctd-breathe var(--t-breathe) var(--ease) infinite alternate; }
@keyframes ctd-breathe {
  from { opacity: 0.75; transform: scale(1); }
  to   { opacity: 1;    transform: scale(1.06); }
}

/* ---------- and so does the violet half of a headline ----------
   The halo breathes on the same clock as the moon, because two ambient
   things drifting at different rates read as two effects rather than as
   one lit room. Display type only: this is a text-shadow on 40px+ Oswald,
   never on reading copy. --fx-glow-none is the resting end of the pulse. */
.ctd-h1 .alt { animation: ctd-halo var(--t-breathe) var(--ease) infinite alternate; }
@keyframes ctd-halo {
  from { text-shadow: var(--fx-glow-none); }
  to   { text-shadow: var(--fx-glow-headline); }
}

/* ---------- scroll reveal ----------
   js/ctd-reveal.js marks anything below the fold at load with `.is-armed`,
   then adds `.is-in` as it arrives. Nothing is hidden by CSS alone, so a
   JS-off visitor and everything already on screen render normally — there is
   no flash of hidden content and no content that can get stuck invisible. */
.ctd-reveal.is-armed {
  opacity: 0;
  transform: translateY(14px);
}
.ctd-reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--t-reveal) var(--ease-out), transform var(--t-reveal) var(--ease-out);
  transition-delay: calc(var(--ctd-i, 0) * 70ms);
}

/* ---------- reduced motion ----------
   Everything ambient stops dead; everything held back is simply shown. The
   reveal in particular must fall back to "visible", never to "hidden". */
@media (prefers-reduced-motion: reduce) {
  .ctd-orn__glint, .ctd-orn__ring, .ctd-home__hero::before,
  .ctd-wall--torchlit::after,
  .ctd-orn__chain-run,
  .ctd-trace:hover::after,
  .ctd-trace:focus-within::after, .ctd-rule__line,
  .ctd-edge::before, .ctd-edge::after, .ctd-h1 .alt,
  .ctd-turf__w {
    animation: none;
  }
  /* A stopped ring just hangs straight: rotate(0) is the rattle's rest pose. */
  .ctd-orn__glint { opacity: 0; }
  .ctd-reveal.is-armed, .ctd-reveal.is-in { opacity: 1; transform: none; transition: none; }
}
