
    /* LOD ladder — the single zoom vector, named by chief-facing
       CARDS-ON-SCREEN intent.  Zoom itself is arbitrary; card-count
       is what the chief feels.  Values are z-anchors derived for a
       nominal 260×150 tile on a typical viewport; the actual clamp
       expressions (below) use their own raw numeric bounds — these
       constants are the doc's chief-facing vocabulary, referenced
       from /web/interaction-thresholds.tvg4.json (the LOD map). */
    :root {
      --rung-many:   0.5;   /* ~30+ cards visible — scene reads as a diagram */
      --rung-some:   0.85;  /* ~10 cards visible — working neighborhood     */
      --rung-few:    1.5;   /* ~3–5 cards visible — one focus + siblings    */
      --rung-one:    3;     /* ~1 card fills viewport — single subject      */
      --rung-deep:   4;     /* card bigger than viewport — chief has drilled in */
      /* Global nominal-text scalar — the dashboard's font slider
         writes here.  1 = design default; every font-size expression
         multiplies by this so a single knob adjusts all card text. */
      --text-nominal-scale: 1;
      /* Nominal on-screen font sizes (in unitless px).  The two
         --*-scale primitives above HOLD each font at these sizes on
         screen across their zoom band.  The LOD-diagram sliders
         write these values live.  Defaults live in the fallback
         syntax at each use-site — see SOURCE OF TRUTH note below. */
      /* ── SCREEN-HELD SCALES ────────────────────────────────────
       * Two multipliers hold an element's rendered SIZE constant on
       * screen across a range of cumulative zoom.  Physical intent:
       * a body-text glyph, a resize handle, a card border, a
       * titlebar caption — each of these should look about the same
       * on the chief's screen whether they're panning across ten
       * cards or peering close at one.
       *
       * How the clamp reads: 1 / z is the counter-multiplier that
       * would hold an element at its NOMINAL screen size (11 px
       * prose, 12 px corner handle, 1 px border, 12 px title).  We
       * clamp 1 / z to a band [min, max] — inside the band, the
       * element is screen-constant; outside, the multiplier
       * saturates and the element starts drifting with the scene:
       *
       *   chief zoomed way OUT (z ≪ 1)  →  1 / z is large
       *                                  → clamp at MAX
       *                                  → element grew as big as we
       *                                    allow; further zoom-out
       *                                    shrinks it with the scene
       *
       *   chief zoomed way IN  (z ≫ 1)  →  1 / z is small
       *                                  → clamp at MIN
       *                                  → element shrank as small
       *                                    as we allow; further
       *                                    zoom-in grows it with the
       *                                    scene
       *
       * Two families, two bands:
       *
       *   --body-scale       body text, resize handles, card border
       *                      — fine detail, narrower band [0.5, 2]
       *                      (font sits between 5.5 and 22 screen px)
       *   --head-scale       titlebar cap text — louder detail,
       *                      wider band [0.25, 4]
       *                      (title sits between ~3 and 48 screen px)
       *
       * REFLOW NOTE — inside the band, the textbox body font is
       * screen-anchored but the card WIDTH scales linearly with z
       * (unclamped, because the card lives on the pan-zoom stage).
       * So chars-per-line = card-width-in-screen-px / char-width-in-
       * screen-px changes with zoom → the text wraps differently at
       * each zoom.  Reflow is the price of a screen-constant font
       * on a scene-scaled card.  The design accepts it inside the
       * band; outside the band both font and card scale together
       * and reflow stops.
       *
       * The LOD-diagram controller writes these live so the chief
       * can retune the bands in real time.
       *
       * SOURCE OF TRUTH — the LOD-controlled variables (band bounds,
       * ramp thresholds, font nominal-sizes, body-font anchor) are
       * NOT declared here.  Their design defaults live at each
       * use-site as var(--foo, default) fallbacks; the primary
       * declaration comes from the server-injected :root override
       * (server.js → _readLodDefaultsStyle).  One primary
       * declaration, one place to change it — no cascade fight, no
       * !important. */
    }
    /* --cumulative-zoom lives on the tile so every LOD-derived value
       resolves per-card.  Non-fullpage tiles ride the pan-zoom stage;
       .fullpage (the chrome / root card) drops the local --zoom
       because it's not on the stage. */
    .node:not(.fullpage) {
      /* QUANTIZED PER CARD, written by pan-zoom when this card's
         apparent size crosses a step — NOT derived from --zoom here.
         
         The LOD axis is apparent SIZE, not zoom.  Every card is a
         different size, so each crosses a given size on a different
         zoom; deriving from --zoom made one camera move invalidate
         every card at once, because they all read the same changing
         variable.  Profiled on a 245-card scene: 69.6% of zoom cost
         was browser style recomputation, not script.
         
         This is the sprite/mipmap problem and has the sprite/mipmap
         answer — pick a discrete level per object from its own screen
         size, and hold it until the object crosses a level.  Cards
         then recompute only when THEY change level, which is a handful
         per frame instead of all of them, and the rest of the time a
         zoom is what it should be: a transform the compositor applies.
         
         Fallback keeps the old behaviour if nothing writes it. */
      --cumulative-zoom: calc(var(--titlebar-effective-zoom, 1) * var(--card-lod-zoom, 1));
    }
    .node.fullpage {
      --cumulative-zoom: var(--titlebar-effective-zoom, 1);
    }
    /* Two primitives over cumulative zoom, pre-instantiated so call
       sites reference by name.  hold(mn,mx) = clamp(mn, 1/z, mx) —
       screen-constant across an octave band, drifts at the extremes.
       ramp(t,w) = clamp(0, (z-t)/w, 1) — 0 below t, 1 above t+w. */
    .node {
      /* Bounds come from the injected :root override; fallbacks are
         the design defaults.  See SCREEN-HELD SCALES for the physical
         meaning of each band. */
      --body-scale: clamp(var(--body-scale-min, 0.5),  calc(1 / var(--cumulative-zoom)), var(--body-scale-max, 2));
      --head-scale: clamp(var(--head-scale-min, 0.25), calc(1 / var(--cumulative-zoom)), var(--head-scale-max, 4));
      --ramp-overview: clamp(0, calc((var(--cumulative-zoom) - var(--ramp-overview-t, 0.5)) / var(--ramp-overview-w, 0.5)), 1);
      /* Titlebar-headroom LOD — the slot reveal ladder.  Same ramp
         shape, different input: painted card width / title natural
         width, rung published from titlebar.tvg4.json.  --titlebar-
         painted-ratio is set on .node by titlebar.js. */
      --ramp-flip:       clamp(0, calc((var(--titlebar-painted-ratio) - var(--threshold-flip,   2)) * 2), 1);
      --ramp-agent:      clamp(0, calc((var(--titlebar-painted-ratio) - var(--threshold-agent,  3)) * 2), 1);
      --ramp-status:     clamp(0, calc((var(--titlebar-painted-ratio) - var(--threshold-status, 4)) * 2), 1);
    }
    /* ── Semantic zoom: cards shrink SLOWER than the space between
       them ───────────────────────────────────────────────────────
       Positions are the camera's — they shrink linearly with zoom,
       because they ARE the transform.  Card SIZE shrinks sub-linearly:
       apparent size goes as zoom^(1/2) rather than zoom^1, which in a
       stage already scaled by zoom means swelling each card by
       1/sqrt(zoom).

       The gaps therefore close faster than the cards do.  Cards
       approach, meet, and form chains — and that chained view IS the
       connectivity reading of the scene, arrived at by a rate
       difference rather than by switching renderers.  Nothing decides
       "now we are topological"; one exponent produces the whole
       continuum.  Chief 2026-07-26: "as we zoom out we should
       transition from a faithful spatial representation of position to
       one respectful of connectivity, where cards tend to stick
       together when they touch."

       What degrades is positional fidelity; what is preserved is
       adjacency.  That is the right thing to trade, because at
       overview zoom a position is already unreadable while a
       neighbour still means something.

       It is a pure function of --zoom: at any zoom these are the
       sizes, recomputed, never accumulated.  A bond that persisted
       once formed would make the layout depend on the path taken to
       reach a zoom, so zooming out slowly and jumping straight there
       would disagree, and zooming back in would not retrace.  That is
       hysteresis, which is order-dependence wearing a physical
       disguise.

       max(1, ...) so zooming IN is untouched: the authored geometry is
       faithful at and above nominal, and only overview zoom relaxes
       it.  Side effect worth having: a card that stays larger keeps
       room for its title far longer, so the zoom at which a title runs
       out of card moves well out. */
    .node {
      /* NOT APPLIED YET — the swell is correct and insufficient on its
         own.  Measured on link-model at zoom 0.15: alpha 0.5 gives 206
         overlapping card pairs, alpha 0.75 gives 94, no swell gives
         none and unreadable titles.  Tuning the exponent only MOVES
         the trade between illegible and overlapping; it cannot escape
         it, because the swell is precisely what makes cards meet.
         Sticking-without-colliding is therefore load-bearing, not
         polish, and it needs edges to move with their cards.  Kept
         here as the mechanism, inert, until that lands. */
      --card-swell: max(1, calc(1 / sqrt(var(--card-lod-zoom, 1))));
    }
    .node { position: absolute;
            /* Border-box so the tile's border sits INSIDE its declared
               width/height.  Chief 2026-07-14: mouse-to-card movement
               shall be 1:1 — no coordinate conversion.  With the
               default content-box, offsetWidth (border-box measured)
               differs from style.width (content-box interpreted) by
               2*border-width, and each resize gesture adds those two
               pixels per iteration (cards drift ahead of the mouse).
               Border-box collapses the two to the same number:
               style.width = offsetWidth, no drift. */
            box-sizing: border-box;
            /* Border width holds screen-constant across the working
               octave band (same clamp handles use).  Colour lifts as
               the chief zooms out toward overview so the mint edge
               stays legible against the black stage. */
            border-style: solid;
            border-width: calc(1px * var(--body-scale));
            border-color: color-mix(in srgb,
              rgba(136,221,192,0.85),
              rgba(136,221,192,0.55) calc(var(--ramp-overview) * 100%));
            border-radius: 4px;
            background: #0c1018;
            display: flex; flex-direction: column;
            box-shadow: 0 2px 8px rgba(0,0,0,0.6);
            /* No contain:paint here: it clips children — including
               the resize handles that extend half-outside the card —
               and their HIT-TESTING along with them, so the chief
               felt the resize cursor only on the inner half (biased
               to the inside).  Without it the handles catch the
               pointer symmetrically around the card edge.  Repaint
               isolation is shifted to .body, which carries the bulk
               of per-tile painting anyway (titlebar paints rarely). */ }
    /* Cap (titlebar) rules live in cap.js — shared across every node-
       type so iframe and textbox tiles get the same zoom behaviour.
       ensureCapStyle() installs them on activate. */
    /* titlebar-bottom variant — cards whose spec (or per-node override)
       sets titlebarAt: 'bottom' get column-reverse layout, moving the
       titlebar to the bottom edge.  Raster node type defaults here
       (chief 2026-07-01) so a raster reads image-first, title-below,
       and pairs cleanly with a caption textbox placed underneath. */
    /* BLESSED cards — participate in the scene→code compiler.  A node
       declares itself blessed by carrying a `role` field: "input",
       "rule", "gate", or "output".  tile-render projects the field to
       `data-role`; this CSS keys off the presence of the attribute so
       any recognised role paints in gold and any annotation keeps the
       mint default.  See /signal-flow-compile.js. */
    .node[data-role="input"],
    .node[data-role="rule"],
    .node[data-role="gate"],
    .node[data-role="output"] {
      border-color: #ddc066;
    }
    /* HYPERCARDS — a card whose content is shared with another
       instance on the same address (same address appears elsewhere
       in the scene, or points at a card in a different file).
       Chief 2026-07-27: hypercards shall be subtly distinguished
       chromatically.  Mint (default) → periwinkle: same lightness
       and saturation, hue rotated ~65° toward blue.  Subtle enough
       that a chief-facing scene stays visually calm; distinct
       enough that hypercards are recognisable at a glance. */
    .node.hypercard {
      border-color: color-mix(in srgb,
        rgba(160,190,235,0.85),
        rgba(160,190,235,0.55) calc(var(--ramp-overview) * 100%));
    }
    /* Resize-handle mint mark also shifts to periwinkle on hypercards
       — that's the most visible chroma cue at working zoom, so the
       distinction actually READS.  Mirrors the border rule above. */
    .node.hypercard .resize-handle::before {
      background: #a0bfeb;
    }
    .node.titlebar-bottom { flex-direction: column-reverse; }
    .node.titlebar-bottom > .titlebar {
      border-bottom: none;
      border-top: 1px solid #1a2030;
    }
    .node.titlebar-bottom > .body {
      border-bottom-left-radius: 0;
      border-bottom-right-radius: 0;
      border-top-left-radius: 4px;
      border-top-right-radius: 4px;
    }
    .node > .body { flex: 1; position: relative; overflow: hidden;
                    border-bottom-left-radius: 4px;
                    border-bottom-right-radius: 4px;
                    /* Paint isolation moved here from .node so the
                       chief's drag/resize/edit on one tile doesn't
                       force a repaint of any other.  Body is where
                       the heavy painting lives (iframes, ink, text
                       editing). */
                    contain: paint; }
    .node > .body > iframe { width: 100%; height: 100%; border: 0; display: block;
                              position: relative; z-index: 2; }
    /* Iframe cards: iframe fills the body at 100 % × 100 % (default
       .node > .body > iframe rule above already does this).  The
       iframe's inner viewport is exactly the card body's dimensions
       in card-space, so responsive inner pages lay themselves out to
       fit — same behavior as opening the panel in a standalone browser
       window of that size.  No transform:scale trick; the content
       IS the viewport. */
    /* NO type-specific titlebar/body positioning.  Chief 2026-07-12:
         "Flip cards are not meant to be different, flip cards are
          meant to have the same behaviour, it's all the same thing.
          If there is something that looks like it is two different
          things, that is a sign that it is wrong and should be one
          thing."
       Iframe, flipcard, textbox, everything: the same flex-column
       stack — titlebar row on top (or bottom via .titlebar-bottom),
       body below, both laid out identically.  Removed the earlier
       absolute-positioned overlay that gave iframe/flipcard a
       preserved-aspect body strip; the trade was aspect-ratio
       invariance across zoom vs. one shared layout, and R3-style
       "one thing" wins. */
    /* Past the depth budget — the card's shape stays so the layout
       doesn't shift, but no iframe runs.  A single ellipsis hints
       that there's more here if the chief drills in. */
    .node > .body > .placeholder-body {
        width: 100%; height: 100%;
        display: flex; align-items: center; justify-content: center;
        color: #88ddc0; opacity: 0.35; font-size: 18px;
    }
    /* runtime type-validation tints — applied by viewer based on the
       kernel's validation report.  The rules know the class names; the
       application is upstream. */
    .node.type-pass { border-color: #88ddc0; box-shadow: 0 0 0 1px rgba(136,221,192,0.35), 0 2px 8px rgba(0,0,0,0.4); }
    .node.type-fail { border-color: #d96666; box-shadow: 0 0 0 1px rgba(217,102,102,0.55), 0 2px 8px rgba(0,0,0,0.4); }
    .node.hover     { box-shadow: 0 0 0 2px rgba(221,192,102,0.55), 0 4px 12px rgba(0,0,0,0.6); }
    /* edge-define-gesture marks the first-clicked card while waiting
       for the second click.  Brighter ring so it reads distinct. */
    .node.edge-define-selected { box-shadow: 0 0 0 3px #ddc066, 0 4px 16px rgba(0,0,0,0.6); }
    /* click-select: tints the SELECTED card.  Cyan/blue ring so it
       reads distinct from .type-fail (red) — selection is a normal
       UI state, type-fail is "something is structurally wrong". */
    .node.selected             { box-shadow: 0 0 0 3px #66bdf2, 0 4px 16px rgba(0,0,0,0.6); }
    /* Selection-neighborhood highlight — a card at the far end of an
       edge whose OTHER end is a selected card.  Same cyan as
       .selected but a thinner ring so the selection itself still
       reads as the primary target.  See the requirement card in
       /click-select.tvg4.json. */
    .node.selection-connected  { box-shadow: 0 0 0 2px rgba(102,189,242,0.6), 0 3px 12px rgba(0,0,0,0.55); }
    /* HOLD-DROP armed — chief held the cursor over this tile for the
       dwell threshold during a drag.  Releasing now will fire the
       drop handler; releasing before this class appears is just a
       move.  Distinct from .hover (orange — passive highlight). */
    .node.drop-armed           { box-shadow: 0 0 0 3px #88ddc0, 0 4px 16px rgba(0,0,0,0.6); }
    /* Per-tile pan-vs-interact gate — driven by APPARENT SIZE, set by
       iframe.js's drainer on every pan-zoom tick.  Scene zoom (the old
       #root.low-zoom gate) is a scalar for the whole canvas; whether a
       given card is "clickable inside" is a per-card question — a big
       card can be readable at a low scene zoom, a small card can be
       illegible at a high one.  Apparent width (natural × cumulative
       zoom) is the right axis, and it's already computed per-tile.
       .pan-target means "this card's apparent width is below LIVE
       (240 px) — treat its body as a pan handle, not an interactive
       surface".  Selection overrides via :not(.selected) — clicking a
       card to select it is an explicit "I want to interact here"
       signal that immediately restores pointer-events. */
    .node.pan-target:not(.selected) > .body > iframe { pointer-events: none; }
    /* cursor: grab — drag handle.  See web/affordances.md, "cursor
       matches click action".  When a tile is in pan-target mode the
       WHOLE card is the drag handle; the cursor advertises that
       everywhere on the tile EXCEPT on link regions (titlebar-link.js's
       CSS overrides cursor:pointer on .titlebar-link-target descendants
       at every zoom).  Selection restores default cursor. */
    .node.pan-target:not(.selected)           { cursor: grab; }
    .node.pan-target:not(.selected):active    { cursor: grabbing; }
    /* textbox node type — plain text body, scrollable, white-space
       preserved.  The container.body is what makeTile creates; the
       textbox-body div is what the textbox registration mounts.    */
    .node > .body > .textbox-body { padding: 8px 10px; height: 100%;
                                    box-sizing: border-box; overflow: auto;
                                    color: #aac;
                                    /* Body prose font is anchored to the SCREEN — it
                                       stays at 11 nominal screen pixels across the
                                       --body-scale band, and drifts with the scene at
                                       the extremes.  Same band drives resize handles
                                       and border width.  Reflow inside the band is a
                                       consequence: the card's screen width scales
                                       with zoom while the font doesn't, so chars-per-
                                       line changes.  See SCREEN-HELD SCALES above for
                                       the physical picture. */
                                    /* Effective multiplier interpolates between the
                                       screen-held clamp (--body-scale) and 1 (scene-
                                       anchored) by --body-font-anchor.  At anchor=1
                                       (default): --body-scale — text is screen-
                                       constant and REFLOWS as the card scales.  At
                                       anchor=0: multiplier collapses to 1 — text
                                       scales with the card, NO reflow. */
                                    font: calc(var(--body-font-px, 11) * 1px
                                               * (var(--body-font-anchor, 1) * (var(--body-scale) - 1) + 1)
                                               * var(--text-nominal-scale, 1)) / 1.55 ui-monospace, monospace;
                                    white-space: pre-wrap; word-break: break-word; }
    /* Inline-edit visuals — both cap and textbox-body share these
       cues so the editing affordance is uniform.  contenteditable
       itself drives the text cursor; we just outline the box.
       cursor: text — see web/affordances.md, "cursor matches click
       action".  An editable region's click places the caret, so the
       caret glyph announces it. */
    .node > .titlebar[contenteditable=true],
    .node > .body > .textbox-body[contenteditable=true] {
        outline: 1px solid #88ddc0; outline-offset: -1px;
        background: rgba(136,221,192,0.08);
        user-select: text; cursor: text;
    }
    /* Prompt bar — appears during body edit, hosts the agent prompt
       input.  Lives as a flex child below .body so contentEditable in
       the body doesn't try to swallow it.  Lifetime is exactly the
       body's edit session; engageBodyEdit appends and removes. */
    .node > .tile-prompt-bar {
        flex: 0 0 26px; display: flex; align-items: center;
        padding: 0 6px; background: #0c1018;
        border-top: 1px solid #1a2030; }
    .node > .tile-prompt-bar input {
        flex: 1; background: transparent;
        border: none; outline: none; color: #88ddc0;
        font: 11px/1.4 ui-monospace, monospace; }
    .node > .tile-prompt-bar input::placeholder {
        color: rgba(136,221,192,0.45); }
    /* Marker for agent-stylised spans (the stub puts these in; the
       real agent will hopefully use richer markup).  Subtle mustard
       highlight so the chief can see which spans the agent touched. */
    .agent-styled { background: rgba(221,192,102,0.18);
                    border-radius: 2px; padding: 0 2px; }
    /* Tight prose typography INSIDE textbox bodies.  Mirrors the
       legacy iframe-tile look (constellation-problem.tile.html) so a
       textbox carrying the same content reads identically — no visible
       regression from the migration off iframes.  Paragraphs get a
       small bottom margin, code is tinted, headings stand out. */
    .textbox-body p     { margin: 0 0 6px 0; }
    .textbox-body code  { color: #cde; }
    .textbox-body h3    { color: #88ddc0; font-size: 11px; margin: 0 0 6px 0;
                          text-transform: uppercase; letter-spacing: 0.06em;
                          font-weight: 600; }
    /* Badges — same palette/treatment as the legacy iframe tile.  The
       textbox just holds the markup (<span class="badge input">…</span>);
       this stylesheet renders it.  Sanitizer keeps class on span. */
    .textbox-body .badge {
        display: inline-block; margin-top: 6px; padding: 1px 6px;
        font-size: 9px; text-transform: uppercase;
        letter-spacing: 0.04em; border-radius: 3px;
        background: rgba(255,255,255,0.04);
    }
    .textbox-body .badge.done    { color: #88ddc0; background: rgba(136,221,192,0.10); }
    .textbox-body .badge.cartoon { color: #7eb6e0; background: rgba(126,182,224,0.10); }
    .textbox-body .badge.todo    { color: #ddc066; background: rgba(221,192,102,0.10); }
    .textbox-body .badge.later   { color: #c694d6; background: rgba(198,148,214,0.10); }
    .textbox-body .badge.input   { color: #aac;    background: rgba(170,170,204,0.08); }
    .textbox-body .badge.output  { color: #d0a046; background: rgba(208,160,70,0.10); }
    /* Kanban state badges — the conventional palette for a task's
       current column.  Use these as <span class="badge doing">…</span>
       etc.  Agents driving boxes write the state into the body via
       autosave (POST /<scene>.tvg4.json); agents driven by boxes
       read it on each load (or via store.subscribe). */
    .textbox-body .badge.doing   { color: #7eb6e0; background: rgba(126,182,224,0.14); }
    .textbox-body .badge.blocked { color: #d96666; background: rgba(217,102,102,0.14); }
    /* filelink — the body holds a centred anchor (the link).  Cursor
       pointer applies ONLY to the anchor text, not to the rest of the
       body — so clicking the link follows it; clicking elsewhere in
       the body drags / selects like every other card.
       Cap is double-click-editable like other types so the chief can
       rename the label without moving the file. */
    .node.type-filelink > .body { display: flex; align-items: center;
                                  justify-content: center; padding: 6px; }
    .filelink-body { display: inline-block;
                     color: #88ddc0; text-decoration: underline;
                     font: 11px/1.4 ui-monospace, monospace;
                     padding: 4px 8px; border-radius: 3px;
                     text-align: center; word-break: break-all; max-width: 100%; }
    .filelink-body:hover { background: rgba(136,221,192,0.10); }
    /* Inert form for filelinks whose target is the current scene.
       Same layout as .filelink-body but muted, no underline, no hover
       highlight — the affordance says "you are here", not "click me". */
    .filelink-here { display: inline-block;
                     color: #88a; text-decoration: none;
                     font: 11px/1.4 ui-monospace, monospace;
                     padding: 4px 8px; border-radius: 3px;
                     text-align: center; word-break: break-all; max-width: 100%;
                     cursor: default; }
    /* Promoted card — the tile stays a normal member of the stage;
       its dimensions and camera have been adjusted by promote.js so
       it fills the canvas.  data-promoted is a hook for affordances
       that want to reflect the state (glyph, etc.); no CSS override
       needed here because the tile is truthfully sized. */

    /* ── LEVEL OF DETAIL — per-tile, apparent-width-driven ────────
       Gate: pan-zoom.js sets .too-small-for-content on each tile
       whose apparent width (model × scene-zoom) is below
       --lod-content-strip-px (published live by /lod-diagram.html;
       default in pan-zoom.js).  Per-tile, not per-scene: a wide
       iframe stays legible where a small note dropped below.

       REQUIREMENT — chief 2026-07-12:
         "The title text shall shrink to fit the card as necessary."
         "We are entirely capable of showing legible titles as cards
          shrink."
       So the TITLEBAR is never touched by LOD.  The horizontal-
       ceiling and vertical-ceiling reflow rules in titlebar.js —
       captured on the requirements graph in /titlebar.tvg4.json —
       own that responsibility.  They yield titlebar-scale < 1 on
       small cards and the title always fits.

       Below the gate we strip only the .body — the heavy content
       (multi-line prose, iframes, custom bodies, embedded scenes)
       that carries the paint + hit-test cost that pinned fleet-
       graph at ~10 fps.  visibility:hidden preserves layout so
       trackTitlebarNaturals can still measure --natural-word-w-num
       and --natural-wrap-h-num from the (invisible-but-laid-out)
       body.  pointer-events:none suppresses hit-test descent into
       the stripped subtree. */
    .node.too-small-for-content > .body {
      visibility: hidden !important;
      pointer-events: none !important;
    }

/* ══ CARD TYPE CASCADE ══════════════════════════════════════════════
   Which form a card presents — symbol, sign, or notecard — as a
   function of how wide it is ON GLASS.  Chief 2026-07-27: "symbols are
   smallest, replaced by signs, replaced by notecards; as apparent size
   changes from zoom."

   Decided here rather than in script, and from --titlebar-painted-w,
   which is the card's painted width already QUANTISED to its own
   mipmap level.  Three consequences of using that number:

     - a card holds its form between levels, so a wheel gesture does
       not re-decide every card's content on every frame;
     - cards of different authored size change form at different
       zooms, so a diagram degrades gradually rather than all at once;
     - no script runs at all — the form is a consequence of two
       declared facts, the fields the card has and the room it has.

   Each gate is a 0-or-1 clamp of painted width against the threshold
   declared in card-cascade.tvg4.json.  Collapsing max-height with
   opacity is the same idiom the titlebar slots use, so a hidden part
   gives up its room rather than sitting invisible. */
.node {
  /* The two gates are NOT the same kind of gate, and they take
     different shapes for that reason.

     --form-note is a SIZE lerp: it multiplies the scale cap below, and
     the notecard's body sits BELOW the titlebar, so the two forms are
     stacked and can crossfade.  It gets a band centred on its declared
     threshold — 220 (160..280) — and the number in
     card-cascade.tvg4.json becomes the MIDPOINT of the transition.

     --form-sign is a CONTENT swap between two elements that occupy the
     SAME centre of the card: .card-symbol is inset:0 centred and the
     sign's titletext is centred on top of it.  Superimposed forms
     cannot crossfade — at any intermediate value the card prints its
     symbol THROUGH its titletext.  Measured on the cascade fixture with
     a 40 px band: the 104 px and 79 px cards showed a ghosted "V_bus"
     over "BUS VOLTAGE".  So this one stays a step, and the invariant
     that exactly one of symbol/sign is ever showing holds.

     The rule, stated once so the next gate inherits it: a gate between
     STACKED forms may be a band; a gate between SUPERIMPOSED forms must
     be a step.

     Why a band.  These gates multiply the scale cap (see
     --titlebar-zoom-cap below), and at width 1 the multiplier went 0 to
     1 across a single pixel — so a card crossing the notecard gate
     changed its title size by 3.2x in one step.  Measured on the
     cascade fixture: a 283 px card set its title at 13 px and the 222 px
     card beside it at 41.8 px.  Chief 2026-07-27, pointing at that
     card: "the text is too large."

     The input is what makes an edge untenable.  --titlebar-painted-w is
     QUANTISED per card (card-lod.js), and the quantisation is loose by
     design — up to 8 % off the width the card actually paints at.  That
     card was 222 px on glass, above the chief's own 220 px threshold,
     and the gate saw 215.  Against a step, a 3 % error in the input
     produces a 320 % error in the output; against a band, a 3 % error
     produces a 3 % error.  The quantisation stays (it is what keeps one
     camera move from invalidating every card); the amplifier goes.

     A fractional gate was already well-defined — the cap and the bar's
     min-height are both linear blends on --form-note, written as lerps
     for exactly this reason.  Only the ramp width changes here. */
  --form-sign: clamp(0, calc((var(--titlebar-painted-w, 260) -  70) /   1), 1);
  --form-note: clamp(0, calc((var(--titlebar-painted-w, 260) - 160) / 120), 1);
}
/* NOTECARD — title and body together.  The body appears only once the
   card is wide enough to be read as prose. */
.node[data-has-body] > .body {
  opacity: var(--form-note, 1);
  max-height: calc(var(--form-note, 1) * 100%);
  overflow: hidden;
}
/* SIGN — the titletext, standing alone below the notecard threshold. */
.node[data-has-title] > .titlebar {
  opacity: var(--form-sign, 1);
  max-height: calc(var(--form-sign, 1) * 100%);
}
/* SYMBOL — the smallest form, and the ONLY one that appears by the
   absence of another: a card shows its symbol exactly when it is too
   small to carry its sign.  1 - --form-sign is that complement, so the
   two can never both be showing and there is no third state to keep
   consistent. */
.card-symbol {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: ui-monospace, monospace;
  /* Scaled to the CARD, not to a nominal font size — a symbol is
     legible at a size no line of text survives, which is the whole
     reason the form exists. */
  /* Fitted to the card in BOTH directions.  A symbol is not always one
     glyph — the chief set one to "V_bus" — and sizing by height alone
     made a long symbol overflow by an amount that differed with every
     card, which reads as rendering inconsistently rather than as
     overflowing.  min() takes whichever bound binds first: 0.55 of the
     card's height, or the width the characters actually need at the
     titlebar font's 0.66em advance. */
  font-size: min(
    calc(var(--card-h-num, 100) * 0.55px),
    calc(var(--card-w-num, 100) * 0.86px / max(1, var(--symbol-len, 1)) / 0.66)
  );
  line-height: 1;
  color: #88ddc0;
  /* Editable, so it must be able to receive a double-click — but only
     while it is the form actually showing.  Collapsing its BOX when the
     sign is up (rather than leaving it full-size and transparent) means
     it cannot intercept a gesture aimed at the card beneath it: at
     form-sign 1 there is no area to hit, at 0 it fills the card.  Same
     idiom as the titlebar slots, which give up their room rather than
     sitting invisible. */
  pointer-events: auto;
  opacity: calc(1 - var(--form-sign, 1));
  max-height: calc((1 - var(--form-sign, 1)) * 100%);
  overflow: hidden;
}
/* A card with no symbol never invents one; nothing here creates the
   element, so absence is simply absence. */

/* ── SIGN: the card IS the sign ─────────────────────────────────────
   Chief 2026-07-27: "in sign mode, titletext shall be centered
   horizontally & vertically, like a hand-painted sign."

   A sign has no body to sit above, so its titlebar is not a bar at the
   top of a card — it is the whole face, with the text in the middle of
   it.  The band grows from its text height to the card's full height as
   the notecard form gives way, which is a lerp on --form-note rather
   than a second layout: at form-note 1 the term vanishes and the bar is
   exactly the height the fit computed, so notecards are untouched. */
.node:not(.fullpage) { --bar-h-text: calc(var(--natural-text-h-num, 16) * 1px * var(--titlebar-scale, 1)); }
.node[data-has-title]:not(.fullpage) > .titlebar {
  min-height: calc(
    var(--bar-h-text)
    + (1 - var(--form-note, 1)) * (var(--card-h-num, 100) * 1px - var(--bar-h-text))
  );
}
/* Centred both ways, always.  In notecard form the band is already the
   text's own height, so vertical centring is a no-op there and one rule
   serves both forms — no branch, nothing to keep consistent. */
.node:not(.fullpage) > .titlebar > .titlebar-text {
  top: 50%;
  transform: translate(-50%, -50%);
}

/* A DERIVED symbol is set slightly back from an authored one — it is
   the system's guess at a card's mark, not the chief's own, and the
   difference should be visible without being distracting.  Editing it
   writes a real symbol field, at which point the attribute goes and it
   renders at full strength. */
.node[data-symbol-derived] > .card-symbol { opacity: calc((1 - var(--form-sign, 1)) * 0.72); }

/* ── A SIGN FILLS ITS CARD ──────────────────────────────────────────
   Chief 2026-07-27, on a sign reading GRAPHICAL INTERFACES in a card
   three times its size: "many pixels are going to waste here."

   They were.  --titlebar-scale is the smallest of three bounds, and one
   of them is the zoom cap, which at nominal zoom is exactly 1 — so a
   title could never grow past its nominal font size regardless of the
   room around it.  That is right for a notecard, where the title shares
   the card with a body and must leave space for it, and wrong for a
   sign, where the title is the whole face.

   Lifting the cap in sign form leaves the two bounds that matter: the
   card's width over the title's natural line width, and the card's
   height over its text height.  The title then grows until one of the
   card's own dimensions stops it, which is what filling a sign means.

   A lerp on --form-note rather than a second rule: at notecard the added
   term is zero and the cap is exactly as before, so nothing about a
   notecard changes.  1000 is simply large enough to drop the cap out of
   the min() entirely. */
.node[data-has-title]:not(.fullpage) {
  /* How much of its card a sign's text occupies.  Chief 2026-07-27:
     "there is such a thing as too big — a big sign has good borders; a
     tiny sign has tiny borders."  Lifting the cap entirely let the text
     run to the card's edges with no margin at all.
     
     A FRACTION rather than a pixel inset is what makes the borders
     scale with the sign: at 0.82 the margin is always the same share of
     the card, so a large sign gets a generous border and a small one a
     proportionally small border, which is how painted signage is set.
     A fixed inset would do the opposite — swallow a small sign whole
     and leave a large one looking unmargined. */
  --sign-fill: 0.82;
  /* The cap becomes the FILL bound in sign form, and stays the zoom
     bound in notecard form.  A lerp on --form-note, so at notecard the
     second term vanishes and a notecard's title is capped exactly as it
     always was.  0.82 of whichever card dimension binds first is below
     both card bounds by construction, so it is what min() selects. */
  --titlebar-zoom-cap: calc(
    max(1, calc(1 / var(--cumulative-zoom, 1))) * var(--form-note, 1)
    + min(
        calc(var(--card-w-num, 100) / max(1, var(--natural-line-w-num, 100))),
        calc(var(--card-h-num, 100) / max(1, var(--natural-text-h-num, 16)))
      ) * var(--sign-fill, 0.82) * (1 - var(--form-note, 1))
  );
}
