/* ============================================================
   deck.css — core engine: background, HUD, transitions, overlays
   ============================================================ */

:root{
  --bg:        #05070c;
  --bg-2:      #0a0e18;
  --panel:     #0e1420;
  --panel-2:   #121a2a;
  --line:      #1e2a3d;
  --line-2:    #2b3b55;
  --txt:       #e8eef8;
  --txt-2:     #9fb0c8;
  --txt-3:     #64748b;

  --amber:     #ffb347;
  --amber-2:   #ffd28a;
  --cyan:      #3fe0d0;
  --blue:      #5aa9ff;
  --violet:    #a78bfa;
  --red:       #ff6b6b;
  --green:     #57e08a;
  --pink:      #f472b6;

  --accent:    var(--amber);
  --accent-dim:#8a5f28;

  --r:   14px;
  --r-s: 9px;

  --mono: ui-monospace, "Cascadia Mono", "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;
  --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  --ease: cubic-bezier(.22,.72,.18,1);
  --hud: 54px;
  /* The bottom HUD's height, and the stage's matching bottom inset (see `.stage`).
     Declared here with the rest of the tokens because two rules read it; an undefined
     custom property invalidates both at computed-value time rather than falling back
     to anything visible. 52px is the measured height of `.bot-row` at this width; the
     breakpoints below restate it, because every rule that resizes `.nbtn` changes it. */
  --hud-bot: 52px;
  /* The edge-feather for the three decorative background layers, shared by all of them
     so the frame's border fades to nothing rather than being cut. Two gradients, not
     one: a single ellipse large enough to feel centred has to be *wider than half the
     frame* to avoid going transparent in the middle, which means it cannot reach zero
     before the left and right edges - measured, an `ellipse 84% 80%` mask is still 72%
     opaque at the frame's left edge and 24% at its corners, so the layers go on
     painting a rectangle and the seam shrinks rather than going.

     One gradient per axis, each reaching zero at its own edge, and the two
     intersected (see `mask-composite` on `#bgcanvas` below). Alpha is then zero on
     all four sides by construction: at the left edge the horizontal gradient is
     already 0, and where both are 0 - the corners - the product is 0. The plateau in
     the middle is fully opaque, so the backdrop still reads as a full-bleed wash and
     only its border is feathered.

     The ramps carry intermediate stops rather than going straight from 0 to opaque.
     A two-stop ramp is a straight line, and where that line meets the flat middle the
     slope changes instantly - the eye reads that corner as a faint band running the
     length of the slide, which is the same "line where the glow ends" artefact as a
     hard edge, only softer. Sampling the ramp at five points approximates a smoothstep
     curve, so the slope changes gradually and the join disappears.

     The endpoints are deliberately unchanged: both gradients are still exactly 0 at
     0% and 100%, so the guarantee that nothing paints at a frame edge is identical.
     Only the interior curve got smoother. */
  --bg-fade-h: linear-gradient(to right,
    rgba(0,0,0,0)   0%,
    rgba(0,0,0,.26) 5%,
    rgba(0,0,0,.55) 10%,
    rgba(0,0,0,.78) 15%,
    rgba(0,0,0,.92) 20%,
    rgba(0,0,0,.98) 24%,
    #000             28%,
    #000             72%,
    rgba(0,0,0,.98) 76%,
    rgba(0,0,0,.92) 80%,
    rgba(0,0,0,.78) 85%,
    rgba(0,0,0,.55) 90%,
    rgba(0,0,0,.26) 95%,
    rgba(0,0,0,0)   100%);
  --bg-fade-v: linear-gradient(to bottom,
    rgba(0,0,0,0)   0%,
    rgba(0,0,0,.24) 4%,
    rgba(0,0,0,.52) 9%,
    rgba(0,0,0,.75) 14%,
    rgba(0,0,0,.90) 19%,
    rgba(0,0,0,.97) 24%,
    #000             30%,
    #000             70%,
    rgba(0,0,0,.97) 76%,
    rgba(0,0,0,.90) 81%,
    rgba(0,0,0,.75) 86%,
    rgba(0,0,0,.52) 91%,
    rgba(0,0,0,.24) 96%,
    rgba(0,0,0,0)   100%);

  /* The same smooth ramp, weighted so the halo keeps dissolving well before its own
     bottom edge - the one deliberate asymmetry here, because the photo finishes only
     ~65px above the frame's bottom and a symmetric ramp would put the glow's
     termination back into that gap. Identical endpoints, so the edge guarantee holds
     here too. */
  --halo-fade-v: linear-gradient(to bottom,
    rgba(0,0,0,0)   0%,
    rgba(0,0,0,.28) 5%,
    rgba(0,0,0,.60) 10%,
    rgba(0,0,0,.82) 16%,
    rgba(0,0,0,.95) 22%,
    #000             28%,
    #000             50%,
    rgba(0,0,0,.88) 60%,
    rgba(0,0,0,.60) 72%,
    rgba(0,0,0,.28) 86%,
    rgba(0,0,0,0)   100%);
}

*,*::before,*::after{ box-sizing:border-box; }

html,body{ height:100%; }
body{
  margin:0;
  /* Matched to the host site's page background (`--bg` in the project's global.css,
     #0a0d14) rather than this deck's own `--bg` (#05070c), so the frame has no
     visible edge where it meets the page. Set on `body` and not on the `--bg`
     variable itself: `--bg` also backs a slide element (the border on a `.tlrow .mk
     i` dot in enigma.css), which was drawn against the deck's darker original, so
     moving the token with it would change that element's contrast for no gain. */
  background:#0a0d14;
  color:var(--txt);
  font-family:var(--sans);
  font-size:16px;
  line-height:1.5;
  overflow:hidden;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

/* The three decorative layers, and why they are masked.
   They paint *over* the body colour and all three are at their strongest at the
   frame's border: `.bg-blobs` puts three blurred colour washes at .55 opacity in
   `screen` blend, and `#bgcanvas` scatters glyphs at up to .09 alpha, and both are
   `position:fixed; inset:0` and so end in a hard rectangle. `.bg-veil` was the worst
   of the three and the real cause of the seam - a vignette running to 90% opaque
   black *at the edge*. Their being on is the point of this file's animation; the
   feather is what lets them be on without a rectangle.

   `mask-composite: intersect` is load-bearing and its default would make all of this
   a no-op: the initial value is `add`, which unions the two axis gradients and puts
   the backdrop back at full strength across the whole frame. WebKit's prefixed
   spelling of the same operator is `source-in`, hence both.

   `mask-image` needs the `-webkit-` prefix alongside it for Safari before 15.4, and
   it is the alpha channel that is read, so a gradient running to `rgba(0,0,0,0)`
   fades a layer out rather than tinting it. */
#bgcanvas{
  position:fixed; inset:0; width:100%; height:100%; z-index:0; pointer-events:none;
  -webkit-mask-image:var(--bg-fade-h), var(--bg-fade-v);
  mask-image:var(--bg-fade-h), var(--bg-fade-v);
  -webkit-mask-composite:source-in; mask-composite:intersect;
}

.bg-blobs{
  position:fixed; inset:0; z-index:0; pointer-events:none; filter:blur(90px); opacity:.55;
  -webkit-mask-image:var(--bg-fade-h), var(--bg-fade-v);
  mask-image:var(--bg-fade-h), var(--bg-fade-v);
  -webkit-mask-composite:source-in; mask-composite:intersect;
}
.bg-blobs span{ position:absolute; border-radius:50%; mix-blend-mode:screen; }
.bg-blobs span:nth-child(1){ width:46vw; height:46vw; left:-10vw; top:-14vw; background:radial-gradient(circle,rgba(255,179,71,.16),transparent 68%); animation:drift1 34s var(--ease) infinite alternate; }
.bg-blobs span:nth-child(2){ width:52vw; height:52vw; right:-14vw; top:8vh;  background:radial-gradient(circle,rgba(63,224,208,.13),transparent 68%); animation:drift2 41s var(--ease) infinite alternate; }
.bg-blobs span:nth-child(3){ width:40vw; height:40vw; left:28vw; bottom:-18vw; background:radial-gradient(circle,rgba(167,139,250,.14),transparent 68%); animation:drift3 47s var(--ease) infinite alternate; }
@keyframes drift1{ to{ transform:translate3d(14vw,10vh,0) scale(1.18); } }
@keyframes drift2{ to{ transform:translate3d(-16vw,14vh,0) scale(1.1); } }
@keyframes drift3{ to{ transform:translate3d(8vw,-12vh,0) scale(1.22); } }

.bg-veil{
  position:fixed; inset:0; z-index:1; pointer-events:none;
  /* The vignette's outermost stop is `rgba(2,4,8,0)` - the same near-black as the
     darkening stops, at zero alpha - rather than the `rgba(2,4,8,.9)` it used to end
     on. That one value is why this deck blends: a vignette that is *most opaque at
     the border* paints the border, so however well the body's colour matches the
     page, a dark rectangle is what meets it. Fading to nothing at the same edge
     dissolves the join without flattening the vignette's effect in the middle, which
     is the part that actually helps a slide read.

     The stop is written as `rgba(2,4,8,0)` and not as the `transparent` keyword on
     purpose. Both resolve to `rgba(0,0,0,0)`, but interpolating toward the keyword
     runs the hue through zero and the gradient picks up a grey cast on some
     engines; naming the colour keeps the fade clean. */
  background:
    radial-gradient(ellipse at 50% 42%, rgba(2,4,8,0) 30%, rgba(2,4,8,.46) 74%, rgba(2,4,8,0) 100%);
  -webkit-mask-image:var(--bg-fade-h), var(--bg-fade-v);
  mask-image:var(--bg-fade-h), var(--bg-fade-v);
  -webkit-mask-composite:source-in; mask-composite:intersect;
}
.bg-veil::after{
  content:""; position:absolute; inset:0; opacity:.035;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/></filter><rect width='140' height='140' filter='url(%23n)'/></svg>");
}

/* ---------- progress ---------- */
/* The progress bar is the deck's position readout: it fills as you move through the
   slides, which is what the removed rail used to also show.

   Where it is drawn has moved out of the deck. The visible one now belongs to the
   *host site's* navbar - the 1px line under `.site-header` - because a 2px bar inside
   this frame sits below the site's own header and reads as a second, unrelated rule
   partway down the page. `core.js` posts the slide number to the parent window from
   `DECK.go()`, and the page draws it there; see `learn.astro`.

   This copy stays in the DOM for exactly one case: **fullscreen**. The site's header
   is not on screen when this deck goes fullscreen, so the readout would vanish
   entirely, and fullscreen is exactly where a deck is most likely to be left running.
   `display:none` by default and `display:block` only under `html:fullscreen` - the
   element fullscreened is `document.documentElement`, so the descendant selector is
   what matches. Keeping the element rather than deleting it also keeps
   `$('#progFill').style.width` in `go()` working unguarded.

   When it does show, it sits on the HUD's bottom border: `top: calc(var(--hud) - 2px)`
   starts the 2px bar one bar-height above the HUD's height, landing it in the HUD's
   last 2px instead of hanging below the boundary into the slide. */
.progress{ position:fixed; top:calc(var(--hud) - 2px); left:0; right:0; height:2px; z-index:60; background:rgba(255,255,255,.05); display:none; }
html:fullscreen .progress{ display:block; }
.progress-fill{
  height:100%; width:0;
  background:linear-gradient(90deg,var(--amber),var(--cyan));
  box-shadow:0 0 12px rgba(255,179,71,.6);
  transition:width .5s var(--ease);
}

/* ---------- HUD ---------- */
.hud{
  position:fixed; left:0; right:0; z-index:50;
  display:flex; align-items:center; gap:16px;
  padding:0 clamp(14px,3vw,34px);
  height:var(--hud);
  pointer-events:none;
}
.hud > *{ pointer-events:auto; }
.hud-top{ top:0; }
.hud-bot{ bottom:0; height:var(--hud-bot); flex-direction:column; gap:0; padding:0; justify-content:flex-end; }

/* The `.brand` rules and the `spinMark` keyframes went with the brand button; none is
   referenced by anything else, and leaving them behind would describe a control this
   deck no longer draws. */

.chapter-now{
  flex:1; text-align:center;
  font-family:var(--mono); font-size:10.5px; letter-spacing:.28em;
  text-transform:uppercase; color:var(--accent);
  transition:color .45s var(--ease);
}

.hud-tools{ display:flex; gap:6px; }
.tbtn{
  width:32px; height:32px; border-radius:8px; cursor:pointer;
  background:rgba(255,255,255,.045); border:1px solid var(--line);
  color:var(--txt-2); font-size:14px; font-family:var(--mono);
  display:grid; place-items:center; transition:.18s var(--ease);
}
.tbtn:hover{ background:rgba(255,179,71,.14); border-color:var(--accent-dim); color:var(--amber); transform:translateY(-1px); }

/* Fullscreen is hidden on the site's Mobile and Tab layouts. It cannot simply use the
   deck's own 1000px idea of "desktop": this deck is embedded in an iframe, and the
   frame is always 47px narrower than the page it sits in (measured on `/security`,
   whose layout this page matches). So a site width of 999px puts a 952px frame in
   front of us and a site width of 1000px puts a 953px one, and those two frames are
   1px apart - the site breakpoint cannot be reproduced from in here by any other
   means than by pinning this threshold to that measurement. `max-width: 952px` is
   therefore exact, where a tidier `999px` would have hidden the button across the
   47px of Desktop layout between site widths 1000 and 1046.

   `display:none`, not removal: the element stays in the DOM, so core.js's click
   binding still resolves, and nothing about the fullscreen capability is lost - it
   remains on F, which is the only way to reach it at these sizes. */
@media (max-width:952px){
  .hud-tools #btnFull{ display:none; }
}

/* The rail is gone: the `.rail` rules went with it, since every one described a
   control this deck no longer draws and none is referenced by anything else. */

.bot-row{
  width:100%; display:flex; align-items:center; gap:14px;
  padding:6px clamp(14px,3vw,34px) 12px;
}
.pagenum{ font-family:var(--mono); font-size:11px; color:var(--txt-3); letter-spacing:.1em; }
.pagenum b{ color:var(--txt); font-weight:600; font-size:13px; }
.pagenum .sep{ margin:0 3px; opacity:.5; }

.slide-title-mini{
  flex:1; text-align:center; font-size:11.5px; color:var(--txt-3);
  letter-spacing:.05em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  font-family:var(--mono);
}

.navbtns{ display:flex; gap:6px; }
.nbtn{
  width:38px; height:34px; border-radius:9px; cursor:pointer;
  background:rgba(255,255,255,.05); border:1px solid var(--line);
  color:var(--txt-2); font-size:20px; line-height:1;
  transition:.18s var(--ease);
}
.nbtn:hover{ background:var(--amber); color:#0a0e18; border-color:var(--amber); transform:translateY(-1px); }
.nbtn:disabled{ opacity:.25; pointer-events:none; }

/* ---------- stage & slides ---------- */
/* The bottom inset reads the *same* `--hud-bot` the footer is sized with, so the two
   cannot drift: the stage stops exactly where the footer starts, at every width. It
   used to be a literal 64px here and a literal 64px on `.hud-bot`, which is the kind
   of pair that survives one edit and not the next - and it still had the rail's 26px
   baked in, since 64px was 26px of rail plus the row. */
.stage{ position:fixed; inset:var(--hud) 0 var(--hud-bot); z-index:10; }

.slide{
  position:absolute; inset:0;
  overflow-y:auto; overflow-x:hidden;
  display:flex; align-items:flex-start; justify-content:center;
  padding:clamp(10px,2.4vh,26px) clamp(16px,4vw,60px);
  opacity:0; visibility:hidden; pointer-events:none;
}
/* Centre the frame with auto margins rather than align-items:center.
   Centring a flex item that overflows clips the top out of the scroll
   range, making the first of the slide impossible to scroll to. */
.frame{ width:100%; max-width:1220px; margin:auto; }

/* When a slide scrolls, soften the cut at the top and bottom edges so a
   panel or table border sliced by the scroll box doesn't read as a hard
   horizontal rule. A static mask — no animation, so it can't collide with
   the slide's own enter/leave transition. */
.slide.scrollable{
  -webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%);
  mask-image:linear-gradient(to bottom, transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%);
}
@media (prefers-reduced-motion:reduce){
  .slide.scrollable{ -webkit-mask-image:none; mask-image:none; }
}
.slide.active{ opacity:1; visibility:visible; pointer-events:auto; }

/* directional transitions */
.slide.entering.from-right{ animation:slideInR .62s var(--ease) both; }
.slide.entering.from-left {{ animation:slideInL .62s var(--ease) both; }}
.slide.leaving.to-left  { animation:slideOutL .55s var(--ease) both; }
.slide.leaving.to-right { animation:slideOutR .55s var(--ease) both; }
@keyframes slideInR { from{ opacity:0; transform:translate3d(6%,0,0) scale(.985); } to{ opacity:1; transform:none; } }
@keyframes slideInL { from{ opacity:0; transform:translate3d(-6%,0,0) scale(.985); } to{ opacity:1; transform:none; } }
@keyframes slideOutL{ from{ opacity:1; transform:none; } to{ opacity:0; transform:translate3d(-4%,0,0) scale(.99); } }
@keyframes slideOutR{ from{ opacity:1; transform:none; } to{ opacity:0; transform:translate3d(4%,0,0) scale(.99); } }

/* content frame — see .frame in the .slide rule above for centring */

/* ---------- entrance animations for descendants ---------- */
.slide.live .anim{ animation:riseIn .68s var(--ease) both; animation-delay:var(--d,0s); }
@keyframes riseIn{
  from{ opacity:0; transform:translate3d(var(--dx,0),26px,0) scale(var(--sc,1)); filter:blur(5px); }
  to  { opacity:1; transform:none; filter:blur(0); }
}
.slide.live .anim-l{ animation:slideXin .7s var(--ease) both; animation-delay:var(--d,0s); }
@keyframes slideXin{
  from{ opacity:0; transform:translate3d(var(--dx,-32px),0,0); }
  to  { opacity:1; transform:none; }
}
.slide.live .anim-w{ animation:wipeIn .75s var(--ease) both; animation-delay:var(--d,0s); }
@keyframes wipeIn{
  from{ opacity:0; clip-path:inset(0 100% 0 0); }
  to  { opacity:1; clip-path:inset(0 0 0 0); }
}

/* ---------- typography ---------- */
.kicker{
  display:inline-flex; align-items:center; gap:9px;
  font-family:var(--mono); font-size:10.5px; letter-spacing:.3em;
  text-transform:uppercase; color:var(--accent);
  padding:5px 12px; border:1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  border-radius:100px; background:color-mix(in srgb, var(--accent) 8%, transparent);
  margin-bottom:18px;
}
.kicker::before{ content:""; width:5px; height:5px; border-radius:50%; background:var(--accent); box-shadow:0 0 8px var(--accent); }

h1.big{
  margin:0 0 6px;
  font-size:clamp(30px,5.1vw,60px);
  line-height:1.02; letter-spacing:-.035em; font-weight:800;
}
h2.title{
  margin:0 0 10px;
  font-size:clamp(25px,3.5vw,42px);
  line-height:1.08; letter-spacing:-.028em; font-weight:750;
}
.lede{
  font-size:clamp(14px,1.42vw,17.5px);
  color:var(--txt-2); line-height:1.62; max-width:76ch; margin:0 0 26px;
}
.lede strong{ color:var(--txt); font-weight:600; }
.lede .hl{ color:var(--accent); font-weight:600; }

.dim{ color:var(--txt-3); }
.mono{ font-family:var(--mono); }
.grad-text{
  background:linear-gradient(100deg,var(--amber) 0%,var(--amber-2) 30%,var(--cyan) 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
}

/* ---------- generic surfaces ---------- */
.panel{
  background:linear-gradient(168deg, var(--panel-2), var(--panel));
  border:1px solid var(--line);
  border-radius:var(--r);
  padding:20px 22px;
  position:relative; overflow:hidden;
}
.panel::before{
  content:""; position:absolute; inset:0 0 auto; height:1px;
  background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--accent) 55%,transparent),transparent);
  opacity:.6;
}
.panel.glow{ box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 18%,transparent), 0 18px 60px -22px color-mix(in srgb,var(--accent) 40%,transparent); }
.panel-t{
  font-family:var(--mono); font-size:10.5px; letter-spacing:.22em; text-transform:uppercase;
  color:var(--accent); margin:0 0 12px; display:flex; align-items:center; gap:8px;
}
.panel-t::after{ content:""; flex:1; height:1px; background:linear-gradient(90deg,color-mix(in srgb,var(--accent) 30%,transparent),transparent); }
.panel p{ margin:0 0 10px; font-size:14px; color:var(--txt-2); line-height:1.62; }
.panel p:last-child{ margin-bottom:0; }
.panel strong{ color:var(--txt); font-weight:600; }

.grid{ display:grid; gap:clamp(12px,1.5vw,20px); }
.g2{ grid-template-columns:repeat(2,minmax(0,1fr)); }
.g3{ grid-template-columns:repeat(3,minmax(0,1fr)); }
.g4{ grid-template-columns:repeat(4,minmax(0,1fr)); }
.split{ display:grid; grid-template-columns:minmax(0,1.02fr) minmax(0,1fr); gap:clamp(20px,3vw,46px); align-items:center; }
.split.wide-l{ grid-template-columns:minmax(0,1.35fr) minmax(0,1fr); }
.split.wide-r{ grid-template-columns:minmax(0,1fr) minmax(0,1.35fr); }

/* ---------- code / cipher output chips ---------- */
.cipherbox{
  font-family:var(--mono); font-size:clamp(13px,1.55vw,19px);
  letter-spacing:.12em; word-break:break-all; line-height:1.75;
  padding:16px 18px; border-radius:var(--r-s);
  background:#070b13; border:1px solid var(--line-2);
  color:var(--cyan); min-height:1.75em;
  box-shadow:inset 0 0 40px rgba(63,224,208,.05);
}
.plainbox{ color:var(--txt); }
.hl-x{ color:var(--amber); text-shadow:0 0 14px rgba(255,179,71,.55); }
.hl-y{ color:var(--violet); }

/* ---------- inputs ---------- */
input[type=text], textarea, select{
  font-family:var(--mono); font-size:13px; color:var(--txt);
  background:#070b13; border:1px solid var(--line-2); border-radius:var(--r-s);
  padding:9px 12px; outline:none; transition:.18s var(--ease); width:100%;
}
input[type=text]:focus, textarea:focus, select:focus{
  border-color:var(--accent); box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 16%,transparent);
}
textarea{ resize:none; line-height:1.6; }
select{ cursor:pointer; appearance:none; padding-right:28px;
  background-image:linear-gradient(45deg,transparent 50%,var(--txt-3) 50%),linear-gradient(135deg,var(--txt-3) 50%,transparent 50%);
  background-position:calc(100% - 15px) 51%, calc(100% - 10px) 51%;
  background-size:5px 5px,5px 5px; background-repeat:no-repeat;
}
select option{ background:#0b1220; }
label.fld{ display:block; margin-bottom:11px; }
label.fld > span{
  display:block; font-family:var(--mono); font-size:9.5px; letter-spacing:.2em;
  text-transform:uppercase; color:var(--txt-3); margin-bottom:5px;
}

/* Range inputs are drawn thin but must stay a comfortable tap target,
   so the visible track is a pseudo-element and the input keeps a 24px box. */
input[type=range]{
  -webkit-appearance:none; appearance:none; width:100%; height:24px;
  background:transparent; outline:none; cursor:pointer; margin:0;
}
input[type=range]::-webkit-slider-runnable-track{
  height:4px; border-radius:4px;
  background:linear-gradient(90deg,var(--accent) var(--p,0%), rgba(255,255,255,.12) var(--p,0%));
}
input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none;
  width:18px; height:18px; border-radius:50%; margin-top:-7px;
  background:var(--txt); border:3px solid var(--accent);
  box-shadow:0 2px 10px rgba(0,0,0,.6); cursor:grab; transition:transform .15s;
}
input[type=range]::-webkit-slider-thumb:active{ transform:scale(1.2); cursor:grabbing; }
input[type=range]::-moz-range-track{ height:4px; border-radius:4px; background:rgba(255,255,255,.12); }
input[type=range]::-moz-range-progress{ height:4px; border-radius:4px; background:var(--accent); }
input[type=range]::-moz-range-thumb{
  width:14px; height:14px; border-radius:50%; background:var(--txt);
  border:3px solid var(--accent); cursor:grab;
}

/* ---------- buttons ---------- */
.btn{
  font-family:var(--mono); font-size:11px; letter-spacing:.14em; text-transform:uppercase;
  padding:9px 16px; border-radius:9px; cursor:pointer;
  background:rgba(255,255,255,.05); border:1px solid var(--line-2); color:var(--txt-2);
  transition:.18s var(--ease); white-space:nowrap;
}
.btn:hover{ background:var(--accent); color:#070b13; border-color:var(--accent); transform:translateY(-1px); box-shadow:0 6px 22px -8px var(--accent); }
.btn:active{ transform:translateY(0); }
.btn.on{ background:var(--accent); color:#070b13; border-color:var(--accent); }
.btn.ghost{ background:transparent; }
/* A ghost button needs its own hover, and this is a bug rather than a style choice.
   `.btn.ghost{ background:transparent }` is declared *after* `.btn:hover` and has the
   same specificity, so on hover a ghost button keeps its transparent background while
   still inheriting `.btn:hover`'s `color:#070b13` - near-black text on a dark page,
   which is unreadable. Roughly thirty ghost buttons in this deck have it; the cover's
   "Try the Enigma Machine" is only where it was noticed.

   The fix states both properties on the ghost's own hover rather than letting it
   inherit half of another rule, and uses the same soft-amber treatment `.tbtn:hover`
   already uses a few lines below - so a ghost reads as a ghost, just lit.

   `.on` is the selected state and has to beat `transparent` for the same reason:
   without it, an active ghost button looks identical to an idle one. */
.btn.ghost:hover{ background:rgba(255,179,71,.14); border-color:var(--accent-dim); color:var(--amber); }
.btn.ghost.on,
.btn.ghost.on:hover{ background:var(--accent); border-color:var(--accent); color:#070b13; }
.btn-row{ display:flex; flex-wrap:wrap; gap:8px; }

/* ---------- pills / tags ---------- */
.pill{
  display:inline-block; font-family:var(--mono); font-size:10px; letter-spacing:.1em;
  padding:3px 9px; border-radius:100px; border:1px solid var(--line-2);
  background:rgba(255,255,255,.03); color:var(--txt-2);
}
.pill.a{ border-color:color-mix(in srgb,var(--amber) 45%,transparent); color:var(--amber); background:color-mix(in srgb,var(--amber) 9%,transparent); }
.pill.c{ border-color:color-mix(in srgb,var(--cyan) 45%,transparent); color:var(--cyan); background:color-mix(in srgb,var(--cyan) 9%,transparent); }
.pill.v{ border-color:color-mix(in srgb,var(--violet) 45%,transparent); color:var(--violet); background:color-mix(in srgb,var(--violet) 9%,transparent); }
.pill.r{ border-color:color-mix(in srgb,var(--red) 45%,transparent); color:var(--red); background:color-mix(in srgb,var(--red) 9%,transparent); }
.pill.g{ border-color:color-mix(in srgb,var(--green) 45%,transparent); color:var(--green); background:color-mix(in srgb,var(--green) 9%,transparent); }

/* ---------- key / value list ---------- */
.kv{ display:grid; grid-template-columns:auto 1fr; gap:5px 16px; font-size:12.5px; }
.kv dt{ font-family:var(--mono); font-size:10.5px; letter-spacing:.12em; text-transform:uppercase; color:var(--txt-3); padding-top:2px; white-space:nowrap; }
.kv dd{ margin:0; color:var(--txt-2); }

/* ---------- stat blocks ---------- */
.stat{ text-align:center; padding:14px 10px; border-radius:var(--r-s); background:rgba(255,255,255,.028); border:1px solid var(--line); }
.stat .v{ font-family:var(--mono); font-size:clamp(18px,2.5vw,28px); font-weight:600; color:var(--accent); letter-spacing:-.01em; }
.stat .l{ font-size:10.5px; color:var(--txt-3); letter-spacing:.09em; text-transform:uppercase; margin-top:4px; font-family:var(--mono); }

/* ---------- overlays ---------- */
.overlay{
  position:fixed; inset:0; z-index:80;
  background:rgba(3,5,10,.86); backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  display:grid; place-items:center; padding:24px;
  animation:fadeIn .22s var(--ease);
}
.overlay[hidden]{ display:none; }
@keyframes fadeIn{ from{opacity:0} to{opacity:1} }
.overlay-inner{
  width:100%; max-width:1080px; max-height:86vh; overflow:auto;
  background:linear-gradient(168deg,var(--panel-2),var(--panel));
  border:1px solid var(--line-2); border-radius:18px; padding:26px 28px;
  animation:popIn .3s var(--ease);
}
.overlay-inner.narrow{ max-width:520px; }
@keyframes popIn{ from{ opacity:0; transform:translateY(14px) scale(.98);} to{opacity:1;transform:none;} }
.overlay-head{ display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; }
.overlay-head h2{ margin:0; font-size:17px; letter-spacing:-.01em; font-family:var(--mono); text-transform:uppercase; letter-spacing:.2em; font-size:12px; color:var(--accent); }
.xbtn{ background:none; border:0; color:var(--txt-3); font-size:26px; cursor:pointer; line-height:1; padding:0 6px; }
.xbtn:hover{ color:var(--red); }

.grid-cards{ display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:10px; }
.gcard{
  text-align:left; cursor:pointer; padding:12px 13px; border-radius:11px;
  background:rgba(255,255,255,.035); border:1px solid var(--line);
  transition:.18s var(--ease); color:var(--txt-2); font-family:inherit;
}
.gcard:hover{ background:rgba(255,179,71,.12); border-color:var(--accent-dim); transform:translateY(-2px); color:var(--txt); }
.gcard.now{ background:color-mix(in srgb,var(--accent) 16%,transparent); border-color:var(--accent); color:var(--txt); }
.gcard .n{ font-family:var(--mono); font-size:9.5px; color:var(--txt-3); letter-spacing:.1em; }
.gcard .t{ font-size:12.5px; font-weight:600; margin-top:3px; line-height:1.3; }
.gcard .c{ font-family:var(--mono); font-size:9px; color:var(--accent); letter-spacing:.14em; text-transform:uppercase; margin-top:5px; opacity:.85; }

.keylist{ display:grid; grid-template-columns:auto 1fr; gap:9px 20px; margin:0; align-items:center; }
.keylist dt{ display:flex; gap:5px; }
.keylist dd{ margin:0; color:var(--txt-2); font-size:13px; }
kbd{
  font-family:var(--mono); font-size:10.5px; padding:3px 7px; border-radius:6px;
  background:rgba(255,255,255,.07); border:1px solid var(--line-2);
  border-bottom-width:2px; color:var(--txt);
}
.tip{ margin:20px 0 0; font-size:12.5px; color:var(--txt-3); }
.tip strong{ color:var(--accent); }

/* blackout */
.blackout{
  position:fixed; inset:0; z-index:95; background:#000;
  display:grid; place-items:center; font-family:var(--mono);
  font-size:11px; letter-spacing:.3em; color:#333; text-transform:uppercase;
}
.blackout[hidden]{ display:none; }

/* ---------- scrollbars ---------- */
.slide::-webkit-scrollbar, .overlay-inner::-webkit-scrollbar{ width:9px; height:9px; }
.slide::-webkit-scrollbar-track, .overlay-inner::-webkit-scrollbar-track{ background:transparent; }
.slide::-webkit-scrollbar-thumb, .overlay-inner::-webkit-scrollbar-thumb{
  background:var(--line-2); border-radius:8px; border:2px solid transparent; background-clip:padding-box;
}
.slide::-webkit-scrollbar-thumb:hover{ background:var(--accent-dim); background-clip:padding-box; }

/* ---------- utility ---------- */
.mt{ margin-top:18px; } .mt-s{ margin-top:10px; } .mt-l{ margin-top:30px; }
.mb{ margin-bottom:18px; } .mb-s{ margin-bottom:10px; }
.center{ text-align:center; } .r{ text-align:right; }
.flex{ display:flex; } .between{ justify-content:space-between; } .aic{ align-items:center; }
.wrap{ flex-wrap:wrap; } .gap-s{ gap:8px; } .gap{ gap:14px; } .gap-l{ gap:22px; }
.f1{ flex:1; min-width:0; }
.nowrap{ white-space:nowrap; }
.hidden{ display:none !important; }

/* typewriter cursor for cipher output */
.caret{
  display:inline-block; width:9px; height:1.05em; vertical-align:-2px;
  background:var(--cyan); margin-left:2px; animation:blink .82s steps(1) infinite;
}
@keyframes blink{ 50%{ opacity:0; } }

/* pop-in when a cipher letter lands */
.letter-pop{ animation:letterPop .34s var(--ease); }
@keyframes letterPop{
  0%{ opacity:0; transform:translateY(-8px) scale(.6); filter:brightness(2); }
  60%{ transform:translateY(1px) scale(1.14); }
  100%{ opacity:1; transform:none; filter:none; }
}

@media (prefers-reduced-motion:reduce){
  *{ animation-duration:.01ms !important; animation-iteration-count:1 !important; transition-duration:.05ms !important; }
}

/* ============================================================
   RESPONSIVE — three tiers
     phone   <= 560px
     tablet  561–820px
     desktop 821px+
   Plus a coarse-pointer tier so touch laptops/tablets get big
   targets even when the window is wide.
   ============================================================ */

/* --- comfort tier: anything phone-sized, tablet-sized, or touch-driven ---
   1024px catches iPad in both orientations; pointer:coarse catches
   touch laptops and tablets however wide the window is.                  */
@media (max-width:1024px), (pointer:coarse){
  .btn{ padding:12px 18px; }
  .nbtn{ width:46px; height:44px; font-size:23px; }
  .tbtn{ width:40px; height:40px; font-size:16px; }
  input[type=range]{ height:30px; }
  /* These 44px-tall nav buttons make `.bot-row` 62px, so the footer and the stage's
     matching bottom inset have to follow. This was missing, and it showed on any
     touch laptop: `pointer:coarse` matches at *any* width, so above 1024px the
     comfort tier was enlarging the buttons while `--hud-bot` stayed at the 52px base
     value, clipping the bottom 10px of the footer. Every rule that resizes `.nbtn`
     now restates `--hud-bot`; the later tiers below narrow it further. */
  :root{ --hud-bot:62px; }
}
/* iOS zooms any focused field under 16px, so never go below it on touch */
@media (max-width:1024px), (pointer:coarse){
  select, input[type=text], textarea{ font-size:16px; }
}

/* --- phone --- */
@media (max-width:560px){
  .btn{ padding:13px 16px; font-size:11px; }
  .nbtn{ width:48px; height:44px; }
  .tbtn{ width:42px; height:42px; }
  select{ padding:12px 26px 12px 11px; }


  /* reclaim vertical space from the chrome */
  :root{ --hud:48px; --hud-bot:56px; }
  .bot-row{ padding:4px 12px 8px; }
  .slide-title-mini{ font-size:10.5px; }

  .kicker{ margin-bottom:10px; padding:4px 10px; }
  h2.title{ font-size:22px; }
  h1.big{ font-size:30px; }
  .lede{ font-size:13px; line-height:1.55; margin-bottom:16px; }
  .panel{ padding:13px 14px; }

  .cipherbox{ font-size:12.5px; letter-spacing:.06em; }

  .grid-cards{ grid-template-columns:1fr 1fr; }
}

/* --- tablet --- */
@media (min-width:561px) and (max-width:1024px){
  :root{ --hud:52px; --hud-bot:62px; }
  .cipherbox{ font-size:14px; }
}
/* short-and-wide (landscape phone) — keep slides usable */
@media (max-height:560px) and (orientation:landscape){
  :root{ --hud:42px; --hud-bot:62px; }
}

/* ---------- the site's Mobile and Tab layouts, as seen from in here ----------
   952px, not 999px, for the reason spelled out beside `.hud-tools` above: this deck
   is in an iframe 47px narrower than the page it sits on, so site widths 999 and 1000
   arrive as frames of 952 and 953. The site's own 880px Mobile/Tab split is *not*
   reproducible from in here - the frame carries no site scale - so this block covers
   both bands at once, and the one place they genuinely differ (the cover's keyboard
   hint) is handled in enigma.css at this same width.

   All of it is placed *after* the comfort and phone tiers on purpose. Same specificity
   as the `.nbtn` rules it overrides, so source order is what decides, and these would
   lose if they sat above them. */
@media (max-width:952px){
  /* The footer is sized from `.bot-row`, which is sized from that button, so the
     height has to come down with it: 62px (the comfort tier's value) minus the 10px
     of button is 52px, measured rather than assumed. Left at 62px here the footer's
     bottom 10px is clipped at every width this block covers. */
  :root{ --hud-bot:52px; }

  /* The prev/next arrow buttons, smaller. The comfort tier above inflates these to
     46x44 / 48x44 for finger-sized targets, which on a short screen costs 10px of
     slide height for a control the keyboard arrows duplicate anyway. 40x34 returns
     them to roughly the Desktop size (38x34) and hands the 10px back to the slide. */
  .nbtn{ width:40px; height:34px; font-size:19px; }

  /* The slide scrollbar, gone. `.slide` is `overflow-y:auto` so an over-long slide
     stays reachable rather than being cut off with no way down - that behaviour is
     kept, only the *bar* is hidden. A phone user scrolling a slide with a 9px gutter
     beside it is reading chrome, and the gutter is what pushes wide content (cipher
     grids, tables) into the scroll it was avoiding. `scrollbar-width:none` is the
     standard property; the `::-webkit-scrollbar` pair is for the WebKit/Blink engine,
     which ignores the standard one. Neither removes the ability to scroll: wheel,
     trackpad, touch and the keyboard all still work. */
  .slide{ scrollbar-width:none; -ms-overflow-style:none; }
  .slide::-webkit-scrollbar{ width:0; height:0; display:none; }
}

/* The deck's phone width: the same 10px comes off the footer again, because the phone
   tier above shrinks `.bot-row`'s own padding to 4px/8px, giving 46px measured rather
   than the 52px that suits the wider band. This has to sit *after* the 952px block,
   or the two would tie on `--hud-bot` and the last one written would win. */
@media (max-width:560px){
  :root{ --hud-bot:46px; }
}

@media (max-width:820px){
  .split, .split.wide-l, .split.wide-r{ grid-template-columns:1fr; gap:22px; }
  .g3, .g4{ grid-template-columns:repeat(2,minmax(0,1fr)); }
  .slide{ align-items:flex-start; }
}
@media (max-width:560px){
  .g2,.g3,.g4{ grid-template-columns:1fr; }
  .chapter-now{ font-size:9px; letter-spacing:.16em; }
}
