/* ==========================================================================
   KodeYak — the weave system
   Chunk 3: the two grounds and what sits on them, ported from the design board
   (docs/weave-system.html) onto the canonical --ky- tokens. The board is the
   proposal; tokens.css is the contract. Never a raw hex here.

   Palette is Folded Light — tokens.css's default, so nothing is set. Both the
   board and tokens.css call it `folded`; Vellum is a different palette (warm
   near-black glass, gold fibre), not another name for this one.
   ========================================================================== */

/* ---- Grounds --------------------------------------------------------------
   One lattice runs under the whole page. Every ground paints it at the same
   spacing, offset by its own DOCUMENT position modulo the major step, so a
   strand entering a panel leaves it still in register. --ky-lattice-x/y are
   written per ground by src/js/lattice.js, in WHOLE PIXELS — a fractional
   background-position antialiases a 1px rule across two rows and reads as a
   doubled line, visible only while scrolling.

   GROUNDS NEVER CARRY A TRANSFORM. A transformed element becomes the
   containing block for its own background, so a ground with parallax anchors
   its lattice to itself and silently drops out of register with the page.
   Parallax belongs to marks, leaves and images.                              */

.ky-paper {
  /* Lattice strength. Dialled down from the board's .20 thread because the
     lattice runs UNDER body copy: where a glyph crosses an intersection, two
     major lines and two minor ones stack, and at .20 that pixel measured
     3.83:1 against muted ink — below the AA floor the system holds. It is only
     a few pixels per crossing, but "a few pixels" is exactly where a stem
     lands. Raise it and re-measure the worst case, not the mean; the mean was
     always fine (7.6:1) and hides this completely. */
  --ky-lattice-major: .13;
  --ky-lattice-minor: .04;

  position: relative;
  isolation: isolate;
  background-color: var(--ky-bg);
  background-image:
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-ink), var(--ky-lattice-minor)) 0 1px, transparent 1px var(--ky-grid-minor)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-ink), var(--ky-lattice-minor)) 0 1px, transparent 1px var(--ky-grid-minor)),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-thread), var(--ky-lattice-major)) 0 1px, transparent 1px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-thread), var(--ky-lattice-major)) 0 1px, transparent 1px var(--ky-grid-major));
  background-position:
    var(--ky-lattice-x) var(--ky-lattice-y), var(--ky-lattice-x) var(--ky-lattice-y),
    var(--ky-lattice-x) var(--ky-lattice-y), var(--ky-lattice-x) var(--ky-lattice-y);
  /* ONE TILE = ONE PERIOD. Without this the register is a near-miss rather than
     a miss, which is why it survived so long. `background-size: auto` makes each
     gradient's tile the size of the ELEMENT, and a repeating gradient restarts
     from its own tile edge — so unless the element is a whole number of periods
     tall, the handover between tiles is a seam and every line after it is out of
     phase. .ky-paper was 2142px tall: 2142 % 120 = 102, and the first two major
     horizontals measured 101px apart (ground y 17 and 118) instead of 120.
     Found 2026-08-04 while porting the bloom, which repaints this same lattice —
     a seam here and none there would have put the lit strand beside the printed
     one near the top of the page. Full explanation in the bloom section below. */
  background-size:
    var(--ky-grid-minor) 100%, 100% var(--ky-grid-minor),
    var(--ky-grid-major) 100%, 100% var(--ky-grid-major);
}

/* Glass: the same lattice, but the major lines are lit rather than printed.
   .ky-glass (tokens.css) flips the semantic colour tokens; this adds the
   substrate. */
.ky-glass-ground {
  position: relative;
  isolation: isolate;
  background-color: var(--ky-glass-900);
  background-image:
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-gridlit), .15) 0 1px, transparent 1px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-gridlit), .15) 0 1px, transparent 1px var(--ky-grid-major)),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-bone), .032) 0 1px, transparent 1px var(--ky-grid-minor)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-bone), .032) 0 1px, transparent 1px var(--ky-grid-minor)),
    radial-gradient(130% 100% at 0% 0%, rgba(var(--ky-rgb-bone), .09), transparent 58%),
    linear-gradient(158deg, var(--ky-glass-grad-a) 0%, var(--ky-glass-grad-b) 46%, var(--ky-glass-grad-c) 100%);
  background-position:
    var(--ky-lattice-x) var(--ky-lattice-y), var(--ky-lattice-x) var(--ky-lattice-y),
    var(--ky-lattice-x) var(--ky-lattice-y), var(--ky-lattice-x) var(--ky-lattice-y),
    0 0, 0 0;
  /* One tile = one period — see .ky-paper above. The wash and the base gradient
     are meant to span the whole surface, so they stay auto and must not tile. */
  background-repeat: repeat, repeat, repeat, repeat, no-repeat, no-repeat;
  background-size:
    var(--ky-grid-major) 100%, 100% var(--ky-grid-major),
    var(--ky-grid-minor) 100%, 100% var(--ky-grid-minor),
    auto, auto;
}

/* ---- The cursor bloom -----------------------------------------------------
   The lit disc. Each ground repaints its OWN lattice a second time, bright,
   and masks that copy to a soft circle under the pointer — so the fibre reads
   as conducting where the cursor touches it rather than as a highlight drawn
   on top of the page.

   Four things carry the whole effect, and each is doing real work:

   (a) THE BLOOM BELONGS TO THE SUBSTRATE. Every layer is a `z-index: -1`
       pseudo-element of the ground itself: above that ground's own background,
       below every word, button and mark sitting on it. A strand therefore
       passes UNDER a headline and can never cross one. This is why tokens.css
       makes `body` keep painting its own background — a negative-z-index child
       blends against its stacking context's background, and if that background
       had propagated to the canvas instead there would be nothing to blend
       against.

   (b) THE LIT LATTICE REUSES --ky-lattice-x/y, so it lands exactly on the
       printed one. Only the MASK is element-relative, which is the other pair
       of values lattice.js supplies (--ky-bloom-x/y). Get this wrong and the
       lit strand sits a few pixels off the dim one, which reads as a doubled
       line rather than as the same fibre coming alive.

   (c) BLEND MODE DOES THE GROUND DETECTION FOR FREE. `screen` is invisible on
       light paper and incandescent on near-black glass; `color` tints without
       touching luminosity. So there is no per-element JS, no region maths and
       no "which ground is the cursor over" test — one pointer listener for the
       whole page, and each ground responds in its own idiom. It also means
       grounds may overlap freely: the panel inside .ky-paper lights as glass
       while the paper around it lights as paper, with no coordination.

   (d) THE HALO IS FAKED WITH OFFSET COMPANIONS, not with `filter: blur()`.
       Each layer draws the crisp 1px strand plus wider, fainter runs shifted
       to sit centred behind it. A per-frame blur on a full-height layer is the
       one thing here that would actually cost something.

   Contrast: the lit lattice runs under body copy exactly as the printed one
   does, so the same measure-the-worst-case rule applies — see the note on
   --ky-lattice-major above, and the measured figures at the end of this
   section. The luminance layer is the only one that can move a contrast ratio;
   the hue layer preserves the ground's luminosity by construction.            */

/* WHICH SURFACES BLOOM, and why .ky-panel is not one of them.
   The bloom lights a ground; it does not light every glass thing on the page.
   Three surfaces are deliberately excluded:

     .ky-panel     A small card that is ENTIRELY body copy. Measured below —
                   it is a contrast failure, not a close call.
     .ky-screen    The hero display panel. No lattice to light: it is a
                   backdrop-filtered plate over artwork, and its glow is the
                   artwork's own.
     .masthead     Same, and it is 60px tall.

   The rule that falls out of this: THE BLOOM FOLLOWS THE LATTICE. Where a
   surface paints no lattice there is nothing to conduct, so adding a disc there
   would be a highlight drawn on the page rather than the substrate coming
   alive — which is the one thing this effect is not.                          */

/* THE LAYER IS A SMALL BOX THAT TRAVELS — it is NOT the size of the ground.
   This is the single most important thing about the implementation, and it is a
   performance constraint with a correctness tail.

   The obvious build is `inset: 0` with the disc punched out by a moving mask.
   That was the first version, and it is unusable on a phone: `.ky-paper` runs the
   whole page, so each layer became a ~390×5000px paint target that the browser
   re-rasterised on every pointer move to show a ~300px disc. Measured on a 6×
   throttled CPU during a drag-scroll, 22–28% of frames blew past 33ms. Clamping
   the layer's height to 900px took that to 0.0% — identical to the bloom being
   switched off. Blend mode and the mask were both exonerated in the same run
   (20.8% and 27.3%, i.e. unchanged), so AREA was the entire cost.

   So the box is 2R square and positioned by the pointer, the mask is a STATIC
   circle at its centre, and the lattice is pulled back by the box's own offset
   to stay document-anchored. Consequences, all of them load-bearing:

   (a) --ky-bloom-x/y NOW FEED A background-position, so they must be WHOLE
       PIXELS. They used to be deliberately sub-pixel, which was fine when they
       only positioned a soft radial. Fractional now means a fractional offset on
       1px lattice rules — the doubled-line artefact. lattice.js rounds them.
   (b) THE GROUND MUST CLIP. A box that travels can overhang its ground; with
       `inset: 0` the layer could not. Only the top edge actually shows (paper's
       sides and bottom leave the viewport), where the amber lattice would appear
       over the hero rail's glass. Hence `overflow: clip` on the grounds below —
       `clip`, not `hidden`, so no scroll container and no BFC side effects.
   (c) Absolute positioning resolves against the containing block's PADDING box
       while --ky-bloom-x/y are measured from the border box. The grounds have no
       padding, which is why these agree. Give a ground padding and the disc will
       sit off by that much.                                                     */

.ky-paper,
.ky-glass-ground { overflow: clip; }

.ky-paper::before,
.ky-paper::after,
.ky-glass-ground::after {
  content: "";
  position: absolute;
  z-index: -1;
  pointer-events: none;
  /* Parked far off by default so nothing is lit until the pointer is placed —
     which on touch is not until first contact. */
  --gb-x: var(--ky-bloom-x, -9999px);
  --gb-y: var(--ky-bloom-y, -9999px);
  width: calc(var(--gb-r) * 2);
  height: calc(var(--gb-r) * 2);
  left: calc(var(--gb-x) - var(--gb-r));
  top: calc(var(--gb-y) - var(--gb-r));
  /* Pull the lattice back by the box's own offset so it stays anchored to the
     document grid rather than travelling with the box. Everything periodic is
     mod --ky-grid-major, so any congruent value works. */
  --gb-ox: calc(var(--ky-lattice-x) - var(--gb-x) + var(--gb-r));
  --gb-oy: calc(var(--ky-lattice-y) - var(--gb-y) + var(--gb-r));
  /* EVERY LATTICE TILE IS EXACTLY ONE GRID PERIOD. This is not an optimisation,
     it is the difference between a lattice and a nearly-lattice.

     With the default `background-size: auto`, a gradient's tile is the size of
     the whole layer box. `repeating-linear-gradient` then repeats WITHIN that
     tile from the tile's own edge — so as soon as the box is not a whole number
     of periods, the point where one tile hands over to the next is a SEAM, and
     the lines restart out of phase. Measured: with a 460px box offset -250px,
     the majors ran 240, 360, then 460, 580 instead of 480, 600 — a 100px gap
     once, then everything after it 20px wrong. It surfaced as the bloom going
     dim and blotchy rather than as anything that looked like a grid bug, because
     the lit strand had simply stopped landing on the printed one.

     Sizing each tile to --ky-grid-major makes the tiling seamless at ANY box
     size, so the box is free to be whatever the paint area wants and the phase
     comes only from background-position. The wash is a single radial, so it does
     not tile at all. */
  background-repeat: repeat, repeat, repeat, repeat, repeat, repeat, no-repeat;
  background-size:
    var(--ky-grid-major) 100%, 100% var(--ky-grid-major),
    var(--ky-grid-major) 100%, 100% var(--ky-grid-major),
    var(--ky-grid-major) 100%, 100% var(--ky-grid-major),
    auto;
  /* Lights INSTANTLY, fades out SLOWLY — and the asymmetry is the whole reason
     this is a class rather than a transitioned custom property. Contact should
     register the moment it happens; a symmetric transition would make the second
     tap take the full settle duration to appear. The base state carries no
     transition at all, so dropping .ky-bloom-off snaps straight back to lit. */
  opacity: 1;
  transition: opacity 0ms;
  /* The one switch that turns the bloom off — reduced motion, the OS contrast
     preference, or the reader's own toggle. All three conditions live in
     tokens.css so this stays a single declaration per layer. */
  display: var(--ky-bloom-display, block);
}

/* The finger has lifted, so the contact point genuinely stopped existing. Fading
   beats snapping it off, and beats leaving it lit under nothing. Touch only — a
   cursor is always somewhere, so lattice.js never puts this class on a
   mouse-driven page. */
.ky-bloom-off::before,
.ky-bloom-off::after {
  opacity: 0;
  transition: opacity var(--ky-duration-settle) var(--ky-ease);
}

/* The masks are STATIC — a circle of radius R at the centre of a 2R box, so it
   exactly inscribes the layer. Nothing here changes as the pointer moves; only
   the box's position does.

   Glass: wide, and falling off roughly linearly — light travels in a near-black
   cavity. */
.ky-glass-ground::after {
  --gb-r: var(--ky-bloom);
  -webkit-mask-image: radial-gradient(circle var(--gb-r) at 50% 50%, #000 0%, rgba(0,0,0,.94) 38%, rgba(0,0,0,.52) 60%, rgba(0,0,0,.16) 76%, transparent 88%);
          mask-image: radial-gradient(circle var(--gb-r) at 50% 50%, #000 0%, rgba(0,0,0,.94) 38%, rgba(0,0,0,.52) 60%, rgba(0,0,0,.16) 76%, transparent 88%);
}

/* Paper absorbs. Its disc is smaller (--ky-bloom-paper) and its mask falls off
   steeply rather than linearly — half gone by 22%, almost nothing past 60% —
   which is roughly how light behaves a few millimetres into a scattering
   sheet. The steepness is the difference between "a fibre under the paper" and
   "a torch held against it". */
.ky-paper::before,
.ky-paper::after {
  --gb-r: var(--ky-bloom-paper);
  -webkit-mask-image: radial-gradient(circle var(--gb-r) at 50% 50%, #000 0%, rgba(0,0,0,.50) 22%, rgba(0,0,0,.20) 42%, rgba(0,0,0,.05) 64%, transparent 82%);
          mask-image: radial-gradient(circle var(--gb-r) at 50% 50%, #000 0%, rgba(0,0,0,.50) 22%, rgba(0,0,0,.20) 42%, rgba(0,0,0,.05) 64%, transparent 82%);
}

/* Paper, layer 1 of 2: HUE.
   `mix-blend-mode: color` takes hue and saturation from this layer and keeps
   the paper's own luminosity, so it tints without bleaching — and, usefully,
   without moving a contrast ratio. This is the layer that makes the lit strand
   read as amber rather than as white: `screen` alone cannot, because on a
   ~88%-lightness ground every channel it touches runs towards 1 and the result
   is a pale grey strand.

   The companions are smooth ramps, not stacked bars — peak on the line,
   nothing 22px out — and each tile is shifted back by its own half-width so
   the peak lands on the major line rather than beside it. */
.ky-paper::before {
  mix-blend-mode: color;
  background-image:
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-hue), 0) 0, rgba(var(--ky-rgb-hue), .20) 22px, rgba(var(--ky-rgb-hue), 0) 44px, rgba(var(--ky-rgb-hue), 0) var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-hue), 0) 0, rgba(var(--ky-rgb-hue), .20) 22px, rgba(var(--ky-rgb-hue), 0) 44px, rgba(var(--ky-rgb-hue), 0) var(--ky-grid-major)),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-hue), 0) 0, rgba(var(--ky-rgb-hue), .46) 7px,  rgba(var(--ky-rgb-hue), 0) 14px, rgba(var(--ky-rgb-hue), 0) var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-hue), 0) 0, rgba(var(--ky-rgb-hue), .46) 7px,  rgba(var(--ky-rgb-hue), 0) 14px, rgba(var(--ky-rgb-hue), 0) var(--ky-grid-major)),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-hue), .56) 0 1px, transparent 1px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-hue), .56) 0 1px, transparent 1px var(--ky-grid-major)),
    radial-gradient(circle var(--gb-r) at 50% 50%, rgba(var(--ky-rgb-hue), .13), transparent 70%);
  background-position:
    calc(var(--gb-ox) - 22px) var(--gb-oy),
    var(--gb-ox) calc(var(--gb-oy) - 22px),
    calc(var(--gb-ox) - 7px)  var(--gb-oy),
    var(--gb-ox) calc(var(--gb-oy) - 7px),
    var(--gb-ox) var(--gb-oy),
    var(--gb-ox) var(--gb-oy),
    0 0;
}

/* Paper, layer 2 of 2: LUMINANCE.
   Only enough lift to overrun the printed copper thread and suggest a source
   underneath. Kept very low and TINTED — never white — so the hue layer stays
   in charge: a fibre seen through paper is a faint coloured patch, not a
   bright one.

   The board (docs/weave-system.html) has this rule TWICE, and its second copy
   overrides the first with hard-coded Weave literals — mint 143,233,200 and
   160,240,210. Only the tokenised version is ported. Reinstating the literals
   would light Folded Light's paper mint green while every other surface stayed
   amber, and would silently defeat a palette swap, which is the one thing the
   channel tokens exist to make possible. */
.ky-paper::after {
  mix-blend-mode: screen;
  background-image:
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-soft), 0) 0, rgba(var(--ky-rgb-soft), .045) 22px, rgba(var(--ky-rgb-soft), 0) 44px, rgba(var(--ky-rgb-soft), 0) var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-soft), 0) 0, rgba(var(--ky-rgb-soft), .045) 22px, rgba(var(--ky-rgb-soft), 0) 44px, rgba(var(--ky-rgb-soft), 0) var(--ky-grid-major)),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-soft), 0) 0, rgba(var(--ky-rgb-soft), .12) 7px,  rgba(var(--ky-rgb-soft), 0) 14px, rgba(var(--ky-rgb-soft), 0) var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-soft), 0) 0, rgba(var(--ky-rgb-soft), .12) 7px,  rgba(var(--ky-rgb-soft), 0) 14px, rgba(var(--ky-rgb-soft), 0) var(--ky-grid-major)),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-core), .20) 0 1px, transparent 1px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-core), .20) 0 1px, transparent 1px var(--ky-grid-major)),
    radial-gradient(circle var(--gb-r) at 50% 50%, rgba(var(--ky-rgb-soft), .022), transparent 74%);
  background-position:
    calc(var(--gb-ox) - 22px) var(--gb-oy),
    var(--gb-ox) calc(var(--gb-oy) - 22px),
    calc(var(--gb-ox) - 7px)  var(--gb-oy),
    var(--gb-ox) calc(var(--gb-oy) - 7px),
    var(--gb-ox) var(--gb-oy),
    var(--gb-ox) var(--gb-oy),
    0 0;
}

/* Glass: one layer, and the strand goes incandescent. --ky-rgb-lit is chosen to
   blow towards warm white at full alpha, which is what a filament actually does;
   the 5px and 13px fibre companions behind it are the halo.

   The ambient wash is deliberately WIDER than the mask disc (the board typed a
   bare 320px against a 260px bloom) so that it is still climbing where the mask
   cuts it off — a wash that peaked inside the disc would show its own edge.
   Expressed as a ratio, not a second literal, so it stays wide at any bloom
   size and collapses to nothing when reduced-motion zeroes --ky-bloom. */
.ky-glass-ground::after {
  --gb-wash: calc(var(--ky-bloom) * 1.25);
  /* Broken out because these four are the contrast controls, not decoration —
     see the measured figures below before touching them. */
  --gb-core: 1;     /* the 1px filament itself */
  --gb-halo: .34;   /* 5px companion  */
  --gb-glow: .16;   /* 13px companion */
  --gb-amb:  .17;   /* ambient wash across the disc */
  mix-blend-mode: screen;
  background-image:
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-lit), var(--gb-core)) 0 1px, transparent 1px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-lit), var(--gb-core)) 0 1px, transparent 1px var(--ky-grid-major)),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-fiber), var(--gb-halo)) 0 5px, transparent 5px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-fiber), var(--gb-halo)) 0 5px, transparent 5px var(--ky-grid-major)),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-fiber), var(--gb-glow)) 0 13px, transparent 13px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-fiber), var(--gb-glow)) 0 13px, transparent 13px var(--ky-grid-major)),
    radial-gradient(circle var(--gb-wash) at 50% 50%, rgba(var(--ky-rgb-fiber), var(--gb-amb)), transparent 70%);
  background-position:
    var(--gb-ox) var(--gb-oy),
    var(--gb-ox) var(--gb-oy),
    calc(var(--gb-ox) - 2px) var(--gb-oy),
    var(--gb-ox) calc(var(--gb-oy) - 2px),
    calc(var(--gb-ox) - 6px) var(--gb-oy),
    var(--gb-ox) calc(var(--gb-oy) - 6px),
    0 0;
}

/* ---- Touch ----------------------------------------------------------------
   THE BLOOM IS NOT MOUSE-ONLY. It lights where a finger makes contact, and it
   follows that finger through a scroll drag. This used to be a `(hover: none)`
   cutout; that was removed on 2026-08-04.

   What makes it work is a quirk of the event model, verified rather than assumed:
   a touch that turns into a scroll fires `pointerdown`, then about TWO
   `pointermove`s, then `pointercancel` — after which the pointer events stop for
   the rest of the gesture. `touchmove`, however, keeps firing the whole way. So
   the driver listens to both, and touch is tracked by `touchmove`.

   The pleasing part is free: during a drag-scroll the content follows the finger
   1:1, so tracking the finger in VIEWPORT space is identical to sticking to the
   point of content under it. The lit spot stays on the piece of lattice the
   finger grabbed, with no special-casing anywhere.

   `touch-action` is deliberately left alone. The bloom is a passive listener on
   a gesture the browser still owns — it must never be allowed to eat a scroll.  */

/* Reduced motion, the OS contrast preference and the reader's toggle all take the
   bloom out entirely, via --ky-bloom-display on the layers above. The conditions
   are in tokens.css. Belt and braces on top of that: tokens.css also zeroes
   --ky-bloom/--ky-bloom-paper under reduced motion, and lattice.js never starts
   the pointer pass — but taking the layers out of the compositor is the cheap and
   unambiguous part, so that is what actually carries it. */

/* ---- Bloom contrast: the measured record ----------------------------------
   Worst-pixel figures, taken on the rendered page in Chrome with a real cursor
   parked at the centre of the block named, NOT from a blend-mode model. Method:
   screenshot with the copy visible and again with it `visibility: hidden`, diff
   the two to get exactly the pixels a glyph covers, then take the LOWEST ratio
   of the element's colour against the ground beneath those pixels. The mean is
   useless here and flatters badly — it is a 1px line every 120px, so the mean
   barely moves while the pixel under a stem does all the damage.

   PAPER — safe by construction, and confirmed:

     block                  at rest    bloom centred on it
     .ky-lede   (19px)       8.15:1          8.15:1
     .ky-muted  (17px)       5.55:1          5.55:1
     .ky-h2     (43px)       6.95:1          6.95:1
     .ky-eyebrow(11px)       6.23:1          6.23:1

   Measured on BARE paper — the "How we do it" block, where the lattice actually
   shows through. Re-measured 2026-08-05 when .ky-lede moved from --ky-text-muted
   to --ky-text for hierarchy; it was 5.55:1 as ink-600, and .ky-muted is now the
   block sitting at that figure.

   .ky-leaf, same day, once it went vellum (55% — see the rule for why):

     block                  opaque leaf    vellum leaf
     .ky-h3     (21.6px)      13.13:1        11.03:1
     .ky-lede   (19px)        13.13:1        10.80:1
     .ky-muted  (17px)         8.95:1         6.82:1

   The card used to be the EASIEST surface on the page because it was opaque and
   no lattice reached the glyphs; it is now a middle case — still well clear of
   AA, but no longer a free pass. Bare paper above remains the worst case and is
   the block to re-check first. Anything that lowers the leaf's alpha walks these
   three towards the bare-paper column, so re-measure rather than assuming the
   headroom is still there.

   Not a rounding coincidence — the worst pixel does not move at all. Paper
   text is dark ink, and NEITHER paper layer can darken the ground behind it:
   `screen` only lifts, and `color` takes hue and saturation while leaving
   luminosity to the backdrop. The bloom therefore lightens the lattice
   intersections it covers, which RAISES contrast under dark ink; the page-wide
   worst pixel simply stays at an intersection outside the disc. So the paper
   bloom can be tuned on looks alone. That is a property of dark-on-light, not
   a property of this palette — a palette with light text on paper would invert
   the whole argument and need re-measuring from scratch.

   GLASS — the opposite direction, and it fails hard. Frost body copy
   (--ky-text-muted #B7C8C6, 9.28:1 at rest on Abyss) over the lit strand,
   swept from the board's values downwards:

     core/halo/glow/amb        .ky-muted   .ky-tech-display
     1 / .34 / .16 / .17  ←board   1.00:1        1.01:1
     .50 / .20 / .10 / .12        1.20:1        1.82:1
     .26 / .13 / .07 / .09        2.00:1        2.74:1
     .13 / .08 / .05 / .06        3.42:1        4.10:1
     .10 / .06 / .04 / .05        3.62:1        4.72:1

   1.00:1 is the strand and the text at the same luminance — the glyph is gone,
   not merely low-contrast. And the sweep is the real finding: even at a TENTH
   of the board's values the copy is still under AA, by which point the core is
   dimmer than the printed line it is supposed to be lighting. There is no
   setting that is both visible and safe, because on glass the text is LIGHT and
   brightening the substrate behind it necessarily walks the ground up towards
   the text's own luminance. Dialling the numbers is not a fix here; it is the
   thing that does not work.

   HENCE THE RULE: a glass ground that blooms carries no body copy directly.
   Copy on glass sits on a panel or a leaf, both of which paint an opaque
   background and so keep the strand behind them — which is the stronger reading
   of "a strand passes under a headline, it never crosses one" that the paper
   side gets for free from the blend modes. If a paragraph is ever set straight
   onto .ky-glass-ground, it will measure ~1:1 under the cursor and the fix is
   to give it a surface, not to dim the bloom.                                 */

/* ---- The chamfer ----------------------------------------------------------
   The HUD cut, shared with the masthead: a corner sliced off at 45° rather
   than rounded. Driven by one variable so every plate on the page cuts to the
   same depth. clip-path rather than a border, because a border cannot follow
   a diagonal and would be clipped away anyway.                               */

.ky-plate {
  --ky-cut: 22px;
  position: relative;
  isolation: isolate;
}

/* Bottom-right cut — the default, matching the hero plate and the glass card. */
.ky-plate--br { clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--ky-cut)), calc(100% - var(--ky-cut)) 100%, 0 100%); }
/* Top-right cut. */
.ky-plate--tr { clip-path: polygon(0 0, calc(100% - var(--ky-cut)) 0, 100% var(--ky-cut), 100% 100%, 0 100%); }

/* ---- Glass panel ----------------------------------------------------------
   A tech surface set into the paper. The lit top-left edge is the board's
   reflective ring; on a chamfered plate it has to be drawn as a clipped layer
   rather than a border, for the same reason the masthead's is.               */

.ky-panel {
  --ky-cut: 26px;
  /* Hoisted, because the bloom's ring clip and its falloff ramp below both have
     to agree with it exactly. Two places deriving the same inset independently is
     how a ring ends up a pixel off the copy it is supposed to sit outside.

     Deliberately larger than a padding value would otherwise need to be: this
     band is also the runway the bloom's falloff fades across, so it has to be
     wide enough for the fade to read as a fade rather than as a soft edge on a
     hard shape. 48px at desktop, floored at 32px so a phone keeps a usable band. */
  --ky-panel-pad: clamp(var(--ky-space-8), 4vw, var(--ky-space-12));
  position: relative;
  isolation: isolate;
  padding: var(--ky-panel-pad);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--ky-cut)), calc(100% - var(--ky-cut)) 100%, 0 100%);
  background-color: var(--ky-glass-900);
  /* The first layer is the CURSOR WASH, and it is the one part of the bloom that
     is allowed over the copy — because it is the only part that costs almost
     nothing. Measured with the cursor dead centre of the panel's text: the worst
     pixel goes 7.10 → 6.32:1 on body copy and 4.99 → 4.96:1 on the eyebrow, both
     still clear of AA. A broad soft gradient lifts the ground evenly, whereas it
     is the 1px and 5px STRANDS that take text to 1.00:1. So the strands are
     ramped out before they reach a word (see .ky-panel::after) and the wash is
     not, which is what stops the middle of the panel — most of its area, and
     where a reader's cursor actually sits — from being inert.

     It lives on the panel's own background rather than on the bloom layer for
     exactly one reason: the bloom layer is masked, and the mask is what removes
     the strands. A wash on that layer would be removed with them.

     Radius tracks --ky-bloom, so reduced motion zeroing that token also collapses
     this to nothing. --ky-bloom-wash is zeroed by every condition that turns the
     bloom off; both live in tokens.css. */
  background-image:
    radial-gradient(circle calc(var(--ky-bloom) * 1.25) at var(--ky-bloom-x, -9999px) var(--ky-bloom-y, -9999px), rgba(var(--ky-rgb-fiber), var(--ky-bloom-wash, .14)), transparent 72%),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-gridlit), .15) 0 1px, transparent 1px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-gridlit), .15) 0 1px, transparent 1px var(--ky-grid-major)),
    radial-gradient(130% 100% at 0% 0%, rgba(var(--ky-rgb-bone), .11), transparent 58%),
    linear-gradient(158deg, var(--ky-glass-grad-a) 0%, var(--ky-glass-grad-b) 46%, var(--ky-glass-grad-c) 100%);
  background-position:
    0 0,
    var(--ky-lattice-x) var(--ky-lattice-y), var(--ky-lattice-x) var(--ky-lattice-y), 0 0, 0 0;
  /* One tile = one period — see .ky-paper above. A panel is small and its height
     is content-driven, so it was never going to be a whole number of periods:
     345px tall meant a seam 105px down. This is the surface where the premise is
     most visible, because a strand is supposed to enter it and leave it in line
     with the paper on either side. */
  background-repeat: no-repeat, repeat, repeat, no-repeat, no-repeat;
  background-size:
    auto,
    var(--ky-grid-major) 100%, 100% var(--ky-grid-major),
    auto, auto;
  box-shadow: 0 24px 48px -30px rgba(var(--ky-rgb-void), .8);
}

/* ---- The panel's bloom ----------------------------------------------------
   The strand crosses the panel and passes BEHIND the copy, then leaves the other
   side in register. This is the same rule the glass grounds follow — "copy on
   glass sits on something opaque, so the strand stays behind it" — applied one
   level in, with the panel's own padding as the plate.

   WHY A RING RATHER THAN A DIMMER BLOOM. Frost body copy over the lit strand
   measures 1.00:1, and that is not a number that can be tuned away: removing the
   1px filament entirely still leaves 1.90:1, because the 5px and 13px soft
   companions cover far more of each glyph than the core does. Dimming the whole
   thing to a tenth was 3.62:1 and by then the core is fainter than the printed
   line it is meant to be lighting. So the strand either crosses the type or it
   does not, and here it does not — which costs nothing, because a strand
   disappearing behind a surface and reappearing aligned on the far side is a
   better read than one drawn across a word.

   THIS ONE USES inset: 0 AND A MOVING MASK, unlike the grounds, which use a
   small travelling box. That is deliberate and not an inconsistency: the grounds
   are page-tall, so a full-size layer was a ~390×5000px repaint per pointer move
   (22–28% of frames over 33ms). A panel is ~460×345. At that size the simple
   form costs nothing and buys a ring clip that can be written in the panel's own
   coordinates instead of the travelling box's.                                */
.ky-panel::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  --gb-r: var(--ky-bloom);
  --gb-wash: calc(var(--gb-r) * 1.25);
  --gb-core: 1;     /* the 1px filament itself */
  --gb-halo: .34;   /* 5px companion  */
  --gb-glow: .16;   /* 13px companion */
  --gb-amb:  .17;   /* ambient wash across the disc */
  mix-blend-mode: screen;
  /* THE FALLOFF. Three mask layers, and the composite is what makes it work:
       disc          the bloom itself, centred on the pointer
       ∩ ( H ∪ V )   full strength at the panel's edges, ramping to NOTHING by
                     the time it reaches the copy
     H and V are one-dimensional edge ramps; their UNION is opaque anywhere near
     the perimeter and zero over the text field, which is the rectangular
     soft-edged hole this needs and which no single gradient can express — an
     ellipse big enough to cover a text rect is bigger than the panel, so it would
     erase the bloom entirely.

     The ramps are pure black-to-transparent, so they interpolate in premultiplied
     space without a grey fringe.

     Because the ramp already reaches zero exactly where the clip-path hole
     begins, THE CLIP EDGE IS INVISIBLE — the bloom has faded out before it gets
     there. The clip stays anyway as the hard floor: if `mask-composite` is ever
     ignored the layers default to `add`, which is a union, which would put the
     bloom over the type. The clip means that failure mode is "less glow", never
     "unreadable text". Only the standard property is declared, deliberately —
     setting -webkit-mask-composite alongside it makes the two fight in Chrome,
     and every browser that supports mask-image at all supports this. */
  --gb-fade: var(--ky-panel-pad);
  -webkit-mask-image:
    radial-gradient(circle var(--gb-r) at var(--ky-bloom-x, -9999px) var(--ky-bloom-y, -9999px), #000 0%, rgba(0,0,0,.94) 38%, rgba(0,0,0,.52) 60%, rgba(0,0,0,.16) 76%, transparent 88%),
    linear-gradient(to right,  #000 0, transparent var(--gb-fade), transparent calc(100% - var(--gb-fade)), #000 100%),
    linear-gradient(to bottom, #000 0, transparent var(--gb-fade), transparent calc(100% - var(--gb-fade)), #000 100%);
          mask-image:
    radial-gradient(circle var(--gb-r) at var(--ky-bloom-x, -9999px) var(--ky-bloom-y, -9999px), #000 0%, rgba(0,0,0,.94) 38%, rgba(0,0,0,.52) 60%, rgba(0,0,0,.16) 76%, transparent 88%),
    linear-gradient(to right,  #000 0, transparent var(--gb-fade), transparent calc(100% - var(--gb-fade)), #000 100%),
    linear-gradient(to bottom, #000 0, transparent var(--gb-fade), transparent calc(100% - var(--gb-fade)), #000 100%);
  mask-composite: intersect, add, add;
  /* The hard floor. A ring: outer rect traced clockwise, inner rect anticlockwise,
     so nonzero winding leaves the middle as a hole. The hole is the content box,
     so the lit layer cannot be painted where words are regardless of what the
     mask does. Safe to clip a pseudo-element — nothing here is focusable, which is
     the one case where clip-path would silently eat the focus ring. */
  clip-path: polygon(
    0 0, 100% 0, 100% 100%, 0 100%, 0 0,
    var(--ky-panel-pad) var(--ky-panel-pad),
    var(--ky-panel-pad) calc(100% - var(--ky-panel-pad)),
    calc(100% - var(--ky-panel-pad)) calc(100% - var(--ky-panel-pad)),
    calc(100% - var(--ky-panel-pad)) var(--ky-panel-pad),
    var(--ky-panel-pad) var(--ky-panel-pad)
  );
  background-image:
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-lit), var(--gb-core)) 0 1px, transparent 1px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-lit), var(--gb-core)) 0 1px, transparent 1px var(--ky-grid-major)),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-fiber), var(--gb-halo)) 0 5px, transparent 5px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-fiber), var(--gb-halo)) 0 5px, transparent 5px var(--ky-grid-major)),
    repeating-linear-gradient(to right,  rgba(var(--ky-rgb-fiber), var(--gb-glow)) 0 13px, transparent 13px var(--ky-grid-major)),
    repeating-linear-gradient(to bottom, rgba(var(--ky-rgb-fiber), var(--gb-glow)) 0 13px, transparent 13px var(--ky-grid-major)),
    radial-gradient(circle var(--gb-wash) at var(--ky-bloom-x, -9999px) var(--ky-bloom-y, -9999px), rgba(var(--ky-rgb-fiber), var(--gb-amb)), transparent 70%);
  /* inset: 0, so the lattice offsets are the ground offsets with no box shift. */
  background-position:
    var(--ky-lattice-x) var(--ky-lattice-y),
    var(--ky-lattice-x) var(--ky-lattice-y),
    calc(var(--ky-lattice-x) - 2px) var(--ky-lattice-y),
    var(--ky-lattice-x) calc(var(--ky-lattice-y) - 2px),
    calc(var(--ky-lattice-x) - 6px) var(--ky-lattice-y),
    var(--ky-lattice-x) calc(var(--ky-lattice-y) - 6px),
    0 0;
  /* One tile = one period — see .ky-paper. */
  background-repeat: repeat, repeat, repeat, repeat, repeat, repeat, no-repeat;
  background-size:
    var(--ky-grid-major) 100%, 100% var(--ky-grid-major),
    var(--ky-grid-major) 100%, 100% var(--ky-grid-major),
    var(--ky-grid-major) 100%, 100% var(--ky-grid-major),
    auto;
  display: var(--ky-bloom-display, block);
}

/* The fibre edge — one amber rule down the lit side. This is the "one accent
   per view" the palette allows, so don't repeat it on every panel. */
.ky-panel--fibre::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: linear-gradient(to bottom,
    var(--ky-fiber-400), rgba(var(--ky-rgb-fiber), .25));
}
.ky-panel > * { position: relative; z-index: 1; }

/* ---- Leaf -----------------------------------------------------------------
   A sheet laid on the paper, not a card: 4px radius, a hairline, and the
   faintest top highlight so it reads as something resting on the ground.     */

/* VELLUM, NOT GLASS — and the difference is that the leaf reveals the lattice
   rather than reprinting it. .ky-panel is opaque and paints its own copy of the
   grid, which is why it needs the ring, the ramps and a hoisted --ky-panel-pad to
   keep strands off the copy. None of that machinery belongs here. The leaf sits
   on .ky-paper, which is `position: relative; isolation: isolate` with its bloom
   at z-index:-1 — so the paint order under this sheet is already ground, then
   printed lattice, then both bloom layers, then the leaf. Giving the background
   an alpha lets all three through UNCHANGED, so register is not reconstructed and
   therefore cannot drift: it is literally the same painted lattice, seen through
   a veil. A strand entering the card is the strand leaving it.

   This is also why the leaf needs no bloom of its own. On paper the bloom can only
   LIFT — `screen` cannot darken and `color` leaves luminosity alone — so under
   dark ink a lit intersection RAISES contrast, and the glass rule about keeping
   strands off the copy simply does not apply. The measured floor here is the
   PRINTED lattice, which is what the sheet's remaining opacity is buying.

   color-mix rather than an rgba() literal, because there is no --ky-rgb-surface
   channel token and adding one would mean touching all three palettes; mixing the
   existing --ky-surface follows a palette swap for free.

   ⚠ THIS LEAF IS NO LONGER A SAFE PLATE ON GLASS. The system's rule is that copy
   on a blooming glass ground sits on a panel or leaf "whose OPAQUE background does
   the same job as the ring" — and opaque is exactly what this one stopped being.
   Nothing is broken today: all three .ky-leaf instances are on paper, and
   .ky-glass-ground still has no consumer at all (docs/STATUS.md). But the first
   time a leaf is dropped onto a blooming glass ground it will pass the strands
   through under LIGHT text, which is the 1.00:1 case measured below — invisible
   glyphs, not merely low contrast. Paper is safe for the opposite reason, and it
   is a property of dark-on-light, not of this class. If a leaf is ever needed on
   glass, give it back a solid background there (.ky-glass .ky-leaf) rather than
   dialling this alpha, which would cost the paper cards the effect entirely. */
.ky-leaf {
  /* 55%, and the number was measured rather than picked. The paper lattice is
     deliberately faint — --ky-lattice-major .13, minor .04, dialled down because
     it runs under body copy — so there is very little signal to transmit, and the
     leaf is LIGHTER than the ground it sits on, which washes out what does get
     through. At 72% the grid was invisible inside the card; 55% reads clearly and
     still holds the sheet apart from the ground. Measured through it: body copy
     8.95 → 6.82:1, and the bloom arrives at 60% of its bare-paper amplitude
     (peak delta 15/255 against 25/255 unveiled — the paper disc is a subtle
     effect to begin with, so proportion matters more than the absolute here). */
  background: color-mix(in srgb, var(--ky-surface) 55%, transparent);
  /* NO backdrop-filter. A blur is the obvious way to make a sheet read as
     scattering vellum rather than tinted glass, and it is self-defeating here:
     the thing being revealed is a 1px rule every 120px, and blur(1.5px) erased
     it — measured, the grid went from clearly visible to gone while the contrast
     figure barely moved, so it fails invisibly. It would also have re-run on
     every pointer move, since the backdrop it filters is the travelling bloom. */
  border: 1px solid var(--ky-border);
  border-radius: var(--ky-radius-leaf);
  /* Hoisted for the same reason --ky-panel-pad is: something inside a leaf may
     need to bleed back out to its edges, and a second copy of this clamp is a
     second thing to keep in step. .ky-demo__screen is the consumer. */
  --ky-leaf-pad: clamp(var(--ky-space-6), 2vw, var(--ky-space-8));
  padding: var(--ky-leaf-pad);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .55),
    0 10px 22px -18px rgba(var(--ky-rgb-ink), .55);
}

/* ---- Type -----------------------------------------------------------------
   Ground decides the face. Serif on paper, sans on glass — the switch IS the
   signal, so never mix them inside one block. Tilt Neon is display-only: it
   ships at a single 400 weight, so anything on glass needing a weight is
   Ubuntu (tokens.css already routes this).                                   */

.ky-display  { font-family: var(--ky-font-heading); font-weight: var(--ky-weight-bold);
               font-size: var(--ky-text-display); line-height: var(--ky-leading-tight);
               letter-spacing: var(--ky-tracking-display); margin: 0; text-wrap: balance; }
/* .ky-h1 and .ky-h2 are the SAME SIZE on purpose, and both take --ky-text-h1.
   On paper the largest heading is the section head; the home page's only h1 is
   the hero's, which is glass and uses .ky-tech-display. So an inner page's title
   sits at the same scale as a section head and is distinguished by the eyebrow
   above it, not by being bigger. Two names for one rule so a page title can be
   marked up as the h1 it is, instead of wearing a class named for the wrong
   level. */
.ky-h1,
.ky-h2       { font-family: var(--ky-font-heading); font-weight: var(--ky-weight-bold);
               font-size: var(--ky-text-h1); line-height: var(--ky-leading-head);
               margin: 0 0 var(--ky-space-4); text-wrap: balance; }
.ky-h3       { font-family: var(--ky-font-heading); font-weight: var(--ky-weight-bold);
               font-size: var(--ky-text-h3); line-height: 1.24; margin: 0 0 var(--ky-space-2); }

/* The tech display face, for glass. Takes the filament glow honestly — it is
   an actual neon face. */
.ky-tech-display {
  font-family: var(--ky-font-tech-display);
  font-weight: var(--ky-weight-regular);
  font-variation-settings: 'XROT' 0, 'YROT' 0;
  font-size: var(--ky-text-tech-display);
  line-height: var(--ky-leading-head);
  letter-spacing: var(--ky-tracking-tech);
  margin: 0;
  text-shadow: 0 0 20px rgba(var(--ky-rgb-fiber), .30);
  /* The only heading that was missing this, and the panel's wider padding is what
     exposed it: a narrower content column broke "AI Help, not AI Hype" as
     "…not AI / Hype", leaving one word stranded. Every other display class
     already balances. */
  text-wrap: balance;
}

/* The lede is the LEAD line, so it carries the primary ink; .ky-muted below it is
   the secondary voice. These two had both been --ky-text-muted, which left a
   19px/17px size step as the only thing separating a summary from its detail —
   about 12%, under the threshold where a reader reads a difference as deliberate
   rather than as an accident. Hierarchy now runs on two channels, colour and
   size, and the pair only ever appears as lede-then-detail (five times, all on
   paper). The direction is forced: the detail cannot go lighter instead, because
   the next step down is --ky-ink-400 at 3.41:1, which tokens.css marks non-text.
   Note the leading is deliberately NOT parallel — 1.55 on the larger line against
   --ky-leading-body 1.62 on the smaller. Larger type wants tighter leading, and
   while the two were the same colour that inversion was quietly working against
   the size cue. */
.ky-lede { font-size: var(--ky-text-body-lg); line-height: 1.55; color: var(--ky-text); max-width: 52ch; }
.ky-muted { color: var(--ky-text-muted); }

/* Eyebrows: the label is where the two worlds tag each other. On paper the
   eyebrow is fibre; on glass it is the cool counter-accent. tokens.css already
   points --ky-text-accent at the right one per ground. */
.ky-eyebrow { display: flex; gap: var(--ky-space-4); align-items: baseline; margin: 0 0 var(--ky-space-4); }
.ky-eyebrow .n { color: var(--ky-text-accent); }
.ky-eyebrow .l { color: var(--ky-text-muted); }

/* ---- Layout ---------------------------------------------------------------
   The 60/40 is a real column rule, not a metaphor — see brand-identity.md §2. */

.ky-wrap { max-width: var(--ky-content-max); margin-inline: auto; padding-inline: var(--ky-gutter); }
.ky-section { padding-block: clamp(var(--ky-space-16), 8vw, var(--ky-space-24)); }
.ky-split { display: grid; gap: clamp(var(--ky-space-6), 3vw, var(--ky-space-12)); align-items: center; }
@media (min-width: 900px) { .ky-split { grid-template-columns: 60fr 40fr; } }
.ky-grid3 { display: grid; gap: var(--ky-space-4); grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
.ky-grid3 > * { min-width: 0; }
.ky-stack > * + * { margin-top: var(--ky-space-4); }

/* Clears the FIXED masthead on a page that has no hero to do it. A spacer, not
   padding on .ky-paper — padding on a ground silently breaks the bloom, because
   absolute positioning resolves against the padding box while --ky-bloom-x/y are
   measured from the border box, and the disc would sit off by exactly that much.
   Same reason .ky-hero is a spacer rather than a padded ground. */
.ky-page-top { height: var(--mh-tall); }

/* An embedded third-party frame — currently the Relevance AI sample agent. Given
   the leaf's treatment so it reads as something laid on the paper rather than a
   hole cut in it, and its own solid ground because the frame's contents are not
   ours to make legible against a lattice. */
.ky-embed {
  margin-top: var(--ky-space-8);
  border: 1px solid var(--ky-border);
  border-radius: var(--ky-radius-leaf);
  background: var(--ky-surface);
  overflow: hidden;
}
.ky-embed iframe { display: block; width: 100%; border: 0; }

/* ---- Demo cards -----------------------------------------------------------
   A summary on the sheet that opens into the working thing on a screen. The
   card is a .ky-leaf and the screen is a .ky-panel, so this adds geometry and
   nothing else — no new surface treatment, and the panel arrives already
   carrying the bloom's ring and ramps.

   Markup and the reasoning behind <details> vs a scripted disclosure:
   src/_includes/partials/demo-card.njk.                                      */

.ky-demos { display: grid; gap: var(--ky-space-6); }

/* clip, so the screen's chamfered bottom-right corner is trimmed back to the
   sheet's own 4px radius where the two disagree — and clip rather than hidden
   because it creates no scroll container to be scrolled to by accident. Safe
   here: nothing focusable comes near the edge (the toggle sits a full
   --ky-leaf-pad inside it), which is the one case where clipping a box quietly
   costs a focus ring. */
.ky-demo { overflow: clip; }

.ky-demo__reveal { margin-top: var(--ky-space-6); }

/* A PILL. The chamfer is the screen's language, and this is a control — but
   here that convention is also load-bearing rather than only stylistic: a
   <summary> is focusable, and a clip-path clips the focus ring away with the
   rest of the element's rendering. Never cut this shape. */
.ky-demo__toggle {
  /* Both, and neither is redundant: display:inline-flex from .ky-btn drops the
     marker in Chrome, list-style covers Firefox, and Safari draws its own. */
  list-style: none;
  width: fit-content;
}
.ky-demo__toggle::-webkit-details-marker { display: none; }
.ky-demo__toggle::marker { content: ""; }

/* The label states what the control WILL do — "Try the agent" while closed,
   "Close" while open — because <details> already reports its own state to
   assistive tech through the expanded/collapsed role. A label that named the
   current state instead would say the opposite of the announcement. */
.ky-demo__toggle-close,
.ky-demo__reveal[open] .ky-demo__toggle-open { display: none; }
.ky-demo__reveal[open] .ky-demo__toggle-close { display: inline; }

/* Bled back out to the leaf's edges. Without this the frame carries the leaf's
   padding AND the panel's on both sides — ~76px of inset each side on a 390px
   phone, which leaves a chat embed about 238px wide. The panel's own padding
   stays put: it is the bloom's falloff runway as well as its padding, and
   shrinking it puts lit strands under the copy. */
.ky-demo__screen {
  margin-top: var(--ky-leaf-pad);
  margin-inline: calc(-1 * var(--ky-leaf-pad));
  margin-bottom: calc(-1 * var(--ky-leaf-pad));
}
.ky-demo__screen > p:first-child { margin-top: 0; }

/* ---- Buttons --------------------------------------------------------------
   Controls are tech-side wherever they sit — a button is a machine part on
   both grounds, so Ubuntu on paper too. The fill LIGHTENS on interaction
   because the label is near-black; if a white-label variant is ever added
   that direction flips, so check the label before copying it.                */

/* A PILL, not a chamfer. The chamfer belongs to the screens — masthead, hero
   panel, rail, glass panels — and a control is a different kind of object. */
.ky-btn {
  font-family: var(--ky-font-tech);
  font-weight: var(--ky-weight-medium);
  font-size: var(--ky-text-small);
  display: inline-flex; align-items: center; gap: var(--ky-space-2);
  padding: var(--ky-space-3) var(--ky-space-6);
  border-radius: var(--ky-radius-pill);
  border: 1px solid transparent;
  background: var(--ky-action-bg);
  color: var(--ky-action-text);
  text-decoration: none; cursor: pointer; line-height: 1.2;
  transition: background var(--ky-duration-fast) var(--ky-ease);
}
.ky-btn:hover  { background: var(--ky-action-bg-hover); }
.ky-btn:active { background: var(--ky-action-bg-active); }

.ky-btn--ghost { background: transparent; border-color: var(--ky-wood-600); color: var(--ky-wood-700); }
.ky-btn--ghost:hover { background: rgba(var(--ky-rgb-thread), .10); }
.ky-glass .ky-btn--ghost,
.ky-panel .ky-btn--ghost { border-color: rgba(var(--ky-rgb-bone), .24); color: var(--ky-bone-100); }
.ky-glass .ky-btn--ghost:hover,
.ky-panel .ky-btn--ghost:hover { background: rgba(var(--ky-rgb-bone), .08); }

:where(a, button, [tabindex]):focus-visible {
  outline: var(--ky-focus-width) solid var(--ky-focus);
  outline-offset: var(--ky-focus-offset);
}

/* The hero moved out. .ky-hero-plate — the transitional plate anchored to the
   bottom-left of the old 16:9 band — was replaced on 2026-08-04 by the pinned
   display panel and the closing rail, which live in their own chunk alongside
   the geometry that keeps them registered: src/css/hero.css. The plate carried
   .ky-glass in the markup rather than hand-set colours, and its replacement
   still does, for the same reason: the eyebrow, muted copy and ghost button all
   take their lit values from the one token flip every glass surface uses.      */

/* ---- Footer ---------------------------------------------------------------
   MOVED to src/css/footer.css, which now owns the whole footer — the closing
   screen's geometry, its glass treatment, the contact block, the colophon and
   the reduce-effects control. It was split across two files for one commit and
   that is exactly how a component drifts. Nothing footer-related belongs here.
   --------------------------------------------------------------------------- */

/* ---- Placeholder ----------------------------------------------------------
   Visibly unfinished on purpose. Launch is gated on content, and a plausible
   placeholder is worse than an obvious one because it can ship unnoticed.
   Never fill one of these with invented copy.                                 */

/* Paper radius, not the tech chamfer: this is a note pinned to the page, not a
   machined surface. Also the only radius left on the rebuilt page besides the
   leaves themselves. */
.ky-todo {
  border: 1px dashed var(--ky-border);
  border-left: 3px solid var(--ky-warning);
  border-radius: var(--ky-radius-leaf);
  padding: var(--ky-space-4) var(--ky-space-6);
  background: var(--ky-surface);
  max-width: var(--ky-measure);
}
.ky-todo p { color: var(--ky-text-muted); font-size: var(--ky-text-small); margin: 0; }
.ky-todo strong { color: var(--ky-warning); font-family: var(--ky-font-tech); }
