/* jjscoolwebsite.com — sitewide chrome
   Colours are lifted from the construction tape sprite so the page and the
   art share one palette. The pixel art carries the retro note; the type is
   deliberately plain so it stays easy to read against it.

   This file holds only what every page needs: the palette, the reset, the
   top bar and the news ticker. Page-specific rules live in home.css and
   construction.css. */

:root {
  --bg: #222034;
  --yellow: #fbf254;
  --yellow-dark: #e9b61d;
  --muted: #7d78ad;
  --green: #99e550;
  --red: #ac3232;
  --white: #ffffff;

  /* Chrome (top bar/ticker): Windows 95's bevel shapes, this site's colours
     — homemade 90s software recoloured its widgets rather than shipping
     grey, and flat #c0c0c0 next to hand-drawn art reads as someone else's
     screenshot. --face is a step lighter than --bg; --lit/--dim are the
     two edges of every bevel in the file. */
  --face: #45375e;
  --lit: #7d78ad;
  --dim: #16121f;

  /* 8px tile behind the page, barely lighter than --bg so it doesn't
     fight the text sitting on it. */
  --bg-tile: #282641;

  /* Edges for a well sunk into --bg, not --face — a bevel is its own face
     lit/shadowed, so borrowing the bar's lavender would put a bright
     stripe down a navy well (e.g. next to the ticker's LIVE chip). */
  --bg-lit: #45406b;
  --bg-dim: #100e1a;

  --rule: 3px;
  /* Whole pixels only, or the highlight lands mid-pixel and blurs. */
  --bevel: 3px;
  --wrap: 1100px;

  /* Cursor hotspots are the tip-of-arrow pixel (top-left) of each url.
     Three names sharing one file/hotspot for now, until there's separate
     art for a hand/dead cursor — new art is just a url swap here. */
  --cursor-default: url("assets/web/shared/cursor/cursor_cool.png") 0 0, auto;
  --cursor-link: url("assets/web/shared/cursor/cursor_cool.png") 0 0, pointer;
  --cursor-blocked: url("assets/web/shared/cursor/cursor_cool.png") 0 0, not-allowed;
}

/* Set by equipping "the normal mouse" from the inventory (data-mouse on
   <html>), handing the site back to the system pointer. */
[data-mouse="system"] {
  --cursor-default: auto;
  --cursor-link: pointer;
  --cursor-blocked: not-allowed;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  color: var(--yellow);
  /* Verdana, the era's actual screen-text face. Named locally, not
     fetched — the CSP has no font-src, so a webfont would be blocked. */
  font-family: Verdana, Geneva, "DejaVu Sans", system-ui, sans-serif;
  cursor: var(--cursor-default);

  /* Checker tile drawn with two offset 45deg gradients — no request,
     no CSP rule needed. */
  background-color: var(--bg);
  background-image:
    linear-gradient(45deg, var(--bg-tile) 25%, transparent 25% 75%, var(--bg-tile) 75%),
    linear-gradient(45deg, var(--bg-tile) 25%, transparent 25% 75%, var(--bg-tile) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
}

/* --- bevels -------------------------------------------------------------
   Raised catches light top-left, shadow bottom-right; sunken is the
   opposite (so a well and a pressed button look identical). Custom
   properties, not utility classes, so the bevel belongs to the component;
   inset box-shadow, not border, so pressing in costs no layout shift.

   Declared on `*`, not `:root` — load-bearing: a var() inside a custom
   property resolves against the element where the property is *declared*,
   so a :root definition would bake in :root's --lit/--dim/--bevel for
   every component regardless of its own overrides. `*` gives each element
   its own copy to resolve against its own values. */

* {
  --raised:
    inset calc(var(--bevel) * -1) calc(var(--bevel) * -1) 0 var(--dim),
    inset var(--bevel) var(--bevel) 0 var(--lit);
  --sunken:
    inset var(--bevel) var(--bevel) 0 var(--dim),
    inset calc(var(--bevel) * -1) calc(var(--bevel) * -1) 0 var(--lit);
}

/* --- scrollbar ----------------------------------------------------------
   A sunken navy groove with a raised lavender thumb — the one piece of
   default browser chrome that would give the emulation away otherwise.

   On a chrome page this is <main>'s scrollbar (see the shell rules below),
   so it runs from under the top bar to the ticker; unqualified so the
   plain document scrollbar gets it too.

   Two mechanisms since no property covers both engines: Firefox's flat
   `scrollbar-color`, and WebKit/Blink's ::-webkit-scrollbar pseudo-
   elements, which support real box-shadow bevels. Stepper buttons are
   dropped (no img-src in the CSP for an arrow image) — a dragged
   scrollbar is the modern half of this anyway.

   Bevels are written out here rather than via --raised/--sunken: `*`
   doesn't match pseudo-elements, so a scrollbar part would just inherit
   html's already-substituted values and overriding --bevel/--lit on the
   part itself would do nothing. */

/* Chrome supports scrollbar-color too and wins outright, silently
   dropping every bevel below — so guard this to engines with no
   ::-webkit-scrollbar, i.e. actually just Firefox. */
@supports not selector(::-webkit-scrollbar) {
  html {
    scrollbar-color: var(--face) var(--bg);
  }
}

::-webkit-scrollbar {
  width: 16px;
  height: 16px;
}

/* Same 8px checker as the page, so the groove reads as background
   showing through a slot in the chrome. */
::-webkit-scrollbar-track {
  background-color: var(--bg);
  background-image:
    linear-gradient(45deg, var(--bg-tile) 25%, transparent 25% 75%, var(--bg-tile) 75%),
    linear-gradient(45deg, var(--bg-tile) 25%, transparent 25% 75%, var(--bg-tile) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
  box-shadow:
    inset 2px 2px 0 var(--bg-dim),
    inset -2px -2px 0 var(--bg-lit);
}

/* Raised like a button — it is one, the one you hold and drag. */
::-webkit-scrollbar-thumb {
  background: var(--face);
  box-shadow:
    inset -2px -2px 0 var(--dim),
    inset 2px 2px 0 var(--lit);
}

::-webkit-scrollbar-thumb:hover {
  background: #574672;
}

::-webkit-scrollbar-thumb:active {
  box-shadow:
    inset 2px 2px 0 var(--dim),
    inset -2px -2px 0 var(--lit);
}

::-webkit-scrollbar-button {
  display: none;
}

::-webkit-scrollbar-corner {
  background: var(--face);
}

/* Sprite capped at 32x32 (Windows' loading ceiling — bigger is silently
   ignored). Every rule ends in a plain keyword as a fallback if the PNG
   never arrives. Named on links/buttons directly since the UA stylesheet
   already sets a cursor there and would otherwise win over inheritance. */
a,
button,
summary {
  cursor: var(--cursor-link);
}

button[disabled] {
  cursor: var(--cursor-blocked);
}

/* Flex column so a child (e.g. .holding) can centre itself with
   margin-block: auto; a lone flex item still lays out like the plain
   block this used to be, so no page's markup needed to change. */
main {
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* --- the chrome shell ---------------------------------------------------
   On a chrome page (layouts/base.njk sets the class when its `chrome`
   flag is on) the document itself doesn't scroll — body is one viewport
   tall and <main>, the band between the bars, is the scroll container.
   That's what puts the scrollbar between the bars rather than behind both.

   min-height: 0 on main is load-bearing: a flex item won't shrink below
   its content by default, so without it main pushes past the viewport
   and shoves the ticker off the bottom.

   The holding page has no chrome and scrolls normally — nothing here is
   required; without it, the sticky bars still work, just against the
   document instead of <main>. */
.has-chrome {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

.has-chrome > main {
  min-height: 0;
  overflow-y: auto;
  /* Bars are outside the scroller now — just enough clearance to avoid
     an anchor landing flush against the top edge. */
  scroll-padding-top: 1rem;
}

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: 1.25rem;
}

/* Present for screen readers, absent on screen. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Visible only once tabbed to — lets a keyboard skip the nav. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  padding: 0.5rem 1rem;
  background: var(--yellow);
  color: var(--bg);
  font-weight: 700;
}

.skip:focus {
  left: 0;
  z-index: 10;
}

:focus-visible {
  outline: 3px solid var(--yellow);
  outline-offset: 2px;
}

/* --- top bar --------------------------------------------------------
   A raised purple panel. --face is dark, so the yellow lettering comes
   back (it was only ever swapped for grey), keeping the bar consistent
   with the rest of the page. */

/* Sticky as a fallback for a page without the chrome shell (where it's
   pinned another way). z-index 5 clears the ticker's 2 and the home
   page's stacked hero letters (up to 3). */
.topbar {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0.75rem 1.5rem;
  padding: calc(11px + var(--bevel)) calc(1.25rem + var(--bevel));
  background: var(--face);
  box-shadow: var(--raised);
  border-bottom: var(--rule) solid var(--dim);
}

/* Without min-width: 0 the nav keeps its max-content width and grows the
   bar (and everything else) past the viewport. */
.nav {
  flex: 1 1 0;
  min-width: 0;
}

.nav__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Drawn dividers, not typed pipes, so a screen reader doesn't read them —
   one dark line + one light one, a bevel a single pixel wide. */
.nav__list li + li {
  border-left: 2px solid var(--dim);
  box-shadow: inset 2px 0 0 var(--lit);
}

/* Once the nav wraps there's no selector for "first in visual row", so
   the divider on row two's first item would read as a stray mark — drop
   dividers below the wrap width and let spacing do the separating. */
@media (max-width: 560px) {
  .nav__list li + li {
    border-left: none;
  }
}

.nav__list a {
  display: block;
  padding: 0.2rem 0.9rem;
  color: var(--yellow);
  font-size: clamp(0.9rem, 2.2vw, 1.15rem);
  font-weight: 700;
  text-decoration: none;
  /* Hard offset shadow, no blur — keeps bright type legible on a busy
     ground, the era's trick for every heading. */
  text-shadow: 2px 2px 0 var(--dim);
}

.nav__list a:hover,
.nav__list a:focus-visible {
  background: var(--yellow);
  color: var(--bg);
  text-shadow: none;
}

/* Drawn nav buttons are excluded — their pressed state is baked into the
   sprite, so a bevel on top would double the same edge. */
.nav__list a:not(.nav__btn):active {
  box-shadow: var(--sunken);
}

.nav__list a[aria-current="page"] {
  text-decoration: underline;
  text-decoration-thickness: 3px;
  text-underline-offset: 4px;
}

/* --- drawn nav buttons --------------------------------------------------
   Some nav items are drawn: three 128x32 frames each, one per state.
   --nav-btn-px is the pixel-art scale (whole numbers only).

   Selectors name both element and class (`a.nav__btn`) to outweigh the
   `.nav__list a` rules above, including the heaviest one
   ([aria-current]'s underline) — drop the `a` and it reappears over the
   sprite.

   Geometry/state machine live once on .nav__btn; each button supplies
   only its three frames as url() values in custom properties (CSS can't
   build a url() from a name, but can pass a finished one through a var),
   so adding a button is a three-line block. */

:root {
  --nav-btn-px: 1;
}

.nav__list a.nav__btn {
  display: block;
  width: calc(128px * var(--nav-btn-px));
  height: calc(32px * var(--nav-btn-px));
  /* Matches the word links' side padding, for even divider spacing. */
  margin-inline: 0.9rem;
  padding: 0;
  background: var(--btn-rest) no-repeat center / 100% 100%;
  image-rendering: crisp-edges;
  image-rendering: pixelated;
  text-decoration: none;
}

/* Focus gets the hover frame too, so tabbing looks like pointing. */
.nav__list a.nav__btn:hover,
.nav__list a.nav__btn:focus-visible {
  background-image: var(--btn-hover);
}

/* translate, not transform, so it can't clobber anything else. */
.nav__list a.nav__btn:active {
  background-image: var(--btn-press);
  translate: 1px 1px;
}

.nav__btn--home {
  --btn-rest: url("assets/web/shared/buttons/home_button.png");
  --btn-hover: url("assets/web/shared/buttons/home_button_hover.png");
  --btn-press: url("assets/web/shared/buttons/home_button_press.png");
}

.nav__btn--projects {
  --btn-rest: url("assets/web/shared/buttons/projects_button.png");
  --btn-hover: url("assets/web/shared/buttons/projects_button_hover.png");
  --btn-press: url("assets/web/shared/buttons/projects_button_press.png");
}

.nav__btn--socials {
  --btn-rest: url("assets/web/shared/buttons/socials_button.png");
  --btn-hover: url("assets/web/shared/buttons/socials_button_hover.png");
  --btn-press: url("assets/web/shared/buttons/socials_button_press.png");
}

.nav__btn--games {
  --btn-rest: url("assets/web/shared/buttons/games_button.png");
  --btn-hover: url("assets/web/shared/buttons/games_button_hover.png");
  --btn-press: url("assets/web/shared/buttons/games_button_press.png");
}

.nav__btn--about {
  --btn-rest: url("assets/web/shared/buttons/about_button.png");
  --btn-hover: url("assets/web/shared/buttons/about_button_hover.png");
  --btn-press: url("assets/web/shared/buttons/about_button_press.png");
}

/* Hidden like .vh — the sprite says the word visually, text stays for
   screen readers. */
.nav__btn-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- settings tab ------------------------------------------------------
   Sits at top-right where the music tab used to — this one is chrome,
   not a quest reward, so it claims the corner the music tab gave up when
   it moved down. Same flyout mechanics as the music tab (grows out of
   the tab, wireframe first), its own independent instance. */

.settings [hidden] {
  display: none !important;
}

.settings__tab {
  --bevel: 2px;
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.8rem;
  border: 0;
  background: var(--face);
  box-shadow: var(--raised);
  color: var(--yellow);
  font: inherit;
  font-size: 0.8rem;
  font-weight: 700;
  text-transform: uppercase;
}

.settings__tab:active {
  box-shadow: var(--sunken);
  translate: 1px 1px;
}

.settings__panel-outline {
  position: fixed;
  z-index: 31;
  box-sizing: border-box;
  border: 2px solid var(--yellow);
  pointer-events: none;
  transition: left 200ms ease-out, top 200ms ease-out, width 200ms ease-out, height 200ms ease-out;
}

.settings__panel {
  position: fixed;
  top: 4.5rem;
  right: 1rem;
  z-index: 31;
  width: 16rem;
  max-width: calc(100vw - 2rem);
  background: var(--face);
  box-shadow: var(--raised);
  border: var(--rule) solid var(--dim);
  padding: calc(0.9rem + var(--bevel));
}

.settings__panel--hidden {
  opacity: 0;
}

.settings__panel--filling {
  animation: music-panel-fill 90ms ease-out both;
}

.settings__panel--fading {
  animation: music-panel-fill 90ms ease-in both reverse;
}

.settings__panel-title {
  margin: 0 0 0.9rem;
  padding: 0.2rem 0.6rem;
  background: var(--bg);
  color: var(--yellow);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.settings__group + .settings__group {
  margin-top: 0.9rem;
}

.settings__label {
  margin: 0 0 0.5rem;
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.settings__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin: 0 0 0.6rem;
  color: var(--yellow);
  font-size: 0.85rem;
}

.settings__row input[type="checkbox"] {
  width: 1.1rem;
  height: 1.1rem;
  accent-color: var(--yellow);
}

.settings__row .music__slider {
  flex: 1 1 auto;
  width: auto;
}

/* The intensity row: label, then the groove at full panel width with its
   own scale under it. Stacked rather than beside its label like the volume
   row, so the three words line up with the three positions they name — at
   16rem of panel there isn't width for both a label and a labelled slider. */
.settings__row--stacked {
  flex-direction: column;
  align-items: stretch;
  gap: 0.4rem;
}

.settings__row--stacked .music__slider {
  width: 100%;
}

.settings__ticks {
  display: flex;
  justify-content: space-between;
  margin: 0;
  color: var(--muted);
  font-size: 0.65rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Which stop the thumb is actually sitting on. Set by bindSettings(). */
.settings__tick--on {
  color: var(--yellow);
  font-weight: 700;
}

/* CRT switched off: the dial it belongs to has nothing to say, so it drains
   rather than disappearing — a control that vanishes takes the panel's
   height with it and the close button jumps up under the pointer. */
.settings__row--off {
  opacity: 0.45;
}

.settings__row--off .settings__tick--on {
  color: var(--muted);
}

.settings__close {
  width: 100%;
  margin-top: 0.3rem;
}

@media (prefers-reduced-motion: reduce) {
  .settings__panel-outline {
    transition: none;
  }

  .settings__panel--filling,
  .settings__panel--fading {
    animation: none;
  }
}

/* --- music tab -------------------------------------------------------------
   A flyout beside the page, not a dialog over it (see partials/music.njk).
   main.js enables the controls once it finds tracks in data-tracks, and
   grows/shrinks the panel out of the tab with the same wireframe travel
   the inventory uses (quest.css). Hidden until the mouse is equipped, the
   same gate the bag tab waits behind. Sits below the settings tab, the
   same distance down from the top bar the bag/achievements buttons sit
   up from the ticker. */

/* Same [hidden]-vs-flex restatement quest.css makes for its own layer. */
.music [hidden] {
  display: none !important;
}

.music__tab {
  --bevel: 2px;
  position: fixed;
  top: 4.5rem;
  right: 1rem;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.8rem;
  border: 0;
  background: var(--face);
  box-shadow: var(--raised);
  color: var(--yellow);
  font: inherit;
  font-size: 0.8rem;
  font-weight: 700;
  text-transform: uppercase;
}

.music__tab:active {
  box-shadow: var(--sunken);
  translate: 1px 1px;
}

.music__panel-outline {
  position: fixed;
  z-index: 31;
  box-sizing: border-box;
  border: 2px solid var(--yellow);
  pointer-events: none;
  transition: left 200ms ease-out, top 200ms ease-out, width 200ms ease-out, height 200ms ease-out;
}

.music__panel {
  position: fixed;
  top: 8rem;
  right: 1rem;
  z-index: 31;
  width: 18rem;
  max-width: calc(100vw - 2rem);
  background: var(--face);
  box-shadow: var(--raised);
  border: var(--rule) solid var(--dim);
  padding: calc(0.9rem + var(--bevel));
}

/* Same hidden-then-fade beat as the inventory panel (quest.css). */
.music__panel--hidden {
  opacity: 0;
}

.music__panel--filling {
  animation: music-panel-fill 90ms ease-out both;
}

.music__panel--fading {
  animation: music-panel-fill 90ms ease-in both reverse;
}

@keyframes music-panel-fill {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.music__panel-title {
  margin: 0 0 0.9rem;
  padding: 0.2rem 0.6rem;
  background: var(--bg);
  color: var(--yellow);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Off frame at rest, playing frame (a GIF) once main.js adds .is-playing. */
.music__display {
  --music-display-px: 3;
  display: block;
  width: calc(32px * var(--music-display-px));
  height: calc(18px * var(--music-display-px));
  margin: 0 auto 0.7rem;
  background: url("assets/web/shared/musicplayer/musicplayer_off.png") no-repeat center / 100% 100%;
  image-rendering: crisp-edges;
  image-rendering: pixelated;
}

.music__display.is-playing {
  background-image: url("assets/web/shared/musicplayer/musicplayer_on.gif");
}

/* Now-playing readout as a sunken well, like every period media player. */
.music__track {
  --bevel: 2px;
  --lit: var(--bg-lit);
  --dim: var(--bg-dim);
  margin: 0 0 0.7rem;
  padding: 0.3rem calc(0.5rem + var(--bevel));
  background: var(--bg);
  box-shadow: var(--sunken);
  color: var(--muted);
  font-size: 0.75rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* --- the sliders ---------------------------------------------------------
   Timeline and volume are both <input type="range"> rebuilt from a
   navy-sunken groove and a lavender-raised handle. Bevels written out
   directly (not via --raised/--sunken) since the two halves need
   different colours and one element only has one --lit/--dim to hand
   down. --fill (set by main.js) is a hard gradient stop marking played/
   turned-up. Every rule doubled per engine — an unknown track/thumb
   selector invalidates its whole list, so they can't be grouped. */
.music__slider {
  --fill: 0%;
  flex: 1 1 auto;
  width: 100%;
  height: 1rem;
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  appearance: none;
  -webkit-appearance: none;
}

.music__slider[disabled] {
  opacity: 0.55;
}

.music__slider::-webkit-slider-runnable-track {
  height: 0.55rem;
  background: linear-gradient(to right, var(--yellow-dark) var(--fill), var(--bg) var(--fill));
  box-shadow: inset 2px 2px 0 var(--bg-dim), inset -2px -2px 0 var(--bg-lit);
}

.music__slider::-moz-range-track {
  height: 0.55rem;
  background: linear-gradient(to right, var(--yellow-dark) var(--fill), var(--bg) var(--fill));
  box-shadow: inset 2px 2px 0 var(--bg-dim), inset -2px -2px 0 var(--bg-lit);
}

/* Negative margin centres the thumb on the groove — WebKit aligns it to
   the track's top by default, so half the height difference is clawed
   back; Firefox centres it without help. */
.music__slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 0.75rem;
  height: 1rem;
  margin-top: -0.225rem;
  border: 0;
  border-radius: 0;
  background: var(--face);
  box-shadow: inset -2px -2px 0 var(--dim), inset 2px 2px 0 var(--lit);
}

.music__slider::-moz-range-thumb {
  width: 0.75rem;
  height: 1rem;
  border: 0;
  border-radius: 0;
  background: var(--face);
  box-shadow: inset -2px -2px 0 var(--dim), inset 2px 2px 0 var(--lit);
}

.music__seek {
  margin: 0 0 0.7rem;
}

/* Tabular figures so digits changing don't shift the row sideways. */
.music__time {
  display: flex;
  justify-content: space-between;
  margin: 0.15rem 0 0;
  color: var(--muted);
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
}

.music__transport {
  display: flex;
  gap: 0.4rem;
  margin: 0 0 0.6rem;
}

.music__vol {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.9rem;
}

.music__vol-label {
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* --bevel is 2px here (not the bar's 3) — these are small, and a 3px
   edge on a 20px control would be all edge and no face. */
.music__btn {
  --bevel: 2px;
  flex: 1 1 0;
  min-width: 2.2rem;
  padding: 0.3rem 0.4rem;
  border: 0;
  background: var(--face);
  box-shadow: var(--raised);
  color: var(--yellow);
  font: inherit;
  font-size: 0.75rem;
  line-height: 1.3;
}

.music__btn:active:not([disabled]) {
  box-shadow: var(--sunken);
  translate: 1px 1px;
}

.music__btn[disabled] {
  color: var(--muted);
}

.music__label {
  margin: 0 0 0.4rem;
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.music__list {
  margin: 0 0 0.9rem;
  padding: 0;
  list-style: none;
  max-height: 9rem;
  overflow-y: auto;
}

.music__item {
  display: block;
  width: 100%;
  padding: 0.35rem 0.5rem;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.75rem;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.music__item:hover,
.music__item:focus-visible {
  background: #2c2a48;
  color: var(--yellow);
}

.music__item--current {
  color: var(--yellow);
  font-weight: 700;
}

.music__close {
  width: 100%;
}

/* --- the payoff: a track has actually started ----------------------------
   Same slide-in as the achievement toast (quest.css), positioned under
   the tab instead. main.js only shows this while the panel is closed —
   open, the display already says what's playing. */
.music__toast {
  position: fixed;
  top: 8rem;
  right: 1rem;
  z-index: 31;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  max-width: min(20rem, calc(100vw - 2rem));
  padding: 0.75rem calc(0.9rem + var(--bevel));
  background: var(--face);
  box-shadow: var(--raised);
  border: var(--rule) solid var(--dim);
  animation: music-toast 400ms cubic-bezier(0.1, 0.9, 0.2, 1) both;
}

.music__toast--leaving {
  animation: music-toast 350ms ease-in both reverse;
}

@keyframes music-toast {
  from { translate: 0 -140%; opacity: 0; }
  to   { translate: 0 0; opacity: 1; }
}

.music__toast-display {
  flex: 0 0 auto;
  width: 32px;
  height: 18px;
  background: url("assets/web/shared/musicplayer/musicplayer_on.gif") no-repeat center / 100% 100%;
  image-rendering: crisp-edges;
  image-rendering: pixelated;
}

.music__toast-kicker {
  margin: 0;
  color: var(--yellow-dark);
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.music__toast-name {
  margin: 0.2rem 0 0;
  color: var(--yellow);
  font-size: 0.85rem;
  font-weight: 700;
  max-width: 14rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

@media (prefers-reduced-motion: reduce) {
  .music__panel-outline {
    transition: none;
  }

  .music__panel--filling,
  .music__panel--fading,
  .music__toast {
    animation: none;
  }
}

/* --- news ticker -------------------------------------------------------- */

/* Sticky to the bottom of the shell (or viewport, without the shell), so
   the broadcast bar is always visible. */
.ticker {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: flex;
  align-items: stretch;
  border-top: var(--rule) solid var(--dim);
  background: var(--face);
  box-shadow: var(--raised);
  font-size: clamp(0.7rem, 1.8vw, 0.85rem);
  line-height: 2.6;
}

.ticker > * {
  padding-inline: 0.75rem;
  white-space: nowrap;
}

.ticker__clock {
  color: var(--yellow-dark);
  border-right: 2px solid var(--dim);
  box-shadow: inset -4px 0 0 -2px var(--lit);
  font-variant-numeric: tabular-nums;
}

/* A coloured panel needs its own --lit/--dim, not the bar's, or a stray
   purple line shows across it. Yellow lifts to near-white / dark amber. */
.ticker__label {
  --lit: #fffbb4;
  --dim: #8a6d0f;
  background: var(--yellow);
  box-shadow: var(--raised);
  color: var(--bg);
  font-weight: 700;
  letter-spacing: 0.04em;
}

/* Filled red, not red lettering — reads as a broadcast LIVE chyron. */
.ticker__live {
  --lit: #d4585c;
  --dim: #5c1414;
  margin-left: auto;
  background: var(--red);
  box-shadow: var(--raised);
  color: var(--white);
  font-weight: 700;
  text-shadow: 1px 1px 0 var(--dim);
}

/* The headline crawl: main.js clones the run enough times to overflow
   the bar, then starts the animation, so a half-built strip never
   slides. No script = the run sits still and readable. Sunken well, so
   headlines read as scrolling behind the panel, not across it. */
.ticker__track {
  --lit: var(--bg-lit);
  --dim: var(--bg-dim);
  flex: 1;
  min-width: 0;
  overflow: hidden;
  padding-inline: 0;
  background: var(--bg);
  box-shadow: var(--sunken);
}

.ticker__scroll {
  display: flex;
  width: max-content;
}

.ticker__scroll--rolling {
  animation: ticker-crawl linear infinite;
}

.ticker__run {
  display: flex;
  flex: 0 0 auto;
}

/* The bullet doubles as spacing, including run-to-run, so the loop reads
   continuously. */
.ticker__run > span::after {
  content: "\2022";
  padding-inline: 1.25rem;
  color: var(--yellow-dark);
}

@keyframes ticker-crawl {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (max-width: 560px) {
  .ticker__logo,
  .ticker__clock {
    display: none;
  }
}

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