/* ============================================================
   MONOLITH OS — style.css
   One stylesheet for the whole shell + every app. Sections are
   labeled below. Design tokens (colors, fonts) live at the top
   in :root — change the look of the whole site by editing those,
   not by hunting through individual rules.
   ============================================================ */

:root {
  /* ---- The one look — a paper/ink surface, not a screen trying
     to look like a screen. (There used to be a dark "CRT" theme
     with a manual toggle; it's gone — this is just the site now.) ----
     cyan = primary/interactive, ochre = warm/secondary,
     amber = alerts/peak states only. */
  --paper: #EDEAE0;
  --paper-raised: #F6F3EA;
  --ink: #1E1C18;
  --ink-dim: #6E6656;
  --line: #C9C1AC;
  --line-strong: #9C9278;
  --cyan: #147C88;
  --cyan-bright: #1FA7B6;
  --cyan-soft: rgba(20, 124, 136, .12);
  --ochre: #96631E;
  --ochre-soft: rgba(150, 99, 30, .14);
  --amber: #B5701A;
  --amber-soft: rgba(181, 112, 26, .16);
  /* Punchier than the old .22 — the window shadows are now short,
     hard-edged Win98-style drops (see .window below) that need more
     opacity to actually read as "sharp" instead of "faint". */
  --shadow: rgba(30, 24, 18, .5);
  /* Toned down slightly from .035 — still visible texture, less hum. */
  --scanline: rgba(30, 28, 24, .024);

  --font-display: 'DotGothic16', ui-monospace, 'Courier New', monospace;
  --font-body: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'Courier New', monospace;

  /* DotGothic16 is a dot-matrix pixel font — every glyph (including
     punctuation) sits in its own roughly-square cell, and the "."
     is drawn as a tiny mark low in an otherwise-empty one. At UI
     sizes that trailing cell space reads as a stray gap, so "J.OS"
     visually comes out "J. OS". .dot-tight (below) pulls the next
     character in to close it — one shared value so both places that
     spell out "J.OS" (the start button, the mobile header) stay in
     sync. This is a by-eye estimate: if a gap is still visible on
     the real page, make this more negative (e.g. -0.2em); if "OS"
     looks like it's crowding the period, move it toward 0. */
  --dot-tighten: -0.14em;

  color-scheme: light;
}

/* Second color scheme, toggled from the Settings app (see
   js/settings-core.js) via document.documentElement's data-theme
   attribute — every rule below already reads var(--paper) etc., so
   this is the ONLY place a new scheme needs defining: override the
   same custom properties, nothing else has to change. Both schemes
   Jo asked for are light (not a light/dark pair) — this one's a
   cooler, blue-grey "slate" surface instead of the default warm
   parchment, with the same cyan/ochre/amber accents carried over
   unchanged so the site's actual identity colors don't shift. */
:root[data-theme="slate"] {
  --paper: #E4E8EC;
  --paper-raised: #EFF2F5;
  --ink: #16212B;
  --ink-dim: #55636F;
  --line: #C3CBD2;
  --line-strong: #93A0AA;
  --shadow: rgba(15, 20, 26, .5);
  --scanline: rgba(15, 20, 26, .024);
}

/* UI-scale setting (Settings app) — one CSS custom property, read
   by the two zoom rules further down. Defaults to 1 (100%) so a
   visitor who's never touched the setting sees no change; --ui-zoom
   itself gets overwritten on document.documentElement by
   js/settings-core.js the moment a saved (or changed) scale exists. */
:root { --ui-zoom: 1; }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  overflow: hidden;
  height: 100vh;
  -webkit-font-smoothing: antialiased;
}

::selection { background: var(--cyan-soft); color: var(--ink); }

button {
  font-family: inherit;
  color: inherit;
  cursor: pointer;
}

/* Wraps the "." in "J.OS" wherever that label appears (start
   button, mobile header) — see --dot-tighten above for why. */
.dot-tight { margin-right: var(--dot-tighten); }

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

/* ---------------------------------------------------------- */
/* Desktop surface + CRT texture                               */
/* ---------------------------------------------------------- */

#desktop {
  position: relative;
  width: 100%;
  height: 100%;
  /* `zoom` (not transform: scale()) on purpose — it's the one CSS
     property that scales layout, hit-testing, AND text together,
     so dragging this above 100% doesn't need any extra math to
     also resize the box it lives in for things to still look
     right; it just runs past the edge of the screen at high
     settings, like a real OS's UI-scale slider would. */
  zoom: var(--ui-zoom, 1);
  background-image: repeating-linear-gradient(
    to bottom, var(--scanline) 0px, var(--scanline) 1px, transparent 1px, transparent 3px
  );
}

#desktop::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 40%, transparent 55%, var(--shadow) 140%);
  opacity: .5;
}

/* ---------------------------------------------------------- */
/* Icon column — icons are added here by each app's            */
/* registerApp() call in shell.js, not hardcoded in HTML.       */
/* ---------------------------------------------------------- */

/* Just a positioning context now — each .icon places itself with
   its own left/top (see renderIcon() in js/shell.js), so icons can
   be dragged anywhere on the desktop instead of sitting in a fixed
   column. */
#icons {
  position: absolute;
  inset: 0;
  z-index: 1;
  /* This box now covers the WHOLE desktop (see the comment above)
     so icons can be dragged anywhere — but a full-size transparent
     div still intercepts pointer events across its entire area by
     default, even where there's no icon, which silently ate clicks
     on anything sitting underneath it (Notes, at z-index 0, was
     the victim — its pencil/close buttons and dragging all stopped
     working). Click-through here, same pattern already used for
     #notes-layer below: the container ignores pointer events, each
     .icon re-enables them for just its own box. */
  pointer-events: none;
}

.icon {
  position: absolute;
  width: 84px;
  background: transparent;
  border: 1px solid transparent;
  padding: 8px 6px 10px;
  display: grid;
  justify-items: center;
  gap: 8px;
  text-align: center;
  touch-action: none;
  pointer-events: auto;
  /* Backs up img.draggable = false / e.preventDefault() in
     js/shell.js's drag handler — belt-and-suspenders against the
     browser trying to start a native image/text drag or text
     selection instead of letting the custom pointer-drag run. */
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}

.icon img { -webkit-user-drag: none; }

/* Icons are <img> now (assets/icons/*.svg by default — drop in a
   PNG/JPG of your own and update the path in the app's file to
   match, see js/apps/*.js). object-fit keeps a non-square image
   from stretching if Jo's own art isn't a perfect 1:1 square. */
.icon img {
  width: 34px;
  height: 34px;
  object-fit: contain;
  /* crisp, hard-edged offset shadow — no blur — the way Win98's
     chunky icon set faked a bit of depth per-pixel */
  filter: drop-shadow(1.5px 1.5px 0 var(--shadow));
}

.icon span {
  font-family: var(--font-display);
  font-size: 1rem;
  letter-spacing: .05em;
  line-height: 1;
  color: var(--ink);
}

/* ---------------------------------------------------------- */
/* Windows                                                      */
/* ---------------------------------------------------------- */

/* box-shadow is computed from the element's raw rectangle, so it
   would ignore the chamfer entirely and leave a rectangular
   shadow "ghost" behind the cut corner. filter: drop-shadow()
   instead follows the actual rendered (already chamfered)
   silhouette of window + its children, so the shadow is cut
   along with the corner.

   Win98-style shadow: offset only right + down (never up or left),
   short, and mostly hard-edged — a single flat drop with just
   enough blur on its trailing layer to keep it from looking like a
   pasted-on rectangle. Two stacked drops: a near-solid short one for
   the "cut" edge, a slightly softer/longer one behind it for the
   tiny bit of fade. */
.window {
  position: absolute;
  min-width: 260px;
  min-height: 160px;
  filter:
    drop-shadow(3px 3px 0 var(--shadow))
    drop-shadow(5px 5px 2px var(--shadow));
}

/* While shell.js is actively dragging or resizing a window (see
   makeDraggable/makeResizable), drop the three stacked shadows
   above. They're the most expensive part of repainting this
   chamfered/clipped shape, and re-rasterizing them on every single
   pointermove frame was what let the window's content visibly lag
   behind its real, live-changing box — most noticeable as an open
   photo preview briefly looking like it overflowed its frame while
   you dragged the resize corner. Restored the instant you let go. */
.window[data-interacting="true"] {
  filter: none;
}

/* Two stacked, chamfered layers stand in for a 1px border:
   .frame is the "border" color, .surface sits 1px inside it.
   The bottom-right corner is cut on both, so the desktop shows
   through the notch. */
.window .frame {
  width: 100%;
  height: 100%;
  padding: 1px;
  background: var(--line-strong);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 38px), calc(100% - 38px) 100%, 0 100%);
}

.window[data-active="true"] .frame { background: var(--cyan); }

.window .surface {
  width: 100%;
  height: 100%;
  background: var(--paper-raised);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 38px), calc(100% - 38px) 100%, 0 100%);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.titlebar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border-bottom: 1px solid var(--line);
  cursor: grab;
  user-select: none;
  touch-action: none;
}

.titlebar:active { cursor: grabbing; }

.titlebar .title {
  flex: 1;
  font-family: var(--font-display);
  font-size: 1.05rem;
  letter-spacing: .04em;
  color: var(--ink-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.window[data-active="true"] .titlebar { border-bottom-color: var(--cyan); }

.window[data-active="true"] .titlebar .title {
  color: var(--ink);
  text-shadow: 0 0 8px var(--cyan-soft);
}

.win-btn {
  width: 18px;
  height: 18px;
  border: 1px solid var(--line-strong);
  background: transparent;
  display: grid;
  place-items: center;
  line-height: 1;
  font-size: .7rem;
  color: var(--ink-dim);
}

.win-btn:hover { border-color: var(--cyan); color: var(--cyan); }
.win-btn.close:hover { border-color: var(--amber); color: var(--amber); }

/* Extra clearance on the right and bottom (bigger than the 16px
   on top/left) so real content never renders into the chamfered
   corner and gets clipped by it — the corner cut is ~38px, this
   gives a bit more than that. */
.win-body {
  flex: 1;
  overflow: auto;
  padding: 16px 42px 42px 16px;
  font-family: var(--font-body);
  font-size: .92rem;
  line-height: 1.5;
}

/* Invisible hit target sitting over the chamfered notch — the
   cut corner itself is the only visual cue; no drawn handle. */
.resize-grip {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 30px;
  height: 30px;
  cursor: nwse-resize;
  background: transparent;
}

/* ---------------------------------------------------------- */
/* File Explorer                                                */
/* ---------------------------------------------------------- */

.explorer-note {
  font-family: var(--font-mono);
  font-size: .74rem;
  color: var(--ink-dim);
  margin: 0 0 12px;
}

.file-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 4px;
}

.file-item {
  border: 1px solid var(--line);
  background: none;
  padding: 10px 8px;
  display: grid;
  /* Without an explicit column, this single-column grid's implicit
     "auto" track sizes itself off its content instead of filling the
     item's own width — and with a percentage-sized <img> inside
     (.file-thumb's width: 100% below) plus real ~500px auto-generated
     thumbnails (see build-manifest.js), that content-based sizing
     locks the column — and therefore the image sitting inside it — to
     roughly the photo's own pixel size, well past the item's actual
     box, overflowing into neighboring tiles ("grid blowout", a
     well-known CSS Grid pitfall; tiny placeholder test images never
     triggered it, which is why it only showed up once real photos
     did). minmax(0, 1fr) pins the column to exactly the item's
     available width instead, so .file-thumb's 100% resolves against
     THAT, not the photo's own dimensions. */
  grid-template-columns: minmax(0, 1fr);
  gap: 8px;
  justify-items: center;
  text-align: center;
  font-family: inherit;
  color: inherit;
  min-width: 0;
}

.file-item:hover, .file-item:focus-visible { border-color: var(--cyan); }

/* Used both as a CSS-pattern placeholder (no thumb available)
   and as the sizing/border wrapper for a real <img> thumbnail. */
.file-thumb {
  width: 100%;
  /* .file-item (this element's direct parent) is itself display: grid,
     which makes .file-thumb a grid item of THAT (inner) grid too — so
     the same min-content blowout .file-item's own min-width: 0 guards
     against at the outer .file-grid level can also happen one level
     down, sized off .file-thumb's own real pixel dimensions instead of
     its width: 100%. This is what actually caused thumbnails to render
     at full photo size and spill into neighboring grid tiles — fixing
     only .file-item wasn't enough, since the blowout was happening in
     this element's own grid placement, not its container's. */
  min-width: 0;
  aspect-ratio: 4 / 3;
  display: block;
  object-fit: cover;
  background:
    repeating-linear-gradient(45deg, var(--ochre-soft) 0 6px, transparent 6px 12px),
    var(--paper);
  border: 1px solid var(--line);
}

.file-item span {
  font-family: var(--font-mono);
  font-size: .72rem;
  color: var(--ink-dim);
}

/* Folder / "up" tiles in the grid use this instead of .file-thumb —
   same footprint, but centers an icon rather than cropping a photo. */
.folder-thumb {
  width: 100%;
  aspect-ratio: 4 / 3;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--paper);
  border: 1px solid var(--line);
}

.folder-thumb svg { width: 55%; height: 55%; }

/* File-preview windows fill their body with the media area and
   let the image/video shrink-to-fit however the window gets
   resized, instead of a fixed aspect-ratio box that can run
   taller than the window and get scrolled/cut off. Scoped to
   .is-media-view (added by explorer.js) so no other app's
   win-body layout is affected. */
.win-body.is-media-view {
  display: flex;
  flex-direction: column;
}

.file-preview {
  display: flex;
  flex-direction: column;
  gap: 12px;
  flex: 1;
  min-height: 0;
}

.file-preview-media {
  flex: 1;
  min-height: 0;
  width: 100%;
  background: var(--paper);
  border: 1px solid var(--line);
}

.file-preview-media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.file-preview-media iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

.file-meta {
  font-family: var(--font-mono);
  font-size: .78rem;
  color: var(--ink-dim);
  line-height: 1.6;
  margin: 0;
}

/* ---------------------------------------------------------- */
/* Radio — see js/apps/radio.js                                 */
/* ---------------------------------------------------------- */

/* Header row: just the live now-playing line. Radio has no
   play/stop control (see js/apps/radio.js — it starts itself);
   this row, .radio-controls (volume + mute), and the Bandcamp
   link below that are the entire manual control surface. */
.radio-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}

.radio-track {
  font-family: var(--font-mono);
  font-size: .8rem;
  color: var(--ink-dim);
  margin: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.radio-track .dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--amber);
  flex: none;
}

/* Slow pulse + an actual color shift (not just a hue-rotate filter
   wobble, which read as too subtle/arbitrary) while actually
   playing — reads as "alive," not as a blinking notification. The
   scale and the background-color ride the same long, slow keyframe
   so they stay in sync; see @keyframes dot-pulse below. Turned off
   entirely for anyone with prefers-reduced-motion set. */
.radio-track .dot[data-live="true"] {
  animation: dot-pulse 3.6s ease-in-out infinite;
}

.radio-track .dot[data-live="false"] { background: var(--line-strong); }

@keyframes dot-pulse {
  0%   { transform: scale(1);    background: var(--amber); opacity: 1;   }
  50%  { transform: scale(1.35); background: var(--ochre); opacity: .75; }
  100% { transform: scale(1);    background: var(--amber); opacity: 1;   }
}

@media (prefers-reduced-motion: reduce) {
  .radio-track .dot[data-live="true"] { animation: none; }
  .radio-track .now-playing.is-loading { animation: none; }
}

.radio-track .now-playing {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* "LOADING…" during the brief gap between opening Radio and the
   first sound actually starting — see js/apps/radio.js's `loading`
   state. Reuses the exact same hard-cut blink @keyframes the boot
   screen's terminal caret already uses (#boot .caret, further down
   this file) rather than inventing a second blink animation — a
   snap on/off, not a fade, matching this site's flat/hard-edged
   visual language. Turned off for prefers-reduced-motion in the
   dot's own media query just above, alongside the dot's pulse. */
.radio-track .now-playing.is-loading {
  animation: blink 1s steps(1) infinite;
}

canvas#viz {
  width: 100%;
  height: 120px;
  background: var(--paper);
  border: 1px solid var(--line);
  box-shadow: inset 0 0 26px -6px var(--cyan-soft);
  display: block;
}

.radio-controls { display: flex; align-items: center; gap: 12px; margin-top: 12px; }

.transport-btn {
  border: 1px solid var(--line-strong);
  background: transparent;
  padding: 6px 12px;
  font-family: var(--font-display);
  font-size: .95rem;
  letter-spacing: .04em;
}

.transport-btn:hover { border-color: var(--cyan); color: var(--cyan); }
.transport-btn[data-on="true"] { border-color: var(--cyan); color: var(--cyan); background: var(--cyan-soft); }
.mute-btn[data-on="true"] { border-color: var(--amber); color: var(--amber); background: var(--amber-soft); }

/* An <a>, not a <button> — reuses .transport-btn's look but needs
   its own display/underline reset. Sits below the volume/mute row
   as a full-width row of its own, not squeezed in next to
   anything. */
.bandcamp-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  margin-top: 10px;
  text-decoration: none;
  color: var(--ink-dim);
  white-space: nowrap;
}

.bandcamp-btn:hover { border-color: var(--ochre); color: var(--ochre); }

input[type="range"] { flex: 1; accent-color: var(--cyan); }
input[type="checkbox"], input[type="radio"] { accent-color: var(--cyan); width: 15px; height: 15px; }

/* Radio's volume slider gets disabled on iOS, where scripted volume
   doesn't work at all (see IOS_NO_VOLUME_CONTROL in js/apps/radio.js)
   — dim it and swap the cursor so it visibly reads as "not usable
   here" instead of looking broken. */
input[type="range"]:disabled { opacity: .4; cursor: not-allowed; }

/* ---------------------------------------------------------- */
/* CV.bat — see js/apps/cv.js and data/cv-data.js                */
/* ---------------------------------------------------------- */

/* Scales with the window instead of sitting at a fixed pixel size
   — a percentage width of win-body (which itself tracks the
   window's live width as you drag the resize grip, same mechanism
   as the Explorer/Web Shop preview images below) capped at both
   ends so it stays a sensible "photo on a CV" size: never a tiny
   speck in a narrow window, never a giant headshot in a wide one.
   aspect-ratio keeps it square without a fixed height. */
.cv-photo-wrap {
  width: 26%;
  min-width: 56px;
  max-width: 140px;
  aspect-ratio: 1 / 1;
  margin: 0 auto 14px;
  border: 1px solid var(--line-strong);
  background: var(--paper);
  overflow: hidden;
}

.cv-photo-wrap img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.cv-name {
  margin: 0 0 2px;
  text-align: center;
  font-family: var(--font-display);
  font-size: 1.1rem;
  letter-spacing: .04em;
  color: var(--ink);
}

.cv-role {
  margin: 0 0 20px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: .74rem;
  color: var(--ink-dim);
}

.cv-section { margin-bottom: 18px; }
.cv-section:last-child { margin-bottom: 0; }

.cv-section h3 {
  margin: 0 0 8px;
  font-family: var(--font-display);
  font-size: .85rem;
  letter-spacing: .06em;
  color: var(--cyan);
  border-bottom: 1px solid var(--line);
  padding-bottom: 6px;
}

.cv-section p {
  margin: 0 0 8px;
  line-height: 1.55;
  white-space: pre-line;
}

.cv-section p:last-child { margin-bottom: 0; }

/* ---------------------------------------------------------- */
/* Tetris / Snake — real games (see js/apps/tetris.js, snake.js) */
/* ---------------------------------------------------------- */

/* Board + side panel sit side by side; each game sets its own
   grid-template-columns/width/height inline (column count and cell
   size differ per game) — everything else about how a "board" looks
   lives here so both games share one visual language. Individual
   cell colors are set directly in JS (style.background per cell)
   rather than through CSS classes, since Tetris alone needs 7
   distinct piece colors — see the COLORS object near the top of
   each game file to retheme them. */
.game-wrap {
  position: relative;
  display: flex;
  gap: 16px;
  align-items: flex-start;
}

.game-board {
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line-strong);
  flex: none;
  /* Width/height are set directly in JS (see fitBoard() in
     tetris.js/snake.js) to grow or shrink the board as the window
     is resized — border-box so that JS-set size already accounts
     for this rule's own border instead of the border adding a
     couple of stray extra pixels on top of it. */
  box-sizing: border-box;
}

.game-board > div { background: var(--paper); }

.game-side {
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-width: 84px;
}

/* Tetris' "next piece" mini-board — same idea as .game-board, just
   a fixed tiny 4x4 grid, so it's its own rule rather than another
   inline-sized .game-board. */
.game-next {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(4, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line-strong);
  width: 56px;
  height: 56px;
  margin-top: 4px;
}

.game-next > div { background: var(--paper); }

.game-hud {
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: .04em;
  color: var(--ink-dim);
}

.game-hud strong {
  display: block;
  font-family: var(--font-display);
  font-size: 1.1rem;
  color: var(--ink);
  margin-top: 2px;
}

/* Covers the board (not the whole window) for the "press start" /
   "game over" states — .game-board-outer (set inline by each game,
   right next to its .game-board) is the positioning context. The
   [hidden] override rule is needed because a plain .game-overlay
   rule setting display:flex would otherwise beat the browser's own
   [hidden]{display:none} default at equal specificity depending on
   source order — this rule's higher specificity makes hiding it
   reliable regardless. */
.game-overlay {
  position: absolute;
  inset: 0;
  background: rgba(237, 234, 224, .93);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  text-align: center;
  padding: 10px;
  cursor: pointer;
}

.game-overlay[hidden] { display: none; }

.game-overlay span {
  font-family: var(--font-display);
  font-size: 1.15rem;
  letter-spacing: .05em;
  color: var(--ink);
}

.game-overlay p {
  margin: 0;
  font-family: var(--font-mono);
  font-size: .74rem;
  color: var(--ink-dim);
}

.game-btn {
  font-family: var(--font-display);
  font-size: .82rem;
  letter-spacing: .04em;
  border: 1px solid var(--line-strong);
  background: var(--paper-raised);
  padding: 6px 16px;
  color: var(--ink);
  cursor: pointer;
}

.game-btn:hover { border-color: var(--cyan); color: var(--cyan); }

/* Game Boy's screen — a real <canvas> the emulator draws into
   directly, instead of a div grid like Tetris/Snake's .game-board,
   so it only needs a border/background to match while blank (still
   loading, or no ROM found). Sized live by fitScreen() in
   js/apps/gameboy.js, the exact same technique as fitBoard() in
   tetris.js/snake.js just fitting a fixed 160:144 box instead of a
   cell grid — canvas.width/height (the real pixel buffer) stays
   fixed at the Game Boy's native 160x144 the whole time; only the
   CSS display size changes, which is what keeps the scaling crisp
   instead of blurry. GameBoyCore.js sets the actual crisp/pixelated
   image-rendering CSS on the canvas itself (see settings[13] in
   gameboy.js), so nothing needed here for that part. */
.gb-screen {
  display: block;
  background: #0b0f08;
  border: 1px solid var(--line-strong);
  flex: none;
  /* Belt-and-suspenders on top of what GameBoyCore.js sets on this
     canvas itself (see settings[13] in js/apps/gameboy.js) — that
     file predates the modern standard "pixelated" keyword and only
     sets older vendor-prefixed image-rendering values, which a
     current browser may not recognize/honor. This is the one that
     actually matters today; keep it even if that upstream file ever
     gets updated. */
  image-rendering: pixelated;
}

.score-row {
  display: flex;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: .8rem;
  border-top: 1px solid var(--line);
  padding: 6px 0;
}

.score-row .rank { color: var(--ochre); }

/* ---------------------------------------------------------- */
/* Taskbar                                                      */
/* ---------------------------------------------------------- */

#taskbar {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  /* 25% bigger than the original 40px, per Jo's request. */
  height: 50px;
  display: flex;
  align-items: center;
  gap: 10px;
  /* Left padding matches #icons' left:24px so the J button's box
     lines up exactly under the desktop icon column (see #start-btn
     below, which also matches .icon's width for the same reason). */
  padding: 0 10px 0 24px;
  background: var(--paper-raised);
  border-top: 1px solid var(--line-strong);
  z-index: 500;
}

/* Taskbar buttons, sized in a deliberate scale: J.OS (the start
   button) reads clearly as the "biggest" thing in the bar without
   being large — bigger than the open-app tabs and the CRT toggle,
   but nowhere near its old size.

   Styled to read as a real, physical button rather than a link:
   neutral chrome (--paper-raised/--ink/--line-strong, same family
   as every window and icon — NOT the cyan accent, which is reserved
   for actually-interactive/active states elsewhere) with a short,
   hard-edged offset shadow — the same Win98 "flat drop, no blur"
   language as .window/#start-menu/.icon img above — so it reads as
   sitting slightly PROUD of the taskbar. :active below tucks that
   shadow away and nudges the button down into it, the classic
   "physically pressed" illusion; deliberately no transition on
   either state, since a real button snaps, it doesn't ease. */
#start-btn {
  position: relative;
  border: 1px solid var(--line-strong);
  background: var(--paper-raised);
  color: var(--ink);
  font-family: var(--font-display);
  /* Bumped up from .74rem and given real weight per Jo's request —
     DotGothic16 only ships one true weight, so "bold" here is the
     browser's synthesized bold rather than a distinct bold font
     file, which is the standard/expected way to bold a single-
     weight webfont. */
  font-size: .82rem;
  font-weight: 700;
  letter-spacing: .03em;
  /* Guards against an actual line-break, on top of .dot-tight above
     fixing the "J. OS" look — belt and suspenders, since this label
     must never wrap at this fixed width. */
  white-space: nowrap;
  /* Fixed width matching .icon's width (84px), with the taskbar's
     left padding matching #icons' left offset (24px) above — this
     locks the button's horizontal center to the same x-position as
     the desktop icon column's center, regardless of font rendering. */
  width: 84px;
  flex: none;
  text-align: center;
  padding: 4px 0;
  box-shadow: 2px 2px 0 var(--shadow);
}

#start-btn:active {
  box-shadow: none;
  transform: translate(2px, 2px);
}

#tabs { flex: 1; display: flex; gap: 6px; overflow-x: auto; }

.tab {
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink-dim);
  font-family: var(--font-mono);
  font-size: .7rem;
  padding: 4px 9px;
  white-space: nowrap;
}

.tab[data-active="true"] { border-color: var(--cyan); color: var(--cyan); }

#status {
  display: flex;
  align-items: center;
  gap: 14px;
  font-family: var(--font-mono);
  font-size: .74rem;
  color: var(--ink-dim);
}

#clock { font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------- */
/* Start menu — a small contact card, not an app launcher       */
/* ---------------------------------------------------------- */

#start-menu {
  position: absolute;
  left: 10px;
  /* Taskbar is 50px tall now — keep the same ~10px gap above it. */
  bottom: 60px;
  width: 220px;
  background: var(--paper-raised);
  border: 1px solid var(--cyan);
  /* Same Win98-style shadow as .window below (see the comment
     there): short, hard-edged, offset only right + down, two
     stacked drops. This used to be a big soft centered blur left
     over from before that convention existed — box-shadow (not
     filter: drop-shadow()) is fine here since, unlike .window, this
     box has no chamfered corner for a rectangular shadow to look
     wrong against. */
  box-shadow: 3px 3px 0 var(--shadow), 5px 5px 2px var(--shadow);
  padding: 14px 16px;
  z-index: 600;
  display: none;
  flex-direction: column;
  gap: 10px;
}

#start-menu[data-open="true"] { display: flex; }

.start-menu-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: .85rem;
  letter-spacing: .08em;
  color: var(--cyan);
  border-bottom: 1px solid var(--line);
  padding-bottom: 8px;
}

.start-menu-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: .78rem;
}

.start-menu-list li { display: flex; flex-direction: column; gap: 1px; }

.start-menu-label {
  font-size: .66rem;
  letter-spacing: .06em;
  color: var(--ink-dim);
}

.start-menu-list a {
  color: var(--ink);
  text-decoration: none;
  word-break: break-word;
}

.start-menu-list a:hover { color: var(--cyan); text-decoration: underline; }

.start-menu-location {
  margin: 0;
  padding-top: 8px;
  border-top: 1px dashed var(--line);
  font-family: var(--font-mono);
  font-size: .7rem;
  letter-spacing: .05em;
  color: var(--ink-dim);
  text-align: right;
}

/* ---------------------------------------------------------- */
/* Boot sequence                                                */
/* ---------------------------------------------------------- */

#boot {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: #060607;
  color: #7FF0F5;
  font-family: var(--font-display);
  font-size: 1.1rem;
  letter-spacing: .02em;
  padding: 10vh 8vw 0;
  line-height: 1.9;
  white-space: pre-wrap;
  transition: opacity .5s ease, filter .5s ease;
}

#boot[data-done="true"] { opacity: 0; filter: brightness(3); pointer-events: none; }

#boot .caret {
  display: inline-block;
  width: .55em;
  height: 1em;
  background: #7FF0F5;
  vertical-align: -.15em;
  animation: blink 1s steps(1) infinite;
}

@keyframes blink { 50% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  #boot { transition: none; }
  #boot .caret { animation: none; }
}

/* ---------------------------------------------------------- */
/* Mobile mode — a phone-sized "OS": one bottom taskbar, one open  */
/* app at a time, fixed/centered instead of draggable             */
/* ---------------------------------------------------------- */

#mobile-view { display: none; }

@media (max-width: 640px) {
  html, body { overflow: hidden; }
  body { height: 100vh; height: 100dvh; }
  #desktop { display: none; }

  #mobile-view {
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
  }

  #mobile-header { flex: none; padding: 14px 16px 8px; }
  #mobile-header h1 { font-family: var(--font-display); font-size: 1.5rem; letter-spacing: .03em; margin: 0; white-space: nowrap; }

  /* The mobile "desktop" — icons live here directly (see
     #mobile-icons below), same as the real desktop, instead of a
     separate taskbar. #mobile-window-area itself is NOT positioned
     (see the comment on .mobile-window below for why that matters)
     so it has zero footprint and never blocks taps on the icons
     when nothing's open. */
  #mobile-desktop { flex: 1 1 auto; position: relative; overflow: hidden; min-height: 0; }

  /* flex-direction: column + flex-wrap: wrap, with the container's
     own height capped to the available space, is what makes icons
     stack downward and then wrap into a second column once a column
     runs out of room — the same "wrap into columns" behavior as the
     real desktop's ICONS_PER_COLUMN math in js/shell.js, but done
     entirely in CSS since mobile icons don't need JS-tracked pixel
     positions the way draggable desktop icons do. */
  #mobile-icons {
    position: absolute;
    left: 8px;
    top: 8px;
    bottom: 8px;
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
    align-content: flex-start;
    gap: 4px; /* 2px * 1.75, rounded */
  }

  /* Same basic look as .icon above (same label font/color), just
     sized up — these were originally kept pixel-for-pixel identical
     to the desktop icons, but at touch scale that read as too small,
     so this is 1.75x the original 74px/32px/.8rem sizing (width,
     image size, font-size, and internal spacing all scaled together)
     — mobile icons only, desktop icons untouched. data-active is the
     one addition beyond sizing — there's no taskbar tab to show
     "this one's open" anymore, so the icon itself gets a quiet cyan
     highlight instead (mostly for a keyboard/screen-reader user,
     since visually the icon is normally covered by its own open
     window — see .mobile-window below). */
  .mobile-icon {
    flex: none;
    width: 130px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 7px;
    border: 1px solid transparent;
    background: transparent;
    padding: 10px 7px;
    color: var(--ink);
    font-family: var(--font-display);
    font-size: 1.4rem;
    letter-spacing: .03em;
    text-align: center;
  }
  .mobile-icon img {
    width: 56px;
    height: 56px;
    object-fit: contain;
    -webkit-user-drag: none;
    filter: drop-shadow(1.5px 1.5px 0 var(--shadow));
  }
  .mobile-icon span { white-space: nowrap; }
  .mobile-icon[data-active="true"] { border-color: var(--cyan); background: var(--cyan-soft); }

  /* Reuses .window/.frame/.surface/.titlebar/.win-body as-is (see
     the "Windows" section above) — this just overrides position/
     sizing so it's fixed and fills #mobile-desktop instead of being
     absolutely positioned by JS and freely draggable/resizable.
     Positioned against #mobile-desktop (the nearest POSITIONED
     ancestor — #mobile-window-area itself is deliberately left
     unpositioned, see above) rather than its immediate parent, which
     is what lets that parent stay a harmless zero-size wrapper when
     empty. No resize grip is rendered for these at all (see
     js/shell.js), and there's nothing to drag a window with no drag
     handle logic attached to, so .titlebar's grab cursor is harmless
     dead weight here rather than something that needs its own
     override. */
  .mobile-window {
    position: absolute;
    inset: 14px;
    min-width: 0;
    min-height: 0;
  }
  .mobile-window .win-body { padding: 14px 30px 30px 14px; }

  /* Explorer/Events/Web Shop's mobile grids all reuse the same
     .file-grid/.file-item markup as the desktop versions (and each
     other) — thumbnails only, no visible label, same as the old
     mobile Explorer grid this replaces; folders/categories/".. UP"
     keep their label since there's no thumbnail image to show. */
  #mobile-window-area .file-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
  #mobile-window-area .file-item { padding: 4px; }
  #mobile-window-area .file-item span { display: none; }
  #mobile-window-area .file-item.is-folder span { display: block; }
}

/* ---------------------------------------------------------- */
/* Mobile lightbox                                              */
/* ---------------------------------------------------------- */

#mobile-lightbox {
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: rgba(3, 4, 6, .94);
  display: none;
}

#mobile-lightbox[data-open="true"] { display: block; }

#mobile-lightbox .lightbox-media {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8%;
}

#mobile-lightbox .lightbox-media img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  display: block;
}

#mobile-lightbox .lightbox-media iframe {
  width: 100%;
  height: 40vh;
  border: 0;
}

/* Optional caption line — only Events and Web Shop's mobile grids
   populate this (see js/apps/events.js / js/apps/webshop.js); plain
   Explorer photos leave it empty, where it just stays invisible.
   pointer-events: none so it never blocks the invisible tap zones
   sitting underneath it (see below) — a tap on the caption still
   reaches whichever zone is behind it. */
#mobile-lightbox .lightbox-caption {
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: 4%;
  z-index: 1;
  margin: 0;
  padding: 8px 10px;
  background: rgba(3, 4, 6, .72);
  border: 1px solid var(--line-strong);
  color: #EAF3F3;
  font-family: var(--font-mono);
  font-size: .74rem;
  line-height: 1.4;
  text-align: center;
  pointer-events: none;
}

#mobile-lightbox .lightbox-caption:empty { display: none; }

/* Invisible tap zones — deliberately no drawn arrows. Sit above
   the media so a tap anywhere on the image still hits a zone. */
#mobile-lightbox .lightbox-zone {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 32%;
  border: 0;
  background: transparent;
  padding: 0;
}

#mobile-lightbox .lightbox-zone.prev { left: 0; }
#mobile-lightbox .lightbox-zone.next { right: 0; }
#mobile-lightbox .lightbox-zone.close { left: 32%; right: 32%; }

#mobile-lightbox .lightbox-close-btn {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 32px;
  height: 32px;
  border: 1px solid var(--line-strong);
  background: var(--paper-raised);
  color: var(--ink-dim);
  font-size: 1rem;
}

/* ---------------------------------------------------------- */
/* Misregistration hover                                       */
/* ---------------------------------------------------------- */

/* A tiny "off-register print" ghost — faint offset cyan/ochre
   copies of the text, like a print run a hair out of registration
   — on hover/focus of a handful of key controls (the J button,
   taskbar tabs, transport buttons). Kept from an earlier
   experimental pass; tuned per Jo's feedback to be more
   transparent and to set the ghost copies closer together than
   the first version. Deliberately not applied everywhere — that
   would read as busy/broken, not loud. */
#start-btn:hover,
#start-btn:focus-visible,
.tab:hover,
.transport-btn:hover {
  text-shadow: 1px 0 rgba(20, 124, 136, .5), -1px 0 rgba(150, 99, 30, .5);
}

/* ---------------------------------------------------------- */
/* Notes — background widgets (see js/apps/notes.js)            */
/* ---------------------------------------------------------- */

/* Sits behind #icons (z-index 1) and every .window (z-index 10+,
   assigned dynamically in js/shell.js) — pointer-events is off on
   the layer itself so an empty stretch of it never blocks clicks
   to anything else, and back on per-note below. */
#notes-layer {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* Deliberately the opposite of .window: no chamfered corner, no
   titlebar, no drop-shadow — just a faint tint and a dashed line,
   so a note reads as part of the desktop surface rather than
   another app window competing for attention. Locked by default
   (data-editing="false", set in js/apps/notes.js) — no resize
   handle, no dragging, read-only text — until the pencil button is
   toggled on. Resizing, when unlocked, is native CSS `resize: both`
   (the browser's own corner grip) rather than a custom JS
   implementation — one less thing to get wrong, and it already
   matches "drag the corner to resize" everywhere else. */
.note {
  position: absolute;
  min-width: 140px;
  min-height: 110px;
  display: flex;
  flex-direction: column;
  background: rgba(150, 99, 30, .07);
  border: 1px dashed var(--line);
  border-radius: 2px;
  pointer-events: auto;
  resize: none;
  overflow: hidden;
}

.note[data-editing="true"] {
  border-style: solid;
  border-color: var(--ochre);
  resize: both;
}

.note-handle {
  flex: none;
  height: 14px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 4px;
  cursor: default;
  touch-action: none;
}

.note[data-editing="true"] .note-handle { cursor: grab; }
.note[data-editing="true"] .note-handle:active { cursor: grabbing; }

/* Both buttons are hidden until the note is hovered/focused so
   they don't clutter the desktop at rest — these are meant to
   blend in, not present a toolbar. While a note IS in edit mode
   they stay faintly visible even without hovering, so the pencil
   (your only way back to "locked") doesn't disappear on you. */
.note-edit,
.note-close {
  border: 0;
  background: transparent;
  color: var(--ink-dim);
  font-size: .8rem;
  line-height: 1;
  padding: 2px 4px;
  opacity: 0;
  transition: opacity .15s ease;
}

.note:hover .note-edit,
.note:hover .note-close,
.note-edit:focus-visible,
.note-close:focus-visible {
  opacity: .55;
}

.note[data-editing="true"] .note-edit,
.note[data-editing="true"] .note-close {
  opacity: .55;
}

.note-edit:hover { opacity: 1 !important; color: var(--cyan); }
.note-close:hover { opacity: 1 !important; color: var(--ochre); }

.note-edit[aria-pressed="true"] { color: var(--ochre); }

.note-body {
  flex: 1;
  border: 0;
  background: transparent;
  resize: none;
  outline: none;
  padding: 2px 10px 10px;
  font-family: var(--font-mono);
  font-size: .78rem;
  line-height: 1.5;
  color: var(--ink);
}

.note-body:read-only { cursor: default; }
.note-body::placeholder { color: var(--ink-dim); opacity: .45; }

/* ---------------------------------------------------------- */
/* Web Shop — see js/apps/webshop.js                            */
/* ---------------------------------------------------------- */

/* Category/item browsing reuses .file-grid/.file-item/.folder-thumb
   /.file-thumb from Explorer as-is (see css rules above) — Web Shop
   is meant to look like a sibling of Explorer, not a new visual
   language. Only the item PREVIEW window needs its own rules,
   below, for the edition-cycling arrows/counter and the info rows. */

.shop-media {
  position: relative;
}

.shop-cycle {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 30px;
  height: 30px;
  border: 1px solid var(--line-strong);
  background: var(--paper-raised);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 1.15rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: .85;
}

.shop-cycle:hover { border-color: var(--cyan); color: var(--cyan); opacity: 1; }
.shop-cycle.prev { left: 8px; }
.shop-cycle.next { right: 8px; }

.shop-edition-counter {
  font-family: var(--font-mono);
  font-size: .74rem;
  letter-spacing: .04em;
  color: var(--ink-dim);
  text-align: center;
  margin: 0;
}

/* .shop-preview extends .file-preview (flex column, media flexes
   to fill, everything below keeps its natural height) — the extra
   rows (edition counter, .file-meta lines) just stack under the
   media the same way. */
.shop-preview .file-meta a { color: var(--cyan); }
.shop-preview .file-meta a:hover { color: var(--ochre); }

/* ---------------------------------------------------------- */
/* Settings — see js/apps/settings.js                          */
/* ---------------------------------------------------------- */

.settings-list {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.settings-row-label {
  font-family: var(--font-display);
  font-size: .82rem;
  letter-spacing: .05em;
  margin: 0 0 6px;
  color: var(--ink);
}

.settings-row-desc {
  font-family: var(--font-mono);
  font-size: .7rem;
  line-height: 1.45;
  color: var(--ink-dim);
  margin: 6px 0 0;
}

.settings-range-wrap {
  display: flex;
  align-items: center;
  gap: 10px;
}

.settings-range-value {
  font-family: var(--font-mono);
  font-size: .78rem;
  color: var(--ink-dim);
  min-width: 3.4em;
  text-align: right;
}

.settings-scheme-wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
}

.settings-scheme-option {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: .78rem;
  color: var(--ink);
  cursor: pointer;
}

.settings-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: .78rem;
  letter-spacing: .04em;
  color: var(--ink);
  cursor: pointer;
}

.settings-ip-wrap {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.settings-ip-value {
  font-family: var(--font-mono);
  font-size: .82rem;
  color: var(--ink);
  background: var(--paper-raised);
  border: 1px solid var(--line);
  padding: 4px 8px;
}

.settings-hal-preview { margin-left: 10px; }

/* ---------------------------------------------------------- */
/* Soundboard — see js/apps/soundboard.js                      */
/* ---------------------------------------------------------- */

.soundboard-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}

.soundboard-pad {
  border: 1px solid var(--line-strong);
  background: var(--paper-raised);
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  position: relative;
}

.soundboard-pad-label {
  font-family: var(--font-mono);
  font-size: .74rem;
  color: var(--ink);
  margin: 0;
  line-height: 1.3;
  /* Long labels shouldn't blow out a pad's fixed grid width —
     clamps to two lines and ellipsizes past that. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.soundboard-pad-error {
  display: none;
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .05em;
  color: var(--amber);
  margin: 0;
}
.soundboard-pad[data-error="true"] .soundboard-pad-error { display: block; }

.soundboard-play-btn { width: 100%; }

/* Per-pad volume stepper (− / +), 0-100 in steps of 10 — see
   "PER-PAD VOLUME" in js/apps/soundboard.js's header comment. */
.soundboard-vol-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.soundboard-vol-btn {
  width: 26px;
  height: 26px;
  border: 1px solid var(--line-strong);
  background: var(--paper-raised);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 1rem;
  line-height: 1;
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
}
.soundboard-vol-btn:hover { border-color: var(--cyan); color: var(--cyan); }
.soundboard-vol-btn:disabled {
  opacity: .45;
  cursor: not-allowed;
  border-color: var(--line-strong);
  color: var(--ink);
}

.soundboard-vol-value {
  font-family: var(--font-mono);
  font-size: .74rem;
  color: var(--ink-dim);
  min-width: 34px;
  text-align: center;
}

.soundboard-loop-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .04em;
  color: var(--ink-dim);
  cursor: pointer;
}

.soundboard-footer {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  flex-wrap: wrap;
  border-top: 1px solid var(--line);
  padding-top: 14px;
}

.soundboard-now {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  flex: 1;
}

.soundboard-now-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: .7rem;
  color: var(--ink);
  background: var(--cyan-soft);
  border: 1px solid var(--cyan);
  padding: 3px 4px 3px 8px;
}

.soundboard-now-stop {
  border: 0;
  background: transparent;
  color: var(--ink-dim);
  font-family: var(--font-display);
  font-size: .9rem;
  line-height: 1;
  padding: 0 2px;
}
.soundboard-now-stop:hover { color: var(--ochre); }

/* ---------------------------------------------------------- */
/* HAL mode — see js/settings-core.js. An ORIGINAL full-screen  */
/* "rogue AI" effect built from this site's own accent colors   */
/* and scanline texture, not a recreation of any specific film's */
/* imagery — the eye shape, colors, and composition here are all */
/* this site's own, and the on-screen lines are original text,   */
/* not quoted dialogue from anywhere.                            */
/*                                                                */
/* 2026-09-17: reworked to actually match the rest of the site   */
/* instead of reading as a generic glowing sci-fi orb — hard-    */
/* edged posterized rings sliced by a conic seam pattern into    */
/* wedges (this site avoids soft gradient blur everywhere else — */
/* see .window/#start-menu/#start-btn's shadows), a fine         */
/* crosshatched pixel-grid layered over it, two jagged crack      */
/* fissures that flicker in and out independently, and a         */
/* stepped/jittery glitch animation instead of a calm breathing   */
/* pulse — reads as something pixelated and coming apart, not     */
/* glowing peacefully. Every color below is one of the site's own */
/* --amber/--ochre tokens or a hand-picked darker step that stays */
/* in that same warm hue family (~30-35°) — nothing here reads as */
/* red the way the previous version's one-off #4A1B08/#0A0504     */
/* shades drifted toward (they sat closer to true red, ~10-17°).  */
/* ---------------------------------------------------------- */

#hal-overlay {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  background: #120C06;
  opacity: 0;
  pointer-events: none;
  transition: opacity .4s ease;
  cursor: pointer;
}
#hal-overlay[data-open="true"] {
  opacity: 1;
  pointer-events: auto;
}

.hal-eye {
  position: relative;
  width: min(38vw, 260px);
  height: min(38vw, 260px);
  border-radius: 50%;
  /* Four flat layers, none of them actually interpolating a smooth
     fade: a repeating conic seam (12 dark spokes, fragmenting the
     disc into wedges), a crosshatched pixel-grid (two fine repeating
     linear-gradients, one per axis), and a posterized radial made of
     hard same-color stop PAIRS instead of a gradient — three solid
     rings (amber / ochre / dark ochre-brown) with sharp edges
     between them, the way a limited-palette sprite would shade a
     circle rather than a smooth CSS blur. */
  background:
    repeating-conic-gradient(from 0deg, rgba(10, 7, 4, .6) 0deg 2deg, transparent 2deg 30deg),
    repeating-linear-gradient(0deg, rgba(10, 7, 4, .3) 0 2px, transparent 2px 4px),
    repeating-linear-gradient(90deg, rgba(10, 7, 4, .3) 0 2px, transparent 2px 4px),
    radial-gradient(circle at 50% 50%,
      var(--amber) 0% 16%,
      var(--ochre) 16% 16%, var(--ochre) 16% 38%,
      #5C3B14 38% 38%, #5C3B14 38% 62%,
      #2B1B0A 62% 62%, #2B1B0A 62% 100%);
  /* A solid ring rim instead of a soft glow, plus the exact same
     hard offset drop this site uses on every window/icon/button. */
  box-shadow: 0 0 0 3px #2B1B0A, 4px 4px 0 var(--shadow);
  animation: hal-glitch 2.2s steps(1) infinite;
}

/* steps(1) on every HAL keyframe below is deliberate — a smooth
   ease reads as breathing calmly, an instant snap between held
   states reads as a signal glitching, which is the point. */
@keyframes hal-glitch {
  0%, 100% { transform: scale(1) translate(0, 0); filter: brightness(1); }
  6%  { transform: scale(1.05) translate(-2px, 1px); filter: brightness(1.3); }
  8%  { transform: scale(.97) translate(2px, -1px); filter: brightness(.82); }
  10% { transform: scale(1) translate(0, 0); filter: brightness(1); }
  46% { transform: scale(1.06) translate(1px, -2px); filter: brightness(1.35); }
  49% { transform: scale(1) translate(0, 0); filter: brightness(1); }
  73% { transform: scale(.96) translate(-1px, 1px); filter: brightness(.8); }
  76% { transform: scale(1) translate(0, 0); filter: brightness(1); }
}
.hal-line {
  font-family: var(--font-display);
  font-size: 1.1rem;
  letter-spacing: .08em;
  color: var(--amber);
  text-shadow: 0 0 12px rgba(181, 112, 26, .8);
  margin: 0;
  text-align: center;
  padding: 0 8vw;
}

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