/* =============================================================================
   BLACKOUT.CSS — the Vane blackout (ASTOR §14.6).

   For the handful of actions that take the page hostage: a Monday board pull is
   a synchronous walk of several hundred rows under the tenancy's own token, and
   for the thirty-to-sixty seconds it runs the page looked untouched. People
   clicked again, clicked away, or pressed back — and a pull that is interrupted
   leaves the whole TEAM on the older snapshot without anyone being told.

   So the screen goes dark and Vane's orb comes up on it. It is not decoration:
   the scrim is the control. Nothing on the page beneath it is clickable while it
   is raised, Escape does not dismiss it, and the copy says in words what will
   happen if you leave.

   Palette: this is the one surface in the product that is ink-on-ink rather than
   ink-on-bone, and the only place text is centred outside §7's Editorial Void —
   both because it is a takeover, not a page: there is no layout to sit inside and
   nothing else on screen to align to. It is the Vane console's dock composition
   (static/planning/vane.css), lit from the other side.

   Motion is ql-stitch (the indeterminate meter), astor-spin
   (the working arc) and astor-rise (the entrance) — the sanctioned three, §8.3.
   No new keyframes.

   Wiring lives in static/planning/busy.js; the markup in planning/templates/
   _blackout.html, rendered once per tools page from base_tools.html.
   ============================================================================= */
@layer components {

.blk {
  position: fixed;
  inset: 0;
  /* Above everything: the toast tray and the nav progress bar are 9999, the Vane
     widget 960. A blackout that something can paint over is not a blackout. */
  z-index: 10000;
  display: grid;
  place-items: center;
  padding: var(--space-6);
  /* DARKENED, not sealed. Cain, 2026-09-04, on the first solid-black build: *"you
     took blackout very literally which is cool, but i think i meant a little more
     like darken the screen rather than a full blackout."*

     The objection to a translucent scrim was real — at 96% opacity alone the page's
     own headings and buttons still read straight through, measured at 1440px, and a
     screen you can read the page through does not say STOP. The blur is what answers
     it: at 92% ink with an 8px backdrop blur the page survives as shape and colour and
     dies as text, so it is plainly still there and plainly not available. Opacity
     alone could not do both — measured at 1440px over a real table, 86%/6px still
     left the page heading legible.

     Ink rather than black: ink-on-ink is the palette (§2), and her wave function is
     light drawn on it. */
  background: color-mix(in srgb, var(--c-ink) 92%, transparent);
  color: var(--c-white);
  /* The scrim IS the interruption guard — it must eat every click, not pass one
     through to the button underneath. */
  pointer-events: auto;
  overscroll-behavior: contain;
  animation: astor-rise 0.3s ease both;
}
/* display:grid would otherwise beat the attribute, and the overlay would sit on
   every page permanently. */
.blk[hidden] { display: none; }

/* THE BLUR IS ON THE PAGE, NOT ON THE SCRIM. It was `backdrop-filter: blur(8px)` on `.blk`,
   and a backdrop blur is recomputed over the whole screen on EVERY frame anything above it
   moves — measured 2026-10-15 in Chromium at 1280x800: 15–20 fps with the orb running,
   60 fps with the blur off. That is the "stop motion" Cain saw (2026-10-14). A `filter` on
   the page beneath is drawn once and held, because the page does not move while it is
   covered: same 8px, same 92% ink, 60 fps. `:has` keeps the blackout itself out of it. */
body.blk-lock > :not(.blk):not(:has(.blk)) { filter: blur(8px); }

.blk__stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  max-width: 460px;
}

/* ── The orb ──────────────────────────────────────────────────────────────────
   v2, 2026-11-02 (https://claude.ai/artifact/5fvvkvhuMMAWWdFiMp7cQD). Cain: *"v2 is better"*,
   and the brief behind it: *"no sparkle particles, no dotted lines, no solid circle, a particle
   as a wave function"*. So there is no disc, no ring and no electron: the mark is the nucleus,
   and round it `.blk__field` carries |psi|^2 as one continuous density that busy.js `orbit()`
   draws each frame. Where the lobes are bright is where she is most likely to be. */
.blk__orb {
  position: relative;
  width: 168px;
  height: 168px;
  display: grid;
  place-items: center;
  margin-bottom: var(--space-8);
  /* Its own stacking context, so the field (1) and the nucleus (2) resolve here and
     cannot reach out into the page. */
  isolation: isolate;
  will-change: transform;      /* the rAF float lives on this node */
}
.blk__bloom,
.blk__spin {
  position: absolute;
  border-radius: 50%;            /* sanctioned: the orb is the one circle (§3) */
}
/* Her light. A soft bloom behind the field, breathing on the brand's 2.8s rhythm — the loop
   writes its opacity, so it rests dark. Cain, 2026-10-14: "343 guilty spark quality". */
.blk__bloom {
  inset: -44px;
  background: radial-gradient(circle,
              color-mix(in srgb, var(--c-white) 20%, transparent) 0%,
              color-mix(in srgb, var(--c-white) 7%, transparent) 40%,
              transparent 70%);
  opacity: 0;
  will-change: opacity;
  pointer-events: none;
}
/* The working arc — the sanctioned astor-spin spinner. It is the REDUCED-MOTION
   fallback and nothing else: when the rAF loop runs, the field already carries the
   motion, and a thing going round at another speed is noise. When the loop is refused
   (one still frame), this is the only moving indicator left and §14 requires one — so
   it turns on exactly there and is off here. */
.blk__spin {
  inset: -8px;
  border: 2px solid transparent;
  border-top-color: var(--c-white);
  opacity: 0;
  animation: astor-spin 0.9s linear infinite;
}
/* The wave function. 14px wider than the orb on every side, which is where the field's
   cut-off sits (r = 98 of 168): busy.js sizes its backing store off this box, so the
   geometry lives here once. Transparent until the loop's first frame — a script that
   never runs leaves the mark alone, not a stray shape. */
.blk__field {
  position: absolute;
  inset: -14px;
  width: calc(100% + 28px);
  height: calc(100% + 28px);
  z-index: 1;
  will-change: transform;        /* the lobes turn; the nucleus does not */
  pointer-events: none;
}
/* The nucleus — the mark, still and lit. The mock's nucleus does not pulse: the field is
   the motion, and a second rhythm in the middle of it is noise. */
.blk__mark {
  position: relative;
  z-index: 2;
  font-size: 40px;
  line-height: 0;
  color: var(--c-white);
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--c-white) 55%, transparent));
}
.blk__mark svg { display: block; }

/* ── Copy ─────────────────────────────────────────────────────────────────── */
.blk__eyebrow {
  margin: 0 0 var(--space-3);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--c-white) 52%, transparent);
}
.blk__title {
  margin: 0 0 var(--space-3);
  font-size: 19px;
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.35;
  color: var(--c-white);
}
.blk__body {
  margin: 0;
  font-size: 13px;
  font-weight: 300;
  line-height: 1.7;
  color: color-mix(in srgb, var(--c-white) 62%, transparent);
}

/* ── Indeterminate meter (§8.3: ql-stitch is the sanctioned indeterminate fill) ── */
.blk__meter {
  width: 220px;
  height: 2px;
  margin-top: var(--space-7);
  background: color-mix(in srgb, var(--c-white) 12%, transparent);
}
.blk__meter-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--c-white);
  opacity: 0.55;
  animation: ql-stitch 2.8s ease-in-out infinite;
}

/* ── Elapsed line — the honest number. Not a fake percentage. ─────────────── */
.blk__timer {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: color-mix(in srgb, var(--c-white) 46%, transparent);
}
/* The Live Stitch dot: 5px square, ql-stitch, never a circle (§8.3).
   WHITE here, and this is the one place in the product it is not oxblood. Oxblood
   on black measures a 1.6:1 contrast ratio — not a quiet dot but an invisible one,
   and an indicator nobody can see is not an indicator. §13 keeps
   oxblood as the only SIGNAL colour — this is a liveness pulse on an ink ground,
   the same job §14.5 already gives a solid white dot. The rule's purpose survives;
   its default value does not ([[feedback_a_written_decision_is_not_a_veto_over_legibility]]). */
.blk__dot {
  width: 5px;
  height: 5px;
  flex-shrink: 0;
  background: var(--c-white);
  opacity: 0.7;
  animation: ql-stitch 2.8s ease-in-out infinite;
}

/* ── The release (§8.2 ghost button, inverted onto the scrim) ─────────────── */
.blk__release {
  margin-top: var(--space-7);
  padding: 6px 32px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.05em;
  color: var(--c-white);
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--c-white) 40%, transparent);
  border-radius: 0;
  cursor: pointer;
  animation: astor-rise 0.3s ease both;
}
.blk__release:hover { opacity: 0.7; }
.blk__release:focus-visible { outline: 2px solid var(--c-white); outline-offset: 3px; }
.blk__release[hidden] { display: none; }

.blk__release-note {
  margin: var(--space-3) 0 0;
  font-size: 11px;
  line-height: 1.6;
  color: color-mix(in srgb, var(--c-white) 40%, transparent);
}
.blk__release-note[hidden] { display: none; }

/* Scroll lock — set on <body> while the blackout is raised, so a trackpad flick
   cannot reveal the page behind it. */
.blk-lock { overflow: hidden; }

@media (max-width: 640px) {
  .blk__orb { width: 124px; height: 124px; margin-bottom: var(--space-6); }
  .blk__mark { font-size: 30px; }
  .blk__title { font-size: 17px; }
  .blk__meter { width: 180px; }
}

/* ── The same orb, FLOATING on a light page ─────────────────────────────────────
   The covenant alert on /lender/funding/ is a second instance of this orb (Cain,
   2026-09-16: "as good as if not better than loading screen vane ... loading screen vane
   has the orbiting electrons"). Same markup (`_vane_orb.html`), same loop (`busy.js`
   `orbit()`), and its geometry stays HERE beside the phone variant above rather than as a
   redrawn copy somewhere else.
   · `--float` is the size: an 88px box, which busy.js reads to scale the field and the
     wander (88 / 168).
   · `--on-light`: this composition is white light on an ink scrim, and white on a white
     page is nothing. So the field (drawn by busy.js), the nucleus and the working arc
     take ink, and the bloom goes. */
.blk__orb--float { width: 88px; height: 88px; margin: 0; }
.blk__orb--float .blk__spin     { inset: -4px; }
.blk__orb--float .blk__field    { inset: -7px; width: calc(100% + 14px); height: calc(100% + 14px); }
.blk__orb--float .blk__mark     { font-size: 22px; }
.blk__orb--on-light .blk__spin  { border-top-color: var(--c-ink); }
.blk__orb--on-light .blk__mark  { color: var(--c-ink); filter: none; }
/* Light is for the dark: on a white page a white glow is nothing, so it goes. The field
   is drawn in ink there by busy.js, which reads this same modifier. */
.blk__orb--on-light .blk__bloom { display: none; }

/* The blackout's orb takes a click: it sends her flying (busy.js `flight()`), and a
   second click brings her home. The pointer says so only where she can actually fly —
   under reduced motion the click does nothing, so the cursor must not promise it. Scoped
   under `.blk`, so the covenant alert's orb (same markup, not in a `.blk`) is untouched. */
@media (prefers-reduced-motion: no-preference) {
  .blk .blk__orb { cursor: pointer; }
}

/* Her wake (busy.js `wake()`): one canvas the loop draws on while she flies — a long-exposure
   trail and her glow stretched along her velocity (Cain, 2026-10-22: "I want B & C"). It sits
   under her and under the copy's layer; it takes no clicks. */
.blk__trail {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* The galaxy she flies through (busy.js `sky()`): one canvas, beneath her wake and the
   copy, drawn from the same loop. Decoration; it takes no clicks. */
.blk__sky,
.blk__galaxy {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* The faint glow of far nebulae: static. On the galaxy's layer, which repaints every 250 ms —
   on the star layer, repainted every frame, it cost the flight half its frames (1280, measured
   2026-10-20). */
.blk__galaxy {
  background:
    radial-gradient(ellipse 55% 40% at 78% 72%, color-mix(in srgb, var(--c-white) 4%, transparent), transparent 70%),
    radial-gradient(ellipse 40% 30% at 12% 85%, color-mix(in srgb, var(--c-white) 3%, transparent), transparent 70%);
}

/* Honour the OS setting: the blackout still appears and still blocks, it just
   stops moving. A user who asked for no motion still needs the interruption
   guard — the arc is a spinner, not decoration, so it is the LAST thing to go. */
@media (prefers-reduced-motion: reduce) {
  .blk,
  .blk__meter-fill,
  .blk__dot { animation: none; }
  /* The field stops: busy.js draws a single frame under this setting, so the wave
     function is placed and then stands still. That would leave the screen entirely
     motionless, so the arc comes back, slowed — a "working" screen with nothing moving
     on it is indistinguishable from a hung one. */
  .blk__spin { opacity: 0.5; animation-duration: 2.4s; }
  /* THE FLOATING ALERT IS NOT A WORKING SCREEN. Nothing is in progress when a covenant
     breaks, and `.blk__spin` means "working" — an alert that spins reads as loading
     (reviewer's ruling, 2026-09-16). So the floating orb takes the loop's single placement
     frame, the field still, and no arc. The blackout's rule above is
     untouched. */
  .blk__orb--float .blk__spin { opacity: 0; animation: none; }
}

} /* end @layer components */
