:root{
  --void:#17111f;
  --void-2:#211830;
  --line:#3a2d4d;
  --dust:#f6e3c0;
  --muted:#9384a8;
  --accent:#f0a35e;          /* shifts as you advance epochs */
  --accent-soft:#f0a35e33;

  /* one color per epoch, warm to cool — used for connectors and the sheet */
  --epoch-1:#f0a35e;
  --epoch-2:#ff8f6b;
  --epoch-3:#ff6f91;
  --epoch-4:#e8567a;
  --epoch-5:#c06fd0;
  --epoch-6:#a97bf0;
  --epoch-7:#6fd3d1;
  /* 8 is Divergence, the only epoch that hasn't happened yet — so it sits past
     the end of the warm-to-cool run rather than continuing it. */
  --epoch-8:#a9b8ff;

  /* Colour does two jobs on a node, split by which PART of it you're looking at:
       inside  (glyph + fill) = the EPOCH  — identity, variety, 7 colours
       outside (ring + glow)  = the STATE  — owned / affordable / available
     That keeps "what can I afford" a one-glance question while letting the
     tree stop being one flat colour. Locked nodes reveal neither — an
     unknown shouldn't leak which era it belongs to.

     Ordered by how loudly each state should call for attention:
     affordable (act now) > available (soon) > owned (done) > locked. That's
     why owned is the quiet one — it's finished, it shouldn't compete. It's
     also deliberately NOT --epoch-1's hex any more; those two used to be the
     same colour, which collapsed the whole inside/outside distinction for
     the entire first epoch. */
  --state-owned:#6b5f80;
  --state-affordable:#6fd88f;
  --state-available:#6fa8f0;
  --state-available-soft:#6fa8f059;

  /* atmosphere + Firmament palette — kept here rather than inline in rules,
     same as every other colour in the project */
  --fog-warm:#6b4a7a55;
  --fog-cool:#7a5a6b4d;
  --fog-deep:#4a3a5e66;
  --sky-glow:#2a1d3f;        /* the light at the top of the Firmament view */
  --light-bright:#bff3f2;    /* a charted keystone, brighter than --epoch-7 */

  /* ---- the Celestial Canvas ----
     Struck from the EPOCH variables, not from new hex. The first version used
     a literal neon triad (#06b6d4 / #4f46e5 / #9333ea) and it read as a
     different application bolted on: those are cold blues, and this game is
     warm dusk from the Big Bang to Ascension. Three consequences of doing it
     this way, all of them the reason the rest of the stylesheet works like
     this already:
       - it harmonises by construction, because these are the colours the tree
         and the sheet are already using;
       - the Firmament keeps its established identity — --epoch-7 is the teal
         the star map, its portal and its sheet have always been;
       - a palette cosmetic recolours the whole Canvas for free, which a
         hardcoded hex could never do.
     Only the ground is its own value, and it stays in --void's hue family. */
  --canvas-void:#140f1e;     /* the Canvas ground — a shade under --void, same warm hue */
  --canvas-key:var(--epoch-7);    /* the Firmament's own colour: actions, lit state, readouts */
  --canvas-deep:var(--epoch-6);   /* the recessive half of the gradients */
  --canvas-glow:var(--epoch-5);   /* unallocated Light, and the role line */

  /* ---- real stellar colour, by spectral class ----
     O and B stars are genuinely blue-white and M stars genuinely red; these
     are approximate blackbody colours for each class. They live here as data
     rather than in the constellation markup so a star's colour is a property
     of its physics, and both themes get to re-strike them. */
  --spec-o:#9bb0ff;
  --spec-b:#aabfff;
  --spec-a:#cad8ff;
  --spec-f:#f6f4ff;
  --spec-g:#fff4e8;
  --spec-k:#ffd2a1;
  --spec-m:#ff9d6b;
  --scrim:#0a0710;           /* modal/backdrop wash, alpha applied per use */
  /* Note: the starfield dots stay as literal #ffffffNN inside their gradient
     lists. They're decorative alpha-white at nine different opacities, the
     file already used that convention before this, and color-mix() per dot
     would make the gradient unreadable for no gain. */

  /* ---- control sizing ----
     Every interactive thing in the game is at least --tap tall. 44px is the
     floor Apple's HIG and WCAG 2.5.5 both land on, and an audit found almost
     nothing here met it: the account toggle was 24px, the hint's close 29px,
     the quantity buttons 32px, the panel closes 34px, even the HUD buttons
     42px. Small targets aren't merely awkward on a phone, they're the reason
     a tap "does nothing" and gets repeated.

     These are geometry, not colour, so they are deliberately NOT re-struck in
     the light theme — the theme block is for palette only. */
  --tap: 46px;              /* minimum height of anything you can press */
  --hud-btn: clamp(46px, 5vw, 56px);   /* the round rail buttons, bigger on a desktop screen */
  --hud-icon: clamp(20px, 2.2vw, 24px);
}

/* A finger is not a mouse pointer. Where the primary input is coarse, the
   floor goes up rather than down — and the HUD buttons stop scaling with the
   viewport, because a tablet is a big screen you still touch. */
@media (pointer: coarse){
  :root{
    --tap: 48px;
    --hud-btn: 52px;
    --hud-icon: 23px;
  }
}

*{box-sizing:border-box;margin:0;padding:0}

html{-webkit-text-size-adjust:100%}   /* stop iOS auto-zooming the page's text scale */

html, body{height:100%;overflow:hidden}   /* the tree owns scrolling now; the page itself never scrolls */
*{-webkit-tap-highlight-color:transparent}

/* Dragging the tree used to smear a text selection across every node label it
   passed, so the pan read as a highlight instead of a drag. Selection is off
   everywhere by default and switched back on only where text is genuinely
   meant to be read closely, copied, or typed. */
body{-webkit-user-select:none;user-select:none}
input, textarea,
.guide-panel, .codex-flavour, .sheet-flavour, .save-hint,
/* The Canvas is prose a player reads closely — what a constellation does, and
   the real astronomy behind it — sitting on a screen with no map to drag. */
.socket-effect, .socket-detail,
/* A friend code exists to be handed to somebody. The blanket rule above made
   your own code the one string in the game you couldn't copy. */
.friend-code{
  -webkit-user-select:text;user-select:text;
}
/* And on touch, a long press on a node used to raise the OS copy/share callout
   mid-drag. Same reasoning as touch-action:none on these two. */
.tree-viewport, .sky-viewport{-webkit-touch-callout:none}

body{
  /* Only the epoch glow lives here now — the stars moved to their own layer
     so they can fade in at First Light instead of always existing. */
  background:
    radial-gradient(900px 600px at 50% -10%, var(--accent-soft), transparent 70%),
    var(--void);
  color:var(--dust);
  font-family:"IBM Plex Sans",system-ui,sans-serif;
  transition:background-color 1.2s ease;
  position:relative;

  /* per-epoch atmosphere knobs, overridden by the body[data-epoch] rules */
  --mote-size:3px;
  --mote-opacity:.5;
}

/* ---------- atmosphere: the sky changes as the universe does ----------
   Driven by body[data-epoch], set in applyEpochColor(). Every value here is
   a slow transition, so advancing an epoch reads as the world changing
   rather than a repaint. All of it inherits the global
   prefers-reduced-motion rule at the bottom of this file. */
.starfield{
  position:fixed;inset:0;z-index:0;pointer-events:none;
  opacity:0;transition:opacity 3s ease;
  background:
    radial-gradient(1px 1px at 12% 22%, #ffffff77, transparent),
    radial-gradient(1px 1px at 78% 8%, #ffffff66, transparent),
    radial-gradient(1.5px 1.5px at 34% 64%, #ffffff5a, transparent),
    radial-gradient(1px 1px at 91% 48%, #ffffff55, transparent),
    radial-gradient(1px 1px at 60% 88%, #ffffff4a, transparent),
    radial-gradient(1.5px 1.5px at 8% 78%, #ffffff66, transparent),
    radial-gradient(1px 1px at 46% 14%, #ffffff50, transparent),
    radial-gradient(1px 1px at 24% 44%, #ffffff45, transparent),
    radial-gradient(1.5px 1.5px at 68% 72%, #ffffff5a, transparent);
}

/* The opaque early universe. Free electrons scattered every photon for
   380,000 years — so for two epochs the screen is genuinely fogged, and
   buying Recombination clears it. That clearing is the payoff. */
.epoch-fog{
  position:fixed;inset:0;z-index:1;pointer-events:none;
  opacity:0;transition:opacity 3.5s ease;
  background:
    radial-gradient(60% 50% at 30% 30%, var(--fog-warm), transparent 70%),
    radial-gradient(70% 55% at 72% 66%, var(--fog-cool), transparent 72%),
    radial-gradient(90% 70% at 50% 50%, var(--fog-deep), transparent 80%);
}

/* 1 Quantum Foam — no stars, thick seething fog, small fast motes */
body[data-epoch="1"] .epoch-fog{opacity:.5}
body[data-epoch="1"]{--mote-size:2px;--mote-opacity:.65}

/* 2 Recombination — the fog is at its heaviest right before it lifts */
body[data-epoch="2"] .epoch-fog{opacity:.68}
body[data-epoch="2"]{--mote-size:2.5px;--mote-opacity:.6}

/* 3 First Light — the fog is gone and the stars exist for the first time */
body[data-epoch="3"] .starfield{opacity:.7}
body[data-epoch="3"]{--mote-size:3px;--mote-opacity:.5}

/* 4 Heavy Elements — a denser, brighter sky */
body[data-epoch="4"] .starfield{opacity:.85}
body[data-epoch="4"]{--mote-size:3.5px;--mote-opacity:.5}

/* 5 Worlds — fewer, larger, slower grains: dust becoming rock */
body[data-epoch="5"] .starfield{opacity:.9}
body[data-epoch="5"]{--mote-size:4.5px;--mote-opacity:.45}

/* 6 Emergence — soft and organic */
body[data-epoch="6"] .starfield{opacity:.95}
body[data-epoch="6"]{--mote-size:4px;--mote-opacity:.55}

/* 7 Ascension and beyond — the clearest the sky ever gets */
body[data-epoch="7"] .starfield,
body[data-epoch="8"] .starfield{opacity:1}
body[data-epoch="7"],
body[data-epoch="8"]{--mote-size:3.5px;--mote-opacity:.6}

/* ---------- HUD: floats over the full-screen tree ---------- */
.hud-top{
  position:fixed;top:0;left:0;right:0;z-index:5;
  display:flex;flex-direction:column;align-items:center;gap:16px;
  padding:clamp(16px,4vw,28px) clamp(12px,4vw,18px) 30px;
  background:linear-gradient(var(--void) 0%, var(--void) 40%, transparent 100%);
  pointer-events:none;   /* lets drags pass through to the tree beneath except on real controls */
}

/* ---------- epoch banner ---------- */
.epoch-banner{text-align:center}
.epoch-label{
  font-size:11px;letter-spacing:.22em;text-transform:uppercase;
  color:var(--muted);font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
}
.epoch-name{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;
  font-size:clamp(24px,6vw,34px);line-height:1.15;margin-top:7px;
  color:var(--accent);transition:color 1.2s ease;
}
.epoch-next{font-size:12.5px;color:var(--muted);margin-top:9px}

/* ---------- counter ---------- */
.counter{text-align:center}
.count{
  font-family:"Share Tech Mono",ui-monospace,monospace;
  font-size:clamp(26px,8vw,42px);
  /* Share Tech Mono ships ONE weight (400). Asking for 500 here would get a
     browser-synthesised fake bold — smeared strokes, and wider glyphs than the
     real ones, which is the exact jitter this whole rule set exists to stop. */
  /* Tabular figures are now structural rather than requested: the numeric face
     is monospaced, so every digit is the same width by construction. The
     property stays because it still governs the fallback stack, and because
     the day someone swaps this face is the day it matters again. */
  font-variant-numeric:tabular-nums;
  letter-spacing:-.02em;
}
.rate{font-size:13px;color:var(--muted);margin-top:5px;font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums}

/* the cosmic clock — the age of the universe you're building. Deliberately
   quiet: it's atmosphere and context, not a stat you act on. */
.cosmic-clock{
  margin-top:9px;font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;
  letter-spacing:.12em;text-transform:uppercase;color:var(--muted);
}
.cosmic-clock::before{content:"T + ";opacity:.55}
.cosmic-clock.at-present::before{content:""}

/* Time until the cheapest reachable node is affordable. Hidden the instant
   anything is buyable, so it informs during a wait and never nags. */
.next-unlock{
  display:none;
  margin-top:7px;font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;
  color:var(--state-available);opacity:.9;
}
.next-unlock:not([hidden]){display:block}
.next-unlock::before{content:"next: ";opacity:.6}

/* weekly + dated calendar cards */
.week-card{
  border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin-bottom:12px;
}
.week-card:last-child{margin-bottom:0}
.week-card-name{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;font-size:14px;color:var(--accent);
  margin-bottom:5px;
}
.week-card-desc{font-size:12.5px;line-height:1.6;color:var(--muted)}
.week-today{display:none;border-color:var(--accent)}
.week-today:not([hidden]){display:block}
.week-card-when{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--muted);margin-bottom:6px;
}

/* Time of day: an extremely light wash over everything, atmosphere only —
   never mechanical. Night is the baseline the game was designed in, so it
   adds nothing at all. */
.tod-wash{
  position:fixed;inset:0;z-index:1;pointer-events:none;
  opacity:0;transition:opacity 4s ease, background 4s ease;
}
body[data-tod="dawn"] .tod-wash{opacity:.16;background:linear-gradient(180deg, var(--epoch-2), transparent 60%)}
body[data-tod="day"]  .tod-wash{opacity:.10;background:linear-gradient(180deg, var(--epoch-7), transparent 55%)}
body[data-tod="dusk"] .tod-wash{opacity:.16;background:linear-gradient(180deg, var(--epoch-5), transparent 60%)}

/* records grid in the drawer */
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px 16px}
.stat-cell{display:flex;flex-direction:column;gap:4px;min-width:0}
.stat-label{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;letter-spacing:.14em;
  line-height:1.5;text-transform:uppercase;color:var(--muted);
}
.stat-value{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:15px;color:var(--dust);
  overflow-wrap:anywhere;
}

.event-bonus-badge{
  margin-top:6px;display:none;padding:3px 10px;border-radius:20px;
  background:var(--state-affordable);color:var(--void);
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;
  pointer-events:none;
}
.event-bonus-badge:not([hidden]){display:inline-block}

/* The deep path's payout landing. Same hidden/display discipline as every
   other JS-toggled element here: display:none in the base rule, and the real
   value only once JS removes the attribute. */
.burst-flash{
  margin-top:6px;display:none;padding:3px 12px;border-radius:20px;
  background:color-mix(in srgb, var(--epoch-8) 22%, transparent);
  border:1px solid var(--epoch-8);color:var(--epoch-8);
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;
  pointer-events:none;
}
.burst-flash:not([hidden]){display:inline-block;animation:burst-land .9s ease-out}
@keyframes burst-land{
  0%{transform:scale(.82);opacity:0}
  22%{transform:scale(1.06);opacity:1}
  100%{transform:scale(1);opacity:1}
}

.event-prompt{
  /* z-index 31 puts the Supernova window above every panel — the sheet (21),
     the drawer (25), the guide and codex (28). It used to sit at 7, behind
     all of them, so a window that opened while you were shopping was visible
     through the scrim, unclickable, and always missed. It's a 12-second
     window: it has to outrank whatever you happen to have open. */
  position:fixed;top:38%;left:50%;transform:translate(-50%,-50%) scale(.9);z-index:31;
  display:none;flex-direction:column;align-items:center;gap:8px;
  padding:20px 30px;border-radius:16px;cursor:pointer;
  background:var(--void-2);border:2px solid var(--state-affordable);
  box-shadow:0 0 30px -6px var(--state-affordable);
  opacity:0;pointer-events:none;transition:opacity .25s ease, transform .25s ease;
  animation:event-pulse 1.4s ease-in-out infinite;
}
.event-prompt:not([hidden]){display:flex}
.event-prompt.show{opacity:1;pointer-events:auto;transform:translate(-50%,-50%) scale(1)}
@keyframes event-pulse{50%{box-shadow:0 0 46px 4px var(--state-affordable)}}
.event-prompt-title{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;font-size:20px;line-height:1.2;
  color:var(--state-affordable);
}
.event-prompt-sub{font-size:13px;line-height:1.5;color:var(--dust);font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums}

/* ---------- floating HUD buttons ----------
   Two rails rather than one stack of five. Which side a button is on carries
   meaning: LEFT changes the view and can never cost you anything, RIGHT opens
   something of yours. A player shouldn't have to remember which icon is which
   — the side already tells them what kind of thing it is. */
.hud-rail{
  position:fixed;z-index:6;top:clamp(16px,4vw,28px);
  display:flex;flex-direction:column;align-items:center;gap:10px;
}
.hud-rail-left{left:clamp(12px,4vw,18px)}
.hud-rail-right{right:clamp(12px,4vw,18px)}
.hud-rail-label{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:8px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--muted);opacity:.55;margin-bottom:-2px;
}

.hud-btn{
  position:relative;
  width:var(--hud-btn);height:var(--hud-btn);border-radius:28%;
  background:var(--void-2);border:1px solid var(--line);
  display:grid;place-content:center;cursor:pointer;
  transition:border-color .18s ease, color .18s ease, transform .18s ease, box-shadow .18s ease;
  /* colour comes from the data-hue block below */
}
/* This class carries a bare `display`, so any hud-btn that JS toggles with
   the `hidden` attribute needs it beaten explicitly — author CSS wins over
   the browser's built-in [hidden]{display:none} regardless of specificity.
   See the standing note in CLAUDE.md; this has shipped as a real bug twice. */
.hud-btn[hidden]{display:none}
.hud-btn svg{width:var(--hud-icon);height:var(--hud-icon)}
.hud-btn:hover{transform:translateY(-2px)}   /* colour on hover: see the data-hue block below */
.hud-btn:active{transform:translateY(0)}
.hud-btn:focus-visible{outline:2px solid var(--dust);outline-offset:2px}

/* The button's name, PERMANENTLY, under the icon.
   It used to be a tooltip that appeared on hover and was switched off entirely
   under (hover:none) — which meant that on a phone, the entire HUD was six
   unlabelled glyphs and the only way to learn what any of them did was to
   press it and find out. A caption costs eight pixels of rail and removes the
   guessing. One word each, because the rail is narrow and two words collide
   with the next button down. */
.hud-btn::after{
  content:attr(data-label);position:absolute;
  top:calc(100% + 3px);left:50%;transform:translateX(-50%);
  white-space:nowrap;pointer-events:none;
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:9px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted);
  transition:color .18s ease;
}
.hud-btn:hover::after, .hud-btn:focus-visible::after{color:var(--btn-hue, var(--accent))}

/* The rail needs to make room for the captions now that they're always there. */
.hud-rail{gap:22px}

/* ---- each button's own colour ----
   Six identical grey glyphs are six things you have to read. A hue per button
   makes them findable by shape AND colour, and the hues come from the epoch
   variables rather than new hex, so a palette cosmetic recolours the whole HUD
   for free and the light theme is already handled.

   The resting state stays quiet — a tinted icon on the panel's own background,
   not a coloured chip — because six saturated buttons over the tree would
   compete with the nodes, which are the thing you're supposed to be looking
   at. The colour comes up on hover and focus. */
.hud-btn[data-hue="1"]{--btn-hue:var(--epoch-1)}
.hud-btn[data-hue="2"]{--btn-hue:var(--epoch-2)}
.hud-btn[data-hue="3"]{--btn-hue:var(--epoch-3)}
.hud-btn[data-hue="4"]{--btn-hue:var(--epoch-4)}
.hud-btn[data-hue="5"]{--btn-hue:var(--epoch-5)}
.hud-btn[data-hue="6"]{--btn-hue:var(--epoch-6)}
.hud-btn[data-hue="7"]{--btn-hue:var(--epoch-7)}
.hud-btn{color:color-mix(in srgb, var(--btn-hue, var(--dust)) 62%, var(--dust))}
.hud-btn:hover{
  border-color:var(--btn-hue, var(--accent));
  color:var(--btn-hue, var(--accent));
  box-shadow:0 6px 18px -10px var(--btn-hue, var(--accent));
}

/* Signed in, the profile button wears your initial instead of a stock person
   glyph — the same pip the friends list and the tree markers already use, so
   you recognise yourself by the same mark everywhere. */
.hud-avatar{
  position:absolute;inset:0;display:none;place-content:center;
  border-radius:inherit;
  background:color-mix(in srgb, var(--btn-hue, var(--accent)) 22%, transparent);
  color:var(--btn-hue, var(--accent));
  font-family:"Space Grotesk",system-ui,sans-serif;   /* a letter, so the display face, not the numeric one */
  font-size:calc(var(--hud-btn) * .42);font-weight:700;line-height:1;
}
.hud-avatar:not([hidden]){display:grid}

.guide-toggle{font-family:"Space Grotesk",system-ui,sans-serif;font-size:20px;font-weight:600;line-height:1}

/* ---------- the affordable compass ----------
   The tree is far bigger than the screen, so "there is something you can
   afford, it's just off to the left" was completely invisible — you had to
   go hunting for it. This rides the edge of the free space between the two
   HUDs, points at it, names it, and flies you there when tapped.
   `display:none` in the base rule + the :not([hidden]) rule below — see the
   standing note in CLAUDE.md about hidden-toggled elements. */
.afford-compass{
  position:fixed;z-index:6;display:none;
  align-items:center;gap:9px;
  transform:translate(-50%,-50%);
  padding:9px 15px 9px 11px;border-radius:22px;cursor:pointer;
  background:var(--void-2);border:1px solid var(--state-affordable);
  color:var(--state-affordable);font:inherit;
  box-shadow:0 0 22px -8px var(--state-affordable);
  animation:compass-nudge 1.8s ease-in-out infinite;
}
.afford-compass:not([hidden]){display:flex}
.afford-compass:focus-visible{outline:2px solid var(--dust);outline-offset:3px}
@keyframes compass-nudge{50%{box-shadow:0 0 32px -4px var(--state-affordable)}}
/* Only the arrow rotates — the label stays upright and readable at every angle. */
.afford-compass svg{
  width:16px;height:16px;flex:0 0 auto;
  transform:rotate(var(--compass-angle,0deg));
}
.afford-compass-name{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;letter-spacing:.06em;
  color:var(--dust);white-space:nowrap;max-width:40vw;overflow:hidden;text-overflow:ellipsis;
}

/* Sound on: the two arcs show, no slash. Sound off: the arcs go and the slash
   appears. Both states are drawn inside the icon's own viewBox (see
   index.html), so the slash sits on the speaker at the right angle whatever
   size the button is — the previous version was a CSS bar positioned against
   the BUTTON, which could never line up with the glyph inside it. */
.sound-toggle .mute-slash{opacity:0}
.sound-toggle.muted{color:var(--muted)}
.sound-toggle.muted .wave-on{opacity:0}
.sound-toggle.muted .mute-slash{opacity:1}

/* ---------- bottom HUD: hint above the core, stacked in flow so the
   core's responsive size can never overlap the hint above it ---------- */
.bottom-hud{
  position:fixed;left:50%;bottom:0;transform:translateX(-50%);
  z-index:5;
  display:flex;flex-direction:column;align-items:center;gap:20px;
  padding-bottom:calc(env(safe-area-inset-bottom) + 24px);
  transition:transform .28s ease;   /* matches the sheet's own slide, so they move together */
}

/* ---- the core stays live while a panel is open ----
   The detail sheet is bottom-anchored and lands exactly on the core, so the
   bottom HUD lifts by the panel's measured height (--sheet-lift, set in
   render.js) and sits above the sheet's scrim but below the panel itself.
   The drawer is a side panel, so it only needs the z-index — on a narrow
   screen the drawer is 86vw and still covers the core, which is fine: the
   drawer is somewhere you dip into, the sheet is where you sit and spend. */
/* The raised z-index is conditional on the core actually CLEARING the panel
   (see liftBottomHud). It's 22 against the panel's 21, so when there isn't
   room to lift the core clear, leaving it raised would draw it straight over
   the sheet's text and controls — which is exactly the bug this replaced.
   Without the class the core keeps its resting place and the sheet covers it,
   which is the correct fallback on a screen too short for both. */
body.sheet-open.sheet-core-clear .bottom-hud{
  z-index:22;                        /* sheet scrim is 20, sheet panel 21 */
  transform:translateX(-50%) translateY(calc(-1 * var(--sheet-lift, 0px)));
}
body.drawer-open .bottom-hud{z-index:25}   /* drawer scrim is 24 */

/* Under the sheet the two hint lines are just noise stacked on the panel —
   the core is the only thing that needs to be reachable up there. */
body.sheet-open .first-buy-hint{display:none}

.first-buy-hint{
  position:relative;pointer-events:auto;
  font-size:13px;line-height:1.65;color:var(--dust);text-align:center;max-width:min(360px,84vw);
  background:var(--void-2);border:1px solid var(--accent);border-radius:12px;
  padding:14px 34px 14px 18px;
  box-shadow:0 0 20px -8px var(--accent);
}
.first-buy-hint:not([hidden]){display:block}
.first-buy-hint-close{
  position:absolute;top:4px;right:6px;background:none;border:0;color:var(--muted);
  font-size:16px;line-height:1;cursor:pointer;padding:4px;
}
.first-buy-hint-close:hover{color:var(--dust)}

/* ---------- the core (signature element) — floats bottom-center over the tree ---------- */
.core{
  position:relative;
  width:clamp(104px,26vw,150px);height:clamp(104px,26vw,150px);
  border-radius:50%;
  border:1px solid var(--line);
  background:radial-gradient(circle at 50% 45%, var(--accent) 0%, #7a3f6b 45%, var(--void-2) 72%);
  cursor:pointer;overflow:hidden;
  box-shadow:0 0 60px -12px var(--accent);
  transition:transform .08s ease, box-shadow 1.2s ease, background 1.2s ease;
  -webkit-tap-highlight-color:transparent;
}
.core:active{transform:scale(.955)}
.core:focus-visible{outline:2px solid var(--dust);outline-offset:6px}
.core-label{
  position:absolute;inset:0;display:grid;place-content:center;
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;letter-spacing:.18em;
  text-transform:uppercase;color:#1c1226;text-shadow:0 1px 0 #ffffff40;
}
.halo{
  position:absolute;inset:-2px;border-radius:50%;
  border:1px solid var(--accent);opacity:.5;
  animation:breathe 4s ease-in-out infinite;
}
@keyframes breathe{50%{transform:scale(1.07);opacity:.15}}

/* the core's one big moment: fired once per epoch transition, on top of the constant breathing halo */
.core.epoch-pulse{animation:epoch-pulse 1.2s ease-out}
@keyframes epoch-pulse{
  0%{box-shadow:0 0 60px -12px var(--accent)}
  35%{box-shadow:0 0 140px 20px var(--accent);transform:scale(1.18)}
  100%{box-shadow:0 0 60px -12px var(--accent);transform:scale(1)}
}

.ripple{
  position:absolute;left:50%;top:50%;width:12px;height:12px;
  margin:-6px 0 0 -6px;border-radius:50%;border:1px solid var(--dust);
  transform:rotate(var(--ripple-rot, 0deg));
  animation:ripple .6s ease-out forwards;pointer-events:none;
}
@keyframes ripple{to{transform:rotate(var(--ripple-rot, 0deg)) scale(13);opacity:0}}
.ripple-auto{opacity:.5}

.pop{
  position:absolute;font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:14px;
  color:var(--dust);pointer-events:none;animation:pop .8s ease-out forwards;
}
@keyframes pop{to{transform:translateY(-46px) translateX(var(--pop-drift, 0px));opacity:0}}
.pop-auto{font-size:11px;color:var(--muted)}

/* ---------- shop ---------- */
.shop-head{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;letter-spacing:.22em;
  text-transform:uppercase;color:var(--muted);
  border-bottom:1px solid var(--line);padding-bottom:10px;margin-bottom:8px;
}
/* The drawer's sections used to butt straight into each other — the global
   `*{margin:0}` reset means nothing separates them unless it's said here. */
.drawer section + section{margin-top:30px}
.gen{
  width:100%;display:grid;grid-template-columns:1fr auto;gap:4px 12px;
  align-items:center;text-align:left;
  background:transparent;border:0;border-bottom:1px solid var(--line);
  color:inherit;font:inherit;padding:14px 4px;cursor:pointer;
  transition:background .15s ease;
}
.gen:hover:not(:disabled){background:color-mix(in srgb, var(--dust) 4%, transparent)}
.gen:disabled{opacity:.38;cursor:not-allowed}
.gen:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.gen-name{font-weight:500}
.gen-owned{color:var(--muted);font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:13px}
.gen-sub{grid-column:1;font-size:12px;color:var(--muted)}
.gen-cost{
  grid-column:2;font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:13px;
  color:var(--accent);
}
.gen:disabled .gen-cost{color:var(--muted)}

/* ---------- ambient dust motes — pure CSS, drift slowly behind the tree ---------- */
.motes{position:fixed;inset:0;z-index:1;overflow:hidden;pointer-events:none}
.mote{
  position:absolute;
  width:var(--mote-size,3px);height:var(--mote-size,3px);border-radius:50%;
  background:var(--accent);opacity:0;
  transition:width 2s ease, height 2s ease;
  animation:mote-drift linear infinite;
}
@keyframes mote-drift{
  0%{opacity:0;transform:translateY(0)}
  8%{opacity:var(--mote-opacity,.5)}
  92%{opacity:var(--mote-opacity,.5)}
  100%{opacity:0;transform:translateY(-70vh)}
}

/* ---------- the tree — the whole screen ---------- */
.tree-viewport{
  position:fixed;inset:0;z-index:2;
  overflow:hidden;   /* the canvas is moved by transform now, not native scroll */
  background:transparent;
  touch-action:none;   /* we handle pan/pinch ourselves — don't let the browser scroll/zoom underneath it */
  cursor:grab;
}
.tree-viewport.dragging{cursor:grabbing}
.tree-viewport.easing .tree-canvas{transition:transform .5s cubic-bezier(.2,.7,.3,1)}
.tree-canvas{
  position:relative;
  transform-origin:0 0;
}
/* will-change ONLY while a gesture is in flight, never at rest.
   A permanent `will-change:transform` pins the canvas to its own compositor
   layer, which the browser rasterises once and then rescales as a bitmap —
   so zooming in magnified stale pixels and every label and connector went
   soft. Scoping it to the gesture keeps the pan/pinch cheap and lets the
   browser re-rasterise at the final scale the moment you let go, which is
   when you are actually reading the tree. */
.tree-viewport.dragging .tree-canvas,
.tree-viewport.zooming .tree-canvas,
.tree-viewport.easing .tree-canvas{will-change:transform}
.tree-lines,.sky-lines{position:absolute;top:0;left:0;overflow:visible;pointer-events:none}

/* connector base state: dim, requirement not yet met */
.tree-lines path{
  fill:none;stroke:var(--line);stroke-width:2;
  transition:stroke 1s ease, stroke-dashoffset 1s ease-out;
}
/* lit: this connector's parent node is owned — the "circuit energising" state.
   The dash pattern + slow offset animation reads as current flowing downstream. */
.tree-lines path.lit{
  stroke:var(--node-color, var(--accent));
  stroke-dasharray:2 10;
  animation:connector-flow 1.2s linear infinite;
}
@keyframes connector-flow{to{stroke-dashoffset:-24}}

/* Node construction: the button itself stays an unclipped, unstyled box so
   its focus outline and its owned-count badge can sit OUTSIDE the silhouette.
   The visible node is two stacked pseudo-elements sharing one clip-path:
     ::before = the full shape, painted in the state colour  -> reads as the ring
     ::after  = the same shape inset 2.5px, painted in the fill -> the interior
   Clipping the button directly would have eaten both the badge and the
   keyboard focus ring, which is the whole reason for the pseudo-element pair. */
.tree-node{
  position:absolute;top:0;left:0;
  width:58px;height:58px;   /* fallback only — the real size is set inline from the node's rate */
  transform:translate(-50%,-50%);   /* the coordinate is the node's centre, per spec */
  background:transparent;
  border:0;
  color:var(--epoch-color, var(--muted));   /* the glyph, via currentColor */
  display:grid;place-content:center;
  cursor:pointer;padding:0;
  transition:transform .12s ease, color .4s ease, opacity .4s ease;
}
.tree-node::before{
  content:"";position:absolute;inset:0;z-index:0;
  clip-path:var(--shape, circle(50%));
  background:var(--state-color, var(--line));
  transition:background .4s ease, filter .3s ease;
}
.tree-node::after{
  content:"";position:absolute;inset:2.5px;z-index:1;
  clip-path:var(--shape, circle(50%));
  background:var(--node-fill, var(--void));
  transition:background .4s ease;
}
.tree-node.keystone::after{inset:3.5px}
.tree-node svg{width:var(--icon-size,26px);height:var(--icon-size,26px);position:relative;z-index:2}
.tree-node-owned{z-index:3}
.tree-node:hover{transform:translate(-50%,-50%) scale(1.08)}
.tree-node:focus-visible{outline:2px solid var(--dust);outline-offset:3px}

/* One silhouette per epoch. Shape carries epoch identity independently of
   colour, so the eras stay distinguishable for a colourblind player — and it
   gives the tree a rhythm as you climb it.
   Keyed off data-epoch rather than a class so the legend in the guide can
   reuse the exact same shapes without duplicating the polygons. */
[data-epoch="1"]{--shape:circle(50%)}
[data-epoch="2"]{--shape:polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)}
[data-epoch="3"]{--shape:polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)}
[data-epoch="4"]{--shape:polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)}
[data-epoch="5"]{--shape:inset(0 round 24%)}
[data-epoch="6"]{--shape:polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)}
[data-epoch="7"]{--shape:polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)}
/* 8 Divergence — a shape that splits, for the epoch where the tree does. */
[data-epoch="8"]{--shape:polygon(50% 0%, 100% 32%, 78% 100%, 50% 62%, 22% 100%, 0% 32%)}

/* the legend's mini-nodes: same two-layer construction, smaller */
.legend-node{
  position:relative;width:30px;height:30px;flex:0 0 auto;
  color:var(--epoch-color, var(--muted));
}
.legend-node::before{
  content:"";position:absolute;inset:0;
  clip-path:var(--shape, circle(50%));
  background:var(--state-color, var(--line));
}
.legend-node::after{
  content:"";position:absolute;inset:2px;
  clip-path:var(--shape, circle(50%));
  background:var(--node-fill, var(--void));
}
.legend-node svg{width:15px;height:15px;position:absolute;inset:0;margin:auto;z-index:2}
.legend-node.owned{--state-color:var(--state-owned);--node-fill:color-mix(in srgb, var(--epoch-color) 18%, var(--void-2))}
.legend-node.affordable{--state-color:var(--state-affordable)}
/* mirrors the thick affordable ring on the real node, so the key matches the tree */
.legend-node.affordable::after{inset:3.5px}
.legend-node.available{--state-color:var(--state-available)}
.legend-node.locked{--state-color:var(--line);--epoch-color:var(--line);opacity:.55}

/* keystone size is folded into nodeSize() now — this class only carries the
   thicker interior inset and the idle animation below */

/* Keystones are the seven moments that close an epoch, so their glyph
   breathes — slowly and out of phase with the state pulses on the ring, so
   the two readings don't beat against each other. Locked keystones stay
   still: an unknown shouldn't advertise itself as important.
   The global prefers-reduced-motion rule disables this with everything else. */
.tree-node.keystone:not(.locked) svg{
  transform-origin:50% 50%;
  animation:keystone-idle 5.5s ease-in-out infinite;
}
@keyframes keystone-idle{50%{transform:scale(1.13);opacity:.8}}

/* ---- the four states: ring colour + shaped glow ----
   The glow is filter: drop-shadow(), not box-shadow — box-shadow is painted
   outside the clip-path and gets cut away, so it would draw a rectangular
   halo around a hexagon. drop-shadow follows the silhouette. */
.tree-node.locked{
  --state-color:var(--line);
  --epoch-color:var(--line);      /* an unknown must not reveal its epoch colour */
  cursor:not-allowed;opacity:.45;
}
.tree-node.locked:hover{transform:translate(-50%,-50%)}

.tree-node.available{--state-color:var(--state-available)}
.tree-node.available::before{animation:pulse-available 2.6s ease-in-out infinite}
@keyframes pulse-available{50%{filter:drop-shadow(0 0 7px var(--state-available-soft))}}

.tree-node.affordable{--state-color:var(--state-affordable)}
.tree-node.affordable::before{
  filter:drop-shadow(0 0 5px var(--state-affordable));
  animation:pulse-affordable 1.6s ease-in-out infinite;
}
@keyframes pulse-affordable{50%{filter:drop-shadow(0 0 13px var(--state-affordable))}}

/* A second, NON-COLOUR cue for the one pair that colour alone separated.
   available vs affordable differed by ring hue and pulse rate — and
   prefers-reduced-motion switches both pulses off, leaving a colourblind
   player with hue as the only signal, on the single most important
   distinction in the game. Affordable now also carries a visibly thicker
   ring, which survives reduced motion and greyscale alike. */
.tree-node.affordable::after{inset:5px}
.tree-node.keystone.affordable::after{inset:6.5px}

/* The beacon: the first affordable node before you own anything at all.
   Louder than affordable on purpose — it's onboarding, and it retires itself
   the moment the first node is bought. */
.tree-node.beacon::before{
  animation:pulse-beacon 1.1s ease-in-out infinite;
}
@keyframes pulse-beacon{
  50%{filter:drop-shadow(0 0 22px var(--state-affordable)) drop-shadow(0 0 8px var(--dust))}
}
/* Reduced motion kills the animation, so the beacon needs a still form too. */
@media (prefers-reduced-motion:reduce){
  .tree-node.beacon::before{filter:drop-shadow(0 0 18px var(--state-affordable))}
}

/* Owned: the ring goes quiet and the interior picks up a wash of the epoch's
   colour, so a finished node reads as filled-in rather than as loud. */
.tree-node.owned{
  --state-color:var(--state-owned);
  --node-fill:color-mix(in srgb, var(--epoch-color) 18%, var(--void-2));
}
.tree-node.owned::before{animation:owned-breathe 3.2s ease-in-out infinite}
@keyframes owned-breathe{50%{filter:drop-shadow(0 0 8px var(--state-owned))}}
.tree-node.keystone.owned::before{filter:drop-shadow(0 0 6px var(--epoch-color))}

/* The purchase burst — fired from burstNode() on every buy. Deliberately on
   the button, not the clipped pseudo-elements, so it can grow past the node's
   silhouette instead of being cut to it. */
.node-burst{
  position:absolute;inset:-2px;border-radius:50%;pointer-events:none;
  border:2px solid var(--epoch-color, var(--accent));
  animation:node-burst .55s cubic-bezier(.2,.7,.3,1) forwards;
}
@keyframes node-burst{
  from{opacity:.9;transform:scale(1)}
  to{opacity:0;transform:scale(2.5)}
}

.tree-node-owned{
  position:absolute;top:-4px;right:-4px;min-width:18px;height:18px;padding:0 4px;
  background:var(--epoch-color, var(--state-owned));color:var(--void);
  border-radius:9px;font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;
  display:grid;place-content:center;
}

.tree-node-cost{
  position:absolute;top:0;left:0;transform:translate(-50%,11px);
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;color:var(--muted);
  white-space:nowrap;pointer-events:none;
}
/* The epoch rule and its name, behind the nodes. height:0 + border-top so the
   element IS the line; the label rides above it, so no background is needed to
   punch a hole through the rule. */
.epoch-band{
  position:absolute;left:0;height:0;pointer-events:none;
  display:flex;justify-content:center;
  border-top:1px solid color-mix(in srgb, var(--epoch-color) 24%, transparent);
}
.epoch-band-label{
  transform:translateY(-100%);padding-bottom:7px;white-space:nowrap;
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:10px;letter-spacing:.22em;text-transform:uppercase;
  color:color-mix(in srgb, var(--epoch-color) 62%, var(--muted));
}

/* A locked node's name sits dimmer than a reached one — legible enough to read
   the road ahead, quiet enough that the frontier still reads as the bright
   edge. Prose, not a number, so it is not a tabular-nums rule. */
.tree-node-name.locked{color:var(--muted);opacity:.72}
/* What would open it, in place of the price. Wraps rather than running off
   sideways: "Needs Baryogenesis and Recombination" is longer than any cost. */
.tree-node-req{
  position:absolute;top:0;left:0;transform:translate(-50%,11px);
  width:116px;text-align:center;
  font-size:9.5px;line-height:1.3;color:var(--muted);opacity:.8;
  pointer-events:none;
}

/* milestone multiplier chip, under the cost */
.tree-node-mult{
  position:absolute;top:0;left:0;transform:translate(-50%,11px);
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;
  color:var(--epoch-color,var(--accent));
  white-space:nowrap;pointer-events:none;
}

/* Owned nodes glow harder the further up the milestone ladder they are, so
   depth is visible from across the tree and not only inside the sheet. */
.tree-node.owned[data-mult="2"]::before{filter:drop-shadow(0 0 7px var(--epoch-color))}
.tree-node.owned[data-mult="4"]::before{filter:drop-shadow(0 0 11px var(--epoch-color))}
.tree-node.owned[data-mult="8"]::before{filter:drop-shadow(0 0 16px var(--epoch-color))}
.tree-node.owned[data-mult="16"]::before{
  animation:owned-breathe-max 2.6s ease-in-out infinite;
}
@keyframes owned-breathe-max{
  0%,100%{filter:drop-shadow(0 0 20px var(--epoch-color))}
  50%{filter:drop-shadow(0 0 32px var(--epoch-color))}
}

/* ---- node labels ----
   Two things keep these readable, and both are needed:

   z-index — every node is its own stacking context (they're all
   `transform:translate(-50%,-50%)`), and labels used to sit at `auto` in DOM
   order, so any node rendered LATER painted straight over the label of a node
   rendered earlier. That's the "text is covered" bug; a whole label could
   disappear behind a neighbour.

   the halo — the tree is dense, and a label will sometimes cross a connector
   or a neighbour whatever the spacing is. A ring of the background colour
   behind the glyphs keeps it legible when that happens, without needing a
   solid plate that would clutter the map. */
.tree-node-name, .tree-node-cost, .tree-node-mult{
  z-index:2;
  text-shadow:0 0 4px var(--void), 0 0 8px var(--void), 0 0 12px var(--void);
}

.tree-node-name{
  position:absolute;top:0;left:0;transform:translate(-50%,-100%);
  width:104px;margin-top:-15px;   /* clears the node's edge, and its glow, by a readable gap */
  font-size:11px;font-weight:500;line-height:1.35;text-align:center;color:var(--dust);
  pointer-events:none;
}

.tree-node.discover{animation:discover-node .9s ease-out}
@keyframes discover-node{
  0%{opacity:0;transform:translate(-50%,-50%) scale(.4)}
  55%{opacity:1;transform:translate(-50%,-50%) scale(1.2)}
  100%{transform:translate(-50%,-50%) scale(1)}
}


/* ---------- detail sheet ---------- */
.sheet-backdrop{
  position:fixed;inset:0;background:color-mix(in srgb, var(--scrim) 67%, transparent);opacity:0;pointer-events:none;
  transition:opacity .2s ease;z-index:20;
}
.sheet-panel{
  position:fixed;left:0;right:0;bottom:0;z-index:21;
  max-width:620px;margin:0 auto;
  max-height:85vh;max-height:85dvh;overflow-y:auto;   /* stays usable on short/landscape phone screens */
  background:var(--void-2);border-top:1px solid var(--line);border-radius:16px 16px 0 0;
  padding:22px 22px calc(22px + env(safe-area-inset-bottom));
  transform:translateY(100%);transition:transform .28s ease;
}
/* Keyed to the CLASS, not to #detailSheet. Both sheets are built from the same
   markup and both get `open` from JS, but only the timeline's id was ever
   written here — so the Firmament's sheet was left parked at translateY(100%)
   with an invisible backdrop, and tapping an object in the Firmament appeared
   to do nothing at all. Anything that styles one sheet must style `.sheet`. */
.sheet.open .sheet-backdrop{opacity:1;pointer-events:auto}
.sheet.open .sheet-panel{transform:translateY(0)}

/* The double-tap window, held open.
   A tap opens the sheet on the next frame, and the moment it opens the
   backdrop covers the whole screen — so the SECOND tap of a double-tap hit the
   backdrop instead of the node, elementFromPoint found no .tree-node, and the
   buy never happened. The window to beat it was one animation frame, which is
   why double-tap-to-buy felt like it needed inhuman speed.

   For the length of the double-tap window the backdrop stays untouchable and
   taps fall through to the map underneath. Tap-outside-to-close is only
   deferred by that window, never lost. */
.sheet.open.tap-through .sheet-backdrop{pointer-events:none}

/* The Firmament is its own full-screen layer at z-index 22, so a sheet opened
   OVER it has to clear that; the timeline's sheet only ever has to clear the
   tree. Still below the Supernova prompt at 31, which outranks every panel.

   Keyed to `body.firmament-open`, NOT to a sheet's id. It was `#skySheet` and
   that is precisely how the second Firmament sheet (the constellation panel)
   shipped invisible: its panel sat at 21, under the view at 22, so tapping a
   constellation hub appeared to do nothing at all. Same failure the id-keyed
   `.sheet.open` rules caused once before. Written this way, any sheet that
   opens over the Firmament clears it automatically and the next one added
   can't inherit the bug. */
body.firmament-open .sheet-backdrop{z-index:23}
body.firmament-open .sheet-panel{z-index:24}

.sheet-close{
  position:absolute;top:14px;right:16px;background:none;border:0;color:var(--muted);
  font-size:20px;line-height:1;cursor:pointer;padding:4px;
}
.sheet-close:hover{color:var(--dust)}

.sheet-icon{
  width:56px;height:56px;border-radius:50%;display:grid;place-content:center;
  background:var(--void);border:2px solid var(--node-color, var(--accent));color:var(--node-color, var(--accent));
  margin-bottom:10px;
}
.sheet-icon svg{width:26px;height:26px}
.sheet-epoch{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--node-color, var(--muted));margin-bottom:5px;
}
.sheet-name{font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;font-size:22px;line-height:1.25;margin-bottom:6px}
.sheet-when{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;color:var(--muted);
  margin-bottom:16px;
}
.sheet-flavour{font-size:13.5px;line-height:1.7;color:var(--dust);margin-bottom:22px}

.sheet-stats{display:flex;gap:16px 28px;margin-bottom:24px;flex-wrap:wrap}
.sheet-stat-label{
  display:block;font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;letter-spacing:.15em;
  text-transform:uppercase;color:var(--muted);margin-bottom:5px;
}
.sheet-stats > div{font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:15px}

/* quantity selector — sits directly above the buy button it modifies */
/* Five options now, and on a narrow phone five in a row is four too many
   characters wide — so it wraps rather than squeezing each button below a
   readable width. */
.qty-select{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
.qty-btn{
  flex:1 1 56px;padding:8px 4px;border-radius:6px;cursor:pointer;
  background:transparent;border:1px solid var(--line);color:var(--muted);
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:12px;
  transition:border-color .15s ease, color .15s ease, background .15s ease;
}
.qty-btn:hover{border-color:var(--dust);color:var(--dust)}
.qty-btn.selected{
  border-color:var(--node-color, var(--accent));color:var(--node-color, var(--accent));
  background:color-mix(in srgb, var(--dust) 5%, transparent);
}
.qty-btn:focus-visible{outline:2px solid var(--dust);outline-offset:2px}
/* The milestone button on a node that has reached every milestone. */
.qty-btn:disabled{opacity:.35;cursor:not-allowed}
.qty-btn:disabled:hover{border-color:var(--line);color:var(--muted)}

.sheet-buy{
  width:100%;padding:14px;border-radius:8px;border:0;cursor:pointer;
  background:var(--node-color, var(--accent));color:var(--void);
  font:inherit;font-weight:500;font-size:14px;
  transition:opacity .15s ease;
}
.sheet-buy:disabled{opacity:.4;cursor:not-allowed}
/* The one buy in the game that closes a door behind it. Outlined rather than
   filled, so it reads as a decision to weigh instead of the usual one-tap
   purchase — the same button, deliberately not the same invitation. */
.sheet-buy.is-choice{
  background:none;color:var(--epoch-8);
  border:1px solid var(--epoch-8);
  box-shadow:inset 0 0 24px -12px var(--epoch-8);
}

.sheet-milestone{margin-bottom:18px}
.sheet-milestone-label{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;color:var(--muted);margin-bottom:6px;
}
.sheet-milestone-bar{height:6px;border-radius:3px;background:var(--line);overflow:hidden}
.sheet-milestone-fill{height:100%;background:var(--node-color, var(--accent));transition:width .3s ease}

/* ---------- prestige ---------- */
.collapse-btn{
  width:100%;margin-top:10px;padding:14px;border-radius:8px;
  background:transparent;border:1px solid var(--line);color:var(--muted);
  font:inherit;font-size:13px;cursor:pointer;
  transition:border-color .15s ease, color .15s ease;
}
.collapse-btn:hover:not(:disabled){border-color:var(--accent);color:var(--accent)}
.collapse-btn:disabled{opacity:.4;cursor:not-allowed}

/* ---------- seed pairs ---------- */
.seed-pair{margin-bottom:14px}
.seed-sides{display:flex;gap:8px;margin-top:6px}
.seed-side{
  flex:1 1 0;min-width:0;min-height:var(--tap);
  text-align:left;padding:8px 10px;border-radius:8px;cursor:pointer;
  background:transparent;border:1px solid var(--line);color:var(--muted);
  display:flex;flex-direction:column;gap:2px;
  transition:border-color .15s ease, color .15s ease, background .15s ease;
}
.seed-side:hover:not(:disabled){border-color:var(--dust);color:var(--dust)}
.seed-side.selected{
  border-color:var(--accent);color:var(--accent);
  background:color-mix(in srgb, var(--accent) 8%, transparent);
}
/* Disabled means "locked in for this universe", not "broken" — so it stays
   readable rather than fading to nothing. The selected side is never disabled,
   so what you HAVE is always at full contrast. */
.seed-side:disabled{opacity:.55;cursor:not-allowed}
.seed-side-name{font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;font-size:12px}
.seed-side-effect{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:10px;line-height:1.35;opacity:.85;
}
.seed-respec-note{
  margin-top:5px;font-size:10px;line-height:1.4;color:var(--muted);opacity:.75;
}

.carry-block{
  display:none;
  max-width:420px;margin:22px auto 0;text-align:left;
  border:1px solid var(--line);border-radius:12px;padding:14px 16px;
}
.carry-block:not([hidden]){display:block}
.carry-head{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--accent);margin-bottom:6px;
}
.carry-note{font-size:12px;line-height:1.5;color:var(--muted);margin-bottom:12px}
.carry-options{display:flex;flex-wrap:wrap;gap:8px}
.carry-option{
  display:flex;align-items:center;gap:7px;
  background:transparent;border:1px solid var(--line);border-radius:20px;
  color:var(--muted);font:inherit;font-size:12px;padding:7px 13px;cursor:pointer;
  transition:border-color .15s ease, color .15s ease, background .15s ease;
}
.carry-option:hover{border-color:var(--dust);color:var(--dust)}
.carry-option.selected{
  border-color:var(--accent);color:var(--accent);
  background:var(--accent-soft);
}
.carry-option-mult{font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px}

.collapse-actions{display:flex;gap:12px;justify-content:center;margin-top:22px;flex-wrap:wrap}
.collapse-cancel, .collapse-confirm-btn{
  padding:12px 22px;border-radius:8px;font:inherit;font-size:13px;cursor:pointer;
}
.collapse-cancel{background:transparent;border:1px solid var(--line);color:var(--muted)}
.collapse-cancel:hover{border-color:var(--dust);color:var(--dust)}
.collapse-confirm-btn{background:var(--accent);border:0;color:var(--void);font-weight:500}

/* ---------- the codex ---------- */
.codex-head{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;letter-spacing:.22em;
  text-transform:uppercase;color:var(--epoch-color,var(--muted));
  border-bottom:1px solid var(--line);
  padding-bottom:7px;margin:24px 0 14px;
}
.codex-head:first-child{margin-top:0}

.codex-entry{display:flex;gap:14px;align-items:flex-start;margin-bottom:18px}
.codex-entry:last-child{margin-bottom:0}
.codex-icon{
  flex:0 0 auto;width:40px;height:40px;border-radius:50%;
  display:grid;place-content:center;
  background:var(--void);
  border:1px solid var(--epoch-color,var(--line));
  color:var(--epoch-color,var(--muted));
}
.codex-icon.locked{border-color:var(--line);color:var(--line)}
.codex-icon svg{width:20px;height:20px}
.codex-text{display:flex;flex-direction:column;gap:3px;min-width:0}
.codex-name{font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;font-size:15px;color:var(--dust)}
.codex-when{font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;color:var(--epoch-color,var(--muted))}
.codex-flavour{font-size:13px;line-height:1.55;color:var(--dust)}

/* Undiscovered entries dim via COLOUR, not opacity. Opacity multiplies
   through to the text and dropped these below readable contrast — the point
   is "not found yet", not "illegible". */
.codex-entry:not(.unlocked) .codex-name{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-weight:400;font-size:12px;
  letter-spacing:.1em;text-transform:uppercase;color:var(--muted);
}

/* ---------- achievements: compact list ---------- */
.record-row{
  display:flex;gap:10px;align-items:flex-start;
  padding:10px 4px;border-bottom:1px solid var(--line);
}
/* Locked rows dim by colour rather than opacity, same reasoning as above. */
.record-row:not(.unlocked) .record-name{color:var(--muted)}
.record-row:not(.unlocked) .record-desc{color:var(--muted)}

.drawer-link{
  width:100%;text-align:left;background:transparent;
  border:1px solid var(--line);border-radius:8px;color:var(--dust);
  font:inherit;font-size:13px;padding:11px 13px;cursor:pointer;
  transition:border-color .15s ease, color .15s ease;
}
.drawer-link:hover{border-color:var(--accent);color:var(--accent)}
.record-check{
  flex:0 0 auto;width:18px;text-align:center;
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:13px;color:var(--muted);
}
.record-row.unlocked .record-check{color:var(--state-affordable)}
.record-body{display:flex;flex-direction:column;gap:2px}
.record-name{font-weight:500;font-size:13px}
.record-desc{font-size:12px;color:var(--muted);line-height:1.4}

/* ---------- drawer: everything that isn't the tree or the core ---------- */
.drawer-backdrop{
  position:fixed;inset:0;z-index:24;background:color-mix(in srgb, var(--scrim) 67%, transparent);
  opacity:0;pointer-events:none;transition:opacity .25s ease;
}
.drawer-backdrop:not([hidden]){display:block}
.drawer-backdrop.show{opacity:1;pointer-events:auto}

.drawer{
  position:fixed;top:0;right:0;bottom:0;z-index:25;
  width:min(340px,86vw);
  background:var(--void-2);border-left:1px solid var(--line);
  padding:calc(env(safe-area-inset-top) + 20px) 20px 20px;
  overflow-y:auto;
  transform:translateX(100%);transition:transform .3s ease;
}
.drawer:not([hidden]){display:block}
.drawer.show{transform:translateX(0)}

.drawer-close{
  position:absolute;top:14px;right:16px;background:none;border:0;color:var(--muted);
  font-size:20px;line-height:1;cursor:pointer;padding:4px;
}
.drawer-close:hover{color:var(--dust)}

.drawer-footer{
  margin-top:30px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;
  font-size:11px;color:var(--muted);font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
}
.drawer-footer button{
  background:none;border:1px solid var(--line);color:var(--muted);
  font:inherit;padding:5px 10px;border-radius:4px;cursor:pointer;
}
.drawer-footer button:hover{border-color:var(--accent);color:var(--accent)}

.save-hint{font-size:12px;line-height:1.5;color:var(--muted);margin-bottom:12px}
.save-actions{display:flex;gap:10px;flex-wrap:wrap}
.save-actions button{
  background:none;border:1px solid var(--line);color:var(--dust);
  font:inherit;font-size:12px;padding:8px 12px;border-radius:6px;cursor:pointer;
}
.save-actions button:hover{border-color:var(--accent);color:var(--accent)}

.toast{
  position:fixed;left:50%;bottom:calc(env(safe-area-inset-bottom) + 14px);transform:translateX(-50%);
  z-index:15;
  max-width:calc(100vw - 32px);
  background:var(--void-2);border:1px solid var(--line);
  padding:10px 16px;border-radius:6px;font-size:13px;
  opacity:0;transition:opacity .4s ease;pointer-events:none;
  text-align:center;
}
.toast.show{opacity:1}

/* ---------- the update banner ----------
   Sits above the toast rather than beside it: a toast is a passing remark and
   this one waits for an answer. It's the only surface in the game with its own
   buttons at the bottom of the screen, so it stops short of the core's space
   and is dismissible — an update nobody wants shouldn't camp on the thumb.

   display:none in the base rule, display:flex only once JS drops the `hidden`
   attribute. Author CSS beats the browser's built-in [hidden]{display:none}
   regardless of specificity, so a bare display here would make this render
   permanently — the trap documented in CLAUDE.md. */
.update-banner{
  position:fixed;left:50%;bottom:calc(env(safe-area-inset-bottom) + 68px);
  transform:translateX(-50%);
  z-index:16;                       /* over the toast, under the Supernova prompt */
  display:none;
  align-items:center;gap:10px;
  max-width:calc(100vw - 24px);
  padding:8px 8px 8px 16px;border-radius:10px;
  background:var(--void-2);
  border:1px solid color-mix(in srgb, var(--epoch-7) 55%, var(--line));
  box-shadow:0 0 18px -4px color-mix(in srgb, var(--epoch-7) 45%, transparent);
  opacity:0;transition:opacity .3s ease;
}
.update-banner:not([hidden]){display:flex}
.update-banner.show{opacity:1}
.update-banner-text{font-size:13px;color:var(--dust)}
.update-banner-btn{
  background:none;border:1px solid var(--epoch-7);color:var(--epoch-7);
  font:inherit;font-size:12px;padding:0 14px;border-radius:6px;cursor:pointer;
  transition:background .18s ease, color .18s ease;
}
.update-banner-btn:hover{background:var(--epoch-7);color:var(--void)}
.update-banner-close{
  background:none;border:none;color:var(--muted);
  font-size:20px;line-height:1;cursor:pointer;
  display:grid;place-content:center;padding:0;
}
.update-banner-close:hover{color:var(--dust)}

/* Narrow phones: the text wraps rather than pushing the buttons off screen. */
@media (max-width:420px){
  .update-banner{gap:8px;padding-left:12px}
  .update-banner-text{font-size:12px}
}

/* ---------- the guide: what everything on screen means ---------- */
.guide-overlay{
  position:fixed;inset:0;z-index:28;display:none;
  background:color-mix(in srgb, var(--scrim) 90%, transparent);
  opacity:0;transition:opacity .25s ease;
  overflow-y:auto;
  padding:clamp(16px,5vw,40px) clamp(12px,4vw,24px);
}
.guide-overlay:not([hidden]){display:block}
.guide-overlay.show{opacity:1}

.guide-panel{
  max-width:560px;margin:0 auto;
  background:var(--void-2);border:1px solid var(--line);border-radius:16px;
  padding:clamp(20px,5vw,30px);position:relative;
}
.guide-title{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;font-size:clamp(22px,6vw,28px);
  color:var(--accent);margin-bottom:6px;
}
.guide-intro{font-size:13px;line-height:1.6;color:var(--dust);margin-bottom:22px}
.guide-section{margin-bottom:22px}
.guide-section:last-child{margin-bottom:0}
.guide-head{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;letter-spacing:.22em;
  text-transform:uppercase;color:var(--muted);
  border-bottom:1px solid var(--line);padding-bottom:7px;margin-bottom:12px;
}
.guide-row{display:flex;gap:12px;align-items:flex-start;margin-bottom:12px}
.guide-row:last-child{margin-bottom:0}
.guide-row-body{display:flex;flex-direction:column;gap:2px;min-width:0}
.guide-row-name{font-weight:500;font-size:13px}
.guide-row-desc{font-size:12px;line-height:1.45;color:var(--muted)}
.guide-key{
  flex:0 0 auto;min-width:74px;
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;color:var(--accent);
  padding-top:1px;
}
.guide-close{
  position:absolute;top:12px;right:14px;background:none;border:0;color:var(--muted);
  font-size:22px;line-height:1;cursor:pointer;padding:4px;
}
.guide-close:hover{color:var(--dust)}

/* ---------- THE FIRMAMENT ----------
   A fitted view, not a navigable canvas: objects sit at percentage
   coordinates and the field simply reflows into whatever space it has. That
   is why there is no pan/zoom here and no fixed canvas size. */
.light-readout{
  display:none;
  margin-top:4px;font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;
  letter-spacing:.12em;text-transform:uppercase;color:var(--epoch-7);
}
.light-readout:not([hidden]){display:block}

/* The way in: a portal standing on the timeline, not a button in the chrome.
   It has to read as a place rather than a control, so it's bigger than a node
   and carries its own slow rotating rings. */
/* One construction, two doors. Each portal is tinted with the colour of the
   place it LEADS TO, not the place it stands in — so the cyan one on the
   timeline reads as "the Firmament is through here", and the warm one in the
   Firmament reads as the way home. */
/* A DOOR, not a coloured circle. It was 92px of radial gradient with two thin
   rings, which at tree zoom read as one more node in a slightly different hue
   — and the one thing this element must never read as is a node. It's the only
   way to the second place in the game, so it gets to be the loudest object on
   either map.

   Four layers build the depth, and the depth is the whole trick: an event
   horizon that is DARKER than the canvas at the centre (so it reads as an
   opening rather than a blob), a bright rim around that opening, two counter-
   rotating rings, and an outer aura on the far side of the rim. Every one of
   them is a pseudo-element or an existing span — no extra markup, and the two
   portals stay one construction with two tints. */
.tree-portal,.sky-portal{
  position:absolute;top:0;left:0;width:148px;height:148px;
  transform:translate(-50%,-50%);
  border:0;padding:0;cursor:pointer;color:var(--portal-color);
  display:grid;place-content:center;
  background:none;
  transition:transform .3s cubic-bezier(.2,.7,.3,1), filter .3s ease;
  filter:drop-shadow(0 0 26px color-mix(in srgb, var(--portal-color) 55%, transparent));
}

/* The mouth: dark in the middle, brightening to a hot rim. A portal you can
   see THROUGH is a portal; a filled disc is a button. */
.tree-portal::before,.sky-portal::before{
  content:"";position:absolute;inset:20px;border-radius:50%;
  background:
    radial-gradient(circle at 50% 50%,
      color-mix(in srgb, var(--scrim) 92%, transparent) 0%,
      color-mix(in srgb, var(--scrim) 70%, transparent) 38%,
      color-mix(in srgb, var(--portal-color) 42%, transparent) 72%,
      color-mix(in srgb, var(--portal-color) 88%, transparent) 88%,
      transparent 100%);
  box-shadow:
    inset 0 0 26px color-mix(in srgb, var(--portal-color) 45%, transparent),
    0 0 30px -6px color-mix(in srgb, var(--portal-color) 70%, transparent);
  animation:portal-breathe 5.5s ease-in-out infinite;
}
@keyframes portal-breathe{50%{transform:scale(1.045)}}

/* The aura outside the rim — what makes it visible from across the map when
   you are zoomed out and hunting for it. */
.tree-portal::after,.sky-portal::after{
  content:"";position:absolute;inset:-6px;border-radius:50%;
  background:radial-gradient(circle at 50% 50%, transparent 58%, color-mix(in srgb, var(--portal-color) 26%, transparent) 74%, transparent 84%);
  pointer-events:none;
}

.tree-portal{--portal-color:var(--epoch-7)}
.sky-portal{--portal-color:var(--accent)}

.tree-portal svg,.sky-portal svg{width:52px;height:52px;position:relative;z-index:2;opacity:.95}
.tree-portal:hover,.sky-portal:hover{
  transform:translate(-50%,-50%) scale(1.06);
  filter:drop-shadow(0 0 38px color-mix(in srgb, var(--portal-color) 75%, transparent));
}
.tree-portal:focus-visible,.sky-portal:focus-visible{
  outline:2px solid var(--dust);outline-offset:6px;border-radius:50%;
}

/* Two counter-rotating rings, dashed rather than solid so the rotation is
   actually visible — a solid circle spinning looks like a circle standing
   still. Sized to sit just outside the mouth. */
.portal-ring{
  position:absolute;inset:8px;border-radius:50%;
  border:1px dashed var(--portal-color);opacity:.5;
  animation:portal-spin 14s linear infinite;
}
.portal-ring-2{
  inset:0;opacity:.28;border-style:solid;
  border-width:1px;border-color:color-mix(in srgb, var(--portal-color) 60%, transparent) transparent color-mix(in srgb, var(--portal-color) 60%, transparent) transparent;
  animation-duration:9s;animation-direction:reverse;
}
@keyframes portal-spin{to{transform:rotate(360deg)}}
.portal-label{color:var(--epoch-7)}
.portal-label-back{width:130px;text-align:center}
.portal-label-back .sky-label-name{color:var(--accent)}
.portal-label-back .sky-label-dist{color:var(--muted)}

/* The core and both button rails belong to the timeline, not here — the
   Firmament has its own back button and its own header. */
body.firmament-open .bottom-hud,
body.firmament-open .hud-rail,
body.firmament-open .hud-top{display:none}

.firmament-view{
  position:fixed;inset:0;z-index:22;display:none;
  flex-direction:column;
  background:
    radial-gradient(1200px 800px at 50% 0%, var(--sky-glow) 0%, transparent 70%),
    radial-gradient(1px 1px at 18% 32%, #ffffff66, transparent),
    radial-gradient(1px 1px at 72% 18%, #ffffff55, transparent),
    radial-gradient(1.5px 1.5px at 44% 74%, #ffffff4a, transparent),
    radial-gradient(1px 1px at 87% 58%, #ffffff45, transparent),
    radial-gradient(1px 1px at 28% 88%, #ffffff40, transparent),
    var(--void);
  opacity:0;transition:opacity .26s ease;
}
/* No padding and no flex column any more: the map fills the view edge to edge
   and every readout floats over it absolutely. */
.firmament-view:not([hidden]){display:block}
.firmament-view.show{opacity:1}

.firmament-label{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;letter-spacing:.24em;
  text-transform:uppercase;color:var(--muted);
}
.firmament-reach{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;
  font-size:clamp(20px,5.5vw,30px);color:var(--epoch-7);line-height:1.15;margin-top:2px;
}
.firmament-sub{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;color:var(--muted);
  margin-top:4px;letter-spacing:.08em;
}

/* ============================================================
   THE FIRMAMENT — one map, edge to edge
   ============================================================

   No panel, no card grid, no tab bar. The viewport IS the view and everything
   else floats over it, exactly the way the timeline works. Three kinds of
   thing share the sky: objects (which produce Light), stars (bought one at a
   time), and constellation hubs (the dial, once complete).

   Colours come from the epoch variables via --canvas-key/-deep/-glow, so this
   sits inside the game's palette and follows a palette cosmetic. The Firmament
   is a different PLACE, not a different product. */

/* Floating readouts. pointer-events:none throughout — they sit over a map you
   drag, and a readout that eats a pan is a readout that broke the map. */
.sky-readout{
  position:absolute;z-index:3;pointer-events:none;
  top:calc(env(safe-area-inset-top) + 14px);
}
.sky-readout-reach{left:50%;transform:translateX(-50%);text-align:center;max-width:56vw}
.sky-readout-light{right:14px;text-align:right}
.sky-light-line{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:clamp(14px,3.6vw,18px);color:var(--canvas-glow);
  text-shadow:0 0 14px color-mix(in srgb, var(--canvas-glow) 55%, transparent);
}
.sky-light-sub{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:10px;letter-spacing:.06em;color:var(--muted);margin-top:2px;
}

/* Bottom-left, clear of the top-left Back button and of the core's old space. */
.sky-actions{
  position:absolute;z-index:3;
  left:14px;bottom:calc(env(safe-area-inset-bottom) + 14px);
  display:flex;gap:8px;flex-wrap:wrap;
}

/* ---- stars ----
   A point of light, sized by its real apparent magnitude and coloured by its
   real spectral class. No ring, no fill, no badge: the star map's grammar is
   luminosity, and a star that looked like a timeline node would say the two
   places are the same place. */
.sky-star{
  position:absolute;transform:translate(-50%,-50%);
  padding:0;border:0;border-radius:50%;cursor:pointer;
  background:var(--star, var(--light-bright));
  opacity:.35;
  transition:opacity .3s ease, filter .3s ease, transform .18s ease;
}
.sky-star:hover{transform:translate(-50%,-50%) scale(1.25)}
.sky-star:focus-visible{outline:2px solid var(--dust);outline-offset:3px}
/* Unowned and unaffordable: a faint smudge, visibly out of reach. */
.sky-star.available{opacity:.22}
/* Affordable: it starts to shine, and pulses gently. */
.sky-star.affordable{
  opacity:.8;
  filter:drop-shadow(0 0 7px var(--star, var(--light-bright)));
  animation:star-ready 2.6s ease-in-out infinite;
}
@keyframes star-ready{50%{filter:drop-shadow(0 0 13px var(--star, var(--light-bright)))}}
/* Charted: full brightness, permanently. */
.sky-star.owned{
  opacity:1;
  filter:drop-shadow(0 0 10px var(--star, var(--light-bright)));
}

/* Distance tracks. Faint, still, and behind everything — an astrolabe's rings,
   not a decoration. Each sits at one object's own radius, so the spacing
   between them IS the log scale made visible. */
.sky-orbit{
  fill:none;
  stroke:color-mix(in srgb, var(--epoch-7) 13%, transparent);
  stroke-width:1;vector-effect:non-scaling-stroke;
  stroke-dasharray:2 10;
}
/* The rail. The one line the whole map hangs off, so it is the only thing here
   drawn solid — everything else is dashed or faint against it. */
.sky-spine{
  stroke:color-mix(in srgb, var(--epoch-7) 30%, transparent);
  stroke-width:2;vector-effect:non-scaling-stroke;stroke-linecap:round;
}
/* Distance gridlines crossing the rail, and the zone boundaries. Both sit UNDER
   the rail in weight: they are the grid it is drawn on, not more content. */
.sky-scale-ring{
  fill:none;
  stroke:color-mix(in srgb, var(--epoch-7) 16%, transparent);
  stroke-width:1;vector-effect:non-scaling-stroke;
  stroke-dasharray:2 6;
}
.sky-zone-edge{
  stroke:color-mix(in srgb, var(--epoch-7) 11%, transparent);
  stroke-width:1;vector-effect:non-scaling-stroke;
  stroke-dasharray:1 14;
}
.sky-zone-label{
  position:absolute;transform:translate(-50%,-50%);
  white-space:nowrap;pointer-events:none;
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;
  font-size:15px;letter-spacing:.3em;text-transform:uppercase;
  color:color-mix(in srgb, var(--epoch-7) 34%, var(--muted));
  opacity:.6;
}
.sky-scale-label{
  position:absolute;transform:translate(-50%,-50%);
  padding:0 8px;white-space:nowrap;pointer-events:none;
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:9.5px;letter-spacing:.2em;text-transform:uppercase;
  color:color-mix(in srgb, var(--epoch-7) 42%, var(--muted));
  opacity:.75;
}

/* A label on the left half of the map flips to the other side of its node and
   right-aligns, so it reads outward instead of pointing back across the
   centre. translateX(-100%) is what actually moves it; `left` is already set
   to the node's inner edge by the renderer. */
.sky-label.flip, .star-label.flip{
  transform:translate(-100%, -50%);
  text-align:right;
}
.sky-label.flip{align-items:flex-end}
.star-label.flip{align-items:flex-end}

.star-label{
  position:absolute;transform:translateY(-50%);
  display:flex;flex-direction:column;gap:1px;pointer-events:none;
  opacity:.6;transition:opacity .3s ease;
}
.star-label.owned{opacity:1}
.star-label-name{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:500;
  font-size:11px;color:var(--dust);white-space:nowrap;
}
.star-label-cost{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:9px;letter-spacing:.06em;color:var(--muted);white-space:nowrap;
}

/* ---- the figure lines ----
   Still, thin, and only drawn between two stars you actually own — so the
   shape assembles as you buy it and a finished constellation is a finished
   drawing. Deliberately NOT the timeline's animated flow. */
.star-line{
  stroke:color-mix(in srgb, var(--dust) 10%, transparent);
  stroke-width:1;vector-effect:non-scaling-stroke;
  stroke-dasharray:3 5;
  transition:stroke .5s ease;
}
.star-line.drawn{
  stroke:color-mix(in srgb, var(--canvas-key) 45%, transparent);
  stroke-dasharray:none;
}
.star-line.complete{stroke:color-mix(in srgb, var(--canvas-key) 75%, transparent)}
/* The tether from the object that opens a region — fainter than a real
   connector, because it's a route rather than a dependency you bought. */
.sky-tether{opacity:.3}

/* ---- the constellation hub ---- */
.sky-hub{
  position:absolute;transform:translate(-50%,-50%);
  display:flex;flex-direction:column;align-items:center;gap:2px;
  padding:8px 14px;border-radius:10px;cursor:pointer;
  background:color-mix(in srgb, var(--canvas-void) 72%, transparent);
  border:1px solid transparent;
  transition:border-color .3s ease, box-shadow .3s ease, opacity .3s ease;
}
.sky-hub:focus-visible{outline:2px solid var(--dust);outline-offset:3px}
.hub-name{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;
  font-size:15px;color:var(--dust);white-space:nowrap;
}
.hub-state{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:10px;letter-spacing:.08em;color:var(--muted);white-space:nowrap;
}
.sky-hub.locked{opacity:.4;cursor:default}
.sky-hub.partial{border-color:color-mix(in srgb, var(--line) 80%, transparent)}
.sky-hub.complete{border-color:color-mix(in srgb, var(--canvas-key) 55%, transparent)}
.sky-hub.complete .hub-state{color:var(--canvas-key)}
/* Lit: the constellation is running, and the hub says so from across the map. */
.sky-hub.lit{
  border-color:var(--canvas-key);
  box-shadow:0 0 26px -6px color-mix(in srgb, var(--canvas-key) 70%, transparent);
}
.sky-hub.lit .hub-state{color:var(--canvas-glow)}

/* ---- the constellation sheet ---- */
.socket-sub{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--muted);margin-top:3px;
}
.socket-effect{
  font-family:"IBM Plex Sans",system-ui,sans-serif;
  font-size:12.5px;line-height:1.5;color:var(--muted);margin:8px 0 0;
}
.socket-locked, .socket-no-socket{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:11px;line-height:1.6;letter-spacing:.04em;color:var(--muted);margin-top:12px;
}
.socket-no-socket{color:var(--epoch-4);margin-top:0}

.socket-readout{
  margin-top:12px;padding:9px 11px;border-radius:8px;
  background:color-mix(in srgb, var(--canvas-void) 55%, transparent);
  border-left:2px solid var(--canvas-key);
  display:flex;flex-direction:column;gap:3px;
}
.socket-headline{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:15px;color:var(--canvas-key);
}
.socket-detail{
  font-family:"IBM Plex Sans",system-ui,sans-serif;
  font-size:11.5px;line-height:1.45;color:var(--muted);
}

.socket-alloc{display:none;margin-top:14px;flex-direction:column;gap:10px}
.socket-alloc:not([hidden]){display:flex}

/* The only range input in the game, so it's styled from scratch in both
   engines. Track in the Canvas ground, thumb in the Firmament's own colour. */
.socket-slider{
  -webkit-appearance:none;appearance:none;width:100%;background:none;cursor:pointer;
}
.socket-slider:disabled{opacity:.4;cursor:default}
.socket-slider::-webkit-slider-runnable-track{
  height:5px;border-radius:3px;
  background:linear-gradient(90deg, var(--canvas-deep), var(--canvas-glow));
}
.socket-slider::-moz-range-track{
  height:5px;border-radius:3px;
  background:linear-gradient(90deg, var(--canvas-deep), var(--canvas-glow));
}
.socket-slider::-webkit-slider-thumb{
  -webkit-appearance:none;appearance:none;
  width:20px;height:20px;border-radius:50%;margin-top:-7.5px;
  background:var(--canvas-key);border:2px solid var(--void-2);
  box-shadow:0 0 12px -2px var(--canvas-key);
}
.socket-slider::-moz-range-thumb{
  width:20px;height:20px;border-radius:50%;border:2px solid var(--void-2);
  background:var(--canvas-key);box-shadow:0 0 12px -2px var(--canvas-key);
}
.socket-slider:focus-visible{outline:2px solid var(--dust);outline-offset:4px}

.socket-amount{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:11px;color:var(--muted);text-align:center;
}
.socket-buttons{display:flex;gap:6px}
.socket-btn{
  flex:1 1 0;min-width:0;
  background:none;border:1px solid var(--line);color:var(--dust);
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:11px;border-radius:7px;cursor:pointer;
  transition:border-color .18s ease, color .18s ease;
}
.socket-btn:hover{border-color:var(--canvas-key);color:var(--canvas-key)}
.socket-btn-reset:hover{border-color:var(--epoch-4);color:var(--epoch-4)}

/* The Firmament's own tree viewport — same machinery as the timeline's, its
   own instance. Nodes reuse .tree-node wholesale (states, shapes, glows) with
   the epoch colour forced to Light, so there's one visual language across
   both places and only the orientation and palette differ. */
.sky-viewport{
  position:absolute;inset:0;overflow:hidden;
  touch-action:none;cursor:grab;
}
.sky-viewport.dragging{cursor:grabbing}
.sky-viewport.easing .sky-canvas{transition:transform .5s cubic-bezier(.2,.7,.3,1)}
/* The canvas carries NO background of its own, exactly like .tree-canvas. It is
   a coordinate space, not a surface.

   This is the whole reason the Firmament used to look like a page with a map
   sitting on it. A background here — even a soft-edged one — paints a
   SKY_W x SKY_H rectangle that pans and zooms with the content, so the sky had
   an edge, and past that edge was something else. Feathering the edge only made
   the box blurry; it was still a box. The atmosphere belongs to .firmament-view,
   which is position:fixed inset:0 — it IS the screen, so there is nothing for
   the sky to sit inside and no boundary to find by panning. */
.sky-canvas{
  position:relative;transform-origin:0 0;
}
/* Same reasoning as .tree-canvas above — crisp at rest, cheap while moving. */
.sky-viewport.dragging .sky-canvas,
.sky-viewport.zooming .sky-canvas,
.sky-viewport.easing .sky-canvas{will-change:transform}

/* Sightlines, not circuitry. The timeline's connectors animate like current
   flowing through a circuit; these are thin, still, dashed — the geometry of
   looking at something far away, not of energy moving. */
.sky-lines path{
  fill:none;stroke:var(--line);stroke-width:1.2;
  stroke-dasharray:1 7;stroke-linecap:round;
  transition:stroke .8s ease, opacity .8s ease;
  opacity:.5;
}
.sky-lines path.lit{stroke:var(--epoch-7);opacity:.85}

/* Objects are points of light. No ring-and-fill construction, no state
   badge — brightness carries both how far you've got and how much you own,
   which is the one thing a sky can say that a circuit can't. */
.sky-node{
  position:absolute;top:0;left:0;
  transform:translate(-50%,-50%);
  border:0;padding:0;background:transparent;cursor:pointer;
  color:var(--epoch-7);
  display:grid;place-content:center;
  transition:transform .12s ease, color .4s ease, filter .4s ease;
}

/* ---- celestial bodies ----
   An object is drawn as the thing it actually is, not as a glyph of it. Every
   body is built the same way and differs only in its surface:

     1. a base fill in the object's real colour (--base)
     2. a surface, per `kind`, from layered gradients
     3. limb darkening + a terminator, both from one inset shadow, with the
        light coming from the UPPER LEFT on every body — that consistency is
        what makes a circle read as a sphere instead of a disc
     4. a thin rim of --accent on the lit side, which is the atmosphere

   All CSS, so there are no binary assets, nothing to license, nothing to
   precache, and it stays sharp at any zoom. A node carrying `image:` gets a
   real photograph instead and skips all of this — same markup, same size. */
.sky-body{
  position:absolute;inset:6%;border-radius:50%;
  background-color:var(--base);
  background-repeat:no-repeat;
  z-index:2;
  box-shadow:
    inset -7px -7px 16px rgba(0,0,0,.68),          /* the night side */
    inset 3px 3px 10px color-mix(in srgb, var(--accent) 35%, transparent);
}
.sky-body-image{background-size:cover;background-position:center}

/* Stars are lit from within, so they get no terminator — a sun with a night
   side is the one body that would look wrong. */
.body-star{
  background-image:
    radial-gradient(circle at 50% 50%, var(--accent) 0%, var(--base) 46%, color-mix(in srgb, var(--base) 70%, #000) 100%),
    radial-gradient(circle at 32% 30%, color-mix(in srgb, var(--accent) 70%, transparent) 0%, transparent 34%);
  box-shadow:
    inset 0 0 14px color-mix(in srgb, var(--accent) 60%, transparent),
    0 0 22px color-mix(in srgb, var(--base) 75%, transparent);
}

/* Rock: craters. Several small offset radials, darker on one edge and lit on
   the other, which is what a crater actually looks like from above. */
.body-rocky{
  background-image:
    radial-gradient(circle at 30% 26%, color-mix(in srgb, var(--accent) 55%, transparent) 0%, transparent 40%),
    radial-gradient(circle at 62% 58%, rgba(0,0,0,.34) 0%, transparent 16%),
    radial-gradient(circle at 36% 68%, rgba(0,0,0,.28) 0%, transparent 12%),
    radial-gradient(circle at 72% 30%, rgba(0,0,0,.26) 0%, transparent 10%),
    radial-gradient(circle at 50% 44%, rgba(255,255,255,.10) 0%, transparent 9%);
}

/* Gas: banding. Horizontal because that IS how a rotating fluid planet bands,
   with one oval storm sitting in the southern bands. */
.body-gas{
  background-image:
    radial-gradient(ellipse 22% 12% at 66% 62%, color-mix(in srgb, var(--base) 40%, #7a2f1e) 0%, transparent 70%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--accent) 45%, transparent) 0%,
      transparent 12%,
      color-mix(in srgb, #000 18%, transparent) 22%,
      transparent 32%,
      color-mix(in srgb, var(--accent) 35%, transparent) 44%,
      transparent 56%,
      color-mix(in srgb, #000 16%, transparent) 68%,
      transparent 78%,
      color-mix(in srgb, var(--accent) 28%, transparent) 90%);
}

/* Ice: bright, high albedo, with a cracked sheen rather than craters. */
.body-ice{
  background-image:
    radial-gradient(circle at 34% 28%, var(--accent) 0%, transparent 46%),
    linear-gradient(118deg, transparent 46%, color-mix(in srgb, var(--accent) 55%, transparent) 48%, transparent 50%),
    linear-gradient(62deg, transparent 60%, color-mix(in srgb, var(--accent) 40%, transparent) 62%, transparent 64%);
}

/* A belt is not a body. No terminator and no disc — scattered rubble, and the
   ring of it is the shape you're meant to read. */
.body-dust{
  background-color:transparent;
  box-shadow:none;
  background-image:
    radial-gradient(circle at 20% 44%, var(--accent) 0%, transparent 7%),
    radial-gradient(circle at 74% 32%, var(--base) 0%, transparent 6%),
    radial-gradient(circle at 52% 72%, var(--accent) 0%, transparent 5%),
    radial-gradient(circle at 34% 22%, var(--base) 0%, transparent 4%),
    radial-gradient(circle at 84% 66%, var(--accent) 0%, transparent 4%),
    radial-gradient(circle at 12% 74%, var(--base) 0%, transparent 3%),
    radial-gradient(circle at 62% 12%, var(--accent) 0%, transparent 3%);
}

/* Diffuse gas — a boundary or a cavity, so it has no edge at all. The soft
   falloff is the point: you can't stand on a heliopause. */
.body-cloud{
  background-color:transparent;
  box-shadow:none;
  background-image:
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--accent) 40%, transparent) 0%, color-mix(in srgb, var(--base) 30%, transparent) 42%, transparent 72%),
    radial-gradient(ellipse 60% 40% at 36% 38%, color-mix(in srgb, var(--accent) 30%, transparent) 0%, transparent 60%);
  filter:blur(.4px);
}

/* A spiral: bright core, arms sweeping out of it, seen at an angle. */
.body-galaxy{
  background-color:transparent;
  box-shadow:none;
  background-image:
    radial-gradient(circle at 50% 50%, var(--accent) 0%, color-mix(in srgb, var(--accent) 50%, transparent) 12%, transparent 28%),
    conic-gradient(from 20deg at 50% 50%,
      color-mix(in srgb, var(--base) 70%, transparent) 0deg,
      transparent 60deg,
      color-mix(in srgb, var(--base) 55%, transparent) 150deg,
      transparent 220deg,
      color-mix(in srgb, var(--base) 65%, transparent) 300deg,
      transparent 360deg);
  transform:rotate(-18deg) scaleY(.62);
}

/* A black hole is a hole. The centre is genuinely black — darker than the
   canvas — and everything you can see is the accretion disc around it. */
.body-void{
  background-color:#000;
  box-shadow:0 0 20px -2px color-mix(in srgb, var(--accent) 70%, transparent);
  background-image:
    radial-gradient(circle at 50% 50%, #000 0%, #000 38%, var(--accent) 43%, color-mix(in srgb, var(--accent) 40%, transparent) 52%, transparent 62%),
    radial-gradient(ellipse 78% 16% at 50% 50%, color-mix(in srgb, var(--accent) 85%, transparent) 0%, transparent 72%);
}

/* Filaments and voids — the largest structure there is, and the only one on
   this map that isn't round. */
.body-web{
  background-color:transparent;
  box-shadow:none;
  background-image:
    linear-gradient(28deg, transparent 44%, color-mix(in srgb, var(--accent) 55%, transparent) 46%, transparent 48%),
    linear-gradient(-52deg, transparent 40%, color-mix(in srgb, var(--accent) 45%, transparent) 42%, transparent 44%),
    linear-gradient(96deg, transparent 56%, color-mix(in srgb, var(--base) 70%, transparent) 58%, transparent 60%),
    radial-gradient(circle at 32% 38%, var(--accent) 0%, transparent 6%),
    radial-gradient(circle at 68% 62%, var(--accent) 0%, transparent 5%),
    radial-gradient(circle at 56% 26%, var(--base) 0%, transparent 4%);
}

/* A body must never swallow the tap it sits on: the button underneath is the
   tap target, and .sky-body covers almost all of it. */
.sky-body{pointer-events:none}

/* ============================================================
   BESPOKE BODIES — each object drawn as ITSELF
   ============================================================

   The `body-<kind>` rules above are the generic treatment: they make any new
   node render sensibly the instant it's added to FIRMAMENT. These are the
   per-object ones, keyed on data-body, and they exist because nine kinds of
   shaded ball is still nine shaded balls. What makes a map worth looking at is
   silhouette — a thing you recognise before you've read its label.

   So: the gas giants get a ring, Alpha Centauri is three stars because it IS
   three stars, the heliopause is a bow shock rather than a sphere because it's
   the shape of the Sun's wake through the interstellar medium, and Sagittarius
   A* gets jets. Two extra paint layers per body (::before and ::after) is the
   whole budget, and none of it costs an asset.

   Anything that moves here is slow enough to read as drift rather than
   animation, and the global prefers-reduced-motion rule switches all of it
   off. */

/* ---- Sol: a corona, not an orange dot ----
   The corona is a conic gradient so it has actual structure rather than an
   even halo, and it turns once a minute — barely perceptible, but it stops the
   star reading as a static sticker. */
.sky-body[data-body="sol"]::before{
  content:"";position:absolute;inset:-42%;border-radius:50%;
  background:conic-gradient(from 0deg,
    color-mix(in srgb, var(--base) 34%, transparent) 0deg,
    transparent 26deg,
    color-mix(in srgb, var(--accent) 26%, transparent) 58deg,
    transparent 96deg,
    color-mix(in srgb, var(--base) 30%, transparent) 148deg,
    transparent 196deg,
    color-mix(in srgb, var(--accent) 22%, transparent) 244deg,
    transparent 292deg,
    color-mix(in srgb, var(--base) 28%, transparent) 330deg,
    transparent 360deg);
  -webkit-mask:radial-gradient(circle, transparent 38%, #000 52%, transparent 88%);
  mask:radial-gradient(circle, transparent 38%, #000 52%, transparent 88%);
  animation:sol-corona 68s linear infinite;
  pointer-events:none;
}
@keyframes sol-corona{to{transform:rotate(360deg)}}
/* Granulation + a couple of sunspots, so the disc has a surface. */
.sky-body[data-body="sol"]{
  background-image:
    radial-gradient(circle at 62% 58%, rgba(120,40,0,.42) 0%, transparent 9%),
    radial-gradient(circle at 38% 36%, rgba(120,40,0,.32) 0%, transparent 6%),
    radial-gradient(circle at 50% 50%, var(--accent) 0%, var(--base) 52%, color-mix(in srgb, var(--base) 62%, #7a2600) 100%);
}

/* ---- The Inner Worlds: FOUR planets, because that is what they are ----
   One sphere for four rocky planets was the laziest thing on the map. No
   shading on the container; each world is its own little lit disc. */
.sky-body[data-body="inner_worlds"]{
  background-color:transparent;box-shadow:none;
  background-image:
    radial-gradient(circle at 22% 62%, #d9d0c6 0%, #6e6259 52%, transparent 53%),
    radial-gradient(circle at 44% 34%, #f0d9a8 0%, #b08d52 55%, transparent 56%),
    radial-gradient(circle at 68% 60%, #7fc3e8 0%, #2f6f9e 55%, transparent 56%),
    radial-gradient(circle at 86% 34%, #e08a5a 0%, #8c3d22 55%, transparent 56%);
  background-size:26% 26%, 30% 30%, 32% 32%, 24% 24%;
  background-position:16% 60%, 36% 30%, 60% 58%, 82% 30%;
}

/* ---- The Belt: an annulus of rubble seen at an angle ----
   A belt is a ring with a hole in it. Drawn as one, and tilted, so it can
   never be mistaken for a planet. */
.sky-body[data-body="asteroid_belt"]{
  background-color:transparent;box-shadow:none;background-image:none;
}
.sky-body[data-body="asteroid_belt"]::before{
  content:"";position:absolute;inset:-6%;border-radius:50%;
  border:2px dotted color-mix(in srgb, var(--accent) 70%, transparent);
  transform:rotate(-16deg) scaleY(.36);
}
.sky-body[data-body="asteroid_belt"]::after{
  content:"";position:absolute;inset:0;
  background-image:
    radial-gradient(circle at 12% 50%, var(--accent) 0%, transparent 6%),
    radial-gradient(circle at 84% 46%, var(--base) 0%, transparent 5%),
    radial-gradient(circle at 46% 34%, var(--accent) 0%, transparent 4%),
    radial-gradient(circle at 62% 66%, var(--base) 0%, transparent 4%),
    radial-gradient(circle at 30% 66%, var(--accent) 0%, transparent 3%);
  transform:rotate(-16deg) scaleY(.5);
}

/* ---- The Gas Giants: bands AND a ring ----
   The ring is what makes this instantly readable at a glance, and it's the
   single most recognisable silhouette in the solar system. */
.sky-body[data-body="gas_giants"]::after{
  content:"";position:absolute;inset:-30% -46%;border-radius:50%;
  border:2px solid color-mix(in srgb, var(--accent) 62%, transparent);
  border-top-color:color-mix(in srgb, var(--accent) 22%, transparent);
  transform:rotate(-19deg) scaleY(.3);
  pointer-events:none;
}

/* ---- The Kuiper Belt: a wide, sparse, icy ring ----
   Forty AU out and mostly empty, so it's drawn far wider and fainter than the
   asteroid belt — the two must not read as the same object twice. */
.sky-body[data-body="kuiper_belt"]{
  background-color:transparent;box-shadow:none;background-image:none;
}
.sky-body[data-body="kuiper_belt"]::before{
  content:"";position:absolute;inset:-18% -30%;border-radius:50%;
  border:1px dashed color-mix(in srgb, var(--accent) 55%, transparent);
  transform:rotate(-10deg) scaleY(.42);
}
.sky-body[data-body="kuiper_belt"]::after{
  content:"";position:absolute;inset:0;
  background-image:
    radial-gradient(circle at 50% 50%, var(--accent) 0%, transparent 14%),
    radial-gradient(circle at 16% 44%, var(--accent) 0%, transparent 5%),
    radial-gradient(circle at 88% 54%, var(--accent) 0%, transparent 4%);
}

/* ---- The Heliopause: a bow shock ----
   The heliosphere is not a bubble — it's blunt into the interstellar wind and
   drawn out into a tail behind. An asymmetric border-radius gives that shape
   without a clip-path, and it's the only non-round body in the solar system. */
.sky-body[data-body="heliopause"]{
  background-color:transparent;box-shadow:none;
  border-radius:64% 36% 40% 60% / 58% 58% 42% 42%;
  background-image:
    radial-gradient(ellipse 70% 90% at 34% 50%, color-mix(in srgb, var(--accent) 42%, transparent) 0%, color-mix(in srgb, var(--base) 26%, transparent) 46%, transparent 74%);
}
.sky-body[data-body="heliopause"]::before{
  content:"";position:absolute;inset:-14% -8% -14% -22%;
  border-radius:64% 36% 40% 60% / 58% 58% 42% 42%;
  border-left:2px solid color-mix(in srgb, var(--accent) 60%, transparent);
  border-top:1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-bottom:1px solid color-mix(in srgb, var(--accent) 30%, transparent);
}

/* ---- Alpha Centauri: THREE stars ----
   Rigil Kentaurus and Toliman are a close sunlike pair; Proxima is a small red
   dwarf well off to one side. Drawing one star here was simply wrong. */
.sky-body[data-body="alpha_centauri"]{
  background-color:transparent;box-shadow:none;
  background-image:
    radial-gradient(circle at 36% 42%, #fff8e8 0%, #ffd08a 40%, transparent 62%),
    radial-gradient(circle at 62% 56%, #fff2d8 0%, #ffbf6a 42%, transparent 60%);
  background-size:56% 56%, 40% 40%;
  background-position:22% 30%, 54% 50%;
}
.sky-body[data-body="alpha_centauri"]::after{
  content:"";position:absolute;right:2%;top:12%;width:16%;height:16%;
  border-radius:50%;background:#e8603c;
  box-shadow:0 0 7px #e8603c;
}

/* ---- The Local Bubble: a cavity, not an object ----
   An irregular hollow blown clear by supernovae. Ragged edge, stars visible
   inside it, and nothing at the centre — because there is nothing there. */
.sky-body[data-body="local_bubble"]{
  background-color:transparent;box-shadow:none;
  border-radius:58% 42% 36% 64% / 46% 62% 38% 54%;
  background-image:
    radial-gradient(circle at 50% 50%, transparent 42%, color-mix(in srgb, var(--base) 30%, transparent) 62%, transparent 82%),
    radial-gradient(circle at 34% 40%, var(--accent) 0%, transparent 4%),
    radial-gradient(circle at 68% 58%, var(--accent) 0%, transparent 3%),
    radial-gradient(circle at 52% 72%, var(--accent) 0%, transparent 3%);
  border:1px solid color-mix(in srgb, var(--base) 45%, transparent);
}

/* ---- The Orion Arm: a SEGMENT of a spiral, not a galaxy ----
   We're inside this one, so you can't see it face on. It's a swept band with
   the star-forming ridge running along it. */
.sky-body[data-body="orion_arm"]{
  background-color:transparent;box-shadow:none;
  border-radius:50%;
  background-image:
    radial-gradient(ellipse 90% 26% at 50% 50%, color-mix(in srgb, var(--accent) 55%, transparent) 0%, color-mix(in srgb, var(--base) 40%, transparent) 40%, transparent 72%),
    radial-gradient(circle at 30% 50%, var(--accent) 0%, transparent 5%),
    radial-gradient(circle at 58% 46%, var(--accent) 0%, transparent 4%),
    radial-gradient(circle at 76% 54%, var(--accent) 0%, transparent 3%);
  transform:rotate(-24deg);
}

/* ---- Sagittarius A*: a shadow, a photon ring, and jets ----
   The bright ring is lensed light passing behind it; the centre is genuinely
   black. The jets are what a supermassive black hole is actually visible by. */
.sky-body[data-body="sagittarius_a"]{
  background-color:#000;
  background-image:
    radial-gradient(circle at 50% 50%, #000 0%, #000 34%, var(--accent) 38%, color-mix(in srgb, var(--accent) 55%, transparent) 46%, transparent 58%);
  box-shadow:0 0 26px -4px color-mix(in srgb, var(--accent) 80%, transparent);
}
.sky-body[data-body="sagittarius_a"]::before{
  content:"";position:absolute;inset:-10% -34%;border-radius:50%;
  background:radial-gradient(ellipse 100% 40% at 50% 50%, color-mix(in srgb, var(--accent) 55%, transparent) 0%, transparent 68%);
  transform:rotate(-8deg) scaleY(.42);
}
.sky-body[data-body="sagittarius_a"]::after{
  content:"";position:absolute;left:50%;top:-46%;width:8%;height:192%;
  transform:translateX(-50%) rotate(14deg);
  background:linear-gradient(180deg,
    transparent 0%, color-mix(in srgb, var(--accent) 45%, transparent) 22%,
    transparent 46%, transparent 54%,
    color-mix(in srgb, var(--accent) 45%, transparent) 78%, transparent 100%);
  pointer-events:none;
}

/* ---- The Local Group: several galaxies, different sizes and angles ----
   The Milky Way, Andromeda larger than it, and the small ones. A cluster reads
   as a cluster only if its members differ. */
.sky-body[data-body="local_group"]{
  background-color:transparent;box-shadow:none;background-image:none;
}
.sky-body[data-body="local_group"]::before{
  content:"";position:absolute;left:2%;top:14%;width:52%;height:52%;
  border-radius:50%;
  background:radial-gradient(circle at 50% 50%, var(--accent) 0%, color-mix(in srgb, var(--base) 65%, transparent) 34%, transparent 70%);
  transform:rotate(-28deg) scaleY(.44);
}
.sky-body[data-body="local_group"]::after{
  content:"";position:absolute;right:0;bottom:8%;width:40%;height:40%;
  border-radius:50%;
  background:
    radial-gradient(circle at 50% 50%, var(--accent) 0%, color-mix(in srgb, var(--base) 60%, transparent) 36%, transparent 72%);
  transform:rotate(22deg) scaleY(.5);
  box-shadow:-140% -60% 0 -34% color-mix(in srgb, var(--accent) 60%, transparent),
              90% -120% 0 -36% color-mix(in srgb, var(--base) 70%, transparent);
}

/* ---- The Cosmic Web: filaments meeting at nodes ----
   Sheets and threads around voids. The bright points are where filaments
   cross, which is where the clusters actually sit. */
.sky-body[data-body="cosmic_web"]{
  background-color:transparent;box-shadow:none;
  background-image:
    linear-gradient(34deg, transparent 47%, color-mix(in srgb, var(--accent) 60%, transparent) 48.5%, transparent 50%),
    linear-gradient(-58deg, transparent 43%, color-mix(in srgb, var(--accent) 48%, transparent) 44.5%, transparent 46%),
    linear-gradient(88deg, transparent 58%, color-mix(in srgb, var(--base) 75%, transparent) 59.5%, transparent 61%),
    linear-gradient(-12deg, transparent 66%, color-mix(in srgb, var(--base) 60%, transparent) 67.5%, transparent 69%);
}
.sky-body[data-body="cosmic_web"]::after{
  content:"";position:absolute;inset:0;
  background-image:
    radial-gradient(circle at 33% 39%, var(--accent) 0%, transparent 5%),
    radial-gradient(circle at 67% 61%, var(--accent) 0%, transparent 4%),
    radial-gradient(circle at 58% 27%, var(--base) 0%, transparent 4%),
    radial-gradient(circle at 24% 70%, var(--base) 0%, transparent 3%);
}


.sky-node svg{
  width:var(--icon-size,26px);height:var(--icon-size,26px);
  position:relative;z-index:2;
}
.sky-node::before{
  content:"";position:absolute;inset:0;border-radius:50%;
  background:radial-gradient(circle at 50% 50%,
    color-mix(in srgb, currentColor 38%, transparent) 0%,
    color-mix(in srgb, currentColor 10%, transparent) 45%,
    transparent 72%);
  transition:background .4s ease;
}
.sky-node:hover{transform:translate(-50%,-50%) scale(1.09)}
.sky-node:focus-visible{outline:2px solid var(--dust);outline-offset:4px;border-radius:50%}

/* ---- branch cards ----
   A branch is a rectangular card, a spine body is a circle. Different shapes
   because they are different kinds of thing: a card is something you buy FOR a
   place, a circle IS a place. Square-ish corners and a plate behind the art, so
   a card reads as an item in a list even when its art is a little planet. */
.sky-node.branch{
  border-radius:10px;
  background:color-mix(in srgb, var(--void) 78%, transparent);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--epoch-7) 22%, transparent);
}
.sky-node.branch.affordable{
  box-shadow:inset 0 0 0 1px var(--state-affordable),
             0 0 14px color-mix(in srgb, var(--state-affordable) 45%, transparent);
}
.sky-node.branch.owned{
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--light-bright) 55%, transparent);
}
.sky-node.branch.locked{
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--line) 60%, transparent);
  filter:grayscale(1);
}
/* Rank counter on an owned card — "3" beside the art, the card equivalent of
   the brightness the spine bodies use to show how many you hold. */
.sky-rank{
  position:absolute;right:-6px;bottom:-6px;
  min-width:17px;height:17px;padding:0 4px;border-radius:9px;
  display:grid;place-content:center;
  background:var(--void);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--light-bright) 55%, transparent);
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:10px;color:var(--light-bright);
}

.sky-node.locked{color:var(--line);cursor:not-allowed}
.sky-node.locked:hover{transform:translate(-50%,-50%)}
.sky-unknown{
  width:8px;height:8px;border-radius:50%;background:var(--line);
  display:block;
}

.sky-node.available{color:var(--state-available);filter:drop-shadow(0 0 4px var(--state-available))}
.sky-node.affordable{
  color:var(--state-affordable);
  animation:sky-ready 1.8s ease-in-out infinite;
}
@keyframes sky-ready{
  0%,100%{filter:drop-shadow(0 0 5px var(--state-affordable))}
  50%{filter:drop-shadow(0 0 16px var(--state-affordable))}
}

/* --glow scales with how many you own: the sky brightens as you fill it in */
.sky-node.owned{
  color:var(--epoch-7);
  filter:drop-shadow(0 0 var(--glow,10px) var(--epoch-7));
}
.sky-node.keystone.owned{color:var(--light-bright)}

.sky-node.discover{animation:sky-appear 1.1s ease-out}
@keyframes sky-appear{
  0%{opacity:0;transform:translate(-50%,-50%) scale(.2)}
  60%{opacity:1;transform:translate(-50%,-50%) scale(1.15)}
  100%{transform:translate(-50%,-50%) scale(1)}
}

/* Labels sit to the right of each object, stacked — the map reads outward,
   so a label above the node would fight the direction of travel. */
/* 155px, not 130. The distance line is the widest thing in here and it is real
   prose — "300.0 million light years" is 25 characters, which overflowed a
   130px label and wrapped "years" onto a ragged line of its own, right-aligned
   by the .flip rule. Sized to the longest string the catalogue can produce
   rather than shortening the wording to fit the box. tools/canvas-test.js
   measures collisions at this width, so the two cannot drift apart. */
.sky-label{
  position:absolute;transform:translateY(-50%);
  display:flex;flex-direction:column;gap:1px;
  pointer-events:none;width:155px;
}
.sky-label-name{font-size:12px;font-weight:500;color:var(--dust);line-height:1.2}
.sky-label-dist{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;color:var(--epoch-7);opacity:.85;
}
.sky-label-cost{font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;color:var(--muted)}
/* What would open an unreached object, in place of a price it doesn't have. */
.sky-label-req{font-size:9.5px;line-height:1.3;color:var(--muted);opacity:.85}
/* An unreached object's label reads quieter than a charted one, so the charted
   sky still carries the eye while the dark half stays legible. */
.sky-label.dim .sky-label-name{color:var(--muted)}
.sky-label.dim{opacity:.72}

.sky-exit{
  position:absolute;top:calc(env(safe-area-inset-top) + 14px);left:14px;
  z-index:3;
}
.firmament-back{
  background:transparent;border:1px solid var(--line);color:var(--muted);
  font:inherit;font-size:12px;padding:9px 18px;border-radius:20px;cursor:pointer;
}
.firmament-back:hover{border-color:var(--epoch-7);color:var(--epoch-7)}
/* The zone jump wears the same pill as its neighbours — it sits in the same
   row and does the same kind of job, so it should not look like a form field
   that wandered in. appearance:none because the native select chrome is a
   different shape on every platform. */
.sky-jump{appearance:none;-webkit-appearance:none;padding-right:30px;
  background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),
                   linear-gradient(135deg,currentColor 50%,transparent 50%);
  background-position:calc(100% - 15px) 50%,calc(100% - 10px) 50%;
  background-size:5px 5px,5px 5px;background-repeat:no-repeat}
.sky-jump option{background:var(--void);color:var(--dust)}

.sky-icon{border-color:var(--epoch-7);color:var(--epoch-7)}
.sky-epoch{color:var(--epoch-7)}
.sky-buy{--node-color:var(--epoch-7)}

@media (max-height:620px){
  .firmament-title{font-size:clamp(18px,5vw,24px)}
  .firmament-view{padding-top:calc(env(safe-area-inset-top) + 10px)}
}

/* At phone width the two floating readouts compete for one strip and lose.
   The reach is centred with max-width:56vw; on a 390px screen that is 218px,
   which "300.0 million light years" wraps to two lines to fill, putting its
   centred box at x 86-304 — straight under the Light block, which right-aligns
   from 220 to 376. They overlapped by 84px and the heading ran through
   "0.0 assigned · 0/2 sockets".

   There is no font size that fixes this: a centred 25-character title and a
   right-aligned four-line readout cannot share a 390px row. So below 560px the
   reach drops beneath the Light block and takes the full width instead of
   fighting it. Both stay pointer-events:none, so neither can take a tap meant
   for the sky. */
@media (max-width:560px){
  .sky-readout-reach{
    top:calc(env(safe-area-inset-top) + 78px);
    max-width:calc(100vw - 28px);
  }
  .firmament-reach{font-size:clamp(18px,6vw,24px)}
}

/* ---------- welcome-back panel ---------- */
.offline-figure{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:clamp(26px,8vw,40px);
  color:var(--state-affordable);letter-spacing:-.02em;margin-bottom:4px;
}
.offline-detail{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:12px;color:var(--muted);
  margin-bottom:18px;
}
.offline-cap{
  display:none;   /* only once JS clears [hidden] — see the note in CLAUDE.md */
  font-size:12px;line-height:1.5;color:var(--muted);
  border-left:2px solid var(--line);padding-left:10px;margin-bottom:18px;
}
.offline-cap:not([hidden]){display:block}
.offline-dismiss{--node-color:var(--accent)}

.daily-block{
  display:none;   /* only once JS clears [hidden] */
  border:1px solid var(--accent);border-radius:12px;
  padding:14px 16px;margin-bottom:18px;
  background:var(--accent-soft);
}
.daily-block:not([hidden]){display:block}
.daily-streak{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--accent);margin-bottom:6px;
}
.daily-reward{font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:20px;color:var(--dust)}
.daily-note{font-size:12px;line-height:1.5;color:var(--muted);margin-top:6px}

/* ---------- epoch transition — the payoff for the whole loop ---------- */
.transition-overlay{
  position:fixed;inset:0;z-index:30;display:none;align-items:center;justify-content:center;
  padding:clamp(20px,6vw,32px);text-align:center;
  background:radial-gradient(700px 500px at 50% 40%, var(--accent-soft), var(--void) 75%);
  opacity:0;pointer-events:none;transition:opacity .5s ease;
}
/* only take over the screen (and start intercepting clicks) once JS removes `hidden` */
.transition-overlay:not([hidden]){display:flex;cursor:pointer}
.transition-overlay.show{opacity:1;pointer-events:auto}

.transition-label{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:12px;letter-spacing:.3em;text-transform:uppercase;
  color:var(--muted);margin-bottom:14px;
  opacity:0;transform:translateY(6px);
  animation:transition-rise .6s ease-out .15s forwards;
}
.transition-name{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;color:var(--accent);
  font-size:clamp(30px,10vw,56px);line-height:1.05;margin-bottom:20px;
  opacity:0;transform:translateY(10px) scale(.94);
  animation:transition-rise-name .8s cubic-bezier(.2,.8,.3,1) .35s forwards;
}
.transition-flavour{
  max-width:420px;font-size:14px;line-height:1.6;color:var(--dust);
  opacity:0;transform:translateY(6px);
  animation:transition-rise .6s ease-out .75s forwards;
}
@keyframes transition-rise{to{opacity:1;transform:translateY(0)}}
@keyframes transition-rise-name{to{opacity:1;transform:translateY(0) scale(1)}}

@media (max-height:560px){
  .core{width:clamp(84px,22vw,120px);height:clamp(84px,22vw,120px)}
  .bottom-hud{gap:8px;padding-bottom:calc(env(safe-area-inset-bottom) + 12px)}
  .hud-top{padding:10px 12px 10px;gap:4px}
  /* The left rail can reach four buttons once the install pill appears, and a
     landscape phone doesn't have 290px of vertical rail to give. The captions
     stay — they're the reason the rail is legible at all. */
  .hud-rail{gap:14px;top:12px}
  /* The banner clears the core on a tall screen; on a short one it has to come
     down to the toast's own height or it lands in the middle of the tree. */
  .update-banner{bottom:calc(env(safe-area-inset-bottom) + 12px)}
  .epoch-name{font-size:clamp(18px,4.5vw,24px);margin-top:2px}
  .epoch-next{margin-top:2px}
  .count{font-size:clamp(20px,6vw,30px)}
  .rate{margin-top:0}
  .cosmic-clock{margin-top:2px;font-size:10px}
  .tree-node-name{font-size:10px;width:88px;margin-top:-11px}
  /* the readability pass gave these room to breathe; a short/landscape
     screen doesn't have the room to give, so they go back to compact */
  .first-buy-hint{font-size:12px;line-height:1.5;padding:10px 28px 10px 14px}
  .event-prompt{padding:14px 20px;gap:5px}
  .event-prompt-title{font-size:17px}
  .event-prompt-sub{font-size:12px}
}

@media (prefers-reduced-motion:reduce){
  *{animation:none!important;transition:none!important}
  .tree-node.available, .tree-node.affordable{animation:none}
  /* With its animation off the burst ring would just sit at full opacity for
     the 600ms before burstNode() removes it — a flash, not a flourish. */
  .node-burst{display:none}
}

/* ---------- account, profile and friends ---------- */
.account-form{display:flex;flex-direction:column;gap:14px;margin-bottom:14px}
.account-field{display:none;flex-direction:column;gap:6px}
/* #nameField is toggled with the `hidden` attribute (it only belongs on the
   Create account tab), and a bare `display` in the base rule would beat the
   browser's own [hidden]{display:none} regardless of specificity. See the
   standing note in CLAUDE.md — this has now shipped as a real bug three times. */
.account-field:not([hidden]){display:flex}
.account-field span{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--muted);
}
.account-field input, .account-inline input{
  background:var(--void);border:1px solid var(--line);border-radius:8px;
  color:var(--dust);font:inherit;font-size:14px;padding:11px 13px;
}
.account-field input:focus, .account-inline input:focus{outline:none;border-color:var(--accent)}
.account-inline{flex-direction:row;gap:10px;flex-wrap:wrap;align-items:center}
.account-inline input{
  flex:1 1 160px;min-width:0;
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;letter-spacing:.18em;text-transform:uppercase;
}
.account-submit{
  background:var(--accent);border:0;border-radius:8px;color:var(--void);
  font:inherit;font-weight:500;font-size:14px;padding:12px 20px;cursor:pointer;
}
.account-submit:disabled{opacity:.5;cursor:not-allowed}
.account-toggle{
  background:none;border:0;color:var(--muted);font:inherit;font-size:12px;
  cursor:pointer;padding:4px 0;text-decoration:underline;
}
.account-toggle:hover{color:var(--dust)}
.account-error{
  display:none;   /* only once JS clears [hidden] — see the note in CLAUDE.md */
  font-size:12px;line-height:1.5;color:var(--epoch-4);
  border-left:2px solid var(--epoch-4);padding-left:10px;
}
.account-error:not([hidden]){display:block}
.friend-code{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;letter-spacing:.2em;color:var(--accent);
}

.friend-row{display:flex;gap:12px;align-items:center;padding:11px 0;border-bottom:1px solid var(--line)}
.friend-row:last-child{border-bottom:0}
.friend-body{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
.friend-name{font-weight:500;font-size:14px;overflow-wrap:anywhere}
.friend-meta{font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;color:var(--muted)}
.friend-pip{
  flex:0 0 auto;width:30px;height:30px;border-radius:50%;
  display:grid;place-content:center;
  background:var(--void);border:2px solid var(--epoch-color,var(--accent));
  color:var(--epoch-color,var(--accent));
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:600;font-size:14px;
}
/* The pip borrows the epoch palette, so how far along a friend is reads as
   colour in the list exactly as it does on the tree. */
.friend-pip[data-epoch="1"]{--epoch-color:var(--epoch-1)}
.friend-pip[data-epoch="2"]{--epoch-color:var(--epoch-2)}
.friend-pip[data-epoch="3"]{--epoch-color:var(--epoch-3)}
.friend-pip[data-epoch="4"]{--epoch-color:var(--epoch-4)}
.friend-pip[data-epoch="5"]{--epoch-color:var(--epoch-5)}
.friend-pip[data-epoch="6"]{--epoch-color:var(--epoch-6)}
.friend-pip[data-epoch="7"]{--epoch-color:var(--epoch-7)}
.friend-pip[data-epoch="8"]{--epoch-color:var(--epoch-8)}

.request-actions{display:flex;gap:6px;flex-shrink:0}
.request-actions button{
  background:none;border:1px solid var(--line);color:var(--muted);
  font:inherit;font-size:11px;padding:6px 10px;border-radius:6px;cursor:pointer;
}
.request-actions button:hover{border-color:var(--accent);color:var(--accent)}

/* Friends on the tree. These live on #treeCanvas with the nodes, so they pan
   and zoom with it for free — no second transform to keep in step. */
.friend-marker{
  position:absolute;transform:translate(-50%,-50%);
  display:flex;flex-direction:column;align-items:center;gap:3px;
  pointer-events:none;   /* never in the way of buying the node underneath */
  z-index:4;
}
.friend-marker .friend-pip{width:26px;height:26px;font-size:12px;box-shadow:0 0 12px -4px var(--epoch-color,var(--accent))}
.friend-marker-name{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:9px;letter-spacing:.08em;
  color:var(--dust);background:var(--void-2);border:1px solid var(--line);
  border-radius:10px;padding:2px 7px;white-space:nowrap;max-width:110px;
  overflow:hidden;text-overflow:ellipsis;
}

.conflict-pair{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:18px;margin-top:18px}
.conflict-card{border:1px solid var(--line);border-radius:12px;padding:16px}
.conflict-card .account-submit{width:100%;margin-top:16px}

/* ---------- settings rows (inside the profile panel) ---------- */
.setting-row{
  display:flex;gap:16px;align-items:center;justify-content:space-between;
  padding:14px 0;border-bottom:1px solid var(--line);flex-wrap:wrap;
}
.setting-row:last-child{border-bottom:0}
.setting-body{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1 1 200px}
.setting-name{font-size:14px;font-weight:500;color:var(--dust)}
.setting-desc{font-size:12px;line-height:1.5;color:var(--muted)}
.setting-row > button{
  flex:0 0 auto;
  background:none;border:1px solid var(--line);color:var(--dust);
  font:inherit;font-size:12px;padding:9px 14px;border-radius:6px;cursor:pointer;
  transition:border-color .15s ease, color .15s ease;
}
.setting-row > button:hover{border-color:var(--accent);color:var(--accent)}
/* The save buttons are a group, so they wrap as one under their description
   rather than one of them stranding itself on a line of its own. */
.setting-stack{align-items:flex-start}
.setting-stack .save-actions{flex:1 1 100%}
.setting-danger .setting-name{color:var(--epoch-4)}
.setting-danger > button{border-color:var(--epoch-4);color:var(--epoch-4)}
.setting-danger > button:hover{background:var(--epoch-4);color:var(--void);border-color:var(--epoch-4)}

.profile-line{font-family:"Space Grotesk",system-ui,sans-serif;font-size:20px;font-weight:600;margin-bottom:6px}
.drawer-footer-note{font-size:11px;color:var(--muted);line-height:1.4}

/* ---------- the shop ---------- */
/* ============================================================
   THE SHOP — a cabinet of specimens, not a list of products

   The governing idea: a cosmetics shop where you cannot see the cosmetic is
   selling text. Every card therefore RENDERS THE THING, using the very rules
   that will style it once worn — the cosmetics block below adds `.spec-orb`,
   `.spec-palette` and `.spec-shapes` to its own selectors rather than
   restating a single gradient here. One source of truth: a new skin is still
   one block in this file, and it turns up in the shop for free.

   Scale carries price. `--tier` (1..3, set inline from the gem cost) drives
   the specimen size and, past 560px, lets the dearest things take two
   columns. Below that everything is one column, because a two-column span in
   a two-column grid is just a full-width card with worse spacing.
   ============================================================ */

.gem-balance{
  display:none;
  align-items:center;gap:10px;margin-bottom:clamp(16px,3vw,22px);
  padding:clamp(9px,1.6vw,12px) clamp(14px,2.4vw,18px);
  border-radius:14px;
  background:linear-gradient(135deg, var(--accent-soft), transparent 70%);
  border:1px solid var(--accent);color:var(--accent);
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:clamp(13px,2.4vw,15px);
}
.gem-balance:not([hidden]){display:inline-flex}
.gem-balance::before{
  content:"";width:13px;height:13px;flex:none;background:currentColor;
  clip-path:polygon(50% 0%, 100% 36%, 50% 100%, 0% 36%);
}

/* The shop gets its own width. The 560px reading column is right for the
   guide, which is prose; it is wrong for a cabinet, where the whole point is
   that objects sit next to each other at different scales. */
.shop-panel{max-width:min(1040px, 96vw)}

/* ---------- section headers: a plate number, set enormous ----------
   The numeral is the loudest thing on the screen and carries no information
   — which is exactly why it can be that loud. It is aria-hidden and bleeds
   off the left edge, so the eye reads it as a mark on the cabinet rather
   than as text it has to parse. */
.spec-section{position:relative;margin-bottom:clamp(30px,6vw,58px)}
.spec-section-head{
  display:flex;align-items:flex-end;gap:clamp(10px,2vw,18px);
  margin-bottom:clamp(12px,2.4vw,20px);
  border-bottom:2px solid var(--dust);padding-bottom:9px;
}
.spec-section-index{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:800;font-variant-numeric:tabular-nums;
  font-size:clamp(34px,9vw,68px);line-height:.76;letter-spacing:-.05em;
  color:transparent;-webkit-text-stroke:1.5px var(--accent);
  flex:none;margin-left:clamp(-6px,-1.4vw,-14px);
  opacity:.85;
}
.spec-section-title{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:800;
  font-size:clamp(16px,4.4vw,30px);letter-spacing:-.03em;line-height:.95;
  color:var(--dust);text-transform:uppercase;flex:1;
}
.spec-section-count{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;
  font-size:clamp(10px,2vw,12px);color:var(--muted);flex:none;padding-bottom:2px;
}

/* ---------- the grid: deliberately uneven ----------
   auto-fit + dense means the tier-3 pieces claim two columns and the small
   ones backfill the holes, so no two rows land on the same rhythm. That
   unevenness is the design; a tidy matrix is what made it look ordinary. */
.spec-grid{
  display:grid;gap:clamp(10px,1.8vw,16px);
  grid-template-columns:repeat(auto-fill,minmax(min(100%,152px),1fr));
  grid-auto-flow:row dense;
}
@media (min-width:600px){
  .spec[data-tier="3"]{grid-column:span 2;grid-row:span 2}
  .spec[data-tier="3"] .spec-well{flex:1}
}
@media (min-width:900px){
  .spec[data-tier="2"]:nth-of-type(4n+1){grid-column:span 2}
}

/* ---------- the card ---------- */
/* Lit from above rather than boxed in. The old card was a 1px rectangle
   around every item regardless of what it cost; light and shadow can say
   "this one matters" where a uniform border cannot. */
.spec{
  position:relative;display:flex;flex-direction:column;gap:8px;
  padding:clamp(11px,2.2vw,16px);
  border:1px solid transparent;border-radius:3px 3px 14px 14px;
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--accent) 7%, transparent), transparent 32%),
    linear-gradient(160deg, var(--void-2), var(--void) 78%);
  box-shadow:inset 0 1px 0 color-mix(in srgb, var(--dust) 14%, transparent),
             0 12px 24px -20px #000;
  text-align:left;color:inherit;font:inherit;cursor:pointer;
  transition:transform .2s cubic-bezier(.2,.8,.3,1), box-shadow .2s ease, border-color .2s ease;
}
.spec:hover:not(:disabled){
  border-color:var(--accent);transform:translateY(-5px);
  box-shadow:inset 0 1px 0 color-mix(in srgb, var(--dust) 22%, transparent),
             0 22px 40px -26px var(--accent);
}
/* The dear things get a brighter rim than the cheap ones, all the time. */
.spec[data-tier="3"]{
  border-color:color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius:3px 3px 18px 18px;
}
.spec:disabled{cursor:default}
.spec.is-owned{border-color:var(--state-owned)}

/* Price made physical. Both values feed clamp()s, so the tier sets the
   ceiling and the viewport still gets the final say on small screens. */
.spec[data-tier="1"]{--well:64px;--orb:32px}
.spec[data-tier="2"]{--well:86px;--orb:48px}
.spec[data-tier="3"]{--well:116px;--orb:68px}

/* The vitrine. overflow:hidden is load-bearing — the core skins carry an
   80px glow sized for a 96px core, and clipping it to the well turns that
   spill into the specimen's own light instead of a bloom over the card. */
.spec-well{
  position:relative;display:grid;place-items:center;
  height:clamp(58px,13vw,var(--well,78px));
  border-radius:9px;overflow:hidden;
  background:radial-gradient(circle at 50% 120%, var(--void-2), var(--void) 70%);
  border:1px solid var(--line);
}

/* ---------- specimen: a core skin ---------- */
.spec-orb{
  position:relative;border-radius:50%;
  width:clamp(30px,8vw,var(--orb,46px));aspect-ratio:1;
  border:1px solid var(--line);
  background:radial-gradient(circle at 50% 45%, var(--accent), var(--void-2) 90%);
}
.spec-orb-label{
  position:absolute;inset:0;display:grid;place-items:center;
  font-family:"Space Grotesk",system-ui,sans-serif;font-size:9px;color:var(--void);
}

/* ---------- specimen: a palette (the seven epochs, struck in ink) ---------- */
.spec-palette{display:flex;width:100%;height:100%}
.spec-palette i{flex:1}
.spec-palette i:nth-child(1){background:var(--epoch-1)}
.spec-palette i:nth-child(2){background:var(--epoch-2)}
.spec-palette i:nth-child(3){background:var(--epoch-3)}
.spec-palette i:nth-child(4){background:var(--epoch-4)}
.spec-palette i:nth-child(5){background:var(--epoch-5)}
.spec-palette i:nth-child(6){background:var(--epoch-6)}
.spec-palette i:nth-child(7){background:var(--epoch-7)}
.spec-palette i:nth-child(8){background:var(--epoch-8)}

/* ---------- specimen: a shape set (the real clip-paths) ---------- */
.spec-shapes{display:flex;gap:5px;align-items:center;flex-wrap:wrap;justify-content:center;padding:6px}
.spec-shapes i{
  width:clamp(9px,2.4vw,14px);aspect-ratio:1;flex:none;
  clip-path:var(--shape);
}
.spec-shapes i[data-epoch="1"]{background:var(--epoch-1)}
.spec-shapes i[data-epoch="2"]{background:var(--epoch-2)}
.spec-shapes i[data-epoch="3"]{background:var(--epoch-3)}
.spec-shapes i[data-epoch="4"]{background:var(--epoch-4)}
.spec-shapes i[data-epoch="5"]{background:var(--epoch-5)}
.spec-shapes i[data-epoch="6"]{background:var(--epoch-6)}
.spec-shapes i[data-epoch="7"]{background:var(--epoch-7)}
.spec-shapes i[data-epoch="8"]{background:var(--epoch-8)}

/* ---------- specimen: a title, shown as the nameplate it becomes ---------- */
.spec-plate{
  padding:5px 13px;border:1px solid var(--accent);border-radius:100px;
  background:var(--accent-soft);color:var(--accent);
  font-family:"Space Grotesk",system-ui,sans-serif;font-size:clamp(10px,2.4vw,13px);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:92%;
}

/* ---------- specimen: a booster, read as the multiplier it is ---------- */
.spec-factor{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:800;line-height:.9;
  font-size:clamp(19px,5.5vw,30px);color:var(--state-affordable);
}
.spec-factor small{
  display:block;font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-weight:400;
  font-size:10px;color:var(--muted);margin-top:5px;letter-spacing:.06em;
}

/* ---------- specimen: the four effects, sketched ---------- */
.spec-fx{position:relative;width:100%;height:100%;overflow:hidden}
.spec-fx[data-fx="fx_trail_dust"] i{
  position:absolute;bottom:12%;width:5px;height:5px;border-radius:50%;
  background:radial-gradient(circle, var(--accent), transparent 70%);
  animation:spec-rise 2.1s ease-out infinite;
}
.spec-fx[data-fx="fx_trail_dust"] i:nth-child(1){left:32%;animation-delay:0s}
.spec-fx[data-fx="fx_trail_dust"] i:nth-child(2){left:50%;animation-delay:.5s}
.spec-fx[data-fx="fx_trail_dust"] i:nth-child(3){left:66%;animation-delay:1s}
@keyframes spec-rise{from{transform:translateY(0);opacity:0}30%{opacity:1}to{transform:translateY(-38px);opacity:0}}

.spec-fx[data-fx="fx_ripple_ring"] i{
  position:absolute;left:50%;top:50%;width:14px;height:14px;margin:-7px 0 0 -7px;
  border:2px solid var(--accent);border-radius:50%;
  animation:spec-ripple 1.7s ease-out infinite;
}
.spec-fx[data-fx="fx_ripple_ring"] i:nth-child(2){animation-delay:.85s}
@keyframes spec-ripple{from{transform:scale(.3);opacity:1}to{transform:scale(3.4);opacity:0}}

.spec-fx[data-fx="fx_connector_arc"] i{
  position:absolute;left:8%;right:8%;top:50%;height:2px;
  background:repeating-linear-gradient(90deg, var(--accent) 0 3px, transparent 3px 10px);
  animation:spec-flow .6s linear infinite;
}
@keyframes spec-flow{to{background-position:10px 0}}

.spec-fx[data-fx="fx_sky_dense"]{
  background-image:radial-gradient(1.5px 1.5px at 20% 30%, var(--dust), transparent),
    radial-gradient(1.5px 1.5px at 70% 25%, var(--dust), transparent),
    radial-gradient(1.5px 1.5px at 45% 60%, var(--dust), transparent),
    radial-gradient(1.5px 1.5px at 85% 70%, var(--dust), transparent),
    radial-gradient(1.5px 1.5px at 12% 75%, var(--dust), transparent),
    radial-gradient(1.5px 1.5px at 60% 85%, var(--dust), transparent),
    radial-gradient(1.5px 1.5px at 32% 12%, var(--dust), transparent);
}

/* ---------- card text ---------- */
.spec-name{
  font-family:"Space Grotesk",system-ui,sans-serif;font-size:clamp(12.5px,2.7vw,15px);
  line-height:1.25;color:var(--dust);
}
.spec-desc{
  font-size:clamp(10.5px,2.2vw,11.5px);line-height:1.5;color:var(--muted);flex:1;
}
.spec-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:2px}
.spec-cost{
  display:inline-flex;align-items:center;gap:5px;
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:clamp(11.5px,2.4vw,13px);color:var(--accent);
}
.spec-cost::before{
  content:"";width:9px;height:9px;flex:none;background:currentColor;
  clip-path:polygon(50% 0%, 100% 36%, 50% 100%, 0% 36%);
}
.spec-cost.cant{color:var(--muted)}
.spec-tag{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;letter-spacing:.08em;
  text-transform:uppercase;padding:3px 8px;border-radius:100px;border:1px solid currentColor;
}
.spec-tag.owned{color:var(--state-owned)}
.spec-tag.running{color:var(--state-affordable)}

/* ---------- gem packs: the only real-money row, so it leads ----------
   Sized by what they cost, not evenly: the R100 pack is physically the
   largest object in the shop and sits raised above the other two. A price
   ladder that all looks the same is a ladder with no top. */
.spec-grid.packs{
  grid-template-columns:repeat(auto-fit,minmax(min(100%,150px),1fr));
  align-items:end;
}
.spec-pack{
  align-items:center;text-align:center;gap:9px;
  border-color:color-mix(in srgb, var(--accent) 45%, transparent);
}
.spec-pack:nth-child(2){--well:104px}
.spec-pack:nth-child(3){--well:128px}
@media (min-width:600px){
  .spec-pack:nth-child(3){
    transform:translateY(-14px);
    border-color:var(--accent);
    box-shadow:inset 0 1px 0 color-mix(in srgb, var(--dust) 22%, transparent),
               0 26px 46px -28px var(--accent);
  }
  .spec-pack:nth-child(3):hover:not(:disabled){transform:translateY(-19px)}
}
.spec-pack .spec-well{
  background:
    radial-gradient(circle at 50% 118%, var(--accent-soft), transparent 62%),
    radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--dust) 8%, transparent), transparent 55%),
    var(--void);
}
.spec-gem{width:clamp(26px,7vw,42px);height:auto;display:block;overflow:visible}
.spec-gem .facet-l{fill:var(--accent);opacity:.55}
.spec-gem .facet-r{fill:var(--accent);opacity:.85}
.spec-gem .facet-t{fill:var(--dust);opacity:.9}
.spec-pack-gems{
  font-family:"Space Grotesk",system-ui,sans-serif;font-weight:800;
  font-size:clamp(16px,4vw,22px);color:var(--dust);line-height:1;
}
.spec-pack-name{font-family:"Space Grotesk",system-ui,sans-serif;font-size:clamp(11px,2.4vw,13px);color:var(--muted)}
.spec-pack-price{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:clamp(12px,2.6vw,14px);color:var(--accent);
}
.spec-pack-bonus{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;color:var(--state-affordable);
  border:1px solid currentColor;border-radius:100px;padding:2px 8px;
}

/* ---------- cursors ----------
   Inline SVG data URIs, not image files: this project has no build step and
   must run from disk, so an asset that needs fetching is an asset that can
   fail. Each ends with a keyword fallback for anywhere the data URI is
   refused, and the hotspot coordinates are stated explicitly — omitting them
   puts the click point at the image's top-left corner, which feels broken.

   Fine-pointer only. A touch device has no cursor, and a coarse pointer with
   a 24px ring drawn under it would just be wrong. */
@media (hover:hover) and (pointer:fine){
  /* The default mark is an ARROW, because a bare ring reads as a target
     reticle and left people unsure what was actually being pointed at. This
     one is a swept, back-raked arrowhead — a probe rather than an office
     cursor — with a dark outline under a light fill so it survives against
     both the deep field and a white panel. Hotspot at the very tip (0 0). */
  body{
    cursor:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26"><path d="M1 1 L1 18.5 L6.1 13.9 L9.2 21.4 L12.6 19.9 L9.5 12.6 L16.4 12.2 Z" fill="%23120d1a" stroke="%23120d1a" stroke-width="2.6" stroke-linejoin="round"/><path d="M1 1 L1 18.5 L6.1 13.9 L9.2 21.4 L12.6 19.9 L9.5 12.6 L16.4 12.2 Z" fill="%23f6e3c0"/></svg>') 1 1, default;
  }
  /* Anything you can press gets the same arrow with a small ignition spark at
     its tail — the pointer answers "can I click this?" without needing a
     hover style on every element, and the silhouette stays constant so the
     shape never jumps as you move across the screen. */
  button, .tree-node, .sky-node, .tree-portal, a, summary, [role="button"]{
    cursor:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30"><path d="M1 1 L1 18.5 L6.1 13.9 L9.2 21.4 L12.6 19.9 L9.5 12.6 L16.4 12.2 Z" fill="%23120d1a" stroke="%23120d1a" stroke-width="2.6" stroke-linejoin="round"/><path d="M1 1 L1 18.5 L6.1 13.9 L9.2 21.4 L12.6 19.9 L9.5 12.6 L16.4 12.2 Z" fill="%23f0a35e"/><circle cx="20.5" cy="20.5" r="3.4" fill="%23f0a35e" opacity="0.3"/><circle cx="20.5" cy="20.5" r="1.5" fill="%23f0a35e"/></svg>') 1 1, pointer;
  }
  /* Selecting text keeps a real I-beam, but struck in the game's own ink
     rather than the OS default, so the one place you read closely doesn't
     suddenly look like a form on a different website. */
  input, textarea, .guide-panel, .codex-flavour, .sheet-flavour, .save-hint, .friend-code{
    cursor:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="24"><path d="M4 2h8M4 22h8M8 2v20" stroke="%23120d1a" stroke-width="4.2" fill="none" stroke-linecap="round"/><path d="M4 2h8M4 22h8M8 2v20" stroke="%23f6e3c0" stroke-width="1.6" fill="none" stroke-linecap="round"/></svg>') 8 12, text;
  }
  /* The tree keeps grab/grabbing: on a surface you drag, the hand is a
     stronger signal than any custom mark, and it changes on press. */
  .tree-viewport, .sky-viewport{cursor:grab}
  .tree-viewport.dragging, .sky-viewport.dragging{cursor:grabbing}
  /* NOTE: the old `input, textarea{cursor:text}` lived here and silently beat
     the custom I-beam above it — later rule, same specificity. Don't put a
     bare keyword cursor after the block. */
  button:disabled{cursor:not-allowed}
}

/* ---------- the deep background ----------
   Three slow-drifting nebula clouds behind the starfield. They use the epoch
   accent, so the sky warms and cools as you advance without any JS: the
   --epoch-N variables remain the single source of truth for colour.
   Sits at z-index 0, below the stars (1) and everything else. */
.nebula{
  position:fixed;inset:-20%;z-index:0;pointer-events:none;
  opacity:.5;
  background:
    radial-gradient(38% 30% at 22% 26%, var(--accent-soft), transparent 68%),
    radial-gradient(32% 26% at 78% 62%, var(--fog-deep), transparent 70%),
    radial-gradient(46% 34% at 52% 88%, var(--fog-warm), transparent 72%);
  filter:blur(28px);
  animation:nebula-drift 90s ease-in-out infinite alternate;
}
@keyframes nebula-drift{
  from{transform:translate3d(-1.5%,-1%,0) scale(1)}
  to{transform:translate3d(1.5%,1.5%,0) scale(1.06)}
}

/* ---------- the deep field ----------
   Three layers of distant matter that parallax against the tree: --par-x/y
   are written by applyTransform() from the canvas translate, and each layer
   multiplies them by a different (small) factor. Far things move least,
   which is the whole trick — the tree stops reading as a drawing on a flat
   backdrop and starts reading as something suspended in depth.

   Distinct from .starfield on purpose: those are pin-sharp foreground stars,
   these are soft, larger, dimmer smudges — distant galaxies and clusters. */
.deep-field{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
.deep-layer{
  position:absolute;inset:-12%;
  will-change:transform;
}

.deep-far{
  opacity:.32;
  transform:translate3d(calc(var(--par-x,0px) * .012), calc(var(--par-y,0px) * .012), 0);
  background:
    radial-gradient(7px 3px at 14% 18%, var(--fog-deep), transparent 60%),
    radial-gradient(5px 2px at 63% 11%, var(--fog-cool), transparent 60%),
    radial-gradient(9px 4px at 82% 44%, var(--fog-deep), transparent 62%),
    radial-gradient(6px 3px at 31% 57%, var(--fog-warm), transparent 60%),
    radial-gradient(8px 3px at 52% 79%, var(--fog-cool), transparent 60%),
    radial-gradient(5px 2px at 9% 86%, var(--fog-deep), transparent 60%);
}
.deep-mid{
  opacity:.42;
  transform:translate3d(calc(var(--par-x,0px) * .03), calc(var(--par-y,0px) * .03), 0);
  background:
    radial-gradient(14px 5px at 24% 33%, var(--fog-warm), transparent 66%),
    radial-gradient(11px 4px at 71% 24%, var(--fog-deep), transparent 64%),
    radial-gradient(16px 6px at 47% 62%, var(--fog-cool), transparent 68%),
    radial-gradient(12px 5px at 88% 71%, var(--fog-warm), transparent 64%);
}
/* The near layer carries the epoch's own colour, so the closest matter is the
   thing that most obviously changes as the universe advances. */
.deep-near{
  opacity:.3;
  transform:translate3d(calc(var(--par-x,0px) * .06), calc(var(--par-y,0px) * .06), 0);
  background:
    radial-gradient(120px 44px at 18% 72%, var(--accent-soft), transparent 70%),
    radial-gradient(90px 34px at 78% 22%, var(--accent-soft), transparent 70%);
  filter:blur(2px);
}

/* A single slow band of light across the sky. One element, one animation —
   the restraint is deliberate: this sits behind everything a player is
   actually reading. */
.aurora{
  position:fixed;left:-25%;right:-25%;top:22%;height:38vh;z-index:0;
  pointer-events:none;opacity:.16;
  background:linear-gradient(100deg, transparent 8%, var(--accent) 38%, var(--epoch-7) 58%, transparent 88%);
  filter:blur(60px);
  transform:rotate(-7deg);
  animation:aurora-sway 42s ease-in-out infinite alternate;
}
@keyframes aurora-sway{
  from{transform:rotate(-9deg) translate3d(-4%,-2%,0) scaleY(.9)}
  to{transform:rotate(-4deg) translate3d(4%,3%,0) scaleY(1.15)}
}

/* ============================================================
   LIGHT THEME — "the star chart"

   The game is designed dark and stays dark by default. Light is not an
   inversion of it: a photograph of deep space cannot be turned inside out and
   still mean anything. It's a different object — an astronomical chart
   printed on paper, with the same shapes and the same information in ink.

   This works at all because the stylesheet never hardcodes a colour outside
   :root. Everything below is a variable override; not one rule is repeated.
   Anything new that hardcodes a hex will simply be wrong here, which is a
   useful way to catch it.
   ============================================================ */
html[data-theme="light"]{
  --void:#f1ebdf;              /* paper */
  --void-2:#fbf7ef;            /* cards, panels, the raised surfaces */
  --line:#d5c8b2;
  --dust:#2a2233;              /* ink — this is the text colour */
  --muted:#6d6280;
  --accent:#a8531d;
  --accent-soft:#a8531d26;

  /* The seven epochs, re-struck for ink on paper. Same order, same warm-to-
     cool journey, darkened until each one carries its own weight against a
     light ground. */
  --epoch-1:#a8531d;
  --epoch-2:#b0432a;
  --epoch-3:#a92d54;
  --epoch-4:#8c2549;
  --epoch-5:#68308c;
  --epoch-6:#4438a6;
  --epoch-7:#0d6675;
  --epoch-8:#3a4a9e;

  --state-owned:#8b8199;
  --state-affordable:#15713e;
  --state-available:#1c5aa0;
  --state-available-soft:#1c5aa033;

  --fog-warm:#c9a97a4d;
  --fog-cool:#b3a0c246;
  --fog-deep:#a297bd42;
  --sky-glow:#dde7f3;
  --light-bright:#0d6675;
  --scrim:#3a3145;

  /* The Canvas on paper. Only the ground needs restriking now — the other
     three are var(--epoch-N) references, so they follow this block's own epoch
     colours automatically and can't fall out of step with it. */
  --canvas-void:#e7e2d6;

  /* A star chart printed in ink still separates its spectral classes — that's
     what the colour is FOR — so the classes survive the theme, darkened
     rather than inverted. */
  --spec-o:#2f4fbf;
  --spec-b:#3a5cc4;
  --spec-a:#5a6fae;
  --spec-f:#7a7580;
  --spec-g:#8a6a3a;
  --spec-k:#a86a25;
  --spec-m:#b3441c;
}

/* The starfield's dots are literal white — the one documented exception to
   the no-hex rule, because nine alpha-white dots inside one gradient list
   would be unreadable as variables. Inverting the layer turns them into ink
   specks on paper instead, which is exactly what a chart wants. */
html[data-theme="light"] .starfield{filter:invert(1);opacity:.55}

/* Deep space glows; paper doesn't. These layers are what would give the
   theme away as a recolour, so they're pulled right back. */
html[data-theme="light"] .nebula{opacity:.3;filter:blur(38px)}
html[data-theme="light"] .aurora{opacity:.1}
html[data-theme="light"] .deep-far{opacity:.22}
html[data-theme="light"] .deep-mid{opacity:.26}
html[data-theme="light"] .deep-near{opacity:.2}
html[data-theme="light"] .epoch-fog{opacity:.35!important}

/* On paper the core is the one thing still lit from within, so it keeps its
   dark interior — a bright disc on a bright page would vanish. */
html[data-theme="light"] .core{box-shadow:0 0 50px -18px var(--accent)}
html[data-theme="light"] .core-label{color:var(--void-2);text-shadow:0 1px 2px #00000055}

/* The theme picker itself */
.theme-choice{display:flex;gap:8px;flex:0 0 auto}
.theme-choice button{
  background:none;border:1px solid var(--line);color:var(--muted);
  font:inherit;font-size:12px;padding:9px 14px;border-radius:6px;cursor:pointer;
  transition:border-color .15s ease, color .15s ease, background .15s ease;
}
.theme-choice button:hover{border-color:var(--accent);color:var(--accent)}
.theme-choice button[aria-pressed="true"]{
  border-color:var(--accent);background:var(--accent-soft);color:var(--accent);
}

/* ============================================================
   CONTROL SIZING — one place, so it can't drift
   ============================================================

   Each of these already has its own rule above with its own look; this block
   only guarantees the box is big enough to hit. Keeping it together means the
   next control someone adds gets checked against a list rather than against
   memory, and it means the floor can be raised for every control at once by
   editing --tap.

   Rule of thumb for anything new: if it's a word, give it min-height. If it's
   a glyph, give it both dimensions and centre the glyph — a 20px "x" with 4px
   of padding is a 28px target however big the character looks. */

/* Text and pill buttons. */
.qty-btn,
.drawer-link,
.collapse-btn,
.collapse-cancel, .collapse-confirm-btn,
.account-submit, .account-toggle,
.firmament-back,
.setting-row > button,
.save-actions button,
.update-banner-btn,
.realign-btn, .socket-btn,
.theme-choice button{
  min-height:var(--tap);
}

/* The range input's own box, so the thumb has a full-height strip to be
   grabbed in rather than a 5px track. */
.socket-slider{height:var(--tap)}

/* Inputs, which need the same floor — and 16px specifically, because iOS
   Safari zooms the whole page in when a focused field's text is smaller. */
.account-field input, .account-inline input{
  min-height:var(--tap);
  font-size:16px;
}

/* Glyph buttons: square, and the glyph centred in it rather than sitting in a
   corner of a box that grew around it. `padding:0` undoes the old 4px, which
   would otherwise fight place-content. */
.sheet-close, .drawer-close, .guide-close, .update-banner-close{
  width:var(--tap);height:var(--tap);padding:0;
  display:grid;place-content:center;
  border-radius:10px;
}
/* They were positioned as small glyphs, so the bigger box needs pulling back
   into the corner it used to sit in. */
.sheet-close, .drawer-close{top:8px;right:10px}
.guide-close{top:6px;right:8px}

/* The exception: the first-buy hint is a small pill and a 46px close would be
   taller than the thing it closes. The visible glyph stays small and the HIT
   AREA is extended past it instead — the standard fix when layout genuinely
   can't give a control more room. */
.first-buy-hint-close{position:relative}
.first-buy-hint-close::after{
  content:"";position:absolute;inset:-12px;
}

/* ============================================================
   COSMETICS — what a purchase actually looks like

   The server owns what an item costs and who owns it; this file owns how it
   looks. That split is why adding a cosmetic touches exactly two files:
   api/config/shop.php for the price, and this block for the appearance.

   Everything keys off data attributes on <html>, written by applyCosmetics()
   in js/social.js from the server's `equipped` map. One attribute per slot,
   so wearing a skin and a palette and a shape set at once needs no combining
   logic — they simply don't collide.
   ============================================================ */

/* ---------- core skins ---------- */
html[data-skin="skin_ember"] .core,
.spec-orb[data-skin="skin_ember"]{
  background:radial-gradient(circle at 50% 45%, #ffd39a 0%, #e8622a 42%, #6b1d10 74%, var(--void-2) 92%);
  box-shadow:0 0 70px -10px #e8622a;
}
html[data-skin="skin_void"] .core,
.spec-orb[data-skin="skin_void"]{
  background:radial-gradient(circle at 50% 45%, #2a2338 0%, #120d1a 55%, #05030a 78%);
  box-shadow:0 0 60px -14px #8f7fd4, inset 0 0 22px -6px #b7a8ff;
  border-color:#b7a8ff;
}
html[data-skin="skin_void"] .core-label,
.spec-orb[data-skin="skin_void"] .spec-orb-label{color:#d8ccff;text-shadow:none}
html[data-skin="skin_aurora"] .core,
.spec-orb[data-skin="skin_aurora"]{
  background:radial-gradient(circle at 50% 45%, #b8ffe6 0%, #45d6a0 34%, #6b4fd0 68%, var(--void-2) 90%);
  box-shadow:0 0 70px -10px #45d6a0;
}
html[data-skin="skin_pulsar"] .core,
.spec-orb[data-skin="skin_pulsar"]{
  background:radial-gradient(circle at 50% 45%, #ffffff 0%, #9fd8ff 30%, #2b5fa8 62%, var(--void-2) 88%);
  box-shadow:0 0 74px -8px #9fd8ff;
}
/* The pulsar earns its name: a beam sweeps the disc. Inherits the global
   prefers-reduced-motion rule with everything else. */
html[data-skin="skin_pulsar"] .core::after,
.spec-orb[data-skin="skin_pulsar"]::after{
  content:"";position:absolute;inset:0;border-radius:50%;
  background:conic-gradient(from 0deg, transparent 0deg, #ffffffaa 12deg, transparent 30deg, transparent 360deg);
  animation:pulsar-sweep 2.4s linear infinite;pointer-events:none;
}
@keyframes pulsar-sweep{to{transform:rotate(360deg)}}
html[data-skin="skin_goldleaf"] .core,
.spec-orb[data-skin="skin_goldleaf"]{
  background:radial-gradient(circle at 50% 45%, #fff3c4 0%, #e0b040 38%, #8a6110 70%, var(--void-2) 90%);
  box-shadow:0 0 66px -10px #e0b040;
  border-color:#f0d68a;
}
html[data-skin="skin_goldleaf"] .core-label,
.spec-orb[data-skin="skin_goldleaf"] .spec-orb-label{color:#4a3405;text-shadow:0 1px 0 #ffffff66}
html[data-skin="skin_singularity"] .core,
.spec-orb[data-skin="skin_singularity"]{
  background:radial-gradient(circle at 50% 45%, #000000 0%, #000000 44%, #6b4a9e 60%, #d9c4ff 68%, var(--void-2) 86%);
  box-shadow:0 0 80px -6px #a98bff, inset 0 0 30px 6px #000;
  border-color:#d9c4ff;
}
html[data-skin="skin_singularity"] .core-label,
.spec-orb[data-skin="skin_singularity"] .spec-orb-label{color:#e6dbff;text-shadow:0 0 6px #000}

/* Nebula: two offset clouds rather than one radial, so the centre of light and
   the centre of the sphere don't coincide — which is what stops it reading as
   a billiard ball. */
html[data-skin="skin_nebula"] .core,
.spec-orb[data-skin="skin_nebula"]{
  background:
    radial-gradient(circle at 34% 32%, #ffd7f2 0%, #b268d6 26%, transparent 62%),
    radial-gradient(circle at 68% 66%, #6fd0ff 0%, #2b4a9e 34%, transparent 70%),
    radial-gradient(circle at 50% 45%, #43206b 0%, var(--void-2) 88%);
  box-shadow:0 0 70px -12px #b268d6;
  border-color:#d8a8ff;
}
html[data-skin="skin_nebula"] .core-label,
.spec-orb[data-skin="skin_nebula"] .spec-orb-label{color:#f6e4ff;text-shadow:0 0 6px #2a1140}

/* Iron: banded rather than smooth. The repeating-linear-gradient is the whole
   idea — a shell structure, seen edge on. */
html[data-skin="skin_iron"] .core,
.spec-orb[data-skin="skin_iron"]{
  background:
    repeating-linear-gradient(118deg, #6e6a66 0 6px, #55514e 6px 12px, #7d7873 12px 17px),
    radial-gradient(circle at 50% 45%, #9b958f 0%, #3a3734 76%, var(--void-2) 96%);
  background-blend-mode:soft-light, normal;
  box-shadow:0 0 44px -16px #b9b2aa, inset 0 -10px 24px -8px #000;
  border-color:#9b958f;
}
html[data-skin="skin_iron"] .core-label,
.spec-orb[data-skin="skin_iron"] .spec-orb-label{color:#1d1b19;text-shadow:0 1px 0 #ffffff55}

/* ---------- palettes: re-strike all seven epochs at once ----------
   Only the --epoch-N variables change. Everything downstream — nodes,
   connectors, the sheet, the accent, friend pips — follows for free, which is
   the whole reason the palette lives in variables. */
html[data-palette="theme_deepsea"],
.spec-palette[data-palette="theme_deepsea"]{
  --epoch-1:#4ec3c9; --epoch-2:#3aa8c4; --epoch-3:#2f8bbf;
  --epoch-4:#3a6fb0; --epoch-5:#4a55a3; --epoch-6:#5b4b95; --epoch-7:#7ee0d4; --epoch-8:#b9f0ff;
}
html[data-palette="theme_rust"],
.spec-palette[data-palette="theme_rust"]{
  --epoch-1:#d98b4a; --epoch-2:#c46b3a; --epoch-3:#a8502f;
  --epoch-4:#8c4030; --epoch-5:#6f4436; --epoch-6:#8a7355; --epoch-7:#d9c4a0; --epoch-8:#f2e6cf;
}
html[data-palette="theme_neon"],
.spec-palette[data-palette="theme_neon"]{
  --epoch-1:#ff5fa2; --epoch-2:#ff3d8f; --epoch-3:#e030c0;
  --epoch-4:#a040e8; --epoch-5:#5f5cff; --epoch-6:#2fd0ff; --epoch-7:#5fffd0; --epoch-8:#c08cff;
}
html[data-palette="theme_mono"],
.spec-palette[data-palette="theme_mono"]{
  --epoch-1:#e8e2ea; --epoch-2:#cfc8d4; --epoch-3:#b4acbd;
  --epoch-4:#9990a6; --epoch-5:#7d748c; --epoch-6:#615a70; --epoch-7:#f4f0f6; --epoch-8:#ffffff;
}
html[data-palette="theme_solarflare"],
.spec-palette[data-palette="theme_solarflare"]{
  --epoch-1:#fff2b0; --epoch-2:#ffd166; --epoch-3:#ffa733;
  --epoch-4:#ff7420; --epoch-5:#e8451f; --epoch-6:#b52418; --epoch-7:#ffe9a8; --epoch-8:#fffbe8;
}
html[data-palette="theme_bloom"],
.spec-palette[data-palette="theme_bloom"]{
  --epoch-1:#d7e86b; --epoch-2:#a8d858; --epoch-3:#6fc25c;
  --epoch-4:#3fa367; --epoch-5:#2f8272; --epoch-6:#276274; --epoch-7:#e3c766; --epoch-8:#f6f0c0;
}
html[data-palette="theme_ember"],
.spec-palette[data-palette="theme_ember"]{
  --epoch-1:#ffd9a0; --epoch-2:#ffa85e; --epoch-3:#f4713a;
  --epoch-4:#d24a2c; --epoch-5:#a03325; --epoch-6:#6d2320; --epoch-7:#3f1a1c; --epoch-8:#ff8b4a;
}

/* ---------- shape sets: replace all eight silhouettes ----------
   Same override point the base game uses ([data-epoch]{--shape}), so the
   legend in the guide picks these up without knowing they exist. */
html[data-shapes="shapes_crystal"] [data-epoch="1"],
.spec-shapes[data-shapes="shapes_crystal"] [data-epoch="1"]{--shape:polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)}
html[data-shapes="shapes_crystal"] [data-epoch="2"],
.spec-shapes[data-shapes="shapes_crystal"] [data-epoch="2"]{--shape:polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)}
html[data-shapes="shapes_crystal"] [data-epoch="3"],
.spec-shapes[data-shapes="shapes_crystal"] [data-epoch="3"]{--shape:polygon(50% 0%, 90% 30%, 72% 100%, 28% 100%, 10% 30%)}
html[data-shapes="shapes_crystal"] [data-epoch="4"],
.spec-shapes[data-shapes="shapes_crystal"] [data-epoch="4"]{--shape:polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)}
html[data-shapes="shapes_crystal"] [data-epoch="5"],
.spec-shapes[data-shapes="shapes_crystal"] [data-epoch="5"]{--shape:polygon(20% 0%, 80% 0%, 100% 40%, 50% 100%, 0% 40%)}
html[data-shapes="shapes_crystal"] [data-epoch="6"],
.spec-shapes[data-shapes="shapes_crystal"] [data-epoch="6"]{--shape:polygon(50% 0%, 100% 20%, 85% 80%, 50% 100%, 15% 80%, 0% 20%)}
html[data-shapes="shapes_crystal"] [data-epoch="7"],
.spec-shapes[data-shapes="shapes_crystal"] [data-epoch="7"]{--shape:polygon(50% 0%, 68% 32%, 100% 38%, 76% 64%, 84% 100%, 50% 82%, 16% 100%, 24% 64%, 0% 38%, 32% 32%)}
html[data-shapes="shapes_crystal"] [data-epoch="8"],
.spec-shapes[data-shapes="shapes_crystal"] [data-epoch="8"]{--shape:polygon(50% 0%, 100% 26%, 88% 74%, 50% 58%, 12% 74%, 0% 26%)}

html[data-shapes="shapes_organic"] [data-epoch="1"],
.spec-shapes[data-shapes="shapes_organic"] [data-epoch="1"]{--shape:circle(50%)}
html[data-shapes="shapes_organic"] [data-epoch="2"],
.spec-shapes[data-shapes="shapes_organic"] [data-epoch="2"]{--shape:ellipse(46% 50%)}
html[data-shapes="shapes_organic"] [data-epoch="3"],
.spec-shapes[data-shapes="shapes_organic"] [data-epoch="3"]{--shape:inset(0 round 46% 54% 50% 50%)}
html[data-shapes="shapes_organic"] [data-epoch="4"],
.spec-shapes[data-shapes="shapes_organic"] [data-epoch="4"]{--shape:inset(0 round 54% 46% 52% 48%)}
html[data-shapes="shapes_organic"] [data-epoch="5"],
.spec-shapes[data-shapes="shapes_organic"] [data-epoch="5"]{--shape:inset(0 round 40% 60% 45% 55%)}
html[data-shapes="shapes_organic"] [data-epoch="6"],
.spec-shapes[data-shapes="shapes_organic"] [data-epoch="6"]{--shape:inset(0 round 60% 40% 55% 45%)}
html[data-shapes="shapes_organic"] [data-epoch="7"],
.spec-shapes[data-shapes="shapes_organic"] [data-epoch="7"]{--shape:inset(0 round 50% 50% 38% 62%)}
html[data-shapes="shapes_organic"] [data-epoch="8"],
.spec-shapes[data-shapes="shapes_organic"] [data-epoch="8"]{--shape:inset(0 round 62% 38% 50% 50%)}

/* Runic: cut marks rather than solids — every one of these is a shape with at
   least one concave corner, which is what makes them read as carved. */
html[data-shapes="shapes_runic"] [data-epoch="1"],
.spec-shapes[data-shapes="shapes_runic"] [data-epoch="1"]{--shape:polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%, 25% 50%, 50% 22%, 75% 50%)}
html[data-shapes="shapes_runic"] [data-epoch="2"],
.spec-shapes[data-shapes="shapes_runic"] [data-epoch="2"]{--shape:polygon(20% 0%, 80% 0%, 80% 34%, 100% 34%, 50% 100%, 0% 34%, 20% 34%)}
html[data-shapes="shapes_runic"] [data-epoch="3"],
.spec-shapes[data-shapes="shapes_runic"] [data-epoch="3"]{--shape:polygon(50% 0%, 92% 26%, 92% 74%, 50% 100%, 8% 74%, 8% 26%, 50% 50%)}
html[data-shapes="shapes_runic"] [data-epoch="4"],
.spec-shapes[data-shapes="shapes_runic"] [data-epoch="4"]{--shape:polygon(0% 0%, 100% 0%, 70% 40%, 100% 100%, 0% 100%, 30% 60%)}
html[data-shapes="shapes_runic"] [data-epoch="5"],
.spec-shapes[data-shapes="shapes_runic"] [data-epoch="5"]{--shape:polygon(50% 0%, 100% 30%, 82% 100%, 50% 70%, 18% 100%, 0% 30%)}
html[data-shapes="shapes_runic"] [data-epoch="6"],
.spec-shapes[data-shapes="shapes_runic"] [data-epoch="6"]{--shape:polygon(12% 0%, 88% 0%, 100% 50%, 88% 100%, 12% 100%, 0% 50%, 26% 50%)}
html[data-shapes="shapes_runic"] [data-epoch="7"],
.spec-shapes[data-shapes="shapes_runic"] [data-epoch="7"]{--shape:polygon(50% 0%, 70% 30%, 100% 30%, 76% 56%, 88% 100%, 50% 76%, 12% 100%, 24% 56%, 0% 30%, 30% 30%)}
html[data-shapes="shapes_runic"] [data-epoch="8"],
.spec-shapes[data-shapes="shapes_runic"] [data-epoch="8"]{--shape:polygon(50% 0%, 66% 34%, 100% 34%, 74% 100%, 50% 56%, 26% 100%, 0% 34%, 34% 34%)}

/* ---------- connectors: the lines between the nodes ----------
   A whole slot for the surface you look at longest. Every rule here overrides
   the same two selectors the base tree uses (.tree-lines path and .lit), and
   each carries a second selector for the shop specimen — which is a REAL
   .tree-lines svg with a REAL .lit path, so a card cannot show a line the tree
   wouldn't draw. */
html[data-connector="wire_filament"] .tree-lines path.lit,
.spec-wire[data-connector="wire_filament"] .tree-lines path.lit{
  stroke-width:1;stroke-dasharray:none;animation:none;opacity:.9;
}
html[data-connector="wire_pulse"] .tree-lines path.lit,
.spec-wire[data-connector="wire_pulse"] .tree-lines path.lit{
  stroke-width:2.6;stroke-dasharray:6 42;stroke-linecap:round;
  animation:connector-flow 1.9s linear infinite;
  filter:drop-shadow(0 0 5px var(--node-color, var(--accent)));
}
html[data-connector="wire_root"] .tree-lines path.lit,
.spec-wire[data-connector="wire_root"] .tree-lines path.lit{
  stroke-width:5;stroke-dasharray:none;stroke-linecap:round;opacity:.55;
  animation:none;
  filter:blur(.6px) drop-shadow(0 0 6px var(--node-color, var(--accent)));
}
/* Rootwork thickens the unlit lines too — the whole tree reads heavier, which
   is the point of it. */
html[data-connector="wire_root"] .tree-lines path,
.spec-wire[data-connector="wire_root"] .tree-lines path{stroke-width:4;opacity:.7}

/* The specimen well. The svg stretches to it, so the curve is the same curve
   at any card size. */
.spec-wire{
  width:100%;height:clamp(30px,9vw,52px);
  display:grid;place-items:stretch;
}
.spec-wire svg{width:100%;height:100%;position:static}
.spec-wire .tree-lines path.lit{--node-color:var(--accent)}

/* ---------- effects ---------- */
/* Dust Trail: the "+N" numbers become drifting motes instead of text. */
html[data-effect~="fx_trail_dust"] .pop{
  font-size:0;width:7px;height:7px;border-radius:50%;
  background:radial-gradient(circle, var(--accent), transparent 70%);
}
/* Ring Ripple: a cleaner, wider, thinner ring on every tap. */
html[data-effect~="fx_ripple_ring"] .ripple{
  border-width:2px;border-color:var(--accent);
  animation-duration:.85s;
}
/* Arc Current: the connectors run brighter and faster. */
html[data-effect~="fx_connector_arc"] .tree-lines path.lit{
  stroke-width:2.4;stroke-dasharray:3 7;
  animation-duration:.6s;
  filter:drop-shadow(0 0 4px var(--node-color, var(--accent)));
}
/* Dense Starfield: a second, offset copy of the star layer. */
html[data-effect~="fx_sky_dense"] .starfield{
  background-size:70% 70%, 55% 55%, 85% 85%, 60% 60%, 75% 75%, 50% 50%, 65% 65%, 90% 90%, 45% 45%;
}

/* ---------- cursor cosmetics ----------
   Each mark is declared ONCE, as a --mark custom property on a selector that
   covers both the worn cursor (on <html>) and its shop preview. The cursor
   rule and the preview rule then both read var(--mark), so a card can never
   show a different arrow from the one you get.

   The dark under-stroke in each path is not decoration: a cursor crosses the
   deep field, a lit node and a white panel in one sweep, and a single-colour
   mark disappears against one of them. */
html[data-cursor="cursor_comet"],   .spec-cursor[data-cursor-preview="cursor_comet"]{
  --mark:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30"><circle cx="19" cy="19" r="2.6" fill="%237fd8ff" opacity="0.28"/><circle cx="22.5" cy="22.5" r="1.7" fill="%237fd8ff" opacity="0.18"/><path d="M1 1 L1 17 L5.6 12.9 L8.4 19.6 L11.5 18.2 L8.7 11.7 L14.9 11.3 Z" fill="%23120d1a" stroke="%23120d1a" stroke-width="2.6" stroke-linejoin="round"/><path d="M1 1 L1 17 L5.6 12.9 L8.4 19.6 L11.5 18.2 L8.7 11.7 L14.9 11.3 Z" fill="%23dff3ff"/></svg>');
}
html[data-cursor="cursor_probe"],   .spec-cursor[data-cursor-preview="cursor_probe"]{
  --mark:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30"><path d="M1.5 1.5 L1.5 20 L6 15.5 L14 13 Z" fill="%23120d1a" stroke="%23120d1a" stroke-width="2.4" stroke-linejoin="round"/><path d="M1.5 1.5 L1.5 20 L6 15.5 L14 13 Z" fill="%23a9f0d1"/><path d="M14 13 L23 8" stroke="%23120d1a" stroke-width="3.4" stroke-linecap="round"/><path d="M14 13 L23 8" stroke="%23a9f0d1" stroke-width="1.4" stroke-linecap="round"/><circle cx="23.6" cy="7.6" r="2.2" fill="%23a9f0d1"/></svg>');
}
html[data-cursor="cursor_reticle"], .spec-cursor[data-cursor-preview="cursor_reticle"]{
  --mark:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26"><circle cx="13" cy="13" r="7" fill="none" stroke="%23120d1a" stroke-width="3.4"/><circle cx="13" cy="13" r="7" fill="none" stroke="%23f6e3c0" stroke-width="1.5"/><path d="M13 2.5v4M13 19.5v4M2.5 13h4M19.5 13h4" stroke="%23120d1a" stroke-width="3.2" stroke-linecap="round"/><path d="M13 2.5v4M13 19.5v4M2.5 13h4M19.5 13h4" stroke="%23f6e3c0" stroke-width="1.4" stroke-linecap="round"/><circle cx="13" cy="13" r="1.5" fill="%23f6e3c0"/></svg>');
}
html[data-cursor="cursor_shard"],   .spec-cursor[data-cursor-preview="cursor_shard"]{
  --mark:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30"><path d="M1 1 L3.5 15.5 L7.5 12 L12 21 L15.5 18.5 L11 10.5 L17 9.5 Z" fill="%23120d1a" stroke="%23120d1a" stroke-width="2.6" stroke-linejoin="round"/><path d="M1 1 L3.5 15.5 L7.5 12 L12 21 L15.5 18.5 L11 10.5 L17 9.5 Z" fill="%23e0c4ff"/><path d="M1 1 L11 10.5 L3.5 15.5 Z" fill="%23ffffff" opacity="0.55"/></svg>');
}
html[data-cursor="cursor_eclipse"], .spec-cursor[data-cursor-preview="cursor_eclipse"]{
  --mark:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"><circle cx="14" cy="14" r="9.5" fill="%23ffb35e" opacity="0.35"/><circle cx="14" cy="14" r="7.5" fill="none" stroke="%23ffd08a" stroke-width="2.2"/><circle cx="14" cy="14" r="6.6" fill="%23090610"/><path d="M14 1.5v3M14 23.5v3M1.5 14h3M23.5 14h3" stroke="%23ffd08a" stroke-width="1.6" stroke-linecap="round"/></svg>');
}

/* The worn cursor. Inside the same fine-pointer guard as the default mark —
   a coarse pointer has nothing to draw and must not be charged for one. */
@media (hover:hover) and (pointer:fine){
  html[data-cursor] body{cursor:var(--mark) 1 1, default}
  /* Centred marks put the hotspot in the middle, not at a corner tip. */
  html[data-cursor="cursor_reticle"] body{cursor:var(--mark) 13 13, default}
  html[data-cursor="cursor_eclipse"] body{cursor:var(--mark) 14 14, default}
}

/* A coarse pointer draws no cursor, so these are not merely inert there — they
   must not be ON SALE there. The section is withheld by the same media query
   that decides whether a cursor can be worn at all, which is the only way the
   shop and the game are guaranteed to agree about what a device supports.
   (display:none first, so the default on an unknown device is "don't sell it".) */
.spec-section[data-slot="cursor"]{display:none}
@media (hover:hover) and (pointer:fine){
  .spec-section[data-slot="cursor"]{display:block}
}

/* The shop preview: the same artwork, standing still and enlarged. */
.spec-cursor{
  width:clamp(30px,8vw,46px);aspect-ratio:1;
  background-image:var(--mark);
  background-repeat:no-repeat;background-position:center;background-size:contain;
}

/* ---------- the equip picker ---------- */
.wearing-slot{margin-bottom:16px}
.wearing-slot:last-child{margin-bottom:0}
.wearing-label{
  font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--muted);margin-bottom:7px;
}
.wearing-options{display:flex;gap:8px;flex-wrap:wrap}
.wearing-option{
  background:none;border:1px solid var(--line);color:var(--muted);
  font:inherit;font-size:12px;padding:8px 13px;border-radius:20px;cursor:pointer;
  transition:border-color .15s ease, color .15s ease, background .15s ease;
}
.wearing-option:hover{border-color:var(--dust);color:var(--dust)}
.wearing-option[aria-pressed="true"]{
  border-color:var(--accent);background:var(--accent-soft);color:var(--accent);
}

/* The title you're wearing, on your own profile. */
.profile-title{
  display:block;font-family:"Share Tech Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums;font-size:11px;
  letter-spacing:.18em;text-transform:uppercase;color:var(--accent);margin-top:4px;
}
