/* scifi-ui  ...  see README.md
 * Drop this in, wrap any media in <div class="holoframe">, load hologram.js.
 * Tokens you will want to change are at the top. */

:root {
  --holo-line: 196 228 255;   /* hairlines and readouts */
  --holo-beam: 178 216 248;   /* the running beam core  */
  --holo-glow:  74 150 224;   /* its soft outer glow    */
  --holo-warm: 232 169  58;   /* the single warm accent */
  --holo-ink:  239 244 251;
  --holo-dim:  154 162 177;
  --holo-panel: 15 18 24;

  /* a second accent, teal leaning cyan. Deliberately not --holo-line: the
     card and the section rail read cooler and harder than the rest of the
     library, and that difference is the point. */
  --holo-cyan: 126 224 255;
  /* the violet the dialog rim cycles through, scoped to that one component */
  --holo-viol: 138  96 230;
}

/* A projected panel rather than a black bar: cool blue instead of true black,
     faint scanlines, a lit lower edge, and bloom where it meets the render. */
  .heropanel {
    position: absolute; top: 0; left: 0; right: 0; z-index: 2;
    background: linear-gradient(to bottom,
      rgba(6,13,24,.975) 0%, rgba(7,15,28,.955) 58%,
      rgba(9,19,34,.80) 84%, rgba(10,20,36,.26) 96%, transparent 100%);
    border-bottom: 1px solid rgba(112,190,255,.30);
    box-shadow: 0 1px 26px rgba(62,150,240,.16);
  }

.heropanel::after {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    background-image: repeating-linear-gradient(0deg,
      rgba(150,205,255,.05) 0 1px, transparent 1px 3px);
  }

.heropanel .inner {
    position: relative; z-index: 1; max-width: 800px; margin: 0 auto;
    padding: clamp(20px,4vw,40px) clamp(16px,5vw,32px) clamp(22px,4vw,38px);
  }

.heropanel h1 { margin: 0 0 5px; }

.heropanel .lede { margin: 0 0 16px; color: #A9BDD6; }

.heropanel .cite {
    margin: 0; background: rgba(14,26,44,.62);
    border: 1px solid rgba(96,160,230,.20);
  }

.holo {
    background: linear-gradient(158deg, rgba(18,30,52,.86), rgba(9,14,26,.74));
    border: 1px solid rgba(88,148,238,.34); border-radius: 3px;
    padding: 13px 15px 12px; max-width: 268px;
    box-shadow: 0 0 22px rgba(0,0,0,.55);
  }

.holo .k {
    font-size: 10px; letter-spacing: .19em; text-transform: uppercase;
    color: #6BA4E6; margin: 0 0 5px;
  }

.holo .v {
    font-size: 27px; font-weight: 500; line-height: 1.05; margin: 0;
    font-variant-numeric: tabular-nums; color: #EFF4FB;
  }

.holo .d { font-size: 12.5px; color: #93A3BC; margin: 5px 0 0; line-height: 1.5; }

.holo + .holo { margin-top: 10px; }

/* the readout is a tap host (`.holo` is in the tap list), so a tap already
   puts .holo-on on it; without a rule to answer that class the tap did
   nothing. Now it lights the same way every other panel does: the edge comes
   up, a soft bloom lifts it forward, and the number caps a glow, so a point or
   a tap reads as the panel coming alive rather than dead ornament. */
.holo {
    transition: border-color .42s ease, box-shadow .42s ease, transform .42s ease;
  }
.holo:hover, .holo:focus-within, .holo.holo-on {
    border-color: rgba(150,208,255,.62);
    box-shadow: 0 12px 30px rgba(0,0,0,.5), 0 0 42px rgba(62,150,240,.20),
                inset 0 1px 0 rgba(204,232,255,.16);
  }
.holo:hover .v, .holo:focus-within .v, .holo.holo-on .v {
    color: #F4F9FF; text-shadow: 0 0 20px rgba(120,190,255,.60);
  }
.holo:hover .k, .holo:focus-within .k, .holo.holo-on .k {
    color: #8FC0FF;
  }
@media (prefers-reduced-motion: reduce) {
    .holo { transition: none; }
  }

@media (min-width: 760px) {
    .holo-tr { position: absolute; top: 1%; right: 0; }
    .holo-bl { position: absolute; bottom: 3%; left: 0; }
  }

@media (max-width: 759px) {
    .holo { max-width: none; margin-top: 10px; }
    .holo-tr { margin-bottom: 10px; }
  }

/* ---- hologram UI ---------------------------------------------------------
     Every piece of media sits in a frame that reads like an instrument readout:
     corner brackets draw in, a scan line passes once, and a cyan rim lifts it off
     the page. Transform and opacity only, so none of it costs layout. */
  /* the trace runs a beam a good 22px outside the frame, plus its glow, so the
     frame needs more vertical room than a plain figure would: 34px keeps the
     light clear of the paragraph above and the caption below rather than letting
     it wash over the text. */
  .holoframe { position: relative; isolation: isolate; margin: 34px 0; }

/* the frame owns the fit. Without this the media lays itself out at its
     intrinsic width and a 1400px render scrolls the page sideways */
  .holoframe img, .holoframe video {
    display: block; width: 100%; max-width: 100%; height: auto;
    transition: filter .5s ease, transform .55s cubic-bezier(.22,.9,.28,1);
    will-change: transform;
  }

.holoframe:hover img, .holoframe:hover video,
  .holoframe:focus-within img, .holoframe:focus-within video,
  .holoframe.holo-on img, .holoframe.holo-on video {
    transform: translateY(-3px) scale(1.006);
  }

.holoframe:hover img, .holoframe:hover video,
  .holoframe:focus-within img, .holoframe:focus-within video,
  .holoframe.holo-on img, .holoframe.holo-on video {
    filter: brightness(1.03) saturate(1.03)
            drop-shadow(0 6px 18px rgba(0,0,0,.45))
            drop-shadow(0 0 12px rgba(62,150,240,.14));
  }

/* the readouts on the full bleed cell pick up the same idle shimmer */
  .holo { transition: border-color .4s ease, box-shadow .4s ease, transform .4s ease; }

.holo:hover, .holo:focus-within, .holo.holo-on {
    border-color: rgba(120,190,255,.75); transform: translateY(-2px);
    box-shadow: 0 0 26px rgba(0,0,0,.6), 0 0 22px rgba(62,150,240,.18);
  }

/* the particle trace draws itself on a canvas; see the script at the foot */
  .holoframe canvas.trace {
    position: absolute; inset: -22px; width: calc(100% + 44px);
    height: calc(100% + 44px); z-index: 3; pointer-events: none;
  }

/* the play affordance. The native control is small and lands on top of the
     colour key burned into some of these clips, so the frame carries its own. */
  .playbadge {
    position: absolute; left: 50%; top: 50%; z-index: 5;
    transform: translate(-50%,-50%); width: 74px; height: 74px;
    border-radius: 50%; cursor: pointer; padding: 0;
    border: 2px solid rgba(150,215,255,.85);
    background: radial-gradient(circle at 50% 50%,
      rgba(20,45,80,.72), rgba(8,16,30,.62));
    box-shadow: 0 0 0 6px rgba(62,150,240,.10), 0 0 26px rgba(62,150,240,.35);
    transition: transform .25s ease, box-shadow .25s ease,
                opacity .25s ease, border-color .25s ease;
  }

.playbadge::before {
    content: ""; position: absolute; left: 52%; top: 50%;
    transform: translate(-50%,-50%);
    border-left: 20px solid rgba(226,244,255,.96);
    border-top: 13px solid transparent; border-bottom: 13px solid transparent;
  }

.playbadge:hover, .playbadge:focus-visible, .playbadge.holo-on {
    transform: translate(-50%,-50%) scale(1.07);
    border-color: rgba(190,235,255,1);
    box-shadow: 0 0 0 8px rgba(62,150,240,.14), 0 0 34px rgba(62,150,240,.55);
  }

/* the badge is also reached by keyboard, so the ring has to be real. A bare
     var() with no fallback computes to nothing and the ring vanishes. */
  .playbadge:focus-visible {
    outline: 2px solid rgb(var(--holo-beam, 178 216 248));
    outline-offset: 4px;
  }

.holoframe.playing .playbadge { opacity: 0; pointer-events: none; }

/* ---- HUD annotation ------------------------------------------------------
     Hairlines and small readouts, in the language of a targeting overlay. Every
     value shown is real: media dimensions and duration are read off the element,
     and the science figures come from measurements on the page. */
  .hud {
    position: absolute; inset: -1px; z-index: 4; pointer-events: none;
    opacity: 0; transition: opacity .34s ease;
  }

.holoframe:hover .hud, .holoframe:focus-within .hud,
  .holoframe.holo-on .hud { opacity: 1; }

.hud i {
    position: absolute; width: 13px; height: 13px;
    border: 1px solid rgba(196,228,255,.5);
    transition: transform .42s cubic-bezier(.2,.8,.25,1);
  }

.hud i.tl { top: 0; left: 0; border-right: 0; border-bottom: 0; transform: translate(4px,4px); }

.hud i.tr { top: 0; right: 0; border-left: 0; border-bottom: 0; transform: translate(-4px,4px); }

.hud i.bl { bottom: 0; left: 0; border-right: 0; border-top: 0; transform: translate(4px,-4px); }

.hud i.br { bottom: 0; right: 0; border-left: 0; border-top: 0; transform: translate(-4px,-4px); }

.holoframe:hover .hud i.tl, .holoframe:focus-within .hud i.tl,
  .holoframe.holo-on .hud i.tl { transform: translate(-7px,-7px); }

.holoframe:hover .hud i.tr, .holoframe:focus-within .hud i.tr,
  .holoframe.holo-on .hud i.tr { transform: translate(7px,-7px); }

.holoframe:hover .hud i.bl, .holoframe:focus-within .hud i.bl,
  .holoframe.holo-on .hud i.bl { transform: translate(-7px,7px); }

.holoframe:hover .hud i.br, .holoframe:focus-within .hud i.br,
  .holoframe.holo-on .hud i.br { transform: translate(7px,7px); }

.hud b {
    position: absolute; font-weight: 500; font-size: 9.5px; letter-spacing: .17em;
    text-transform: uppercase; color: rgba(190,222,252,.82);
    font-variant-numeric: tabular-nums; white-space: nowrap;
    text-shadow: 0 1px 6px rgba(0,0,0,.8);
  }

.hud b.a { top: 9px; left: 22px; }

.hud b.b { top: 9px; right: 22px; color: rgba(232,169,58,.9); }

/* b.c carries the measured science value with its unit, e.g. "337 × 266 × 92
   µm". It must NOT be uppercased: text-transform corrupts the micro sign into a
   Greek capital Mu, so µm renders as MM, a different unit off by a thousand per
   axis. This is the same trap the readout component's header warns about, and
   it was live here. a and b stay uppercase; only the value opts out. */
  .hud b.c { bottom: 9px; left: 22px; text-transform: none; letter-spacing: .1em; }

/* The HUD used to go on touch as well, on the grounds that a hover treatment
     has no job there. It has one now: a tap puts .holo-on on the frame and the
     brackets push out. The canvas used to be hidden here too, on the theory
     that its 22px overhang a side would push past the viewport. It does not:
     the frame sits inside the wrap's 30px gutter, so the 22px overhang lands in
     that gutter with 8px to spare, and the beam is one of the nicer things on
     the page to lose on a phone. So it runs on touch now, struck by the tap
     that lights the frame; only reduced motion still turns it off, below. */
  @media (max-width: 700px) {
    /* 30px, not 10: now the beam runs on touch too, its 22px overhang needs
       clearance from the caption and the copy above or it paints over them */
    .holoframe { margin: 30px 0; }
  }

@media (prefers-reduced-motion: reduce) {
    .hud, .hud i { transition: none; }
    .holoframe img, .holoframe video, .holo, h2::before { transition: none; }
    .holoframe:hover img, .holoframe:hover video,
    .holoframe.holo-on img, .holoframe.holo-on video { transform: none; }
    .playbadge { transition: none; }
    .holoframe canvas.trace { display: none; }
  }

/* ---- 5. step rail --------------------------------------------------------
     A counter in tabular caps, a hairline rail, the run so far filled and
     glowing, a brighter dot at the step you are on, dimmer ticks after it.
     Write one empty div with data-steps and hologram.js builds the parts.
     Any number of steps, clickable, arrow keys, and it reports the current
     step back on a holobar:change event. */
  .holobar {
    display: flex; align-items: center; gap: 12px;
    font: 500 9px/1 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  }

.holobar-count {
    flex: 0 0 auto; letter-spacing: .25em; text-transform: uppercase;
    font-variant-numeric: tabular-nums; white-space: nowrap;
    color: rgb(var(--holo-ink, 239 244 251) / .58);
    transition: color .3s ease;
  }

.holobar-count s { text-decoration: none; color: rgb(var(--holo-ink, 239 244 251) / .28); }

.holobar:hover .holobar-count, .holobar:focus-within .holobar-count,
  .holobar.holo-on .holobar-count {
    color: rgb(var(--holo-ink, 239 244 251) / .85);
  }

/* 8px of side room so the end tick boxes cannot push past the wrapper.
     grab, because the rail is draggable as well as clickable, and touch-action
     none so a drag that starts on the rail is not stolen by the page scroll. */
  .holobar-rail {
    position: relative; flex: 1 1 auto; height: 2px; min-width: 44px; margin: 0 8px;
    cursor: grab; touch-action: none;
  }

.holobar-rail::before {
    content: ""; position: absolute; inset: 0; border-radius: 999px;
    background: rgb(var(--holo-ink, 239 244 251) / .11);
  }

.holobar-fill {
    position: absolute; top: 0; bottom: 0; left: 0; border-radius: 999px;
    width: calc(var(--holobar-progress, 0) * 100%);
    background: linear-gradient(90deg,
      rgb(var(--holo-beam, 178 216 248) / .7), rgb(var(--holo-beam, 178 216 248)));
    box-shadow: 0 0 6px rgb(var(--holo-beam, 178 216 248) / .55);
    transition: width .7s cubic-bezier(.16,1,.3,1);
  }

/* the hit area is 16px so a finger can find it. Only the dot inside is small */
  .holobar-tick {
    position: absolute; top: 50%; width: 16px; height: 16px;
    margin: 0; padding: 0; border: 0; background: none; cursor: grab;
    transform: translate(-50%,-50%); touch-action: none;
    display: flex; align-items: center; justify-content: center;
  }

.holobar-tick i {
    display: block; width: 3px; height: 3px; border-radius: 50%;
    background: rgb(var(--holo-ink, 239 244 251) / .2);
    transition: width .3s ease, height .3s ease,
                background .3s ease, box-shadow .3s ease;
  }

.holobar-tick[data-done] i { background: rgb(var(--holo-beam, 178 216 248) / .72); }

.holobar-tick[aria-current] i {
    width: 7px; height: 7px; background: rgb(var(--holo-beam, 178 216 248));
    box-shadow: 0 0 7px rgb(var(--holo-beam, 178 216 248) / .95);
  }

.holobar-tick:hover i, .holobar-tick:focus-visible i,
  .holobar-tick.holo-on i {
    width: 6px; height: 6px; background: rgb(var(--holo-beam, 178 216 248) / .9);
  }

.holobar-tick:focus-visible {
    outline: 2px solid rgb(var(--holo-beam, 178 216 248) / .9);
    outline-offset: 0; border-radius: 3px;
  }

/* mid drag the fill has to track the pointer, so the 700ms ease that makes a
     click feel considered is dropped for the duration, and the cursor changes
     on the whole control rather than only on whatever is under the pointer */
  .holobar.is-dragging .holobar-rail,
  .holobar.is-dragging .holobar-tick { cursor: grabbing; }

.holobar.is-dragging .holobar-fill { transition: none; }

.holobar.is-dragging .holobar-tick i {
    transition: background .3s ease, box-shadow .3s ease;
  }

@media (prefers-reduced-motion: reduce) {
    .holobar-fill, .holobar-tick i, .holobar-count { transition: none; }
  }

/* ---- 6. loading state ----------------------------------------------------
     A cell draws itself in while you wait: apical arbor, basal arbor half a
     cycle behind it so the two trees do not pulse in lockstep, a soma that
     breathes, and a bar underneath that reports real state when you have a
     total to report. A loading state is the one place a loop is honest, so
     this is the only piece in this file that repeats. */
  .holoload { display: flex; flex-direction: column; align-items: center; gap: 16px; }

.holoload-figure {
    position: relative; display: grid; place-items: center;
    width: var(--holoload-size, 132px);
  }

.holoload-figure::before {
    content: ""; position: absolute; inset: 14% 10%; border-radius: 50%;
    background: rgb(var(--holo-glow, 74 150 224) / .3); filter: blur(24px);
  }

.holoload-glyph {
    position: relative; display: block; width: 100%; height: auto;
    color: rgb(var(--holo-beam, 178 216 248));
    /* two layers of glow, the near one to define the stroke and the wide one to
       light the space around it, the way the login box lights its own arbor */
    filter: drop-shadow(0 0 6px rgb(var(--holo-beam, 178 216 248) / .7))
            drop-shadow(0 0 16px rgb(var(--holo-beam, 178 216 248) / .28));
  }

/* each path carries pathLength="1", so its length is normalised to 1 no matter
   how long it really is. A dash of 1 with the offset trimmed 1 -> 0 then draws
   every branch as a proper trim path, growing from its soma end to its tip at
   the same rate, rather than the old fixed 200 dash that wiped the short
   branches on in a single frame. */
  .holoload-glyph .arbor path {
    stroke-dasharray: 1;
    animation: holo-load-draw 2.6s cubic-bezier(.4,0,.2,1) infinite;
  }

/* every path is authored starting at the soma, so the draw grows the tree
   outward from the cell body rather than in toward it */
  .holoload-glyph .arbor.basal path { animation-delay: 1.3s; }

.holoload-glyph .soma {
    transform-box: fill-box; transform-origin: center;
    animation: holo-load-pulse 1.6s ease-in-out infinite;
  }

.holoload-glyph .soma-halo {
    transform-box: fill-box; transform-origin: center; opacity: .18;
    animation: holo-load-halo 1.6s ease-in-out infinite;
  }

/* the signal. Dots ride in along the dendrites, through the soma and out the
   axon, the login box's travelling particle idea run along a real morphology.
   Struck in the cyan accent so the moving light reads against the cooler
   arbor. components/loader-neuron.js builds the dots and drives their position;
   here is only their colour and glow. */
  .holoload-glyph .signal-dot {
    fill: rgb(var(--holo-cyan, 126 224 255));
    filter: drop-shadow(0 0 5px rgb(var(--holo-cyan, 126 224 255)));
  }

/* when loader-neuron.js is driving the figure it owns every offset and the soma
   transform frame by frame, so the CSS loops step aside. The CSS draw/pulse
   above stays as the no-JS fallback (a cell that still draws itself in). */
  .holoload-live .holoload-glyph .arbor path { animation: none; }
  .holoload-live .holoload-glyph .soma,
  .holoload-live .holoload-glyph .soma-halo { animation: none; }

@keyframes holo-load-halo {
    0%, 100% { transform: scale(1);    opacity: .15; }
    50%      { transform: scale(1.28); opacity: .3; }
  }

@keyframes holo-load-draw {
    0%   { stroke-dashoffset: 1; opacity: .2; }
    35%  { stroke-dashoffset: 0; opacity: 1; }
    75%  { stroke-dashoffset: 0; opacity: 1; }
    100% { stroke-dashoffset: 1; opacity: .2; }
  }

@keyframes holo-load-pulse {
    0%, 100% { transform: scale(1);    filter: drop-shadow(0 0 6px currentColor); }
    50%      { transform: scale(1.18); filter: drop-shadow(0 0 16px currentColor); }
  }

.holoload-label {
    margin: 0; font-size: 10px; letter-spacing: .4em; text-transform: uppercase;
    color: rgb(var(--holo-ink, 239 244 251) / .48);
  }

.holoload-bar {
    position: relative; width: var(--holoload-bar, 176px); max-width: 100%;
    height: 2px; border-radius: 999px; overflow: hidden;
    background: rgb(var(--holo-ink, 239 244 251) / .11);
  }

.holoload-bar i {
    position: absolute; top: 0; bottom: 0; left: 0; border-radius: 999px;
    width: calc(var(--holoload-progress, 0) * 100%);
    background: rgb(var(--holo-beam, 178 216 248));
    box-shadow: 0 0 12px rgb(var(--holo-beam, 178 216 248) / .8);
    transition: width .3s ease;
  }

.holoload[data-indeterminate] .holoload-bar i {
    width: 34%; animation: holo-load-sweep 1.4s ease-in-out infinite;
  }

@keyframes holo-load-sweep {
    0%   { left: -34%; }
    100% { left: 100%; }
  }

.holoload-count {
    margin: 0; font: 500 11px/1 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
    letter-spacing: .25em; text-transform: uppercase;
    font-variant-numeric: tabular-nums;
    color: rgb(var(--holo-ink, 239 244 251) / .58);
  }

/* with motion off the cell is simply drawn, and the indeterminate bar stops
     pretending to know something it does not */
  @media (prefers-reduced-motion: reduce) {
    .holoload-glyph .arbor path { stroke-dasharray: none; animation: none; }
    .holoload-glyph .soma, .holoload-glyph .soma-halo { animation: none; }
    /* the travelling dots are motion with no information in them, so they go
       entirely rather than sitting frozen on the arbor */
    .holoload-glyph .signal-dot { display: none; }
    .holoload-bar i { transition: none; }
    .holoload[data-indeterminate] .holoload-bar i {
      width: 100%; animation: none; opacity: .35; box-shadow: none;
    }
  }

/* ---- 7. panel that boots up ----------------------------------------------
     Add holoboot to any panel. hologram.js drops in two decorative spans and
     adds is-online when the panel first scrolls into view: a twin tip sweep
     runs the border, an inner backlight swells and tapers, and the panel
     itself resolves out of a blur. Once, never on a loop. Both spans sit at
     z-index -1, above the panel background and below its content, so the
     host keeps its own border, shadow and pseudo elements. */
  @property --holo-ring-angle {
    syntax: "<angle>"; inherits: false; initial-value: 45deg;
  }

.holoboot { position: relative; isolation: isolate; }

.holoboot > .holoboot-ring, .holoboot > .holoboot-glow {
    position: absolute; inset: 0; border-radius: inherit;
    pointer-events: none; z-index: -1; opacity: 0;
  }

/* two bright heads 180 degrees apart, each running half the perimeter, drawn
     only on the 1px ring by excluding the content box from the mask */
  .holoboot > .holoboot-ring {
    padding: 1px;
    background:
      conic-gradient(from var(--holo-ring-angle, 45deg),
        rgb(var(--holo-beam, 178 216 248) / 1) 0deg,
        rgb(var(--holo-beam, 178 216 248) / .45) 14deg,
        rgb(var(--holo-beam, 178 216 248) / 0) 30deg,
        transparent 30deg, transparent 330deg,
        rgb(var(--holo-beam, 178 216 248) / 0) 330deg,
        rgb(var(--holo-beam, 178 216 248) / .45) 346deg,
        rgb(var(--holo-beam, 178 216 248) / 1) 360deg),
      conic-gradient(from calc(var(--holo-ring-angle, 45deg) + 180deg),
        rgb(var(--holo-beam, 178 216 248) / 1) 0deg,
        rgb(var(--holo-beam, 178 216 248) / .45) 14deg,
        rgb(var(--holo-beam, 178 216 248) / 0) 30deg,
        transparent 30deg, transparent 330deg,
        rgb(var(--holo-beam, 178 216 248) / 0) 330deg,
        rgb(var(--holo-beam, 178 216 248) / .45) 346deg,
        rgb(var(--holo-beam, 178 216 248) / 1) 360deg);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
  }

/* boot on hover or focus, and again on each new hover, because a boot you
   cannot replay is a boot you only ever see once by accident. On touch the
   replay comes from a tap: not the sticky .holo-on class, which stays on and so
   could never restart the animation, but is-online, which the script lifts and
   re-adds on each holotap:on. That is why a tap replays every time now, not
   only the first. Hover and focus stay pure CSS. */
.holoboot.is-online, .holoboot:hover, .holoboot:focus-within {
  animation: holo-boot-in 900ms cubic-bezier(.16,1,.3,1) both;
}

.holoboot.is-online > .holoboot-ring,
.holoboot:hover > .holoboot-ring,
.holoboot:focus-within > .holoboot-ring {
  animation: holo-ring-sweep 1500ms linear forwards;
}

.holoboot.is-online > .holoboot-glow,
.holoboot:hover > .holoboot-glow,
.holoboot:focus-within > .holoboot-glow {
  animation: holo-panel-online 1500ms linear forwards;
}

@keyframes holo-boot-in {
    from { opacity: 0; transform: translateY(7px) scale(.994);
           filter: blur(7px) brightness(1.5); }
    to   { opacity: 1; transform: translateY(0) scale(1);
           filter: blur(0) brightness(1); }
  }

/* linear timing with a multi stop tail, so nothing drops to zero abruptly */
  @keyframes holo-ring-sweep {
    0%   { --holo-ring-angle: 45deg;  opacity: 1;
           filter: drop-shadow(0 0 4px rgb(var(--holo-beam, 178 216 248) / .55)); }
    50%  { --holo-ring-angle: 225deg; opacity: 1;
           filter: drop-shadow(0 0 10px rgb(var(--holo-beam, 178 216 248) / .95)); }
    62%  { opacity: .85; }
    75%  { opacity: .55; }
    88%  { opacity: .22; }
    100% { --holo-ring-angle: 225deg; opacity: 0;
           filter: drop-shadow(0 0 0 rgb(var(--holo-beam, 178 216 248) / 0)); }
  }

@keyframes holo-panel-online {
    0%   { opacity: 1; box-shadow: inset 0 0 0 0 rgb(var(--holo-beam, 178 216 248) / 0); }
    50%  { opacity: 1; box-shadow: inset 0 0 80px -22px rgb(var(--holo-beam, 178 216 248) / .5); }
    75%  { opacity: .7; box-shadow: inset 0 0 52px -22px rgb(var(--holo-beam, 178 216 248) / .3); }
    100% { opacity: 0; box-shadow: inset 0 0 0 0 rgb(var(--holo-beam, 178 216 248) / 0); }
  }

@media (prefers-reduced-motion: reduce) {
    .holoboot.is-online, .holoboot:hover, .holoboot:focus-within { animation: none; }
    .holoboot > .holoboot-ring, .holoboot > .holoboot-glow { display: none; }
  }

/* ---- 8. hairline underline ----------------------------------------------
     Draws itself left to right the first time its heading comes into view.
     One pass. Sits on a real element so it inherits the text colour box. */
  .holounder { position: relative; display: inline-block; padding-bottom: 5px; }

.holounder::after {
    content: ""; position: absolute; left: 0; bottom: 0; width: 100%; height: 1px;
    background: linear-gradient(90deg,
      rgb(var(--holo-beam, 178 216 248) / .85) 0%,
      rgb(var(--holo-beam, 178 216 248) / .4) 60%, transparent 100%);
    filter: drop-shadow(0 0 3px rgb(var(--holo-beam, 178 216 248) / .6));
    transform-origin: left center;
  }

/* the resting state is the finished line, so a page with the script stripped
     out, or a tab that was never looked at, still shows the underline. It draws
     again from hover, and from a tap on every tap. The tap path alternates two
     trigger classes, is-drawn and is-redraw, whose animations are identical but
     differently named: a single class re-added cannot be trusted to restart a
     pseudo element's animation from inside a pointer handler, but a change of
     the computed animation name always restarts it. Hover stays pure CSS. */
  .holounder.is-drawn::after,
  .holounder.is-redraw::after,
  .holounder:hover::after {
    animation: holo-underline-draw 900ms cubic-bezier(.16,1,.3,1) both;
  }
  .holounder.is-redraw::after { animation-name: holo-underline-redraw; }

@keyframes holo-underline-draw {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }
@keyframes holo-underline-redraw {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }

@media (prefers-reduced-motion: reduce) {
    .holounder.is-drawn::after,
    .holounder.is-redraw::after,
    .holounder:hover::after { animation: none; }
  }

/* ---- 9. scan sweep ------------------------------------------------------
     A thin line passes across a panel once when you point at it or tab into
     it. The band is a background at a fixed height on a full size overlay,
     so one pass covers the panel whatever its height. */
  .holosweep { position: relative; overflow: hidden; }

.holosweep::after {
    content: ""; position: absolute; inset: 0; z-index: 6;
    pointer-events: none; opacity: 0;
    background-image: linear-gradient(180deg,
      rgb(var(--holo-beam, 178 216 248) / 0) 0%,
      rgb(var(--holo-beam, 178 216 248) / .55) 50%,
      rgb(var(--holo-beam, 178 216 248) / 0) 100%);
    background-size: 100% 14px; background-repeat: no-repeat;
    filter: drop-shadow(0 0 4px rgb(var(--holo-beam, 178 216 248) / .6));
  }

.holosweep:hover::after, .holosweep:focus-within::after,
  .holosweep.holo-on::after {
    animation: holo-sweep 820ms cubic-bezier(.16,1,.3,1);
  }

@keyframes holo-sweep {
    0%   { transform: translateY(-14px); opacity: 1; }
    100% { transform: translateY(100%);  opacity: 0; }
  }

/* the sweep used to be off on touch as well. A tap runs it now, so the only
     thing that stops it is the motion preference. */
  @media (prefers-reduced-motion: reduce) {
    .holosweep:hover::after, .holosweep:focus-within::after,
    .holosweep.holo-on::after { animation: none; }
  }

/* ---- 10. glass surface --------------------------------------------------
     The one utility worth lifting whole: a blurred, slightly saturated pane.
     Saturating the backdrop is what stops it going grey over colour. */
  .hologlass {
    background: rgb(var(--holo-panel, 15 18 24) / .42);
    border: 1px solid rgb(var(--holo-line, 196 228 255) / .1);
    backdrop-filter: blur(20px) saturate(1.4);
    -webkit-backdrop-filter: blur(20px) saturate(1.4);
  }

/* ---- 11. hologram dialog -----------------------------------------------
     The EyeWire II sign in modal, reproduced, with every trace of
     authentication taken out of it. A real dialog element opened with
     showModal, so the focus trap and Escape are the user agent's job rather
     than a script's.

     Exactly two things loop, because both are ambient rather than event
     driven: the particle field drifts, and the rim gradient cycles blue,
     violet and teal. Everything else runs once, and since a closed dialog is
     display none, opening it starts all of them over. */
  dialog.holodialog {
    /* position: fixed, NOT relative. A modal dialog is placed in the top layer
       and the user agent anchors it to the viewport; overriding that with
       `relative` drops it back into normal document flow, so it lays out
       wherever the <dialog> element happens to sit in the markup. On a long page
       that means opening it while scrolled down puts it somewhere you cannot
       see, and the 0.8s entrance plays out off screen and is over by the time
       you find it. Both symptoms, one cause. Fixed also still makes this a
       containing block for the rim and the particle field, which is the only
       reason `relative` looked right. */
    box-sizing: border-box; position: fixed; inset: 0;
    /* still scrollable when a short viewport needs it, but the bar itself is
       hidden: a scrollbar down the side of a hologram reads as a browser
       chrome leak rather than part of the panel */
    overflow-x: hidden; overflow-y: auto;
    scrollbar-width: none;
    /* a phone in landscape is shorter than this dialog is tall, and with the
       overflow hidden the close control was simply unreachable */
    max-height: calc(100% - 24px);
    width: min(90%, 460px); max-width: 460px; margin: auto;
    padding: 44px 48px 32px; text-align: center;
    font: inherit; color: rgb(var(--holo-ink, 239 244 251));
    border: 1px solid rgb(var(--holo-glow, 74 150 224) / .18);
    border-radius: 2px;
    background: linear-gradient(165deg,
      rgb(10 18 35 / .95) 0%, rgb(5 10 22 / .98) 100%);
    /* four layers: a near glow, a very wide faint one that only registers as
       the room being lit, an inset that fills the body, and a real drop */
    box-shadow:
      0 0 80px rgb(var(--holo-glow, 74 150 224) / .08),
      0 0 200px rgb(var(--holo-glow, 74 150 224) / .03),
      inset 0 0 80px rgb(var(--holo-glow, 74 150 224) / .04),
      0 20px 60px rgb(0 0 0 / .5);
  }

/* the WebKit half of scrollbar-width: none, so the scroll bar is gone in
   Safari and Chrome too while the dialog stays scrollable */
  dialog.holodialog::-webkit-scrollbar { width: 0; height: 0; }

/* the backdrop cannot count on inheriting your tokens, so it carries values */
  dialog.holodialog::backdrop {
    background: radial-gradient(ellipse at center,
      rgba(0,15,40,.85) 0%, rgba(0,0,0,.95) 100%);
    backdrop-filter: blur(12px) saturate(1.3);
    -webkit-backdrop-filter: blur(12px) saturate(1.3);
  }

/* the inner ambient glow, a wash down from the top edge. It sits at z-index 0
     so it stays under the body content, which is at 2 */
  dialog.holodialog::after {
    content: ""; position: absolute; z-index: 0; pointer-events: none;
    top: -50%; left: -50%; width: 200%; height: 200%;
    background: radial-gradient(ellipse at 50% 0%,
      rgb(var(--holo-glow, 74 150 224) / .06) 0%, transparent 60%);
  }

/* the materialise: it arrives blurred, overbright and slightly too large, and
     settles through a soft overshoot at 60 per cent */
  dialog.holodialog[open] {
    animation: holodialog-materialize .8s cubic-bezier(.16,1,.3,1) both;
  }

@keyframes holodialog-materialize {
    0%   { opacity: 0; transform: scale(1.04) translateY(-10px);
           filter: blur(20px) brightness(3); }
    30%  { opacity: .6; filter: blur(3px) brightness(1.5); }
    60%  { opacity: 1; transform: scale(.99); filter: blur(0) brightness(1.1); }
    100% { opacity: 1; transform: scale(1) translateY(0);
           filter: blur(0) brightness(1); }
  }

/* the cycling gradient border. In the original this is a ::before at inset
     -1px sitting at z-index -1, which a dialog with overflow hidden clips
     away, so here it is a real element at inset 0. Same trick otherwise: a
     1px ring cut out with mask-composite, the gradient laid out at 300 per
     cent so the six second cycle has somewhere to travel. */
  .holodialog-rim {
    position: absolute; inset: 0; z-index: 1; pointer-events: none;
    padding: 1px; border-radius: inherit;
    background: linear-gradient(135deg,
      rgb(var(--holo-glow, 74 150 224) / .4) 0%,
      rgb(var(--holo-glow, 74 150 224) / .05) 25%,
      rgb(var(--holo-viol, 138 96 230) / .18) 50%,
      rgb(var(--holo-glow, 74 150 224) / .05) 75%,
      rgb(var(--holo-cyan, 126 224 255) / .3) 100%);
    background-size: 300% 300%;
    animation: holodialog-border 6s ease-in-out infinite;
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
  }

@keyframes holodialog-border {
    0%   { background-position: 0% 50%; }
    50%  { background-position: 100% 50%; }
    100% { background-position: 0% 50%; }
  }

/* the drifting particle grid. Not a canvas and not one element per dot: six
     radial gradients, each a single dot placed at its own percentage, each
     tiled at its own background size, so the six layers beat against each
     other and never line up. The whole field travels 60px by 40px over twenty
     seconds. The original translates the element, which opens a gap at the
     trailing edge once the box is only 460px wide, so the background position
     moves instead. */
  /* the drifting particle field. It reproduces the original's six tiled dot
     grids, but at the source's alphas the dots were all but invisible against
     this panel, so the field read as empty. Brighter dots, a touch larger, and
     the whole field carried up to .9, so it actually looks like the field the
     panel is named for. Nine tiles now instead of six for a little more depth;
     the drift keyframe lists a position per tile. */
  .holodialog-field {
    position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: .9;
    background-image:
      radial-gradient(1.6px 1.6px at 20% 30%, rgb(var(--holo-glow, 74 150 224) / .6) 0%, transparent 100%),
      radial-gradient(1.2px 1.2px at 40% 70%, rgb(var(--holo-cyan, 126 224 255) / .55) 0%, transparent 100%),
      radial-gradient(1.4px 1.4px at 80% 40%, rgb(var(--holo-viol, 138 96 230) / .5) 0%, transparent 100%),
      radial-gradient(2px 2px at 65% 80%, rgb(var(--holo-glow, 74 150 224) / .55) 0%, transparent 100%),
      radial-gradient(1.2px 1.2px at 90% 15%, rgb(var(--holo-cyan, 126 224 255) / .45) 0%, transparent 100%),
      radial-gradient(1.4px 1.4px at 10% 85%, rgb(var(--holo-glow, 74 150 224) / .5) 0%, transparent 100%),
      radial-gradient(1.6px 1.6px at 33% 12%, rgb(var(--holo-cyan, 126 224 255) / .5) 0%, transparent 100%),
      radial-gradient(1.2px 1.2px at 55% 50%, rgb(var(--holo-glow, 74 150 224) / .48) 0%, transparent 100%),
      radial-gradient(1.4px 1.4px at 78% 66%, rgb(var(--holo-viol, 138 96 230) / .42) 0%, transparent 100%);
    background-size: 250px 250px, 300px 300px, 200px 200px,
                     350px 350px, 180px 180px, 280px 280px,
                     230px 230px, 320px 320px, 210px 210px;
    animation: holodialog-drift 20s linear infinite;
  }

@keyframes holodialog-drift {
    from { background-position: 0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0; }
    to   { background-position: -60px -40px, -60px -40px, -60px -40px,
                                -60px -40px, -60px -40px, -60px -40px,
                                -60px -40px, -60px -40px, -60px -40px; }
  }

/* two rings around the glyph, counter rotating. In the original both spin
     forever. Here each turns exactly once as the panel materialises and then
     holds, on a decelerating curve so it settles rather than stops dead. */
  .holodialog-rings {
    position: relative; z-index: 2; display: inline-flex;
    align-items: center; justify-content: center;
    width: 130px; height: 130px; margin-bottom: 14px;
  }

.holodialog-ring {
    position: absolute; inset: 0; border-radius: 50%;
    border: 1.5px solid rgb(var(--holo-glow, 74 150 224) / .25);
    border-top-color: rgb(var(--holo-cyan, 126 224 255) / .8);
    border-right-color: rgb(var(--holo-viol, 138 96 230) / .4);
    animation: holodialog-spin 3s cubic-bezier(.16,1,.3,1) both;
  }

.holodialog-ring--inner {
    inset: 10px;
    border-color: rgb(var(--holo-glow, 74 150 224) / .1);
    border-bottom-color: rgb(var(--holo-cyan, 126 224 255) / .5);
    border-left-color: rgb(var(--holo-cyan, 126 224 255) / .3);
    animation-duration: 5s; animation-direction: reverse;
  }

@keyframes holodialog-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
  }

/* the glyph takes one breath on arrival rather than breathing forever */
  .holodialog-icon {
    position: relative; z-index: 2;
    width: 90px; height: 100px;
    display: flex; align-items: center; justify-content: center;
    filter: drop-shadow(0 0 12px rgb(var(--holo-glow, 74 150 224) / .4));
    animation: holodialog-breathe 3s ease-in-out both;
  }

.holodialog-icon svg { display: block; width: 90px; height: 100px; }
/* the synaptic sparks and the ripples are SMIL, which does not read the motion
   query, so a reader who asked for less motion gets the neuron drawn but still,
   no travelling signals. */
@media (prefers-reduced-motion: reduce) {
    .holodialog-icon .hd-spark, .holodialog-icon .hd-ripple { display: none; }
  }

@keyframes holodialog-breathe {
    0%, 100% { filter: drop-shadow(0 0 12px rgb(var(--holo-glow, 74 150 224) / .4)); }
    50%      { filter: drop-shadow(0 0 24px rgb(var(--holo-cyan, 126 224 255) / .7)); }
  }

.holodialog-eyebrow {
    position: relative; z-index: 2; margin: 0 0 6px;
    font-size: .58em; font-weight: 700; letter-spacing: .25em;
    text-transform: uppercase;
    color: rgb(var(--holo-cyan, 126 224 255) / .5);
  }

.holodialog-title {
    position: relative; z-index: 2; margin: 0 0 8px;
    font-size: 1.5em; font-weight: 300; letter-spacing: .06em;
    text-transform: uppercase; color: rgb(200 230 255 / .95);
    text-shadow: 0 0 30px rgb(var(--holo-glow, 74 150 224) / .3);
  }

.holodialog-desc {
    position: relative; z-index: 2; margin: 0 0 24px;
    font-size: .78em; line-height: 1.6; letter-spacing: .02em;
    color: rgb(var(--holo-dim, 154 162 177) / .78);
  }

/* the status rows. The 2px lit left edge is the whole idea of them, and it is
     the same idiom as the field below */
  .holodialog-rows {
    position: relative; z-index: 2; margin: 0 0 24px; text-align: left;
  }

.holodialog-row {
    display: flex; align-items: center; gap: 10px; margin-bottom: 6px;
    padding: 10px 14px; border-radius: 1px;
    background: rgb(var(--holo-glow, 74 150 224) / .04);
    border: 1px solid rgb(var(--holo-glow, 74 150 224) / .1);
    border-left: 2px solid rgb(var(--holo-cyan, 126 224 255) / .3);
    transition: background .3s ease, border-color .3s ease;
  }

/* one pulse on arrival, not a heartbeat */
  .holodialog-row[data-state="live"] {
    border-left-color: rgb(var(--holo-cyan, 126 224 255) / .6);
    animation: holodialog-row-pulse 1.5s ease-in-out both;
  }

@keyframes holodialog-row-pulse {
    0%, 100% { background: rgb(var(--holo-glow, 74 150 224) / .04); }
    50%      { background: rgb(var(--holo-glow, 74 150 224) / .08); }
  }

.holodialog-row[data-state="done"] {
    background: rgb(0 255 150 / .03);
    border-color: rgb(0 255 150 / .1);
    border-left-color: rgb(0 255 150 / .5);
  }

.holodialog-mark {
    flex: 0 0 auto; width: 22px;
    display: flex; align-items: center; justify-content: center;
  }

.holodialog-mark i {
    display: block; width: 8px; height: 8px; border-radius: 50%;
    background: rgb(var(--holo-glow, 74 150 224) / .3);
    border: 1px solid rgb(var(--holo-cyan, 126 224 255) / .5);
    box-shadow: 0 0 6px rgb(var(--holo-cyan, 126 224 255) / .3);
  }

.holodialog-mark b {
    font-weight: 700; font-size: .9em; color: rgb(0 255 150 / .9);
    text-shadow: 0 0 8px rgb(0 255 150 / .5);
  }

.holodialog-row span {
    flex: 1 1 auto; font-size: .78em; letter-spacing: .03em;
    font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
    color: rgb(var(--holo-cyan, 126 224 255) / .6);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

.holodialog-row em {
    flex: 0 0 auto; font-style: normal; font-size: .68em; font-weight: 700;
    letter-spacing: .12em; text-transform: uppercase;
    color: rgb(0 255 150 / .8); text-shadow: 0 0 10px rgb(0 255 150 / .3);
  }

.holodialog-label {
    position: relative; z-index: 2;
    display: block; margin: 0 0 7px; text-align: left;
    font-size: .58em; font-weight: 700; letter-spacing: .2em;
    text-transform: uppercase;
    color: rgb(var(--holo-cyan, 126 224 255) / .5);
  }

/* the lit left edge is the whole trick on these fields */
  .holodialog-input {
    box-sizing: border-box; position: relative; z-index: 2;
    display: block; width: 100%; margin: 0 0 20px;
    padding: 11px 14px; font: inherit; font-size: .82em; letter-spacing: .03em;
    text-align: left; color: rgb(var(--holo-ink, 239 244 251) / .92);
    background: rgb(var(--holo-glow, 74 150 224) / .06);
    border: 1px solid rgb(var(--holo-glow, 74 150 224) / .2);
    border-left: 2px solid rgb(var(--holo-cyan, 126 224 255) / .4);
    border-radius: 1px;
    transition: background .25s ease, border-color .25s ease, box-shadow .25s ease;
  }

.holodialog-input::placeholder { color: rgb(var(--holo-dim, 154 162 177) / .5); }

.holodialog-input:hover, .holodialog-input.holo-on {
    background: rgb(var(--holo-glow, 74 150 224) / .1);
    border-color: rgb(var(--holo-glow, 74 150 224) / .34);
    border-left-color: rgb(var(--holo-cyan, 126 224 255) / .7);
  }

.holodialog-input:focus-visible {
    outline: 2px solid rgb(var(--holo-cyan, 126 224 255) / .9);
    outline-offset: 2px;
    border-left-color: rgb(var(--holo-cyan, 126 224 255));
    box-shadow: inset 0 0 22px rgb(var(--holo-glow, 74 150 224) / .12);
  }

/* the main button: a blue to violet gradient on the diagonal, and a band of
     light that crosses it once as the panel arrives */
  .holobtn {
    display: block; box-sizing: border-box; position: relative; z-index: 2;
    width: 100%; overflow: hidden; padding: 14px 24px;
    font: inherit; font-size: .88em; font-weight: 600;
    letter-spacing: .1em; text-transform: uppercase; cursor: pointer;
    color: rgb(200 230 255 / .95);
    border: 1px solid rgb(var(--holo-glow, 74 150 224) / .3);
    border-radius: 2px;
    background: linear-gradient(135deg,
      rgb(var(--holo-glow, 74 150 224) / .28) 0%,
      rgb(var(--holo-glow, 74 150 224) / .34) 50%,
      rgb(var(--holo-viol, 138 96 230) / .24) 100%);
    transition: background .3s ease, border-color .3s ease,
                box-shadow .3s ease, transform .3s ease;
  }

.holobtn-text { position: relative; z-index: 2; }

.holobtn-glow {
    position: absolute; inset: 0; z-index: 1; pointer-events: none;
    background: linear-gradient(90deg, transparent 0%,
      rgb(var(--holo-cyan, 126 224 255) / .15) 50%, transparent 100%);
    animation: holodialog-btn-sweep 3s ease-in-out .6s both;
  }

@keyframes holodialog-btn-sweep {
    from { transform: translateX(-100%); }
    to   { transform: translateX(100%); }
  }

.holobtn:hover:not(:disabled), .holobtn.holo-on:not(:disabled) {
    border-color: rgb(var(--holo-cyan, 126 224 255) / .6);
    background: linear-gradient(135deg,
      rgb(var(--holo-glow, 74 150 224) / .38) 0%,
      rgb(var(--holo-glow, 74 150 224) / .46) 50%,
      rgb(var(--holo-viol, 138 96 230) / .3) 100%);
    box-shadow: 0 0 30px rgb(var(--holo-glow, 74 150 224) / .15),
                inset 0 0 30px rgb(var(--holo-glow, 74 150 224) / .05);
    transform: translateY(-1px);
  }

.holobtn:active:not(:disabled) {
    transform: translateY(0);
    box-shadow: 0 0 40px rgb(var(--holo-cyan, 126 224 255) / .25);
  }

.holobtn:focus-visible {
    outline: 2px solid rgb(var(--holo-cyan, 126 224 255) / .9); outline-offset: 3px;
  }

.holobtn:disabled { opacity: .4; cursor: default; }

.holobtn--ghost {
    display: block; box-sizing: border-box; position: relative; z-index: 2;
    width: 100%; margin-top: 14px; padding: 8px;
    background: none; border: none; cursor: pointer;
    font: inherit; font-size: .65em; font-weight: 700;
    letter-spacing: .2em; text-transform: uppercase;
    color: rgb(var(--holo-dim, 154 162 177) / .4);
    transition: color .15s ease;
  }

.holobtn--ghost:hover:not(:disabled),
.holobtn--ghost.holo-on:not(:disabled) {
    color: rgb(var(--holo-cyan, 126 224 255) / .5);
    box-shadow: none; transform: none; background: none;
  }

.holobtn--ghost:focus-visible {
    color: rgb(var(--holo-cyan, 126 224 255) / .7);
    outline: 2px solid rgb(var(--holo-cyan, 126 224 255) / .9); outline-offset: 2px;
  }

/* the data stream line at the foot, which flickers once on arrival */
  .holodialog-note {
    position: relative; z-index: 2; margin: 20px 0 0;
    font-size: .5em; letter-spacing: .08em;
    font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
    color: rgb(var(--holo-cyan, 126 224 255) / .28);
    animation: holodialog-flicker 5s ease-in-out both;
  }

@keyframes holodialog-flicker {
    0%, 92% { opacity: 1; }
    93%     { opacity: .3; }
    94%     { opacity: 1; }
    96%     { opacity: .6; }
    97%     { opacity: 1; }
    100%    { opacity: 1; }
  }

/* 48px of side padding is most of a phone, so the box tightens up. It also
     caps its width rather than running to the edges: a modal that fills the
     screen reads as a page, not a panel, so it keeps a real margin on both
     sides. */
  @media (max-width: 430px) {
    dialog.holodialog { width: min(100% - 40px, 348px); padding: 28px 20px 24px; }
    .holodialog-rings { width: 104px; height: 104px; margin-bottom: 10px; }
    .holodialog-icon { width: 72px; height: 80px; }
    .holodialog-icon svg { width: 72px; height: 80px; }
  }

/* both loops stop, and so does everything that ran once */
  @media (prefers-reduced-motion: reduce) {
    dialog.holodialog[open], .holodialog-rim, .holodialog-field,
    .holodialog-ring, .holodialog-icon, .holodialog-row,
    .holodialog-note { animation: none; }
    .holodialog-input, .holobtn, .holobtn--ghost, .holodialog-row {
      transition: none;
    }
    .holobtn:hover:not(:disabled),
    .holobtn.holo-on:not(:disabled) { transform: none; }
    .holobtn-glow { display: none; }
  }


/* ---- 10. motes that flee the cursor ---------------------------------------
   Small HUD marks scattered over a hero image that get out of the pointer's
   way. Each one is pushed along the vector from the cursor, with a falloff, and
   springs back on its own transition when the cursor leaves. No animation loop:
   the transform is set on pointermove and CSS does the easing, which keeps it
   off the main thread and costs nothing when the mouse is still.

   The repulsion is the one hover treatment in the library with no tap
   equivalent, and not for want of trying: a tap cannot say "the pointer is
   here and travelling", which is the whole input the effect reads. So the
   repulsion is left to devices with a real pointer, and every device gets the
   ambient drift below instead, a few pixels of slow wander on the translate
   property. It rides on translate precisely because the repulsion writes an
   inline transform: the two compose instead of fighting, so a mote can drift
   and flee at once. Drift is ambient, which is what earns it a loop. */
.holoswarm {
  position: absolute; inset: 0; pointer-events: none; z-index: 2;
  /* the widest mark is a 46px tick rail, and one placed near the right edge of
     a phone used to push the document 10px wider than the viewport. Two things
     stop that now: each mark is laid out from whichever edge it is nearer, so
     it grows inward, and this clip means no mark can contribute scroll width
     even if a host moves one. */
  overflow: hidden;
}
.holoswarm > i {
  position: absolute; display: block; pointer-events: none;
  transition: transform 620ms cubic-bezier(.18,.9,.24,1), opacity 400ms ease;
  will-change: transform;
  color: rgb(var(--holo-line, 196 228 255));
  animation: holo-mote-drift 9s ease-in-out infinite;
}
/* two drift paths and four periods, dealt out by child index with negative
   delays, so twenty marks sharing two keyframe sets never read as a formation.
   The amplitude stays under 7px: the marks should breathe, not patrol. */
.holoswarm > i:nth-child(2n) {
  animation-name: holo-mote-drift-b; animation-delay: -3.1s;
}
.holoswarm > i:nth-child(3n) { animation-duration: 12s; animation-delay: -7.4s; }
.holoswarm > i:nth-child(4n + 1) { animation-duration: 10.5s; }
.holoswarm > i:nth-child(5n) { animation-duration: 13.5s; animation-delay: -5.2s; }
@keyframes holo-mote-drift {
  0%, 100% { translate: 0 0; }
  33%      { translate: 4px -6px; }
  66%      { translate: -3px 3px; }
}
@keyframes holo-mote-drift-b {
  0%, 100% { translate: 0 0; }
  33%      { translate: -5px 4px; }
  66%      { translate: 3px -4px; }
}
/* a dot */
.holoswarm > i.dot {
  width: 4px; height: 4px; border-radius: 50%;
  background: currentColor; opacity: .5;
  box-shadow: 0 0 7px rgba(var(--holo-glow, 74 150 224) / .85);
}
/* a corner bracket */
.holoswarm > i.brk {
  width: 13px; height: 13px; opacity: .42;
  border-left: 1px solid currentColor; border-top: 1px solid currentColor;
}
.holoswarm > i.brk.flip { transform-origin: center; rotate: 180deg; }
/* a short tick rail */
.holoswarm > i.rail {
  width: 46px; height: 5px; opacity: .38;
  background-image: repeating-linear-gradient(90deg,
    currentColor 0 1px, transparent 1px 7px);
}
/* a micro readout */
.holoswarm > i.num {
  font: 500 8.5px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: .16em; opacity: .5; white-space: nowrap;
  font-variant-numeric: tabular-nums; font-style: normal;
}
/* a tiny ring */
.holoswarm > i.ring {
  width: 15px; height: 15px; border-radius: 50%; opacity: .34;
  border: 1px solid currentColor;
}
/* a phone gets a thinned set. Twenty marks were composed for a 1280px hero and
   read as clutter across 375px, so the ones tagged wide sit out below 700px.
   The tick rails are all in that group: they are the widest marks and the least
   legible at that size. */
@media (max-width: 700px) {
  .holoswarm > i.wide { display: none; }
}

/* with motion off nothing here moves at all, so there is nothing left to see.
   The marks go rather than sit as decoration that implies a behaviour the
   preference has switched off. */
@media (prefers-reduced-motion: reduce) {
  .holoswarm { display: none; }
}

/* ---- 12. section rail ---------------------------------------------------
   The vertical section rail off the FlyWire neuron gallery, reimplemented from
   React state into plain CSS plus a small init. A counter, a hairline running
   the height of the viewport, the run so far filled, a head that travels, and
   one dot per section with a label that appears when you point at it or tab to
   it. hologram.js builds it from every h2 on the page and an IntersectionObserver
   keeps the active section marked.

   It is fixed, so it never contributes to the document scroll width, and it
   hides itself below the width at which there is a gutter for it to sit in.
   Everything about it is a transition rather than an animation: nothing here
   loops, because the position it reports is the position you are at. */
.holorail {
  position: fixed; left: 26px; top: 26px; bottom: 26px; z-index: 30;
  display: flex; flex-direction: column; align-items: flex-start; gap: 16px;
  pointer-events: none;
  font: 500 9px/1 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
}

.holorail-count {
  flex: 0 0 auto; letter-spacing: .25em; text-transform: uppercase;
  font-variant-numeric: tabular-nums; white-space: nowrap;
  color: rgb(var(--holo-ink, 239 244 251) / .58);
}

.holorail-count s {
  text-decoration: none; color: rgb(var(--holo-ink, 239 244 251) / .28);
}

.holorail-track { position: relative; flex: 1 1 auto; width: 1px; }

.holorail-track::before {
  content: ""; position: absolute; inset: 0;
  background: rgb(var(--holo-ink, 239 244 251) / .1);
}

.holorail-fill {
  position: absolute; top: 0; left: 0; right: 0;
  height: calc(var(--holorail-progress, 0) * 100%);
  background: linear-gradient(180deg,
    rgb(var(--holo-cyan, 126 224 255) / .8) 0%,
    rgb(var(--holo-cyan, 126 224 255) / .55) 55%,
    rgb(var(--holo-cyan, 126 224 255) / .25) 100%);
  transition: height .7s cubic-bezier(.16,1,.3,1);
}

.holorail-head {
  position: absolute; left: -3px; width: 7px; height: 7px; border-radius: 50%;
  top: calc(var(--holorail-progress, 0) * 100% - 3.5px);
  background: rgb(var(--holo-cyan, 126 224 255));
  box-shadow: 0 0 8px rgb(var(--holo-cyan, 126 224 255) / .95),
              0 0 18px rgb(var(--holo-cyan, 126 224 255) / .5);
  transition: top .7s cubic-bezier(.16,1,.3,1);
}

/* a 19px hit box around a 3px dot, and it is a real link so it works with the
   script stripped out and lands in the tab order for free */
.holorail-dot {
  position: absolute; left: -9px; width: 19px; height: 19px;
  display: flex; align-items: center; justify-content: center;
  pointer-events: auto; text-decoration: none; border-radius: 50%;
}

.holorail-dot i {
  display: block; width: 3px; height: 3px; border-radius: 50%;
  background: rgb(var(--holo-ink, 239 244 251) / .28);
  transition: transform .3s ease, width .3s ease, height .3s ease,
              background .3s ease, box-shadow .3s ease;
}

.holorail-dot[data-done] i {
  background: rgb(var(--holo-cyan, 126 224 255) / .55);
  box-shadow: 0 0 4px rgb(var(--holo-cyan, 126 224 255) / .45);
}

.holorail-dot[aria-current] i {
  width: 5px; height: 5px;
  background: rgb(var(--holo-cyan, 126 224 255) / .95);
  box-shadow: 0 0 7px rgb(var(--holo-cyan, 126 224 255) / .85);
}

.holorail-dot:hover i, .holorail-dot:focus-visible i,
.holorail-dot.holo-on i { transform: scale(1.5); }

/* the label is capped and clipped rather than allowed to run, because past
   132px it would cross into the reading column on a 1280px screen */
.holorail-dot b {
  position: absolute; left: 20px; top: 50%; translate: 0 -50%;
  max-width: 132px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; padding: 6px 9px; border-radius: 3px;
  font-weight: 400; font-size: 9px; letter-spacing: .22em;
  text-transform: uppercase; color: rgb(var(--holo-ink, 239 244 251) / .9);
  background: rgb(0 0 0 / .8);
  border: 1px solid rgb(var(--holo-line, 196 228 255) / .12);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0; pointer-events: none; transition: opacity .2s ease;
}

.holorail-dot:hover b, .holorail-dot:focus-visible b,
.holorail-dot.holo-on b { opacity: 1; }

.holorail-dot:focus-visible {
  outline: 2px solid rgb(var(--holo-cyan, 126 224 255) / .9); outline-offset: 1px;
}

/* the heading the rail scrolls to needs room above it, or it lands under the
   top of the viewport with no air */
.holorail-anchor { scroll-margin-top: 30px; }

/* 860px of column plus 60px of padding leaves 180px a side at 1280px, which
   is room for the rail and a clipped label. Below that there is none. */
@media (max-width: 1239px) {
  .holorail { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .holorail-fill, .holorail-head, .holorail-dot i, .holorail-dot b {
    transition: none;
  }
}

/* ---- 13. holo card ------------------------------------------------------
   The card off the FlyWire neuron gallery. Near black ink instead of the white
   tinted glass a panel usually gets, so a render inside it pops rather than
   being washed out, faint scanlines every 3px, and a rim plus a backlight that
   live entirely in the box shadow so they never lighten the card body.

   The hover state is shadow only. Nothing moves, nothing scales, and the image
   is never repainted, which is why it stays calm at any size. The negative
   spread on the middle layer, 24px at -6px, is the detail that matters: it
   pulls the glow back so it hugs the card instead of bleeding off it.

   The accent is --holo-cyan, its own token. It is a teal leaning cyan and is
   deliberately not the blue white the rest of the library uses. */
.holocard {
  position: relative; isolation: isolate; border-radius: 3px;
  background-color: rgb(4 6 12 / .78);
  background-image: repeating-linear-gradient(0deg,
    rgb(var(--holo-cyan, 126 224 255) / .018) 0px,
    rgb(var(--holo-cyan, 126 224 255) / .018) 1px,
    transparent 1px, transparent 3px);
  border: 1px solid rgb(var(--holo-cyan, 126 224 255) / .1);
  box-shadow: 0 0 0 1px rgb(var(--holo-cyan, 126 224 255) / .05),
              inset 0 1px 0 rgb(var(--holo-cyan, 126 224 255) / .06);
  transition: box-shadow 400ms ease, border-color 400ms ease, transform 400ms ease;
}

.holocard > * { position: relative; z-index: 1; }

.holocard:hover, .holocard:focus-within, .holocard.holo-on {
  box-shadow: 0 0 0 1px rgb(var(--holo-cyan, 126 224 255) / .32),
              0 0 24px -6px rgb(var(--holo-cyan, 126 224 255) / .32),
              inset 0 0 0 1px rgb(var(--holo-cyan, 126 224 255) / .1);
}

/* the edge trace: a conic gradient whose bright head covers about a quarter of
   the turn, masked down to the 1px ring, rotating through a registered angle.
   It is the only loop in the library outside the loading state, and it is only
   ever running while you are pointing at the card. On a browser with no
   @property support the angle holds still and the ring simply fades in, which
   is a fine degradation. */
@property --holocard-angle {
  syntax: "<angle>"; inherits: false; initial-value: 0deg;
}

@keyframes holocard-trace { to { --holocard-angle: 360deg; } }

.holocard > .holocard-trace {
  position: absolute; inset: 0; z-index: 3; border-radius: inherit;
  pointer-events: none; opacity: 0; padding: 1px;
  transition: opacity 600ms ease;
  background: conic-gradient(from var(--holocard-angle, 0deg),
    transparent 0deg,
    transparent 270deg,
    rgb(var(--holo-cyan, 126 224 255) / 0) 290deg,
    rgb(var(--holo-cyan, 126 224 255) / .9) 350deg,
    rgb(var(--holo-cyan, 126 224 255) / 0) 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  filter: drop-shadow(0 0 4px rgb(var(--holo-cyan, 126 224 255) / .7));
}

.holocard:hover > .holocard-trace,
.holocard:focus-within > .holocard-trace,
.holocard.holo-on > .holocard-trace {
  opacity: 1; animation: holocard-trace 3.4s linear infinite;
}

.holocard img, .holocard video {
  display: block; width: 100%; max-width: 100%; height: auto;
}

@media (prefers-reduced-motion: reduce) {
  .holocard { transition: none; }
  .holocard > .holocard-trace { display: none; }
}
