  /* =====================================================================
     Girlypop desktop shell. Palette and geometry come from the tweaker
     mockup (see docs/superpowers/specs/2026-09-03-girlypop-shell-design.md).
     These are the base rules, not an override layer - the previous pass
     stacked a second skin on top of an older one and let cascade order
     decide, which is why it read as neither.
     ===================================================================== */
  :root {
    /* light palette (`kh.light` in the tweaker bundle) */
    --cream: #FDFAF4;
    --card: #FFFDFA;
    --face: #F4EBF3;
    --rim: #5E4763;
    --ink-text: #3F3145;
    --hot: #FF7FA8;
    --pink: #F7C3D8;
    --blue: #BFD9F2;
    --butter: #F7E7A0;
    --chip: rgba(255,255,255,.62);
    --chip-text: #3F3145;
    --overlay: 255,255,255;
    --pop: #FF9DB8;
    --font-ui: "Archivo", system-ui, sans-serif;
    --font-pixel: "Silkscreen", "Courier New", monospace;

    /* wallpaper stops at creamLift 0% / pinkPunch 100% */
    --wall-start: #C6C1E7;
    --wall-mid: #D6C4D4;
    --wall-end: #FF9DB8;

    /* Default sticker shadow. script.js overrides this per app with a tint
       hashed from the slug, so each app keeps a stable identity now that
       every titlebar shares one gradient. */
    --sticker-shadow: #FF7FA8;

    --grain-opacity: .192;      /* film grain 48% */
    --grid-line: rgba(255,255,255,0.100);
    --pattern-fill: rgba(255,255,255,0.42);
    --vignette-tint: rgba(94,71,99,0.296);
    --blob-opacity: .42;
    --blob-a: #FF9DBB;
    --blob-b: #BFD9F2;
    --scanline: rgba(60,40,70,0.036);
    --blush: rgba(255,127,168,0.182);
    --grain-blend: overlay;
    --glitter-a: #5E4763;
    --glitter-b: #FF3D7C;
    --glitter-c: #2F6BA8;

    /* Pixel cursor, redrawn. The tweaker's path
       (M3 2h2v2h2v2h2v2h2v2h2v2h-4v2h-2v-2h-2v-2H3z) is a staircase that
       never closes into an arrowhead, and crispEdges fights its 1.2 stroke,
       so it rendered as a smear. This is a real arrow: left edge, tail
       notch, tail, then the diagonal back to the tip. Hotspot 3 2 = the tip.
       Kept under 32x32, which is the size ceiling browsers enforce here. */
    --cursor-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><path d='M3 2 L3 18 L6.5 14.5 L10 20.5 L12.5 19.5 L9 13.5 L14 13.5 Z' fill='%23FFFFFF' stroke='%235E4763' stroke-width='1.4' stroke-linejoin='round'/></svg>") 3 2, auto;
    --cursor-pointer: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><circle cx='12' cy='12' r='7' fill='%23FFD3E6' stroke='%23E274A6' stroke-width='1.8'/></svg>") 12 12, pointer;
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body {
    width: 100%; height: 100%;
    font-family: var(--font-ui);
    overflow: hidden;
  }

  /* Toggled on <body> for the duration of any window/widget drag or resize
     so fast pointer moves don't select surrounding desktop text/icons. */
  body.no-select, body.no-select * {
    user-select: none;
  }

  /* The gradient angle oscillates rather than sitting still. A registered
     custom property is the only way to interpolate an <angle> in a
     gradient; browsers without @property support get a static 160deg,
     which is the chosen value and a perfectly good fallback. */
  @property --grad-angle {
    syntax: '<angle>';
    inherits: false;
    initial-value: 160deg;
  }
  body {
    --grad-angle: 160deg;
    background: linear-gradient(
      var(--grad-angle),
      var(--wall-start) 0%,
      var(--wall-mid) 38%,
      var(--wall-end) 82%
    );
    background-attachment: fixed;
    position: relative;
    animation: gradSweep 90s ease-in-out infinite;
    cursor: var(--cursor-arrow);
  }
  /* 140deg -> 180deg -> 140deg. A full 360 spin would park the saturated
     pink end under the icon grid and label chips for half the cycle, which
     is exactly where label contrast is tightest. */
  @keyframes gradSweep {
    0%, 100% { --grad-angle: 140deg; }
    50%      { --grad-angle: 180deg; }
  }
  @media (prefers-reduced-motion: reduce) {
    body { animation: none; --grad-angle: 160deg; }
  }

  /* wallpaper grid: lines, 20% strength, 34px pitch */
  body::before {
    content: "";
    position: fixed; inset: 0;
    z-index: 0;
    background-image:
      repeating-linear-gradient(0deg,  var(--grid-line) 0 1px, transparent 1px 34px),
      repeating-linear-gradient(90deg, var(--grid-line) 0 1px, transparent 1px 34px);
    pointer-events: none;
  }

  button, .win-controls span, .taskbar-folder, .start-pie, .desktop-icon, .file-item {
    cursor: var(--cursor-pointer);
  }
  input, textarea { cursor: text; }

  /* cursor sparkle trail - four-point stars, per the tweaker. Size and
     rotation are set inline by script.js. */
  .sparkle {
    position: fixed;
    width: 12px; height: 12px;
    background: var(--hot);
    clip-path: polygon(50% 0,58% 42%,100% 50%,58% 58%,50% 100%,42% 58%,0 50%,42% 42%);
    pointer-events: none;
    z-index: 9999;
    animation: sparkleFade 700ms ease-out forwards;
  }
  @keyframes sparkleFade {
    0%   { transform: scale(1) rotate(var(--spin, 0deg)); opacity: 0.95; }
    100% { transform: scale(0.2) rotate(calc(var(--spin, 0deg) + 90deg)); opacity: 0; }
  }

  /* ---------- Desktop icons ----------
     Mockup geometry: 136 x 137px cells, 64px art on an 86px sticker tile.
     Cells are laid out by the .icon-section grid, not by absolute top/left
     coordinates - the old 90px pitch could not fit a 64px icon. */
  .desktop-icon {
    position: relative;
    width: 136px; height: 137px;
    display: flex; flex-direction: column;
    align-items: center; justify-content: flex-start;
    padding-top: 6px;
    gap: 9px;
    text-align: center;
    color: var(--ink-text);
    font-size: 13px;
    user-select: none;
    /* Idle wiggle. --wig-dur/--wig-delay are set per icon by script.js from
       the icon's index, never Math.random(), so the grid never pulses in
       unison and never jumps between renders. */
    animation: iconWiggle var(--wig-dur, 3.6s) ease-in-out var(--wig-delay, 0s) infinite;
  }
  @keyframes iconWiggle {
    0%, 100% { transform: rotate(-1.2deg); }
    50%      { transform: rotate(1.2deg); }
  }
  .desktop-icon:hover { animation-play-state: paused; }
  @media (prefers-reduced-motion: reduce) { .desktop-icon { animation: none; } }

  .desktop-icon.selected .glyph { outline: 2px dashed var(--rim); outline-offset: 3px; }
  .desktop-icon.bounce { animation: iconBounce 340ms ease; }
  @keyframes iconBounce {
    0% { transform: scale(1); }
    35% { transform: scale(0.86); }
    65% { transform: scale(1.08); }
    100% { transform: scale(1); }
  }

  /* blush glow, 33% - sits behind the tile, reads as backlight not selection */
  .desktop-icon::before {
    content: "";
    position: absolute; left: 50%; top: 10px;
    width: 94px; height: 94px;
    transform: translateX(-50%);
    border-radius: 50%; pointer-events: none; z-index: 0;
    background: radial-gradient(circle, var(--blush) 0%, transparent 70%);
    filter: blur(6px);
  }

  /* sticker tile: 86px face, 2px rim, die-cut cream ring, then the
     per-app tinted offset shadow. Ring first or it paints over the drop. */
  .desktop-icon .glyph {
    position: relative;
    z-index: 1;
    width: 86px; height: 86px; flex: 0 0 86px;
    display: flex; align-items: center; justify-content: center;
    background: var(--card);
    border: 2px solid var(--rim);
    border-radius: 22%;
    box-shadow:
      0 0 0 5px var(--cream),
      4px 4px 0 5px var(--sticker-shadow);
    transition: transform 160ms cubic-bezier(.34,1.56,.64,1), box-shadow 160ms ease;
  }
  /* saturate() pulls the chunky emoji-style icons back toward the thin
     pastel line-art ones so the set reads as one language */
  .desktop-icon .glyph img {
    width: 64px; height: 64px;
    object-fit: contain;
    filter: saturate(.72);
  }
  .desktop-icon:hover .glyph,
  .desktop-icon:focus-visible .glyph {
    transform: rotate(-10deg) translateY(-3px);
    box-shadow:
      0 0 0 5px var(--cream),
      6px 6px 0 5px var(--sticker-shadow);
  }
  .desktop-icon:nth-child(even):hover .glyph { transform: rotate(10deg) translateY(-3px); }

  /* peeling corner - on every tile, matching the mockup */
  .desktop-icon .glyph::after {
    content: "";
    position: absolute; right: -1px; bottom: -1px;
    width: 15px; height: 15px;
    background: linear-gradient(315deg, #EFE2EE 0 48%, var(--card) 52%);
    border-left: 1px solid var(--rim);
    border-top: 1px solid var(--rim);
    clip-path: polygon(100% 0, 100% 100%, 0 100%);
    transition: width .18s ease, height .18s ease;
  }
  .desktop-icon:hover .glyph::after { width: 22px; height: 22px; }

  /* label chip. max-width + two reserved lines fixes the wrap bug where
     "Thought For Food" broke to two lines and shunted the whole grid. */
  /* Single line, hugging the text, exactly as the mockup renders it. The
     earlier two-line reservation existed only because Tahoma's wider metrics
     wrapped "Thought For Food"; in Archivo nothing wraps, so the chip can go
     back to inline-block and the tile keeps its square. */
  .desktop-icon .icon-label {
    display: inline-block;
    max-width: 124px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 3px 8px;
    border-radius: 8px;
    background: var(--chip);
    backdrop-filter: blur(2px);
    color: var(--chip-text);
    font-size: 13px;
    font-weight: 500;
    line-height: 1.25;
    text-shadow: none;
  }

  /* Unread badges (tweak #14). Built to spec but OFF: a badge is a claim
     that something needs attention, and a decorative one trains you to
     ignore all of them. Turn on by rendering .icon-badge with a real count
     (TFF drafts / unread blog entries / music queue) and an aria-label. */
  .icon-badge {
    position: absolute; top: 2px; right: 26px;
    min-width: 20px; height: 20px; padding: 0 5px;
    display: none;
    place-items: center;
    background: var(--hot); color: #fff;
    font-size: 12px; font-weight: 700;
    border: 1.5px solid var(--rim); border-radius: 999px;
    box-shadow: 1.5px 1.5px 0 var(--rim);
    z-index: 2;
  }

  .icon-monitor { top: 18px; left: 18px; }
  .icon-note { top: 108px; left: 26px; }
  .icon-photo { top: 190px; left: 26px; }
  .icon-uni { top: 262px; left: 26px; }

  .icon-stuff { top: 18px; right: 130px; }
  .icon-documents { top: 18px; right: 26px; }
  .icon-photos2 { top: 96px; right: 130px; }
  .icon-memories { top: 96px; right: 26px; }
  .icon-folder2 { top: 170px; right: 26px; }

  /* little pixel-ish icon glyphs made from CSS shapes */
  .glyph-monitor {
    width: 40px; height: 32px;
    background: linear-gradient(#e7ecf3, #c9d3e2);
    border: 2px solid #7d8aa3;
    border-radius: 2px;
    position: relative;
  }
  .glyph-monitor::after {
    content: "";
    position: absolute; inset: 5px 5px 10px 5px;
    background: #ffd6e6;
    border: 1px solid #d98cae;
  }

  .glyph-doc {
    width: 26px; height: 32px;
    background: #fff;
    border: 1.5px solid #b9bfd0;
    position: relative;
    box-shadow: 2px 2px 0 rgba(0,0,0,0.08);
  }
  .glyph-doc::before, .glyph-doc::after {
    content: "";
    position: absolute; left: 4px; right: 4px;
    height: 2px; background: #cfd4e2;
  }
  .glyph-doc::before { top: 8px; }
  .glyph-doc::after { top: 14px; }

  .glyph-photo {
    width: 34px; height: 30px;
    background: linear-gradient(135deg, #ffc9de, #ff9fc4);
    border: 2px solid #e881ab;
    border-radius: 2px;
  }

  .glyph-file-blue {
    width: 34px; height: 30px;
    background: linear-gradient(135deg, #a9c6ff, #6f97ee);
    border: 2px solid #5578cf;
    border-radius: 2px;
  }

  .folder-icon {
    width: 42px; height: 32px;
    position: relative;
  }
  .folder-icon .tab {
    position: absolute; top: 0; left: 2px;
    width: 16px; height: 6px;
    background: var(--folder-c1, #f6cf8e);
    border-radius: 3px 3px 0 0;
    border: 1px solid var(--folder-border, #d9a85c);
    border-bottom: none;
  }
  .folder-icon .body {
    position: absolute; top: 5px; left: 0; right: 0; bottom: 0;
    background: linear-gradient(180deg, var(--folder-c1, #f6cf8e), var(--folder-c2, #eeb968));
    border: 1px solid var(--folder-border, #d9a85c);
    border-radius: 2px;
  }
  .folder-icon.pink { --folder-c1:#ffd3e6; --folder-c2:#ffb3d3; --folder-border:#e284ac; }
  .folder-icon.blue { --folder-c1:#cfe0ff; --folder-c2:#a9c4fb; --folder-border:#6f8fd8; }
  .folder-icon.peach { --folder-c1:#ffe2c2; --folder-c2:#f6c48c; --folder-border:#d9a85c; }

  /* ---------- Windows (generic chrome) ---------- */
  /* Pastel sticker card. The offset shadow uses --sticker-shadow, which
     script.js sets per window from the app's slug hash - that is where each
     app's identity now lives, since every titlebar shares one gradient. */
  .win {
    position: absolute;
    background: var(--cream);
    color: var(--ink-text);
    border: 1.5px solid var(--rim);
    border-radius: 16px;
    box-shadow: 4px 4px 0 var(--sticker-shadow);
    overflow: hidden;
    transform-origin: top left;
    animation: winOpen 220ms cubic-bezier(.2,1.4,.4,1) both;
  }
  .win-resize-handle {
    position: absolute;
    right: 0; bottom: 0;
    width: 16px; height: 16px;
    cursor: nwse-resize;
    z-index: 5;
    touch-action: none;
  }
  .win-resize-handle::after {
    content: "";
    position: absolute;
    right: 3px; bottom: 3px;
    width: 8px; height: 8px;
    background:
      linear-gradient(135deg, transparent 0 50%, rgba(90,74,106,0.4) 50% 60%, transparent 60% 100%),
      linear-gradient(135deg, transparent 0 70%, rgba(90,74,106,0.4) 70% 80%, transparent 80% 100%);
  }
  .win.closing {
    animation: winClose 160ms ease-in forwards;
  }
  @keyframes winOpen {
    0% { opacity: 0; transform: scale(0.85) translateY(10px); }
    100% { opacity: 1; transform: scale(1) translateY(0); }
  }
  @keyframes winClose {
    0% { opacity: 1; transform: scale(1); }
    100% { opacity: 0; transform: scale(0.85) translateY(8px); }
  }
  .win.dragging { box-shadow: 7px 7px 0 var(--sticker-shadow); }

  /* One pastel bar for every window (tweak #30). The old per-title hashed
     hue is gone; per-app identity moved to the shadow tint instead. */
  .win-titlebar,
  .titlebar-blue, .titlebar-pink, .titlebar-navy, .titlebar-peach {
    background: linear-gradient(90deg, var(--hot), var(--pink) 60%, var(--blue));
  }
  .win-titlebar {
    height: 38px;
    display: flex; align-items: center; justify-content: space-between;
    gap: 8px;
    padding: 7px 9px;
    color: var(--ink-text);
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.2px;
    user-select: none;
    touch-action: none;
    border-bottom: 1.5px solid var(--rim);
  }

  /* Chunky squares with a real press state, not thin glyphs. */
  .win-controls { display: flex; gap: 5px; flex: 0 0 auto; }
  /* app icon ahead of the title, as the mockup has it */
  .win-titlebar .win-appicon { width: 18px; height: 18px; flex: 0 0 auto; display: block; }
  .win-titlebar .win-title-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
  .win-titlebar span.glitter { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .win-controls span {
    width: 22px; height: 22px;
    background: var(--card);
    border: 1.5px solid var(--rim);
    border-radius: 7px;
    box-shadow: 1.5px 1.5px 0 var(--rim);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 12px; color: var(--ink-text);
    line-height: 1;
    cursor: var(--cursor-pointer);
    transition: transform 100ms ease, background 100ms ease, box-shadow 100ms ease;
  }
  .win-controls span:hover { background: var(--pink); }
  .win-controls span:active {
    transform: translate(1.5px, 1.5px);
    box-shadow: 0 0 0 var(--rim);
  }
  /* max/restore is one button in two states - updateMaxIcon() in script.js
     swaps the glyph and win.dataset.maxed holds the pre-maximise rect. */

  /* ---------- Explorer window ---------- */
  .win-explorer { width: 400px; height: 232px; }
  .explorer-toolbar {
    display: flex; align-items: center; justify-content: space-between;
    padding: 6px 8px;
    background: var(--face);
    color: var(--ink-text);
  }
  .explorer-search {
    background: var(--card);
    border: 1.5px solid var(--rim);
    border-radius: 999px;
    font-size: 11px;
    color: var(--ink-text);
    padding: 3px 8px;
    width: 110px;
    box-sizing: border-box;
    transition: box-shadow 150ms ease, border-color 150ms ease;
  }
  .explorer-search input {
    border: none; outline: none; font: inherit; color: var(--ink-text); width: 100%;
    background: transparent;
  }
  .explorer-search:focus-within, .explorer-search.focused {
    box-shadow: 0 0 0 3px var(--blue);
  }
  .explorer-body {
    display: flex;
    flex-wrap: wrap;
    padding: 8px 14px 0 14px;
    gap: 18px;
    position: relative;
  }
  .file-item {
    width: 62px;
    text-align: center;
    font-size: 10.5px;
    color: var(--ink-text);
    border-radius: 4px;
    padding: 3px 2px;
    transition: background 120ms ease, transform 120ms ease;
  }
  .file-item:hover { transform: translateY(-1px); }
  .file-item.selected { background: rgba(255,127,168,0.28); }
  .file-item .icon-wrap {
    height: 40px;
    display: flex; align-items: flex-end; justify-content: center;
    margin-bottom: 3px;
  }
  .swatch {
    width: 34px; height: 26px;
    border-radius: 2px;
  }
  .swatch.pink { background: linear-gradient(135deg,#ffb6d9,#ff8dbf); border: 1px solid #e274a6; }
  .swatch.blue { background: linear-gradient(135deg,#9db8ff,#6f8fef); border: 1px solid #5a75d1; }
  .swatch.orange { background: linear-gradient(135deg,#ffcf94,#f7ac5a); border: 1px solid #d9903c; }

  /* ---------- search results window ---------- */
  .search-results-body {
    padding: 8px;
    overflow-y: auto;
    height: calc(100% - 60px);
  }
  .search-type-filters {
    display: flex;
    gap: 6px;
    padding: 6px 8px;
    border-bottom: 1.5px solid var(--rim);
  }
  .search-type-filters span {
    font-size: 10.5px;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--face);
    color: var(--ink-text);
    cursor: pointer;
    user-select: none;
  }
  .search-type-filters span.active {
    background: var(--hot);
    color: #fff;
  }
  .search-result-row {
    padding: 6px 8px;
    border-radius: 4px;
    cursor: var(--cursor-pointer);
    font-size: 11.5px;
    color: var(--ink-text);
  }
  .search-result-row:hover {
    background: rgba(255,127,168,0.20);
  }
  .search-result-row .sr-title {
    font-weight: 600;
  }
  .search-result-row .sr-snippet {
    color: var(--ink-text); opacity: .7;
    font-size: 10.5px;
  }
  .search-empty {
    padding: 16px;
    text-align: center;
    color: #999;
    font-size: 11px;
  }

  /* ---------- AHF90 cassette player ---------------------------------
     Ported from a standalone reference component (colors, layout, type
     and motion values kept verbatim); classes are prefixed ahf90- since
     the source names (.page, .slip, .stage, ...) are too generic for a
     shared site stylesheet. */
  .ahf90-shell {
    position: fixed;
    right: 24px;
    /* Anchored well above the viewport edge (not just 24px) because the
       track-list dropdown (.ahf90-slip) extends ~78px below the shell's
       own scaled bottom edge when open - anything less and it gets cut
       off by the viewport at typical window heights. */
    bottom: 130px;
    z-index: 1000;
    width: 368px;
    height: 276px;
    transform: scale(1.5);
    transform-origin: bottom right;
    animation: modal-pop-in 0.18s ease;
  }
  .ahf90-shell.is-dragging { animation: none; }
  .ahf90-dragbar {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    height: 55px;
    cursor: grab;
    z-index: 1;
  }
  .ahf90-shell.is-dragging .ahf90-dragbar { cursor: grabbing; }
  .ahf90-close {
    position: absolute;
    right: 48px;
    top: 37px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: none;
    background: #efeade;
    color: #2b2850;
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    z-index: 2;
    box-shadow: 0 4px 10px rgba(9, 1, 30, 0.4);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
  }
  .ahf90-shell:hover .ahf90-close { opacity: 1; pointer-events: auto; }
  .ahf90-close:hover { background: #fff; }

  /* ---------- Contact card (RAZR flip-phone) --------------------------
     Spawned by the Contact desktop icon (data-launch="contact") as a
     floating widget with the same treatment as the AHF90 cassette
     player above - draggable, no backdrop, hover-reveal close - not a
     titled .win or a modal.

     Sits in the real open gap below the cassette player and to the
     right of the polaroid card (left:598-812), before the shamaz/blog/
     mascot column starts (left:1125). left/top are raw (pre-zoom)
     values - .contact-shell is in the same zoom tiers as the rest of
     the widget cluster below, so it scales and repositions in lockstep
     with that column instead of drifting out of the gap on wide
     screens. The scene itself lives in an iframe (apps/contact/) with
     its own isolated CSS and a transparent background, since its class
     names (.stage, .veil, .tray, ...) are far too generic to inline
     into this shared stylesheet. */
  .contact-shell {
    position: fixed;
    left: 850px;
    top: 460px;
    z-index: 1000;
    width: 260px;
    height: min(500px, calc(100vh - 560px));
    animation: contact-pop-in 0.18s ease;
  }
  /* The height cap above assumes no zoom; re-derive it for the zoom
     tiers below so the rendered (post-zoom) bottom edge still clears
     the taskbar on shorter monitors instead of running under it. */
  @media (min-width: 1700px) {
    .contact-shell { height: min(500px, calc((100vh - 630px) / 1.2)); }
  }
  @media (min-width: 2200px) {
    .contact-shell { height: min(500px, calc((100vh - 760px) / 1.45)); }
  }
  /* Below 1500px the shamaz/blog/mascot column shifts left to close the
     gap this widget sits in (see the media query these widgets share
     further down), so there's no room left there - fall back to a
     plain corner anchor instead of overlapping it. */
  @media (max-width: 1499px) {
    .contact-shell {
      left: auto; top: auto;
      right: 24px; bottom: 110px;
      height: min(480px, calc(100vh - 130px));
    }
  }
  .contact-shell.dragged { animation: none; }
  .contact-dragbar {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    height: 40px;
    cursor: grab;
    z-index: 1;
    touch-action: none;
  }
  .contact-shell.dragged .contact-dragbar { cursor: grabbing; }
  .contact-shell iframe { width: 100%; height: 100%; border: 0; display: block; background: transparent; }
  .contact-close {
    position: absolute;
    right: 8px;
    top: 8px;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    border: 1.5px solid var(--rim);
    background: var(--card);
    color: var(--ink-text);
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    z-index: 2;
    box-shadow: 0 4px 10px rgba(9, 1, 30, 0.4);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
  }
  .contact-shell:hover .contact-close { opacity: 1; pointer-events: auto; }
  .contact-close:hover { background: #fff; }
  @keyframes contact-pop-in { from { opacity: 0; transform: scale(.94); } to { opacity: 1; transform: scale(1); } }

  /* ---------- Gallery ----------------------------------
     Deliberately NOT in the zoom-tier selectors further down this file.
     Those key on viewport WIDTH only, so a portrait monitor (1080x1920)
     lands in the max-width:1499px bucket and shrinks, despite having
     enormous vertical space -- which is exactly the complaint about the
     cassette player. This widget is a fixed intrinsic size on every
     screen, and the app inside sizes itself off the iframe, so framing
     is identical everywhere. */
  .gallery-shell {
    position: fixed;
    left: 120px;
    top: 120px;
    z-index: 1000;
    width: 1100px;
    height: 620px;
    resize: both;
    overflow: hidden;
    min-width: 780px;   /* below 760 the app switches to its phone layout */
    min-height: 420px;
  }
  .gallery-shell iframe {
    width: 100%; height: 100%;
    border: 0; background: transparent;
  }
  .gallery-shell--expanded {
    left: 2vw !important; top: 2vh !important;
    width: 96vw !important; height: 96vh !important;
    transition: all .32s cubic-bezier(.16,.84,.24,1);
  }
  .gallery-dragbar {
    position: absolute;
    left: 0; top: 0; right: 0;
    height: 40px;
    cursor: grab;
    z-index: 1;
    touch-action: none;
  }
  .gallery-shell.dragged .gallery-dragbar { cursor: grabbing; }
  .gallery-close {
    position: absolute;
    right: 8px; top: 8px;
    width: 26px; height: 26px;
    border-radius: 50%;
    border: 1.5px solid var(--rim);
    background: var(--card);
    color: var(--ink-text);
    font-size: 13px; line-height: 1;
    cursor: pointer;
    z-index: 2;
    box-shadow: 0 4px 10px rgba(9, 1, 30, 0.4);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
  }
  .gallery-shell:hover .gallery-close { opacity: 1; pointer-events: auto; }
  .gallery-close:hover { background: #fff; }

  .ahf90-stage {
    position: absolute;
    left: 0;
    top: 0;
    width: 736px;
    height: 552px;
    transform: scale(0.5);
    transform-origin: top left;
  }
  .ahf90-svg { position: absolute; left: 0; top: 0; width: 736px; height: 552px; display: block; overflow: visible; }

  @keyframes ahf90ShellFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4.5px); } }
  .ahf90-shellFloat { animation: ahf90ShellFloat 7s ease-in-out infinite; }
  @keyframes ahf90GroundBreathe { 0%, 100% { transform: scale(1); opacity: 0.95; } 50% { transform: scale(0.955) translateY(1px); opacity: 0.8; } }
  .ahf90-groundShadow { transform-box: fill-box; transform-origin: center; animation: ahf90GroundBreathe 7s ease-in-out infinite; }

  @keyframes ahf90ReelSpin { to { transform: rotate(360deg); } }
  @keyframes ahf90ReelFwd { to { transform: rotate(1080deg); } }
  @keyframes ahf90ReelBack { to { transform: rotate(-1080deg); } }
  .ahf90-reelSpin { animation: ahf90ReelSpin 2.6s linear infinite; }
  .ahf90-reelSpin.is-paused { animation-play-state: paused; }
  .ahf90-reelScrubFwd { animation: ahf90ReelFwd 1s cubic-bezier(.16,.72,.24,1); }
  .ahf90-reelScrubBack { animation: ahf90ReelBack 1s cubic-bezier(.16,.72,.24,1); }

  @keyframes ahf90TapeSweep { 0% { transform: translate(310px); } 100% { transform: translate(500px); } }
  .ahf90-tapeSweep { animation: ahf90TapeSweep 3.6s ease-in-out infinite; }

  @keyframes ahf90InkIn { 0% { opacity: 0; transform: translateY(2.5px); } 100% { opacity: 1; transform: translateY(0); } }
  .ahf90-lblTitle, .ahf90-lblArtist { font-family: Gelasio, Georgia, serif; animation: ahf90InkIn 0.42s ease-out both; transform-box: fill-box; }
  @keyframes ahf90Marquee { 0%, 12% { transform: translateX(0); } 50%, 62% { transform: translateX(var(--marquee-dist, 0px)); } 100% { transform: translateX(0); } }
  .ahf90-lblTitle.is-marquee, .ahf90-lblArtist.is-marquee { animation: ahf90InkIn 0.42s ease-out both, ahf90Marquee 7s ease-in-out 0.6s infinite; }
  .ahf90-lblTitle { font-size: 18.6px; fill: #302c4e; letter-spacing: 0.1px; }
  .ahf90-lblArtist { font-size: 14.7px; fill: #2b1b50; letter-spacing: 0.1px; }
  .ahf90-stamp { font-family: "Archivo Narrow", sans-serif; font-weight: 700; font-size: 14px; letter-spacing: 0.6px; fill: #cac8e2; }

  /* Volume control, overlaid on the dusty strip below the tape window
     (panelClip rect: x152 y292 w437 h30 in the SVG's own coordinate
     space, which .ahf90-stage's children share since the stage itself is
     736x552 pre-scale). Collapsed to just the icon at rest, the track
     expands rightwards on hover/focus. */
  .ahf90-volume {
    position: absolute; left: 160px; top: 293px; width: 423px; height: 16px;
    display: flex; align-items: center; z-index: 2;
    padding: 0 6px; margin: -1px 0 0 -6px; border-radius: 8px; box-sizing: content-box;
    background: rgba(20, 14, 42, 0); transition: background-color 0.3s ease;
  }
  .ahf90-volume.is-open { background: rgba(20, 14, 42, 0.5); }
  .ahf90-volIcon { flex: 0 0 auto; color: #d9d6ee; opacity: 0.8; }
  .ahf90-volTrack { max-width: 0; overflow: hidden; margin-left: 6px; transition: max-width 0.28s ease; }
  .ahf90-volume.is-open .ahf90-volTrack { max-width: 390px; }
  .ahf90-volRange {
    -webkit-appearance: none; appearance: none;
    width: 390px; height: 14px; background: transparent; cursor: pointer; vertical-align: middle;
  }
  .ahf90-volRange::-webkit-slider-runnable-track { height: 3px; border-radius: 2px; background: rgba(217, 214, 238, 0.4); }
  .ahf90-volRange::-moz-range-track { height: 3px; border-radius: 2px; background: rgba(217, 214, 238, 0.4); }
  .ahf90-volRange::-webkit-slider-thumb {
    -webkit-appearance: none; margin-top: -4.5px;
    width: 12px; height: 12px; border-radius: 50%;
    background: #d9d6ee; border: 1px solid #4a4370;
  }
  .ahf90-volRange::-moz-range-thumb {
    width: 12px; height: 12px; border-radius: 50%;
    background: #d9d6ee; border: 1px solid #4a4370;
  }

  .ahf90-sideSlot { position: absolute; left: 158px; top: 134px; width: 42px; height: 56px; }
  .ahf90-sideBtn {
    position: absolute; left: -6px; top: -2px; width: 48px; height: 58px;
    padding: 0; border: 0; background: transparent; cursor: pointer;
    border-radius: 3px; transition: background 0.25s; perspective: 320px;
  }
  .ahf90-sideBtn:hover { background: rgba(72, 62, 130, 0.13); }
  .ahf90-sideLetter {
    position: absolute; left: 13px; top: 7px; width: 40px;
    font-family: "Bodoni Moda", Didot, serif; font-weight: 500; font-size: 39.5px; line-height: 1;
    color: #2e3152; text-align: left; transform-origin: 50% 62%;
    animation: ahf90SideFlip 0.5s cubic-bezier(.34,1.4,.42,1) both;
  }
  @keyframes ahf90SideFlip { 0% { transform: scaleX(1.42) rotateX(-88deg); opacity: 0.15; } 100% { transform: scaleX(1.42) rotateX(0); opacity: 1; } }
  .ahf90-sideWord { position: absolute; left: 11px; top: 37px; font-size: 14px; font-weight: 500; letter-spacing: 1.4px; color: #34375a; }
  .ahf90-sideBtn::after {
    content: ""; position: absolute; left: 4px; right: 8px; bottom: -1px; height: 1.6px;
    background: currentColor; color: #6a5fa8; transform: scaleX(0); transform-origin: left;
    transition: transform 0.3s cubic-bezier(.2,.8,.2,1);
  }
  .ahf90-sideBtn:hover::after { transform: scaleX(1); }

  .ahf90-tipWrap { position: absolute; left: 6px; bottom: 66px; pointer-events: none; }
  .ahf90-tipCard {
    position: absolute; left: 0; bottom: 36px; white-space: nowrap; padding: 8px 13px 7px;
    background: linear-gradient(180deg, #efeade, #e2dbcb); border-bottom: 2.5px solid #3b4180;
    border-radius: 2px 2px 3px 3px; box-shadow: 0 10px 26px rgba(9,1,30,0.55);
    opacity: 0; transform: translateY(7px) rotate(-0.7deg);
    transition: opacity 0.24s, transform 0.3s cubic-bezier(.2,.9,.2,1);
  }
  .ahf90-tipMode { display: block; font-size: 17px; font-weight: 600; letter-spacing: 1.6px; text-transform: uppercase; color: #1d1636; }
  .ahf90-tipHint { display: block; margin-top: 2px; font-size: 12.5px; letter-spacing: 0.7px; text-transform: uppercase; color: #6a6350; }
  .ahf90-tipLeader { position: absolute; left: 11px; bottom: 0; width: 1px; height: 36px; background: linear-gradient(180deg, #b9aee6, rgba(185,174,230,0.15)); transform: scaleY(0); transform-origin: bottom; transition: transform 0.32s cubic-bezier(.2,.9,.2,1); }
  .ahf90-tipDot { position: absolute; left: 9px; bottom: -2px; width: 5px; height: 5px; border-radius: 50%; background: #cfc4f5; opacity: 0; transition: opacity 0.2s 0.1s; }
  .ahf90-sideSlot:hover .ahf90-tipCard { opacity: 1; transform: translateY(0) rotate(-0.7deg); }
  .ahf90-sideSlot:hover .ahf90-tipLeader { transform: scaleY(1); }
  .ahf90-sideSlot:hover .ahf90-tipDot { opacity: 1; }

  .ahf90-hubBtn {
    position: absolute; width: 38px; height: 38px; margin: -19px 0 0 -19px;
    border: 0; border-radius: 50%; background: transparent; color: #c9bdf2;
    display: grid; place-items: center; cursor: pointer; opacity: 0.3;
    transition: opacity 0.22s, color 0.22s, transform 0.22s;
  }
  .ahf90-hubL { left: 269.5px; top: 243.2px; }
  .ahf90-hubR { left: 475.5px; top: 243.2px; }
  .ahf90-hubBtn:hover { opacity: 1; color: #fff; transform: scale(1.06); }
  .ahf90-hubBtn:active { transform: scale(0.9); }
  .ahf90-hubBtn svg { filter: drop-shadow(0 0 5px rgba(150,120,250,0.4)); }

  .ahf90-winBtn {
    position: absolute; left: 314px; top: 208px; width: 112px; height: 70px;
    border: 0; border-radius: 3px; background: transparent; color: #e9e3ff;
    display: grid; place-items: center; cursor: pointer; opacity: 0.5; transition: opacity 0.22s;
  }
  .ahf90-winBtn:hover { opacity: 1; }
  .ahf90-winBtn svg { filter: drop-shadow(0 0 9px rgba(150,160,255,0.5)); transition: transform 0.2s cubic-bezier(.2,1.5,.4,1); }
  .ahf90-winBtn:active svg { transform: scale(0.86); }
  .ahf90-winRun { position: absolute; bottom: 9px; left: 50%; transform: translate(-50%); display: flex; gap: 3px; align-items: flex-end; height: 11px; opacity: 0; transition: opacity 0.3s; }
  .ahf90-winBtn.is-playing .ahf90-winRun { opacity: 0.75; }
  .ahf90-winRun i { display: block; width: 2.5px; height: 4px; background: #a9b4ef; border-radius: 1px; animation: ahf90Vu 0.9s ease-in-out infinite; }
  .ahf90-winRun i:nth-child(2) { animation-duration: 1.25s; }
  .ahf90-winRun i:nth-child(3) { animation-duration: 0.72s; }
  .ahf90-winRun i:nth-child(4) { animation-duration: 1.05s; }
  .ahf90-winRun i:nth-child(5) { animation-duration: 0.86s; }
  @keyframes ahf90Vu { 0%, 100% { height: 3px; opacity: 0.55; } 50% { height: 11px; opacity: 1; } }

  .ahf90-browseBtn {
    position: absolute; left: 368px; top: 381px; width: 30px; height: 26px; margin: -13px 0 0 -15px;
    border: 0; background: transparent; color: #4b4874; display: grid; place-items: center;
    cursor: pointer; border-radius: 6px; transition: color 0.2s, transform 0.28s cubic-bezier(.2,.9,.2,1), background 0.2s;
  }
  .ahf90-browseBtn:hover { color: #2b2850; background: rgba(255,255,255,0.3); }
  .ahf90-browseBtn.is-open { transform: rotate(180deg); color: #2b2850; }
  .ahf90-browseBtn svg { filter: drop-shadow(0 1px 0 rgba(255,255,255,0.5)); }

  .ahf90-slip {
    position: absolute; left: 186px; top: 396px; width: 364px; padding: 10px 0 8px;
    background: linear-gradient(180deg, #efeade, #e6e0d0); border-radius: 2px 2px 4px 4px;
    box-shadow: 0 26px 48px rgba(7,1,24,0.62), inset 0 1px rgba(255,255,255,0.65);
    transform-origin: 50% 0; transform: scaleY(0.02) translateY(-4px); opacity: 0; pointer-events: none;
    transition: transform 0.42s cubic-bezier(.2,.9,.2,1), opacity 0.2s;
    max-height: 260px; overflow-y: auto;
  }
  .ahf90-slip.is-open { transform: scaleY(1) translateY(0) rotate(-0.35deg); opacity: 1; pointer-events: auto; }
  .ahf90-slipTop { display: flex; align-items: baseline; gap: 9px; padding: 0 16px 7px; border-bottom: 1.5px solid #c9c2ae; }
  .ahf90-slipSide { font-size: 14px; font-weight: 700; letter-spacing: 1.4px; }
  .ahf90-slipMode { font-size: 13px; letter-spacing: 1.5px; text-transform: uppercase; color: #5d5644; }
  .ahf90-slipCount { margin-left: auto; font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: #8d8674; }
  .ahf90-slipSearchWrap { padding: 7px 16px 5px; border-bottom: 1px solid #d8d2bf; }
  .ahf90-slipSearch {
    width: 100%; box-sizing: border-box; padding: 5px 9px; border: 1px solid #c9c2ae; border-radius: 3px;
    background: rgba(255,255,255,0.55); font-family: Gelasio, Georgia, serif; font-size: 13.5px; color: #241d3f;
    outline: none; transition: border-color 0.16s, background 0.16s;
  }
  .ahf90-slipSearch::placeholder { color: #9a9280; font-style: italic; }
  .ahf90-slipSearch:focus { border-color: #6f6752; background: rgba(255,255,255,0.85); }
  .ahf90-slipList { list-style: none; margin: 0; padding: 3px 0 0; }
  .ahf90-slipList li { opacity: 0; transform: translateY(5px); transition: opacity 0.3s, transform 0.3s; }
  .ahf90-slip.is-open .ahf90-slipList li { opacity: 1; transform: none; }
  .ahf90-slipList button {
    display: grid; grid-template-columns: 24px 1fr auto 38px; align-items: baseline; gap: 10px;
    width: 100%; padding: 4px 16px; border: 0; border-left: 2.5px solid transparent;
    background: transparent; text-align: left; cursor: pointer;
    transition: background 0.16s, border-color 0.16s; font: inherit;
  }
  .ahf90-slipList button:hover { background: rgba(43,34,74,0.07); }
  .ahf90-slipList li.on button { border-left-color: #2b2250; background: rgba(43,34,74,0.05); }
  .ahf90-slipList .n { font-size: 12px; letter-spacing: 0.5px; color: #9a9280; font-variant-numeric: tabular-nums; }
  .ahf90-slipList .tt { font-family: Gelasio, Georgia, serif; font-size: 15.5px; color: #241d3f; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .ahf90-slipList li.on .tt { font-weight: 500; }
  .ahf90-slipList .aa { font-family: Gelasio, Georgia, serif; font-style: italic; font-size: 13.5px; color: #6f6752; white-space: nowrap; }
  .ahf90-slipList .dd { font-size: 12.5px; color: #9a9280; text-align: right; font-variant-numeric: tabular-nums; }

  .ahf90-empty { padding: 16px; font-size: 15px; color: #8d8674; font-family: Gelasio, Georgia, serif; }

  @media (prefers-reduced-motion: reduce) {
    .ahf90-shellFloat, .ahf90-tapeSweep, .ahf90-winRun i { animation: none; }
    .ahf90-lblTitle.is-marquee, .ahf90-lblArtist.is-marquee { animation: ahf90InkIn 0.42s ease-out both; }
    .contact-shell { animation: none; }
  }

  @media (max-width: 640px) {
    .ahf90-shell { right: 12px; bottom: 56px; transform: scale(0.82); transform-origin: bottom right; }
    .contact-shell { right: 12px; bottom: 56px; width: 220px; height: 420px; }
  }

  .win-scrollbar {
    position: absolute; right: 4px; top: 30px; bottom: 6px;
    width: 14px;
    background: var(--face);
    border: 1.5px solid var(--rim);
    border-radius: 999px;
  }
  .win-scrollbar .thumb {
    position: absolute; top: 8px; left: 1px; right: 1px; height: 60px;
    background: var(--hot);
    border: 1px solid var(--rim);
    border-radius: 999px;
  }

  /* ---------- Blank overlay ---------- */
  .blank-overlay {
    position: fixed;
    inset: 0;
    background: #000;
    z-index: 99999;
  }
  .blank-overlay .blank-exit {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: rgba(255,255,255,0.12);
    color: #fff;
    font-size: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    opacity: 0;
    transition: opacity 200ms ease;
    user-select: none;
  }
  .blank-overlay.show-exit .blank-exit {
    opacity: 1;
  }

  /* ---------- shut down fallback ---------- */
  .shutdown-fallback {
    position: fixed;
    inset: 0;
    background: #000;
    color: #ccc;
    font-family: inherit;
    font-size: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 999999;
  }

  .empty-note {
    padding: 24px 10px; text-align: center; font-size: 11px; color: var(--ink-text); opacity: .65;
  }

  /* loading popup inside explorer */
  .loading-popup {
    position: absolute;
    left: 90px; top: 118px;
    width: 240px;
    background: var(--cream);
    color: var(--ink-text);
    border: 1.5px solid var(--rim);
    border-radius: 12px;
    box-shadow: 3px 3px 0 var(--sticker-shadow);
    z-index: 5;
    transition: opacity 300ms ease;
  }
  .loading-popup .win-titlebar { height: 18px; font-size: 10px; }
  .loading-body {
    padding: 10px 12px;
    font-size: 10.5px;
    color: var(--ink-text);
  }
  .loading-bar-track {
    margin-top: 8px;
    height: 12px;
    background: #fff;
    border: 1px solid #b6b6c0;
    border-radius: 2px;
    overflow: hidden;
    display: flex;
  }
  .loading-bar-fill {
    width: 30%;
    background: repeating-linear-gradient(90deg, #6f9ce8 0 8px, #86ade8 8px 16px);
    animation: loadFill 2.4s ease-in-out forwards, stripeMove 600ms linear infinite;
  }
  @keyframes loadFill { 0% { width: 8%; } 100% { width: 92%; } }
  @keyframes stripeMove { 0% { background-position: 0 0; } 100% { background-position: 16px 0; } }

  /* ---------- Note window ---------- */
  .win-note { width: 220px; height: 176px; }
  .note-body {
    padding: 10px;
    font-size: 12px;
    color: var(--ink-text);
    position: relative;
    height: calc(100% - 24px);
  }
  .note-scrollbar {
    position: absolute; right: 3px; top: 6px; bottom: 6px; width: 12px;
    background: #eee; border: 1px solid #ccc; border-radius: 2px;
  }
  .note-scrollbar .thumb {
    position: absolute; top: 4px; left: 1px; right: 1px; height: 30px;
    background: #c9c9d2; border-radius: 2px;
  }

  /* ---------- Reminder window ---------- */
  .win-reminder { width: 180px; }
  .reminder-body {
    padding: 14px 10px 10px;
    text-align: center;
    font-size: 11.5px;
    font-weight: 600;
    color: #2f2f3a;
  }
  .reminder-buttons {
    display: flex; justify-content: center; gap: 10px;
    margin-top: 12px;
  }
  .reminder-buttons button {
    font-family: inherit;
    font-size: 11px;
    padding: 3px 14px;
    background: linear-gradient(180deg,#ffffff,#dfe3ec);
    border: 1px solid #9aa0b0;
    border-radius: 3px;
    color: #333;
    transition: transform 100ms ease, background 100ms ease;
  }
  .reminder-buttons button:hover { background: linear-gradient(180deg,#fff,#ffe6f0); }
  .reminder-buttons button:active { transform: scale(0.94); }

  /* ---------- Taskbar / floating dock ----------
     Frosted at 100%, detached 14px from all three edges. The wallpaper now
     runs behind it, so text is plum ink rather than white. */
  .taskbar {
    position: fixed;
    left: 14px; right: 14px; bottom: 14px;
    height: 48px;
    background: rgba(255,255,255,0.60);
    backdrop-filter: blur(20px) saturate(1.25);
    display: flex; align-items: center;
    padding: 0 20px;
    gap: 12px;
    border: 1.5px solid var(--rim);
    border-radius: 999px;
    box-shadow: 4px 4px 0 var(--hot);
    color: var(--ink-text);
    z-index: 10000;
  }

  /* start button: keeps the duck, adds a flower and a word, in a pill */
  .start-pie {
    display: inline-flex; align-items: center; gap: 7px;
    width: auto; height: auto;
    padding: 5px 14px 5px 7px;
    background: linear-gradient(var(--card), var(--face));
    border: 1.5px solid var(--rim);
    border-radius: 999px;
    box-shadow: 2px 2px 0 var(--hot);
    font-weight: 700;
    color: var(--ink-text);
    flex: 0 0 auto;
    transition: transform 120ms ease, box-shadow 120ms ease;
  }
  .start-pie:hover { box-shadow: 3px 3px 0 var(--hot); }
  .start-pie:active { transform: translate(2px,2px); box-shadow: 0 0 0 var(--hot); }
  .start-duck {
    width: 22px; height: 22px; border-radius: 50%;
    background-color: #fff8ec;
    background-image: url('/assets/icons/duck-mascot.svg');
    background-size: contain; background-position: center; background-repeat: no-repeat;
    border: 1.5px solid #ffffff88;
    flex: 0 0 auto;
  }
  .start-flower { font-size: 13px; flex: 0 0 auto; color: var(--hot); }
  .start-label { font-size: 13px; font-weight: 700; }

  .taskbar-folder {
    width: 26px; height: 20px;
    border-radius: 2px;
    transition: transform 120ms ease, filter 120ms ease;
  }
  .taskbar-folder:hover { transform: translateY(-3px); filter: brightness(1.08); }
  .taskbar-folder.orange { background: linear-gradient(180deg,#ffd79a,#f3ac53); border: 1px solid #d9903c; }
  .taskbar-folder.pink { background: linear-gradient(180deg,#ffd3e6,#ff9fc7); border: 1px solid #e274a6; }
  .taskbar-folder.blue { background: linear-gradient(180deg,#c3d6ff,#8fadf7); border: 1px solid #5a75d1; }
  .taskbar-folder.peach { background: linear-gradient(180deg,#ffe2c2,#f6c48c); border: 1px solid #d9a85c; }

  .taskbar-search {
    margin-left: 8px;
    flex: 0 0 280px;
    height: 32px;
    background: #FFFFFF;
    border-radius: 999px;
    font-size: 13.5px;
    color: var(--ink-text);
    padding: 0 12px;
    display: flex; align-items: center; justify-content: space-between;
    border: 1.5px solid var(--rim);
    box-shadow: 2px 2px 0 var(--hot);
    transition: box-shadow 150ms ease, border-color 150ms ease;
  }
  .taskbar-search.focused,
  .taskbar-search:focus-within { box-shadow: 0 0 0 3px var(--blue); }
  .taskbar-search input {
    border: none; outline: none; font: inherit; color: var(--ink-text); width: 100%;
    background: transparent;
  }
  /* the placeholder was already the lowest-contrast thing in the bar, and
     the frosted background lightened everything under it */
  .taskbar-search input::placeholder { color: #6E5A75; }
  .taskbar-search > span { color: #B7BCC6; font-size: 14px; cursor: var(--cursor-pointer); }

  .taskbar-tasks {
    display: flex; align-items: center; gap: 6px;
    margin-left: 6px;
    flex: 1;
    overflow: hidden;
  }
  .taskbar-task {
    display: flex; align-items: center; gap: 4px;
    background: rgba(94,71,99,0.08);
    color: var(--ink-text);
    font-size: 12px;
    padding: 4px 10px;
    border-radius: 999px;
    border: 1.5px solid rgba(94,71,99,0.28);
    max-width: 130px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    transition: background 120ms ease;
  }
  .taskbar-task:hover { background: rgba(94,71,99,0.14); }
  .taskbar-task.active { background: rgba(255,127,168,0.30); }

  /* pixel clock: 22px time / 11px date, tabular so the width never jitters */
  .taskbar-clock {
    margin-left: auto;
    text-align: right;
    color: var(--ink-text);
    padding-right: 4px;
    min-width: 74px;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    user-select: none;
  }
  .taskbar-clock .time {
    display: block;
    font-family: var(--font-pixel);
    font-variant-numeric: tabular-nums;
    font-size: 17px;
    font-weight: 700;
    letter-spacing: .04em;
  }
  .taskbar-clock .date { font-size: 9px; opacity: .9; letter-spacing: .02em; }

  /* ---------- Start menu ---------- */
  .start-menu {
    position: absolute;
    left: 8px; bottom: 46px;
    width: 190px;
    background: var(--cream);
    color: var(--ink-text);
    border: 1.5px solid var(--rim);
    border-radius: 14px;
    box-shadow: 4px 4px 0 var(--sticker-shadow);
    overflow: hidden;
    z-index: 10001;
    transform-origin: bottom left;
    animation: startOpen 160ms cubic-bezier(.2,1.4,.4,1) both;
  }
  @keyframes startOpen {
    0% { opacity: 0; transform: scale(0.85) translateY(8px); }
    100% { opacity: 1; transform: scale(1) translateY(0); }
  }
  .start-menu .sm-header {
    background: linear-gradient(90deg, var(--hot), var(--pink) 60%, var(--blue));
    color: var(--ink-text); font-size: 12px; font-weight: 700; padding: 8px 10px;
    border-bottom: 1.5px solid var(--rim);
  }
  .start-menu .sm-item {
    display: flex; align-items: center; gap: 8px;
    padding: 7px 10px;
    font-size: 11.5px; color: var(--ink-text);
    transition: background 100ms ease;
  }
  .start-menu .sm-item:hover { background: rgba(255,127,168,0.20); }
  .start-menu .sm-dot {
    width: 10px; height: 10px; border-radius: 50%;
  }
  .start-menu .sm-sep {
    height: 1px;
    margin: 6px 8px;
    background: rgba(94, 71, 99, 0.18);
  }

  /* ---------- right-click context menu ---------- */
  .context-menu {
    position: fixed;
    background: #f4f2f0;
    border: 1px solid #8f8f9a;
    border-radius: 4px;
    box-shadow: 4px 6px 18px rgba(0,0,0,0.3);
    overflow: hidden;
    z-index: 10002;
    min-width: 150px;
    animation: startOpen 120ms cubic-bezier(.2,1.4,.4,1) both;
  }
  .context-menu-item {
    padding: 7px 12px;
    font-size: 11.5px; color: #3a3a4a;
    cursor: pointer;
    transition: background 100ms ease;
  }
  .context-menu-item:hover { background: rgba(111,156,232,0.18); }

  /* ---------- splashscreen ---------- */
  .splash {
    position: fixed; inset: 0; z-index: 20000;
    background: linear-gradient(135deg, #a9c9ec 0%, #f5c6d8 40%, #f7cf9e 100%);
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 16px;
    transition: opacity 300ms ease;
  }
  .splash.hidden { opacity: 0; pointer-events: none; }
  .splash-log {
    font-family: 'Courier New', monospace; font-size: 12px; color: #3a3a4a;
    width: 320px; height: 90px; overflow: hidden;
    text-shadow: 0 1px 2px rgba(255,255,255,0.6);
  }
  .splash-bar-track { width: 240px; height: 10px; background: rgba(255,255,255,0.5); border-radius: 4px; overflow: hidden; }
  .splash-bar-fill { width: 0%; height: 100%; background: linear-gradient(90deg,#6f9ce8,#ee9fc0); transition: width 200ms ease; }

  /* ---------- account chip ---------- */
  .account-chip { margin-left: 8px; font-size: 12px; color: var(--ink-text); display: flex; align-items: center; gap: 6px; }
  .account-chip a { color: inherit; text-decoration: none; }

  /* Generic inline user avatar for leaderboards, reviews, comments, etc. */
  .user-avatar {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    object-fit: cover;
    vertical-align: middle;
    display: inline-block;
    margin-right: 6px;
    background-color: var(--face, #ccc);
    border: 1px solid var(--rim, #000);
  }
  .user-avatar-fallback {
    display: inline-block;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    vertical-align: middle;
    margin-right: 6px;
    background-color: var(--face, #ccc);
    border: 1px solid var(--rim, #000);
  }

  /* Signed-in state. The tweaker mockup only ever renders the signed-out
     "Sign in with Discord" pill, so there is no reference to copy here -
     this is built from the dock's own vocabulary instead: pill, plum rim,
     hard pink offset shadow, pixel face, same 41px height as the sign-in
     button it replaces so the bar doesn't change height on login. */
  .account-chip.is-signed-in {
    display: inline-flex; align-items: center; gap: 8px;
    height: 41px;               /* same as #account-login-link, deliberately */
    padding: 0 16px 0 5px;
    border-radius: 999px;
    background: var(--card);
    border: 1.5px solid var(--rim);
    box-shadow: 2px 2px 0 var(--hot);
    max-width: 200px;
  }
  .account-chip.is-signed-in img {
    width: 30px; height: 30px; border-radius: 50%;
    border: 1.5px solid var(--rim);
    background: var(--face);
    flex: 0 0 auto;
  }
  .account-chip.is-signed-in span {
    font-size: 11px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* chunky pill sign-in - it was bare text pretending to be a link */
  #account-login-link {
    display: inline-flex; align-items: center;
    height: 41px;
    padding: 0 18px;
    border-radius: 999px;
    background: var(--hot);
    color: #fff !important;
    border: 1.5px solid var(--rim);
    box-shadow: 3px 3px 0 var(--hot);
    font-size: 15px;
    font-weight: 600;
    white-space: nowrap;
    text-decoration: none;
    transition: transform .1s, box-shadow .1s;
  }
  #account-login-link:active { transform: translate(2px,2px); box-shadow: 1px 1px 0 var(--rim); }
  #account-login-link:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
  #account-login-link span { min-width: 0; }

  /* ---------- icon section layout (desktop) ----------
     Mockup: 4 columns of 136 x 137px cells at left:36. Top is 78px when the
     review marquee is on and 48px when it is off - the marquee's strip is
     reserved, never overlapped. --grid-top is set on <body> by index.php. */
  .icon-section { position: absolute; z-index: 5; }
  #apps-section.apps-section {
    left: 36px;
    top: var(--grid-top, 78px);
    display: grid;
    grid-template-columns: repeat(4, 136px);
    grid-auto-rows: 137px;
    /* max-height + overflow:visible used to be paired here on the theory
       that grid-auto-rows "capped" the row count against available height -
       it doesn't; a grid always sizes to fit its content, so with
       overflow:visible the excess rows just spilled silently past
       max-height into whatever sat below (see script.js's
       guardDesktopChromeCollision, which exists because of exactly that).
       overflow-y:auto makes the cap real. This only ever shows a scrollbar
       once icon count genuinely exceeds the available height - at today's
       icon count it's invisible and nothing changes visually. */
    max-height: calc(100vh - var(--grid-top, 78px) - 80px);
    overflow-y: auto;
    /* overflow-x must not be "visible" here: per spec, pairing a non-visible
       overflow-y with overflow-x:visible computes the x axis to "auto"
       anyway, which was silently drawing a second (horizontal) scrollbar
       across the bottom of the grid. hidden avoids that trap outright -
       content is fixed at 4 columns and never needs to scroll sideways. */
    overflow-x: hidden;
    /* Scrolling stays live (mouse wheel / touch), the bar itself is just
       not drawn - a native scrollbar clashes with the rest of the desktop
       chrome regardless of how it's tinted. */
    scrollbar-width: none;
  }
  #apps-section.apps-section::-webkit-scrollbar { display: none; }

  /* Utility corner: visually separated from Apps/Playground, bottom-right
     above the dock - a future settings icon lands here. */
  #utility-section.utility-section {
    top: auto; left: auto;
    bottom: 86px; right: 24px;
    display: grid;
    grid-template-columns: 136px;
    grid-auto-rows: 137px;
  }

  /* ---------- tablet tier ----------
     Windows stay draggable/resizable here (now via Pointer Events, so touch
     works too) but controls get a touch-friendlier bump short of the full
     mobile fullscreen/dock treatment. */
  @media (min-width: 641px) and (max-width: 1024px) {
    .win-controls span { width: 26px; height: 24px; font-size: 13px; }
    .win-titlebar { height: 34px; }
    .start-pie { width: 32px; height: 32px; }
    .taskbar-folder { width: 32px; height: 26px; }
    .taskbar { height: 46px; }
  }

  .win-fullscreen-mobile {
    position: fixed !important; inset: 0 !important;
    width: 100% !important; height: 100% !important;
    border-radius: 0 !important;
  }
  .win-fullscreen-mobile .win-resize-handle {
    display: none;
  }

/* =====================================================================
   Wallpaper fx layers. Fixed, pointer-events:none, and deliberately BELOW
   the windows (grain 3 / scanlines 4 vs .win at z-index 100+): app content
   lives in iframes, and washing grain or scanlines across body text costs
   more legibility than the effect returns.
   ===================================================================== */
.wallpaper-grain {
  position: fixed; inset: 0; pointer-events: none; z-index: 3;
  opacity: var(--grain-opacity);
  mix-blend-mode: var(--grain-blend);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='180' height='180' filter='url(%23n)'/></svg>");
  background-size: 180px 180px;
}

/* hearts, 52px tile, drawn in translucent white so it reads as texture on
   the gradient rather than a second competing hue */
.desktop-pattern {
  position: fixed; inset: 0; pointer-events: none; z-index: 1;
  background-size: 52px 52px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='52' height='52'><path d='M26 34c-6-4.4-9-7-9-10.6 0-2.7 2.1-4.6 4.6-4.6 1.8 0 3.3.9 4.4 2.4 1.1-1.5 2.6-2.4 4.4-2.4 2.5 0 4.6 1.9 4.6 4.6C35 27 32 29.6 26 34z' fill='rgba(255,255,255,0.34)'/></svg>");
}

.desktop-vignette {
  position: fixed; inset: 0; pointer-events: none; z-index: 2;
  background: radial-gradient(115% 95% at 42% 38%, transparent 42%, var(--vignette-tint) 100%);
}

.blob {
  position: fixed; border-radius: 50%; filter: blur(70px);
  pointer-events: none; z-index: 1; opacity: var(--blob-opacity);
  will-change: transform;
}
.blob--a { width: 460px; height: 380px; background: var(--blob-a); left: -6%; top: 8%;  animation: driftA 30s ease-in-out infinite alternate; }
.blob--b { width: 400px; height: 340px; background: var(--blob-b); right: 4%; top: 44%; animation: driftB 34s ease-in-out infinite alternate; }
@keyframes driftA { to { transform: translate(120px, 70px) scale(1.12); } }
@keyframes driftB { to { transform: translate(-90px, -60px) scale(1.08); } }

.crt-scanlines {
  position: fixed; inset: 0; pointer-events: none; z-index: 4;
  background: repeating-linear-gradient(180deg, var(--scanline) 0 1px, transparent 1px 3px);
}

/* ambient stars - positions and delays come from the index in script.js,
   never Math.random(), or they jump on every re-render */
#wall-stars { position: fixed; inset: 0; pointer-events: none; z-index: 1; }
.wall-star {
  position: absolute;
  color: #fff;
  opacity: 0;
  /* mockup cycles roughly 5-17px; the old 2-8px range left several
     effectively invisible */
  animation: twinkle var(--dur, 8s) ease-in-out var(--delay, 0s) infinite;
}
@keyframes twinkle {
  0%, 100% { opacity: 0;   transform: scale(.6) rotate(0deg); }
  50%      { opacity: .45; transform: scale(1) rotate(45deg); }
}
@media (prefers-reduced-motion: reduce) {
  .blob, .wall-star { animation: none; }
}

/* =====================================================================
   Type. Pixel face on OS chrome only - at icon-label size a bitmap face is
   unreadable, so labels stay proportional. Regular capitalisation
   throughout (tweak #23 is off).
   ===================================================================== */
/* The whole dock is the pixel face in the mockup, so the sign-in button,
   search placeholder, task chips and clock date all inherit it - not just
   the start label and the time. */
/* Pixel face on OS chrome. The marquee is deliberately NOT in it: those are
   real sentences (venue names, post titles) scrolling past, and a bitmap
   face at 15px on moving text is genuinely hard to read. Chrome is short
   fixed strings, which is what a pixel face is good at. */
.taskbar, .win-titlebar span {
  font-family: var(--font-pixel);
  letter-spacing: .02em;
}
.win-titlebar span { font-size: 13px; }
.marquee__item { font-family: var(--font-ui); font-size: 14px; font-weight: 600; }
.taskbar-task { font-size: 11px; }
.start-label { font-size: 11px; }
#account-login-link { font-size: 12px; }
.taskbar-search { font-size: 12px; }
.visitor-counter .vc-label { font-size: 12px; }
.construction { font-size: 13px; }

/* glitter gradient text: marquee and window titles only, never body text */
.glitter {
  /* Dark stops, per the mockup. The tweaker's *emitted* CSS uses pastels
     (#FF7FA8, #F7E7A0, #BFD9F2) while its own guidance for the same control
     says "keep the stops dark enough to read" - on a 60%-white marquee bar
     the pastel set is very nearly invisible. The mockup follows the advice,
     not the snippet. */
  background: linear-gradient(100deg, var(--glitter-a), var(--glitter-b), var(--glitter-c), var(--glitter-a));
  background-size: 220% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: glitterSweep 7s linear infinite;
}
@keyframes glitterSweep { to { background-position: 220% 0; } }
@media (prefers-reduced-motion: reduce) { .glitter { animation: none; } }
/* without a fallback, unsupported browsers render transparent text on a
   transparent background - i.e. nothing at all */
@supports not ((background-clip: text) or (-webkit-background-clip: text)) {
  .glitter { color: var(--glitter-a); background: none; -webkit-text-fill-color: initial; }
}

/* =====================================================================
   Desktop furniture: widgets, marquee, badges, counter, construction
   ===================================================================== */
.desktop-widget {
  position: fixed;
  z-index: 6;
  background: var(--card);
  border: 1.5px solid var(--rim);
  border-radius: 14px;
  /* hard pink offset, matching the sticker language used by the icons,
     dock and windows - the old translucent plum broke it */
  box-shadow: 4px 4px 0 var(--hot);
  padding: 13px;
  width: 214px;
  font-size: 12.5px;
  color: var(--ink-text);
  cursor: grab;
  touch-action: none;
}
.desktop-widget:active { cursor: grabbing; }
/* Two columns. Column A carries the (large) player and the polaroid; column
   B starts clear of the player's full box, not just its artwork. */
/* Column A: player (598..1095) then the polaroid below it.
   Column B: clears the player's full box, not just its artwork. */
.desktop-widget--polaroid   { left: 1125px; transform: rotate(-2.5deg); }
.desktop-widget--shamaz     { left: 1140px; transform: rotate(2deg); }
.desktop-widget--blog       { left: 1132px; transform: rotate(1.5deg); }
.desktop-widget--mascot     { left: 1125px; transform: rotate(-1.25deg); }

/* Narrower desktops cannot fit a 497px player beside a second card column,
   so below 1500px the player drops back to card scale and column B moves in. */

/* newest blog post */
.dw-post-title { font-size: 13.5px; font-weight: 700; line-height: 1.25; }
.dw-post-ex { font-size: 11.5px; color: #6E5A75; margin-top: 4px; }
.dw-post-date { font-size: 10.5px; color: #6E5A75; margin-top: 6px; }
.desktop-widget a { color: inherit; text-decoration: none; }
.desktop-widget a:hover .dw-post-title { text-decoration: underline; }

/* shamaz daily leaderboard */
.dw-lb + .dw-lb { margin-top: 10px; }
.dw-lb-mode {
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  color: #6E5A75; margin-bottom: 4px;
}
.dw-lb ol { list-style: none; margin: 0; padding: 0; }
.dw-lb li { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; line-height: 1.5; }
.dw-lb li b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dw-lb li span { font-family: var(--font-pixel); font-size: 11px; flex: 0 0 auto; }
.dw-lb li:first-child b { color: var(--hot); }

/* The real cassette player, pinned as a widget rather than opened as a
   window. Scaled down from its native 368x276 and tilted to match the
   other stickers. */
.ahf90-shell--pinned {
  position: fixed;
  left: 598px; top: 74px;
  right: auto; bottom: auto;
  /* Sized against the FLOATING player, which is the reference: that one is
     scale(1.5), putting its artwork at ~371px. Matching the widget cards was
     the wrong target - it made the player a third the size it is when you
     open it. 1.35 lands just past the floating player once the 1.2 scale
     tier applies. (Only 495 of the SVG's 736-unit viewBox is drawn, so the
     shell box is always ~33% wider than the visible cassette.) */
  transform: scale(1.35) rotate(-2deg);
  transform-origin: top left;
  z-index: 6;
}

/* Placed AFTER the base .ahf90-shell--pinned rule on purpose: media
   queries carry no extra specificity, so a scale() declared earlier in
   the file loses to the base one declared later. */
@media (max-width: 1499px) {
  .ahf90-shell--pinned { transform: scale(.85) rotate(-2deg); }
  .desktop-widget--polaroid { left: 950px; }
  .desktop-widget--shamaz { left: 946px; }
  .desktop-widget--blog   { left: 956px; }
  .desktop-widget--mascot { left: 966px; }
}
.desktop-widget .dw-title {
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: #6E5A75; margin-bottom: 7px;
}
/* now-playing card */
.dw-np { display: flex; align-items: center; gap: 11px; }
.dw-vinyl {
  width: 42px; height: 42px; border-radius: 50%; flex: 0 0 auto;
  background: conic-gradient(var(--hot), var(--blue), var(--pink), var(--hot));
  border: 1.5px solid var(--rim);
  animation: dwSpin 4.5s linear infinite;
  display: grid; place-items: center;
}
.dw-vinyl::after { content: ""; width: 9px; height: 9px; border-radius: 50%; background: var(--cream); border: 1px solid var(--rim); }
@keyframes dwSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .dw-vinyl { animation: none; } }
.dw-track { font-size: 14px; font-weight: 700; line-height: 1.2; }
.dw-artist { font-size: 12px; color: #6E5A75; }
.dw-rail { margin-top: 10px; height: 5px; border-radius: 999px; background: #E7D9E6; overflow: hidden; }
.dw-rail i { display: block; height: 100%; background: var(--hot); }
/* polaroid photo panel + score */
.dw-photo {
  height: 112px; border-radius: 4px; border: 1px solid var(--rim);
  background: linear-gradient(150deg, #F7C9A6, #E8809B 55%, #B98FC4);
  display: grid; place-items: center; margin-bottom: 9px;
}
.dw-score { font-size: 30px; font-weight: 800; color: #fff; line-height: 1; text-shadow: 0 2px 4px rgba(0,0,0,0.8), 0 0 10px rgba(0,0,0,0.5); }
.dw-venue { font-size: 12.5px; font-weight: 600; }
.dw-caption { font-size: 11.5px; color: #6E5A75; margin-top: 3px; }
.dw-mascot { display: flex; align-items: center; gap: 10px; }
.dw-mascot svg { width: 38px; height: 38px; flex: 0 0 auto; }
.desktop-widget .dw-dismiss {
  position: absolute; top: -8px; right: -8px;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--card); border: 1.5px solid var(--rim);
  display: grid; place-items: center; font-size: 12px;
  color: var(--ink-text);
}
.desktop-widget.dismissed { display: none; }
/* Set by guardWidgetStackCollision() in script.js when a widget's real
   rendered bottom edge would run under the taskbar - same measured-not-
   guessed pattern as .desktop-bottom-left.chrome-collision above. */
.desktop-widget.chrome-collision { display: none; }

/* Marquee. The icon grid's top offset (--grid-top) accounts for this strip,
   so the two never overlap. */
.marquee {
  position: fixed; top: 0; left: 0; right: 0; z-index: 7;
  overflow: hidden; white-space: nowrap;
  background: rgba(255,255,255,0.55);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid rgba(255,127,168,0.5);
  padding: 5px 0;
}
.marquee__track { display: inline-flex; animation: marqueeSlide 34s linear infinite; }
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track { animation-play-state: paused; }
/* Items are links now (each event points at its app). No underline: with
   background-clip:text the underline paints in the transparent text colour,
   so it is either invisible or a stray artefact depending on the browser. */
.marquee__item { padding: 0 26px; text-decoration: none; color: inherit; }
.marquee__item:hover { filter: brightness(1.15); }
@keyframes marqueeSlide { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .marquee__track { animation: none; } }

.desktop-bottom-left {
  position: fixed;
  left: 36px;
  bottom: 96px;
  z-index: 6;
  display: flex;
  flex-direction: column-reverse;
  gap: 16px;
  align-items: flex-start;
}
/* Set by guardDesktopChromeCollision() in script.js, not a viewport-width/
   height breakpoint: it measures the icon grid's real rendered bottom edge
   against this cluster's real top edge on every load/resize, and only
   collapses it when they'd actually overlap - correct at any OS
   display-scale factor and any icon count, rather than a guessed
   threshold tuned to whatever happened to be true today. */
.desktop-bottom-left.chrome-collision { display: none; }
.badge-strip-viewport {
  width: 330px;
  height: 156px;
  overflow: hidden;
  position: relative;
}
.badge-strip { 
  display: grid;
  grid-template-columns: repeat(330, 1px);
  grid-auto-rows: 20px;
  grid-auto-flow: dense;
  width: 330px;
  gap: 6px 0;
  position: absolute;
  top: 0;
  left: 0;
}
/* Decorative tags, not links (nothing to point them at yet). */
.badge-88x31.b-bmth { --badge-bg: #2B2438; --badge-fg: #F7E7A0; }
.badge-88x31.b-prx  { --badge-bg: #6B3FA0; --badge-fg: #FFC2E2; }
.badge-88x31.b-tech { --badge-bg: #BFD9F2; --badge-fg: #2B2438; }
.badge-88x31.b-food { --badge-bg: #FF7FA8; --badge-fg: #FDFAF4; }
span.badge-88x31 { cursor: default; }
.badge-88x31 {
  width: 88px; height: 31px;
  display: grid; place-items: center;
  font-family: var(--font-ui);
  font-size: 12px; font-weight: 700; letter-spacing: -0.01em;
  text-align: center; line-height: 1.15; padding: 2px;
  color: var(--badge-fg, var(--ink-text)); text-decoration: none;
  background: var(--badge-bg, var(--cream));
  border-top: 1px solid #FFFFFF; border-left: 1px solid #FFFFFF;
  border-right: 1px solid var(--rim); border-bottom: 1px solid var(--rim);
  image-rendering: pixelated;
}

/* Stacked bottom-left, layout managed by .desktop-bottom-left */
.visitor-counter {
  display: flex; flex-direction: row; align-items: center; gap: 7px;
  color: var(--ink-text);
}
.visitor-counter .vc-label {
  font-family: var(--font-pixel);
  background: rgba(255,255,255,.62); padding: 3px 7px; border-radius: 6px;
}
.hits { display: inline-flex; gap: 2px; }
.hits span {
  width: 17px; height: 24px;
  display: grid; place-items: center;
  background: #2B2438; color: var(--butter);
  font-family: "Fragment Mono", ui-monospace, monospace; font-size: 15px;
  border: 1px solid var(--rim);
}

/* Reusable under-construction sign (see _lib/under_construction.php).
   Blink is 1.4s and hazard 2.6s - never faster than 3Hz, which is a real
   seizure threshold and one of the reasons <blink> died. */
.construction {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 13px; color: var(--ink-text); font-weight: 800;
  font-family: var(--font-pixel);
  border: 2px solid var(--rim);
  background: repeating-linear-gradient(45deg, var(--butter) 0 12px, #3F3145 12px 24px);
  background-size: 200% 100%;
  animation: hazard 2.6s linear infinite;
}
/* The cream plate goes behind the WORDS - they are unreadable printed
   straight onto the 45-degree hazard stripes. */
.construction b {
  background: var(--cream); padding: 2px 9px;
  animation: blinkSign 1.4s steps(2) infinite;
}
/* .construction--desktop layout is handled by .desktop-bottom-left wrapper */
@keyframes hazard { to { background-position: 48px 0; } }
@keyframes blinkSign { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .construction, .construction b { animation: none; } }

/* =====================================================================
   Large displays. The mockup was authored around laptop width, where four
   136px columns fill a fair share of the screen. Past ~1600px the whole
   desktop strands itself in the top-left corner, which is what the old
   #desktop-scale wrapper existed to prevent. Scale the chrome as a unit
   rather than re-flowing it, so the mockup's proportions survive intact.

   zoom (not transform: scale) because it affects layout: transform would
   leave fixed-position furniture and hit-testing in the unscaled frame.
   ===================================================================== */
:root { --desktop-zoom-auto: 1; }
@media (min-width: 1700px) { :root { --desktop-zoom-auto: 1.2; } }
@media (min-width: 2200px) { :root { --desktop-zoom-auto: 1.45; } }

/* --desktop-zoom is unset by default, so these fall through to the
   viewport-width tiers above. Settings' "Desktop Scale" slider sets it as
   an inline style on <html> (script.js), which - being inline - outranks
   the tiers regardless of which media query matched, letting one manual
   value override the width-based guess on a per-device basis (OS display
   scaling shrinks the effective CSS viewport width in a way these tiers
   can't tell apart from "the user has a small monitor"). */
.icon-section, .taskbar, .desktop-widget, .ahf90-shell--pinned, .contact-shell,
.badge-strip-viewport, .visitor-counter, .construction--desktop, .marquee {
  zoom: var(--desktop-zoom, var(--desktop-zoom-auto, 1));
}

/* Mobile lives here, after the furniture rules above: media queries add
   no specificity, so a display:none declared earlier in the file loses to
   a display:flex declared later regardless of the query. */

  /* ---------- mobile: 4-column tile grid ----------
     The desktop 136×137px cells are too large for a phone, but the 4-column
     grid itself is kept — icons shrink to ~74px tiles with labels below,
     matching the iOS home-screen layout the mockup specifies.
     The broken list-row override (tile-left, label-beside) is gone. */
  @media (max-width: 640px) {
    html, body { overflow-y: auto; }
    body { height: auto; min-height: 100%; }

    /* ---- icon grid ---- */
    .icon-section {
      position: static;
      width: auto; height: auto;
    }
    /* id+class specificity to beat the desktop #apps-section.apps-section rule */
    #apps-section.apps-section,
    #utility-section.utility-section {
      position: static;
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      grid-auto-rows: auto;
      gap: 8px 4px;
      padding: calc(var(--grid-top, 78px) + 12px) 8px 0;
      top: auto; left: auto; bottom: auto; right: auto;
      max-height: none;
      overflow: visible;
    }
    #utility-section.utility-section { padding-bottom: 80px; }

    /* tile-above, label-below — matching the mockup and desktop layout */
    .icon-section .desktop-icon {
      flex-direction: column;
      align-items: center;
      justify-content: flex-start;
      width: auto; height: auto;
      padding: 4px 2px 6px;
      gap: 14px;
      text-align: center;
      animation: none;  /* wiggle reads as jitter on a phone */
    }
    .desktop-icon::before { display: none; } /* blush is tuned for the desktop grid */
    /* smaller sticker tile: 54px face on a 62px glyph cell */
    .desktop-icon .glyph {
      width: 62px; height: 62px; flex: 0 0 62px;
      border-radius: 20%;
      box-shadow: 0 0 0 4px var(--cream), 3px 3px 0 4px var(--sticker-shadow);
    }
    .desktop-icon .glyph img { width: 44px; height: 44px; }
    /* peel corner slightly smaller at this scale */
    .desktop-icon .glyph::after { width: 11px; height: 11px; }
    .desktop-icon:hover .glyph::after { width: 16px; height: 16px; }
    /* label chip: single line, truncate, slightly smaller */
    .desktop-icon .icon-label {
      max-width: 100%;
      font-size: 11px;
      padding: 2px 6px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    /* ---- taskbar ---- */
    /* Stays at the bottom, no dock replacement. Gets touch-friendly height,
       straight edges (no gap from screen edge), and safe-area padding for
       notched phones (Fold 5 punch-hole / iPhone home bar). */
    .taskbar-search { display: none; }
    .taskbar {
      left: 0; right: 0; bottom: 0;
      height: calc(56px + env(safe-area-inset-bottom, 0px));
      padding: 0 12px env(safe-area-inset-bottom, 0px);
      border-radius: 0;
      border-left: 0; border-right: 0; border-bottom: 0;
      box-shadow: 0 -2px 10px rgba(94,71,99,0.20);
    }
    /* enlarge start button for thumb tap */
    .start-pie { min-height: 40px; }
    .win-titlebar { height: 44px; font-size: 14px; padding: 0 10px; }
    .win-controls span { width: 36px; height: 34px; font-size: 14px; }
    .taskbar-task { font-size: 12px; padding: 10px 12px; }
    /* The clock is the last flex item and must never lose the fight for
       space to the sign-in pill - min-width alone doesn't stop it being
       pushed off the edge of a narrow screen (html/body clip overflow). */
    .taskbar-clock { flex-shrink: 0; }
    .taskbar-clock .time { font-size: 18px; }
    /* "Sign in with Discord" (and a signed-in username) is wider than a
       phone has room for once the clock is guaranteed its space - clamp
       and ellipsis rather than let it shove the clock off-screen. */
    .account-chip { margin-left: 4px; max-width: 92px; min-width: 0; overflow: hidden; }
    #account-login-link { padding: 0 10px; font-size: 11px; max-width: 100%; overflow: hidden; }
    #account-login-link span {
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block;
    }
    /* start menu anchors just above the taskbar's content area */
    .start-menu { bottom: calc(58px + env(safe-area-inset-bottom, 0px)); }


    /* ---- full-screen windows (Option B) ---- */
    /* win-fullscreen-mobile is already set by syncMobileClass() in script.js
       for ALL open windows at ≤640px. These rules augment that class to:
       - slide in from the bottom (slide-up entry animation)
       - leave room for the taskbar at the bottom via pb margin */
    .win-fullscreen-mobile {
      /* Override the generic rule's inset:0 to leave the taskbar visible */
      top: 0 !important;
      left: 0 !important;
      right: 0 !important;
      bottom: calc(56px + env(safe-area-inset-bottom, 0px)) !important;
      width: 100% !important;
      height: auto !important;
      border-radius: 0 !important;
      animation: mobileWinSlideUp 300ms cubic-bezier(.2,.86,.24,1) both !important;
    }
    @keyframes mobileWinSlideUp {
      from { transform: translateY(60px); opacity: 0; }
      to   { transform: translateY(0);   opacity: 1; }
    }
    /* closing: slide back down */
    .win-fullscreen-mobile.closing {
      animation: mobileWinSlideDown 200ms ease-in forwards !important;
    }
    @keyframes mobileWinSlideDown {
      from { transform: translateY(0);   opacity: 1; }
      to   { transform: translateY(60px); opacity: 0; }
    }

    /* ---- cassette player on mobile ---- */
    /* Centers the DOM widget on the screen and scales it down to fit.
       !important overrides any inline left/top coordinates set by desktop dragging. */
    .ahf90-shell {
      left: 0 !important; right: 0 !important;
      top: 0 !important; bottom: 0 !important;
      margin: auto !important;
      transform: scale(0.9);
      transform-origin: center;
    }
    .ahf90-shell.ahf90-shell--pinned {
      transform: scale(0.9) !important;
    }

    /* ---- fixed-position desktop furniture — no room on a phone ---- */
    .badge-strip-viewport, .badge-strip,
    .visitor-counter, .construction--desktop,
    .desktop-widget { display: none; }
  }

  /* ---------- Small phones: 3 columns at ≤ 380px ----------
     At ~375px (iPhone SE) or ~340px (Fold 5), 4 columns gives ~70-80px
     per cell which is tight. Drop to 3 columns → ~100px per cell, much
     more comfortable for thumbs. Labels stay below tiles. */
  @media (max-width: 380px) {
    #apps-section.apps-section,
    #utility-section.utility-section {
      grid-template-columns: repeat(3, 1fr);
    }
    /* Slightly larger glyphs to fill the extra width */
    .desktop-icon .glyph {
      width: 68px; height: 68px; flex: 0 0 68px;
    }
    .desktop-icon .glyph img { width: 48px; height: 48px; }
  }

/* =====================================================================
   Dark mode. Every token above has a dark counterpart in the tweaker
   (`kh.dark`); this re-points the variables rather than re-declaring rules.
   ===================================================================== */

/* =====================================================================
   Dark mode. Every token has a dark counterpart in the tweaker (`kh.dark`);
   this re-points the variables rather than re-declaring component rules, so
   the two selectors below are the entire dark theme.

   The token list is duplicated for the system-preference case because CSS
   cannot share a declaration block across a media query boundary. Edit one,
   edit both.
   ===================================================================== */
:root[data-theme="dark"],
:root:not([data-theme="light"]) {
  --dark-cream: #241C33;
  --dark-card: #33284A;
  --dark-face: #2E2440;
  --dark-rim: #7A6699;
  --dark-ink-text: #F2E9F7;
  --dark-hot: #FF6FA0;
  --dark-pink: #C97FA6;
  --dark-blue: #6E93C9;
  --dark-butter: #D9C173;
  --dark-chip: rgba(23,17,34,.62);
  --dark-wall-start: #2A2140;
  --dark-wall-mid: #3B2A45;
  --dark-wall-end: #8A2F55;
  /* overlay crushes toward black on a dark wall and the grain disappears,
     so soft-light at roughly two thirds the opacity */
  --dark-grain-opacity: .125;
  --dark-grid-line: rgba(255,236,246,0.040);
  --dark-vignette-tint: rgba(10,6,18,0.479);
  --dark-blob-a: #7A2E52;
  --dark-blob-b: #2C3E66;
  /* dark scanlines on a dark wall do nothing; pale ones read as phosphor
     glow between raster rows, which is the effect actually wanted */
  --dark-scanline: rgba(255,226,240,0.025);
  --dark-blush: rgba(255,111,160,0.238);
  --dark-pattern: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='52' height='52'><path d='M26 34c-6-4.4-9-7-9-10.6 0-2.7 2.1-4.6 4.6-4.6 1.8 0 3.3.9 4.4 2.4 1.1-1.5 2.6-2.4 4.4-2.4 2.5 0 4.6 1.9 4.6 4.6C35 27 32 29.6 26 34z' fill='rgba(255,236,246,0.15)'/></svg>");
  /* a white-filled arrow disappears against a dark desktop */
  --dark-cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><path d='M3 2 L3 18 L6.5 14.5 L10 20.5 L12.5 19.5 L9 13.5 L14 13.5 Z' fill='%23241C33' stroke='%23F2E9F7' stroke-width='1.4' stroke-linejoin='round'/></svg>") 3 2, auto;
}

/* Applying the dark values. Explicit opt-in via Settings. */
:root[data-theme="dark"] {
  --cream: var(--dark-cream);      --card: var(--dark-card);
  --face: var(--dark-face);        --rim: var(--dark-rim);
  --ink-text: var(--dark-ink-text);
  --hot: var(--dark-hot);          --pink: var(--dark-pink);
  --blue: var(--dark-blue);        --butter: var(--dark-butter);
  --chip: var(--dark-chip);        --chip-text: var(--dark-ink-text);
  --overlay: 255,236,246;          --pop: var(--dark-wall-end);
  --wall-start: var(--dark-wall-start);
  --wall-mid: var(--dark-wall-mid);
  --wall-end: var(--dark-wall-end);
  --sticker-shadow: var(--dark-hot);
  --grain-opacity: var(--dark-grain-opacity);
  --grain-blend: soft-light;
  --grid-line: var(--dark-grid-line);
  --vignette-tint: var(--dark-vignette-tint);
  --blob-opacity: .58;
  --blob-a: var(--dark-blob-a);    --blob-b: var(--dark-blob-b);
  --scanline: var(--dark-scanline);
  --blush: var(--dark-blush);
  --cursor-arrow: var(--dark-cursor);
}
:root[data-theme="dark"] .desktop-pattern { background-image: var(--dark-pattern); }
:root[data-theme="dark"] .taskbar { background: rgba(40,30,58,0.60); }
:root[data-theme="dark"] .marquee { background: rgba(40,30,58,0.60); }
:root[data-theme="dark"] .taskbar-search { background: rgba(40,30,58,0.75); }
:root[data-theme="dark"] .taskbar-search input::placeholder { color: rgba(242,233,247,0.62); }
:root[data-theme="dark"] .hits span { background: #0E0A18; }
:root[data-theme="dark"] .construction {
  background: repeating-linear-gradient(45deg, var(--butter) 0 12px, #120D1F 12px 24px);
}
/* A hard offset shadow needs a lighter surface behind it and simply vanishes
   on a dark wall. Same job (this thing floats), opposite physics: a small
   near-black offset to keep it lifted, plus a wide accent bloom underneath. */
:root[data-theme="dark"] .desktop-icon .glyph {
  box-shadow: 0 0 0 5px #FDFAF4, 3px 3px 0 5px rgba(15,10,25,.6), 0 0 20px 5px rgba(255,111,160,.16);
}
:root[data-theme="dark"] .win {
  box-shadow: 4px 4px 0 rgba(15,10,25,.55), 0 0 16px rgba(255,111,160,.18);
}

/* Same, for visitors who never opened Settings and are on a dark system theme. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --cream: var(--dark-cream);      --card: var(--dark-card);
    --face: var(--dark-face);        --rim: var(--dark-rim);
    --ink-text: var(--dark-ink-text);
    --hot: var(--dark-hot);          --pink: var(--dark-pink);
    --blue: var(--dark-blue);        --butter: var(--dark-butter);
    --chip: var(--dark-chip);        --chip-text: var(--dark-ink-text);
    --overlay: 255,236,246;          --pop: var(--dark-wall-end);
    --wall-start: var(--dark-wall-start);
    --wall-mid: var(--dark-wall-mid);
    --wall-end: var(--dark-wall-end);
    --sticker-shadow: var(--dark-hot);
    --grain-opacity: var(--dark-grain-opacity);
    --grain-blend: soft-light;
    --grid-line: var(--dark-grid-line);
    --vignette-tint: var(--dark-vignette-tint);
    --blob-opacity: .58;
    --blob-a: var(--dark-blob-a);    --blob-b: var(--dark-blob-b);
    --scanline: var(--dark-scanline);
    --blush: var(--dark-blush);
    --cursor-arrow: var(--dark-cursor);
  }
  :root:not([data-theme="light"]) .desktop-pattern { background-image: var(--dark-pattern); }
  :root:not([data-theme="light"]) .taskbar { background: rgba(40,30,58,0.60); }
  :root:not([data-theme="light"]) .marquee { background: rgba(40,30,58,0.60); }
  :root:not([data-theme="light"]) .taskbar-search { background: rgba(40,30,58,0.75); }
  :root:not([data-theme="light"]) .taskbar-search input::placeholder { color: rgba(242,233,247,0.62); }
  :root:not([data-theme="light"]) .hits span { background: #0E0A18; }
  :root:not([data-theme="light"]) .construction {
    background: repeating-linear-gradient(45deg, var(--butter) 0 12px, #120D1F 12px 24px);
  }
  :root:not([data-theme="light"]) .desktop-icon .glyph {
    box-shadow: 0 0 0 5px #FDFAF4, 3px 3px 0 5px rgba(15,10,25,.6), 0 0 20px 5px rgba(255,111,160,.16);
  }
  :root:not([data-theme="light"]) .win {
    box-shadow: 4px 4px 0 rgba(15,10,25,.55), 0 0 16px rgba(255,111,160,.18);
  }
}
