/* ============================================================
   lilharper.dev — main stylesheet
   Feminine, purple, soft-glow. Elegant but still cozy/gamer at heart.
   ============================================================ */

/* ---------- Design tokens ----------
   Dark purple, techy / cyberpunk-feminine. Near-black violet background,
   neon purple + magenta + cyan accents. */
:root {
  --bg:            #08050f;
  --bg-2:          #0c0716;
  --panel:         rgba(20, 12, 32, 0.75);
  --panel-solid:   #140c20;
  --panel-2:       rgba(28, 16, 44, 0.65);
  --border:        rgba(168, 85, 247, 0.22);
  --border-strong: rgba(217, 70, 239, 0.5);

  --text:          #f5ecff;
  --text-muted:    #b39ddb;
  --text-dim:      #897ba3; /* nudged twice: #7a6a98 failed AA at 4.18:1, then #8576a0 still came in at 4.39:1 on the lighter card backgrounds (methodology notes, pick counts, the terminal path). This clears 4.5:1 everywhere it is actually used. */

  --purple:        #7c1fd6;
  --purple-bright: #a855f7;
  --pink:          #e018c1; /* was #cc16b0, 3.92:1 as small text on --panel; this clears 4.6:1 */
  --pink-bright:   #ff5ecb;
  --cyan:          #22e5ff;
  --green:         #2de8a0;
  --amber:         #ffcc66;
  /* Deliberately outside the purple/pink/cyan family. This is the one
     colour on the site that is allowed to shout, and it only gets used
     by the Ko-fi button in the sidebar (.side-kofi). Borrowed from the
     can it is asking you to buy. If it ever appears somewhere else, the
     button stops being the loudest thing on the page and the whole
     point of it is gone. */
  --lime:          #b4ff39;
  --lime-deep:     #7ee800;

  --grad-brand: linear-gradient(120deg, var(--purple-bright), var(--pink-bright));
  --grad-aurora: linear-gradient(160deg, #150a28 0%, #0a0616 45%, #170a2a 100%);
  --glow-purple: 0 0 28px rgba(168, 85, 247, 0.4);
  --glow-pink:   0 0 28px rgba(255, 94, 203, 0.32);
  --glow-cyan:   0 0 24px rgba(34, 229, 255, 0.3);

  --radius:   16px;
  --radius-sm: 11px;
  /* Fixed 248px read as proportionally tiny on ultrawide monitors (a fixed
     sidebar next to an unbounded 1fr main column is what made those look
     off) — scales gently between 248px and 300px past 1600px viewport
     width so it keeps pace with the content column instead of looking
     stranded. Below 1600px this resolves to exactly 248px, unchanged. */
  --sidebar-w: clamp(248px, 15vw, 300px);
  --topbar-h: 64px;
  --font: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
  --font-display: 'Fraunces', 'Georgia', serif;
}

/* Light theme */
html[data-theme="light"] {
  /* Dusk lavender, not paper-white — the original near-white bg (#f7f3fb)
     read as too bright/glary; this keeps it a genuine "light" theme while
     giving the page itself some color instead of blinding white. Panels
     stay near-white so cards still pop against the deeper background. */
  --bg:            #ddd0ea;
  --bg-2:          #cfbfe0;
  --panel:         rgba(255, 255, 255, 0.55);
  --panel-solid:   #fbf8fd;
  --panel-2:       rgba(255, 255, 255, 0.4);
  --border:        rgba(90, 20, 150, 0.22);
  --border-strong: rgba(90, 20, 150, 0.48);
  --text:          #221232;
  --text-muted:    #4d3a68;
  --text-dim:      #5f4d78; /* re-checked against the darker bg — still clears 4.5:1 */
  --grad-aurora: linear-gradient(160deg, #d2c0e6 0%, #ddd0ea 45%, #ecc8e2 100%);
}

/* ---------- Reset ---------- */
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
  font-family: var(--font);
  background:
    linear-gradient(rgba(168,85,247,0.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(168,85,247,0.045) 1px, transparent 1px),
    var(--bg);
  background-size: 42px 42px, 42px 42px, auto;
  color: var(--text);
  line-height: 1.6;
  min-height: 100vh;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}
html[data-theme="light"] body { background-image: none; }
a { color: inherit; text-decoration: none; }
img { max-width: 100%; display: block; }
h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 700; line-height: 1.15; }
::selection { background: rgba(168, 85, 247, 0.35); color: #fff; }

/* Visually hidden but still announced by screen readers — use for form
   labels where a visible label would duplicate a placeholder. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Consistent visible focus ring for keyboard users — the design uses no
   default outline anywhere, so without this, Tab-navigating is invisible. */
a:focus-visible, button:focus-visible, input:focus-visible,
textarea:focus-visible, select:focus-visible {
  outline: 2px solid var(--purple-bright);
  outline-offset: 2px;
  border-radius: 4px;
}

#starfield { position: fixed; inset: 0; z-index: 0; pointer-events: none; }

/* ============================================================
   Persistent glitch/static background layer
   ------------------------------------------------------------
   2026-07-26: Harper sent reference glitch-art photos plus a mocked
   Steam profile with a constant colour-noise backdrop behind clean
   content cards, and asked for that aesthetic applied to the whole
   site, not just used as one-off photo content. glitch.js already
   covers momentary corruption bursts sitewide; this is the companion
   piece -- a low, ALWAYS-present static/colour-noise texture sitting
   in the same background tier as the starfield (inserted immediately
   after it in the DOM so it paints on top of the stars, still behind
   .chaos-rift and all real content), giving the site a constant
   undertone of that look instead of only flashing it occasionally.
   Kept deliberately faint (opacity ~0.05-0.09) so it reads as
   atmosphere, not interference -- every content panel sits on a
   solid/near-solid background (--panel), so text contrast is
   unaffected regardless of what this layer is doing underneath it. */
#staticGlitch {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    repeating-linear-gradient(0deg, rgba(255,255,255,0.035) 0px, rgba(255,255,255,0.035) 1px, transparent 1px, transparent 3px),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-size: 100% 3px, 140px 140px;
  mix-blend-mode: screen;
  animation: static-glitch-hue 26s linear infinite, static-glitch-breathe 5s ease-in-out infinite;
}
@keyframes static-glitch-hue {
  0%   { filter: hue-rotate(-60deg); }
  100% { filter: hue-rotate(300deg); }
}
@keyframes static-glitch-breathe {
  0%, 100% { opacity: 0.045; }
  50%      { opacity: 0.09; }
}
@media (prefers-reduced-motion: reduce) {
  #staticGlitch { animation: none; opacity: 0.05; }
}

/* ---------- App shell ---------- */
.app { position: relative; z-index: 1; display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; }

/* ---------- Sidebar ---------- */
.sidebar {
  position: sticky; top: 0; align-self: start; height: 100vh; overflow-y: auto;
  padding: 22px 18px;
  background: linear-gradient(180deg, rgba(28,20,38,0.94), rgba(15,10,23,0.97));
  border-right: 1px solid var(--border);
  backdrop-filter: blur(12px);
  scrollbar-width: thin; scrollbar-color: var(--purple) transparent;
}
.sidebar::-webkit-scrollbar { width: 6px; }
.sidebar::-webkit-scrollbar-thumb { background: var(--purple); border-radius: 6px; }

.brand { margin-bottom: 22px; }
.brand .logo { font-family: var(--font-display); font-size: 1.35rem; font-weight: 800; letter-spacing: -0.5px; }
.brand .logo b { background: var(--grad-brand); -webkit-background-clip: text; background-clip: text; color: transparent; filter: drop-shadow(0 0 10px rgba(168,85,247,0.5)); }
.brand .tagline { font-size: 0.78rem; color: var(--text-muted); margin-top: 2px; }
.sidebar-close,
.mobile-site-nav,
.mobile-brand { display: none; }

/* Sidebar picture — just the ring/photo now, no name/pronouns/roles
   text (moved out in the 2026-07-24 sidebar redesign; see side-controls,
   side-lofi, side-rpg below for what replaced the old profile+currently
   card). */
.side-picture { text-align: center; margin-bottom: 16px; }
.avatar-ring {
  position: relative; z-index: 0;
  width: 128px; height: 128px; margin: 6px auto 14px; border-radius: 50%; padding: 4px;
  background: conic-gradient(from 180deg, var(--purple-bright), var(--pink-bright), var(--amber), var(--cyan), var(--purple-bright));
  box-shadow: var(--glow-purple);
  animation: spin-ring 4s ease-in-out infinite, avatar-ring-glow-flicker 1.6s ease-in-out infinite;
}
.avatar-ring img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; object-position: center top; border: 3px solid var(--bg); }

/* Aurora glow flicker around the avatar. Originally shipped as a fire-
   colored halo (echoing Harper's Discord avatar decoration); recolored
   2026-07-26 to the site's own purple/cyan/green palette instead of
   fire tones, per Harper's request -- this is the reusable "site glow"
   this file's --site-glow-* variables (below) are built from, so any
   future glow accent shares this exact recipe. */
@keyframes avatar-ring-glow-flicker {
  0%, 100% { box-shadow: var(--glow-purple), 0 0 20px 5px rgba(168,85,247,0.4), 0 0 38px 12px rgba(34,229,255,0.22); }
  35%      { box-shadow: var(--glow-purple), 0 0 30px 8px rgba(45,232,160,0.55), 0 0 54px 18px rgba(168,85,247,0.38); }
  60%      { box-shadow: var(--glow-purple), 0 0 22px 6px rgba(34,229,255,0.5), 0 0 42px 14px rgba(45,232,160,0.3); }
}

/* A blurred, rotating conic-gradient "corona" sitting just behind the
   ring/photo (z-index: -1 inside the ring's own stacking context) so
   it reads as light orbiting the frame rather than a static halo
   behind it. Two animations layered: a slow constant rotation (the
   glow "traveling" around the ring) and an irregular flicker on top
   of it (brightness/blur/opacity), so it never looks like a perfectly
   smooth spinning gradient. Recolored 2026-07-26 from fire tones
   (yellow/orange/red) to the purple/cyan/green palette used
   everywhere else on the site. */
.avatar-ring::after {
  content: "";
  position: absolute; inset: -6px; z-index: -1;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    var(--purple-bright), var(--cyan), var(--green), var(--amber), var(--pink-bright),
    var(--purple-bright), var(--cyan), var(--green), var(--amber), var(--pink-bright), var(--purple-bright));
  filter: blur(6px);
  animation: avatar-ring-orbit-spin 2.6s linear infinite, avatar-ring-orbit-flicker 0.85s ease-in-out infinite;
}
@keyframes avatar-ring-orbit-spin { to { transform: rotate(360deg); } }
@keyframes avatar-ring-orbit-flicker {
  0%, 100% { opacity: 0.7; filter: blur(6px) brightness(1); }
  30%      { opacity: 0.95; filter: blur(7px) brightness(1.15); }
  55%      { opacity: 0.6; filter: blur(5px) brightness(0.95); }
  80%      { opacity: 0.9; filter: blur(6.5px) brightness(1.1); }
}

@media (prefers-reduced-motion: reduce) {
  .avatar-ring { animation: none; }
  .avatar-ring::after { animation: none; }
}

/* ---------- Ko-fi button ----------
   Sits directly under the bird, above the theme controls, so it is the
   first thing under the only picture in the sidebar and it is on every
   page for free (the sidebar lives outside the router's swap target).

   WHY THIS IS NOT KO-FI'S WIDGET
   Ko-fi ships two scripts (overlay-widget.js and Widget_2.js) that inject
   a floating iframe. That would mean a third-party script and an iframe
   on all 38 pages, a cookie the rest of the site does not set, a button
   that ignores the hue slider and the light theme, and a floating panel
   fighting the guide who already walks along the bottom of the page.
   A link does the identical job: it opens the same Ko-fi page. So this is
   a plain <a>, styled here, and nothing loads from ko-fi.com until the
   moment somebody actually clicks it.

   The colour is the loudest thing on the site on purpose, per the brief.
   Contrast is not sacrificed for it: near-black text on lime clears AA
   comfortably in both themes, which a lime-on-dark text treatment would
   not have. */
.side-kofi {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  margin: 0 0 14px; padding: 10px 12px;
  border-radius: 999px;
  background: linear-gradient(135deg, var(--lime), var(--lime-deep));
  border: 1px solid rgba(220, 255, 150, 0.85);
  color: #0d1a00;
  font-size: 0.84rem; font-weight: 800; letter-spacing: 0.2px;
  box-shadow: 0 0 20px rgba(163, 255, 58, 0.38), 0 2px 10px rgba(0, 0, 0, 0.35);
  position: relative; overflow: hidden;
  transition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease;
}
.side-kofi:hover { transform: translateY(-2px); filter: brightness(1.08); box-shadow: 0 0 30px rgba(163, 255, 58, 0.6), 0 6px 16px rgba(0, 0, 0, 0.4); }

/* The same travelling border beam as the nav buttons, in green/cyan to
   match this button's own lime rather than fighting it (2026-07-27,
   Harper's request). Reuses --beam and the nav-beam keyframes instead of
   declaring a second animation that does the same thing.

   TWO CONSTRAINTS THIS HAD TO WORK AROUND, both easy to miss:

   1. ::after is ALREADY TAKEN by the shine sweep further down this file.
      A second .side-kofi::after would not stack, it would lose to the
      later rule and render nothing at all. So the beam is ::before.
   2. .side-kofi sets overflow:hidden (it has to, to clip that sweep),
      so a beam at inset:-2px would be cut off on every edge. It runs at
      inset:0 instead, along the inside of the button, which on a
      999px pill reads the same and cannot be clipped.

   The 999px mask radius has to match the button's, or the corners stop
   looking like a border and start looking like a mistake. */
.side-kofi::before {
  content: "";
  position: absolute; inset: 0; z-index: 1;
  border-radius: 999px;
  padding: 2px;
  background: conic-gradient(from var(--beam),
    transparent 0turn, transparent 0.60turn,
    rgba(45, 232, 160, 0.55) 0.70turn,
    #ffffff 0.785turn,
    rgba(34, 229, 255, 0.95) 0.85turn,
    transparent 0.95turn, transparent 1turn);
  -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;
  animation: nav-beam 3.4s linear infinite;
  pointer-events: none;
}
.side-kofi:hover::before { animation-duration: 1.8s; }
@media (prefers-reduced-motion: reduce) { .side-kofi::before { animation: none; } }
.side-kofi:active { transform: translateY(0); }
.side-kofi .kofi-ico { font-size: 1rem; line-height: 1; }

/* A single shine that crosses the button every eight seconds. Long gap on
   purpose: something that pulses constantly in the corner of the eye reads
   as an advert and gets tuned out, and this sidebar is already busy. The
   sweep is a pseudo-element so it cannot affect layout, and it is the only
   animated thing here. */
.side-kofi::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 40%;
  left: 0;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.55), transparent);
  animation: kofi-shine 8s ease-in-out infinite;
  pointer-events: none;
}
@keyframes kofi-shine {
  0%, 78% { transform: translateX(-260%); }
  92%, 100% { transform: translateX(450%); }
}
@media (prefers-reduced-motion: reduce) {
  .side-kofi::after { animation: none; display: none; }
  .side-kofi { transition: none; }
  .side-kofi:hover { transform: none; }
}

/* Light theme keeps the same fill, since the button is filled rather than
   tinted, but the outer glow is dialled back: a neon halo that reads as
   glow on a near-black page reads as a smudge on a pale one. */
html[data-theme="light"] .side-kofi {
  border-color: rgba(90, 140, 0, 0.55);
  box-shadow: 0 0 14px rgba(126, 232, 0, 0.35), 0 2px 8px rgba(40, 20, 60, 0.2);
}

/* Theme + hue controls, relocated here from the topbar. */
.side-controls { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 16px; }

/* ---------- Focus mode ----------
   Keeps the same site, palette and content, but turns down the ambient
   layers and lets fixed interaction controls wait quietly until asked
   for. The reader opts in from the book button in the sidebar. */
html[data-focus="true"] body { background-image: none; }
html[data-focus="true"] #staticGlitch,
html[data-focus="true"] .chaos-rift {
  opacity: 0.08 !important;
  animation: none !important;
}
html[data-focus="true"] .chaos-rift *,
html[data-focus="true"] .lh-glitch-layer {
  animation: none !important;
}
html[data-focus="true"] .chaos-sticker,
html[data-focus="true"] .brat-sticker,
html[data-focus="true"] .fx-flame {
  display: none !important;
}
html[data-focus="true"] .rx-float,
html[data-focus="true"] .askme-wrap {
  opacity: 0.22;
  transition: opacity 0.2s ease;
}
html[data-focus="true"] .rx-float:hover,
html[data-focus="true"] .rx-float:focus-within,
html[data-focus="true"] .askme-wrap:hover,
html[data-focus="true"] .askme-wrap:focus-within,
html[data-focus="true"] .askme-wrap.is-open {
  opacity: 1;
}
#focusToggle[aria-pressed="true"],
#focusToggleTop[aria-pressed="true"] {
  color: var(--cyan);
  background: rgba(34, 229, 255, 0.12);
  box-shadow: 0 0 16px rgba(34, 229, 255, 0.2);
}
@media (prefers-reduced-motion: reduce) {
  html[data-focus="true"] .rx-float,
  html[data-focus="true"] .askme-wrap { transition: none; }
}

/* Lofi Girl mini player */
.side-lofi { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px; margin-bottom: 16px; }
.side-lofi-label { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); margin-bottom: 8px; }
.side-lofi-frame { border-radius: 8px; overflow: hidden; background: #000; }
.side-lofi-frame iframe { display: block; width: 100%; height: 100%; border: 0; }
/* Spotify's compact embed is a fixed-height player, not a 16:9 video —
   this replaces the old aspect-ratio box the YouTube embed needed. */
/* Spotify's compact-with-artwork player (152px) — the real player with
   album art, title and controls, not the thin single-bar preview strip.
   Height matches the iframe's height attr so it scales cleanly in the box. */
.side-lofi-frame.spotify { height: 152px; }
.side-lofi-frame.spotify iframe { border-radius: 12px; }

/* Compact "RPG stats" teaser — same data/classes as the full character
   sheet (stat-label/stat-score/meter-row/meter-track/meter-fill), just
   shrunk down for the sidebar via the .mini-stat / .meter-row.mini
   overrides below. */
.side-rpg { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px 13px; margin-bottom: 16px; }
.side-rpg .head { font-size: 0.62rem; letter-spacing: 1.5px; text-transform: uppercase; color: var(--text-dim); margin-bottom: 10px; }
.mini-stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-bottom: 10px; }
.mini-stat { text-align: center; background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; padding: 6px 2px; }
.mini-stat .stat-label { font-size: 0.58rem; }
.mini-stat .stat-score { font-size: 1rem; margin: 2px 0 0; }
.meter-row.mini { max-width: none; margin-bottom: 8px; }
.meter-row.mini .meter-labels { font-size: 0.68rem; }
.meter-row.mini .meter-track { height: 7px; border-radius: 5px; }
.side-rpg .more-link { display: block; text-align: right; font-size: 0.78rem; font-weight: 600; color: var(--purple-bright); margin-top: 2px; }
.side-rpg .more-link:hover { text-decoration: underline; }

/* Curated quick-links (Hytale / Infrastructure / Work With Me / Contact)
   — the sidebar's only remaining nav; the full page list now lives in
   the top bar only (see js/layout.js TOPNAV_IDS). */
.side-nav { display: flex; flex-direction: column; gap: 2px; margin-bottom: 18px; }
.side-nav a { display: flex; align-items: center; gap: 11px; padding: 9px 12px; border-radius: 10px; font-size: 0.88rem; color: var(--text-muted); transition: all 0.18s ease; }
.side-nav a .ico { width: 18px; text-align: center; opacity: 0.85; }
.side-nav a:hover { background: var(--panel-2); color: var(--text); }
.side-nav a.active { background: linear-gradient(90deg, rgba(168, 85, 247,0.22), rgba(255, 94, 203,0.1)); color: #fff; box-shadow: inset 2px 0 0 var(--purple-bright); }

/* The hidden vault link — real, focusable, just quiet until you notice it. */
.side-secret {
  display: block; text-align: center; font-size: 0.95rem; opacity: 0.14;
  padding: 6px; margin-top: 2px; border-radius: 8px; transition: opacity 0.2s ease, transform 0.2s ease;
}
.side-secret:hover, .side-secret:focus-visible { opacity: 0.9; transform: scale(1.15); }

/* ---------- Achievement-unlock toast ---------- */
.lh-toast {
  position: fixed; right: 20px; bottom: 20px; z-index: 200;
  display: flex; align-items: center; gap: 12px;
  background: var(--panel-solid); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: 14px 18px; box-shadow: var(--glow-purple);
  max-width: 320px;
  opacity: 0; transform: translateY(16px) scale(0.97);
  transition: opacity 0.35s ease, transform 0.35s ease;
}
.lh-toast.show { opacity: 1; transform: translateY(0) scale(1); }
.lh-toast-ico { font-size: 1.6rem; line-height: 1; }
.lh-toast-head { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 1px; color: var(--purple-bright); font-weight: 700; }
.lh-toast-title { font-size: 0.92rem; font-weight: 600; color: var(--text); margin-top: 2px; }
@media (max-width: 480px) {
  .lh-toast { left: 16px; right: 16px; max-width: none; }
}

/* ---------- Main column ---------- */
.main { display: flex; flex-direction: column; align-items: center; min-width: 0; }

.topbar {
  position: sticky; top: 0; z-index: 20; height: var(--topbar-h);
  align-self: stretch; /* .main centers its children now — the topbar still needs to span edge-to-edge, only .page should be a capped, centered column */
  display: flex; align-items: center; gap: 6px; padding: 0 26px;
  background: rgba(21, 14, 30, 0.78); backdrop-filter: blur(14px); border-bottom: 1px solid var(--border);
}
html[data-theme="light"] .topbar { background: rgba(250,245,252,0.85); }
.mobile-brand {
  min-width: 0;
  font-family: var(--font-display);
  font-weight: 700;
  white-space: nowrap;
}
.mobile-brand b {
  background: var(--grad-brand);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Vertical padding exists purely to give .nav-highlight's corona room to
   render. .top-nav is a horizontal scroll container (overflow-x: auto),
   and per spec a non-visible value on one axis forces the other axis to
   `auto` too -- so anything drawn outside a child's box, box-shadow and
   ::after corona alike, gets clipped. The negative margin cancels the
   padding's effect on the topbar's fixed height so nothing shifts. */
.top-nav { display: flex; gap: 4px; flex: 1; overflow-x: auto; scrollbar-width: none; padding: 11px 6px; margin: -11px -6px; }
.top-nav::-webkit-scrollbar { display: none; }
.top-nav a { padding: 10px 14px; border-radius: 9px; font-size: 0.9rem; font-weight: 500; color: var(--text-muted); white-space: nowrap; transition: all 0.18s; border-bottom: 2px solid transparent; }
.top-nav a:hover { color: var(--text); background: var(--panel-2); }
.top-nav a.active { color: #fff; border-bottom-color: var(--purple-bright); }

/* Nav dropdown (e.g. "Adult Dynamics"). The top-nav scrolls horizontally
   (overflow-x:auto), which would clip an absolutely-positioned submenu —
   so the menu is position:fixed and placed by JS from the toggle's rect,
   the same trick the hue popover uses. */
.nav-dropdown { position: relative; display: inline-flex; }
.nav-drop-toggle {
  padding: 10px 14px; border-radius: 9px; font-size: 0.9rem; font-weight: 500;
  color: var(--text-muted); white-space: nowrap; background: none; border: 0;
  border-bottom: 2px solid transparent; cursor: pointer; font-family: var(--font);
  transition: all 0.18s; display: inline-flex; align-items: center; gap: 5px;
}
/* ---------- The one loud item in the top bar ----------
   Blog sits on its own at the far end and is lime rather than purple,
   because it is the only page here that changes. Everything else on this
   site is a reference you read once; this is the one worth coming back
   for, and it was previously the middle item of a dropdown.

   Same lime as the Ko-fi button on purpose. Two loud things in two
   different colours would just look like a mistake; two in the same
   colour reads as "these are the things Harper is pointing at".

   Tinted rather than filled: a solid lime block in a row of text links
   reads as an advert, and the bar has to stay a navigation bar. */
/* Recolored 2026-07-26 from a flat lime tint to the site's own glow, at
   Harper's request: same recipe as .avatar-ring (the sidebar photo), in
   purple/blue/pink rather than the avatar's purple/cyan/green, and shaped
   to a pill instead of a circle. Two layers, exactly as the avatar does
   it: an animated box-shadow on the element itself, plus a blurred
   rotating conic-gradient ::after sitting behind it (z-index -1 inside
   the link's own stacking context) so the light reads as orbiting the
   button rather than sitting statically behind it. */
/* Toned down 2026-07-26 after Harper saw the first version: "too much."
   Lighter lavender fill, the shine sitting UNDER the button and around
   its border rather than blazing off it, and every animation slowed to
   4.5-7s so it reads as a slow breath instead of a flicker. A nav bar
   is furniture, and furniture that pulses at 1.9s is furniture you
   end up looking at instead of through. */
.top-nav a.nav-highlight {
  position: relative; z-index: 0;
  color: #ede4ff;
  font-weight: 700;
  background: rgba(196, 165, 255, 0.16);
  border: 1px solid rgba(196, 165, 255, 0.5);
  border-bottom-width: 1px;
  animation: nav-highlight-glow 4.5s ease-in-out infinite;
}
/* The light travels ALONG the border rather than being a gradient
   spinning behind the button (2026-07-26, Harper's note).

   Two things make that work, and both are load-bearing:

   1. The ANGLE is animated, not the element. Rotating the ::after box
      itself sweeps its own diagonal, and since the pill is much wider
      than it is tall, that threw bright streaks well above and below
      the nav bar. Animating `--beam` through @property rotates only
      the gradient, so nothing ever paints outside the box.
   2. The mask keeps only the 2px ring. `padding: 2px` plus a
      content-box mask composited as `exclude` punches out the middle,
      so the gradient survives on the border and nowhere else.

   If @property is unsupported the custom property never animates and
   the beam simply sits still -- a static gradient edge, not a broken
   button. */
.top-nav a.nav-highlight::after {
  content: "";
  position: absolute; inset: -2px; z-index: -1;
  border-radius: 11px;
  padding: 2px;
  background: conic-gradient(from var(--beam),
    transparent 0turn, transparent 0.60turn,
    rgba(180, 130, 255, 0.35) 0.70turn,
    #ffffff 0.785turn,
    rgba(255, 158, 226, 0.95) 0.85turn,
    transparent 0.95turn, transparent 1turn);
  -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;
  animation: nav-beam 3.4s linear infinite;
}
.top-nav a.nav-highlight:hover {
  color: #fff;
  background: rgba(196, 165, 255, 0.28);
  border-color: rgba(180, 130, 255, 0.85);
  /* Was rgba(216, 190, 255, ...), a leftover from before the 2026-07-27
     Blog/Twitch/nav-highlight color unification below -- that pass never
     touched this hover rule, this keyframe, or the ::after beam above, so
     all three kept animating through the old lavender after everything
     else moved to the new purple. Found in the 2026-07-28 color audit. */
}
.top-nav a.nav-highlight:hover::after { animation-duration: 1.8s; }
.top-nav a.nav-highlight.active {
  color: #fff;
  background: linear-gradient(135deg, var(--purple), var(--pink));
  border-color: rgba(255, 158, 226, 0.9);
}
/* Kept deliberately tight. A wider shadow looks better in isolation but
   .top-nav is a scroll container, so anything past the 11px of padding
   above is clipped flat at the bar's edge, and the horizontal spill (which
   is NOT clipped) hazes over whatever link sits to the left. Verified in a
   browser at 1400px: at this size the corona reads as a ring around the
   pill instead of a rectangle of fog over "Connect". */
/* Offset downward (0 4px) rather than centred, so the light pools
   under the button like it's sitting on a lit surface. */
@keyframes nav-highlight-glow {
  0%, 100% { box-shadow: 0 4px 10px -2px rgba(145, 70, 255, 0.4); }
  50%      { box-shadow: 0 5px 13px -2px rgba(180, 130, 255, 0.5); }
}
@keyframes nav-highlight-corona {
  0%, 100% { opacity: 0.34; }
  50%      { opacity: 0.5; }
}
/* Animatable angle for the border beams below. @property is what makes
   a custom property interpolate at all -- without the registration a
   keyframe on --beam would jump from 0 to 360 with nothing in between. */
@property --beam { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
@keyframes nav-beam { to { --beam: 360deg; } }

/* ---------- The help button ----------
   Harm reduction, deliberately NOT lavender. Amber/red, a first-aid
   emoji, and a warmer, slower pulse than the two lavender buttons, so
   someone scanning the bar at 3am reads it as "help lives here" rather
   than as another page Harper is proud of. Amber reads as caution
   without the alarm of pure red, and the red only shows up in the glow
   underneath, which is what stops it looking like a warning about the
   page rather than an offer of help.

   There is exactly one of these. If a second ever gets added, both stop
   meaning anything -- same argument as the lavender pair, but sharper,
   because this one is the only element on the site claiming urgency. */
.top-nav a.nav-help {
  position: relative; z-index: 0;
  color: #ffe6b0;
  font-weight: 700;
  background: rgba(255, 204, 102, 0.15);
  border: 1px solid rgba(255, 176, 92, 0.55);
  animation: nav-help-glow 5s ease-in-out infinite;
}

/* The Live button (2026-07-27). Twitch purple, and deliberately calmer
   than .nav-help: this one is an invitation, not an emergency, so it
   gets a steady tint and a soft glow rather than the sweeping beam. The
   red dot only appears when she is ACTUALLY live -- js/twitch-live.js
   sets body.tw-is-live -- so the nav never claims a stream that isn't
   happening. */
/* ---------- Blog image picker (js/blog-admin.js) ---------- */
.bla-browse-row { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
.bla-img-preview {
  width: 54px; height: 54px; object-fit: cover;
  border-radius: var(--radius-sm); border: 1px solid var(--border);
}
.bla-browse-btn { padding: 7px 13px; font-size: 0.82rem; }
/* Drop target feedback. Without it there is no way to tell the field
   accepts a drag at all. */
.is-dropping { outline: 2px dashed var(--purple-bright); outline-offset: 4px; border-radius: var(--radius-sm); }

.bla-picker-back {
  position: fixed; inset: 0; z-index: 400;
  background: rgba(4, 2, 8, 0.78); backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.bla-picker {
  width: min(920px, 100%); max-height: 82vh; display: flex; flex-direction: column;
  background: var(--panel-solid, var(--panel));
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: 0 24px 60px -12px rgba(0,0,0,0.8); overflow: hidden;
}
.bla-picker-head {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px; border-bottom: 1px solid var(--border);
}
.bla-picker-search { flex: 1; }
.bla-picker-close {
  background: none; border: 1px solid var(--border); border-radius: 50%;
  width: 28px; height: 28px; color: var(--text-muted); cursor: pointer;
  font-size: 1.1rem; line-height: 1; padding: 0; flex-shrink: 0;
}
.bla-picker-close:hover { color: var(--text); border-color: var(--border-strong); }
.bla-picker-body { overflow-y: auto; padding: 14px 16px 20px; }
.bla-picker-folder {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-dim); font-weight: 700; margin: 16px 0 8px;
}
.bla-picker-folder:first-child { margin-top: 0; }
.bla-picker-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 10px;
}
.bla-picker-cell {
  display: flex; flex-direction: column; gap: 5px; padding: 6px;
  background: var(--panel-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); cursor: pointer; font-family: var(--font);
  transition: border-color 0.15s, transform 0.15s;
}
.bla-picker-cell:hover { border-color: var(--purple-bright); transform: translateY(-2px); }
.bla-picker-cell:focus-visible { outline: 2px solid var(--purple-bright); outline-offset: 2px; }
.bla-picker-cell img {
  width: 100%; aspect-ratio: 1; object-fit: cover;
  border-radius: 4px; background: #000; display: block;
}
.bla-picker-name {
  font-size: 0.66rem; color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Real pixel size, from the manifest. A 165x220 photo looks perfectly
   fine as a 104px thumbnail and falls apart at 900px, so the number has
   to be visible at the moment of choosing, not discovered afterwards. */
.bla-picker-dim { font-size: 0.6rem; color: var(--text-dim); opacity: 0.75; }
.bla-picker-dim.is-small { color: var(--amber); opacity: 1; }
.bla-picker-cell.is-small { border-color: rgba(255, 204, 102, 0.4); }
.bla-hint.is-warn { color: var(--amber); }
@media (prefers-reduced-motion: reduce) { .bla-picker-cell:hover { transform: none; } }

/* ---------- Share menu (js/reactions.js) ----------
   Replaces a Share button that silently copied to the clipboard and
   gave no feedback, which is why it read as broken. Anchored to the
   button's own row, so it works identically in the inline strip and in
   the floating corner bubble. */
.rx-row { position: relative; }
.rx-share-menu {
  position: absolute; bottom: calc(100% + 8px); right: 0;
  z-index: 300; min-width: 172px;
  display: flex; flex-direction: column;
  background: var(--panel-solid, var(--panel));
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: 0 12px 34px -8px rgba(0,0,0,0.75);
  overflow: hidden;
  animation: rx-share-in 0.14s ease-out;
}
@keyframes rx-share-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.rx-share-item {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 13px; font-size: 0.84rem;
  color: var(--text-muted); text-decoration: none;
  background: none; border: none; width: 100%;
  font-family: var(--font); cursor: pointer; text-align: left;
}
.rx-share-item:hover { background: rgba(168,85,247,0.16); color: var(--text); }
.rx-share-item:focus-visible { outline: 2px solid var(--purple-bright); outline-offset: -2px; }
.rx-share-ico { font-size: 0.95rem; width: 1.15em; text-align: center; }
.rx-share-copy { border-top: 1px solid var(--border); }
/* In the corner bubble the menu would otherwise run off-screen right. */
.rx-float .rx-share-menu { right: 0; left: auto; }
@media (prefers-reduced-motion: reduce) { .rx-share-menu { animation: none; } }

/* ---------- The loud row: Blog || Twitch ----------
   All three share ONE colour as of 2026-07-27. They used to be lavender,
   lavender and Twitch-purple, which read as "two things plus an odd one
   out" rather than as a set. Same treatment for all three, with a pipe
   between them so they don't smear into one another. */
.top-nav a.nav-highlight,
.top-nav a.nav-twitch,
/* The Twitch parent is an <a class="nav-drop-toggle nav-twitch">, so it
   needs the same treatment as the standalone loud buttons or it renders
   as a plain dropdown label sitting between two glowing ones. */
.top-nav .nav-drop-toggle.nav-twitch,
.top-nav .nav-drop-toggle.nav-highlight {
  position: relative;
  color: #dcc7ff;
  font-weight: 700;
  background: rgba(145, 70, 255, 0.18);
  border: 1px solid rgba(145, 70, 255, 0.55);
  box-shadow: 0 0 14px -4px rgba(145, 70, 255, 0.7);
}
.top-nav a.nav-highlight:hover,
.top-nav a.nav-twitch:hover,
.top-nav .nav-drop-toggle.nav-twitch:hover,
.top-nav .nav-drop-toggle.nav-highlight:hover { color: #fff; background: rgba(145, 70, 255, 0.3); }
/* An <a> styled as a toggle inherits none of the button reset, so match
   it explicitly rather than relying on the shared .nav-drop-toggle rule. */
.top-nav a.nav-drop-toggle { text-decoration: none; display: inline-flex; align-items: center; }

/* ---------- Split nav control: link + caret ----------
   Twitch is both a destination and a group. The label navigates, the
   caret opens the menu. They are two separate elements because one click
   cannot do both jobs, but they have to READ as one tab, so the shared
   edge loses its radius on both sides and the caret inherits whatever
   loud treatment the label has. */
.top-nav a.nav-drop-link {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  padding-right: 8px;
}
.nav-drop-caret {
  display: inline-flex; align-items: center; justify-content: center;
  /* 12px right / 8px left against a ~7px glyph gives a 27px hit area.
     It was 20px when first shipped, under the 24px minimum, which on a
     phone means the tab beside it catches the tap instead. */
  min-width: 24px;
  padding: 10px 12px 10px 8px;
  border-radius: 0 9px 9px 0;
  background: none; border: 0;
  border-bottom: 2px solid transparent;
  color: var(--text-muted);
  cursor: pointer;
  font-family: var(--font);
  transition: all 0.18s;
}
.nav-drop-caret .caret { font-size: 0.7em; transition: transform 0.18s; }
.nav-drop-caret[aria-expanded="true"] .caret { transform: rotate(180deg); }
.nav-drop-caret:hover { color: var(--text); background: var(--panel-2); }
.nav-drop-caret:focus-visible {
  outline: 2px solid var(--purple-bright, #a855f7);
  outline-offset: -2px;
}
/* The loud pair: the caret carries the same fill, border and glow as the
   label, minus the left border, so the seam between them disappears. */
.top-nav .nav-drop-caret.nav-twitch,
.top-nav .nav-drop-caret.nav-highlight {
  color: #dcc7ff;
  background: rgba(145, 70, 255, 0.18);
  border: 1px solid rgba(145, 70, 255, 0.55);
  border-left: 0;
}
.top-nav .nav-drop-caret.nav-twitch:hover,
.top-nav .nav-drop-caret.nav-highlight:hover {
  color: #fff; background: rgba(145, 70, 255, 0.3);
}
/* The label keeps the breathing glow; the caret only needs the fill, or
   the two animations beat against each other at the seam. */
.top-nav .nav-drop-caret.nav-twitch { animation: none; }

/* Twitch gets a live glow of its own on top of the shared loud styling
   (2026-07-27, Harper's ask). The pair of drop-shadows is deliberate:
   one tight and bright to define the edge, one wide and soft to throw
   colour onto the bar around it. A single large shadow just makes the
   button look blurry.

   This is the only ANIMATED glow among the loud three, which is the
   point of it. Blog is static purple, so a breathing one
   reads as "this is the thing that might be happening right now"
   without needing a second colour. When the channel is actually live,
   body.tw-is-live speeds it up and the red dot appears on top. */
.top-nav a.nav-twitch,
.top-nav .nav-drop-toggle.nav-twitch {
  animation: nav-twitch-breathe 3.4s ease-in-out infinite;
}
@keyframes nav-twitch-breathe {
  0%, 100% {
    box-shadow: 0 0 10px -3px rgba(145, 70, 255, 0.75),
                0 0 22px -6px rgba(145, 70, 255, 0.35);
    border-color: rgba(145, 70, 255, 0.55);
  }
  50% {
    box-shadow: 0 0 16px -2px rgba(169, 112, 255, 0.95),
                0 0 40px -6px rgba(145, 70, 255, 0.7);
    border-color: rgba(190, 145, 255, 0.9);
  }
}
/* Live: same glow, faster and hotter. Nothing new to learn, just more. */
body.tw-is-live .top-nav a.nav-twitch,
body.tw-is-live .top-nav .nav-drop-toggle.nav-twitch {
  animation-duration: 1.6s;
}
@media (prefers-reduced-motion: reduce) {
  .top-nav a.nav-twitch,
  .top-nav .nav-drop-toggle.nav-twitch {
    animation: none;
    box-shadow: 0 0 16px -3px rgba(169, 112, 255, 0.9);
  }
}

.nav-sep {
  align-self: center;
  color: rgba(145, 70, 255, 0.5);
  font-weight: 700;
  letter-spacing: -1px;
  padding: 0 2px;
  user-select: none;
}

/* ---------- MINIMUM TARGET SIZE ----------
   WCAG 2.5.8 asks for 24x24 CSS pixels on anything you tap. A mobile
   audit on 27 July found four controls under that: the nav caret (20px
   wide), the guestbook "sign it" link in the sidebar journey card
   (59x15), and the two modal close buttons (20x24 and 26x26 with no
   padding to spare).

   Inline links inside a paragraph are deliberately NOT included. The
   spec exempts them, and padding out a link mid-sentence would break
   the line spacing of every page on the site to solve a problem the
   guideline says is not one. */
#lh-journey .journey-row .val a {
  display: inline-block;
  min-height: 24px;
  line-height: 24px;
  padding: 0 4px;
  margin: -0 -4px;
}
.gd-close,
.askme-close {
  min-width: 28px;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* ---------- The Help sign ----------
   Not a nav link. A hazard sign: red body, yellow/black diagonal
   hatching top and bottom, arrows pointing in from both sides. It is
   allowed to be the ugliest thing on the site -- that is the job. */
.nav-help-sign {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 8px; border-radius: 8px; text-decoration: none;
  background: #c0181f;
  border: 1px solid #ff5a4d;
  box-shadow: 0 0 16px -4px rgba(255, 60, 50, 0.85);
  position: relative;
  overflow: hidden;
}
.nav-help-sign::before,
.nav-help-sign::after {
  content: ""; position: absolute; left: 0; right: 0; height: 4px;
  background: repeating-linear-gradient(45deg,
    #ffcc00 0 6px, #101010 6px 12px);
}
.nav-help-sign::before { top: 0; }
.nav-help-sign::after  { bottom: 0; }
.nhs-body {
  font-size: 0.82rem; font-weight: 800; color: #fff; white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0,0,0,0.6);
  padding: 3px 2px;
}
.nhs-arrow {
  color: #ffcc00; font-size: 0.9rem; line-height: 1;
  animation: nhs-point 1.1s ease-in-out infinite;
}
.nhs-arrow-r { animation-delay: 0.55s; }
@keyframes nhs-point {
  0%, 100% { transform: translateX(0); opacity: 0.55; }
  50%      { transform: translateX(3px); opacity: 1; }
}
.nhs-arrow-r { }
/* Red / orange / white glow (2026-07-27, Harper's ask). Three layered
   shadows rather than one, cycling outward: white at the edge, orange
   just past it, red furthest out. That ordering matters. It is how a hot
   thing actually looks, white at the source falling off through orange
   to red, and reversing it reads as a cheap neon outline.

   Slower than the arrow animation next to it (2.4s against 1.1s) so the
   two do not lock into a rhythm and start looking like one blinking
   unit. The sign should look lit, not like it is flashing at you. */
.nav-help-sign {
  animation: nhs-glow 2.4s ease-in-out infinite;
}
@keyframes nhs-glow {
  0%, 100% {
    box-shadow: 0 0 6px -1px rgba(255, 255, 255, 0.5),
                0 0 16px -3px rgba(255, 138, 40, 0.65),
                0 0 30px -6px rgba(224, 29, 37, 0.6);
  }
  50% {
    box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.9),
                0 0 26px -2px rgba(255, 154, 60, 0.95),
                0 0 52px -4px rgba(240, 40, 40, 0.85);
  }
}
.nav-help-sign:hover { background: #e01d25; }
.nav-help-sign:focus-visible { outline: 2px solid #ffcc00; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .nhs-arrow { animation: none; opacity: 1; }
  /* Still lit, just not moving. An emergency affordance losing its
     visual weight at this setting would be the wrong trade. */
  .nav-help-sign {
    animation: none;
    box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.8),
                0 0 24px -2px rgba(255, 154, 60, 0.9),
                0 0 46px -4px rgba(240, 40, 40, 0.8);
  }
}
@media (max-width: 900px) {
  /* The bar wraps here and a divider mid-wrap looks like a broken glyph. */
  .nav-sep { display: none; }
  .nhs-body { font-size: 0.76rem; }
}

body.tw-is-live .top-nav a.nav-twitch::after {
  content: ""; position: absolute; top: 4px; right: 4px;
  width: 7px; height: 7px; border-radius: 50%; background: #ff4d4d;
  box-shadow: 0 0 8px rgba(255, 77, 77, 0.9);
  animation: tw-pulse 1.8s ease-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  body.tw-is-live .top-nav a.nav-twitch::after { animation: none; }
}
.top-nav a.nav-help::after {
  content: "";
  position: absolute; inset: -2px; z-index: -1;
  border-radius: 11px;
  padding: 2px;
  background: conic-gradient(from var(--beam),
    transparent 0turn, transparent 0.60turn,
    rgba(255, 204, 102, 0.4) 0.70turn,
    #fff3d6 0.785turn,
    rgba(255, 120, 90, 0.95) 0.85turn,
    transparent 0.95turn, transparent 1turn);
  -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;
  animation: nav-beam 3.4s linear infinite;
}
.top-nav a.nav-help:hover {
  color: #fff4dd;
  background: rgba(255, 204, 102, 0.26);
  border-color: rgba(255, 196, 120, 0.9);
}
.top-nav a.nav-help:hover::after { animation-duration: 1.8s; }
.top-nav a.nav-help.active {
  color: #2b1400;
  background: linear-gradient(135deg, var(--amber), #ff8a5c);
  border-color: rgba(255, 224, 170, 0.9);
}
@keyframes nav-help-glow {
  0%, 100% { box-shadow: 0 4px 10px -2px rgba(255, 140, 80, 0.42); }
  50%      { box-shadow: 0 5px 13px -2px rgba(255, 204, 102, 0.5); }
}
html[data-theme="light"] .top-nav a.nav-help,
html[data-theme="light"] .top-nav a.nav-help:hover { color: #6b3200; }
@media (prefers-reduced-motion: reduce) {
  .top-nav a.nav-help { animation: none; box-shadow: 0 4px 10px -2px rgba(255, 140, 80, 0.4); }
  .top-nav a.nav-help::after { animation: none; }
}

/* Light theme keeps the glow but darkens the label, since white-on-tint
   loses too much contrast against a pale bar. */
html[data-theme="light"] .top-nav a.nav-highlight,
html[data-theme="light"] .top-nav a.nav-highlight:hover { color: #4a1063; }
html[data-theme="light"] .top-nav a.nav-highlight.active { color: #fff; }

@media (prefers-reduced-motion: reduce) {
  .top-nav a.nav-highlight { animation: none; box-shadow: 0 4px 10px -2px rgba(168, 85, 247, 0.4); }
  .top-nav a.nav-highlight::after { animation: none; }
}

/* Adding a ninth item to the bar cost about 70px, and .top-nav scrolls
   horizontally with the scrollbar hidden, so on a 1280px laptop the
   overflow was silent and the thing it pushed off the end was Blog: the
   one item added specifically to be seen. Tightening the spacing between
   1024px and 1400px buys back roughly 78px, which covers it.

   Below 1024px the bar has always scrolled, and Blog also lives in the
   sidebar quick-links, which is what the mobile menu opens. */
@media (max-width: 1400px) {
  .top-nav { gap: 2px; }
  .top-nav a, .nav-drop-toggle { padding-left: 10px; padding-right: 10px; }
}

.nav-drop-toggle:hover { color: var(--text); background: var(--panel-2); }
.nav-drop-toggle.active { color: #fff; border-bottom-color: var(--purple-bright); }
.nav-drop-toggle .caret { font-size: 0.7em; transition: transform 0.18s; }
.nav-drop-toggle[aria-expanded="true"] .caret { transform: rotate(180deg); }
.nav-drop-menu {
  position: fixed; z-index: 300; min-width: 220px;
  background: var(--panel-solid); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: 6px; box-shadow: 0 16px 44px rgba(0,0,0,0.55);
  display: none; flex-direction: column; gap: 2px;
  /* Adult Dynamics grew to ~29 entries (2026-07-25). Without a cap, a
     fixed-position flyout that tall runs off the bottom of shorter
     viewports. Scroll it instead of letting it grow unbounded. */
  max-height: min(70vh, 560px); overflow-y: auto; scrollbar-width: thin;
}
.nav-drop-menu.open { display: flex; }
.nav-drop-menu a {
  padding: 9px 12px; border-radius: 7px; font-size: 0.88rem; color: var(--text-muted);
  white-space: nowrap; border-bottom: 0;
}
.nav-drop-menu a:hover { background: var(--panel-2); color: var(--text); }
.nav-drop-menu a.active { color: var(--purple-bright); background: rgba(168,130,255,0.1); }

/* Group headings inside a dropdown (js/layout.js emits these for Adult
   Dynamics). Presentational only: not focusable, not clickable, and
   pointer-events:none so dragging across one never eats a click meant
   for the link underneath the cursor. */
.nav-drop-head {
  padding: 4px 12px;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
}
/* A rule above each group rather than a heavier heading. The list is
   already dense; a second strong weight per group would make it worse,
   whereas a hairline reads as structure at a glance. */
.nav-drop-head + * { margin-top: 0; }
.nav-drop-head:not(:first-child) {
  margin-top: 7px;
  padding-top: 11px;
  border-top: 1px solid var(--border);
}
.top-actions { display: flex; align-items: center; gap: 6px; }
.icon-btn { width: 44px; height: 44px; display: grid; place-items: center; border-radius: 10px; color: var(--text-muted); cursor: pointer; background: transparent; border: none; font-size: 1.05rem; transition: all 0.18s; }

.hue-picker { position: relative; }
.hue-popover {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 50;
  display: none; flex-direction: column; gap: 10px;
  width: 210px; padding: 14px 16px;
  background: var(--panel-solid); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  box-shadow: 0 16px 40px rgba(0,0,0,0.4);
}
.hue-popover.show { display: flex; }
.hue-popover-label { font-size: 0.78rem; font-weight: 600; color: var(--text-muted); }
.hue-popover input[type="range"] {
  -webkit-appearance: none; appearance: none; width: 100%; height: 10px; border-radius: 6px;
  background: linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000);
  outline: none; cursor: pointer;
}
/* outline:none above is needed (the default ring sits badly on a rainbow
   track) but it beats the sitewide focus rule on specificity, which left
   this control with no keyboard focus state at all. Ring drawn as a shadow
   instead so it follows the rounded track. */
.hue-popover input[type="range"]:focus-visible {
  box-shadow: 0 0 0 2px var(--panel-solid), 0 0 0 4px var(--purple-bright);
}
.hue-popover input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%;
  background: #fff; border: 2px solid var(--purple-bright); box-shadow: 0 0 6px rgba(0,0,0,0.5); cursor: pointer;
}
.hue-popover input[type="range"]::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%; background: #fff; border: 2px solid var(--purple-bright);
  box-shadow: 0 0 6px rgba(0,0,0,0.5); cursor: pointer;
}
.hue-reset {
  align-self: flex-start; font-size: 0.78rem; font-weight: 600; color: var(--purple-bright);
  background: transparent; border: none; cursor: pointer; padding: 0;
}
.hue-reset:hover { color: var(--pink-bright); }
@media (max-width: 380px) {
  /* Only very narrow phones need help; right:0 already clears the topbar
     edge comfortably down to ~320px because the theme toggle sits to the
     picker's right. Shrink the popover slightly instead of shifting it
     further toward the edge (a prior fix here did that and made things
     worse — verified by measuring against .top-actions layout). */
  .hue-popover { width: 180px; padding: 12px 14px; }
}
.icon-btn:hover { background: var(--panel-2); color: var(--purple-bright); }
.menu-toggle { display: none; }

.page {
  padding: 30px 34px 60px; width: 100%; margin: 0 auto;
  /* Previously a flat max-width: 1400px with no auto margin, so .main
     (a flex column) left-aligned it — on ultrawide monitors that meant a
     1400px column flush against the sidebar with a large dead void on the
     right, not a centered layout. Now centers properly. width:100% already
     caps this at .main's real available width (viewport minus sidebar) on
     anything up to a large laptop screen, so the clamp()'s min bound only
     needs to sit at-or-below that natural floor — 1400px matters starting
     around a 1650px-ish viewport, never forces an unreachable width below
     it. The ceiling scales with viewport so genuine ultrawide (3440px+)
     gets meaningfully more page width for the grid-heavy Phase 2 layouts,
     instead of a fixed island with growing dead margins on either side. */
  max-width: clamp(1400px, 82vw, 2200px);
}
.page.wide { max-width: clamp(1500px, 88vw, 2320px); }

/* ---------- Hero (home) ---------- */
.hero { position: relative; overflow: hidden; border-radius: 24px; padding: 54px 48px; background: var(--grad-aurora); border: 1px solid var(--border-strong); box-shadow: var(--glow-purple); margin-bottom: 26px; }
.hero::before {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(60% 90% at 78% 15%, rgba(45, 232, 160,0.22), transparent 60%),
    radial-gradient(50% 80% at 92% 40%, rgba(168, 85, 247,0.3), transparent 60%),
    radial-gradient(40% 70% at 60% 5%, rgba(255, 94, 203,0.2), transparent 60%);
  filter: blur(8px);
  animation: aurora-drift 12s ease-in-out infinite alternate;
}
.hero::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.35;
  background-image:
    linear-gradient(rgba(168,85,247,0.12) 1px, transparent 1px),
    linear-gradient(90deg, rgba(168,85,247,0.12) 1px, transparent 1px);
  background-size: 34px 34px;
  mask-image: radial-gradient(ellipse 80% 80% at 50% 20%, #000 40%, transparent 90%);
}
.hero > * { position: relative; z-index: 1; }
.hero h1 { font-size: clamp(2.4rem, 5vw, 3.6rem); letter-spacing: -1.5px; }
.hero h1 .accent { background: var(--grad-brand); -webkit-background-clip: text; background-clip: text; color: transparent; filter: drop-shadow(0 0 18px rgba(168,85,247,0.55)) drop-shadow(0 0 32px rgba(255,94,203,0.3)); }
.hero .lead { font-size: 1.15rem; color: var(--text); margin: 16px 0 8px; max-width: 620px; } /* was a hardcoded light color — invisible against the light theme's bg once it got theme-aware; --text ~= the old value in dark mode anyway */
.hero .sub { color: var(--text-muted); max-width: 600px; }
.hero-btns { display: flex; gap: 14px; margin-top: 28px; flex-wrap: wrap; }

/* The homepage used to open like a competent but generic portfolio.
   These are the five words Harper actually uses for herself, rendered as
   tiny profile-status pills rather than another paragraph of biography. */
.hero-kicker {
  display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 14px;
  font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
  font-size: 0.68rem; letter-spacing: 0.7px; text-transform: uppercase;
}
.hero-kicker span {
  padding: 4px 9px; border: 1px solid var(--border-strong); border-radius: 999px;
  color: var(--pink-bright); background: rgba(224, 24, 193, 0.08);
  box-shadow: 0 0 12px rgba(224, 24, 193, 0.08);
}
.hero-kicker span:nth-child(2n) { color: var(--cyan); background: rgba(34, 229, 255, 0.07); }

.harper-manifesto {
  display: grid; grid-template-columns: auto 1fr; gap: 16px; align-items: center;
  margin: -8px 0 24px; padding: 15px 18px;
  border: 1px solid var(--border); border-left: 3px solid var(--pink-bright);
  border-radius: var(--radius-sm); background: linear-gradient(100deg, rgba(224,24,193,0.1), var(--panel));
}
.harper-manifesto p { color: var(--text-muted); font-size: 0.9rem; margin: 0; }
.manifesto-mark {
  color: var(--green); font: 700 0.7rem/1 ui-monospace, SFMono-Regular, Consolas, monospace;
  letter-spacing: 1px; white-space: nowrap;
}
@media (max-width: 560px) {
  .harper-manifesto { grid-template-columns: 1fr; gap: 8px; }
}

.btn { display: inline-flex; align-items: center; gap: 9px; padding: 12px 22px; border-radius: 12px; font-weight: 600; font-size: 0.92rem; cursor: pointer; border: 1px solid transparent; transition: all 0.2s; }
.btn-primary { background: var(--grad-brand); color: #fff; box-shadow: var(--glow-purple); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 6px 30px rgba(168, 85, 247,0.6); }
.btn-secondary {
  color: var(--text);
  background: rgba(168, 85, 247, 0.13);
  border-color: var(--border-strong);
  box-shadow: inset 0 0 0 1px rgba(168, 85, 247, 0.08);
}
.btn-secondary:hover {
  transform: translateY(-2px);
  background: rgba(168, 85, 247, 0.22);
  border-color: var(--purple-bright);
}
.btn-ghost { background: rgba(255,255,255,0.05); border-color: var(--border-strong); color: var(--text); }
.btn-ghost:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }

.stats { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin-bottom: 30px; }
.stat { padding: 20px 14px; text-align: center; border-right: 1px solid var(--border); }
.stat:last-child { border-right: none; }
.stat .ico { font-size: 1.15rem; margin-bottom: 6px; }
.stat .num { font-family: var(--font-display); font-size: 1.5rem; font-weight: 800; color: var(--purple-bright); }
.stat .lbl { font-size: 0.72rem; color: var(--text-muted); }

.sec-head { display: flex; align-items: center; justify-content: space-between; margin: 8px 0 16px; }
.sec-head h2 { font-size: 1.35rem; display: flex; align-items: center; gap: 10px; }
.sec-head h2 .mark { color: var(--purple-bright); }
.sec-head .more { font-size: 0.85rem; color: var(--purple-bright); font-weight: 500; }
.sec-head .more:hover { text-decoration: underline; }

/* ---------- Cards / panels ---------- */
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px; backdrop-filter: blur(8px); }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; }

.project-card { position: relative; overflow: hidden; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px; transition: all 0.25s ease; }
.project-card::after { content: ""; position: absolute; top: 0; right: 0; width: 140px; height: 140px; background: radial-gradient(circle at top right, rgba(168, 85, 247,0.25), transparent 70%); opacity: 0; transition: opacity 0.3s; }
.project-card:hover { transform: translateY(-4px); border-color: var(--border-strong); box-shadow: 0 12px 40px rgba(168, 85, 247,0.18); }
.project-card:hover::after { opacity: 1; }
.project-glance {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 1fr));
  gap: 12px;
  margin: 0 0 24px;
}
.project-glance-card {
  display: block;
  padding: 17px 18px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.project-glance-card:hover {
  transform: translateY(-2px);
  border-color: var(--border-strong);
  background: var(--panel-2);
}
.project-glance-card .pg-icon { display: block; font-size: 1.2rem; margin-bottom: 7px; }
.project-glance-card strong { display: block; color: var(--text); margin-bottom: 3px; }
.project-glance-card span:last-child { color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; }
.project-deep-dive {
  max-width: 760px;
  margin: 16px 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: rgba(10, 6, 18, 0.2);
}
.project-deep-dive summary {
  padding: 12px 15px;
  color: var(--purple-bright);
  font-weight: 700;
  cursor: pointer;
}
.project-deep-dive summary:hover { color: var(--pink-bright); }
.project-deep-dive-body { padding: 0 15px 6px; }
@media (prefers-reduced-motion: reduce) {
  .project-glance-card:hover { transform: none; }
}
.project-card .pc-head { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
.project-card .pc-icon { width: 42px; height: 42px; border-radius: 11px; display: grid; place-items: center; font-size: 1.3rem; background: rgba(168, 85, 247,0.15); border: 1px solid var(--border); }
.project-card h2, .project-card h3 { font-size: 1.15rem; }
.project-card .pc-tag { font-size: 0.78rem; color: var(--pink-bright); font-weight: 600; margin-bottom: 12px; }
.project-card p { font-size: 0.9rem; color: var(--text-muted); margin-bottom: 14px; }
.tags { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 14px; }
.tag { display: inline-block; white-space: nowrap; font-size: 0.72rem; padding: 4px 10px; border-radius: 7px; background: rgba(168, 85, 247,0.12); border: 1px solid var(--border); color: var(--purple-bright); }
.pc-link { font-size: 0.85rem; color: var(--purple-bright); font-weight: 600; }
.pc-link:hover { text-decoration: underline; }

/* ---------- Home layout ---------- */
.home-grid { display: grid; grid-template-columns: 1fr 320px; gap: 26px; align-items: start; }
.now-home {
  margin: 0 0 30px;
  padding: clamp(20px, 3vw, 28px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
}
.now-home .sec-head { align-items: baseline; }
.now-home-intro { color: var(--text-muted); margin: -4px 0 16px; }
.now-home-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.now-home-grid .now-item { margin: 0; height: 100%; }
@media (max-width: 1150px) { .now-home-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px) { .now-home-grid { grid-template-columns: 1fr; } }
.home-side-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; align-items: start; }
.home-side-grid .grid-3 { grid-template-columns: 1fr; }
.rail { display: flex; flex-direction: column; gap: 18px; position: sticky; top: calc(var(--topbar-h) + 20px); }
.rail-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.rail-card .rc-img { width: 100%; height: 280px; object-fit: cover; object-position: center top; }
.rail-card .rc-body { padding: 16px 18px; }
.rail-card h3, .rail-card h4 { font-size: 1rem; display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.rail-card h3 .em, .rail-card h4 .em { color: var(--pink); }
.rail-card p { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 8px; }
.rail-card .rc-link { font-size: 0.82rem; color: var(--purple-bright); font-weight: 600; }

.rail-card .rc-link:hover { text-decoration: underline; }
.rc-stats { display: flex; gap: 18px; margin: 4px 0 12px; }
.rc-stats .s .n { font-family: var(--font-display); font-weight: 800; font-size: 1.15rem; color: var(--purple-bright); }
.rc-stats .s .l { font-size: 0.68rem; color: var(--text-dim); }

/* ---------- Infra diagram (legacy linear, still used for VPS service list) ---------- */
.infra-flow { display: flex; align-items: stretch; gap: 10px; flex-wrap: wrap; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; margin-bottom: 10px; }
.node { flex: 1; min-width: 120px; background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 18px 14px; text-align: center; }
.node .ico { font-size: 1.6rem; margin-bottom: 8px; }
.node .t { font-weight: 600; font-size: 0.92rem; }
.node .s { font-size: 0.74rem; color: var(--text-muted); }
.arrow { display: grid; place-items: center; color: var(--purple); font-size: 1.3rem; }
.svc-list { flex: 1.3; min-width: 150px; background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px; }
.svc-list .svc { display: flex; align-items: center; justify-content: space-between; padding: 5px 4px; font-size: 0.85rem; }
.svc-list .svc .l { display: flex; align-items: center; gap: 8px; }

/* ---------- Home page now reuses the real infra topology diagram
   (js/infra-data.js + js/infra-topology.js, same one infrastructure.html
   uses) instead of a separate static hub widget — see the
   body[data-page="home"] .topology-* size overrides down near the
   topology diagram rules for the smaller "preview" sizing. ---------- */

/* ---------- About page ---------- */
.about-hero { display: grid; grid-template-columns: 260px 1fr; gap: 30px; align-items: center; margin-bottom: 30px; }
.about-hero .photo { border-radius: 20px; overflow: hidden; border: 1px solid var(--border-strong); box-shadow: var(--glow-purple); }
.about-hero .photo img { width: 100%; height: 420px; object-fit: cover; object-position: center top; }

/* The 260x420 box above is tuned for the portrait photos the rest of
   the site uses here (about.html's smile.jpg is 904x1200). Harley2.jpg
   on the "Fluent in Dog" page is a wide 1200x900 shot with Harley at
   the far left edge and Harper crouching at the far right edge, so a
   centered crop into that narrow portrait box cut both of them out
   and left only the empty grass/tree in the middle. Widening the box
   to roughly match the photo's own 4:3 shape lets `cover` crop almost
   nothing instead of cropping symmetrically from both edges. */
body[data-page="dogbehaviorist"] .about-hero .photo img { object-position: center center; }
@media (min-width: 641px) {
  body[data-page="dogbehaviorist"] .about-hero { grid-template-columns: 420px 1fr; }
  body[data-page="dogbehaviorist"] .about-hero .photo img { height: 315px; }
}
/* ------------------------------------------------------------
   Floating text panel (2026-07-27). Body copy sitting directly on
   the aurora/glitch background was fighting the glow for contrast.
   Same card language .panel and .artist-embed already use (var(--panel)
   background, var(--border) border, blurred backdrop) rather than a
   new material -- reads as "this text is in its own frame over the
   effect," the same way a video embed reads as a frame over the page,
   while the panel's own alpha (0.75) still lets the aurora/starfield
   glow bleed through faintly instead of blacking it out. */
.prose {
  max-width: 760px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 28px 32px;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.28);
}
@media (max-width: 640px) {
  .prose { padding: 20px 22px; }
}
.prose h2 { font-size: 1.4rem; margin: 30px 0 12px; }
.prose h2:first-child, .prose h3:first-child { margin-top: 0; }
.prose h3 { font-size: 1.1rem; margin: 22px 0 8px; color: var(--purple-bright); }
.prose p { color: var(--text-muted); margin-bottom: 14px; }
.prose strong { color: var(--text); }
.pill-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 20px; }
.pill { font-size: 0.8rem; padding: 6px 13px; border-radius: 20px; background: rgba(255, 94, 203,0.12); border: 1px solid rgba(255, 94, 203,0.3); color: var(--pink-bright); }

.photo-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 24px 0; }
/* min-width: 0 is load-bearing. A `1fr` grid track is minmax(auto, 1fr),
   and `auto` floors at min-content, which for an image is its intrinsic
   width. Once these images carried real width attributes, three 1035px
   photos refused to shrink below that and pushed the document 520px
   wider than a phone screen. Setting it on the item works regardless of
   the inline grid-template-columns some of these strips override with. */
.photo-strip img { width: 100%; min-width: 0; height: 260px; object-fit: cover; object-position: center top; border-radius: 12px; border: 1px solid var(--border); transition: transform 0.25s; }
.photo-strip img:hover { transform: scale(1.03); }

.value-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; margin: 20px 0; }
.value { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 18px; }
.value .ico { font-size: 1.5rem; margin-bottom: 8px; }
.interest-card h3, .interest-card h4 { font-size: 1rem; margin-bottom: 6px; }
.value h3, .value h4 { font-size: 1rem; margin-bottom: 6px; }
.value p { font-size: 0.85rem; color: var(--text-muted); }

/* ---------- Hytale page ---------- */
.hy-banner { border-radius: 22px; padding: 40px; margin-bottom: 24px; background: linear-gradient(135deg, #16233a, #0f1626); border: 1px solid var(--border); position: relative; overflow: hidden; }
.hy-banner::before { content:""; position:absolute; inset:0; background: radial-gradient(50% 90% at 80% 20%, rgba(143,127,232,0.25), transparent 60%); }
.hy-banner > * { position: relative; }
.hy-banner h1 { font-size: 2.4rem; }
.hy-banner .addr { font-family: monospace; font-size: 1rem; color: var(--cyan); margin-top: 8px; }
/* The raw IP under the friendly hostname. Quieter, because it is the
   fallback for a client that will not resolve a name, not the address
   anybody should be reading first. */
.hy-banner .addr-alt { font-family: monospace; font-size: 0.8rem; color: var(--text-dim); margin-top: 3px; }
.hy-banner .addr-alt code { color: var(--text-muted); background: none; padding: 0; }
.status-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 16px; margin-bottom: 24px; }
.status-box { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 20px; text-align: center; }
.status-box .k { font-size: 0.72rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
.status-box .v { font-family: var(--font-display); font-size: 1.8rem; font-weight: 800; color: var(--purple-bright); margin-top: 4px; }
.badge { display: inline-flex; align-items: center; gap: 7px; padding: 5px 12px; border-radius: 20px; font-size: 0.82rem; font-weight: 600; }
.badge.online { background: rgba(45, 232, 160,0.15); color: var(--green); border: 1px solid rgba(45, 232, 160,0.4); }
.badge.offline { background: rgba(248,113,113,0.15); color: #f87171; border: 1px solid rgba(248,113,113,0.4); }
.badge.checking { background: rgba(255,204,102,0.15); color: var(--amber, #ffcc66); border: 1px solid rgba(255,204,102,0.4); }
.mod-list { display: grid; grid-template-columns: repeat(2,1fr); gap: 10px; }
.mod { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 10px; font-size: 0.88rem; }
.mod .mi { color: var(--cyan); }
.player-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.player-chip { font-size: 0.82rem; padding: 6px 12px; border-radius: 20px; background: rgba(45, 232, 160,0.12); border: 1px solid rgba(45, 232, 160,0.3); color: var(--green); }
.empty-note { font-size: 0.85rem; color: var(--text-dim); font-style: italic; }

/* ---------- Lab / code page ---------- */
.code-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin-bottom: 20px; }
.code-card .cc-head { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
.code-card .cc-head h3 { font-size: 1.02rem; }
/* Was two components for the same thing: lab.html's ".lang" and adhd.html's
   ".cc-file" both label the filename in a code-card header, but with
   different class names and font stacks (one plain "monospace", one
   "SF Mono"/Consolas). Standardized on .cc-file (adhd.html's name, the more
   descriptive one) and on the site's actual code font -- 2026-07-28 pass. */
.code-card .cc-head .cc-file { font-size: 0.72rem; color: var(--cyan); font-family: 'JetBrains Mono', 'Fira Code', monospace; }
pre { margin: 0; padding: 18px; overflow-x: auto; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.82rem; line-height: 1.6; background: rgba(0,0,0,0.28); }
pre code { color: #d6d0f0; }
.tok-kw { color: #c084fc; } .tok-str { color: #7dd3a8; } .tok-com { color: #7f789c; font-style: italic; } /* was #6b6488, 3.46:1 against the code panel */ .tok-fn { color: #38bdf8; } .tok-num { color: #fbbf24; }
.code-card .cc-desc { padding: 14px 18px; font-size: 0.86rem; color: var(--text-muted); border-top: 1px solid var(--border); }

/* ---------- Contact ---------- */
.contact-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 18px; margin-top: 20px; }
.contact-card { display: flex; align-items: center; gap: 16px; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px; transition: all 0.2s; }
.contact-card:hover { transform: translateY(-3px); border-color: var(--border-strong); box-shadow: 0 10px 30px rgba(168, 85, 247,0.15); }
.contact-card .ci { width: 50px; height: 50px; border-radius: 13px; display: grid; place-items: center; font-size: 1.4rem; background: rgba(168, 85, 247,0.15); border: 1px solid var(--border); }
.contact-card .ct { font-size: 0.72rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
.contact-card .cv { font-size: 0.98rem; font-weight: 600; }
.contact-card .cs { font-size: 0.8rem; color: var(--text-muted); }

/* ---------- Pull-quote inside a card ----------
   Used by the writing page's excerpt cards. The quote is the point of
   those cards, so it gets the display face and a left bar rather than
   quotation marks: real punctuation inside the quote then reads as the
   writing's own rather than as part of the decoration. */
.wp-card { display: flex; flex-direction: column; }
.wp-quote {
  margin: 12px 0 14px;
  padding: 2px 0 2px 16px;
  border-left: 3px solid var(--purple-bright);
  font-family: var(--font-display);
  font-size: 1.02rem;
  line-height: 1.5;
  color: var(--text);
  flex: 1;
}
.wp-quote em { color: var(--pink-bright); font-style: italic; }

/* The Ko-fi card. Same shape as its neighbours, so it does not turn the
   contact grid into an advert, but it borrows the sidebar button's lime
   for the icon tile and the value line so the two are recognisably the
   same thing in two places. Deliberately NOT a full lime fill here: in a
   grid of five equal cards, one solid neon block reads as a banner ad and
   people's eyes skip it. */
.contact-card.is-kofi { border-color: rgba(163, 255, 58, 0.4); }
.contact-card.is-kofi:hover { border-color: var(--lime); box-shadow: 0 10px 30px rgba(163, 255, 58, 0.18); }
.contact-card.is-kofi .ci { background: linear-gradient(135deg, var(--lime), var(--lime-deep)); border-color: rgba(220, 255, 150, 0.85); }
.contact-card.is-kofi .cv { color: var(--lime); }
html[data-theme="light"] .contact-card.is-kofi .cv { color: #3f6b00; } /* lime text on a near-white panel is unreadable; this is the same hue at AA */

/* ---------- Resource cards (Trans page, etc.) ---------- */
.resource-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 16px; margin: 16px 0 20px; }
.resource-card { display: flex; align-items: flex-start; gap: 14px; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; transition: all 0.2s; }
.resource-card:hover { transform: translateY(-3px); border-color: var(--border-strong); box-shadow: 0 10px 30px rgba(168, 85, 247,0.15); }
.resource-card.static:hover { transform: none; border-color: var(--border); box-shadow: none; }
/* cursor:default lives here now, not inline -- love.html/poly.html/
   therapy.html/trans.html/safety.html hand-repeated the identical
   style="cursor:default;" 45 times across every .resource-card.static
   instance between them. Found in the 2026-07-28 inline-style audit. */
.resource-card.static { cursor: default; }
.resource-card .ri { font-size: 1.3rem; flex-shrink: 0; margin-top: 2px; }
.resource-card .rt { font-size: 0.98rem; font-weight: 600; color: var(--text); margin-bottom: 3px; }
.resource-card .rv { font-size: 0.84rem; color: var(--text-muted); line-height: 1.5; }

/* ---------- Shared utility classes ---------- */
/* Pull-quote: used for excerpted/attributed quotes (About, Trans, Writing).
   Replaces one-off inline styles that were previously duplicated per-page. */
.pull-quote { font-style: italic; border-left: 3px solid var(--purple-bright); padding-left: 16px; color: var(--text); margin: 14px 0; }
.pull-quote footer, .pull-quote .attribution { font-style: normal; font-size: 0.85rem; color: var(--text-dim); margin-top: 6px; }

/* Inline accent link: matches .accent's gradient-adjacent purple for links
   embedded in prose paragraphs, without repeating inline color styles. */
.link-accent { color: var(--purple-bright); text-decoration: none; }
.link-accent:hover { text-decoration: underline; }

/* Muted section note: small-print context under a section (costs, caveats,
   asides) that shouldn't compete visually with body copy. */
.section-note { font-size: 0.85rem; color: var(--text-muted); margin-top: 8px; }

/* ---------- Character sheet (RPG stat page) ---------- */
.charsheet-header { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 26px; margin-bottom: 26px; }
/* Same purple/cyan/green pulsing glow as the sidebar .avatar-ring
   (see avatar-ring-glow-flicker above), applied to a second circular
   portrait per Harper's request to spread that glow to more elements.
   Reuses the existing keyframe rather than defining a new one -- this
   is the site's one 'glowing circle' recipe, not a new one-off. No
   ::after corona here (charsheet-avatar keeps overflow: hidden to
   clip the portrait itself, which would also clip a corona sitting
   outside its bounds) -- the animated box-shadow alone still reads as
   the same glow since box-shadow paints outside overflow:hidden's own
   clip, just without the extra rotating halo layer. */
.charsheet-avatar { width: 100px; height: 100px; border-radius: 50%; overflow: hidden; border: 3px solid var(--purple-bright); flex-shrink: 0; animation: avatar-ring-glow-flicker 1.6s ease-in-out infinite; }
.charsheet-avatar img { width: 100%; height: 100%; object-fit: cover; object-position: top center; }
@media (prefers-reduced-motion: reduce) { .charsheet-avatar { animation: none; box-shadow: 0 0 0 4px rgba(168, 85, 247, 0.15); } }
.charsheet-title h2 { font-size: 1.6rem; margin-bottom: 6px; }
.charsheet-title .sub { color: var(--purple-bright); font-weight: 600; font-size: 0.95rem; margin-bottom: 10px; }
.charsheet-meta { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 0.83rem; color: var(--text-muted); }
.charsheet-meta b { color: var(--text); }

.stat-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; margin: 18px 0 26px; }
.stat-box { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 16px 10px; text-align: center; }
.stat-box .stat-label { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); }
.stat-box .stat-score { font-family: var(--font-display); font-size: 1.7rem; font-weight: 800; color: var(--purple-bright); margin: 4px 0; }
.stat-box .stat-flavor { font-size: 0.7rem; color: var(--text-muted); line-height: 1.35; }

.meter-row { margin-bottom: 14px; max-width: 620px; }
.meter-labels { display: flex; justify-content: space-between; font-size: 0.85rem; margin-bottom: 5px; }
.meter-labels .val { color: var(--text-dim); font-size: 0.78rem; }
.meter-track { height: 14px; border-radius: 8px; background: var(--panel-2); border: 1px solid var(--border); overflow: hidden; }
.meter-fill { height: 100%; border-radius: 8px; background: linear-gradient(90deg, var(--purple-bright), var(--pink-bright)); }
.meter-fill.mana { background: linear-gradient(90deg, var(--cyan), var(--purple-bright)); }
.meter-fill.sanity { background: linear-gradient(90deg, var(--amber), var(--pink-bright)); }
.meter-fill.social { background: linear-gradient(90deg, #f87171, var(--amber)); }

.skill-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px 28px; margin: 14px 0 6px; }
.skill-row .skill-labels { display: flex; justify-content: space-between; font-size: 0.85rem; margin-bottom: 4px; }
.skill-row .skill-labels .lvl { color: var(--purple-bright); font-weight: 700; flex-shrink: 0; margin-left: 10px; }
.skill-track { height: 10px; border-radius: 6px; background: var(--panel-2); border: 1px solid var(--border); overflow: hidden; }
.skill-fill { height: 100%; background: linear-gradient(90deg, var(--purple-bright), var(--pink-bright)); border-radius: 6px; }

.inventory-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 14px 0 22px; }
.inv-item { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; font-size: 0.85rem; transition: all 0.2s; }
.inv-item:hover { transform: translateY(-2px); border-color: var(--border-strong); }
.inv-item .rarity { display: block; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 700; margin-bottom: 3px; }
.inv-item .rarity.legendary { color: #f5a623; }
.inv-item .rarity.epic { color: var(--purple-bright); }
.inv-item .rarity.rare { color: var(--cyan); }
.inv-item .rarity.common { color: var(--text-dim); }
.inv-item .iname { font-weight: 600; color: var(--text); display: block; margin-bottom: 2px; }
.inv-item .idesc { color: var(--text-muted); font-size: 0.78rem; }

.quest-list { display: flex; flex-direction: column; gap: 10px; margin: 14px 0 22px; }
.quest-item { display: flex; gap: 12px; align-items: flex-start; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 12px 16px; }
.quest-item .qs { font-size: 0.64rem; font-weight: 700; text-transform: uppercase; padding: 3px 8px; border-radius: 6px; flex-shrink: 0; margin-top: 2px; white-space: nowrap; }
.quest-item .qs.active { background: rgba(168, 85, 247, 0.15); color: var(--purple-bright); }
.quest-item .qs.done { background: rgba(45, 232, 160, 0.15); color: var(--green); }
.quest-item .qs.ongoing { background: rgba(255, 94, 203, 0.15); color: var(--pink-bright); }
.quest-item .qt { font-size: 0.9rem; }
.quest-item .qt b { color: var(--text); }
.quest-item .qt span { color: var(--text-muted); font-size: 0.83rem; }

@media (max-width: 900px) {
  .stat-grid { grid-template-columns: repeat(3, 1fr); }
  .skill-grid, .inventory-grid { grid-template-columns: 1fr; }
}
@media (max-width: 620px) {
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
  .charsheet-header { flex-direction: column; text-align: center; }
  .charsheet-meta { justify-content: center; }
}

/* ---------- Generic page header ---------- */
.page-head {
  margin-bottom: 26px;
  padding: clamp(20px, 3vw, 30px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: linear-gradient(135deg, rgba(21, 14, 30, 0.88), rgba(31, 20, 45, 0.68));
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 14px 40px -26px rgba(168, 85, 247, 0.75);
  overflow: hidden;
  isolation: isolate;
}
html[data-theme="light"] .page-head {
  background: linear-gradient(135deg, rgba(255,255,255,0.72), rgba(244,232,250,0.62));
}
.page-head .eyebrow { font-size: 0.75rem; letter-spacing: 2px; text-transform: uppercase; color: var(--pink-bright); font-weight: 600; }
.page-head h1 { font-size: 2.3rem; margin: 6px 0; letter-spacing: -1px; }
.page-head h1 .accent { background: var(--grad-brand); -webkit-background-clip: text; background-clip: text; color: transparent; }
.page-head p { color: var(--text-muted); max-width: 640px; }

/* ---------- Blog / list ---------- */
.post-list { display: flex; flex-direction: column; gap: 14px; }
.post { display: grid; grid-template-columns: 90px 1fr; gap: 18px; align-items: center; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; transition: all 0.2s; }
.post:hover { transform: translateX(4px); border-color: var(--border-strong); }
.post .date { text-align: center; }
.post .date .d { font-family: var(--font-display); font-size: 1.5rem; font-weight: 800; color: var(--purple-bright); }
.post .date .m { font-size: 0.72rem; color: var(--text-dim); text-transform: uppercase; }
.post h3 { font-size: 1.08rem; margin-bottom: 4px; }
.post p { font-size: 0.85rem; color: var(--text-muted); }
.coming { display: inline-block; margin-top: 4px; font-size: 0.72rem; color: var(--amber); }

/* Gallery */
.gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.gallery-grid img { width: 100%; height: 300px; object-fit: cover; object-position: center top; border-radius: 14px; border: 1px solid var(--border); transition: transform 0.25s; }
.gallery-grid img:hover { transform: scale(1.02); box-shadow: var(--glow-purple); }
.gallery-cat { margin: 30px 0 14px; font-size: 0.75rem; letter-spacing: 2px; text-transform: uppercase; color: var(--text-dim); }
.gallery-archive,
.cl-archive {
  margin: 18px 0 28px;
}
.gallery-archive > summary,
.cl-archive > summary {
  width: fit-content;
  cursor: pointer;
  color: var(--cyan);
  font-weight: 700;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
}
.gallery-archive[open] > summary,
.cl-archive[open] > summary { margin-bottom: 20px; }
.gallery-archive .photo-strip { margin-top: 18px; }
.cl-archive { padding-left: 0; }

/* ---------- Cannabis page ---------- */
.strain-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 36px; align-items: stretch; }
.strain-card { display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; transition: transform 0.25s, border-color 0.25s; }
.strain-card:hover { transform: translateY(-3px); border-color: var(--border-strong); }
.strain-photos { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 2px; background: rgba(0,0,0,0.25); align-items: stretch; }
.strain-photos img { width: 100%; height: 190px; object-fit: cover; object-position: center; display: block; }
.strain-photos.no-photo { display: flex; align-items: center; justify-content: center; height: 190px; background: linear-gradient(135deg, rgba(168,85,247,0.1), rgba(255,94,203,0.06)); }
.strain-photos.no-photo .np-ico { font-size: 2.2rem; opacity: 0.5; }
.strain-body { display: flex; flex-direction: column; flex: 1; padding: 18px 20px; }
.strain-head h3 { font-size: 1.15rem; margin-bottom: 3px; }
.strain-brand { font-size: 0.78rem; color: var(--pink-bright); font-weight: 600; margin-bottom: 12px; }
.strain-lineage { color: var(--text-dim); font-weight: 500; font-style: italic; }
.strain-review { font-size: 0.9rem; color: var(--text-muted); line-height: 1.6; margin-bottom: 14px; }
.strain-specs { border-top: 1px solid var(--border); padding-top: 10px; margin-top: auto; }
.strain-spec-row { display: flex; justify-content: space-between; gap: 12px; padding: 4px 0; font-size: 0.82rem; }
.strain-spec-row .ssr-label { color: var(--text-dim); flex-shrink: 0; }
.strain-spec-row .ssr-val { color: var(--text-muted); text-align: right; }
.ocs-link { display: inline-flex; align-items: center; gap: 5px; margin-top: 12px; font-size: 0.8rem; font-weight: 600; color: var(--cyan); }
.ocs-link:hover { text-decoration: underline; }
.ocs-link .ocs-note { font-weight: 500; color: var(--text-dim); }
.microscope-grid img { height: 220px; object-position: center; }

@media (max-width: 900px) {
  .strain-grid { grid-template-columns: 1fr; }
}

/* ---------- Drugs page ---------- */
.principle-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 30px; }
.principle-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; }
.principle-card .pc-ico { font-size: 1.5rem; margin-bottom: 8px; }
.principle-card h3 { font-size: 1.02rem; margin-bottom: 6px; }
.principle-card p { font-size: 0.86rem; color: var(--text-muted); line-height: 1.55; }

.warning-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-bottom: 10px; }
.warning-card { background: rgba(255, 204, 102, 0.06); border: 1px solid rgba(255, 204, 102, 0.28); border-radius: var(--radius); padding: 16px 20px; }
.warning-card h3 { font-size: 0.98rem; color: var(--amber); margin-bottom: 6px; }
.warning-card p { font-size: 0.86rem; color: var(--text-muted); line-height: 1.55; }

.substance-grid { display: grid; grid-template-columns: 1fr; gap: 16px; margin-bottom: 10px; }
.substance-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; }
.substance-card .sub-head { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.substance-card h3 { font-size: 1.15rem; }
.sub-category { font-size: 0.74rem; letter-spacing: 0.5px; text-transform: uppercase; color: var(--pink-bright); font-weight: 600; }
.sub-summary { font-size: 0.9rem; color: var(--text-muted); line-height: 1.6; margin-bottom: 16px; }
.sub-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.sub-col-head { font-size: 0.82rem; font-weight: 700; margin-bottom: 8px; color: var(--text); }
.sub-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.sub-list li { font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; padding-left: 16px; position: relative; }
.sub-list li::before { content: "—"; position: absolute; left: 0; color: var(--text-dim); }
.sub-risks .sub-list li::before { color: #f87171; }
.sub-practice .sub-list li::before { color: var(--green); }

.drug-contact-card { display: flex; gap: 18px; align-items: flex-start; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px; margin-bottom: 30px; }
.drug-contact-card .dc-ico { font-size: 2rem; flex-shrink: 0; }
.drug-contact-card h3 { font-size: 1.1rem; margin-bottom: 6px; }
.drug-contact-card p { font-size: 0.88rem; color: var(--text-muted); line-height: 1.55; margin-bottom: 14px; max-width: 560px; }

@media (max-width: 900px) {
  .principle-grid { grid-template-columns: 1fr; }
  .warning-grid { grid-template-columns: 1fr; }
  .sub-cols { grid-template-columns: 1fr; gap: 12px; }
  .drug-contact-card { flex-direction: column; }
}

/* ---------- Markdown post content ---------- */
.md-content { color: var(--text-muted); font-size: 0.98rem; max-width: 760px; }
.md-content h1, .md-content h2, .md-content h3 { color: var(--text); margin: 28px 0 12px; }
.md-content h1 { font-size: 1.6rem; }
.md-content h2 { font-size: 1.3rem; }
.md-content h3 { font-size: 1.1rem; color: var(--purple-bright); }
.md-content p { margin-bottom: 16px; line-height: 1.75; }
.md-content a { color: var(--purple-bright); text-decoration: underline; text-decoration-color: rgba(192,132,252,0.35); }
.md-content a:hover { color: var(--pink-bright); }
.md-content strong { color: var(--text); }
.md-content ul, .md-content ol { margin: 0 0 16px 22px; }
.md-content li { margin-bottom: 6px; }
.md-content blockquote { border-left: 3px solid var(--purple-bright); padding: 4px 18px; margin: 20px 0; background: var(--panel-2); border-radius: 0 10px 10px 0; color: var(--text); }
.md-content blockquote p { margin-bottom: 0; }
.md-content img { border-radius: var(--radius-sm); border: 1px solid var(--border); margin: 18px 0; }
.md-content code { font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.85em; background: rgba(168, 85, 247,0.12); padding: 2px 6px; border-radius: 5px; color: var(--purple-bright); }
.md-content pre { border-radius: var(--radius-sm); border: 1px solid var(--border); margin: 18px 0; }
.md-content pre code { background: none; padding: 0; color: inherit; }
.md-content hr { border: none; border-top: 1px solid var(--border); margin: 28px 0; }
.md-content table { width: 100%; border-collapse: collapse; margin: 18px 0; font-size: 0.9rem; }
.md-content th, .md-content td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; }
.md-content th { background: var(--panel-2); color: var(--text); }

/* Footer */
.foot { margin-top: 40px; padding-top: 22px; border-top: 1px solid var(--border); font-size: 0.82rem; color: var(--text-dim); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
.foot .heart { color: var(--pink); }

/* Now page */
.now-list { display: flex; flex-direction: column; gap: 12px; }
.now-item { display: flex; gap: 14px; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 16px 18px; align-items: flex-start; }
.now-item .ico { font-size: 1.3rem; }
.now-item .t { font-weight: 600; font-size: 0.92rem; margin-bottom: 3px; }
.now-item .d { font-size: 0.85rem; color: var(--text-muted); }

/* Community */
.discord-cta { border-radius: 22px; padding: 36px 40px; background: linear-gradient(135deg, #241a3d, #1a1030); border: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin-bottom: 26px; }
.discord-cta h2 { font-size: 1.6rem; margin-bottom: 8px; }
.discord-cta p { color: var(--text-muted); max-width: 480px; }
.rule-list { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
.rule { display: flex; gap: 12px; padding: 14px 16px; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.rule .n { font-family: var(--font-display); font-weight: 800; color: var(--purple-bright); }

/* ---------- Lab: scrollable code feed ---------- */
.lab-scroll {
  max-height: 900px; overflow-y: auto; padding-right: 6px;
  scrollbar-width: thin; scrollbar-color: var(--purple-bright) transparent;
}
.lab-scroll::-webkit-scrollbar { width: 8px; }
.lab-scroll::-webkit-scrollbar-thumb { background: var(--purple-bright); border-radius: 8px; }
.code-card .cc-shot { width: 100%; display: block; border-top: 1px solid var(--border); }
.code-card .cc-shot img { width: 100%; max-height: 340px; object-fit: cover; object-position: top; }

/* Featured panel screenshot (lab.html's Command Center card) -- shown
   at full size/uncropped instead of the standard 340px cover-cropped
   thumbnail, since it's dense with small text (buttons, console log)
   that a crop would cut off. */
.code-card.cc-featured { margin-bottom: 28px; }
.code-card .cc-shot.cc-shot-feature { background: rgba(0,0,0,0.2); padding: 14px; }
.code-card .cc-shot.cc-shot-feature img {
  max-height: none;
  object-fit: contain;
  border-radius: var(--radius-sm);
  box-shadow: 0 12px 34px rgba(0,0,0,0.35);
}
@media (max-width: 700px) {
  .code-card .cc-shot.cc-shot-feature { padding: 8px; }
}

/* ---------- Games grid ---------- */
.game-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; }
.game-chip { position: relative; border-radius: 10px; overflow: hidden; border: 1px solid var(--border); aspect-ratio: 2/3; background: var(--panel-2); }
.game-chip img { width: 100%; height: 100%; object-fit: cover; }
.game-chip .gh { position: absolute; bottom: 0; left: 0; right: 0; padding: 8px 6px 6px; font-size: 0.68rem; color: #fff; background: linear-gradient(180deg, transparent, rgba(0,0,0,0.85)); }

/* ---------- Music page ---------- */
.music-hero { border-radius: 22px; padding: 40px; margin-bottom: 26px; background: var(--grad-aurora); border: 1px solid var(--border); position: relative; overflow: hidden; }
.music-hero::before { content:""; position:absolute; inset:0; background: radial-gradient(60% 90% at 85% 10%, rgba(255, 94, 203,0.25), transparent 60%), radial-gradient(50% 80% at 10% 90%, rgba(124, 31, 214,0.3), transparent 60%); }
.music-hero > * { position: relative; }
.top-song-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-bottom: 30px; }
.artist-embed { position: relative; width: 100%; padding-top: 56.25%; border-radius: var(--radius-sm); overflow: hidden; margin-top: 12px; border: 1px solid var(--border); }
.artist-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* ---- Video facade (click-to-load showcase embed) ----
   Stands in for a live iframe until someone actually wants to watch --
   see buildShowcaseCard() in js/music.js. A thumbnail plus a play badge,
   built as a real <button> so it's keyboard-operable and gets a proper
   focus ring, not a div pretending to be one. */
.video-facade {
  position: absolute; inset: 0; width: 100%; height: 100%;
  padding: 0; border: 0; cursor: pointer; background: #000;
  display: block;
}
.video-facade-thumb {
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0.82; transition: opacity 0.2s ease;
}
.video-facade:hover .video-facade-thumb,
.video-facade:focus-visible .video-facade-thumb { opacity: 1; }
.video-facade-play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 56px; height: 56px; border-radius: 50%;
  background: rgba(13, 6, 22, 0.72);
  border: 2px solid rgba(255, 255, 255, 0.85);
  box-shadow: var(--glow-purple);
  transition: transform 0.15s ease, background 0.15s ease;
}
.video-facade-play::after {
  content: ""; position: absolute; top: 50%; left: 56%;
  transform: translate(-50%, -50%);
  border-style: solid; border-width: 10px 0 10px 17px;
  border-color: transparent transparent transparent #fff;
}
.video-facade:hover .video-facade-play,
.video-facade:focus-visible .video-facade-play {
  transform: translate(-50%, -50%) scale(1.1);
  background: var(--pink-bright);
}
.video-facade:focus-visible { outline: 2px solid var(--purple-bright); outline-offset: 2px; }

/* ---- Showcase pager ----
   Prev/Next arrows under the video showcase grid. Re-renders only
   #music-showcase-mount's contents (see renderShowcase() in
   js/music.js) -- no page navigation, no reload. */
.showcase-pager {
  display: flex; align-items: center; justify-content: center; gap: 16px;
  flex-wrap: wrap; padding: 6px 0 2px;
}
.showcase-pager-btn { min-width: 96px; }
.showcase-pager-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.showcase-pager-label { color: var(--text-dim); font-size: 0.85rem; white-space: nowrap; }
@media (max-width: 480px) {
  .showcase-pager { gap: 10px; }
  .showcase-pager-label { width: 100%; order: 3; text-align: center; }
}

/* ---- "More like this" (post-video suggestions, js/music.js) ----
   Inserted right after the card whose video was just played. At the
   top level that is a direct sibling inside #music-showcase-mount's
   .grid-3, so it needs to span every column instead of behaving like
   one more third-width card; nested inside a suggestion item (a
   suggestion's own suggestion, if someone keeps clicking play) the
   parent is a flex column instead, so it just takes that column's
   width with no override needed. */
.grid-3 > .music-suggests { grid-column: 1 / -1; }
.music-suggests {
  margin: 2px 0 10px; padding: 16px 16px 14px;
  border: 1px dashed var(--border-strong); border-radius: var(--radius);
  background: rgba(168, 85, 247, 0.05);
}
.music-suggests-head {
  font-size: 0.78rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--purple-bright); margin-bottom: 12px;
}
.music-suggests-item { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.ms-badge { font-size: 0.74rem; color: var(--text-dim); }
.ms-badge.sourced { color: var(--green); font-weight: 600; }
@media (max-width: 640px) {
  .music-suggests-grid { grid-template-columns: 1fr; }
}
/* Enlarged featured-artist cards */
.artist-featured .project-card { padding: 28px; }
.artist-featured .pc-head { gap: 14px; margin-bottom: 6px; }
.artist-featured .pc-icon { width: 54px; height: 54px; border-radius: 14px; font-size: 1.7rem; }
.artist-featured .pc-head h2, .artist-featured .pc-head h3 { font-size: 1.55rem; }
.artist-featured p { font-size: 1rem; }
.artist-featured .artist-embed { margin-top: 16px; }
.artist-featured .pc-link { font-size: 0.95rem; }
.top-song-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; transition: all 0.2s; }
.top-song-card:hover { transform: translateY(-4px); border-color: var(--border-strong); }
.top-song-card .ts-body { padding: 16px 18px; }
.top-song-card .ts-rank { font-family: var(--font-display); font-size: 1.6rem; font-weight: 800; color: var(--purple-bright); }
.top-song-card h3, .top-song-card h4 { font-size: 1rem; margin: 4px 0 2px; }
.top-song-card .ts-artist { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 10px; }

/* ---------- Whitelist / status panel ---------- */
.whitelist-box { display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.whitelist-box .wl-stat { text-align: center; }
.whitelist-box .wl-stat .n { font-family: var(--font-display); font-size: 1.8rem; font-weight: 800; color: var(--purple-bright); }
.whitelist-box .wl-stat .l { font-size: 0.72rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
.uuid-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-dim); }

.wl-form { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
.wl-form input { flex: 1; min-width: 180px; background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 11px 14px; color: var(--text); font-family: var(--font); font-size: 0.88rem; }
.wl-form input:focus { outline: none; border-color: var(--border-strong); box-shadow: var(--glow-purple); }
.wl-form input::placeholder { color: var(--text-dim); }
.wl-form button { border: none; border-radius: var(--radius-sm); padding: 11px 20px; background: var(--grad-brand); color: #fff; font-weight: 600; font-size: 0.88rem; cursor: pointer; transition: all 0.18s; white-space: nowrap; }
.wl-form button:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(168,85,247,0.4); }
.wl-form button:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
.wl-msg { font-size: 0.82rem; margin-top: 10px; min-height: 1.2em; }
.wl-msg.ok { color: var(--green); }
.wl-msg.err { color: var(--pink-bright); }

/* ---------- Work/Collaborate form ---------- */
.work-type-toggle { display: flex; gap: 10px; margin: 24px 0 20px; flex-wrap: wrap; }
.work-type-btn { flex: 1; min-width: 180px; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 18px; color: var(--text-muted); font-family: var(--font); font-size: 0.92rem; font-weight: 600; cursor: pointer; transition: all 0.18s; }
.work-type-btn:hover { border-color: var(--border-strong); color: var(--text); }
.work-type-btn.is-active { background: var(--grad-brand); color: #fff; border-color: transparent; box-shadow: 0 6px 24px rgba(168,85,247,0.35); }

.work-form { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 26px; max-width: 640px; display: flex; flex-direction: column; gap: 18px; }
.work-field { display: flex; flex-direction: column; gap: 7px; }
.work-field label { font-size: 0.82rem; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
.work-field input, .work-field select, .work-field textarea { background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px 14px; color: var(--text); font-family: var(--font); font-size: 0.92rem; resize: vertical; }
.work-field input:focus, .work-field select:focus, .work-field textarea:focus { outline: none; border-color: var(--border-strong); box-shadow: var(--glow-purple); }
.work-field input::placeholder, .work-field textarea::placeholder { color: var(--text-dim); }
.work-msg { font-size: 0.85rem; min-height: 1.3em; }
.work-msg.ok { color: var(--green); }
.work-msg.err { color: var(--pink-bright); }

/* ---------- Passion pills / interest grid ---------- */
.interest-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin: 20px 0; }
.interest-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 16px; text-align: center; }
.interest-card .ico { font-size: 1.7rem; margin-bottom: 6px; }
.interest-card .t { font-size: 0.85rem; font-weight: 600; }
.interest-card .s { font-size: 0.72rem; color: var(--text-dim); margin-top: 2px; }

/* ---------- Music page — data-driven sections (Phase 2) ---------- */
.music-stats { grid-template-columns: repeat(4, 1fr); }
.music-methodology-note {
  font-size: 0.78rem; color: var(--text-dim); line-height: 1.6;
  background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 12px 16px; margin: 10px 0 26px;
}

.artist-bars { display: flex; flex-direction: column; gap: 10px; margin-bottom: 8px; }
.artist-bar-row { display: grid; grid-template-columns: 34px minmax(120px, 200px) 1fr 90px; align-items: center; gap: 12px; }
.ab-rank { font-family: var(--font-display); font-weight: 800; color: var(--text-dim); font-size: 0.85rem; }
.ab-name { font-size: 0.88rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ab-track { height: 12px; background: var(--panel-2); border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
.ab-fill { height: 100%; border-radius: 6px; transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1); }
.ab-count { font-size: 0.78rem; color: var(--text-dim); text-align: right; white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .ab-fill, .decade-bar-fill { transition: none !important; } }

.genre-chart-wrap { display: flex; align-items: center; gap: 40px; flex-wrap: wrap; margin-bottom: 8px; }
.genre-donut { width: 220px; height: 220px; flex-shrink: 0; }
.genre-donut circle { transition: stroke-dasharray 0.6s ease; }
.donut-center-num { font-family: var(--font-display); font-weight: 800; font-size: 1.7rem; fill: var(--text); }
.donut-center-lbl { font-size: 0.72rem; fill: var(--text-dim); }
.genre-legend { flex: 1; min-width: 240px; display: flex; flex-direction: column; gap: 10px; }
.genre-legend-row { display: grid; grid-template-columns: 12px 1fr auto auto; align-items: center; gap: 10px; font-size: 0.88rem; }
.genre-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
.genre-legend-label { font-weight: 600; }
.genre-legend-pct { color: var(--purple-bright); font-weight: 700; font-family: var(--font-display); }
.genre-legend-count { color: var(--text-dim); font-size: 0.78rem; }

/* Genre section, extra graphs (donut stays as-is; these two use the
   width the donut alone was leaving empty on wider screens): a ranked
   bar chart of the same genreBreakdown counts side-by-side with the
   donut, and a separate composition bar for genres among the top 20
   most-played artists specifically (a distinct, real slice of the
   data — not just the donut redrawn). */
.genre-section-grid { display: grid; grid-template-columns: minmax(300px, 460px) minmax(280px, 1fr); gap: 36px; align-items: start; margin-bottom: 22px; }
/* Those two px floors add up to 580px plus the gap, which a phone cannot
   satisfy, so the grid was forcing the whole document 242px wide at 390px
   and giving the music page a horizontal scrollbar. Two columns are only
   meaningful once both can actually be read side by side; below that the
   donut and the ranked bars stack. */
@media (max-width: 780px) {
  .genre-section-grid { grid-template-columns: 1fr; gap: 24px; }
}
.genre-subhead { font-size: 0.92rem; font-weight: 700; margin-bottom: 12px; }
.genre-rank-wrap .artist-bars { gap: 8px; }

.genre-top20-block { margin-bottom: 20px; }
.genre-stack-bar { display: flex; height: 34px; border-radius: 8px; overflow: hidden; border: 1px solid var(--border); background: var(--panel-2); }
.genre-stack-seg { height: 100%; transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1); }
.genre-stack-legend { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 12px; }
.genre-stack-chip { display: inline-flex; align-items: center; gap: 7px; font-size: 0.82rem; color: var(--text-muted); }
@media (prefers-reduced-motion: reduce) { .genre-stack-seg { transition: none !important; } }

.decade-bars { display: flex; align-items: flex-end; gap: 16px; height: 200px; margin-bottom: 8px; padding: 0 8px; }
.decade-bar-col { flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; }
.decade-bar-track { flex: 1; width: 100%; max-width: 56px; display: flex; align-items: flex-end; background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px 8px 4px 4px; overflow: hidden; }
.decade-bar-fill { width: 100%; background: linear-gradient(180deg, var(--purple-bright), var(--pink-bright)); border-radius: 6px 6px 0 0; transition: height 0.7s cubic-bezier(0.16, 1, 0.3, 1); }
.decade-bar-count { font-size: 0.75rem; color: var(--text-dim); margin-bottom: 4px; }
.decade-bar-label { font-size: 0.8rem; font-weight: 600; margin-top: 8px; }

.mood-narrative-box { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px; margin-bottom: 8px; }
.mood-narrative-box p { color: var(--text); line-height: 1.7; }
.mood-narrative-box p strong { color: var(--purple-bright); }
.mood-disclaimer {
  display: flex; gap: 10px; align-items: flex-start; font-size: 0.8rem; color: var(--text-dim);
  background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 12px 14px; margin-bottom: 16px; line-height: 1.5;
}
.mood-disclaimer-ico { flex-shrink: 0; }

.music-showcase-card .pc-tag { color: var(--text-dim); font-weight: 500; }
.lyrics-excerpt-box { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 14px; }
.lyrics-excerpt-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; flex-wrap: wrap; }
.lyrics-excerpt-head > span:first-child { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); }
.lyrics-genius-link { font-size: 0.78rem; color: var(--purple-bright); font-weight: 600; }
.lyrics-genius-link:hover { text-decoration: underline; }
.lyrics-excerpt-scroll {
  position: relative; max-height: 130px; overflow-y: auto; background: rgba(0,0,0,0.22);
  border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px 14px;
}
.lyrics-excerpt-scroll::after {
  content: ""; position: sticky; display: block; bottom: 0; left: 0; right: 0; height: 28px; margin-top: -28px;
  background: linear-gradient(180deg, transparent, rgba(0,0,0,0.5)); pointer-events: none;
}
/* The unavailable state used to hide the box entirely. It now carries a
   short explanation instead, so it stays visible but drops the fade-out
   overlay and the fixed height that only make sense for real lyrics. */
.lyrics-excerpt-box.unavailable .lyrics-excerpt-scroll { max-height: none; }
.lyrics-excerpt-box.unavailable .lyrics-excerpt-scroll::after { display: none; }
.lyrics-excerpt-box.unavailable .lyrics-excerpt-text { color: var(--text-dim); font-style: italic; font-size: 0.78rem; }
.lyrics-excerpt-text { font-size: 0.82rem; line-height: 1.6; color: var(--text-muted); white-space: pre-wrap; font-family: var(--font); }

/* ---------- Infrastructure topology diagram (Phase 2 centerpiece) ---------- */
.topology-wrap {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 20px; margin-bottom: 14px;
}
/* The SVG's viewBox is a square (0 0 100 100), so width:100% + height:auto
   means its rendered height always equals its rendered width. Left
   unbounded, that scales the diagram up to match .page's own width — up
   to a 2200px square on an ultrawide monitor, which is what made this
   comically oversized instead of just "wide". Capping max-width here
   keeps it a sensible, fixed size regardless of monitor width; it still
   shrinks normally below that cap via the existing responsive rules. */
.topology-wrap { max-width: 720px; }
/* The home preview's column is as wide as the 3-card project row above it
   (900px+), so a small fixed cap here just left a big dead gap to the
   right of the diagram — 460px was sized for "smaller than the full
   page's diagram" without accounting for how wide the column actually is.
   620px fills that row much better; centering turns whatever's left into
   even, intentional-looking framing instead of a lopsided gap. */
body[data-page="home"] .topology-wrap { max-width: 620px; margin-left: auto; margin-right: auto; }
.topology-svg { width: 100%; height: auto; min-height: 420px; display: block; }
body[data-page="home"] .topology-svg { min-height: 260px; }
body[data-page="home"] .topology-legend { font-size: 0.74rem; gap: 12px; justify-content: center; }

.topology-edge-line { fill: none; stroke: var(--border-strong); stroke-width: 0.35; transition: stroke-width 0.2s, opacity 0.2s; }
.topology-edge--api .topology-edge-line { stroke-dasharray: 1.4 1; stroke: var(--cyan); opacity: 0.6; }
.topology-edge--config .topology-edge-line { stroke-dasharray: 0.6 1.2; stroke: var(--amber); opacity: 0.5; }
.topology-edge.is-highlighted .topology-edge-line { stroke: var(--purple-bright); stroke-width: 0.7; opacity: 1; }
.topology-edge.is-dimmed .topology-edge-line { opacity: 0.12; }

.topology-packet { fill: var(--purple-bright); filter: drop-shadow(0 0 2px var(--purple-bright)); }
.topology-edge--api .topology-packet { fill: var(--cyan); }
@media (prefers-reduced-motion: reduce) {
  .topology-packet, .topology-packet animateMotion { display: none; }
}

.topology-node { cursor: pointer; outline: none; }
.topology-node-circle {
  fill: var(--panel-solid); stroke-width: 0.5; transition: all 0.2s;
}
.topology-node--dashed .topology-node-circle { stroke-dasharray: 1.2 0.8; }
.topology-node-icon { font-size: 4.2px; }
.topology-node-label { font-size: 2.6px; font-weight: 600; fill: var(--text); font-family: var(--font); }
/* Compact variant: the five game-server nodes (2026-07-28), whose labels
   ("Minecraft Server", "Enshrouded Server") were long enough to clip past
   the viewBox's right edge even after repositioning. Smaller circle,
   icon and label together, not just a smaller circle with the same
   label overrunning it. */
.topology-node--compact .topology-node-icon { font-size: 3.4px; }
.topology-node--compact .topology-node-label { font-size: 2.1px; }
.topology-node-badge {
  font-size: 2px; fill: var(--amber); font-family: var(--font); font-weight: 700;
  letter-spacing: 0.3px; text-transform: uppercase;
}
.topology-node.is-active .topology-node-circle { stroke-width: 0.9; filter: drop-shadow(0 0 3px currentColor); }
.topology-node.is-dimmed { opacity: 0.25; }
.topology-node:focus-visible .topology-node-circle { stroke-width: 0.9; }

.topology-status-dot--unknown { fill: var(--text-dim); }
.topology-status-dot--online { fill: var(--green); filter: drop-shadow(0 0 2px var(--green)); }
.topology-status-dot--offline { fill: var(--pink-bright); }

.topology-legend { display: flex; gap: 20px; flex-wrap: wrap; padding: 4px 4px 0; }
.topology-legend-item { display: inline-flex; align-items: center; gap: 7px; font-size: 0.8rem; color: var(--text-muted); }
.topology-legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }

/* ---------- Responsive ---------- */
@media (max-width: 1150px) {
  .topology-svg { min-height: 360px; }
  .music-stats { grid-template-columns: repeat(2, 1fr); }
  .artist-bar-row { grid-template-columns: 28px 1fr; row-gap: 4px; }
  .artist-bar-row .ab-track { grid-column: 1 / -1; order: 3; }
  .artist-bar-row .ab-count { grid-column: 1 / -1; order: 4; text-align: left; }
  .genre-chart-wrap { justify-content: center; }
  .home-grid { grid-template-columns: 1fr; }
  .home-side-grid { grid-template-columns: 1fr; }
  .home-side-grid .grid-3 { grid-template-columns: repeat(3, 1fr); }
  .rail { position: static; flex-direction: row; flex-wrap: wrap; }
  .rail-card { flex: 1; min-width: 260px; }
  .stats { grid-template-columns: repeat(3, 1fr); }
  .stat:nth-child(3) { border-right: none; }
}
@media (max-width: 900px) {
  .app { grid-template-columns: 1fr; }
  body.menu-open { overflow: hidden; }
  .sidebar {
    position: fixed; left: 0; top: 0; width: min(88vw, 340px); z-index: 60;
    transform: translateX(-100%); transition: transform 0.28s ease;
    padding-bottom: max(24px, env(safe-area-inset-bottom));
  }
  .sidebar.open { transform: translateX(0); box-shadow: 0 0 60px rgba(0,0,0,0.6); }
  .menu-toggle { display: grid; }
  .sidebar-close {
    display: grid; place-items: center; position: absolute; top: 12px; right: 12px; z-index: 3;
    width: 44px; height: 44px; border-radius: 11px; border: 1px solid var(--border);
    color: var(--text); background: var(--panel-solid); font: 400 1.65rem/1 var(--font);
    cursor: pointer;
  }
  .topbar { padding: 0 12px; gap: 8px; }
  .top-nav { display: none; }
  .mobile-brand { display: block; flex: 1; overflow: hidden; text-overflow: ellipsis; }
  .top-actions { gap: 4px; }
  #focusToggleTop { display: none; }
  .nav-help-sign { min-height: 44px; padding-inline: 10px; }
  .nhs-arrow { display: none; }
  .scrim { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 55; opacity: 0; pointer-events: none; transition: opacity 0.25s; }
  .scrim.show { opacity: 1; pointer-events: auto; }
  .mobile-site-nav {
    display: grid; gap: 5px; margin: 4px 0 18px; padding: 14px 0 4px;
    border-top: 1px solid var(--border);
  }
  .mobile-nav-title {
    padding: 0 10px 7px; color: var(--text-dim); font-size: 0.68rem;
    font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase;
  }
  .mobile-nav-link,
  .mobile-nav-summary {
    min-height: 44px; display: flex; align-items: center; gap: 10px;
    padding: 10px 12px; border-radius: 10px; color: var(--text-muted);
    font: 600 0.9rem/1.35 var(--font); cursor: pointer;
  }
  .mobile-nav-link:hover,
  .mobile-nav-summary:hover { color: var(--text); background: var(--panel-2); }
  .mobile-nav-link.active,
  .mobile-nav-group.active > .mobile-nav-summary {
    color: #fff; background: linear-gradient(90deg, rgba(168,85,247,0.24), rgba(255,94,203,0.1));
    box-shadow: inset 3px 0 0 var(--purple-bright);
  }
  .mobile-nav-help { color: #ffe6b0; border: 1px solid rgba(255,176,92,0.35); }
  .mobile-nav-group { border-radius: 10px; }
  .mobile-nav-group > summary { list-style: none; justify-content: space-between; }
  .mobile-nav-group > summary::-webkit-details-marker { display: none; }
  .mobile-nav-caret { transition: transform 0.18s ease; }
  .mobile-nav-group[open] .mobile-nav-caret { transform: rotate(180deg); }
  .mobile-nav-children {
    display: grid; gap: 2px; margin: 2px 0 7px 16px; padding-left: 11px;
    border-left: 1px solid var(--border);
  }
  .mobile-nav-children a {
    min-height: 42px; display: flex; align-items: center; padding: 8px 11px;
    border-radius: 8px; color: var(--text-muted); font-size: 0.82rem; line-height: 1.35;
  }
  .mobile-nav-children a:hover { color: var(--text); background: var(--panel-2); }
  .mobile-nav-children a.active { color: var(--purple-bright); background: rgba(168,85,247,0.12); }
  .mobile-nav-head {
    padding: 11px 11px 4px; color: var(--text-dim); font-size: 0.64rem;
    font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase;
  }
  .grid-3, .value-grid, .status-grid, .gallery-grid { grid-template-columns: 1fr 1fr; }
  .about-hero { grid-template-columns: 1fr; }
  .photo-strip { grid-template-columns: repeat(2,1fr); }
  .game-grid { grid-template-columns: repeat(4, 1fr); }
  .top-song-grid, .interest-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (prefers-reduced-motion: reduce) and (max-width: 900px) {
  .sidebar, .scrim, .mobile-nav-caret { transition: none; }
}
@media (max-width: 620px) {
  .page { padding: 20px 16px 50px; }
  .hero { padding: 34px 24px; }
  .stats { grid-template-columns: repeat(2,1fr); }
  .grid-3, .grid-2, .value-grid, .status-grid, .mod-list, .contact-grid, .gallery-grid, .resource-grid { grid-template-columns: 1fr; }
  /* top-nav now stays visible (horizontally scrollable) even on small
     phones — it's the only place the full page list lives now that the
     sidebar only keeps a few curated quick links, so hiding it here
     would leave narrow-viewport visitors with no way to reach most
     pages. */
  .infra-flow { flex-direction: column; }
  .arrow { transform: rotate(90deg); }
  .about-hero .photo img { height: 260px; }
  .game-grid { grid-template-columns: repeat(3, 1fr); }
  .top-song-grid, .interest-grid { grid-template-columns: 1fr; }
  .music-stats { grid-template-columns: 1fr; }
  .decade-bars { height: 150px; gap: 8px; }
  .decade-bar-label { font-size: 0.7rem; }
  .decade-bar-count { font-size: 0.68rem; }
  .genre-donut { width: 170px; height: 170px; }
  .topology-svg { min-height: 460px; } /* diagram is naturally wide/landscape; give it more height so nodes don't crowd on narrow phones */
  .topology-node-label { font-size: 3.4px; }
  .topology-node-icon { font-size: 5px; }
  .topology-legend { gap: 12px; font-size: 0.74rem; }
}

/* ============================================================
   Sensitive / adult-topic pages
   Pages with <body data-sensitive="true"> get an automatic red
   "warning" treatment; pages that also set data-consent="true"
   show the one-time consent gate (styled below). Reds are fixed
   here on purpose so the site-hue picker doesn't wash them out —
   a warning that changes color with the theme isn't much of a
   warning.
   ============================================================ */
body.sensitive-page::before {
  content: "";
  position: fixed; top: 0; left: 0; right: 0; height: 3px;
  background: linear-gradient(90deg, #f87171, #ef4444, #f87171);
  z-index: 400; pointer-events: none;
}
body.sensitive-page .page > .page-head::before {
  content: "⚠  Sensitive / adult topics ahead — please read with care and kindness.";
  display: block;
  background: linear-gradient(90deg, rgba(248,113,113,0.16), rgba(248,113,113,0.05));
  border: 1px solid rgba(248,113,113,0.4);
  border-left: 3px solid #f87171;
  color: #fca5a5;
  font-size: 0.82rem; font-weight: 600; letter-spacing: 0.3px;
  padding: 10px 16px; border-radius: 12px; margin-bottom: 18px;
}
body.sensitive-page .page-head .eyebrow { color: #f87171; }
body.sensitive-page .page-head h1 .accent { color: #f87171; }

/* Consent gate */
.lh-consent-overlay {
  position: fixed; inset: 0; z-index: 600;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(120% 120% at 50% 0%, #2a1414 0%, #120708 72%);
  padding: 24px;
}
.lh-consent-card {
  max-width: 470px; width: 100%;
  background: var(--panel-solid);
  border: 1px solid rgba(248,113,113,0.5);
  border-radius: var(--radius);
  padding: 34px 30px; text-align: center;
  box-shadow: 0 0 0 1px rgba(248,113,113,0.18), 0 24px 60px rgba(0,0,0,0.6);
}
.lh-consent-ico { font-size: 2.2rem; margin-bottom: 10px; }
.lh-consent-card h2 { font-size: 1.35rem; margin: 0 0 12px; color: #fca5a5; }
.lh-consent-card p { color: var(--text-muted); font-size: 0.92rem; margin: 0 0 12px; line-height: 1.55; }
.lh-consent-card p em { color: var(--text); font-style: italic; }
.lh-consent-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 20px; }
.btn-danger { background: #c81e1e; border: 1px solid #c81e1e; color: #fff; } /* was #ef4444; white on it was 3.76:1 */
.btn-danger:hover { background: #dc2626; border-color: #dc2626; transform: translateY(-2px); }

/* ============================================================
   Site index features (site-index.js) — related cards, search
   palette, "Your journey" sidebar card, sidebar shuffle link.
   Added 2026-07-24.
   ============================================================ */

/* --- sidebar: playlist shuffle link ------------------------- */
.side-lofi-shuffle {
  display: block; margin-top: 8px; font-size: 0.74rem; font-weight: 600;
  color: var(--text-dim); text-align: center; transition: color 0.2s;
}
.side-lofi-shuffle:hover { color: var(--purple-bright); }

/* --- related-page cards ("Keep exploring") ------------------ */
/* Constrained to the same measure as .prose. It sits inside .page, which
   grows to 2200px, so at two fixed columns each card was stretching to
   roughly a thousand pixels wide to hold one line of blurb. Capping the
   block and letting the grid add columns instead keeps the cards a
   card-like shape on any monitor. */
.related-block { margin-top: 40px; max-width: clamp(720px, 62vw, 1140px); }
.related-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin-bottom: 14px; padding-top: 22px; border-top: 1px solid var(--border);
}
.related-title {
  font-family: var(--font-display); font-weight: 700; font-size: 1.05rem;
}
.related-sub { font-size: 0.78rem; color: var(--text-dim); }
.related-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: 12px;
}
.related-card {
  display: flex; align-items: flex-start; gap: 12px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 14px 15px;
  transition: transform 0.2s ease, border-color 0.2s, box-shadow 0.2s;
}
.related-card:hover {
  transform: translateY(-2px); border-color: var(--border-strong);
  box-shadow: 0 8px 28px rgba(168, 85, 247, 0.16);
}
.related-card .rc-ico { font-size: 1.25rem; line-height: 1.3; }
.related-card .rc-body { flex: 1; min-width: 0; }
.related-card .rc-title { display: block; font-weight: 600; color: var(--text); font-size: 0.92rem; }
.related-card .rc-blurb {
  display: block; font-size: 0.8rem; color: var(--text-muted); margin-top: 3px; line-height: 1.45;
}
.related-card .rc-arrow {
  color: var(--text-dim); font-weight: 600; transition: color 0.2s, transform 0.2s; align-self: center;
}
.related-card:hover .rc-arrow { color: var(--purple-bright); transform: translateX(3px); }
.rc-badge {
  display: inline-block; font-size: 0.62rem; font-weight: 700; letter-spacing: 0.5px;
  color: #ff6b81; border: 1px solid rgba(255, 107, 129, 0.5); border-radius: 5px;
  padding: 1px 5px; margin-left: 6px; vertical-align: 1px;
}
@media (max-width: 760px) {
  .related-grid { grid-template-columns: 1fr; }
}

/* Sensitive (red-themed) pages: related cards pick up the warning hue
   border so the section doesn't clash with the page theme. */
.sensitive-page .related-card:hover {
  border-color: rgba(255, 107, 129, 0.5); box-shadow: 0 8px 28px rgba(255, 80, 100, 0.12);
}

/* --- search palette ----------------------------------------- */
.lh-search-overlay {
  position: fixed; inset: 0; z-index: 300; display: none;
  background: rgba(5, 3, 10, 0.72); backdrop-filter: blur(6px);
  padding: 9vh 16px 16px; overflow-y: auto;
}
.lh-search-overlay.show { display: block; animation: lh-search-in 0.16s ease; }
@keyframes lh-search-in { from { opacity: 0; } to { opacity: 1; } }
.lh-search-box {
  max-width: 560px; margin: 0 auto;
  background: var(--panel-solid); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--glow-purple), 0 24px 70px rgba(0,0,0,0.6);
  overflow: hidden;
}
.lh-search-inputwrap {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 16px; border-bottom: 1px solid var(--border);
}
.lh-search-inputwrap .lh-search-ico { font-size: 1rem; }
.lh-search-inputwrap input {
  flex: 1; background: none; border: 0; outline: 0;
  color: var(--text); font-family: var(--font); font-size: 0.98rem;
}
/* The search dialog autofocuses this input, so "outline: 0" with no
   replacement meant the one focused control in the dialog was invisible.
   The ring goes on the wrapper rather than the bare input so it traces the
   whole search bar including the icon and the kbd hint. */
.lh-search-inputwrap:focus-within {
  border-color: var(--purple-bright);
  box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.25);
}
.lh-search-inputwrap input::placeholder { color: var(--text-dim); }
.lh-search-inputwrap kbd {
  font-size: 0.66rem; color: var(--text-dim); border: 1px solid var(--border);
  border-radius: 5px; padding: 2px 6px; text-transform: uppercase; letter-spacing: 0.5px;
}
.lh-search-results { max-height: 54vh; overflow-y: auto; padding: 6px; }
.lh-search-results a {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 10px 12px; border-radius: var(--radius-sm);
  border: 1px solid transparent;
}
.lh-search-results a:hover,
.lh-search-results a.active {
  background: var(--panel-2); border-color: var(--border);
}
.lh-search-results a.active { border-color: var(--border-strong); }
.lh-search-results .rc-ico { font-size: 1.1rem; line-height: 1.4; }
.lh-search-results .rc-title { display: block; font-weight: 600; font-size: 0.9rem; color: var(--text); }
.lh-search-results .rc-blurb { display: block; font-size: 0.78rem; color: var(--text-muted); margin-top: 2px; }
.lh-search-empty { padding: 22px 16px; text-align: center; color: var(--text-dim); font-size: 0.88rem; }

/* --- "Your journey" sidebar card ---------------------------- */
#lh-journey:empty { display: none; }
#lh-journey .journey-row {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 0.78rem; color: var(--text-muted); margin: 7px 0 4px;
}
#lh-journey .journey-row .val { font-weight: 700; color: var(--text); font-size: 0.78rem; }
#lh-journey .journey-row .val a { color: var(--purple-bright); font-weight: 600; }
#lh-journey .journey-track { height: 6px; margin: 2px 0 4px; }
#lh-journey .journey-fill {
  background: var(--grad-brand); box-shadow: 0 0 10px rgba(168, 85, 247, 0.5);
}
#lh-journey .journey-done {
  margin-top: 9px; font-size: 0.74rem; color: var(--amber); line-height: 1.4;
}

/* ============================================================
   Genre Galaxy (js/music-galaxy.js + js/music-graph.js)
   150 artists, positioned by researched connections.
   ============================================================ */
.galaxy-wrap { margin-bottom: 30px; }
.galaxy-stage {
  position: relative; background: radial-gradient(ellipse at 45% 45%, #100a1e 0%, var(--bg) 72%);
  border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;
}
.galaxy-svg { display: block; width: 100%; height: auto; }

/* --- connection lines ---
   2026-07-28 pass 2: Harper's ask was specific -- glowy, chaotic,
   black-hole-esque, "less straight LED line, more galaxy opening up and
   distorting with technical glitches/colour distortion and pure
   corruption around the elements." Pass 1 (the fracture-path jitter)
   only covered "not straight." This covers the rest:
     - galaxy-link-glow + galaxy-corrupt (defined in the SVG <defs> in
       js/music-galaxy.js) give every line a soft glow and a fixed-seed
       noise displacement, so it reads as corrupted signal rather than
       clean vector art.
     - .galaxy-core (below) is the black-hole core the gravity bend in
       fracturePath() actually warps lines around.
     - .galaxy-link-chroma is the chromatic-aberration glitch accent on
       ~1 in 7 lines (js/music-galaxy.js picks which, deterministically).
   Nothing here changes which artists are connected or what a line
   click-throughs to -- it's all rendering on top of the same real data. */
.galaxy-link {
  /* 2026-07-29: the "chaotic black hole" look from the 07-28 passes was
     tuned as a HOVER payoff but was shipping as the resting state of the
     whole page -- 215 edges all glowing/corrupted/jittering at once reads
     as noise, not a readable star map, and Harper flagged it as "this
     looks fucking terrible" / "the concept for the background of the
     website, not the background of this genre page." Fix: calm at rest
     (thin, low-opacity, no distortion filter), full drama reserved for
     .is-on (see below) -- the fracture-path *shape* (jagged, not straight)
     stays either way, since that part reads fine even at low opacity. */
  fill: none; stroke-width: 1; opacity: 0.12; stroke-linecap: round;
  transition: opacity 0.2s, stroke-width 0.2s, filter 0.2s;
}
.galaxy-link.is-on {
  opacity: 0.95; stroke-width: 2.2;
  filter: url(#galaxy-corrupt) url(#galaxy-link-glow);
}
.galaxy-svg[data-focus] .galaxy-link { opacity: 0.05; }
.galaxy-svg[data-focus] .galaxy-link.is-on { opacity: 0.95; stroke-width: 2.2; }
.galaxy-link.is-glitch.is-on { animation: galaxy-line-glitch 3.2s steps(1) infinite; }

/* Lightning forks (js/music-galaxy.js buildForkPaths()): short jagged
   branches off the main fracture, purely decorative. Used to render
   ambient at 0.24 opacity across all ~215 edges' 2-4 forks each --
   the single biggest contributor to the "wall of noise" look, since it
   put 400-800 glowing jagged elements on screen before any interaction.
   Now near-invisible at rest; the shape is still there for texture, it
   just doesn't compete with the actual star map. */
.galaxy-link-fork {
  fill: none; stroke-width: 1; opacity: 0.05; stroke-linecap: round;
  pointer-events: none; transition: opacity 0.2s;
}
.galaxy-svg[data-focus] .galaxy-link-fork { opacity: 0.03; }

/* White-hot core: same "d" as the real line directly above it in the
   DOM, invisible until that sibling gets .is-on, then flickers up to a
   bright near-white bolt over its coloured halo -- the reference-image
   look, reserved for whichever one connection is actually highlighted. */
.galaxy-link-core {
  fill: none; stroke: #f4f0ff; stroke-width: 1; stroke-linecap: round;
  opacity: 0; pointer-events: none; filter: url(#galaxy-link-glow);
  transition: opacity 0.12s;
}
.galaxy-link.is-on + .galaxy-link-core {
  opacity: 0.92; stroke-width: 2;
  animation: galaxy-lightning-flicker 0.5s steps(2) infinite;
}
@keyframes galaxy-lightning-flicker {
  0%, 100% { opacity: 0.92; }
  50% { opacity: 0.5; }
}
@media (prefers-reduced-motion: reduce) { .galaxy-link-core { animation: none; } }
@keyframes galaxy-line-glitch {
  0%, 74%, 100% { stroke-dasharray: none; }
  76% { stroke-dasharray: 14 3 2 6; }
  79% { stroke-dasharray: none; }
  88% { stroke-dasharray: 3 8; }
  90% { stroke-dasharray: none; }
}

/* Chromatic-aberration ghosts: two colour-split copies of the same
   fractured path, offset a couple of pixels, sitting under the real
   line. Screen blend keeps them additive light rather than muddy
   overlap.
   2026-07-29: this used to flicker constantly on ~14.5% of all 215
   edges via an always-running animation, and was only ever turned OFF
   while actually focused/hovering a star -- exactly backwards, and a
   real contributor to the "wall of noise" complaint, since it kept
   flashing across the whole idle page. Now off at rest by default; kept
   purely as a static, very faint accent under the highlighted edge so
   the glitch look survives as a hover payoff instead of ambient clutter. */
.galaxy-link-chroma {
  fill: none; stroke-width: 1.1; stroke-linecap: round; opacity: 0;
  mix-blend-mode: screen; pointer-events: none;
  transition: opacity 0.2s;
}
.galaxy-link-chroma.chroma-r { transform: translate(-1.6px, 0.7px); stroke: #ff3d6e; }
.galaxy-link-chroma.chroma-b { transform: translate(1.6px, -0.7px); stroke: #22e5ff; }
.galaxy-svg[data-isolate] .galaxy-link-chroma { opacity: 0 !important; }

/* --- galaxy core: the black hole the gravity bend warps lines around --- */
.galaxy-core { pointer-events: none; animation: galaxy-core-pulse 7s ease-in-out infinite; }
@keyframes galaxy-core-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.7; } }

/* Faint scanline texture across the whole stage -- the "technical" half
   of "technical glitches," on top of the galaxy rather than baked into
   any one element, so it reads as a monitor rendering the galaxy rather
   than the galaxy itself glitching. */
.galaxy-stage::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: repeating-linear-gradient(0deg,
    rgba(255,255,255,0.035) 0px, rgba(255,255,255,0.035) 1px,
    transparent 1px, transparent 3px);
  mix-blend-mode: overlay; opacity: 0.5;
}
@media (prefers-reduced-motion: reduce) {
  .galaxy-link.is-glitch { animation: none; }
  .galaxy-link-chroma { animation: none; opacity: 0 !important; }
  .galaxy-core { animation: none; }
}

/* --- stars --- */
.galaxy-star { cursor: pointer; outline: none; }
.gs-halo { opacity: 0.12; transition: opacity 0.22s; }
.gs-core {
  transition: opacity 0.22s;
  animation: galaxy-twinkle 5s ease-in-out infinite;
}
@keyframes galaxy-twinkle { 0%,100% { opacity: 0.8; } 50% { opacity: 1; } }
.galaxy-star:hover .gs-halo, .galaxy-star:focus-visible .gs-halo { opacity: 0.34; }
.galaxy-star:focus-visible .gs-core { stroke: #fff; stroke-width: 1.5; }

/* focus mode: dim everything except the hovered star and its links */
.galaxy-svg[data-focus] .galaxy-star .gs-core { opacity: 0.2; }
.galaxy-svg[data-focus] .galaxy-star .gs-halo { opacity: 0.03; }
.galaxy-svg .galaxy-star.is-related .gs-core { opacity: 1 !important; }
.galaxy-svg .galaxy-star.is-related .gs-halo { opacity: 0.3 !important; }
.galaxy-svg .galaxy-star.is-focus .gs-core { opacity: 1 !important; stroke: #fff; stroke-width: 1.5; }
.galaxy-svg .galaxy-star.is-focus .gs-halo { opacity: 0.45 !important; }

/* genre isolate from the legend */
.galaxy-svg[data-isolate] .galaxy-star .gs-core,
.galaxy-svg[data-isolate] .galaxy-star .gs-halo { opacity: 0.07; }
.galaxy-svg[data-isolate] .galaxy-link { opacity: 0.03; }
.galaxy-svg[data-isolate="HipHopRap"]      .g-HipHopRap      .gs-core,
.galaxy-svg[data-isolate="RockMetal"]      .g-RockMetal      .gs-core,
.galaxy-svg[data-isolate="ElectronicEDM"]  .g-ElectronicEDM  .gs-core,
.galaxy-svg[data-isolate="Pop"]            .g-Pop            .gs-core,
.galaxy-svg[data-isolate="Alternative"]    .g-Alternative    .gs-core,
.galaxy-svg[data-isolate="RBSoul"]         .g-RBSoul         .gs-core,
.galaxy-svg[data-isolate="Other"]          .g-Other          .gs-core { opacity: 1 !important; }
.galaxy-svg[data-isolate="HipHopRap"]      .g-HipHopRap      .gs-halo,
.galaxy-svg[data-isolate="RockMetal"]      .g-RockMetal      .gs-halo,
.galaxy-svg[data-isolate="ElectronicEDM"]  .g-ElectronicEDM  .gs-halo,
.galaxy-svg[data-isolate="Pop"]            .g-Pop            .gs-halo,
.galaxy-svg[data-isolate="Alternative"]    .g-Alternative    .gs-halo,
.galaxy-svg[data-isolate="RBSoul"]         .g-RBSoul         .gs-halo,
.galaxy-svg[data-isolate="Other"]          .g-Other          .gs-halo { opacity: 0.28 !important; }

/* --- tooltip --- */
.galaxy-tip {
  position: absolute; pointer-events: none; z-index: 5;
  background: var(--panel-solid); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: 8px 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.65);
  display: flex; flex-direction: column; gap: 2px; white-space: nowrap; max-width: 300px;
}
.galaxy-tip .gt-name { font-weight: 700; font-size: 0.88rem; color: var(--text); }
.galaxy-tip .gt-meta { font-size: 0.74rem; color: var(--text-muted); }
.galaxy-tip .gt-links { font-size: 0.72rem; color: var(--purple-bright); }

/* --- connection-type key --- */
.galaxy-linkkey-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; margin-top: 13px;
}
.galaxy-linkkey {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.74rem; color: var(--text-muted);
}
.galaxy-linkkey i { width: 15px; height: 2px; border-radius: 2px; display: inline-block; }
.galaxy-linkkey-note { font-size: 0.74rem; color: var(--text-dim); margin-left: auto; }

/* --- genre legend --- */
.galaxy-legend { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.galaxy-legend-item {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 7px 12px;
  font-family: var(--font); color: var(--text-muted); font-size: 0.78rem;
  transition: border-color 0.2s, transform 0.2s;
}
.galaxy-legend-item:hover, .galaxy-legend-item:focus-visible,
.galaxy-legend-item.active { border-color: var(--border-strong); transform: translateY(-1px); }
.galaxy-legend-item .gl-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; box-shadow: 0 0 8px currentColor; }
.galaxy-legend-item .gl-name { font-weight: 600; color: var(--text); }
.galaxy-legend-item .gl-count { color: var(--text-dim); }

/* --- detail panel (click a star) --- */
.galaxy-detail {
  margin-top: 14px; background: var(--panel); border: 1px solid var(--border-strong);
  border-radius: var(--radius); padding: 16px 18px;
}
.gd-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.gd-dot { width: 11px; height: 11px; border-radius: 50%; box-shadow: 0 0 10px currentColor; }
.gd-name { font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; color: var(--text); }
.gd-meta { font-size: 0.78rem; color: var(--text-dim); }
.gd-close {
  margin-left: auto; background: none; border: 1px solid var(--border); color: var(--text-muted);
  border-radius: 7px; width: 26px; height: 26px; cursor: pointer; font-size: 0.75rem;
}
.gd-close:hover { border-color: var(--border-strong); color: var(--text); }
.gd-list { list-style: none; display: flex; flex-direction: column; gap: 11px; }
.gd-list li {
  display: grid; grid-template-columns: auto 1fr auto; gap: 4px 10px; align-items: baseline;
  padding-bottom: 11px; border-bottom: 1px solid var(--border);
}
.gd-list li:last-child { border-bottom: 0; padding-bottom: 0; }
.gd-type {
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase;
  border: 1px solid; border-radius: 5px; padding: 2px 7px; white-space: nowrap;
}
.gd-who {
  font: inherit; font-weight: 600; color: var(--text); font-size: 0.9rem;
  background: none; border: 0; padding: 0; cursor: pointer;
}
.gd-who:hover, .gd-who:focus-visible { text-decoration: underline; }
.gd-src { grid-column: 3; grid-row: 1; font-size: 0.75rem; color: var(--purple-bright); white-space: nowrap; }
.gd-note { grid-column: 2 / -1; font-size: 0.8rem; color: var(--text-muted); line-height: 1.55; }
.gd-empty { font-size: 0.85rem; color: var(--text-muted); line-height: 1.6; }

.galaxy-note {
  font-size: 0.8rem; color: var(--text-dim); max-width: 820px;
  margin-top: 16px; line-height: 1.65;
}
.galaxy-note em { color: var(--text-muted); font-style: normal; font-weight: 600; }

@media (max-width: 700px) {
  .galaxy-legend-item .gl-count { display: none; }
  .galaxy-linkkey-note { margin-left: 0; flex-basis: 100%; }
  .gd-list li { grid-template-columns: 1fr; }
  .gd-note, .gd-src { grid-column: 1; grid-row: auto; }
}
@media (prefers-reduced-motion: reduce) { .gs-core { animation: none; } }

/* --- artist search (search box + autocomplete dropdown) --------- */
.galaxy-search { position: relative; margin-bottom: 14px; }
.galaxy-search-label { display: block; font-size: 0.78rem; color: var(--text-dim); margin-bottom: 6px; }
.galaxy-search-inputwrap { max-width: 420px; }
.galaxy-search-input { width: 100%; }
.galaxy-search-results {
  position: absolute; z-index: 5; top: calc(100% + 6px); left: 0; right: 0;
  max-width: 420px; list-style: none; margin: 0; padding: 6px;
  background: var(--panel-solid, var(--panel)); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); box-shadow: 0 14px 34px rgba(0, 0, 0, 0.45);
  max-height: 280px; overflow-y: auto;
}
.galaxy-search-results li {
  display: flex; align-items: baseline; gap: 8px; padding: 8px 10px;
  border-radius: 7px; cursor: pointer; font-size: 0.86rem;
}
.galaxy-search-results li:hover, .galaxy-search-results li.active { background: var(--panel-2); }
.gsr-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; box-shadow: 0 0 6px currentColor; }
.gsr-name { font-weight: 600; color: var(--text); }
.gsr-meta { color: var(--text-dim); font-size: 0.76rem; margin-left: auto; white-space: nowrap; }

/* --- search-match glow ring, drawn on the galaxy itself ----------
   Same "site glow" recipe as the sidebar .avatar-ring: a pulsing
   purple/cyan/green ring plus a rotating corona. The stars live in a
   scaling SVG viewBox rather than fixed HTML, so this is a native SVG
   equivalent (colour-cycling stroke + a spinning dashed ring) instead
   of a literal conic-gradient overlay that would need repositioning
   on every resize. */
.galaxy-searchring { pointer-events: none; }
.gxr-ring {
  fill: none; stroke-width: 3; stroke: var(--purple-bright);
  filter: url(#galaxy-glow);
  animation: gxr-glow-flicker 1.6s ease-in-out infinite;
}
.gxr-corona {
  fill: none; stroke-width: 2; stroke: var(--purple-bright);
  stroke-dasharray: 7 6; opacity: 0.75;
  transform-box: fill-box; transform-origin: center;
  animation: gxr-spin 2.6s linear infinite, gxr-corona-flicker 0.85s ease-in-out infinite;
}
@keyframes gxr-glow-flicker {
  0%, 100% { stroke: var(--purple-bright); }
  35%      { stroke: var(--green); }
  60%      { stroke: var(--cyan); }
}
@keyframes gxr-spin { to { transform: rotate(360deg); } }
@keyframes gxr-corona-flicker {
  0%, 100% { opacity: 0.55; }
  30%      { opacity: 0.9; }
  55%      { opacity: 0.4; }
  80%      { opacity: 0.8; }
}
@media (prefers-reduced-motion: reduce) { .gxr-ring, .gxr-corona { animation: none; } }

/* --- detail modal: same-genre fallback + top songs --------------- */
.gd-genre-picks { margin-top: 14px; }
.gd-genre-picks-title, .gd-songs-title {
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-dim); margin: 0 0 8px;
}
.gd-genre-list { display: flex; flex-direction: column; gap: 4px; }
.gd-genre-pick {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  background: none; border: 0; border-radius: 6px; padding: 6px;
  font: inherit; font-size: 0.86rem; color: var(--text); cursor: pointer;
}
.gd-genre-pick:hover, .gd-genre-pick:focus-visible { background: var(--panel-2); }
.gd-genre-count { margin-left: auto; color: var(--text-dim); font-size: 0.76rem; }
.gd-songs { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
.gd-songs-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
.gd-songs-list li { font-size: 0.86rem; color: var(--text-muted); padding-left: 14px; position: relative; }
.gd-songs-list li::before { content: '♪'; position: absolute; left: 0; color: var(--purple-bright); font-size: 0.75rem; }

@media (max-width: 700px) {
  .galaxy-search-inputwrap, .galaxy-search-results { max-width: none; }
}

/* ============================================================
   18+ access gate (js/agegate.js) — agere.html, bdsm.html
   ============================================================ */
.lh-gate-overlay { display: block; margin: 8px 0 30px; }
.lh-gate-card {
  max-width: 680px; margin: 0 auto;
  background: var(--panel-solid);
  border: 1px solid rgba(255, 107, 129, 0.45);
  border-radius: var(--radius); padding: 30px 28px;
  box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 40px rgba(255, 80, 100, 0.1);
}
.lh-gate-ico { font-size: 2rem; text-align: center; margin-bottom: 6px; }
.lh-gate-card h2 {
  font-size: 1.4rem; text-align: center; margin-bottom: 10px; color: var(--text);
}
.lh-gate-lede {
  color: var(--text-muted); font-size: 0.92rem; line-height: 1.65;
  text-align: center; margin-bottom: 20px;
}

.lh-gate-disclaimer {
  max-height: 260px; overflow-y: auto;
  background: rgba(255, 107, 129, 0.05);
  border: 1px solid rgba(255, 107, 129, 0.25);
  border-radius: var(--radius-sm); padding: 16px 18px; margin-bottom: 20px;
}
.lh-gate-disclaimer:focus-visible { outline: 2px solid var(--border-strong); outline-offset: 2px; }
.lh-gate-disclaimer h3 {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 1.2px;
  color: #ff6b81; margin-bottom: 12px;
}
.lh-gate-disclaimer p {
  font-size: 0.85rem; color: var(--text-muted); line-height: 1.6; margin-bottom: 11px;
}
.lh-gate-disclaimer p:last-child { margin-bottom: 0; }
.lh-gate-disclaimer strong { color: var(--text); }

/* .lh-gate-row and .lh-gate-hp (the name/email fields and their honeypot)
   were removed from js/agegate.js on 25 July -- the gate no longer collects
   or sends anything, just three confirmation checkboxes -- so their rules
   were removed here too rather than left as dead weight. */

.lh-gate-check {
  display: flex; align-items: flex-start; gap: 10px; cursor: pointer;
  font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; margin-bottom: 11px;
}
.lh-gate-check input {
  margin-top: 2px; width: 17px; height: 17px; flex: none;
  accent-color: #ff6b81; cursor: pointer;
}
.lh-gate-check strong { color: var(--text); }

.lh-gate-error {
  color: #ff6b81; font-size: 0.85rem; font-weight: 600;
  background: rgba(255, 107, 129, 0.1);
  border: 1px solid rgba(255, 107, 129, 0.3);
  border-radius: var(--radius-sm); padding: 9px 12px; margin: 4px 0 12px;
}
.lh-gate-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
.lh-gate-actions .btn { flex: 1; min-width: 150px; text-align: center; }
.lh-gate-foot {
  font-size: 0.76rem; color: var(--text-dim); line-height: 1.6;
  margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border);
}
.lh-gate-loading { color: var(--text-dim); text-align: center; padding: 40px 0; }

/* ------------------------------------------------------------
   Section contents pages (js/section-hub.js)
   ------------------------------------------------------------ */
/* Tighter than a normal .sec-head: these repeat every few hundred
   pixels down a contents page, and at full section-heading weight the
   page reads as nine separate pages stapled together. */
.hub-head { margin-top: 30px; }
.hub-head:first-child { margin-top: 0; }
.hub-head h2 { font-size: 1.12rem; }

/* The 18+ marker on a card title. Several grids here mix gated and
   ungated pages deliberately (Safety and the regression pages sit
   beside the kink ones), so this has to be legible at a glance rather
   than decorative. */
.hub-badge {
  display: inline-block; vertical-align: middle;
  margin-left: 2px; padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid rgba(255, 107, 129, 0.5);
  background: rgba(255, 107, 129, 0.12);
  color: #ff8f9f;
  font-size: 0.62rem; font-weight: 700; letter-spacing: 0.06em;
  /* Sits inside a link that already has its own transition; opting out
     keeps it from smearing on hover. */
  transition: none;
}

/* ------------------------------------------------------------
   Step 2 of the gate: the statement (js/agegate.js).
   ------------------------------------------------------------
   Deliberately not a scroll box. The step-1 disclaimer is a legal
   formality and a 260px scroller is the honest treatment of it; this
   is something Harper actually wants read, so it gets the width and
   the type size of the page proper and the visitor scrolls the page
   rather than a panel inside it.

   Also deliberately not red. The rest of the gate is warning-coloured
   because it is a warning. This is an explanation, and dressing an
   explanation as a hazard tells the reader to skip it. */
.lh-gate-statement {
  max-width: 760px;
  border-color: var(--border-strong);
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.lh-gate-statement h2 { text-align: left; font-size: 1.5rem; line-height: 1.3; }
.lh-stmt-body { margin-top: 18px; }
.lh-stmt-body p {
  color: var(--text-muted); font-size: 0.95rem; line-height: 1.75; margin-bottom: 15px;
}
.lh-stmt-body strong { color: var(--text); }
.lh-stmt-body h3 {
  font-size: 0.74rem; text-transform: uppercase; letter-spacing: 1.2px;
  color: var(--purple-bright); margin: 26px 0 12px;
}
.lh-stmt-list { list-style: none; margin: 0 0 18px; padding: 0; }
.lh-stmt-list li {
  position: relative; padding: 0 0 0 20px; margin-bottom: 9px;
  color: var(--text-muted); font-size: 0.93rem; line-height: 1.65;
}
/* Marker drawn rather than a list bullet, so it can carry the accent
   colour without dragging ::marker support along with it. */
.lh-stmt-list li::before {
  content: ""; position: absolute; left: 3px; top: 0.62em;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--purple-bright);
}
.lh-stmt-close {
  margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--border);
  color: var(--text) !important; font-weight: 600;
}
/* The heading takes focus when this step opens (see agegate.js), which
   would otherwise draw a ring around something the visitor never
   clicked. Programmatic focus only, so :focus-visible leaves it alone. */
.lh-gate-statement h2:focus { outline: none; }

@media (max-width: 560px) {
  .lh-gate-card { padding: 22px 18px; }
  .lh-gate-actions .btn { flex: 1 1 100%; }
}

/* --- Full artist list: fixed-height scroll box (was a static top-20) --- */
.artist-scroll {
  max-height: 560px; overflow-y: auto; overscroll-behavior: contain;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--panel); padding: 14px 16px;
  scrollbar-width: thin; scrollbar-color: var(--purple) transparent;
}
.artist-scroll::-webkit-scrollbar { width: 10px; }
.artist-scroll::-webkit-scrollbar-track { background: transparent; }
.artist-scroll::-webkit-scrollbar-thumb {
  background: rgba(168, 85, 247, 0.35); border-radius: 6px;
  border: 3px solid transparent; background-clip: content-box;
}
.artist-scroll::-webkit-scrollbar-thumb:hover { background: rgba(168, 85, 247, 0.6); background-clip: content-box; }
.artist-scroll-note { font-size: 0.78rem; color: var(--text-dim); margin-top: 9px; }
.artist-scroll-note strong { color: var(--text-muted); }

/* ============================================================
   Music visualisations (js/music-viz.js + js/music-viz-data.js)
   Emotional compass, the long tail, and recurring title words.
   Additive only — nothing above this block is touched. The tooltip
   deliberately reuses .galaxy-tip so the whole music page has one
   hover treatment rather than three slightly different ones.
   ============================================================ */
.viz-wrap { margin-bottom: 30px; }
.viz-stage {
  position: relative;
  background: radial-gradient(ellipse at 50% 45%, #100a1e 0%, var(--bg) 74%);
  border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;
}
html[data-theme="light"] .viz-stage {
  background: radial-gradient(ellipse at 50% 45%, #efe6f8 0%, #e3d6ef 74%);
}
.viz-svg { display: block; width: 100%; height: auto; }
.viz-svg:focus-visible { outline: 2px solid var(--purple-bright); outline-offset: -2px; }

/* --- shared chart furniture --- */
.viz-grid { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 5; }
.viz-axis { stroke: var(--border-strong); stroke-width: 1.2; }
.viz-axis-lbl { font-family: var(--font); font-size: 12px; font-weight: 600; fill: var(--text-muted); }
.viz-quad-lbl {
  font-family: var(--font-display); font-size: 15px; font-weight: 700;
  fill: var(--text-dim); opacity: 0.55; letter-spacing: 0.5px;
}
.viz-tick { font-family: var(--font); font-size: 11px; fill: var(--text-dim); }
.viz-tick--right { fill: var(--pink-bright); }

.viz-keyrow, .viz-scale-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-top: 13px;
}
.viz-scale-row { margin: 0 0 13px; }
.viz-key { display: inline-flex; align-items: center; gap: 7px; font-size: 0.76rem; color: var(--text-muted); }
.viz-key i { width: 11px; height: 11px; border-radius: 50%; display: inline-block; box-shadow: 0 0 8px currentColor; }
.viz-keynote { font-size: 0.74rem; color: var(--text-dim); margin-left: auto; max-width: 460px; }

.viz-note { font-size: 0.82rem; color: var(--text-dim); max-width: 820px; margin-top: 16px; line-height: 1.7; }
.viz-note p + p { margin-top: 10px; }
.viz-note strong { color: var(--text-muted); }
.viz-note em { color: var(--text-muted); font-style: normal; font-weight: 600; }

/* --- 1. emotional compass --- */
.compass-bubble, .compass-dot { cursor: pointer; outline: none; }
.cb-fill { fill-opacity: 0.13; transition: fill-opacity 0.2s; }
.cb-ring { fill: none; stroke-width: 1.6; stroke-opacity: 0.6; transition: stroke-opacity 0.2s, stroke-width 0.2s; }
.cb-name { font-family: var(--font); font-size: 13px; font-weight: 700; }
.cb-sub { font-family: var(--font); font-size: 10.5px; fill: var(--text-dim); }
/* Labels sit over a field of artist dots, so give them a background-coloured
   halo drawn behind the glyphs. Keeps them legible without moving the data. */
.cb-name, .cb-sub, .tail-node-lbl, .tail-mark-lbl, .tail-mark-sub {
  paint-order: stroke; stroke: var(--bg); stroke-width: 3.5px; stroke-linejoin: round;
}
.compass-bubble.is-on .cb-fill, .compass-bubble:focus-visible .cb-fill { fill-opacity: 0.26; }
.compass-bubble.is-on .cb-ring, .compass-bubble:focus-visible .cb-ring { stroke-opacity: 1; stroke-width: 2.4; }
.cd-core { stroke: rgba(255,255,255,0.55); stroke-width: 0.8; transition: r 0.15s; }
.compass-dot.is-on .cd-core, .compass-dot:focus-visible .cd-core { stroke: #fff; stroke-width: 2; }

/* --- 2. the long tail --- */
.tail-area { transition: d 0.5s ease; }
.tail-cum { fill: none; stroke: var(--pink-bright); stroke-width: 2; stroke-linejoin: round; }
.tail-node { fill: var(--pink-bright); stroke: var(--bg); stroke-width: 1.5; }
.tail-node-lbl { font-family: var(--font); font-size: 11.5px; font-weight: 600; fill: var(--text-muted); }
.tail-mark { stroke: var(--cyan); stroke-width: 1; stroke-dasharray: 4 4; opacity: 0.5; }
.tail-mark-lbl { font-family: var(--font); font-size: 11.5px; font-weight: 600; fill: var(--cyan); }
.tail-mark-sub { font-family: var(--font); font-size: 10.5px; fill: var(--text-dim); }
.tail-hair { stroke: var(--text-muted); stroke-width: 1; opacity: 0.5; pointer-events: none; }

.viz-chips { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 16px; }
.viz-chip {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 13px 15px; display: flex; flex-direction: column; gap: 3px;
}
.viz-chip .vc-num { font-family: var(--font-display); font-weight: 800; font-size: 1.35rem; color: var(--purple-bright); }
.viz-chip .vc-lbl { font-size: 0.74rem; color: var(--text-dim); line-height: 1.45; }

/* --- 3. recurring words (reuses .artist-bars / .ab-* from above) --- */
.word-bar-row { display: grid; grid-template-columns: 34px minmax(76px, 110px) 1fr minmax(0, 200px) 74px; align-items: center; gap: 12px; }
.word-bars .ab-name { font-family: var(--font-display); font-weight: 700; }
.wb-ex { font-size: 0.76rem; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.word-bar-head { font-size: 0.7rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 2px; }
.word-bar-head .ab-name, .word-bar-head .wb-ex, .word-bar-head .ab-count { font-family: var(--font); font-weight: 600; font-size: 0.7rem; color: var(--text-dim); }

@media (max-width: 900px) {
  .viz-chips { grid-template-columns: repeat(2, 1fr); }
}
/* The shared .artist-bar-row rules already drop the bar and the count onto
   their own rows below 1150px. Follow that, but keep the word and its count
   together on one line so a 24-row chart doesn't turn into a mile of scroll. */
@media (max-width: 1150px) {
  .word-bar-row { grid-template-columns: 28px 1fr auto; row-gap: 5px; }
  .word-bar-row .ab-count { grid-column: 3; order: 0; text-align: right; }
  .word-bar-row .ab-track { grid-column: 1 / -1; order: 3; }
  .word-bar-row .wb-ex { grid-column: 1 / -1; order: 4; }
  .word-bar-head { display: none; }
}
@media (max-width: 700px) {
  .viz-keynote { margin-left: 0; flex-basis: 100%; }
  .word-bar-row .wb-ex { display: none; }
  .viz-chip .vc-num { font-size: 1.15rem; }
}
@media (prefers-reduced-motion: reduce) {
  .tail-area, .cb-fill, .cb-ring, .cd-core { transition: none !important; }
}

/* Small screens: these two charts carry real text inside the SVG, so squashing
   them to 350px makes the labels illegible. Let the stage scroll sideways at a
   readable minimum width instead of shrinking the type into nothing. */
.viz-scroll-hint { display: none; font-size: 0.74rem; color: var(--text-dim); margin-top: 8px; }
@media (max-width: 760px) {
  .viz-stage { overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin; scrollbar-color: var(--purple) transparent; }
  .viz-stage .viz-svg { min-width: 680px; }
  /* the compass carries more type than the tail chart, so it needs more room */
  .viz-stage .compass-svg { min-width: 820px; }
  .viz-stage::-webkit-scrollbar { height: 8px; }
  .viz-stage::-webkit-scrollbar-thumb { background: rgba(168, 85, 247, 0.45); border-radius: 5px; }
  .viz-scroll-hint { display: block; }
}

/* ============================================================
   Ambient lofi toggle (site-wide)
   ------------------------------------------------------------
   The engine lives in js/lofi.js, injected on every page by
   layout.js. landing.html overrides the positioning with its own
   later-cascading rules; everywhere else the button floats in the
   bottom-right so it is always reachable without competing with
   the topbar controls.
   ============================================================ */
.lofi-toggle {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 14px; border-radius: 999px;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  color: var(--text-dim);
  font-family: inherit; font-size: 0.75rem; letter-spacing: 0.2px;
  cursor: pointer; backdrop-filter: blur(12px);
  transition: color .2s ease, border-color .2s ease, box-shadow .2s ease;
}
.lofi-toggle:hover,
.lofi-toggle:focus-visible { color: var(--text); border-color: var(--purple-bright); }
.lofi-toggle[aria-pressed="true"] {
  color: var(--purple-bright);
  border-color: var(--purple-bright);
  box-shadow: var(--glow-purple);
}
.lofi-toggle-floating { position: fixed; right: 18px; bottom: 18px; z-index: 60; }
.lofi-toggle .lofi-icon { display: inline-block; font-size: 0.95rem; line-height: 1; }
.lofi-toggle[aria-pressed="true"] .lofi-icon { animation: lofi-bob 1.6s ease-in-out infinite; }
@keyframes lofi-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2px); } }
@media (prefers-reduced-motion: reduce) {
  .lofi-toggle[aria-pressed="true"] .lofi-icon { animation: none; }
}
@media (max-width: 700px) {
  .lofi-toggle-floating { right: 12px; bottom: 12px; padding: 8px 12px; font-size: 0.7rem; }
}

/* Timestamp under the self-set sidebar meters. Small and quiet: it is
   there so nobody mistakes a hand-set number for live sensor data. */
.status-stamp {
  margin: -2px 0 10px;
  font-size: 0.68rem;
  color: var(--text-dim);
  opacity: 0.75;
  letter-spacing: 0.2px;
}

/* "not yet" state on the journey card's consent row. */
.journey-muted { color: var(--text-dim); opacity: 0.7; }

/* ============================================================
   Sidebar lofi player
   ------------------------------------------------------------
   Replaced the Spotify embed. Markup is rendered by js/lofi.js into
   the empty #lh-lofi-panel that layout.js puts in the sidebar.
   ============================================================ */
.lofi-panel { display: flex; flex-direction: column; gap: 10px; }

.lofi-play {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  width: 100%; padding: 11px 14px;
  border-radius: var(--radius-sm);
  background: var(--grad-brand);
  border: none; color: #fff;
  font-family: inherit; font-size: 0.85rem; font-weight: 600;
  cursor: pointer; transition: filter .2s ease, transform .12s ease;
}
.lofi-play:hover { filter: brightness(1.1); }
.lofi-play:active { transform: translateY(1px); }
.lofi-play:focus-visible { outline: 2px solid var(--purple-bright); outline-offset: 2px; }
.lofi-play-ico { font-size: 0.8rem; line-height: 1; letter-spacing: -1px; }

/* Volume slider. The right level differs by 20+ dB between headphones
   and laptop speakers, so this is a real control rather than a nicety. */
.lofi-vol-row { display: flex; align-items: center; gap: .5rem; margin: .55rem 0 .35rem; }
.lofi-vol-ico { font-size: .85rem; opacity: .7; flex: none; }
.lofi-vol {
  flex: 1; min-width: 0; height: 3px;
  -webkit-appearance: none; appearance: none;
  background: color-mix(in srgb, var(--purple-bright) 30%, transparent);
  border-radius: 2px; cursor: pointer;
}
.lofi-vol::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--purple-bright); border: none; cursor: pointer;
}
.lofi-vol::-moz-range-thumb {
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--purple-bright); border: none; cursor: pointer;
}
.lofi-vol:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; }

/* Says plainly that this is synthesized, not a track someone recorded. */
.lofi-note { font-size: 0.64rem; line-height: 1.4; color: var(--text-dim); opacity: 0.75; }

.side-lofi-link { padding-top: 0; margin-top: -4px; }


/* ============================================================
   MISSING COMPONENTS + FLUID SCALING  (added 2026-07-25)
   ------------------------------------------------------------
   Two problems are fixed together here because they had the same
   visible symptom: pages that looked unfinished on a big monitor.

   1. TWELVE CLASSES WERE USED IN THE HTML AND NEVER DEFINED IN CSS.
      .callout, .callout-warm, .callout-boundary, .contrast-grid,
      .contrast-col, .this, .not-this, .safety-grid, .glossary-term,
      .toc-chips, .boundary-list, .dyn-hub and .comfort-layout appear
      across dynamics.html and all six content/*-body.html files, and
      every one of them rendered as bare unstyled text sitting directly
      on the starfield. The CG/L vs MD/lg comparison was the most
      obvious case: two lists of plain bullets where two cards were
      clearly intended. Nothing was broken in the markup; the rules had
      simply never been written.

   2. RIGID GRIDS + A 760px PROSE COLUMN LEFT LARGE EMPTY AREAS.
      Nearly every grid was repeat(N, 1fr), so a wider viewport made
      each card wider rather than fitting more of them, and .prose was
      pinned at 760px inside a .page that grows to 2200px. On a 1440p
      or ultrawide display that reads as a narrow ribbon of content
      with a lot of nothing beside it.

      The fix is auto-fit + minmax everywhere, so column count responds
      to available width, plus a prose column that scales. Paragraph
      text is deliberately still capped near 74ch: line length is a
      readability constraint, not wasted space, and 200-character lines
      would be worse than the gap they'd fill. Cards and grids inside
      prose take the full widened width, so the page fills out while
      the reading measure stays sane.

      A side effect worth knowing: because auto-fit/minmax is
      inherently responsive, several of the older max-width media
      queries above are now redundant rather than overridden. They are
      left in place as harmless belt-and-braces.
   ============================================================ */

/* ---------- Callouts ----------
   An aside that isn't the main thread of the page. The left bar carries
   the meaning; the tint is secondary so it still reads in light theme. */
.callout,
.callout-warm,
.callout-boundary {
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--purple-bright);
  border-radius: var(--radius-sm);
  padding: 16px 20px;
  margin: 18px 0;
}
.callout p,
.callout-warm p,
.callout-boundary p { color: var(--text-muted); font-size: 0.92rem; }
.callout a,
.callout-warm a,
.callout-boundary a { color: var(--purple-bright); font-weight: 600; }
.callout a:hover,
.callout-warm a:hover,
.callout-boundary a:hover { text-decoration: underline; }

.callout-warm {
  border-left-color: var(--pink-bright);
  background: linear-gradient(100deg, rgba(255, 94, 203, 0.07), var(--panel) 60%);
}
.callout-warm a { color: var(--pink-bright); }

/* The safety boxes on the rope/impact/power-exchange pages. Heavier on
   purpose: these are the paragraphs where skimming past has an actual
   cost, so they get amber rather than the house purple, which every
   other box on the page already uses. */
.callout-boundary {
  border-left-color: var(--amber);
  border-color: rgba(255, 204, 102, 0.28);
  background: linear-gradient(100deg, rgba(255, 204, 102, 0.08), var(--panel) 55%);
  padding: 20px 22px;
}
.callout-boundary h2, .callout-boundary h3 {
  color: var(--amber);
  font-size: 1.05rem;
  margin-bottom: 10px;
}
.callout-boundary a { color: var(--amber); }

/* ---------- Side-by-side comparison ---------- */
.contrast-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 16px;
  margin: 18px 0 16px;
}
.contrast-col {
  background: var(--panel);
  border: 1px solid var(--border);
  border-top: 3px solid var(--purple-bright);
  border-radius: var(--radius-sm);
  padding: 18px 20px;
}
.contrast-col h3, .contrast-col h4 {
  font-size: 1rem;
  margin-bottom: 10px;
  color: var(--purple-bright);
}
.contrast-col ul { list-style: none; display: grid; gap: 7px; }
.contrast-col li {
  position: relative;
  padding-left: 18px;
  font-size: 0.89rem;
  color: var(--text-muted);
  line-height: 1.5;
}
.contrast-col li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.62em;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.5;
}
/* The .a / .b pair is a neutral comparison (CG/L vs MD/lg): two valid
   things, so they differ in hue only, and neither is coded as correct. */
.contrast-col.b { border-top-color: var(--pink-bright); }
.contrast-col.b h3, .contrast-col.b h4 { color: var(--pink-bright); }

/* .this / .not-this IS a judgement, and on the safety pages it's the
   whole point of the section, so it gets green/red rather than a hue
   difference. */
.contrast-col.this { border-top-color: var(--green); }
.contrast-col.this h3, .contrast-col.this h4 { color: var(--green); }
.contrast-col.not-this { border-top-color: #ff6b6b; }
.contrast-col.not-this h3, .contrast-col.not-this h4 { color: #ff8585; }

/* ---------- Safety card grid (wraps existing .value cards) ---------- */
.safety-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 16px;
  margin: 18px 0;
}
.safety-grid .value h3, .safety-grid .value h4 { font-size: 0.98rem; margin-bottom: 6px; }
.safety-grid .value p { font-size: 0.87rem; color: var(--text-muted); line-height: 1.5; }

/* ---------- Glossary ---------- */
.glossary-term {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
  margin-bottom: 10px;
}
.glossary-term dt {
  font-weight: 700;
  color: var(--purple-bright);
  font-size: 0.95rem;
  margin-bottom: 4px;
}
.glossary-term dd {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.88rem;
  line-height: 1.55;
}
.glossary-term dd b, .glossary-term dd strong { color: var(--text); }

/* ---------- Jump-to-section chips ----------
   Uses the existing .tag pill; this only lays them out and makes them
   feel clickable, since .tag alone is a static label elsewhere. */
.toc-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 4px 0 22px;
}
.prose > .toc-chips {
  position: sticky;
  top: calc(var(--topbar-h) + 8px);
  z-index: 12;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: rgba(21, 14, 30, 0.94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 10px 30px -20px rgba(0,0,0,0.8);
}
html[data-theme="light"] .prose > .toc-chips { background: rgba(251,248,253,0.94); }
.toc-chips .tag { transition: all 0.18s ease; }
.toc-chips a.tag:hover {
  background: rgba(168, 85, 247, 0.22);
  border-color: var(--border-strong);
  color: var(--text);
}
.toc-chips a.tag:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }

@media (max-width: 640px) {
  .prose > .toc-chips {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: thin;
    margin-left: -10px;
    margin-right: -10px;
  }
  .prose > .toc-chips .tag { flex: 0 0 auto; }
}

.loading-state {
  min-height: 0;
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: 10px;
  padding: 4px 0;
  color: var(--text-muted);
  font-size: 0.86rem;
}

.writing-switch {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: -10px 0 28px;
  padding: 5px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel);
}
.writing-switch a {
  padding: 8px 14px;
  border-radius: 999px;
  color: var(--text-muted);
  font-size: 0.84rem;
  font-weight: 700;
}
.writing-switch a:hover { color: var(--text); background: rgba(168,85,247,0.12); }
.writing-switch a.is-active {
  color: #fff;
  background: var(--grad-brand);
  box-shadow: 0 4px 16px rgba(168,85,247,0.22);
}

.safety-quick {
  margin-bottom: 22px;
  padding: clamp(20px, 3vw, 28px);
  border-radius: var(--radius);
  border: 1px solid rgba(255, 204, 102, 0.48);
  background: linear-gradient(145deg, rgba(255, 204, 102, 0.13), rgba(248, 113, 113, 0.08));
}

.review-note {
  margin: -18px 0 24px;
  color: var(--text-dim);
  font-size: 0.82rem;
}
.review-note a { color: var(--cyan); }
.safety-quick h2 { margin: 6px 0 14px; }
.safety-quick ul { display: grid; gap: 9px; padding-left: 20px; color: var(--text-muted); }
.safety-quick .hero-btns { margin-top: 18px; }

/* ---------- Hard-limit lists ---------- */
.boundary-list {
  list-style: none;
  display: grid;
  gap: 9px;
  margin: 14px 0 18px;
}
.boundary-list li {
  position: relative;
  padding-left: 26px;
  color: var(--text-muted);
  font-size: 0.91rem;
  line-height: 1.55;
}
.boundary-list li::before {
  content: "\2022";
  position: absolute;
  left: 6px;
  top: -0.06em;
  color: var(--amber);
  font-size: 1.15rem;
  line-height: 1.5;
}

/* .dyn-hub was removed 2026-07-26. It wrapped the hand-written card
   index on dynamics.html, which is now generated by js/section-hub.js
   into the shared .resource-grid. The historical note further up this
   file still names it among the twelve undefined classes fixed on
   25 July; that account is left as written rather than quietly edited,
   since it is a record of what happened. */

/* ---------- Comfort-item figure + text ---------- */
.comfort-layout {
  display: grid;
  grid-template-columns: minmax(220px, 300px) minmax(280px, 1fr);
  gap: 26px;
  align-items: start;
  margin: 18px 0 22px;
}
.comfort-figure { margin: 0; }
.comfort-figure img {
  width: 100%;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
.comfort-figure figcaption {
  margin-top: 9px;
  font-size: 0.8rem;
  color: var(--text-dim);
  line-height: 1.45;
}
@media (max-width: 780px) {
  .comfort-layout { grid-template-columns: 1fr; gap: 16px; }
}

/* ---------- Small orphans ---------- */
.dc-body { color: var(--text-muted); font-size: 0.9rem; line-height: 1.6; }
.gb-error { font-size: 0.85rem; line-height: 1.5; }
.lofi-label { font-size: 0.78rem; letter-spacing: 0.2px; }
.lofi-play-txt { font-weight: 600; }
.lh-gate-form { display: grid; gap: 12px; margin-top: 14px; }
.side-quicklinks { display: grid; gap: 6px; }


/* ============================================================
   FLUID SCALING — column count responds to width, not card size
   ============================================================ */

/* Prose column grows with the viewport instead of stopping at 760px,
   so cards and grids inside it fill a large screen. */
.prose { max-width: clamp(720px, 62vw, 1140px); }

/* ...but running text keeps a readable measure. This is the one place
   where leftover space is correct: long lines are genuinely harder to
   read, and the cards beside/below them do the filling instead. */
.prose > p,
.prose > h2,
.prose > h3,
.prose > h4,
.prose > ul,
.prose > ol,
.prose > blockquote { max-width: 70ch; }

.grid-2         { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.grid-3         { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.value-grid     { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.interest-grid  { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.resource-grid  { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.contact-grid   { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.principle-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.warning-grid   { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.photo-strip    { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.gallery-grid   { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.top-song-grid  { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.inventory-grid { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.skill-grid     { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.mod-list       { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.status-grid    { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.game-grid      { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
.strain-grid    { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.stats          { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.stat-grid      { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }
.mini-stat-grid { grid-template-columns: repeat(auto-fit, minmax(64px, 1fr)); }

/* Re-assert the home page's own override. It sits earlier in the file,
   so without repeating it here the .grid-3 rule above would win and the
   home rail's single-column stack would silently become multi-column. */
.home-side-grid .grid-3 { grid-template-columns: 1fr; }

/* The sidebar's own lists must not inherit the wide-grid treatment. */
.sidebar .grid-2, .sidebar .grid-3 { grid-template-columns: 1fr; }


/* ============================================================
   THE GUIDE  (js/guide.js)
   ------------------------------------------------------------
   The character who wanders along the bottom of the window and
   opens site search. She replaced the magnifying-glass button in
   the top bar.

   Two constraints shaped this more than the look did:

   - She must never eat a click meant for the page. The wrapper is
     pointer-events:none and only she and the bubble turn it back
     on, so the strip of screen she occupies stays clickable.
   - She must not be a moving distraction. Movement is a slow
     transition between resting spots rather than a constant
     animation, and prefers-reduced-motion removes it entirely
     along with the idle bob and the blink.
   ============================================================ */
.lh-guide {
  position: fixed;
  /* Offset by the sidebar width: the sidebar is fixed, so a plain vw
     position parks her on top of the playlist card and the stat meters
     rather than in the content area she is supposed to be guiding you
     around. js/guide.js keeps the same offset when it moves her. */
  left: calc(var(--sidebar-w) + 2vw);
  bottom: 0;
  z-index: 60;
  width: 96px;
  pointer-events: none;
  transition: left 3.2s cubic-bezier(0.45, 0, 0.25, 1);
}
.lh-guide.guide-gone { display: none; }

.guide-body {
  pointer-events: auto;
  display: block;
  width: 96px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  filter: drop-shadow(0 4px 18px rgba(168, 85, 247, 0.35));
  transition: transform 0.22s ease, filter 0.22s ease;
}
.guide-body:hover {
  transform: translateY(-5px) scale(1.04);
  filter: drop-shadow(0 8px 26px rgba(255, 94, 203, 0.5));
}
.guide-body:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 4px;
  border-radius: 12px;
}
.guide-svg { width: 100%; height: auto; display: block; }

/* Idle bob and an occasional blink. Small enough to read as alive rather
   than as an animation demanding attention. */
@keyframes guide-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-4px); }
}
@keyframes guide-blink {
  0%, 92%, 100% { transform: scaleY(1); }
  95%           { transform: scaleY(0.1); }
}
/* The bob is on the artwork, NOT on the button. Animating the button
   itself means its hit box is never still, which makes it measurably
   harder to click for anyone with imprecise pointing, and browsers treat
   a perpetually-moving target as unstable. The SVG moves; the thing you
   actually click does not. */
.guide-svg         { animation: guide-bob 4.2s ease-in-out infinite; }
.guide-body:hover .guide-svg { animation-play-state: paused; }
.guide-eye {
  transform-box: fill-box;
  transform-origin: center;
  animation: guide-blink 6.5s ease-in-out infinite;
}

/* Speech bubble, sitting above her head. */
.guide-bubble {
  pointer-events: auto;
  position: absolute;
  bottom: 150px;
  left: 50%;
  transform: translate(-50%, 8px);
  width: max-content;
  max-width: min(280px, 60vw);
  background: var(--panel-solid);
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  padding: 11px 30px 11px 14px;
  font-size: 0.83rem;
  line-height: 1.45;
  color: var(--text);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), 0 0 20px rgba(168, 85, 247, 0.18);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s;
}
.guide-bubble.show { opacity: 1; visibility: visible; transform: translate(-50%, 0); }
/* The little tail. */
.guide-bubble::after {
  content: "";
  position: absolute;
  bottom: -7px;
  left: 50%;
  margin-left: -6px;
  width: 12px; height: 12px;
  background: var(--panel-solid);
  border-right: 1px solid var(--border-strong);
  border-bottom: 1px solid var(--border-strong);
  transform: rotate(45deg);
}
.guide-bubble-x {
  position: absolute;
  top: 3px; right: 4px;
  width: 30px; height: 30px;
  border: none;
  background: none;
  color: var(--text-dim);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  border-radius: 6px;
}
.guide-bubble-x:hover { color: var(--text); background: rgba(255, 255, 255, 0.07); }

/* The self-care nudges carry their own opt-out. Deliberately a separate
   control from the × above: muting reminders should not cost you the guide,
   and dismissing the guide should not be the only way to stop being nudged. */
.guide-bubble-mute {
  display: block;
  margin: 8px 0 0 auto;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 0.72rem;
  letter-spacing: 0.2px;
  cursor: pointer;
}
.guide-bubble-mute:hover { color: var(--text); border-color: var(--border-strong); }

/* While she is off exploring the far side of the window she must not be
   clickable, or a stray tap opens search from a character who is not
   visibly there. */
.lh-guide.guide-escaping { pointer-events: none; }
.lh-guide.guide-escaping .guide-bubble { opacity: 0; visibility: hidden; }

/* The shove as she squeezes out through the edge. Applied to .app rather
   than <body> or <html>: transforming an ancestor of a position:fixed
   element makes it the containing block, which would drag the sidebar and
   the guide out of place mid-animation. */
.guide-window-shove { animation: guide-shove 0.62s cubic-bezier(.36,.07,.19,.97) both; }
@keyframes guide-shove {
  0%, 100% { transform: translate3d(0, 0, 0); }
  12%      { transform: translate3d(-7px, 1px, 0); }
  28%      { transform: translate3d(5px, -2px, 0); }
  46%      { transform: translate3d(-4px, 1px, 0); }
  64%      { transform: translate3d(3px, 0, 0); }
  82%      { transform: translate3d(-2px, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .guide-window-shove { animation: none; }
}


/* ---- Installed and in rotation ----
   Was a run of .tag chips, which at this length read as a wall of little
   boxes rather than a list of games you could actually scan. Cards give each
   title room to breathe and a hit area big enough to matter on a phone.

   The auto-fit floor is wrapped in min() deliberately: a bare px minimum in
   auto-fit is a horizontal-overflow bug waiting to happen, which this site
   has already been bitten by twice (the music genre grid and the ADHD
   screenshot grid). min(200px, 100%) collapses to the container on a narrow
   screen instead of forcing the page wider than the viewport. */
.installed-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: 10px;
  margin-bottom: 10px;
}
.installed-card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 3px;
  min-height: 62px;
  padding: 13px 15px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
}
.installed-card:hover {
  border-color: var(--border-strong);
  background: var(--panel-solid);
  transform: translateY(-2px);
}
.installed-card .ig-name {
  color: var(--text);
  font-weight: 600;
  font-size: 0.95rem;
  line-height: 1.35;
}
.installed-card .ig-note {
  color: var(--text-dim);
  font-size: 0.74rem;
  letter-spacing: 0.2px;
}
/* The one she is actually playing gets the accent, so the sentence under
   the grid does not have to do that work on its own. */
.installed-card.is-now {
  border-color: var(--purple-bright);
  box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.25), 0 6px 20px rgba(168, 85, 247, 0.12);
}
.installed-card.is-now .ig-name { color: var(--purple-bright); }
.installed-card.is-now .ig-note { color: var(--text-muted); }

@media (prefers-reduced-motion: reduce) {
  .installed-card { transition: none; }
  .installed-card:hover { transform: none; }
}

/* Hazel's "let her out" control. Sits next to the mute button and is only
   ever rendered where Document Picture-in-Picture actually exists. */
.guide-bubble-out {
  display: block;
  margin: 8px 0 0 auto;
  padding: 3px 10px;
  border: 1px solid var(--purple);
  border-radius: 999px;
  background: none;
  color: var(--purple-bright);
  font: inherit;
  font-size: 0.72rem;
  letter-spacing: 0.2px;
  cursor: pointer;
}
.guide-bubble-out:hover { background: rgba(168, 85, 247, 0.14); }
/* While she is pacing about in the floating window she should not also be
   standing in the page. */
.lh-guide.guide-in-pip { opacity: 0.25; pointer-events: none; }

/* The one franchise tile that Steam cannot see. Flagged rather than blended
   in, because the section's whole claim is that the counts come from the
   library. */
.fr-tile.is-offsteam { border-color: rgba(255, 204, 102, 0.45); }
.fr-tile .fr-flag {
  display: block;
  margin-top: 4px;
  color: var(--amber);
  font-size: 0.66rem;
  letter-spacing: 0.4px;
  text-transform: uppercase;
}

/* Work page: two choice cards in place of the old segmented toggle. The
   .work-type-btn class is kept so js/work.js needs no changes; only the
   presentation moved from a switch to a pair of equal options. */
.work-type-choice {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: 14px;
  margin-bottom: 22px;
}
.work-type-choice .work-type-btn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  width: 100%;
  padding: 18px 20px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}
.work-type-choice .work-type-btn:hover {
  border-color: var(--border-strong);
  background: var(--panel-solid);
  transform: translateY(-2px);
}
.work-type-choice .work-type-btn.is-active {
  border-color: var(--purple-bright);
  background: var(--panel-solid);
  box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.28), 0 8px 26px rgba(168, 85, 247, 0.12);
}
.work-type-choice .wt-ico { font-size: 1.5rem; line-height: 1; }
.work-type-choice .wt-title { font-weight: 700; font-size: 1.02rem; }
.work-type-choice .work-type-btn.is-active .wt-title { color: var(--purple-bright); }
.work-type-choice .wt-desc { color: var(--text-muted); font-size: 0.86rem; line-height: 1.5; }
/* The selected card is the one carrying state, so it needs a focus ring that
   does not rely on the accent colour alone. */
.work-type-choice .work-type-btn:focus-visible {
  outline: 2px solid var(--purple-bright);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .work-type-choice .work-type-btn { transition: none; }
  .work-type-choice .work-type-btn:hover { transform: none; }
}

/* Screenshots of the site on the projects page. Same treatment as the
   changelog grid; min() on the floor rather than a bare px value, which is
   the pattern that has caused horizontal overflow on this site before. */
.pj-shot-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: 14px;
  margin: 4px 0 10px;
}
.pj-shot {
  margin: 0;
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  background: var(--panel);
}
.pj-shot img { width: 100%; display: block; transition: transform 0.25s ease; }
.pj-shot:hover img { transform: scale(1.03); }
.pj-shot figcaption { font-size: 0.78rem; color: var(--text-dim); padding: 9px 12px; }
@media (prefers-reduced-motion: reduce) {
  .pj-shot img, .pj-shot:hover img { transition: none; transform: none; }
}

/* ---- Love / Like / Share ----
   Sits under the page head on the pages that carry it. Buttons render
   immediately and gain their number when the server answers; a zero that
   corrects itself a moment later reads as "nobody has ever reacted to
   this", which is a worse first impression than no number at all. */
.rx-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 2px 0 6px;
}
.rx-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 16px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel);
  color: var(--text-muted);
  font: inherit;
  font-size: 0.86rem;
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}
.rx-btn:hover:not(:disabled) {
  border-color: var(--border-strong);
  color: var(--text);
  background: var(--panel-solid);
  transform: translateY(-1px);
}
.rx-btn:disabled { opacity: 0.5; cursor: default; }
.rx-btn:focus-visible { outline: 2px solid var(--purple-bright); outline-offset: 2px; }
.rx-ico { font-size: 1.05rem; line-height: 1; }
.rx-count {
  padding: 1px 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
  color: var(--text);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
}
/* Your own reactions are marked rather than merely coloured, since the
   label changes too ("Love this" becomes "Loved"). Colour alone would
   leave the state invisible to anyone who cannot see the difference. */
/* Love/Like count once and stay counted, so a reaction that is already
   yours is not a button any more -- it keeps its highlighted look but
   drops the pointer and the hover lift, because nothing happens if you
   press it again. */
.rx-btn.is-mine { color: var(--text); border-color: var(--purple-bright); background: var(--panel-solid); cursor: default; }
.rx-btn.is-mine:hover { transform: none; }
.rx-btn.rx-love.is-mine { color: var(--pink); border-color: var(--pink); }
.rx-btn.rx-love.is-mine .rx-count { background: rgba(255, 94, 203, 0.16); }
.rx-btn.rx-like.is-mine .rx-count { background: rgba(168, 85, 247, 0.18); }
.rx-note { margin: 6px 0 0; color: var(--text-dim); font-size: 0.78rem; }
@media (prefers-reduced-motion: reduce) {
  .rx-btn { transition: none; }
  .rx-btn:hover:not(:disabled) { transform: none; }
}

/* Keyboard skip link. Off-screen until focused, then pinned top-left above
   everything. Not display:none, which would take it out of the tab order and
   defeat the point. */
.skip-link {
  position: fixed;
  top: -60px;
  left: 12px;
  z-index: 200;
  padding: 10px 16px;
  border: 1px solid var(--purple-bright);
  border-radius: 0 0 10px 10px;
  background: var(--panel-solid);
  color: var(--text);
  font-size: 0.88rem;
  text-decoration: none;
  transition: top 0.15s ease;
}
.skip-link:focus { top: 0; outline: 2px solid var(--purple-bright); outline-offset: 2px; }
/* The target takes focus programmatically, so it must not draw a ring. */
.page:focus { outline: none; }
@media (prefers-reduced-motion: reduce) { .skip-link { transition: none; } }

/* Left behind when she is dismissed, so search never becomes unreachable.
   Gets the same purple/cyan/green glow ring as .avatar-ring (a small
   rotating corona behind it, plus the same box-shadow flicker) --
   this is a small, always-on-screen circle, so it's a good second
   home for that glow per Harper's request to use it on more
   elements, reusing the exact same keyframes rather than new ones. */
.guide-recall {
  position: fixed;
  left: 18px; bottom: 18px;
  z-index: 60;
  width: 42px; height: 42px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--panel-solid);
  color: var(--text);
  font-size: 1rem;
  cursor: pointer;
  /* Was on avatar-ring-glow-flicker (1.6s, full-intensity purple/cyan/
     green) and Harper called it too intense for a button that is
     on screen on every page all the time. Its own slow, dim keyframes
     now -- same family, a quarter of the volume. */
  animation: guide-recall-glow 5s ease-in-out infinite;
}
.guide-recall:hover { border-color: var(--border-strong); }
@keyframes guide-recall-glow {
  0%, 100% { box-shadow: 0 3px 10px -1px rgba(168, 85, 247, 0.38); }
  50%      { box-shadow: 0 4px 13px -1px rgba(196, 165, 255, 0.48); }
}
.guide-recall::after {
  content: '';
  position: absolute; inset: -5px; z-index: -1;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    var(--purple-bright), var(--cyan), var(--green), var(--cyan),
    var(--purple-bright), var(--cyan), var(--green), var(--cyan), var(--purple-bright));
  filter: blur(5px);
  opacity: 0.38;
  animation: avatar-ring-orbit-spin 8s linear infinite, nav-highlight-corona 5s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .guide-recall { animation: none; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4); }
  .guide-recall::after { animation: none; opacity: 0.5; }
}

/* On narrow screens she shrinks and tucks into the corner so she is not
   standing on top of the content. */
@media (max-width: 700px) {
  .lh-guide { width: 68px; left: auto !important; right: 10px; }
  .guide-body { width: 68px; }
  .guide-bubble { bottom: 110px; left: auto; right: 0; transform: translate(0, 8px); max-width: 62vw; }
  .guide-bubble.show { transform: translate(0, 0); }
  .guide-bubble::after { left: auto; right: 22px; }
}

@media (prefers-reduced-motion: reduce) {
  .lh-guide { transition: none; }
  .guide-svg, .guide-eye { animation: none; }
  .guide-bubble { transition: opacity 0.2s ease, visibility 0.2s; }
  .guide-body:hover { transform: none; }
}


/* ============================================================
   CHARACTER SHEET: skill rows at wide widths
   ------------------------------------------------------------
   Two faults, both only visible past about 1600px.

   1. `.lvl` had flex-shrink: 0, which is correct while the value is a
      two-digit number and wrong the moment it is a phrase. Rows like
      "Known Languages / HTML, CSS, Python, Sarcasm (fluent)" and
      "Off-Site Backups / 0 (untrained, and I know)" therefore refused to
      give up any width, pushed the label out of its own cell, and
      overlapped the neighbouring column.
   2. The auto-fit floor of 260px produced seven columns on a wide
      monitor, which is more than the longest labels can live in even
      once they wrap.

   Both spans can now shrink and wrap, the floor is raised, and the grid
   is capped so the sheet stays readable instead of spreading to fill
   whatever monitor it lands on.
   ============================================================ */
.skill-grid {
  grid-template-columns: repeat(auto-fit, minmax(min(330px, 100%), 1fr));
  max-width: 1500px;
  gap: 12px 30px;
}
.skill-row .skill-labels {
  align-items: baseline;
  gap: 12px;
  line-height: 1.35;
}
/* min-width:0 is the load-bearing part: flex items default to min-width
   auto, so without this a long label cannot shrink below its own text and
   simply overflows. */
.skill-row .skill-labels > span { min-width: 0; overflow-wrap: anywhere; }
.skill-row .skill-labels .lvl {
  flex-shrink: 1;
  margin-left: 0;
  text-align: right;
  white-space: normal;
}

/* Same treatment for the other two sheet grids, for the same reason. */
.inventory-grid { grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); max-width: 1500px; }
.stat-grid      { grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr)); max-width: 1100px; }

/* The sheet as a whole. Meter rows were stretching to the full 2200px
   page width, which made a 62/100 bar read as a runway. */
body[data-page="character"] .meter-row { max-width: 720px; }


/* ============================================================
   GLITCH BANDS  (js/glitch.js)
   ------------------------------------------------------------
   Horizontal tearing that fires occasionally while scrolling.

   backdrop-filter is what makes this cheap: each band is an empty div
   that hue-shifts whatever is already painted behind it, so the effect
   applies to text, images, the starfield and the topology canvas alike
   without duplicating any of them.

   The green/cyan bias is deliberate. Corruption in real captured video
   skews that way because chroma is subsampled and the green channel
   carries most of the luminance, so a hue rotation toward green reads as
   a decoding fault rather than as a colour overlay.
   ============================================================ */
.lh-glitch-layer {
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
  overflow: hidden;
}

.lh-glitch-band {
  position: absolute;
  left: 0;
  width: 100%;
  pointer-events: none;
  will-change: transform, opacity;
  /* Shift the underlying pixels sideways and push them green. saturate()
     and contrast() are what stop it looking like a polite tint. */
  backdrop-filter: hue-rotate(var(--hue, 110deg)) saturate(3.2) contrast(1.35);
  -webkit-backdrop-filter: hue-rotate(var(--hue, 110deg)) saturate(3.2) contrast(1.35);
  animation: lh-tear 180ms steps(3, end) forwards;
}

/* The one harder tear per burst also gets a chromatic edge, which is the
   detail that sells it as a signal fault rather than a CSS filter. */
.lh-glitch-band.hard {
  box-shadow:
    inset 2px 0 0 rgba(45, 232, 160, 0.55),
    inset -2px 0 0 rgba(255, 94, 203, 0.45);
  mix-blend-mode: screen;
}

/* steps() rather than a smooth ease: real tearing snaps between states,
   it does not glide. */
@keyframes lh-tear {
  0%   { transform: translateX(0);                opacity: 0; }
  18%  { transform: translateX(var(--dx, 12px));  opacity: 1; }
  46%  { transform: translateX(calc(var(--dx, 12px) * -0.55)); opacity: 0.9; }
  74%  { transform: translateX(calc(var(--dx, 12px) * 0.3));   opacity: 0.65; }
  100% { transform: translateX(0);                opacity: 0; }
}

/* Firefox has historically shipped backdrop-filter behind a flag. Where it
   is unsupported the bands would be invisible, so fall back to a faint
   coloured scanline that at least does something. */
@supports not (backdrop-filter: hue-rotate(90deg)) {
  .lh-glitch-band {
    background: linear-gradient(90deg,
      rgba(45, 232, 160, 0.18), rgba(34, 229, 255, 0.28), rgba(255, 94, 203, 0.18));
    mix-blend-mode: screen;
  }
}

@media (prefers-reduced-motion: reduce) {
  .lh-glitch-layer { display: none; }
}

/* ------------------------------------------------------------
   Glitch extensions (2026-07-27): letters, background blocks,
   buttons, and the sidebar terminal prompt. All four reuse the exact
   mechanisms already established above (backdrop-filter bands spawned
   by glitch.js's burst(), or the steps()-timed "long quiet stretch,
   short violent burst" cadence .chaos-sticker.cs-tag already uses) --
   this is that one system reaching further, not a second one.
   ------------------------------------------------------------ */

/* Letter-level tearing: a handful of THIN vertical slices (not full-
   width like .lh-glitch-band) scoped to one heading's bounding box by
   glitch.js, so it reads as "these specific characters glitched" next
   to the wide horizontal tears rather than duplicating them. */
.lh-glitch-letter {
  position: absolute;
  pointer-events: none;
  will-change: transform, opacity;
  backdrop-filter: hue-rotate(var(--hue, 110deg)) saturate(3.4) contrast(1.4);
  -webkit-backdrop-filter: hue-rotate(var(--hue, 110deg)) saturate(3.4) contrast(1.4);
  animation: lh-tear-y 160ms steps(3, end) forwards;
}
@keyframes lh-tear-y {
  0%   { transform: translateY(0);               opacity: 0; }
  20%  { transform: translateY(var(--dy, 6px));   opacity: 1; }
  55%  { transform: translateY(calc(var(--dy, 6px) * -0.5)); opacity: 0.85; }
  100% { transform: translateY(0);                opacity: 0; }
}

/* Background blocks: a small blocky patch (not a full-width band) at a
   random spot in the viewport -- "random parts of the background"
   distinct from the wide tears, same backdrop-filter trick, chewed up
   with a jittering clip-path instead of a sideways shift. */
.lh-glitch-block {
  position: absolute;
  pointer-events: none;
  will-change: transform, opacity, clip-path;
  backdrop-filter: hue-rotate(var(--hue, 110deg)) saturate(3) contrast(1.3);
  -webkit-backdrop-filter: hue-rotate(var(--hue, 110deg)) saturate(3) contrast(1.3);
  animation: lh-block-glitch 220ms steps(4, end) forwards;
}
@keyframes lh-block-glitch {
  0%   { clip-path: inset(0 0 60% 0);   opacity: 0;    transform: translateX(0); }
  25%  { clip-path: inset(0 40% 0 0);   opacity: 1;    transform: translateX(-6px); }
  55%  { clip-path: inset(30% 0 0 30%); opacity: 0.8;  transform: translateX(5px); }
  80%  { clip-path: inset(0 0 20% 60%); opacity: 0.6;  transform: translateX(-3px); }
  100% { clip-path: inset(0 0 0 0);     opacity: 0;    transform: translateX(0); }
}

@supports not (backdrop-filter: hue-rotate(90deg)) {
  .lh-glitch-letter, .lh-glitch-block {
    background: linear-gradient(180deg, rgba(45,232,160,0.18), rgba(255,94,203,0.22));
    mix-blend-mode: screen;
  }
}
@media (prefers-reduced-motion: reduce) {
  .lh-glitch-letter, .lh-glitch-block { display: none; }
}

/* Button micro-glitch: CSS-only, no JS trigger needed since it isn't
   spawning/removing elements -- just filter/text-shadow on a long
   steps(1) cycle so it sits quiet ~95% of the time and snaps through a
   couple of corrupted frames the rest. Deliberately animates `filter`
   and `text-shadow` only, never `transform`, so it can't fight the
   existing :hover lift transitions on these same elements (both would
   be setting `transform` on every frame otherwise, and the animation
   -- not the hover transition -- would always win). */
.btn, .icon-btn, .side-nav a {
  animation: lh-btn-glitch 10s steps(1, end) infinite;
}
.side-nav a:nth-child(3n+1) { animation-delay: 2.4s; }
.side-nav a:nth-child(3n+2) { animation-delay: 5.1s; }
.side-nav a:nth-child(3n)   { animation-delay: 7.8s; }
.icon-btn:nth-of-type(2) { animation-delay: 3.6s; }
.icon-btn:nth-of-type(3) { animation-delay: 6.2s; }
@keyframes lh-btn-glitch {
  0%, 95%, 100% { filter: none; text-shadow: none; }
  96%  { filter: hue-rotate(25deg) saturate(1.5) brightness(1.1); text-shadow: 2px 0 rgba(255,0,90,0.65), -2px 0 rgba(0,225,255,0.6); }
  97%  { filter: hue-rotate(-15deg) saturate(1.3); text-shadow: -2px 0 rgba(255,0,90,0.6), 2px 0 rgba(0,225,255,0.55); }
  98%  { filter: none; text-shadow: none; }
}

/* Terminal prompt flash: root@lilharper flips to red/pink for one
   short frame roughly once every 18s, then straight back to its normal
   green. That cadence is intentionally nowhere near seizure-risk
   territory (WCAG's general flash guidance caps out at three flashes
   per second; this is about one flash per eighteen seconds, ~200ms
   long, on small text) -- "occasionally corrupted," never a strobe. */
.st-prompt {
  animation: st-prompt-flash 18s steps(1, end) infinite;
}
.side-terminal-line:nth-of-type(3) .st-prompt { animation-delay: 7s; }
@keyframes st-prompt-flash {
  0%, 96.5%     { color: #7fffb0; text-shadow: none; }
  97%           { color: #ff2d55; text-shadow: 0 0 6px rgba(255,45,85,0.8), 1px 0 rgba(255,0,90,0.6), -1px 0 rgba(0,225,255,0.4); }
  97.6%         { color: #ff5ecb; text-shadow: 0 0 6px rgba(255,94,203,0.7); }
  98.2%, 100%   { color: #7fffb0; text-shadow: none; }
}

/* Shared off-switch for all of the above: the localStorage manual
   toggle (LH_GLITCH.disable(), see js/glitch.js) and CALM ROOMS
   (data-calm) both already turn off the scroll/idle bursts; these are
   the two pure-CSS effects that can't check that state on their own,
   so glitch.js mirrors it onto body.lh-glitch-off at load and on every
   enable()/disable() call. */
body.lh-glitch-off .btn,
body.lh-glitch-off .icon-btn,
body.lh-glitch-off .side-nav a,
body.lh-glitch-off .st-prompt,
body[data-calm] .btn,
body[data-calm] .icon-btn,
body[data-calm] .side-nav a,
body[data-calm] .st-prompt {
  animation: none;
}
@media (prefers-reduced-motion: reduce) {
  .btn, .icon-btn, .side-nav a, .st-prompt { animation: none; }
}


/* ============================================================
   STEAM LIBRARY  (js/games.js)
   ------------------------------------------------------------
   A fixed-height scroll box rather than 458 rows down the page,
   for the same reason the music page caps its artist list: a long
   list is a wall, and a wall is not browsable.
   ============================================================ */
.games-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 12px; }
.games-search {
  flex: 1; min-width: 240px; max-width: 460px;
  background: var(--panel-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 11px 14px;
  color: var(--text); font-family: var(--font); font-size: 0.95rem;
}
.games-search:focus {
  outline: none; border-color: var(--purple-bright);
  box-shadow: 0 0 0 3px rgba(168, 130, 255, 0.15);
}
.games-count {
  font-size: 0.8rem; color: var(--text-dim);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.games-scroll {
  max-height: 520px; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 6px;
  max-width: 900px;
}
.games-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding: 8px 12px; border-radius: var(--radius-sm);
}
.games-row:nth-child(odd) { background: rgba(255, 255, 255, 0.02); }
.games-row:hover { background: rgba(168, 85, 247, 0.1); }
.games-name { font-size: 0.9rem; color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.games-genre {
  font-size: 0.7rem; color: var(--text-dim); white-space: nowrap;
  flex-shrink: 0; letter-spacing: 0.2px;
}
.games-empty { padding: 22px 14px; font-size: 0.86rem; color: var(--text-dim); line-height: 1.6; }

/* Genre breakdown bars, reusing the music page's row grid so the two
   pages read as the same system. */
.gm-track {
  flex: 1; height: 9px; background: var(--panel-2);
  border: 1px solid var(--border); border-radius: 6px; overflow: hidden;
}
.gm-fill { height: 100%; background: var(--grad-brand); border-radius: 6px; }
.gm-name { font-size: 0.88rem; color: var(--text); }
.gm-count { font-size: 0.78rem; color: var(--text-dim); font-variant-numeric: tabular-nums; text-align: right; }
.gm-rank { width: 0; }
#games-stats-mount,
#games-time-mount { display: grid; gap: 9px; max-width: 900px; margin-bottom: 6px; }
#games-stats-mount .artist-bar-row,
#games-time-mount .artist-bar-row { grid-template-columns: 0 minmax(150px, 220px) 1fr 90px; }

/* The playtime distribution ends on the two buckets Harper is least keen on
   (under an hour, never launched). Tinting those bars rather than leaving
   them the brand gradient stops the chart reading as an achievement board,
   which would undercut the paragraph sitting directly beneath it. */
#games-time-mount .artist-bar-row:nth-last-child(-n+2) .gm-fill {
  background: linear-gradient(90deg, var(--panel-2), rgba(255,255,255,0.20));
}
#games-time-mount .artist-bar-row:nth-last-child(-n+2) .gm-name,
#games-time-mount .artist-bar-row:nth-last-child(-n+2) .gm-count { color: var(--text-dim); }

@media (max-width: 620px) {
  #games-stats-mount .artist-bar-row,
  #games-time-mount .artist-bar-row { grid-template-columns: 0 minmax(110px, 140px) 1fr 70px; }
  .games-genre { display: none; }
}

/* Playtime column and sort toggle, only rendered once tools/steam-sync.py
   has fetched real hours from the Steam API. */
.games-hours {
  font-size: 0.8rem; color: var(--purple-bright); font-weight: 600;
  font-variant-numeric: tabular-nums; white-space: nowrap;
  flex-shrink: 0; margin-left: auto; padding-left: 10px;
}
.games-hours.zero { color: var(--text-dim); font-weight: 400; font-style: italic; }
.games-sort { display: flex; gap: 6px; }
.games-sort-btn {
  background: var(--panel-2); border: 1px solid var(--border);
  border-radius: 8px; padding: 7px 12px; font-size: 0.78rem;
  color: var(--text-muted); cursor: pointer; font-family: var(--font);
}
.games-sort-btn:hover { color: var(--text); border-color: var(--border-strong); }
.games-sort-btn.active { color: #fff; background: rgba(168, 85, 247, 0.22); border-color: var(--border-strong); }

/* ============================================================
   Character sheet: achievements and traits as plates
   ------------------------------------------------------------
   These two sections were bulleted prose, which is the one format
   that makes a character sheet stop reading like a character sheet.
   They are the same words in the shape the content is imitating:
   an achievement case and a stat block.

   Everything below is layout and colour only. No image assets are
   involved, which matters here — an actual image would not scale,
   would not survive a theme change, could not be read by a screen
   reader, and could not have new achievements appended to it by
   JavaScript when a visitor unlocks one.
   ============================================================ */
.ach-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: 12px;
  max-width: 1180px;
}
.ach-card {
  display: grid;
  grid-template-columns: 54px 1fr;
  gap: 14px;
  align-items: start;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
  transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.ach-card:hover {
  transform: translateY(-2px);
  border-color: var(--border-strong);
  box-shadow: 0 10px 26px rgba(168, 85, 247, 0.14);
}
.ach-ico {
  width: 54px; height: 54px;
  display: grid; place-items: center;
  font-size: 1.5rem;
  border-radius: 12px;
  background: var(--panel-2);
  border: 1px solid var(--border);
}
.ach-body h3 { font-size: 0.95rem; margin: 2px 0 5px; color: var(--text); }
.ach-body p { font-size: 0.85rem; color: var(--text-muted); line-height: 1.55; margin: 0; }
.ach-sub { font-weight: 400; font-size: 0.8rem; color: var(--text-dim); }

/* The one achievement that is about having thrown work away gets the
   gold plate, because on this site that is the rare one. */
.ach-card.ach-rare .ach-ico {
  border-color: rgba(251, 191, 36, 0.45);
  background: rgba(251, 191, 36, 0.10);
}
.ach-card.ach-rare { border-color: rgba(251, 191, 36, 0.28); }

.ach-chip {
  display: inline-block; margin-top: 9px;
  font-size: 0.64rem; text-transform: uppercase; letter-spacing: 1.1px;
  padding: 3px 8px; border-radius: 999px;
  border: 1px solid var(--border); color: var(--text-dim);
}
.ach-card.is-found .ach-ico {
  border-color: rgba(45, 232, 160, 0.5);
  background: rgba(45, 232, 160, 0.10);
}
.ach-card.is-found .ach-chip {
  color: var(--green); border-color: rgba(45, 232, 160, 0.42);
}
/* A locked slot should read as an empty socket, not as a card that
   failed to load: dimmed, dashed, and not lifting on hover. */
.ach-card.is-locked { border-style: dashed; opacity: 0.5; }
.ach-card.is-locked:hover { transform: none; box-shadow: none; border-color: var(--border); }
.ach-card.is-locked .ach-ico { filter: grayscale(1); }

/* --- resistances / vulnerabilities --- */
.trait-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: 14px;
  max-width: 1180px;
}
.trait-card {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 16px 18px;
}
.trait-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.trait-head h3 { font-size: 0.98rem; margin: 0; }
.trait-ico { font-size: 1.15rem; }
.trait-sub { font-weight: 400; font-size: 0.78rem; color: var(--text-dim); }
.trait-chips { display: flex; flex-wrap: wrap; gap: 7px; }
.trait-chip {
  font-size: 0.78rem; padding: 5px 11px; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--border); color: var(--text-muted);
}
.trait-note { font-size: 0.79rem; color: var(--text-dim); margin: 11px 0 0; line-height: 1.5; }

.trait-card.is-weak   { border-left-color: #f87171; }
.trait-card.is-weak   .trait-chip { border-color: rgba(248, 113, 113, 0.32); }
.trait-card.is-resist { border-left-color: var(--green); }
.trait-card.is-resist .trait-chip { border-color: rgba(45, 232, 160, 0.32); }
.trait-card.is-immune { border-left-color: #fbbf24; }
.trait-card.is-immune .trait-chip { border-color: rgba(251, 191, 36, 0.34); }
.trait-card.is-mech   { border-left-color: var(--purple-bright); }
.trait-card.is-mech   .trait-chip { border-color: rgba(168, 85, 247, 0.34); }

/* --- status effects ---
   Scoped to the character sheet on purpose. This block used to be a bare
   .status-grid, which is also Hytale's server-status class (see the rule
   near the top of this file). Being later in the file, it won everywhere,
   quietly overriding Hytale's four-column layout and the responsive
   override in the media query above. Same specificity, no !important, no
   visible error: exactly the kind of collision that survives for months. */
[data-page="character"] .status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: 12px;
  max-width: 1180px;
}
.status-card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 15px 17px;
}
.status-top { display: flex; align-items: center; gap: 9px; margin-bottom: 9px; }
.status-top h3, .status-top h4 { font-size: 0.92rem; margin: 0; color: var(--purple-bright); }
.status-ico { font-size: 1.1rem; }
.status-card p { font-size: 0.85rem; color: var(--text-muted); line-height: 1.6; margin: 0; }
.status-adv { color: var(--green); font-weight: 600; }
.status-dis { color: #f87171; font-weight: 600; }

@media (max-width: 480px) {
  .ach-card { grid-template-columns: 44px 1fr; gap: 11px; }
  .ach-ico { width: 44px; height: 44px; font-size: 1.25rem; }
}

/* ============================================================
   Gaming: the combined hours chart
   ------------------------------------------------------------
   This chart exists because the two halves of the answer lived in
   two different places on the page: a stat row for the MMOs and a
   row of chips for the Steam figures, with nothing making the
   comparison. The MMOs are three to four times bigger than the
   largest Steam entry, and that only lands when they share an axis.

   The MMO rows are tinted differently and labelled in the legend
   rather than silently mixed in, because those four numbers come
   from Harper's own recollection of launchers that report nothing,
   while everything below them is measured by Steam. Same chart,
   two different standards of evidence, said out loud.
   ============================================================ */
.hours-legend { display: flex; gap: 18px; flex-wrap: wrap; margin: 6px 0 14px; }
.hl-key { display: inline-flex; align-items: center; gap: 7px; font-size: 0.76rem; color: var(--text-dim); }
.hl-swatch { width: 22px; height: 9px; border-radius: 5px; display: inline-block; }
.hl-swatch.is-mmo   { background: linear-gradient(90deg, #fbbf24, #f97316); }
.hl-swatch.is-steam { background: var(--grad-brand); }

.hours-chart { display: grid; gap: 8px; max-width: 1000px; margin-bottom: 20px; }
.hr-row {
  display: grid;
  grid-template-columns: 26px minmax(140px, 210px) 1fr 72px;
  align-items: center; gap: 12px;
}
.hr-rank {
  font-size: 0.68rem; color: var(--text-dim);
  font-variant-numeric: tabular-nums; font-family: ui-monospace, monospace;
}
.hr-name { font-size: 0.87rem; color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.hr-sub { font-size: 0.72rem; color: var(--text-dim); }
.hr-track {
  height: 12px; background: var(--panel-2);
  border: 1px solid var(--border); border-radius: 7px; overflow: hidden;
}
.hr-fill { height: 100%; background: var(--grad-brand); border-radius: 7px; }
.hr-row.is-mmo .hr-fill { background: linear-gradient(90deg, #fbbf24, #f97316); }
.hr-row.is-mmo .hr-name { color: #fbbf24; font-weight: 600; }
.hr-val {
  font-size: 0.78rem; color: var(--text-muted);
  font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap;
}

/* The four figures the chart adds up to. Deliberately a strip rather
   than four more cards: it is a footnote to the chart, not a section. */
.totals-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr));
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden; max-width: 1000px; margin-bottom: 8px;
}
.ts-cell { padding: 16px 14px; text-align: center; border-right: 1px solid var(--border); }
.ts-cell:last-child { border-right: none; }
.ts-num {
  font-family: var(--font-display); font-size: 1.35rem; font-weight: 800;
  color: var(--text); font-variant-numeric: tabular-nums;
}
.ts-lbl { font-size: 0.72rem; color: var(--text-muted); margin-top: 3px; }
.ts-cell.is-total { background: rgba(168, 85, 247, 0.08); }
.ts-cell.is-total .ts-num { color: var(--purple-bright); }

/* --- franchise tiles --- */
.fr-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  gap: 10px; max-width: 1180px; margin-bottom: 6px;
}
.fr-tile {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 14px 15px;
  transition: transform 0.18s ease, border-color 0.18s ease;
  cursor: default;
}
.fr-tile:hover { transform: translateY(-2px); border-color: var(--border-strong); }
.fr-count {
  display: block; font-family: var(--font-display); font-size: 1.7rem;
  font-weight: 800; line-height: 1; color: var(--purple-bright);
}
.fr-name { display: block; font-size: 0.85rem; color: var(--text); margin: 5px 0 8px; font-weight: 600; }
.fr-track {
  height: 5px; background: var(--panel-2); border-radius: 4px;
  overflow: hidden; margin-bottom: 8px;
}
.fr-fill { height: 100%; background: var(--grad-brand); border-radius: 4px; }
.fr-eg {
  display: block; font-size: 0.68rem; color: var(--text-dim);
  line-height: 1.45; overflow-wrap: anywhere;
}

@media (max-width: 620px) {
  .hr-row { grid-template-columns: 22px minmax(90px, 120px) 1fr 58px; gap: 8px; }
  .hr-name { font-size: 0.8rem; }
  .hr-val { font-size: 0.72rem; }
}

/* ============================================================
   Music: the 250-artist explorer
   ------------------------------------------------------------
   The ranked list went from 150 rows to 250, which is past the
   point where scrolling is a usable way to answer "does she have
   X". So it gets the same treatment the Steam library got: a
   search box, genre filters, and a live count that reflects the
   filter rather than the total.
   ============================================================ */
.ar-head { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
.ar-head .games-search { flex: 1 1 240px; min-width: 0; }
.ar-filters { display: flex; flex-wrap: wrap; gap: 6px; }
.ar-chip {
  background: var(--panel-2); border: 1px solid var(--border);
  border-radius: 999px; padding: 5px 11px; font-size: 0.74rem;
  color: var(--text-muted); cursor: pointer; font-family: var(--font);
}
.ar-chip:hover { color: var(--text); }
/* The active chip keeps whatever genre colour was set inline on it, so
   the filter and the bars agree without duplicating the palette here. */
.ar-chip.active { color: #fff; background: rgba(168, 85, 247, 0.22); border-color: var(--border-strong); }

.artist-bar-row.is-expandable { cursor: pointer; border-radius: 6px; }
.artist-bar-row.is-expandable:hover { background: rgba(255, 255, 255, 0.03); }
.artist-bar-row.is-expandable:focus-visible { outline: 2px solid var(--purple-bright); outline-offset: 2px; }
.artist-bar-row.is-open { background: rgba(168, 85, 247, 0.07); }

.ar-songs {
  padding: 4px 0 12px 46px;
  border-left: 2px solid rgba(168, 85, 247, 0.3);
  margin: 2px 0 8px 12px;
}
.ar-song-list { margin: 0; padding-left: 16px; }
.ar-song-list li { font-size: 0.8rem; color: var(--text-muted); line-height: 1.6; }
.ar-song-more { font-size: 0.74rem; color: var(--text-dim); margin: 6px 0 0; }

@media (max-width: 620px) {
  .ar-songs { padding-left: 14px; margin-left: 6px; }
}

/* ============================================================
   Genre galaxy: connections as a modal
   ------------------------------------------------------------
   This used to render inline below the galaxy and below the legend,
   which on a tall screen meant clicking a star scrolled nothing and
   changed nothing you could see. Same markup, same content, now
   centred over the map you clicked.

   The element is re-parented to <body> by music-galaxy.js before
   these rules apply, so position:fixed here resolves against the
   viewport rather than against whichever ancestor happens to carry a
   transform this week.
   ============================================================ */
.galaxy-backdrop {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(4, 2, 10, 0.72);
  backdrop-filter: blur(3px);
  animation: gx-fade 0.16s ease;
}
.galaxy-detail.galaxy-modal {
  position: fixed; z-index: 91;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(860px, calc(100vw - 32px));
  max-height: min(78vh, 720px); overflow-y: auto;
  margin-top: 0;
  background: var(--panel); border: 1px solid var(--border-strong);
  border-radius: var(--radius); padding: 18px 20px;
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(168, 85, 247, 0.18);
  animation: gx-pop 0.18s cubic-bezier(0.2, 0.9, 0.3, 1);
}
/* The header carries the close button, so it stays put while the list
   scrolls — otherwise a long connection list scrolls the only exit
   out of view. */
.galaxy-detail.galaxy-modal .gd-head {
  position: sticky; top: -18px; z-index: 1;
  background: var(--panel); padding: 4px 0 10px; margin-bottom: 10px;
  border-bottom: 1px solid var(--border);
}
.galaxy-detail.galaxy-modal .gd-close { margin-left: auto; }
html.galaxy-modal-open { overflow: hidden; }

@keyframes gx-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes gx-pop {
  from { opacity: 0; transform: translate(-50%, -46%) scale(0.98); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .galaxy-backdrop, .galaxy-detail.galaxy-modal { animation: none; }
}
@media (max-width: 560px) {
  .galaxy-detail.galaxy-modal { max-height: 86vh; padding: 14px 15px; }
}

/* On-demand lyric excerpt button. The excerpt is no longer fetched
   automatically (see js/music.js), so each card needs a way to ask. */
.lyrics-fetch-btn {
  margin-top: 8px; background: var(--panel-2); border: 1px solid var(--border);
  border-radius: 8px; padding: 6px 12px; font-size: 0.76rem;
  color: var(--text-muted); cursor: pointer; font-family: var(--font);
}
.lyrics-fetch-btn:hover:not(:disabled) { color: var(--text); border-color: var(--border-strong); }
.lyrics-fetch-btn:disabled { opacity: 0.55; cursor: default; }
.lyrics-excerpt-scroll:empty { display: none; }

/* A showcase pick with no verifiable official upload. Rather than embed a
   guessed videoId, the card says so and links out. */
.showcase-noembed {
  background: var(--panel-2); border: 1px dashed var(--border);
  border-radius: var(--radius-sm); padding: 14px 15px; margin-top: 10px;
}
.se-track { font-weight: 700; color: var(--text); font-size: 0.95rem; margin: 0 0 6px; }
.se-why { font-size: 0.78rem; color: var(--text-dim); line-height: 1.55; margin: 0 0 10px; }
.se-links { display: flex; flex-wrap: wrap; gap: 12px; }
.se-link { font-size: 0.78rem; color: var(--purple-bright); }

/* --- "Artists I'd vouch for": the one hand-picked list on the music page --- */
.pick-grid { display: flex; flex-wrap: wrap; gap: 8px; max-width: 1100px; }
.pick-chip {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 999px; padding: 6px 8px 6px 14px;
  transition: transform 0.16s ease, border-color 0.16s ease;
}
.pick-chip:hover { transform: translateY(-2px); border-color: var(--border-strong); }
.pick-name { font-size: 0.85rem; color: var(--text); }
/* The count is the only measured thing on a chip, so it is visually
   separated from the name rather than run together with it. */
.pick-count {
  font-size: 0.7rem; color: var(--text-dim); font-variant-numeric: tabular-nums;
  background: var(--panel-2); border-radius: 999px; padding: 2px 8px; min-width: 22px; text-align: center;
}

/* Showcase card headings. The icon is gone (see js/music.js) and the song
   title is now printed on the card, because the embed's own title bar is
   clipped at this card width and the song is the point of the card. */
.music-showcase-card .pc-titlewrap { min-width: 0; }
.music-showcase-card .pc-head h2, .music-showcase-card .pc-head h3 { font-size: 0.98rem; overflow-wrap: anywhere; }
.pc-track {
  font-size: 0.86rem; color: var(--purple-bright); font-weight: 600;
  margin-top: 2px; overflow-wrap: anywhere; line-height: 1.35;
}

/* --- "On rotation right now": the one dated, deliberately perishable list --- */
.now-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  gap: 10px; max-width: 1100px;
}
.now-card {
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--purple-bright);
  border-radius: var(--radius-sm); padding: 13px 15px;
}
.now-name { display: block; font-weight: 700; color: var(--text); font-size: 0.92rem; overflow-wrap: anywhere; }
.now-sub { display: block; font-size: 0.73rem; color: var(--text-dim); margin-top: 3px; }

/* --- "What actually played": the real, timestamped Spotify log --- */
.recent-heading { font-size: 0.95rem; color: var(--text); margin: 0 0 6px; }
.recent-list {
  display: flex; flex-direction: column; gap: 1px; max-width: 820px;
  background: var(--border); border-radius: var(--radius-sm); overflow: hidden;
  margin-top: 12px;
}
.recent-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  background: var(--panel); padding: 9px 14px; font-size: 0.83rem;
}
.recent-when { color: var(--text-dim); font-size: 0.76rem; min-width: 108px; flex-shrink: 0; }
.recent-main { flex: 1; min-width: 160px; overflow-wrap: anywhere; }
.recent-track { color: var(--text); font-weight: 600; }
.recent-artist { color: var(--text-muted); }
.recent-badge {
  font-size: 0.7rem; padding: 3px 9px; border-radius: 999px; white-space: nowrap;
  border: 1px solid var(--border);
}
.recent-badge.is-skip { color: var(--text-dim); }
.recent-badge.is-brief { color: var(--amber); border-color: rgba(255, 204, 102, 0.35); background: rgba(255, 204, 102, 0.08); }
.recent-badge.is-played { color: var(--green); border-color: rgba(45, 232, 160, 0.35); background: rgba(45, 232, 160, 0.08); }
/* Was rgba(110, 231, 168, ...) -- a paler green that doesn't match --green's
   actual RGB (45, 232, 160), unlike every other translucent-green surface
   in this file. Found in the 2026-07-28 color-consistency audit. */

/* ============================================================
   My live stream (twitch.html)
   ------------------------------------------------------------
   Twitch purple (#9146FF) is close enough to the site's own
   --purple-bright that the page reads as Twitch-themed without
   fighting the rest of the site. Set as a page-scoped variable so
   the accent can be retuned in one place, and so the chaos rifts
   pick it up through --k-glow like every other themed page does.
   ============================================================ */
body[data-page="twitch"] {
  --tw-purple: #9146ff;
  --tw-purple-soft: rgba(145, 70, 255, 0.14);
  --k-accent: #9146ff;
  --k-soft: rgba(145, 70, 255, 0.13);
  --k-glow: rgba(145, 70, 255, 0.3);
}

.tw-head { margin-bottom: 14px; }
.tw-eyebrow { color: var(--tw-purple); }
.tw-head h1 { margin-bottom: 8px; }
.tw-glyph { filter: drop-shadow(0 0 12px var(--tw-purple)); }

.tw-status { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; min-height: 24px; }
.tw-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--text-dim); flex-shrink: 0; }
.tw-dot.is-live { background: #ff4d4d; box-shadow: 0 0 0 0 rgba(255, 77, 77, 0.7); animation: tw-pulse 1.8s ease-out infinite; }
.tw-dot.is-off { background: var(--text-dim); }
.tw-dot.is-unknown { background: var(--amber); }
@keyframes tw-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(255, 77, 77, 0.65); }
  70%  { box-shadow: 0 0 0 11px rgba(255, 77, 77, 0); }
  100% { box-shadow: 0 0 0 0 rgba(255, 77, 77, 0); }
}
.tw-status-label { font-weight: 800; font-size: 0.8rem; letter-spacing: 0.09em; text-transform: uppercase; color: var(--text); }
.tw-dot.is-live + .tw-status-label { color: #ff6b6b; }
.tw-meta { font-size: 0.8rem; color: var(--text-dim); }
.tw-meta + .tw-meta::before { content: "·"; margin-right: 10px; color: var(--border-strong); }
.tw-title { margin: 8px 0 0; font-size: 0.95rem; color: var(--text-muted); max-width: 820px; }

/* The embed. Player and chat side by side on desktop; chat drops
   under the player on narrow screens rather than being squeezed to
   an unusable width. */
.tw-live-wrap {
  display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 12px;
  margin: 6px 0 26px;
}
.tw-live-video { position: relative; width: 100%; padding-top: 56.25%; border-radius: var(--radius); overflow: hidden; background: #000; border: 1px solid var(--border); }
.tw-live-video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.tw-live-chat { border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border); background: #18181b; min-height: 480px; }
.tw-live-chat iframe { width: 100%; height: 100%; min-height: 480px; border: 0; display: block; }

.tw-offline {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 10px; padding: 54px 26px; margin: 6px 0 26px;
  border-radius: var(--radius);
  border: 1px dashed var(--border-strong);
  background: linear-gradient(150deg, var(--panel-2), var(--bg-2));
}
.tw-offline-ico { font-size: 2.6rem; filter: drop-shadow(0 0 16px var(--tw-purple)); }
.tw-offline-h { margin: 0; font-size: 1.25rem; color: var(--text); }
.tw-offline-p { margin: 0; max-width: 560px; font-size: 0.9rem; color: var(--text-muted); line-height: 1.65; }
.tw-follow-btn { margin-top: 10px; }

.tw-below { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 22px; margin-top: 8px; }
.tw-about h2, .tw-links h2 { font-size: 1.05rem; margin-bottom: 10px; }
.tw-about p { font-size: 0.92rem; color: var(--text-muted); line-height: 1.7; margin-bottom: 12px; }
.tw-note {
  border-left: 3px solid var(--tw-purple); padding: 10px 14px;
  background: var(--tw-purple-soft); border-radius: var(--radius-sm);
  color: var(--text) !important;
}
.tw-links { display: flex; flex-direction: column; gap: 9px; }
.tw-btn-full { width: 100%; text-align: center; }

@media (max-width: 1000px) {
  .tw-live-wrap { grid-template-columns: 1fr; }
  .tw-live-chat { min-height: 360px; }
  .tw-below { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
  .tw-dot.is-live { animation: none; }
}

/* ============================================================
   Identity timeline (.idt) - trans.html "Nine months of rewriting
   my own bio"
   ------------------------------------------------------------
   A vertical, dated list of Harper's own Instagram bio edits, taken
   from her data export. Deliberately restrained styling: the content
   is genuinely personal and a loud treatment would cheapen it, so
   this is a quiet rail with dots, and the only emphasis is on the
   two or three moments that actually turned.
   ============================================================ */
.idt { list-style: none; margin: 20px 0 24px; padding: 0 0 0 4px; max-width: 760px; position: relative; }
.idt::before {
  content: ""; position: absolute; left: 7px; top: 8px; bottom: 8px; width: 2px;
  background: linear-gradient(180deg, transparent, var(--border-strong) 12%, var(--border-strong) 88%, transparent);
}
.idt-item { position: relative; padding: 0 0 22px 30px; }
.idt-item::before {
  content: ""; position: absolute; left: 0; top: 6px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--panel-solid, var(--panel)); border: 2px solid var(--border-strong);
}
/* The turns: the night she cycled three usernames, and the first time
   she wrote "trans girl". Marked, not shouted. */
.idt-item.is-mark::before { border-color: var(--purple-bright); box-shadow: 0 0 12px rgba(168, 85, 247, 0.5); }
.idt-item.is-final::before { border-color: var(--pink); background: var(--pink); box-shadow: 0 0 16px rgba(255, 94, 203, 0.6); }
.idt-item:last-child { padding-bottom: 0; }
.idt-when {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-dim); font-weight: 700; margin-bottom: 5px;
}
.idt-item.is-final .idt-when { color: var(--pink); }
.idt-quote {
  margin: 0 0 7px; padding: 11px 14px;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--purple-bright); border-radius: var(--radius-sm);
  font-size: 0.9rem; color: var(--text); line-height: 1.6;
}
.idt-item.is-final .idt-quote { border-left-color: var(--pink); }
.idt-h { margin: 0 0 7px; font-weight: 700; color: var(--text); font-size: 0.95rem; }
.idt-note { margin: 0; font-size: 0.87rem; color: var(--text-muted); line-height: 1.65; }
.idt-note code {
  font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.85em;
  background: var(--panel-2); border: 1px solid var(--border);
  border-radius: 4px; padding: 1px 6px; color: var(--purple-bright);
}
@media (max-width: 640px) {
  .idt-item { padding-left: 24px; }
  .idt-quote { font-size: 0.86rem; }
}

/* --- "Nine years of receipts": real, dated, quoted Reddit posts --- */
.receipt-block {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 18px 20px; max-width: 900px;
}
.receipt-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  flex-wrap: wrap; margin-bottom: 8px;
}
.receipt-date { font-size: 0.76rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em; }
.receipt-link { font-size: 0.82rem; color: var(--purple-bright); }
.receipt-link:hover { color: var(--pink-bright); }
.receipt-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 10px; margin-top: 14px;
}
.receipt-card {
  display: block; background: var(--panel-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 12px 14px; text-decoration: none;
  transition: border-color 0.18s, transform 0.18s;
}
.receipt-card:hover { border-color: var(--purple-bright); transform: translateY(-2px); }
.receipt-track { display: block; color: var(--text); font-weight: 700; font-size: 0.88rem; overflow-wrap: anywhere; }
.receipt-artist { display: block; color: var(--text-muted); font-size: 0.78rem; margin-top: 2px; }
/* Was #e018c1, which measured 4.37:1 against the card behind it at
   11.8px. Small text needs 4.5. --pink-bright is the token already
   used elsewhere and clears it at about 6.9:1. */
.receipt-note { display: block; color: var(--pink-bright); font-size: 0.74rem; margin-top: 6px; font-style: italic; }
.receipt-quote {
  font-family: var(--font-display); font-size: 1.05rem; color: var(--text);
  border-left: 3px solid var(--purple-bright); padding-left: 14px; margin: 10px 0 0;
}
@media (max-width: 640px) {
  .recent-row { flex-direction: column; align-items: flex-start; gap: 4px; }
  .recent-when { min-width: 0; }
}


/* ============================================================
   Admin panels (blog + guestbook)
   ------------------------------------------------------------
   These styles used to live in blog.html's and guestbook.html's own
   inline <style> blocks, which was fine while those were the only two
   pages that hosted the panels. admin.html then mounted the SAME two
   panel scripts and inherited none of it, so both panels built
   correctly and rendered as unstyled markup -- the compose box, the
   Edit/Publish/Delete buttons and the post list were all present in the
   DOM and effectively invisible.

   A component used by more than one page belongs in the shared
   stylesheet, not in one of its hosts. Anything mounting
   #blog-admin-mount or #gb-admin-mount is styled from here now.
   ============================================================ */

/* -- blog admin -- */
/* --- control panel (only ever rendered at #admin) --- */
  .bla-panel { width: 100%; max-width: 900px; margin: 0 auto; }
.bla-lede { color: var(--text-muted); font-size: 0.87rem; line-height: 1.6; margin-bottom: 12px; }
.bla-login { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.bla-input, .bla-textarea, .bla-select {
    background: var(--panel-2); border: 1px solid var(--border);
    border-radius: var(--radius-sm); padding: 11px 14px; color: var(--text);
    font-family: var(--font); font-size: 0.95rem; width: 100%;
  }
.bla-login .bla-input { flex: 1; min-width: 200px; }
.bla-input:focus, .bla-textarea:focus, .bla-select:focus { outline: none; border-color: var(--purple-bright); box-shadow: 0 0 0 3px rgba(168,130,255,0.15); }
.bla-textarea { resize: vertical; min-height: 260px; line-height: 1.6; font-size: 1rem; padding: 14px 16px; }
.bla-status { font-size: 0.84rem; margin: 12px 0 0; color: var(--text-muted); }
.bla-status.bad { color: #f87171; }
.bla-status.good { color: #38ffa0; }
.bla-bar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
.bla-counts { font-size: 0.9rem; color: var(--text-muted); }
.bla-bar-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.bla-danger { border: 1px solid rgba(248,113,113,0.45); color: #f87171; background: transparent; }
.bla-danger:hover { background: rgba(248,113,113,0.12); }
.bla-danger.armed { background: rgba(248,113,113,0.2); color: #fff; }
.bla-form { display: grid; gap: 14px; margin: 18px 0; padding: 20px 22px; background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius); }
.bla-form label { font-size: 0.82rem; color: var(--text-muted); font-weight: 600; margin-bottom: 5px; display: block; }
.bla-form-title { font-size: 1.05rem; margin: 0; }
.bla-row { display: grid; grid-template-columns: 1fr 200px; gap: 14px; align-items: end; }
.bla-hint { font-size: 0.74rem; color: var(--text-dim); margin-top: 5px; }
.bla-check { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: var(--text-muted); }
.bla-check input { width: auto; }
.bla-form-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.bla-list { display: grid; gap: 12px; margin-top: 18px; }
.bla-entry {
    background: var(--panel-2); border: 1px solid var(--border);
    border-radius: var(--radius-sm); padding: 14px 16px;
  }
.bla-entry.is-draft { opacity: 0.72; border-style: dashed; }
.bla-entry-head { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; align-items: baseline; }
.bla-name { font-weight: 700; color: var(--purple-bright); font-size: 0.95rem; }
.bla-meta { font-size: 0.72rem; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.bla-excerpt { margin: 8px 0 0; color: var(--text-muted); font-size: 0.88rem; line-height: 1.6; word-break: break-word; }
.bla-flag { display: inline-block; margin-top: 8px; font-size: 0.7rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
.bla-entry-actions { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.bla-entry-actions .btn { font-size: 0.78rem; padding: 7px 12px; }
.bla-empty { color: var(--text-dim); font-size: 0.88rem; margin-top: 16px; }

/* -- guestbook admin -- */
/* --- moderation panel (only ever rendered at #admin) --- */
  .gba-panel { width: 100%; max-width: 900px; margin: 0 auto; }
.gba-lede { color: var(--text-muted); font-size: 0.87rem; line-height: 1.6; margin-bottom: 12px; }
.gba-login { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.gba-input {
    flex: 1; min-width: 200px; background: var(--panel-2); border: 1px solid var(--border);
    border-radius: var(--radius-sm); padding: 11px 14px; color: var(--text);
    font-family: var(--font-body); font-size: 0.95rem;
  }
.gba-input:focus { outline: none; border-color: var(--purple-bright); box-shadow: 0 0 0 3px rgba(168,130,255,0.15); }
.gba-status { font-size: 0.84rem; margin: 12px 0 0; color: var(--text-muted); }
.gba-status.bad { color: #f87171; }
.gba-status.good { color: #38ffa0; }
.gba-bar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
.gba-counts { font-size: 0.9rem; color: var(--text-muted); }
.gba-bar-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.gba-danger { border: 1px solid rgba(248,113,113,0.45); color: #f87171; background: transparent; }
.gba-danger:hover { background: rgba(248,113,113,0.12); }
.gba-danger.armed { background: rgba(248,113,113,0.2); color: #fff; }
.gba-list { display: grid; gap: 12px; margin-top: 18px; }
.gba-entry {
    background: var(--panel-2); border: 1px solid var(--border);
    border-radius: var(--radius-sm); padding: 14px 16px;
  }
.gba-entry.is-hidden { opacity: 0.6; border-style: dashed; }
.gba-entry-head { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; align-items: baseline; }
.gba-name { font-weight: 700; color: var(--purple-bright); font-size: 0.95rem; }
.gba-meta { font-size: 0.72rem; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.gba-msg { margin: 8px 0 0; color: var(--text-muted); font-size: 0.9rem; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
.gba-flag { display: inline-block; margin-top: 8px; font-size: 0.7rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
.gba-entry-actions { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.gba-entry-actions .btn { font-size: 0.78rem; padding: 7px 12px; }
.gba-empty { color: var(--text-dim); font-size: 0.88rem; margin-top: 16px; }
.gba-wipe-title { font-size: 1.05rem; margin-bottom: 10px; color: #f87171; }

/* The composer's two-column row collapses on narrow screens. Lived in
   blog.html's media query, which meant admin.html got the desktop
   layout at every width. */
@media (max-width: 560px) {
  .bla-row { grid-template-columns: 1fr; }
}

/* Code-card chrome used by adhd.html's build screenshots. These two were
   referenced in markup but never defined anywhere, so the window dot and
   the filename rendered unstyled. Sitting with the other .cc-* rules. */
.cc-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--purple-bright); display: inline-block; flex: none; }

/* ============================================================
   "Ask me anything" floating widget (js/askme.js)
   ------------------------------------------------------------
   A fixed bottom-right bubble that opens a small one-way message
   form, posting straight to the VPS which Hazel polls and DMs Harper
   about. Deliberately bottom-RIGHT on desktop, opposite Hazel's avatar
   (.lh-guide, which sits offset from the left sidebar), and swaps to
   bottom-LEFT under 700px, because that's exactly where Hazel's avatar
   relocates to on narrow screens (see the .lh-guide mobile rule) --
   without the swap the two would sit on top of each other on a phone.
   ============================================================ */
.askme-wrap { position: fixed; right: 22px; bottom: 22px; z-index: 260; }

.askme-fab {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 16px; border-radius: 999px; border: none; cursor: pointer;
  background: var(--grad-brand); color: #fff; font-weight: 700; font-size: 0.86rem;
  box-shadow: var(--glow-purple), 0 6px 20px rgba(0,0,0,0.4);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.askme-fab:hover { transform: translateY(-2px); box-shadow: 0 6px 30px rgba(168,85,247,0.6); }
.askme-fab:active { transform: translateY(0); }
.askme-fab-ico { font-size: 1.05rem; line-height: 1; }
.askme-wrap.is-open .askme-fab { opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 0.15s ease; }

.askme-panel {
  position: absolute; right: 0; bottom: 0; width: min(340px, 84vw);
  background: var(--panel-solid); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: 0 20px 50px rgba(0,0,0,0.55);
  padding: 16px; opacity: 0; visibility: hidden; transform: translateY(12px) scale(0.98);
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
  transform-origin: bottom right;
}
.askme-panel.is-open { opacity: 1; visibility: visible; transform: translateY(0) scale(1); }

.askme-panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.askme-panel-title { font-weight: 800; font-size: 1rem; margin-bottom: 4px; }
.askme-panel-sub { color: var(--text-dim); font-size: 0.78rem; line-height: 1.4; }
.askme-close {
  background: transparent; border: none; color: var(--text-muted); font-size: 1.3rem;
  line-height: 1; cursor: pointer; padding: 2px 4px; border-radius: 6px; flex: none;
}
.askme-close:hover { color: var(--text); background: rgba(255,255,255,0.06); }

.askme-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; font-size: 0.82rem; color: var(--text-muted); }
.askme-field em { color: var(--text-dim); font-style: normal; }
.askme-field input, .askme-field textarea {
  background: rgba(255,255,255,0.04); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: 9px 11px; color: var(--text);
  font-family: inherit; font-size: 0.88rem; resize: vertical;
}
.askme-field input:focus, .askme-field textarea:focus { outline: 2px solid var(--purple-bright); outline-offset: 1px; }

/* Honeypot -- present in the DOM and in the tab order would defeat the
   point, so it's pulled out of both rather than just visually hidden
   with something a screen reader would still announce. */
.askme-hp { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }

.askme-msg { font-size: 0.82rem; margin: 0 0 10px; }
.askme-msg.ok { color: var(--green, #2de8a0); }
.askme-msg.err { color: #f87171; }

.askme-actions { display: flex; justify-content: flex-end; }
.askme-send { font-size: 0.86rem; padding: 9px 18px; }
.askme-send:disabled { opacity: 0.7; cursor: default; transform: none !important; }

@media (max-width: 700px) {
  .askme-wrap {
    right: auto;
    left: max(10px, env(safe-area-inset-left));
    bottom: max(10px, env(safe-area-inset-bottom));
  }
  .askme-fab { width: 44px; height: 44px; padding: 0; justify-content: center; }
  .askme-fab-label { display: none; }
  .askme-panel { right: auto; left: 0; transform-origin: bottom left; width: min(300px, 80vw); }
}

@media (prefers-reduced-motion: reduce) {
  .askme-fab, .askme-panel { transition: none; }
  .askme-fab:hover { transform: none; }
}
/* ============================================================
   Bratty accent pass (2026-07-25)
   ------------------------------------------------------------
   Harper's brief, from her own Discord profile: the site is polished
   and professional but misses her actual personality, which reads as
   chaotic-cute rather than corporate. Direction given: "Jinx/Smolder-
   coded" -- a punky, hand-drawn, slightly-too-much energy, built from
   pink/cyan/green (all three already exist as --pink-bright, --cyan,
   --green design tokens, so this reuses the palette rather than
   inventing a fourth family that would compete with --lime, which
   stays reserved for exactly the Ko-fi button and the Blog nav pill).

   NOTHING here reproduces any actual character art or trademarked
   character design. These are original decorative shapes (a hand-
   drawn-style squiggle, a rotated sticker badge, a glitch-flicker
   effect) built to evoke that general punk/chaotic-cute energy, not
   to depict Jinx, Smolder, or anyone else. Same reasoning as not
   using Riot's or Disney's actual artwork anywhere on a personal site.

   Every animated piece here respects prefers-reduced-motion by either
   removing the animation or freezing it on its resting frame -- see
   the query block at the end of this section.
   ============================================================ */

/* Squiggly, hand-drawn-style underline under every .accent phrase
   site-wide (the highlighted words in every page's h1 -- "How I
   actually <span class="accent">relate to people</span>" and so on).
   This was already used on every page-head, so this one change reaches
   the whole site for free rather than needing per-page markup. Drawn
   as an SVG background (a simple wavy line with a pink -> cyan -> green
   gradient stroke) rather than a straight border, then "drawn in" with
   a scaleX reveal on load instead of a plain fade, which reads as a
   marker stroke rather than a UI element appearing. */
.page-head h1 .accent, .hero h1 .accent {
  position: relative;
  display: inline-block;
}
.page-head h1 .accent::after, .hero h1 .accent::after {
  content: "";
  position: absolute;
  left: 2%; right: 2%; bottom: -0.14em;
  height: 0.16em;
  min-height: 4px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 16' preserveAspectRatio='none'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='0'><stop offset='0%25' stop-color='%23ff5ecb'/><stop offset='50%25' stop-color='%2322e5ff'/><stop offset='100%25' stop-color='%232de8a0'/></linearGradient></defs><path d='M3,9 Q18,1 33,9 T63,9 T93,9 T123,9 T153,9 T183,9' fill='none' stroke='url(%23g)' stroke-width='5' stroke-linecap='round'/></svg>");
  background-repeat: no-repeat;
  background-size: 100% 100%;
  transform: scaleX(0);
  transform-origin: left center;
  animation: brat-draw-in 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.15s both;
  pointer-events: none;
}
@keyframes brat-draw-in { to { transform: scaleX(1); } }

/* A small colour tab before each top-nav item, cycling pink / cyan /
   green per item (data-brat-tick, set in js/layout.js at render time --
   plain decoration, carries no meaning so it isn't announced to screen
   readers). Reads as sticker tabs peeking out along the top of the
   site, which is the "along the top where the different sections are"
   part of the brief. Small and quiet rather than a wall of colour. */
.brat-tick {
  display: inline-block; width: 10px; height: 10px; border-radius: 3px;
  margin-right: 8px; flex: none;
  transform: rotate(15deg);
  vertical-align: middle;
  transition: transform 0.2s ease;
  animation: brat-tick-pulse 2.4s ease-in-out infinite;
}
@keyframes brat-tick-pulse {
  0%, 100% { transform: rotate(15deg) scale(1); }
  50% { transform: rotate(15deg) scale(1.3); }
}
.top-nav a:hover .brat-tick, .nav-drop-toggle:hover .brat-tick { transform: rotate(15deg) scale(1.5); }
.brat-tick.t1 { background: var(--pink-bright); box-shadow: 0 0 6px rgba(255,94,203,0.7); }
.brat-tick.t2 { background: var(--cyan); box-shadow: 0 0 6px rgba(34,229,255,0.7); }
.brat-tick.t3 { background: var(--green); box-shadow: 0 0 6px rgba(45,232,160,0.7); }

/* A little rotated "sticker" badge pinned to the corner of the sidebar
   logo -- a spark, idly wobbling, like a patch stuck on a jacket. Purely
   decorative (aria-hidden), sitting just off the edge of .brand so it
   doesn't compete with the actual logo wordmark for attention. */
.brand { position: relative; }
.brat-sticker {
  position: absolute; top: -6px; right: -14px;
  font-size: 1.05rem; line-height: 1;
  transform: rotate(-14deg);
  animation: brat-wobble 4.5s ease-in-out infinite;
  filter: drop-shadow(0 0 6px rgba(255,94,203,0.55));
  pointer-events: none;
  user-select: none;
}
@keyframes brat-wobble {
  0%, 100% { transform: rotate(-14deg) translateY(0); }
  50% { transform: rotate(-6deg) translateY(-2px); }
}

/* Logo glitch-flicker on hover/focus -- a brief chromatic-aberration
   split (cyan/pink ghosted copies offset a couple pixels, snapping back)
   rather than a smooth transition, which is the point: it should read
   as a glitch, not an animation. Text-shadow rather than duplicated DOM
   nodes, so it costs nothing extra to render. */
.brand .logo b {
  display: inline-block;
}
.brand:hover .logo b, .brand:focus-visible .logo b {
  animation: brat-glitch 0.5s steps(1, end) both;
}
@keyframes brat-glitch {
  0%   { text-shadow: 0 0 0 transparent; }
  12%  { text-shadow: -2px 0 var(--pink-bright), 2px 0 var(--cyan); }
  24%  { text-shadow: 2px 0 var(--green), -2px 0 var(--pink-bright); }
  36%  { text-shadow: 0 0 0 transparent; }
  48%  { text-shadow: -3px 1px var(--cyan), 3px -1px var(--pink-bright); }
  60%  { text-shadow: 0 0 0 transparent; }
  100% { text-shadow: 0 0 0 transparent; }
}

/* A small, tactile "squish" on every button press site-wide -- juvenile
   in the good way, like a toy with a satisfying click. Layered on top
   of whatever transition/hover treatment a given .btn variant already
   has (Ko-fi, primary, ghost, danger) rather than replacing any of it. */
.btn:active { transform: scale(0.94) !important; transition-duration: 0.08s !important; }

html[data-theme="light"] .brat-tick.t1 { box-shadow: 0 0 4px rgba(255,94,203,0.45); }
html[data-theme="light"] .brat-tick.t2 { box-shadow: 0 0 4px rgba(34,229,255,0.45); }
html[data-theme="light"] .brat-tick.t3 { box-shadow: 0 0 4px rgba(45,232,160,0.45); }

@media (prefers-reduced-motion: reduce) {
  .page-head h1 .accent::after, .hero h1 .accent::after { animation: none; transform: scaleX(1); }
  .brat-sticker { animation: none; }
  .brat-tick { animation: none; }
  .brand:hover .logo b, .brand:focus-visible .logo b { animation: none; }
  .btn:active { transform: none !important; }
}

/* ============================================================
   Muffin callout (content/agere-body.html only)
   ------------------------------------------------------------
   A one-off "Bluey theme" moment scoped to a single callout box on the
   agere.html deep-dive, rather than reskinning the whole page. That
   page is the most vulnerable, carefully-written thing on the site,
   and swapping its entire palette for a kids'-show one for its whole
   length risked undercutting the tone she worked hard to strike there.
   Contained to this one box, it reads as a warm, self-aware joke
   sitting inside a serious page instead of a costume over the whole
   thing. No character art here, on purpose -- Muffin Heeler is
   Ludo Studio/BBC/Disney's design, and this is a text reference to a
   TV character (same as mentioning any show by name), not a drawing
   of her. The palette below is original: warm cream, sky blue, and
   the dusty orange-red the Heeler family is known for, not a copy of
   any specific frame of the show. */
.muffin-callout {
  position: relative;
  margin: 18px 0;
  padding: 16px 18px 16px 52px;
  border-radius: var(--radius-sm);
  background: linear-gradient(135deg, rgba(255, 244, 214, 0.1), rgba(126, 200, 227, 0.08));
  border: 1px solid rgba(126, 200, 227, 0.4);
  border-left: 4px solid #e8834a;
}
.muffin-callout p { color: var(--text); }
.muffin-callout strong { color: #7ec8e3; }
.muffin-pawprint {
  position: absolute; left: 14px; top: 16px;
  font-size: 1.3rem; line-height: 1;
  transform: rotate(-18deg);
}
html[data-theme="light"] .muffin-callout {
  background: linear-gradient(135deg, rgba(255, 244, 214, 0.55), rgba(126, 200, 227, 0.18));
  border-color: rgba(90, 150, 180, 0.5);
}
html[data-theme="light"] .muffin-callout strong { color: #2b7a9e; }

/* Button spark burst (js/particles.js's initButtonSparks). A short-lived
   overlay pinned to wherever a .btn was clicked, well above everything
   else on the page so it's visible regardless of what's under the
   button. Removed from the DOM ~650ms after creation by the script
   itself; this is just the visual for the sparks it builds. */
.brat-spark-host {
  position: fixed; z-index: 9000; width: 0; height: 0;
  pointer-events: none;
}
.brat-spark {
  position: absolute; top: 0; left: 0;
  width: 5px; height: 5px; border-radius: 50%;
  transform: translate(-50%, -50%);
  animation: brat-spark-fly 0.55s cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
@keyframes brat-spark-fly {
  0%   { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--tx)), calc(-50% + var(--ty))) scale(0.3); opacity: 0; }
}

/* Flame burst -- the "click a button, it catches fire" half of the
   effect (js/particles.js's burst()). Same flame silhouette as
   .fx-flame, just animated to billow upward and dissipate instead of
   idly flickering in place. */
.brat-flame {
  position: absolute; top: 0; left: 0;
  width: 10px; height: 14px;
  background: radial-gradient(circle at 50% 75%, #fff6ff 0%, #ffd9f5 32%, var(--pink-bright) 66%, #4a0a5c 100%);
  border-radius: 0 50% 50% 50%;
  transform: translate(-50%, -50%) rotate(-45deg);
  filter: drop-shadow(0 0 7px rgba(255, 94, 203, 0.85)) drop-shadow(0 0 14px rgba(34, 229, 255, 0.35));
  animation: brat-flame-rise 0.6s ease-out both;
}
@keyframes brat-flame-rise {
  0%   { transform: translate(-50%, -50%) rotate(-45deg) scale(1); opacity: 1; }
  60%  { opacity: 0.9; }
  100% { transform: translate(calc(-50% + var(--fx)), calc(-50% + var(--fy))) rotate(-30deg) scale(0.25); opacity: 0; }
}

/* ---- Site-wide glow pulse ----
   A subtle, one-shot echo of the avatar ring's purple/cyan/green glow
   (see .avatar-ring::after) -- spawned alongside the spark/flame burst
   above on every .btn click (js/particles.js's burst()), toned way
   down from the avatar's constant animated corona: it just blooms and
   fades once instead of looping. This is the "apply it across the
   site in less dramatic ways" piece of that same glow, per Harper's
   request. Gated by the same REDUCE_MOTION check that already guards
   initButtonSparks(), so no separate reduced-motion override needed
   here.  */
.brat-glow-pulse {
  position: absolute; top: 0; left: 0;
  width: 70px; height: 70px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(168,85,247,0.5) 0%, rgba(34,229,255,0.32) 45%, rgba(45,232,160,0) 75%);
  filter: blur(3px);
  transform: translate(-50%, -50%) scale(0.4);
  animation: brat-glow-pulse-out 0.6s ease-out both;
}
@keyframes brat-glow-pulse-out {
  0%   { transform: translate(-50%, -50%) scale(0.4); opacity: 0.9; }
  100% { transform: translate(-50%, -50%) scale(1.7); opacity: 0; }
}

/* ============================================================
   Bratty accent pass, part 2 — "this doesn't look as good yet"
   ------------------------------------------------------------
   Round one (the nav ticks / squiggle / logo glitch above) read as
   tasteful rather than actually bratty. This is the louder pass:
   Harper's own words were "RAWR XD, green splatters, fire, chaos,
   animated stuff... explosions, anime rawr bratty little vibes, cute
   stickers, corruption, rifts in the background." Built as its own
   section rather than editing round one in place, so it's obvious
   what changed between the two passes if a third one is ever needed.

   Still no reproduced character art anywhere — the stickers below are
   emoji (standard Unicode glyphs, not anyone's IP) plus a couple of
   hand-built SVG shapes (a paint splatter, a dimensional rift), not
   fan art of Jinx, Smolder, or anyone else.

   Everything animated in this section is disabled outright under
   prefers-reduced-motion. "Corruption" is intentionally not
   re-implemented here — js/glitch.js already covers it sitewide.
   ============================================================ */

/* ---- Background rifts ----
   Two fixed "tear in reality" shapes sitting behind all page content
   (same layer the starfield lives in), one upper-right, one lower-left,
   so they read as atmosphere rather than clutter competing with text.
   A jagged crack outline with a glowing gradient interior, slowly
   breathing rather than static. */
.chaos-rift {
  position: fixed; z-index: 1; pointer-events: none;
  display: block;
}
/* Sizes and count both up on 2026-07-27 ("bigger and more chaotic, put
   them around the website"). Three became six, and the largest went
   from 460px to 720px, so they read as tears in the page rather than as
   decorations sitting politely in the corners. r4-r6 fill the middle
   band, which was previously untouched -- that was the actual reason
   they felt like corner ornaments instead of something happening TO the
   site. */
.chaos-rift.r1 { top: -110px; right: -90px;  width: 720px; height: 720px; transform: rotate(8deg); }
.chaos-rift.r2 { bottom: -130px; left: 180px; width: 620px; height: 620px; transform: rotate(-14deg) scaleX(-1); }
.chaos-rift.r3 { top: 30%; left: -160px;      width: 520px; height: 520px; transform: rotate(38deg); }
.chaos-rift.r4 { top: 58%; right: -140px;     width: 560px; height: 560px; transform: rotate(-27deg) scaleY(-1); }
.chaos-rift.r5 { top: 6%;  left: 38%;         width: 420px; height: 420px; transform: rotate(64deg); opacity: 0.75; }
.chaos-rift.r6 { bottom: 12%; right: 32%;     width: 460px; height: 460px; transform: rotate(-52deg) scaleX(-1); opacity: 0.8; }

/* Soft, blurred, multi-color gas cloud -- the "nebula" half of the
   effect (Harper's ask: "less like a solid object, more like a nebula
   or an actual rift in space"). Screen-blended so it tints the
   starfield behind it instead of sitting on top like an opaque
   sticker, and drifts on its own slow, independent cycle so it reads
   as turbulent gas rather than a shape breathing in and out. Colors
   differ per rift so the three still feel distinct from each other. */
.chaos-rift-haze {
  position: absolute; inset: -25%;
  filter: blur(22px);
  mix-blend-mode: screen;
  animation: chaos-nebula-drift 48s ease-in-out infinite;
}
/* Escalated 2026-07-27 per backlog 4.6: "more light sources" -- each
   rift went from 3 gas pockets to 5, adding a hot pink core flare and a
   pale amber rim so the cloud reads as lit from multiple angles rather
   than one soft blob, and the breathing range widened to sell an
   unstable discharge rather than a gentle idle. */
/* 6th layer, added 2026-07-27: a page-accent light source. On the
   general site there is no --k-glow set, so this gradient renders fully
   transparent and changes nothing. On a themed kink page (page-themes.css
   sets --k-accent/--k-glow per page) it blends that page's own color into
   the nebula, so the rift stops being the same fixed purple/pink/cyan
   everywhere and starts feeling like part of THAT page rather than
   generic sitewide decoration -- real per-page effect, without hand-
   building 29 separate rift palettes. */
.chaos-rift.r1 .chaos-rift-haze {
  background:
    radial-gradient(ellipse 30% 25% at 45% 50%, rgba(255, 94, 203, 0.7), transparent 60%),
    radial-gradient(ellipse 55% 45% at 35% 40%, rgba(168, 85, 247, 0.55), transparent 70%),
    radial-gradient(ellipse 45% 55% at 65% 60%, rgba(255, 94, 203, 0.45), transparent 70%),
    radial-gradient(ellipse 40% 40% at 55% 25%, rgba(34, 229, 255, 0.4), transparent 70%),
    radial-gradient(ellipse 30% 30% at 20% 70%, rgba(255, 204, 102, 0.3), transparent 65%),
    radial-gradient(ellipse 42% 38% at 60% 70%, var(--k-glow, transparent), transparent 70%);
}
.chaos-rift.r2 .chaos-rift-haze {
  animation-delay: -16s;
  background:
    radial-gradient(ellipse 28% 24% at 55% 45%, rgba(34, 229, 255, 0.65), transparent 60%),
    radial-gradient(ellipse 55% 45% at 35% 40%, rgba(45, 232, 160, 0.5), transparent 70%),
    radial-gradient(ellipse 45% 55% at 65% 60%, rgba(168, 85, 247, 0.45), transparent 70%),
    radial-gradient(ellipse 40% 40% at 55% 25%, rgba(255, 94, 203, 0.4), transparent 70%),
    radial-gradient(ellipse 30% 30% at 75% 20%, rgba(255, 204, 102, 0.28), transparent 65%),
    radial-gradient(ellipse 42% 38% at 25% 65%, var(--k-glow, transparent), transparent 70%);
}
.chaos-rift.r3 .chaos-rift-haze {
  animation-delay: -32s;
  background:
    radial-gradient(ellipse 30% 25% at 40% 55%, rgba(168, 85, 247, 0.7), transparent 60%),
    radial-gradient(ellipse 55% 45% at 35% 40%, rgba(255, 94, 203, 0.5), transparent 70%),
    radial-gradient(ellipse 45% 55% at 65% 60%, rgba(34, 229, 255, 0.45), transparent 70%),
    radial-gradient(ellipse 40% 40% at 55% 25%, rgba(168, 85, 247, 0.4), transparent 70%),
    radial-gradient(ellipse 30% 30% at 15% 25%, rgba(255, 204, 102, 0.3), transparent 65%),
    radial-gradient(ellipse 42% 38% at 70% 30%, var(--k-glow, transparent), transparent 70%);
}
.chaos-rift.r4 .chaos-rift-haze {
  animation-delay: -8s;
  background:
    radial-gradient(ellipse 30% 25% at 50% 50%, rgba(255, 94, 203, 0.75), transparent 60%),
    radial-gradient(ellipse 55% 45% at 30% 45%, rgba(34, 229, 255, 0.5), transparent 70%),
    radial-gradient(ellipse 45% 55% at 70% 55%, rgba(168, 85, 247, 0.5), transparent 70%),
    radial-gradient(ellipse 35% 35% at 45% 20%, rgba(255, 204, 102, 0.35), transparent 68%),
    radial-gradient(ellipse 42% 38% at 60% 75%, var(--k-glow, transparent), transparent 70%);
}
.chaos-rift.r5 .chaos-rift-haze {
  animation-delay: -24s;
  background:
    radial-gradient(ellipse 32% 26% at 45% 48%, rgba(45, 232, 160, 0.6), transparent 60%),
    radial-gradient(ellipse 52% 44% at 60% 40%, rgba(168, 85, 247, 0.5), transparent 70%),
    radial-gradient(ellipse 44% 52% at 35% 62%, rgba(255, 94, 203, 0.45), transparent 70%),
    radial-gradient(ellipse 40% 40% at 55% 25%, var(--k-glow, transparent), transparent 70%);
}
.chaos-rift.r6 .chaos-rift-haze {
  animation-delay: -40s;
  background:
    radial-gradient(ellipse 30% 25% at 55% 45%, rgba(34, 229, 255, 0.65), transparent 60%),
    radial-gradient(ellipse 55% 45% at 38% 55%, rgba(255, 94, 203, 0.5), transparent 70%),
    radial-gradient(ellipse 45% 55% at 65% 35%, rgba(45, 232, 160, 0.4), transparent 70%),
    radial-gradient(ellipse 38% 38% at 30% 30%, var(--k-glow, transparent), transparent 70%);
}
.chaos-rift.r4 .chaos-rift-crack { animation-delay: 0.8s; }
.chaos-rift.r5 .chaos-rift-crack { animation-delay: 2.2s; }
.chaos-rift.r6 .chaos-rift-crack { animation-delay: 4.1s; }

@keyframes chaos-nebula-drift {
  0%, 100%  { transform: rotate(0deg) scale(1); opacity: 0.75; }
  27%       { transform: rotate(13deg) scale(1.24); opacity: 1; }
  52%       { transform: rotate(-6deg) scale(0.86); opacity: 0.42; }
  78%       { transform: rotate(-15deg) scale(1.12); opacity: 0.92; }
}

/* The tear itself -- a thin, jagged, open crack (stroke only, no fill),
   so it reads as an actual rip in the scene rather than a solid
   floating gem. Breathes brighter/dimmer like an unstable energy
   discharge rather than pulsing like a heartbeat. */
.chaos-rift-crack {
  position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible;
  /* Outermost glow layer defaults to cyan, but falls back to the page's
     own --k-glow when one is set (page-themes.css) -- same "quiet
     everywhere, tinted where it matters" approach as the haze's 6th
     gradient layer above. --k-glow rather than --k-accent because it's
     already the translucent rgba variant, matching this layer's alpha. */
  filter: drop-shadow(0 0 7px rgba(255,255,255,0.95)) drop-shadow(0 0 26px rgba(168, 85, 247, 0.95)) drop-shadow(0 0 52px rgba(255, 94, 203, 0.7)) drop-shadow(0 0 78px var(--k-glow, rgba(34, 229, 255, 0.35)));
  animation: chaos-rift-breathe 3.6s ease-in-out infinite;
}
.chaos-rift-crack path {
  fill: none; stroke: #fff; stroke-width: 2.6;
  stroke-linecap: round; stroke-linejoin: round;
}
.chaos-rift.r2 .chaos-rift-crack { animation-delay: 1.6s; }
.chaos-rift.r3 .chaos-rift-crack { animation-delay: 3s; }
@keyframes chaos-rift-breathe {
  0%, 100% { opacity: 0.4; }
  50%      { opacity: 1; }
}
@media (max-width: 900px) {
  .chaos-rift.r2 { left: -40px; }
  .chaos-rift.r3 { display: none; }
}

/* ---- Corruption ----
   Already covered sitewide by js/glitch.js (scroll-triggered signal
   tearing, its own cooldown, its own prefers-reduced-motion handling).
   Deliberately not duplicating that here with a second mechanism. */

/* ---------- Rift discharge: pixelated corruption arcs ----------
   Harper: "shoot off visual corruption kinda like lightning but
   pixelated that corrupts the shit it touches."

   Three parts, and the third is the one that sells it:

   1. .chaos-rift-glow -- a bloom pulsing along the crack, so the tear
      reads as energised rather than as a drawn line.
   2. .chaos-arc -- short pixel-blocked bolts that fire out of a rift and
      die. Built from a hard-stop repeating-linear-gradient with no
      antialiasing, so the segments stay square. A smooth bolt is
      lightning; a blocky one is corruption, and the whole difference
      lives in the edges.
   3. .chaos-arc-hit -- what the bolt leaves on whatever it lands on: a
      brief chromatic tear on the element itself. This is the "corrupts
      what it touches" part, and it is deliberately SHORT. Damage that
      persists is just a broken page.

   Injected and driven by js/chaos-arcs.js. */
/* Tuned down hard after the first real render. At inset -12% with a 62%
   falloff, six of these across the viewport stopped being "glow on the
   cracks" and became a purple wash over the entire page, and body text
   lost most of its contrast behind it. Pulled in tight to the crack and
   dropped to roughly a third of the opacity: the rifts still read as
   energised, the words underneath stay readable. Contrast wins. */
.chaos-rift-glow {
  position: absolute; inset: 18%;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle at 50% 50%,
    rgba(255,255,255,0.10) 0%, rgba(168,85,247,0.09) 22%, transparent 48%);
  mix-blend-mode: screen;
  animation: chaos-glow-pulse 2.9s ease-in-out infinite;
}
@keyframes chaos-glow-pulse {
  0%, 100% { opacity: 0.22; transform: scale(0.94); }
  50%      { opacity: 0.5;  transform: scale(1.07); }
}

.chaos-arc {
  position: fixed;
  z-index: 2;
  pointer-events: none;
  height: 6px;
  transform-origin: 0 50%;
  background: repeating-linear-gradient(90deg,
    #ffffff 0 6px,
    rgba(255,94,203,0.95) 6px 12px,
    transparent 12px 18px,
    rgba(34,229,255,0.95) 18px 24px,
    transparent 24px 30px);
  image-rendering: pixelated;
  filter: drop-shadow(0 0 6px rgba(168,85,247,0.95)) drop-shadow(0 0 14px rgba(255,94,203,0.6));
  animation: chaos-arc-fire 0.42s steps(6, end) forwards;
}
@keyframes chaos-arc-fire {
  0%   { opacity: 0; clip-path: inset(0 100% 0 0); }
  15%  { opacity: 1; clip-path: inset(0 55% 0 0); transform: translateY(-2px) scaleY(1.6); }
  45%  { opacity: 1; clip-path: inset(0 0 0 0);   transform: translateY(1px)  scaleY(0.7); }
  70%  { opacity: 0.85; transform: translateY(-1px) scaleY(1.3); }
  100% { opacity: 0; clip-path: inset(0 0 0 82%); }
}

.chaos-arc-hit { animation: chaos-corrupt-hit 0.5s steps(3, end) 1; }
@keyframes chaos-corrupt-hit {
  0%   { filter: none; transform: translate(0,0); }
  20%  { filter: hue-rotate(90deg) saturate(2.4) contrast(1.3);
         transform: translate(-3px, 1px) skewX(-2deg);
         text-shadow: 2px 0 rgba(255,0,110,0.9), -2px 0 rgba(0,229,255,0.9); }
  55%  { filter: hue-rotate(-70deg) saturate(1.8);
         transform: translate(3px, -1px) skewX(2deg);
         text-shadow: -3px 0 rgba(255,0,110,0.8), 3px 0 rgba(0,229,255,0.8); }
  80%  { filter: invert(0.12) saturate(1.4); transform: translate(-1px, 0); }
  100% { filter: none; transform: translate(0,0); text-shadow: none; }
}

@media (prefers-reduced-motion: reduce) {
  .chaos-rift-glow { animation: none; opacity: 0.5; }
  .chaos-arc { display: none; }
  .chaos-arc-hit { animation: none; }
}
@media (max-width: 760px) {
  /* Bolts crossing a viewport where content is edge to edge is not
     atmosphere, it is interference. */
  .chaos-arc { display: none; }
}

/* ---- Chaos stickers ----
   The "cute stickers / anime rawr bratty" component. Emoji glyphs
   (fire, explosion, paw, lightning, sparkle) plus short chaotic text
   tags, scattered around the sidebar and every page-head (injected by
   js/layout.js so it reaches every page without per-page markup).
   Each idles with a small jitter -- a sticker that isn't quite stuck
   down flat -- rather than sitting perfectly still. */
.chaos-sticker {
  position: absolute; z-index: 5; pointer-events: none; user-select: none;
  line-height: 1;
}
.chaos-sticker.cs-emoji { font-size: 2.1rem; animation: chaos-jitter 2.4s ease-in-out infinite; filter: drop-shadow(0 0 10px rgba(255,255,255,0.35)); }
/* Rebuilt 2026-07-27. These were pink pill-shaped bubbles, which read as
   badges -- something the site was labelling itself with. Harper wanted
   them to read as graffiti on top of the page instead: no container, no
   fill, just the words sitting there being bratty, corrupting whatever
   is behind them. Billie Eilish album-art energy, so: flat, ugly-on-
   purpose type, hard chromatic split, no polish.

   THE CORRUPTION IS A ::before, NOT A FILTER ON THE ELEMENT.
   `backdrop-filter` on the tag itself would smear the text along with
   the background. A separate layer behind it takes the backdrop-filter
   and the text stays sharp on top, which is the only way to get "the
   page behind this is glitching" rather than "this text is blurry".

   mix-blend-mode: screen on the aberration copies is what makes the
   red/cyan offsets add light instead of stacking as three opaque
   copies of the same word. */
.chaos-sticker.cs-tag {
  font-family: var(--font-display, inherit);
  font-size: 1.05rem; font-weight: 800; letter-spacing: -0.3px;
  white-space: nowrap;
  padding: 2px 4px;
  color: #fff;
  text-transform: lowercase;
  /* ONE animation, not two. chaos-jitter and cs-tag-glitch both drive
     `transform`, and when two animations target the same property the
     later one wins outright -- the tilt would have silently disappeared.
     So the glitch keyframes carry the rotation themselves. */
  animation: cs-tag-glitch 5.5s steps(1, end) infinite;
  text-shadow:
    1.5px 0 0 rgba(255, 0, 90, 0.85),
    -1.5px 0 0 rgba(0, 225, 255, 0.85),
    0 0 14px rgba(255, 255, 255, 0.35);
  isolation: isolate;
}
/* The corrupted patch of page behind the words. */
.chaos-sticker.cs-tag::before {
  content: "";
  position: absolute; inset: -6px -12px;
  z-index: -1;
  backdrop-filter: blur(1.5px) hue-rotate(35deg) saturate(1.6) contrast(1.15);
  -webkit-backdrop-filter: blur(1.5px) hue-rotate(35deg) saturate(1.6) contrast(1.15);
  /* Torn edges rather than a rectangle: a clean box is the bubble we
     just removed, wearing a different hat. */
  clip-path: polygon(0 12%, 6% 0, 94% 4%, 100% 18%, 98% 86%, 90% 100%, 8% 96%, 2% 82%);
  animation: cs-tag-corrupt 5.5s steps(1, end) infinite;
  pointer-events: none;
}
.chaos-sticker.cs-tag.alt {
  color: #ffd9f2;
  text-shadow:
    1.5px 0 0 rgba(255, 0, 140, 0.9),
    -1.5px 0 0 rgba(120, 0, 255, 0.85),
    0 0 16px rgba(255, 94, 203, 0.4);
}

/* Permanent glitch name tag under the logo -- the "(H4RP3R.EXE)" glitch
   display-name treatment from Harper's own Discord profile, ported as
   part of the brand mark itself rather than scattered flavor. Reuses
   .cs-tag's corrupted chromatic-split text and corner-corruption patch
   wholesale (same mechanism, not a second one) -- only difference is
   position: the scattered stickers are absolutely positioned and idly
   jittering, this one sits inline under the tagline and stays put, so
   it reads as identity rather than as one more sticker peeling off. */
.chaos-sticker.brand-tag {
  position: static;
  /* block, not inline-block, as of 2026-07-27. Inline it sat on the same
     line as the tagline and the glitch animation's translate/skew pushed
     it straight through the text next to it, which read as a rendering
     fault rather than as a deliberate glitch. On its own line it has
     room to come apart without taking anything else with it. */
  display: block;
  width: fit-content;
  margin-top: 8px;
}
/* Long quiet stretches, then a short violent burst. Something that
   glitches continuously stops reading as a glitch and starts reading as
   a loading spinner. steps(1) so it snaps between states rather than
   easing, because eased corruption looks like a transition. */
@keyframes cs-tag-glitch {
  0%, 86%   { transform: rotate(var(--cs-rot, -8deg)) translate(0, 0) skewX(0deg); }
  88%       { transform: rotate(var(--cs-rot, -8deg)) translate(-3px, 1px) skewX(-9deg); }
  90%       { transform: rotate(var(--cs-rot, -8deg)) translate(3px, -1px) skewX(7deg); }
  92%       { transform: rotate(var(--cs-rot, -8deg)) translate(-2px, 0) skewX(0deg); }
  94%, 100% { transform: rotate(var(--cs-rot, -8deg)) translate(0, 0) skewX(0deg); }
}
@keyframes cs-tag-corrupt {
  0%, 86%   { opacity: 0.55; transform: translate(0, 0) scaleX(1); }
  88%       { opacity: 1;    transform: translate(4px, 0) scaleX(1.06); }
  90%       { opacity: 0.8;  transform: translate(-5px, 1px) scaleX(0.96); }
  92%       { opacity: 1;    transform: translate(2px, -1px) scaleX(1.03); }
  94%, 100% { opacity: 0.55; transform: translate(0, 0) scaleX(1); }
}
@keyframes chaos-jitter {
  0%, 100% { transform: rotate(var(--cs-rot, -8deg)) translateY(0); }
  25%      { transform: rotate(calc(var(--cs-rot, -8deg) + 3deg)) translateY(-1px); }
  50%      { transform: rotate(var(--cs-rot, -8deg)) translateY(1px); }
  75%      { transform: rotate(calc(var(--cs-rot, -8deg) - 3deg)) translateY(-1px); }
}

/* Fire specifically flickers rather than just jittering -- a bit of
   scale/opacity pulse layered under the shared jitter transform. */
.chaos-sticker.cs-fire { animation: chaos-jitter 3.2s ease-in-out infinite, chaos-flicker 1.1s ease-in-out infinite; }
@keyframes chaos-flicker {
  0%, 100% { opacity: 1; filter: brightness(1); }
  45%      { opacity: 0.85; filter: brightness(1.25); }
  60%      { opacity: 1; filter: brightness(0.95); }
}

/* ---- Real flame effect ----
   Replaces the 🔥 emoji chaos-stickers. Harper's ask: an actual burning
   effect, not a glyph standing in for one. Hand-built from two layered
   teardrop shapes (a CSS classic: border-radius: 0 50% 50% 50% rotated
   -45deg reads as a flame silhouette) -- a hot orange/red outer body via
   ::self and a brighter yellow inner tongue via ::before, each flickering
   on its own irregular cycle so the two layers never move in lockstep
   the way a single uniform pulse would. Empty element; nothing but CSS
   draws this. */
/* Recolored 2026-07-27 (RAWR XD pass): orange/red "real fire" swapped
   for hot pink/magenta, matching the flame profile effect on Harper's
   own Discord card rather than literal combustion. The flicker/burn/
   tear mechanics below are untouched -- only the palette moved into
   the --pink/--pink-bright/--purple-bright family already used
   everywhere else on the site, so this flame now reads as the same
   glitch-neon material as the rest of the chaos system instead of a
   separate "real fire" object sitting in the middle of it. */
.fx-flame {
  position: absolute; z-index: 5; pointer-events: none; display: block;
  width: 22px; height: 28px;
  background: radial-gradient(circle at 50% 78%, #fff6ff 0%, #ffd9f5 22%, var(--pink-bright) 48%, var(--pink) 72%, #4a0a5c 100%);
  border-radius: 0 50% 50% 50%;
  filter:
    drop-shadow(0 0 6px rgba(255, 94, 203, 0.9))
    drop-shadow(0 0 18px rgba(224, 24, 193, 0.55))
    drop-shadow(0 0 34px rgba(168, 85, 247, 0.32));
  /* Transform lives in ONE animation and brightness/opacity in the
     other. Two animations touching the same property means the later
     one wins outright rather than compositing, which is how the
     sticker tilt disappeared the first time this file grew a second
     keyframe set on transform. */
  animation:
    fx-flame-flicker 0.73s steps(4, end) infinite,
    fx-flame-burn 1.9s ease-in-out infinite;
}

/* Inner tongue. Runs on a deliberately coprime duration to the body so
   the two layers never land on the same frame twice in a row. */
.fx-flame::before {
  content: ""; position: absolute; inset: 20% 28% 8% 28%;
  background: radial-gradient(circle at 50% 82%, #ffffff 0%, #ffe3f8 45%, var(--cyan) 72%, transparent 86%);
  border-radius: 0 50% 50% 50%;
  animation: fx-flame-flicker 0.41s steps(3, end) infinite reverse;
}

/* ---- The corruption layer ----
   A hard-edged chromatic ghost of the same silhouette, offset a couple
   of pixels and screen-blended, sliced by a clip-path that jumps
   between bands. This is what makes the flame read as glitched rather
   than merely animated: the tear is instantaneous (steps(), not ease)
   and lands on a cycle that shares no common factor with either layer
   underneath, so the three never resolve into a loop the eye can
   predict. Same treatment the .cs-tag stickers got. */
.fx-flame::after {
  content: ""; position: absolute; inset: -1px -2px 0 1px;
  background: linear-gradient(180deg, rgba(0, 255, 240, 0.85), rgba(255, 0, 140, 0.7) 60%, transparent);
  border-radius: 0 50% 50% 50%;
  mix-blend-mode: screen;
  opacity: 0.5;
  /* 0.79s against the body's 0.73s and the inner tongue's 0.41s. Three
     coprime durations, so the layers only all realign every few
     hundred cycles and the eye never finds the loop. */
  animation: fx-flame-tear 0.79s steps(1, end) infinite;
}

/* Harsher pass, 2026-07-27 (Harper: "harsher/glitchier"). The throw on
   both rotation and scale roughly doubled, and one frame now overshoots
   hard in each direction. With steps() timing the eye reads the size of
   the jump rather than the path taken, so widening the extremes is what
   makes it read as violent; adding more keyframes would only make it
   smoother, which is the opposite of the ask. */
@keyframes fx-flame-flicker {
  0%   { transform: rotate(-45deg) scale(1, 1); }
  12%  { transform: rotate(-31deg) scale(0.74, 1.36); }
  26%  { transform: rotate(-58deg) scale(1.28, 0.72); }
  38%  { transform: rotate(-40deg) scale(0.88, 1.15); }
  52%  { transform: rotate(-63deg) scale(1.34, 0.66); }
  64%  { transform: rotate(-36deg) scale(0.8, 1.28); }
  78%  { transform: rotate(-49deg) scale(1.1, 0.9); }
  90%  { transform: rotate(-28deg) scale(0.7, 1.42); }
  100% { transform: rotate(-45deg) scale(1, 1); }
}

/* Brightness rather than shape: an occasional hard flare, then a
   guttering dip, so the flame looks like it is consuming something
   instead of idling. */
@keyframes fx-flame-burn {
  0%, 100% { filter:
      drop-shadow(0 0 6px rgba(255, 94, 203, 0.9))
      drop-shadow(0 0 18px rgba(224, 24, 193, 0.55))
      drop-shadow(0 0 34px rgba(168, 85, 247, 0.32)); }
  22%      { filter:
      drop-shadow(0 0 10px rgba(255, 180, 235, 1))
      drop-shadow(0 0 26px rgba(255, 94, 203, 0.8))
      drop-shadow(0 0 46px rgba(34, 229, 255, 0.4))
      brightness(1.3); }
  38%      { filter:
      drop-shadow(0 0 4px rgba(224, 24, 193, 0.7))
      drop-shadow(0 0 12px rgba(140, 15, 120, 0.4))
      brightness(0.82); }
  61%      { filter:
      drop-shadow(0 0 12px rgba(255, 230, 250, 1))
      drop-shadow(0 0 30px rgba(255, 94, 203, 0.85))
      brightness(1.42); }
}

/* The tear does most of the glitch work, so it took most of the
   harshening. Displacement went from 2-3px to 5-7px, the bands are
   thinner (a narrow slice reads as a scanline dropout, a wide one just
   reads as the shape moving), and hue-rotate now slips per frame so the
   chromatic ghost changes colour between jumps instead of only moving.
   Still steps(1): the whole effect depends on nothing ever easing. */
@keyframes fx-flame-tear {
  0%   { clip-path: inset(0 0 62% 0);   transform: translate(-5px, 0);  opacity: 0.75; filter: hue-rotate(0deg); }
  9%   { clip-path: inset(74% 0 14% 0); transform: translate(6px, 2px); opacity: 0.9;  filter: hue-rotate(70deg); }
  17%  { clip-path: inset(38% 0 52% 0); transform: translate(-7px, -1px); opacity: 0.5; filter: hue-rotate(-40deg); }
  27%  { clip-path: inset(0 0 0 0);     transform: translate(0, 0);      opacity: 0.14; filter: hue-rotate(0deg); }
  35%  { clip-path: inset(12% 0 80% 0); transform: translate(7px, -2px); opacity: 0.95; filter: hue-rotate(140deg); }
  46%  { clip-path: inset(22% 0 66% 0); transform: translate(4px, 1px);  opacity: 0.7;  filter: hue-rotate(-90deg); }
  58%  { clip-path: inset(0 0 88% 0);   transform: translate(-6px, 0);   opacity: 0.85; filter: hue-rotate(30deg); }
  66%  { clip-path: inset(58% 0 26% 0); transform: translate(5px, -2px); opacity: 0.6;  filter: hue-rotate(180deg); }
  78%  { clip-path: inset(84% 0 0 0);   transform: translate(-4px, 1px); opacity: 0.8;  filter: hue-rotate(-120deg); }
  88%  { clip-path: inset(0 0 0 0);     transform: translate(0, 0);      opacity: 0.1;  filter: hue-rotate(0deg); }
  100% { clip-path: inset(0 0 62% 0);   transform: translate(-5px, 0);   opacity: 0.75; filter: hue-rotate(0deg); }
}

/* A flame is decorative, and a hard-cut strobing one is exactly the
   sort of thing this query exists for. Left visible and still, so the
   layout does not shift. */
@media (prefers-reduced-motion: reduce) {
  .fx-flame,
  .fx-flame::before,
  .fx-flame::after { animation: none; }
  .fx-flame { transform: rotate(-45deg); }
  .fx-flame::after { opacity: 0.25; clip-path: none; }
}

.fx-flame.side-fire { top: 2px; left: -12px; }
.fx-flame.head-3 { top: 18px; left: 2%; }

/* Anchor points — the sidebar/page-head are position:relative already
   (or become it here) so the absolutely-positioned stickers above sit
   where JS places them rather than escaping to the document root. */
.side-picture { position: relative; }

/* ---- Sidebar terminal flavor ----
   A small fake-terminal strip in the sidebar, nodding at the
   "root@lilharper:~#" prompt look from Harper's own reference mockups.
   Purely decorative flavor text (aria-hidden), not a real shell. */
.side-terminal {
  margin: 14px 0; padding: 10px 12px; border-radius: var(--radius-sm);
  background: #04140a; border: 1px solid rgba(45, 232, 160, 0.35);
  font-family: 'JetBrains Mono', 'Fira Code', monospace;
  font-size: 0.72rem; line-height: 1.65; color: #2de8a0;
  overflow: hidden;
}
.side-terminal-line { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.st-prompt { color: #7fffb0; margin-right: 4px; }
.st-out { color: #8dffc4; opacity: 0.85; }
.st-cursor { animation: st-blink 1s step-end infinite; }
@keyframes st-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .st-cursor { animation: none; opacity: 1; }
}
html[data-theme="light"] .side-terminal { background: #04140a; border-color: rgba(45, 232, 160, 0.5); }
.chaos-sticker.side-spark { top: -18px; left: 46%; --cs-rot: 0deg; }

.page-head { position: relative; }
/* Subtle ambient echo of the avatar ring's purple/cyan/green glow
   (see .avatar-ring::after) -- "around different things," per
   Harper's request to spread that glow elsewhere in quieter ways.
   Much dimmer and slower than the avatar's version, sitting behind
   every gated content page's heading rather than looping in your
   face. z-index -1 keeps it behind the heading text itself. */
.page-head::before {
  content: "";
  position: absolute;
  top: -30px; left: -40px;
  width: 220px; height: 140px;
  background: radial-gradient(ellipse, rgba(168,85,247,0.16) 0%, rgba(34,229,255,0.1) 45%, rgba(45,232,160,0) 75%);
  filter: blur(10px);
  z-index: -1;
  animation: page-head-glow-drift 9s ease-in-out infinite;
  pointer-events: none;
}
@keyframes page-head-glow-drift {
  0%, 100% { opacity: 0.6; transform: translateY(0); }
  50%      { opacity: 1; transform: translateY(6px); }
}
@media (prefers-reduced-motion: reduce) {
  .page-head::before { animation: none; }
}
.chaos-sticker.head-1 { top: -10px; right: 3%; --cs-rot: 10deg; }
.chaos-sticker.head-2 { top: 54px; right: 0%; --cs-rot: -6deg; }

@media (max-width: 760px) {
  .chaos-rift.r1 { width: 220px; height: 220px; top: -40px; right: -50px; }
  .chaos-rift.r2 { width: 190px; height: 190px; bottom: -40px; left: -30px; }
  .chaos-rift.r3, .chaos-rift.r5, .chaos-rift.r6, .chaos-sticker.head-2, .fx-flame.head-3, .chaos-sticker.side-spark { display: none; } /* mobile keeps r1/r2 (shrunk) but thins out the extra stickers instead of hiding everything */
}

@media (prefers-reduced-motion: reduce) {
  .chaos-rift,
  .chaos-rift-haze,
  .chaos-rift-crack,
  .chaos-sticker,
  .chaos-sticker.cs-emoji,
  .chaos-sticker.cs-tag,
  .chaos-sticker.cs-fire,
  .fx-flame,
  .fx-flame::before {
    animation: none;
  }
}

/* Inline pin/unpin on blog cards (js/blog.js pinToggle). Only ever
   rendered for an admin browser, so it's styled as a quiet utility
   control rather than something competing with the post itself. */
.blog-pin-ctl { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 6px 0 2px; }
.blog-pin-btn {
  background: var(--panel-2); color: var(--text-muted);
  border: 1px dashed var(--border-strong); border-radius: 999px;
  padding: 4px 12px; font-size: 0.76rem; font-family: var(--font);
  cursor: pointer; transition: color 0.18s, border-color 0.18s, background 0.18s;
}
.blog-pin-btn:hover { color: var(--text); border-color: var(--purple-bright); background: rgba(168,85,247,0.12); }
.blog-pin-btn.is-pinned {
  color: var(--amber); border-style: solid; border-color: rgba(255, 204, 102, 0.5);
  background: rgba(255, 204, 102, 0.1);
}
.blog-pin-btn:disabled { opacity: 0.55; cursor: default; }
.blog-pin-msg { font-size: 0.76rem; color: var(--text-dim); }
.blog-pin-msg.is-bad { color: var(--pink-bright); }

/* ============================================================
   Littlespace section (about.html #little)
   ------------------------------------------------------------
   The one part of this site that is allowed to be soft. Everything
   else is CRT purple and sharp edges; this section is pink, rounded,
   and gently animated on purpose, because a wall of neon terminal
   styling is the wrong frame for the paragraph inside it. Harper asked
   for "princesses, Bluey, cute, cuddly, stars, hearts, feminine little
   girl energy" -- so it reads as a different room in the same house
   rather than a different house.

   Decorative glyphs are all aria-hidden and carry no information the
   text doesn't already state, so a screen reader gets the paragraphs
   and none of the confetti.
   ============================================================ */
.little-spoiler {
  margin: 34px 0 10px;
  border: 1px solid rgba(255, 158, 226, 0.42);
  border-radius: 22px;
  background:
    radial-gradient(90% 120% at 0 0, rgba(255, 108, 202, 0.13), transparent 65%),
    linear-gradient(135deg, rgba(88, 40, 106, 0.42), rgba(27, 16, 42, 0.88));
  box-shadow: 0 14px 40px -20px rgba(255, 94, 203, 0.55);
  overflow: hidden;
}
.little-spoiler > summary {
  position: relative;
  display: grid;
  gap: 5px;
  padding: 22px 58px 22px 24px;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.little-spoiler > summary::-webkit-details-marker { display: none; }
.little-spoiler > summary::after {
  content: "+";
  position: absolute;
  top: 50%;
  right: 22px;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  transform: translateY(-50%);
  border: 1px solid rgba(255, 198, 235, 0.62);
  border-radius: 50%;
  color: #ffd3ef;
  background: rgba(255, 158, 226, 0.10);
  font-size: 1.35rem;
  line-height: 1;
}
.little-spoiler[open] > summary::after { content: "−"; }
.little-spoiler > summary:hover,
.little-spoiler > summary:focus-visible {
  background: rgba(255, 168, 223, 0.08);
}
.little-spoiler > summary:focus-visible {
  outline: 2px solid #ff9ede;
  outline-offset: -3px;
}
.little-spoiler-kicker {
  color: #ff9ede;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}
.little-spoiler-title {
  color: #ffd4f1;
  font-family: var(--font-display, var(--font));
  font-size: clamp(1.1rem, 3vw, 1.45rem);
  font-weight: 700;
  line-height: 1.25;
}
.little-spoiler-kindness {
  color: rgba(248, 233, 247, 0.76);
  font-size: 0.86rem;
}
.little-spoiler-body {
  padding: 0 20px 20px;
  border-top: 1px solid rgba(255, 158, 226, 0.22);
}
.little-spoiler-body > p:first-child {
  margin: 20px 6px 0;
  color: #f8e9f7;
}
.little-spoiler .littlespace { margin-top: 22px; }

.littlespace {
  position: relative;
  margin: 34px 0 10px;
  padding: 26px 26px 24px;
  border-radius: 26px;
  border: 2px solid rgba(255, 158, 226, 0.52);
  background:
    radial-gradient(120% 90% at 12% 0%, rgba(255, 140, 214, 0.26), transparent 62%),
    radial-gradient(110% 80% at 92% 8%, rgba(186, 128, 255, 0.22), transparent 64%),
    linear-gradient(180deg, rgba(255, 196, 232, 0.13), rgba(255, 196, 232, 0.045));
  box-shadow: 0 12px 40px -12px rgba(255, 94, 203, 0.45), inset 0 1px 0 rgba(255, 220, 244, 0.16);
  overflow: hidden;
}
.littlespace > h2 {
  margin-top: 0;
  color: #ffd0f0;
  text-shadow: 0 0 18px rgba(255, 120, 205, 0.45);
  font-family: var(--font-display, var(--font));
}
.littlespace-crown { font-size: 0.85em; }
.littlespace p { color: #f8e9f7; }
.littlespace p b { color: #ffd9f0; }
.littlespace a { color: #ff9ede; }

/* Drifting stars and hearts along the top edge. Absolutely positioned
   inside the section's overflow:hidden so they can never push layout
   around or spill onto the pages either side. */
.littlespace-stars {
  position: absolute; inset: 0 0 auto 0; height: 66px;
  pointer-events: none; user-select: none;
}
.littlespace-stars span {
  position: absolute; top: 10px;
  font-size: 0.95rem; color: rgba(255, 190, 232, 0.95);
  text-shadow: 0 0 8px rgba(255, 140, 214, 0.6);
  animation: littlespace-float 7s ease-in-out infinite;
}
.littlespace-stars span:nth-child(1) { left: 6%;  animation-delay: 0s;    font-size: 0.9rem; }
.littlespace-stars span:nth-child(2) { left: 19%; animation-delay: -1.1s; }
.littlespace-stars span:nth-child(3) { left: 32%; animation-delay: -2.3s; }
.littlespace-stars span:nth-child(4) { left: 45%; animation-delay: -3.4s; font-size: 0.68rem; }
.littlespace-stars span:nth-child(5) { left: 58%; animation-delay: -4.2s; }
.littlespace-stars span:nth-child(6) { left: 71%; animation-delay: -5.1s; font-size: 0.9rem; }
.littlespace-stars span:nth-child(7) { left: 84%; animation-delay: -6s;   }
.littlespace-stars span:nth-child(8) { left: 93%; animation-delay: -2.8s; font-size: 0.68rem; }
@keyframes littlespace-float {
  0%, 100% { transform: translateY(0) rotate(-6deg);  opacity: 0.5; }
  50%      { transform: translateY(9px) rotate(8deg); opacity: 0.95; }
}

/* Harper + Wolfie, framed as a polaroid stuck to the page at a slight
   angle. Small on purpose: the source is a 165x220 phone photo that the
   chat upload had already compressed, so it is displayed at roughly its
   native size rather than blown up into a soft, blurry hero image. A
   snapshot pinned in a scrapbook is the right register for this section
   anyway. Swap in a full-resolution original and the width can grow. */
.littlespace-photo {
  float: right;
  margin: 2px 0 16px 22px;
  shape-outside: margin-box;
  padding: 10px 10px 0;
  background: linear-gradient(180deg, #fdf4fb, #f6e6f4);
  border-radius: 6px;
  box-shadow: 0 10px 26px -8px rgba(0, 0, 0, 0.6), 0 0 22px -6px rgba(255, 94, 203, 0.45);
  transform: rotate(2.2deg);
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  max-width: 185px;
}
.littlespace-photo:hover { transform: rotate(-1deg) scale(1.04); }
.littlespace-photo img {
  display: block; width: 165px; height: auto; border-radius: 3px;
}
.littlespace-photo figcaption {
  padding: 9px 2px 11px;
  font-size: 0.72rem; line-height: 1.4; text-align: center;
  color: #6b2f5c;
  font-family: var(--font-display);
}
@media (max-width: 560px) {
  .littlespace-photo { float: none; margin: 4px auto 18px; transform: rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .littlespace-photo, .littlespace-photo:hover { transform: none; transition: none; }
}

/* Favourite-things cards. Squishy on hover rather than the sharp
   translateY lift the rest of the site's cards use. */
.littlespace-faves {
  clear: both;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
  gap: 12px; margin-top: 20px;
}
.littlespace-faves .lf {
  background: rgba(255, 196, 232, 0.12);
  border: 1px solid rgba(255, 158, 226, 0.45);
  border-radius: 18px;
  padding: 14px 10px;
  text-align: center;
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.25s, border-color 0.25s;
}
.littlespace-faves .lf:hover {
  transform: scale(1.06) rotate(-1.5deg);
  background: rgba(255, 196, 232, 0.22);
  border-color: rgba(255, 178, 236, 0.85);
  box-shadow: 0 8px 20px -8px rgba(255, 94, 203, 0.55);
}
.littlespace-faves .lf span { display: block; font-size: 1.7rem; line-height: 1; margin-bottom: 7px; }
.littlespace-faves .lf b { display: block; color: #ffd9f0; font-size: 0.9rem; }
.littlespace-faves .lf i { display: block; color: rgba(243, 230, 244, 0.62); font-size: 0.74rem; font-style: normal; margin-top: 3px; }

html[data-theme="light"] .littlespace { border-color: rgba(214, 51, 156, 0.35); }
html[data-theme="light"] .little-spoiler {
  border-color: rgba(214, 51, 156, 0.34);
  background: linear-gradient(135deg, #fff4fb, #f6eafb);
}
html[data-theme="light"] .little-spoiler-title { color: #8d175f; }
html[data-theme="light"] .little-spoiler-kicker { color: #b51678; }
html[data-theme="light"] .little-spoiler-kindness,
html[data-theme="light"] .little-spoiler-body > p:first-child { color: #5b344f; }
html[data-theme="light"] .little-spoiler > summary::after {
  color: #9f1b6c;
  border-color: rgba(159, 27, 108, 0.38);
}
html[data-theme="light"] .littlespace > h2 { color: #a8176f; }
html[data-theme="light"] .littlespace p { color: #4a2140; }
html[data-theme="light"] .littlespace p b { color: #7d1152; }
html[data-theme="light"] .littlespace a { color: #c01a7d; }
html[data-theme="light"] .littlespace-faves .lf b { color: #7d1152; }
html[data-theme="light"] .littlespace-faves .lf i { color: rgba(74, 33, 64, 0.7); }
html[data-theme="light"] .littlespace-stars span { color: rgba(200, 40, 145, 0.6); }

@media (prefers-reduced-motion: reduce) {
  .littlespace-stars span { animation: none; opacity: 0.7; }
  .littlespace-faves .lf:hover { transform: none; }
}

/* ============================================================
   Writing stats (writing.html) -- counted from Harper's own
   Facebook/Instagram data exports. Aggregate figures only; the page
   text itself states what is deliberately not counted and why.
   ============================================================ */
.wstat-hero {
  text-align: center; margin: 22px 0 18px; padding: 26px 18px;
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  background:
    radial-gradient(110% 90% at 50% 0%, rgba(168, 85, 247, 0.16), transparent 65%),
    var(--panel);
}
.wstat-big {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(2.6rem, 9vw, 4.4rem); line-height: 1;
  background: linear-gradient(120deg, var(--purple-bright), var(--cyan), var(--pink-bright));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.wstat-cap { margin-top: 6px; font-size: 1rem; color: var(--text); letter-spacing: 0.02em; }
.wstat-sub { margin-top: 4px; font-size: 0.84rem; color: var(--text-dim); }

.wstat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin: 18px 0 6px; }
.wstat {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 15px 14px; text-align: center;
  transition: transform 0.2s ease, border-color 0.2s ease;
}
.wstat:hover { transform: translateY(-3px); border-color: rgba(168, 85, 247, 0.5); }
.wstat b {
  display: block; font-family: var(--font-display); font-size: 1.75rem;
  color: var(--purple-bright); line-height: 1.1;
}
.wstat span { display: block; margin-top: 7px; font-size: 0.79rem; color: var(--text-dim); line-height: 1.45; }

@media (prefers-reduced-motion: reduce) { .wstat:hover { transform: none; } }

/* Obsession / favourite-RPG cards (gaming.html). Same shape as .wstat but
   left-aligned with a title-plus-note pairing, since these are names with
   a remark attached rather than a figure with a label under it. */
.obsession-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(215px, 1fr)); gap: 12px; margin: 16px 0 8px; }
.obsession {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 13px 15px;
  border-left: 3px solid var(--purple-bright);
  transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.obsession:hover { transform: translateY(-3px); background: var(--panel-2); border-color: rgba(168,85,247,0.45); border-left-color: var(--cyan); }
.obsession b { display: block; color: var(--text); font-size: 0.95rem; }
.obsession span { display: block; margin-top: 4px; color: var(--text-dim); font-size: 0.79rem; line-height: 1.45; }
@media (prefers-reduced-motion: reduce) { .obsession:hover { transform: none; } }

/* ============================================================
   Favourite games (gaming.html #favgames / js/favgames.js)
   ============================================================ */
.fg-cat { margin-top: 26px; }
.fg-cat-head {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 4px; font-size: 1.12rem; color: var(--text);
}
.fg-ico { font-size: 1.15rem; }
.fg-count {
  margin-left: auto; font-family: var(--font); font-size: 0.72rem; font-weight: 700;
  color: var(--purple-bright); background: rgba(168, 85, 247, 0.13);
  border-radius: 999px; padding: 2px 10px;
}
.fg-blurb { margin: 0 0 12px; color: var(--text-dim); font-size: 0.86rem; max-width: 760px; }

/* align-items:start stops a single long quote (Guild Wars 2 is 150 words)
   from stretching every other card in its row to match, which left big
   empty boxes next to one-line entries. */
.fg-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); gap: 12px; align-items: start; }
.fg-card {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm);
  border-left: 3px solid var(--purple-bright);
  padding: 13px 15px; display: flex; flex-direction: column; gap: 8px;
  transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.fg-card:hover { transform: translateY(-3px); background: var(--panel-2); border-left-color: var(--cyan); }
.fg-name { margin: 0; font-size: 0.95rem; color: var(--text); font-family: var(--font); font-weight: 700; }
.fg-note { margin: 0; font-size: 0.81rem; color: var(--text-dim); line-height: 1.5; }

/* Her own words get quote styling so it is never ambiguous which lines
   are hers from the time and which are commentary written for this page. */
.fg-quote {
  margin: 0; padding: 9px 12px;
  border-left: 2px solid rgba(34, 229, 255, 0.55);
  background: rgba(34, 229, 255, 0.055);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.fg-quote p { margin: 0; font-size: 0.82rem; line-height: 1.55; color: var(--text); font-style: italic; }
.fg-cite { display: block; margin-top: 6px; font-size: 0.72rem; color: var(--cyan); font-style: normal; }

.fg-trailer { margin-top: auto; font-size: 0.76rem; color: var(--text-dim); align-self: flex-start; }
.fg-trailer:hover { color: var(--purple-bright); }

@media (prefers-reduced-motion: reduce) { .fg-card:hover { transform: none; } }

/* ============================================================
   Streamers (streamers.html / js/streamers.js)
   ============================================================ */
.tw-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; align-items: start; margin-top: 16px; }
.tw-card {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  border-left: 3px solid #9146ff; /* Twitch purple, the one place it is correct to use */
  padding: 15px 16px; display: flex; flex-direction: column; gap: 9px;
  transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.tw-card:hover { transform: translateY(-3px); background: var(--panel-2); border-color: rgba(145, 70, 255, 0.5); }
.tw-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.tw-name { margin: 0; font-size: 1rem; color: var(--text); font-family: var(--font); font-weight: 700; }
.tw-handle { font-size: 0.74rem; color: var(--text-dim); }
.tw-live {
  margin-left: auto; font-size: 0.66rem; font-weight: 800; letter-spacing: 0.08em;
  color: #fff; background: #e01b3d; border-radius: 999px; padding: 3px 9px;
  animation: tw-live-pulse 2.4s ease-in-out infinite;
}
@keyframes tw-live-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(224, 27, 61, 0.55); }
  50%      { box-shadow: 0 0 0 6px rgba(224, 27, 61, 0); }
}
.tw-card.is-live { border-left-color: #e01b3d; }
.tw-now { margin: 0; font-size: 0.78rem; color: var(--text); }
.tw-status { margin: 6px 0 0; font-size: 0.82rem; color: var(--green); }
.tw-note { margin: 0; font-size: 0.82rem; color: var(--text-dim); line-height: 1.5; }
.tw-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 2px; }
.tw-load { padding: 5px 13px; font-size: 0.8rem; }
.tw-link { font-size: 0.76rem; color: var(--text-dim); }
.tw-link:hover { color: #b189ff; }

/* 16:9 via padding-top, matching .artist-embed
   rather than introducing a third way of doing the same thing. */
.tw-player { position: relative; width: 100%; padding-top: 56.25%; border-radius: var(--radius-sm); overflow: hidden; background: #000; margin-top: 4px; }
.tw-player iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.tw-player .tw-msg {
  position: absolute; inset: 0; margin: 0; display: flex; align-items: center;
  padding: 18px; text-align: center; font-size: 0.8rem; color: var(--text-dim);
  background: var(--panel-2);
}

/* Spotlight row -- Harper's top 3, pulled out above the full grid.
   Same card shape, wider columns and a warmer border so it reads as
   "featured" rather than just "first in the list." */
.tw-spotlight-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin: 14px 0 26px; }
.tw-card--spot {
  border-color: rgba(255, 94, 203, 0.5);
  background: linear-gradient(150deg, rgba(168, 85, 247, 0.12), var(--panel) 55%);
  box-shadow: 0 0 0 1px rgba(255, 94, 203, 0.12) inset;
}
.tw-card--spot:hover { border-color: rgba(255, 94, 203, 0.75); }
.tw-spot-badge {
  font-size: 0.66rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase;
  color: #ff5ecb; background: rgba(255, 94, 203, 0.14); border-radius: 999px; padding: 3px 9px;
}

@media (prefers-reduced-motion: reduce) { .tw-card:hover { transform: none; } .tw-live { animation: none; } }

/* ============================================================
   Floating page reactions (js/reactions.js #rx-float)
   ------------------------------------------------------------
   Bottom-RIGHT on purpose: .guide-recall already owns bottom-left.
   Only ever rendered on pages with no inline .reactions strip, so the
   same counter is never on screen twice.
   ============================================================ */
.rx-float {
  position: fixed;
  right: 18px; bottom: 18px;
  z-index: 55;
  display: flex; gap: 6px;
  padding: 6px;
  border-radius: 999px;
  background: rgba(21, 14, 30, 0.82);
  border: 1px solid var(--border);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 6px 22px -6px rgba(0, 0, 0, 0.6);
}
.rx-float .rx-row { display: flex; gap: 6px; margin: 0; }
.rx-float .rx-btn {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 7px 12px; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--border);
  color: var(--text-muted); font-family: var(--font); font-size: 0.8rem;
  cursor: pointer; transition: color 0.18s, border-color 0.18s, background 0.18s;
}
.rx-float .rx-btn:hover { color: var(--text); border-color: var(--purple-bright); background: rgba(168,85,247,0.14); }
.rx-float .rx-btn.is-mine, .rx-float .rx-btn[aria-pressed="true"] {
  color: var(--text); border-color: rgba(168,85,247,0.55); background: rgba(168,85,247,0.18);
}
/* Love keeps its pink accent floating too, matching the inline strip's
   .rx-btn.rx-love.is-mine -- otherwise every active reaction reads as
   the same generic purple and Love loses the thing that makes it Love. */
.rx-float .rx-btn.rx-love.is-mine, .rx-float .rx-btn.rx-love[aria-pressed="true"] {
  color: var(--pink); border-color: rgba(255,143,208,0.55); background: rgba(255,94,203,0.16);
}
.rx-float .rx-btn .rx-label { display: none; }   /* icon + count only; the label stays for screen readers via aria-label */
.rx-float .rx-count { font-size: 0.76rem; font-weight: 700; color: var(--text); }

/* Both widgets occupy the bottom-right corner on desktop. Keep the
   reaction pill immediately to the left of Ask Me Anything, with a real
   gap rather than relying on their z-indexes to hide the collision. */
@media (min-width: 701px) {
  body:has(.askme-wrap) .rx-float { right: 196px; bottom: 22px; }
  body:has(.askme-wrap.is-open) .rx-float {
    opacity: 0;
    pointer-events: none;
  }
}

@media (max-width: 640px) {
  .rx-float {
    right: max(10px, env(safe-area-inset-right));
    bottom: max(10px, env(safe-area-inset-bottom));
    min-height: 44px;
  }
  .rx-float .rx-btn { padding: 6px 10px; }
}
@media print { .rx-float { display: none; } }

/* ---------- Image lightbox ----------
   Click any content image to see it at its real size. Driven by
   js/lightbox.js, which decides what counts as a content image; this
   file only handles what it looks like once it has decided.

   The zoom affordance is a cursor change plus a hover lift, not a
   permanent badge in the corner: on a page with eight photos, eight
   badges is clutter, and the pointer already answers the question the
   moment you go near one. */
.lb-zoomable {
  cursor: zoom-in;
  transition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease;
}
.lb-zoomable:hover,
.lb-zoomable:focus-visible {
  transform: scale(1.015);
  box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.6),
              0 0 0 1px rgba(168, 85, 247, 0.5);
  filter: brightness(1.05);
}
.lb-zoomable:focus-visible {
  outline: 2px solid var(--purple-bright, #a855f7);
  outline-offset: 3px;
}

/* Scroll lock. position: fixed on the body would jump the page back to
   the top on close; overflow: hidden holds the reading position. */
body.lb-open { overflow: hidden; }

.lb-overlay {
  position: fixed; inset: 0; z-index: 9000;
  display: flex; align-items: center; justify-content: center;
  padding: 40px 64px;
  background: rgba(6, 4, 12, 0.92);
  backdrop-filter: blur(6px);
  animation: lb-in 0.16s ease-out;
}
.lb-overlay[hidden] { display: none; }
@keyframes lb-in { from { opacity: 0; } to { opacity: 1; } }

.lb-figure {
  margin: 0;
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  max-width: 100%; max-height: 100%;
}

.lb-img {
  /* Contained rather than cropped or upscaled: an image smaller than the
     viewport shows at its true size, and a huge one shrinks to fit. Both
     are honest about what the file actually is, which is the whole point
     of opening it. */
  max-width: 100%;
  max-height: calc(100vh - 130px);
  width: auto; height: auto;
  border-radius: 10px;
  box-shadow: 0 24px 70px -20px rgba(0, 0, 0, 0.9),
              0 0 0 1px rgba(168, 85, 247, 0.35);
  opacity: 0;
  transform: scale(0.98);
  transition: opacity 0.2s ease, transform 0.2s ease;
  background: rgba(255, 255, 255, 0.03);
}
.lb-img.lb-ready { opacity: 1; transform: scale(1); }

.lb-cap {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  justify-content: center; text-align: center;
  max-width: 70ch;
  font-size: 0.88rem;
  color: var(--text-muted, #b9aecd);
}
.lb-meta {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.76rem;
  color: rgba(168, 85, 247, 0.85);
  white-space: nowrap;
}

.lb-close,
.lb-nav {
  position: absolute;
  display: flex; align-items: center; justify-content: center;
  background: rgba(20, 14, 32, 0.85);
  border: 1px solid rgba(168, 85, 247, 0.45);
  color: #efe6ff;
  border-radius: 10px;
  cursor: pointer;
  line-height: 1;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.lb-close:hover,
.lb-nav:hover {
  background: rgba(60, 30, 100, 0.95);
  border-color: rgba(199, 146, 255, 0.9);
}
.lb-close:focus-visible,
.lb-nav:focus-visible {
  outline: 2px solid var(--purple-bright, #a855f7);
  outline-offset: 2px;
}
.lb-close {
  top: 16px; right: 18px;
  width: 42px; height: 42px;
  font-size: 1.6rem;
}
.lb-nav {
  top: 50%; transform: translateY(-50%);
  width: 44px; height: 76px;
  font-size: 2rem;
}
.lb-nav:hover { transform: translateY(-50%) scale(1.05); }
.lb-prev { left: 14px; }
.lb-next { right: 14px; }
.lb-nav[hidden] { display: none; }

@media (max-width: 760px) {
  /* The arrows would sit on top of the picture at this width, so they
     move under it and the padding comes off the sides. Swiping is not
     wired up on purpose: it would fight the browser's own back gesture. */
  .lb-overlay { padding: 56px 12px 12px; }
  .lb-img { max-height: calc(100vh - 190px); }
  .lb-nav { top: auto; bottom: 12px; transform: none; width: 56px; height: 44px; }
  .lb-nav:hover { transform: none; }
  .lb-prev { left: 20%; }
  .lb-next { right: 20%; }
}

@media (prefers-reduced-motion: reduce) {
  .lb-overlay { animation: none; }
  .lb-zoomable { transition: none; }
  .lb-zoomable:hover, .lb-zoomable:focus-visible { transform: none; }
  .lb-img { transition: none; transform: none; opacity: 1; }
  .lb-img.lb-ready { transform: none; }
  .lb-nav:hover { transform: translateY(-50%); }
}

/* ============================================================
   Game Servers hub (servers.html) -- 2026-07-27
   ------------------------------------------------------------
   Hytale, Enshrouded, Palworld and (soon) Minecraft, combined into one
   page with a button-driven tab switch, same pattern js/work.js already
   uses for "work request vs friendship application." Reuses
   .hy-banner/.status-grid/.panel/.tags/.callout/.value-grid wholesale
   (they used to belong to hytale.html/enshrouded.html, now folded in
   here) -- only the tab bar itself and the per-game accent wash below
   are new.
   ============================================================ */

.gs-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 24px;
}

.gs-tab {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 18px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel);
  color: var(--text-muted);
  font: inherit;
  font-weight: 600;
  font-size: 0.92rem;
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease, transform 0.18s ease;
}
.gs-tab:hover { border-color: var(--border-strong); color: var(--text); transform: translateY(-1px); }
.gs-tab:focus-visible { outline: 2px solid var(--purple-bright); outline-offset: 2px; }
.gs-tab.is-active {
  background: var(--grad-brand);
  color: #fff;
  border-color: transparent;
  box-shadow: 0 6px 24px rgba(168,85,247,0.35);
}
.gs-tab-ico { font-size: 1.15rem; line-height: 1; }

/* The status chip is always visible (dot + short word), not hidden
   behind hover -- "is this server up" should never require an extra
   interaction to find out. The tooltip below is a bonus for mouse
   users layered on top of that, not a replacement for it. */
.gs-tab-status { display: inline-flex; align-items: center; gap: 6px; position: relative; margin-left: 4px; padding-left: 10px; border-left: 1px solid rgba(255,255,255,0.18); }
.gs-tab.is-active .gs-tab-status { border-left-color: rgba(255,255,255,0.4); }
.gs-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim); flex-shrink: 0; }
.gs-dot.online   { background: var(--green); box-shadow: 0 0 8px rgba(45,232,160,0.7); }
.gs-dot.offline  { background: #f87171; box-shadow: 0 0 8px rgba(248,113,113,0.5); }
.gs-dot.unknown  { background: var(--text-dim); }
.gs-dot.checking { background: var(--amber); animation: gs-dot-pulse 1.4s ease-in-out infinite; }
.gs-status-text { font-size: 0.74rem; font-weight: 600; opacity: 0.9; white-space: nowrap; }
@keyframes gs-dot-pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }

/* Hover/focus tooltip on the status chip -- a bonus for mouse and
   keyboard-focus users, deliberately not load-bearing for
   accessibility: the dot + status-text above are already always
   visible, and full detail (player count, uptime) is one activation
   away by just opening that tab. aria-hidden isn't needed here since
   the tooltip carries genuinely extra detail rather than duplicating
   the always-visible text, but it stays non-interactive (no focusable
   children of its own) so it can never trap keyboard focus. */
.gs-tooltip {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  min-width: 170px;
  padding: 8px 12px;
  border-radius: 10px;
  background: var(--panel-solid);
  border: 1px solid var(--border-strong);
  color: var(--text);
  font-size: 0.76rem;
  font-weight: 500;
  text-align: center;
  line-height: 1.4;
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
  z-index: 5;
}
.gs-tooltip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: var(--border-strong);
}
.gs-tab-status:hover .gs-tooltip,
.gs-tab-status:focus-within .gs-tooltip {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

.gs-soon-pill {
  font-size: 0.72rem;
  font-weight: 700;
  padding: 4px 10px;
  border-radius: 20px;
  background: rgba(255, 94, 203, 0.14);
  color: var(--pink-bright);
  border: 1px dashed rgba(255, 94, 203, 0.5);
  white-space: nowrap;
}
.gs-soon-pill-lg { font-size: 0.85rem; padding: 7px 16px; }
.gs-tab-soon { opacity: 0.85; }
.gs-tab-soon:hover { opacity: 1; }

.gs-panel { display: none; }
.gs-panel.is-active { display: block; animation: gs-panel-in 0.25s ease; }
@keyframes gs-panel-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
.gs-panel:focus-visible { outline: 2px solid var(--purple-bright); outline-offset: 4px; border-radius: 8px; }

/* Per-game accent wash. Each banner keeps the exact same .hy-banner
   shape and just retints its own top-right glow and address colour --
   same "quiet everywhere, tinted where it matters" idea as --k-glow
   elsewhere in this file, scoped locally since these four colours only
   ever apply inside this one page. Hytale keeps the original cyan
   un-overridden, since that was .hy-banner's default all along. */
.gs-banner-enshrouded::before { background: radial-gradient(50% 90% at 80% 20%, rgba(255,204,102,0.25), transparent 60%); }
.gs-banner-enshrouded .addr { color: var(--amber); }
.gs-banner-palworld::before { background: radial-gradient(50% 90% at 80% 20%, rgba(180,255,57,0.22), transparent 60%); }
.gs-banner-palworld .addr { color: var(--lime); }
/* Solid border now that this panel is live, not a placeholder -- the
   dashed border here was this tab's "coming soon" signal, and it
   stopped meaning that the day the Fabric server actually shipped. */
.gs-banner-minecraft::before { background: radial-gradient(50% 90% at 80% 20%, rgba(45,232,160,0.22), transparent 60%); }
.gs-banner-minecraft .addr { color: var(--green); }

.gs-cross-links { margin-top: 22px; }

@media (max-width: 640px) {
  .gs-tabs { gap: 8px; }
  .gs-tab { padding: 10px 14px; font-size: 0.85rem; }
  .gs-tab-status { padding-left: 8px; }
  /* Word drops, dot stays -- full status text is still one tap away by
     opening that tab, so nothing is actually lost at this width. */
  .gs-status-text { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .gs-tab { transition: none; }
  .gs-tab:hover { transform: none; }
  .gs-dot.checking { animation: none; }
  .gs-panel.is-active { animation: none; }
  .gs-tooltip { transition: none; }
}

/* ------------------------------------------------------------
   Quick-reference table (2026-07-28) -- first table on the site.
   Built for cannabis.html's cleaning section (glance-then-detail:
   this table up top, the existing .safety-grid cards below it for
   whoever wants the actual method), but scoped generically so any
   future page needing a real "item / frequency / don't" reference
   can reuse it without a new component. A real <table> rather than
   another card grid, on purpose -- this content genuinely has rows
   and columns (one item, several attributes each), which is exactly
   what a table is for, and screen readers get real header/cell
   association for free instead of a div soup approximating one.
   ------------------------------------------------------------ */
.ref-table-wrap {
  overflow-x: auto;
  margin: 18px 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
}
.ref-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.87rem;
  min-width: 560px; /* forces the scroll instead of a crush at narrow widths */
}
.ref-table caption {
  text-align: left;
  padding: 14px 16px 0;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.ref-table th, .ref-table td {
  padding: 11px 16px;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--border);
}
.ref-table thead th {
  color: var(--text);
  font-size: 0.78rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--border-strong);
}
.ref-table tbody td { color: var(--text-muted); line-height: 1.45; }
.ref-table tbody td:first-child { color: var(--text); font-weight: 600; white-space: nowrap; }
.ref-table tbody tr:last-child td { border-bottom: none; }
.ref-table tbody tr:hover td { background: rgba(255, 255, 255, 0.03); }
.ref-table td.no { color: var(--pink); }

@media (max-width: 640px) {
  .ref-table { font-size: 0.82rem; }
  .ref-table th, .ref-table td { padding: 9px 12px; }
}

/* ============================================================
   Global signal tuner
   ============================================================ */
.signal-picker { position: relative; }
.signal-picker-floating { position: fixed; left: 16px; bottom: 16px; z-index: 460; }
.signal-picker-floating .signal-toggle {
  background: var(--panel-solid); border: 1px solid var(--border-strong);
  box-shadow: var(--glow-purple), 0 10px 30px rgba(0,0,0,0.42);
}
.signal-popover {
  display: none; z-index: 470; max-height: min(620px, calc(100vh - 24px)); overflow: auto;
  padding: 16px; color: var(--text); background: color-mix(in srgb, var(--panel-solid) 94%, #000 6%);
  border: 1px solid var(--border-strong); border-radius: 15px;
  box-shadow: 0 20px 65px rgba(0,0,0,0.58), var(--glow-purple);
}
.signal-popover.show { display: block; }
.signal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.signal-head strong { display: block; margin-top: 2px; font-family: var(--font-display); font-size: 1.08rem; }
.signal-kicker { display: block; color: var(--cyan); font: 700 0.65rem/1 var(--font); letter-spacing: 0.14em; }
.signal-close {
  width: 34px; height: 34px; flex: 0 0 auto; border-radius: 9px; border: 1px solid var(--border);
  color: var(--text-muted); background: var(--panel-2); font-size: 1.25rem; cursor: pointer;
}
.signal-intro { margin: 10px 0 13px; color: var(--text-dim); font-size: 0.78rem; line-height: 1.45; }
.signal-options { display: grid; gap: 7px; }
.signal-option {
  display: grid; grid-template-columns: 36px 1fr; align-items: center; gap: 9px;
  width: 100%; padding: 9px 10px; text-align: left; color: var(--text);
  border: 1px solid transparent; border-radius: 11px; background: rgba(255,255,255,0.025); cursor: pointer;
}
.signal-option:hover { background: var(--panel-2); border-color: var(--border); }
.signal-option.is-active {
  border-color: var(--purple-bright);
  background: linear-gradient(110deg, rgba(168,85,247,0.18), rgba(255,94,203,0.08));
  box-shadow: inset 3px 0 var(--pink-bright);
}
.signal-option-icon { display: grid; place-items: center; font-size: 1.3rem; }
.signal-option strong { display: block; font-size: 0.82rem; }
.signal-option small { display: block; color: var(--text-dim); font-size: 0.7rem; line-height: 1.3; }
.signal-foot { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); color: var(--text-dim); font-size: 0.72rem; }
.signal-current { color: var(--text); font-weight: 700; }

html[data-signal="soft"] {
  --bg: #100817; --bg-2: #180c22; --panel: rgba(38,19,49,0.78); --panel-solid: #24122f;
  --panel-2: rgba(74,36,85,0.5); --border: rgba(244,154,220,0.25); --border-strong: rgba(255,140,213,0.58);
  --purple: #9c62dc; --purple-bright: #c995ff; --pink: #ef63b7; --pink-bright: #ff9bd8;
  --cyan: #8de8ff; --green: #8ae6c0; --grad-aurora: linear-gradient(160deg, #291137 0%, #120a1d 48%, #35122c 100%);
  --glow-purple: 0 0 34px rgba(201,149,255,0.34); --glow-pink: 0 0 34px rgba(255,155,216,0.3);
}
html[data-signal="masc"] {
  --bg: #030b0d; --bg-2: #071315; --panel: rgba(7,27,29,0.8); --panel-solid: #091c1e;
  --panel-2: rgba(13,48,48,0.58); --border: rgba(52,190,175,0.22); --border-strong: rgba(53,211,190,0.5);
  --purple: #277e8c; --purple-bright: #4fb3c2; --pink: #4da987; --pink-bright: #79d6b1;
  --cyan: #54d9ef; --green: #46d48e; --grad-aurora: linear-gradient(160deg, #061c1b 0%, #030a0d 50%, #071b25 100%);
  --glow-purple: 0 0 25px rgba(79,179,194,0.3); --glow-pink: 0 0 24px rgba(70,212,142,0.24);
}
html[data-signal="masc"] .card, html[data-signal="masc"] .panel,
html[data-signal="masc"] .page-head { border-radius: 8px; }
html[data-signal="rift"] {
  --bg: #060811; --bg-2: #0a0c19; --panel: rgba(17,17,35,0.8); --panel-solid: #101326;
  --panel-2: rgba(28,31,58,0.62); --border: rgba(90,214,216,0.25); --border-strong: rgba(244,92,194,0.55);
  --purple: #765dde; --purple-bright: #a27cff; --pink: #e841ad; --pink-bright: #ff6fc5;
  --cyan: #42e2ef; --green: #4bdaa0; --grad-brand: linear-gradient(90deg, #43d8dd 0%, #8581ff 48%, #ff69bf 100%);
  --grad-aurora: linear-gradient(135deg, #062126 0%, #0b0b1a 47%, #2a0b23 100%);
}
html[data-signal="rift"] .page-head, html[data-signal="rift"] .hero {
  box-shadow: -14px 0 34px rgba(66,226,239,0.08), 14px 0 34px rgba(255,111,197,0.08);
}
html[data-signal="feral"] {
  --bg: #070300; --bg-2: #130600; --panel: rgba(24,8,20,0.8); --panel-solid: #1d0718;
  --panel-2: rgba(68,13,56,0.6); --border: rgba(180,255,57,0.3); --border-strong: rgba(255,40,181,0.66);
  --purple: #8428ff; --purple-bright: #c472ff; --pink: #ff199f; --pink-bright: #ff4fbd;
  --cyan: #18eaff; --green: #84ff37;
  --grad-brand: linear-gradient(100deg, #84ff37, #18eaff 28%, #a230ff 58%, #ff199f 82%, #ffcc00);
  --grad-aurora: linear-gradient(145deg, #1b0700 0%, #080008 42%, #200026 72%, #062014 100%);
  --glow-purple: 0 0 36px rgba(196,114,255,0.52); --glow-pink: 0 0 38px rgba(255,25,159,0.5);
  --glow-cyan: 0 0 34px rgba(24,234,255,0.48);
}
html[data-signal="feral"] #staticGlitch { opacity: 0.14; }
html[data-signal="feral"] .card, html[data-signal="feral"] .project-card,
html[data-signal="feral"] .rail-card { transform: rotate(-0.15deg); }
html[data-signal="quiet"] {
  --bg: #0d0c12; --bg-2: #111018; --panel: rgba(25,23,31,0.9); --panel-solid: #19171f;
  --panel-2: rgba(45,41,53,0.62); --border: rgba(190,180,204,0.18); --border-strong: rgba(190,180,204,0.38);
  --purple: #827694; --purple-bright: #afa1c2; --pink: #a77f9d; --pink-bright: #caa5c1;
  --cyan: #9bbbc2; --green: #93b3a5; --grad-brand: linear-gradient(120deg, #afa1c2, #caa5c1);
  --grad-aurora: linear-gradient(160deg, #17141d, #0d0c12 55%, #18151c); scroll-behavior: auto;
}
html[data-signal="quiet"] *, html[data-signal="quiet"] *::before, html[data-signal="quiet"] *::after {
  animation-duration: 0.001ms !important; animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important; scroll-behavior: auto !important;
}
html[data-signal="quiet"] #starfield, html[data-signal="quiet"] #staticGlitch,
html[data-signal="quiet"] #chaosRifts, html[data-signal="quiet"] .chaos-sticker,
html[data-signal="quiet"] .brat-sticker, html[data-signal="quiet"] .fx-flame { display: none !important; }
html[data-signal="quiet"] body { background-image: none; }

@media (max-width: 720px) {
  .signal-popover { padding: 14px; }
  .signal-option { padding: 8px 9px; }
  .signal-picker-floating { left: 10px; bottom: 10px; }
}
