/* ---------------- Theme palettes ----------------
   Same token names in both palettes; only the values flip. Any new
   color token must be added to BOTH blocks so theme switching stays
   total. Hex literals in component CSS are a smell — add a token
   here first, then reference it.

   USER PALETTE LAYERING (see /settings → Custom palette): eight color
   tokens read `var(--user-X, theme-default)` so a user-supplied
   `--user-X` (injected via <style id="lucyna-user-palette"> from the
   pre-paint script) overrides the theme default. Tokens NOT covered
   by the user palette (status, shadow, scrim, panel chrome, etc.)
   ride the theme default unchanged. One palette, both themes still
   adjust around it for tokens the user hasn't touched. */
:root,
[data-theme="dark"] {
    /* Surfaces — bg is the page, raised is panels, elevated is menus/popovers. */
    --bg:          var(--user-bg,          #0a0a0a);
    --bg-raised:   var(--user-bg-raised,   #141414);
    --bg-elevated: var(--user-bg-elevated, #181818);

    /* Foreground — fg → bright text, soft → secondary headers, muted → labels, dim → placeholder, deep → disabled/divider text. */
    --fg:        var(--user-fg,       #e8e8e8);
    --fg-soft:   var(--user-fg-soft,  #b8b8b8);
    --fg-muted:  var(--user-fg-muted, #888);
    --fg-dim:    var(--user-fg-dim,   #555);
    --fg-deep: #3a3a3a;

    /* Accent + variants for hover / muted (selected-row bg etc.). */
    --accent: var(--user-accent, #22d3ee);
    --accent-hover: #5fe3f3;
    --accent-muted: color-mix(in srgb, var(--accent) 18%, transparent);

    /* Rules / borders. `--rule-hot` is the focus-ring + accent-border variant. */
    --rule: var(--user-rule, #2a2a2a);
    --rule-hot: var(--accent);

    /* Status colors. */
    --status-ok: #5fcf80;
    --status-warn: #e8c060;
    --status-err: #e06060;

    /* Panel/card chrome — promoted from per-page CSS so theming is single-source. */
    --panel-bg: #181818;
    --panel-border: #2a2a2a;
    --row-hover: #1f1f1f;
    --btn-bg: #232323;
    --btn-bg-hover: #2a2a2a;
    --btn-border: #353535;

    /* Shadow + scrim recipes. */
    --shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 4px 18px rgba(0, 0, 0, 0.4);
    --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
    --scrim: rgba(0, 0, 0, 0.55);
    --scrim-strong: rgba(0, 0, 0, 0.75);
}
[data-theme="light"] {
    --bg:          var(--user-bg,          #fafafa);
    --bg-raised:   var(--user-bg-raised,   #f0f0f0);
    --bg-elevated: var(--user-bg-elevated, #ffffff);

    --fg:        var(--user-fg,       #141414);
    --fg-soft:   var(--user-fg-soft,  #3a3a3a);
    --fg-muted:  var(--user-fg-muted, #5a5a5a);
    --fg-dim:    var(--user-fg-dim,   #a0a0a0);
    --fg-deep: #c8c8c8;

    --accent: var(--user-accent, #0e7490);
    --accent-hover: #0b5e74;
    --accent-muted: color-mix(in srgb, var(--accent) 12%, transparent);

    --rule: var(--user-rule, #d8d8d8);
    --rule-hot: var(--accent);

    --status-ok: #2f7a3f;
    --status-warn: #b45309;
    --status-err: #b91c1c;

    --panel-bg: #ffffff;
    --panel-border: #d8d8d8;
    --row-hover: #f0f0f0;
    --btn-bg: #f1f1f1;
    --btn-bg-hover: #e7e7e7;
    --btn-border: #c8c8c8;

    --shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.08);
    --shadow-md: 0 4px 18px rgba(0, 0, 0, 0.12);
    --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.18);
    --scrim: rgba(0, 0, 0, 0.35);
    --scrim-strong: rgba(0, 0, 0, 0.55);
}

/* Shared design tokens — type, motion, fonts. Same in both palettes. */
:root {
    --font-mono: Consolas, "SF Mono", Menlo, "Courier New", monospace;
    --font-brand: "JetBrains Mono", Consolas, "SF Mono", Menlo, monospace;

    /* Type scale. 7 steps cover every label / body / heading on the site. */
    --text-xs:   11px;
    --text-sm:   13px;
    --text-base: 14px;
    --text-lg:   16px;
    --text-xl:   18px;
    --text-2xl:  22px;
    --text-3xl:  28px;

    /* Motion. `fast` = hover/color changes, `med` = panels/drawers, `slow` = full-screen reveals. */
    --dur-fast: 0.18s;
    --dur-med:  0.32s;
    --dur-slow: 0.5s;

    /* Easing. `smooth` = iOS deceleration (default), `spring` = overshoot on entry,
       `quick` = Material standard for small UI flips. */
    --ease-smooth: cubic-bezier(0.32, 0.72, 0, 1);
    --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
    --ease-quick:  cubic-bezier(0.4, 0, 0.2, 1);

    /* Control heights — every interactive row-level element points
       at one of these tokens so any two elements in the same flex
       row line up vertically. Two tiers:
         --control-h         : 32 px — topbar buttons, search input,
                                grid/list toggle, select-bar actions
         --control-h-header  : 38 px — site header (Tools dropdown,
                                theme toggle, Settings gear, mobile
                                hamburger)
       Combine with `box-sizing: border-box` on the consumer so
       padding + border are absorbed into the fixed height. */
    --control-h:        32px;
    --control-h-compact: 28px;   /* dense secondary actions (panel toolbars, segmented pills, overlay row actions) */
    --control-h-header: 38px;
}

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

/* Background-tab efficiency. When document.visibilityState flips to
   `hidden`, layout-nav.js sets `<html data-tab-hidden>`. Pausing all
   CSS animations + transitions while the user isn't looking saves
   continuous render work (marquees, gradient sweeps, button flashes
   that happen to be mid-flight on the moment of tab-switch). Audio
   playback continues — MediaSession + <audio> are entirely
   independent of render state. iOS Safari fully suspends JS on tab
   switch so this is mostly a desktop win (Chrome / Firefox / Edge
   throttle background tabs less aggressively than mobile WebKit). */
html[data-tab-hidden] *,
html[data-tab-hidden] *::before,
html[data-tab-hidden] *::after {
    animation-play-state: paused !important;
    transition: none !important;
}

/* Respect the OS "reduce motion" setting (macOS/iOS Accessibility, Windows). We keep
   color/opacity fades near-instant rather than fully off so state changes still read,
   but kill movement, looping animations (marquees, spinners) and smooth-scroll. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
    html { scroll-behavior: auto; }
}

html, body {
    background: var(--bg);
    color: var(--fg);
    font-family: var(--font-mono);
    font-size: 15px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    transition: background var(--dur-fast) var(--ease-smooth), color var(--dur-fast) var(--ease-smooth);
}

body {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

a {
    color: var(--fg);
    text-decoration: none;
    border-bottom: 1px dotted var(--fg-dim);
    transition: color var(--dur-fast) var(--ease-smooth), border-color var(--dur-fast) var(--ease-smooth);
}
a:hover { color: var(--accent); border-bottom-color: var(--accent); }

main {
    flex: 1;
    min-width: 0;
    max-width: 880px;
    width: 100%;
    margin: 0 auto;
    padding: 2.5rem 2rem 5rem;
}

/* ---------------- Site header (always-visible row) ----------------
   Unified at every breakpoint: hamburger + brand only. Kept slim —
   the only chrome that lives here is the hamburger; everything else
   moved into the drawer. iOS-only safe-area floor lives in the
   html.ios override further down. */
.site-header {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding:
        max(0.3rem, env(safe-area-inset-top))
        max(0.6rem, env(safe-area-inset-right))
        0.3rem
        max(0.75rem, env(safe-area-inset-left));
    border-bottom: 1px solid var(--rule);
    background-color: var(--bg);
    transition: border-color var(--dur-fast) var(--ease-smooth), background-color var(--dur-fast) var(--ease-smooth);
}
.site-header .brand {
    font-family: var(--font-brand);
    font-size: 0.9rem;
    text-transform: lowercase;
    letter-spacing: 0.12em;
    color: var(--fg);
    border-bottom: none;
}
.site-header .brand .dot { color: var(--accent); }
.site-header .brand:hover { color: var(--accent); }

/* Hamburger — borderless, just the icon. Visual size 36×36 for a
   slim header; tap target effectively 40×40 once the surrounding row
   padding contributes — still inside Apple/Material thumb-friendly
   territory. No border, no background — color change on hover/active
   is the only visual feedback (matches the rest of the drawer's
   minimal chrome). Pinned to the right edge of the header via
   margin-left: auto so the brand (which comes before it in markup)
   can sit comfortably on the left. */
.menu-toggle {
    display: flex;
    width: 36px;
    height: 36px;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    border-radius: 6px;
    padding: 0;
    margin-left: auto;
    color: var(--fg-muted);
    cursor: pointer;
    flex-shrink: 0;
    transition: color var(--dur-fast) var(--ease-smooth),
                background var(--dur-fast) var(--ease-smooth);
}
.menu-toggle:hover,
.menu-toggle:active {
    color: var(--accent);
    background: var(--row-hover);
}
.menu-toggle:focus { outline: none; }
.menu-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Three-line hamburger that morphs into an X when open. */
.menu-icon {
    position: relative;
    display: block;
    width: 18px;
    height: 14px;
}
.menu-icon::before,
.menu-icon::after,
.menu-icon-bar {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    height: 2px;
    background: currentColor;
    border-radius: 1px;
    transition:
        transform var(--dur-med) var(--ease-smooth),
        opacity var(--dur-fast) var(--ease-smooth);
}
.menu-icon::before { top: 0; }
.menu-icon-bar     { top: 50%; transform: translateY(-50%); }
.menu-icon::after  { bottom: 0; }
[data-mobile-nav="open"] .menu-icon::before { transform: translateY(6px) rotate(45deg); }
[data-mobile-nav="open"] .menu-icon-bar     { opacity: 0; }
[data-mobile-nav="open"] .menu-icon::after  { transform: translateY(-6px) rotate(-45deg); }

/* ---------------- Site footer ----------------
   Document pages (/, /privacy, /terms, /settings) show it at the bottom.
   Full-viewport tool pages (/musiced, /clipped) hide it explicitly —
   the body's `overflow: hidden` doesn't clip flex children, so the
   footer was eating vertical space under the player / bottom-nav on
   mobile until we added the `display: none` rule below. */
body:has(.music-app) .site-footer,
body.musiced .site-footer,
body:has(.clipped-app) .site-footer {
    display: none;
}

/* ---------------- Cross-page mini-player (#6 — Option A) ----------------
   Small persistent strip at the bottom of every non-/musiced page that
   surfaces what's playing on Musiced + a one-tap return-and-resume.
   Audio still pauses during the navigation itself (full HTML page
   reload destroys the <audio> element); the click of the play button
   navigates to /musiced#resume which the music app picks up and uses
   to auto-start playback at the saved position.

   Hidden on:
     - /musiced (the page has its own full player chrome)
     - /clipped (full-viewport video tool, no room for layered chrome)

   On document pages (/, /privacy, /terms, /settings) it sits fixed to
   the viewport bottom above the footer's natural-flow position. iOS
   safe-area padding is applied so it clears the home-bar indicator. */
.mini-player {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding:
        0.5rem
        max(0.85rem, env(safe-area-inset-right))
        max(0.5rem, env(safe-area-inset-bottom))
        max(0.85rem, env(safe-area-inset-left));
    background: color-mix(in srgb, var(--bg) 92%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-top: 1px solid var(--panel-border);
    font-size: 0.82rem;
    /* Force a compositing layer so iOS Safari renders the fixed
       element on its own GPU layer. Without this, scroll on iOS makes
       the bar jitter / jump up and down as the URL bar collapses
       because the layout layer repaints out of sync with the scroll
       gesture. Translate3d + will-change is the standard fix.  */
    transform: translate3d(0, 0, 0);
    -webkit-transform: translate3d(0, 0, 0);
    will-change: transform;
}
.mini-player[hidden] { display: none; }
/* Firefox Android over-reports `env(safe-area-inset-bottom)` for
   fixed-position elements, leaving the mini-player with conspicuously
   more bottom padding than the same chrome on Chrome / Chrome Custom
   Tab. Clamp to the literal 0.5rem floor (no env contribution) on
   Firefox Android only; the gesture indicator is translucent so it
   overlapping the chrome's bottom edge by a hair reads cleanly. */
html.android-firefox .mini-player {
    padding-bottom: 0.5rem;
}
/* Hidden only on /musiced — that page has its own full player bar so a second
   strip on top of it would just be duplicate chrome. Edet KEEPS the mini-player
   (it's the only music control while writing). */
body:has(.music-app) .mini-player,
body.musiced .mini-player {
    display: none;
}
/* When the mini-player is visible, reserve its height as body bottom
   padding so the page content (including /clipped's Mark Out / Crop /
   Export buttons at the bottom of the Controls panel) doesn't sit
   behind it. EXCLUDES /musiced only — that page has its own full
   player bar and the mini-player is display:none'd above; reserving
   padding for an invisible bar would leave a gap at the bottom of the
   music app grid.

   The reservation MUST equal the bar's RENDERED height, or full-viewport
   pages (any future 100dvh tool whose content reaches the bottom) show a
   strip of body background between their content and the bar. `49px` =
   border-top(1) + padding-top(8) + content(40 = glyph / play-button height);
   the `max(8px, env(...))` mirrors the bar's own bottom padding so it stays
   flush as the iOS safe-area grows. (A flat 68px over-reserved by 11px on
   desktop — see CONTEXT.md § 15.) If the glyph/play size changes, update the
   49px base here. Excludes /musiced — it hides the bar above. */
body:not(.musiced):has(.mini-player:not([hidden])) {
    padding-bottom: calc(49px + max(8px, env(safe-area-inset-bottom)));
}
/* Firefox Android forces the bar's bottom padding to a flat 0.5rem
   (see html.android-firefox .mini-player above), so its bar is a flat
   57px regardless of env — reserve exactly that to stay flush. */
html.android-firefox body:not(.musiced):has(.mini-player:not([hidden])) {
    padding-bottom: 57px;
}

/* Hide the footer when the mini-player is showing — the two were
   stacking into two-bar chrome at the bottom, which felt heavy.
   Privacy/Terms remain reachable from the hamburger drawer in this
   state. When no track is playing, the mini-player is hidden and the
   footer resumes its normal role at the bottom of every doc page. */
body:has(.mini-player:not([hidden])) .site-footer {
    display: none;
}

/* ---------------- Edet "glass chrome" (EDET ONLY) ----------------
   In Edet, the editor is a canvas/desk you arrange cards on, so every surrounding
   chrome surface goes translucent — you see the desk through it — while the page
   (paper) and the board cards stay fully opaque (they're the content). ONE even
   level across every surface via --edet-glass, with a backdrop blur for legibility.
   Scoped to body.edet so the global header + mini-player are only translucent here;
   everywhere else they stay solid. */
/* ONE glass recipe for every chrome surface: a "cloudy" frosted look — half-opaque
   + a soft backdrop blur — so the controls read as controls while cards/paper still
   show softly behind. Tune both here and EVERY bar updates in lockstep. */
body.edet { --edet-glass: color-mix(in srgb, var(--bg) 55%, transparent); --edet-blur: blur(9px); }
/* In grid mode the canvas spills to the whole viewport (see edet-tiptap.css); the
   body is the single clip so there are no scrollbars, and every chrome bar floats
   ABOVE the canvas (higher z-index) + translucent, so cards/photos/page show through
   ALL of them — header, toolbar, sidebar, mini-player. */
/* clamp BOTH html + body so the canvas spill (it's beyond main now) can never turn
   into a page scrollbar — the spill is purely visual, clipped at the viewport. */
html:has(body.edet[data-edet-desk="grid"]) { overflow: hidden; height: 100%; }
/* drop the mini-player's bottom reservation in grid mode — the player now floats
   translucent over the canvas, so the editor (and the sidebar) fill the FULL height
   and meet the player flush (no gap between the sidebar and the mini-player). */
body.edet[data-edet-desk="grid"] { overflow: hidden; padding-bottom: 0 !important; }
/* the semantic <main> normally clips Edet (overflow:hidden); open it in grid mode so
   the canvas can spill ABOVE main (under the header) and BELOW it (under the
   mini-player). html+body are the viewport clip, so still no scrollbars. */
body.edet[data-edet-desk="grid"] main { overflow: visible; }
body.edet[data-edet-desk="grid"] .site-header { position: relative; z-index: 60; }

/* ONE shared frost behind ALL the contiguous chrome, so adjacent bars read as a
   SINGLE continuous frosted surface — not stacked per-element frosts that seam at
   their edges + vary over different cards. Two fixed panes carry it:
     ::before = the top horizontal zone (header + toolbar + scenes banner), full width
     ::after  = the left sidebar BELOW that zone
   The bars themselves go transparent and just sit over this shared frost. --edet-top-h
   (set by edet-desk.js) is the live height of the top zone (grows with the banner). */
body.edet[data-edet-desk="grid"]::before,
body.edet[data-edet-desk="grid"]::after {
    content: "";
    position: fixed;
    z-index: 6;                  /* above the canvas (cards), below the bars */
    pointer-events: none;
    background: var(--edet-glass);
    backdrop-filter: var(--edet-blur);
    -webkit-backdrop-filter: var(--edet-blur);
}
body.edet[data-edet-desk="grid"]::before { top: 0; left: 0; right: 0; height: var(--edet-top-h, 96px); }
body.edet[data-edet-desk="grid"]::after  { top: var(--edet-top-h, 96px); left: 0; width: 260px; bottom: 0; }
/* Collapsed sidebar (desktop icon rail): the shared left frost pane is hardcoded to the
   expanded 260px — shrink it to the rail width so the FROST tracks the rail (a thin frosted
   strip) instead of leaving a 260px frosted ghost over the desk. The rail itself is the same
   transparent .dr-sidebar over this pane, so it reads as frosted glass like every other bar.
   Keep 52px in sync with edet-sidebar.css `.edet-app.sidebar-collapsed { --dr-sidebar-w: 52px }`. */
body.edet[data-edet-desk="grid"]:has(.edet-app.sidebar-collapsed)::after { width: 52px; }
/* While panning, the canvas slides behind the frost; re-blurring it every frame is
   what made the chrome (and the grid behind it) jerk. Pause the blur during the pan —
   it snaps back the instant you let go. */
body.edet-panning::before,
body.edet-panning::after,
body.edet-panning .mini-player {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
}
/* when the mini-player shows, stop the sidebar frost AT the player so they don't
   double up at the intersection (the player carries the frost for that strip). */
body.edet[data-edet-desk="grid"]:has(.mini-player:not([hidden]))::after { bottom: 55px; }
/* Focus mode = clean distraction-free desk: the sidebar/toolbar/aux are all hidden, so
   their frosted backing panes (top body::before, left body::after, right .edet-app::after)
   would otherwise paint stray blurred rectangles + a stray top rule. Suppress all of them. */
body.edet-focus::before,
body.edet-focus::after,
body.edet-focus .edet-app::after { display: none !important; }
/* The RIGHT aux panel (Notes/Comments/History/Inspector) is the twin of the sidebar:
   the aux column goes transparent in grid mode (see the transparent-!important rule
   below) and takes ALL its frost from THIS fixed backing pane — the right-side mirror
   of body::after. One single frosted layer, exactly like the sidebar (transparent
   column over body::after). Only when a panel is open, grid desk, desktop (the aux is
   a 300px = --dr-notes-w column there). */
@media (min-width: 721px) {
    body.edet[data-edet-desk="grid"] .edet-app:not([data-aux="none"])::after {
        content: "";
        position: fixed;
        z-index: 6;
        pointer-events: none;
        top: var(--edet-top-h, 96px);
        right: 0;
        width: 300px;
        bottom: 0;
        background: var(--edet-glass);
        backdrop-filter: var(--edet-blur);
        -webkit-backdrop-filter: var(--edet-blur);
    }
    body.edet[data-edet-desk="grid"]:has(.mini-player:not([hidden])) .edet-app:not([data-aux="none"])::after { bottom: 55px; }
}
body.edet-panning .edet-app::after,
body.edet-panning .dr-aux {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
}
/* the bars are transparent — their frost comes from the shared ::before/::after.
   The scenes corkboard joins the top zone (its bottom feeds --edet-top-h), so it's
   transparent too and reads as one continuous frost with the toolbar above it.
   The RIGHT aux panel (.dr-aux) is in this list for the SAME reason: it goes
   transparent and takes its frost ONLY from the .edet-app::after backing pane (the
   right-side mirror of body::after). That makes it a SINGLE frost layer — exactly
   like the sidebar — instead of double-frosting (own --edet-glass + the pane), which
   read darker than the rest of the chrome. One layer everywhere = one consistent tint. */
body.edet[data-edet-desk="grid"] .site-header,
body.edet[data-edet-desk="grid"] .dr-chrome,
body.edet[data-edet-desk="grid"] .dr-sidebar,
body.edet[data-edet-desk="grid"] .dr-aux,
body.edet[data-edet-desk="grid"] .dr-readonly-banner,
body.edet[data-edet-desk="grid"] .dr-suggest-bar,
body.edet[data-edet-desk="grid"] .dr-corkboard {
    background: transparent !important;
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
}
body.edet .site-header { border-bottom: none; }

/* ONE horizontal rule under the top zone, matched on BOTH sides. The toolbar
   (.dr-chrome, editor side) and the aux head (notes/history/comments/inspector, right
   side) start at the same y and are both --dr-chrome-h tall, so their shared
   border-bottom lands on the exact same line (y = --edet-top-h) — continuous across
   the seam, no 1px step. The aux/sidebar have NO vertical dividers (the frost separates
   them), so this horizontal rule is the only line in the chrome. Grid desk only. */
body.edet[data-edet-desk="grid"] .dr-chrome,
body.edet[data-edet-desk="grid"] .dr-notes-head,
body.edet[data-edet-desk="grid"] .dr-history-head,
body.edet[data-edet-desk="grid"] .dr-comments-head {
    border-bottom: 1px solid var(--panel-border);
}
body.edet[data-edet-desk="grid"] .dr-notes-head,
body.edet[data-edet-desk="grid"] .dr-history-head,
body.edet[data-edet-desk="grid"] .dr-comments-head {
    height: var(--dr-chrome-h);
    align-items: center;
}

/* the mini-player is a separate (bottom) bar — keeps its own matching frost */
body.edet .mini-player {
    background: var(--edet-glass);
    border-top: none;
    backdrop-filter: var(--edet-blur);
    -webkit-backdrop-filter: var(--edet-blur);
}
/* (The desk grid now lives INSIDE the transformed canvas — see edet-tiptap.css
   .dr-canvas-grid — so it pans/zooms in pixel-lock with the page and inherits the
   editor theme. The frost panes above still frost it behind the chrome.) */

.mini-player-info {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    color: var(--fg);
    text-decoration: none;
    border-bottom: none;
    overflow: hidden;
}
.mini-player-glyph {
    flex-shrink: 0;
    /* 40 × 40 — sized to read clearly on / + /clipped + /settings at
       mobile widths. (Previously 32 px; bumped so the album art
       actually carries its weight as a "what's playing" anchor.) */
    width: 40px;
    height: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--panel-bg);
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    color: var(--accent);
    font-size: 1.3rem;
    line-height: 1;
    position: relative;
    overflow: hidden;
}
/* Cover art overlays the ♪ glyph. The img is `hidden` until
   refreshMiniPlayer loads a blob URL from OPFS; once shown, it
   absolute-position-covers the glyph parent. If load fails or the
   track has no albumHash, hidden stays on and ♪ shows through. */
.mini-player-cover {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.mini-player-cover[hidden] { display: none; }
.mini-player-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.25;
}
.mini-player-title {
    color: var(--fg);
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.mini-player-artist {
    color: var(--fg-muted);
    font-size: 0.75rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.mini-player-play {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--accent);
    color: var(--bg);
    text-decoration: none;
    border: none;
    padding: 0;
    cursor: pointer;
    font: inherit;
    transition: filter var(--dur-fast) var(--ease-smooth),
                transform var(--dur-fast) var(--ease-smooth);
}
.mini-player-play:hover { filter: brightness(1.08); }
.mini-player-play:active { transform: scale(0.92); }
.mini-player-play:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Dismiss button — small muted ✕ to the right of the play button.
   Stops the music + hides the strip (handler in layout-nav.js). */
.mini-player-close {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 6px;
    background: transparent;
    color: var(--fg-muted);
    border: none;
    padding: 0;
    cursor: pointer;
    font: inherit;
    transition: background var(--dur-fast) var(--ease-smooth),
                color var(--dur-fast) var(--ease-smooth);
}
.mini-player-close:hover { background: var(--row-hover); color: var(--fg); }
.mini-player-close:active { transform: scale(0.92); }
.mini-player-close:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.site-footer {
    margin-top: auto;       /* pushes to bottom thanks to body's flex-column */
    padding:
        1rem
        max(1.5rem, env(safe-area-inset-right))
        max(1rem, env(safe-area-inset-bottom))
        max(1.5rem, env(safe-area-inset-left));
    border-top: 1px solid var(--panel-border);
    display: flex;
    align-items: center;
    gap: 1rem;
    font-size: 0.78rem;
    color: var(--fg-muted);
    background: var(--bg);
}
.site-footer-brand {
    font-family: var(--font-brand);
    text-transform: lowercase;
    letter-spacing: 0.1em;
    color: var(--fg-muted);
}
.site-footer-brand .dot { color: var(--accent); }
.site-footer-nav {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.site-footer-nav a {
    color: var(--fg-muted);
    border-bottom: none;       /* overrides global dotted-underline */
}
.site-footer-nav a:hover { color: var(--accent); }

/* iOS-only floor for Dynamic Island clearance. JS adds `html.ios` on
   any iOS browser. env() adds more when the OS reports a real inset
   value. Desktop browser narrowed for testing doesn't get this class
   so it keeps the tight default padding. Top floor is bumped above
   the base 0.3rem so the hamburger never crowds the notch/Dynamic
   Island on iOS browsers that don't report a meaningful env(). */
html.ios .site-header {
    padding:
        max(0.5rem,  env(safe-area-inset-top))
        max(0.85rem, env(safe-area-inset-right))
        0.5rem
        max(0.85rem, env(safe-area-inset-left));
}

/* ---------------- Side drawer (all widths) ----------------
   Slides in from the right at every breakpoint when the hamburger is
   tapped. Anchored to the right edge so the drawer's close X sits at
   the same screen-right position as the hamburger that opened it —
   one visual toggle, one consistent target. Carries Tools (collapsible),
   Settings, Theme pill, and a Privacy · Terms footer row. */
.site-drawer {
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: 300px;
    max-width: 85vw;
    background: var(--bg);
    border-left: 1px solid var(--rule);
    z-index: 110;
    transform: translateX(100%);
    /* IMPORTANT: box-shadow lives in the open-state selector below,
       not here. The drawer is positioned `right: 0` with
       `transform: translateX(100%)` to hide off-screen — but a
       box-shadow with a negative horizontal offset + blur (e.g.
       `-4px 0 24px`) casts ~28 px BACK into the viewport from the
       drawer's left edge, which (when the drawer is off-screen)
       lands inside the visible page as a faint dark haze along the
       right edge. Permanent, on every route. Apply the shadow only
       when the drawer is actually on-screen. */
    transition:
        transform var(--dur-med) var(--ease-smooth),
        border-color var(--dur-fast) var(--ease-smooth),
        background-color var(--dur-fast) var(--ease-smooth),
        box-shadow var(--dur-med) var(--ease-smooth);
}
[data-mobile-nav="open"] .site-drawer {
    transform: translateX(0);
    box-shadow: -4px 0 24px rgba(0, 0, 0, 0.4);
}

/* Drawer head — mirrors the site-header geometry so the bottom border
   lands at exactly the same Y as the site-header's, giving one
   continuous horizontal line across the viewport when the drawer is
   open. Brand left, close button right; close button is mirrored to
   the hamburger (same 36×36 borderless icon, same distance from the
   adjacent edge). iOS top-floor matches the site-header override
   below. */
.site-drawer-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding:
        max(0.3rem, env(safe-area-inset-top))
        max(0.6rem, env(safe-area-inset-right))
        0.3rem
        max(0.6rem, env(safe-area-inset-left));
    border-bottom: 1px solid var(--rule);
    gap: 0.5rem;
}
html.ios .site-drawer-head {
    padding-top: max(0.5rem, env(safe-area-inset-top));
    padding-bottom: 0.5rem;
}
.site-drawer-head .brand {
    font-family: var(--font-brand);
    font-size: 0.9rem;
    text-transform: lowercase;
    letter-spacing: 0.12em;
    color: var(--fg);
    border-bottom: none;
    padding-left: 0.3rem;          /* visually nudges brand off the corner */
}
.site-drawer-head .brand .dot { color: var(--accent); }
.site-drawer-head .brand:hover { color: var(--accent); }

/* Close — geometric mirror of the hamburger (.menu-toggle). 36×36
   borderless icon button hosting an inline 18 × 18 lucide-X SVG
   (same primitive the music queue's close button uses, see
   `.ms-queue-head #ms-queue-close` in music-queue.css). The svg's
   `stroke: currentColor` picks up the button's color, so hover →
   accent without touching the markup. */
.drawer-close {
    width: 36px;
    height: 36px;
    background: transparent;
    border: none;
    border-radius: 6px;
    padding: 0;
    color: var(--fg-muted);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: color var(--dur-fast) var(--ease-smooth),
                background var(--dur-fast) var(--ease-smooth);
}
.drawer-close:hover,
.drawer-close:active {
    color: var(--accent);
    background: var(--row-hover);
}
.drawer-close:focus { outline: none; }
.drawer-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.drawer-body {
    display: flex;
    flex-direction: column;
    padding: 0.3rem;
    flex: 1;
    min-height: 0;
}

/* All drawer rows share one vertical rhythm: 40 px tall, 0.3rem
   vertical + 0.6rem horizontal padding. Tighter than the iOS HIG
   44 px floor but still inside Material/Apple thumb-friendly
   territory for a list-of-items context. Keeping every row at the
   same height — Tools toggle, items, theme row — means the drawer
   reads as a single uniform stack instead of a varied-height set.
   See .drawer-tools-toggle, .drawer-item, .drawer-theme-row below. */

/* Tools row — section header that expands/collapses children. Label
   on the left, chevron pushed to the right via margin-left:auto. The
   chevron rotates 0° (▸) collapsed, 90° (▾) expanded. Children animate
   open/closed via the grid-rows 1fr ↔ 0fr trick (same primitive used
   in clipped.css's .how-to panel). */
.drawer-tools-toggle {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.3rem 0.6rem;
    min-height: 40px;
    color: var(--fg);
    font: inherit;
    font-size: 0.9rem;
    background: transparent;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    text-align: left;
    transition: color var(--dur-fast) var(--ease-smooth),
                background var(--dur-fast) var(--ease-smooth);
}
.drawer-tools-toggle:hover {
    color: var(--accent);
    background: var(--row-hover);
}
.drawer-tools-toggle:focus { outline: none; }
.drawer-tools-toggle:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}
.drawer-tools-chevron {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.drawer-tools-chevron::before {
    content: '\25B8';                  /* ▸ */
    display: inline-block;
    font-size: 0.85em;
    line-height: 1;
    color: var(--fg-muted);
    transition: transform var(--dur-fast) var(--ease-smooth),
                color var(--dur-fast) var(--ease-smooth);
}
.drawer-tools-toggle[aria-expanded="true"] .drawer-tools-chevron::before {
    transform: rotate(90deg);          /* becomes ▾ */
    color: var(--accent);
}
.drawer-tools-toggle:hover .drawer-tools-chevron::before {
    color: var(--accent);
}

.drawer-tools-list {
    display: grid;
    grid-template-rows: 1fr;
    transition: grid-template-rows var(--dur-med) var(--ease-smooth);
}
.drawer-tools-list[data-open="false"] {
    grid-template-rows: 0fr;
}
.drawer-tools-inner {
    overflow: hidden;
    min-height: 0;
}

.drawer-item {
    display: flex;
    align-items: center;
    padding: 0.3rem 0.6rem;
    min-height: 40px;
    color: var(--fg);
    font-size: 0.9rem;
    text-decoration: none;
    border-bottom: none;           /* overrides global dotted-underline */
    border-radius: 6px;
    transition: color var(--dur-fast) var(--ease-smooth),
                background var(--dur-fast) var(--ease-smooth);
}
.drawer-item:hover {
    color: var(--accent);
    background: var(--row-hover);
}
/* No active-route visual: aria-current="page" is still set by
   layout-nav.js (screen-reader semantics intact), but it does not
   paint the row — the drawer items all read identically regardless
   of where the user is. By design: the user knows what page they're
   on; redundant cyan on the row was noise. */
/* Tools children indent so they read as nested under the Tools
   section header. */
.drawer-item-child {
    padding-left: 1.5rem;
}

/* Account row — two stacked lines: the label + a live status (the signed-in
   email, or a "Sign in to sync" CTA). Makes the account surface discoverable
   from the main nav instead of only inside /settings. */
.drawer-account {
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: 0.1rem;
    min-height: 48px;
}
.drawer-account-main { font-size: 0.9rem; line-height: 1.2; }
.drawer-account-status {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    color: var(--fg-muted);
    transition: color var(--dur-fast) var(--ease-smooth);
}
.drawer-account[data-authed="true"] .drawer-account-status { color: var(--accent); }

/* Theme row — label on the left, sun/moon pill on the right. */
.drawer-theme-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.3rem 0.6rem;
    min-height: 40px;
}
.drawer-theme-label {
    color: var(--fg);
    font-size: 0.9rem;
}
/* Theme switcher — same primitive as the library's grid/list toggle
   (`.ms-mode-toggle`, 6 px radius, 1 px border, accent fill on the
   active half). A `::before` indicator slides between the light/dark
   halves so the active state animates smoothly. */
.drawer-theme-pill {
    position: relative;
    display: inline-flex;
    padding: 2px;
    border-radius: 6px;
    background: var(--btn-bg);
    border: 1px solid var(--btn-border);
}
.drawer-theme-pill::before {
    content: '';
    position: absolute;
    top: 2px;
    bottom: 2px;
    left: 2px;
    width: calc(50% - 2px);
    border-radius: 4px;
    background: var(--accent);
    transition: transform var(--dur-fast) var(--ease-smooth),
                background var(--dur-fast) var(--ease-smooth);
}
.drawer-theme-pill[data-active="dark"]::before {
    transform: translateX(100%);
}
.drawer-theme-opt {
    position: relative;            /* sit above the indicator */
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 28px;
    background: transparent;
    border: none;
    cursor: pointer;
    color: var(--fg-muted);
    font: inherit;
    transition: color var(--dur-fast) var(--ease-smooth);
}
.drawer-theme-opt[aria-pressed="true"] {
    color: var(--bg);
}
.drawer-theme-opt:focus { outline: none; }
.drawer-theme-opt:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 4px;
}
.drawer-theme-opt svg { display: block; }

/* Divider between the theme row and the footer Privacy/Terms row. */
.drawer-divider {
    border: 0;
    border-top: 1px solid var(--rule);
    margin: 0.3rem 0.6rem;
}

/* Footer row — Privacy and Terms on the same row, separated by a
   middot, pinned to the very bottom of the drawer via
   margin-top:auto. Smaller and muted to signal footer-tier
   importance; safe-area-inset-bottom keeps them clear of the iOS
   home indicator. */
.drawer-footer {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    padding: 0.3rem 0.6rem max(0.5rem, env(safe-area-inset-bottom));
    font-size: 0.78rem;
    color: var(--fg-dim);
    margin-top: auto;              /* pin to bottom of drawer-body */
}
.drawer-footer-link {
    color: var(--fg-dim);
    text-decoration: none;
    border-bottom: none;
    padding: 0.3rem 0.4rem;
    border-radius: 4px;
    transition: color var(--dur-fast) var(--ease-smooth);
}
.drawer-footer-link:hover { color: var(--accent); }
/* See note above .drawer-item: no active-route paint, aria-current
   stays for screen-reader semantics. */
.drawer-footer-sep {
    color: var(--fg-deep);
    user-select: none;
}

/* Translucent backdrop. Clicking it closes the drawer.
   IMPORTANT: backdrop-filter intentionally lives ONLY in the open
   state below — Chrome (and Safari, less reliably) leaves
   backdrop-filter active on a fixed full-viewport element even when
   opacity: 0 + visibility: hidden, which slightly desaturates the
   whole page underneath all the time. Keeping the filter out of the
   base state means the dim + blur only paint when the drawer is
   actually open. The opacity transition still smooths the
   scrim's appearance; the blur snaps in alongside it (visually
   indistinguishable from a transition over 320 ms because the
   scrim is fading in at the same time). */
.overlay {
    display: block;
    position: fixed;
    inset: 0;
    background: var(--scrim);
    opacity: 0;
    visibility: hidden;
    z-index: 100;
    cursor: pointer;
    transition:
        opacity var(--dur-med) var(--ease-smooth),
        visibility 0s var(--dur-med);
}
[data-mobile-nav="open"] .overlay,
body[data-queue-open="true"] .overlay {
    opacity: 1;
    visibility: visible;
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    transition:
        opacity var(--dur-med) var(--ease-smooth),
        visibility 0s 0s;
}

/* Lock body scroll while the drawer is open. */
body[data-mobile-nav="open"] {
    overflow: hidden;
}

@media (max-width: 640px) {
    main { padding: 1.5rem 1rem 4rem; }
}

/* Hide brand text at very narrow widths so the header row never wraps. */
@media (max-width: 380px) {
    .site-header .brand { display: none; }
}

/* ---------------- Unified theme transition (applies during toggle) ----------------
   The !important is intentional: every paintable property has to ride the
   same curve while the theme swaps, regardless of any component-local
   transition rules. Without it, panels animate at their own speeds and
   the swap looks "stripey." */
.theme-switching,
.theme-switching *,
.theme-switching *::before,
.theme-switching *::after {
    /* Theme toggle should sweep every theme-aware property
       simultaneously — without this, panels animate at their own
       speeds and the swap looks "stripey." Box-shadow and outline-
       color are included because some panels use color-mix() with
       --bg in their shadows and the focus rings are accent-colored;
       leaving them out causes those bits to snap instantly while the
       rest fades, which reads as visual stutter. */
    transition:
        background-color var(--dur-fast) var(--ease-smooth),
        color var(--dur-fast) var(--ease-smooth),
        border-color var(--dur-fast) var(--ease-smooth),
        fill var(--dur-fast) var(--ease-smooth),
        stroke var(--dur-fast) var(--ease-smooth),
        box-shadow var(--dur-fast) var(--ease-smooth),
        outline-color var(--dur-fast) var(--ease-smooth) !important;
}
