:root{
  --bg:#f4f6f9; --bg-alt:#e7ecf3; --hdr-tint:#dbe1ec; --card:#ffffff; --ink:#101828; --ink-soft:#64748b;
  --line:rgba(16,24,40,0.1); --accent:#16305c; --accent-dark:#0e2244; --olive:#3f5e82; --vinyl:#0e1c33;
  --danger:#b3261e; --placeholder:#a89a89; --success:#1a7a4c;
  --suggest-bg:rgba(22,48,92,0.08); --suggest-border:rgba(22,48,92,0.35); --overlay-bg:rgba(255,255,255,0.88);
  /* Fuzzy near-duplicate warning (TODO.md item 38, 2026-08-23) — a step
     below --danger/--suggest-bg: not "this is an error" or "the app is
     suggesting an action", but "worth a second look before you commit". A
     warm amber, distinct from both --danger's red and --accent's navy. */
  --warn-bg:rgba(180,120,20,0.1); --warn-border:rgba(180,120,20,0.4); --warn-ink:#8a5a10;
  /* Admin Updates badge tints (2026-08-24) — Announcement reuses
     --suggest-bg/border above (it's already the accent tint); Maintenance
     reuses --warn-bg/border/ink above (already the right "heads up" amber).
     These two are the only genuinely new tints: New feature (green, off
     --success) and Fix (muted blue-gray, off --olive). */
  --feature-bg:rgba(26,122,76,0.1); --feature-border:rgba(26,122,76,0.35);
  --fix-bg:rgba(63,94,130,0.1); --fix-border:rgba(63,94,130,0.35);
  /* Badge family (TODO.md item 97, 2026-09-01) — one shape (mono, radius 5,
     3x9), the fill colour carries the role. System labels (release format,
     show type, release type) are filled + UPPERCASE; user words (tag,
     companion) filled + as-typed; a line-up artist is an outline only. Colours
     lifted from the old per-value badges: format/show-type = old DVD slate,
     release-type = old Concert navy, tag = old Musical purple, companion =
     old Cabaret rose, festival day-count = old Other grey. */
  --bdg-system:#3f5e82; --bdg-reltype:#16305c; --bdg-tag:#6b4c9a; --bdg-companion:#9c4b6b; --bdg-count:#6b7280;
  /* Soft slate — the show page line-up's "Also saw" chip (item 97 follow-up):
     a filled tint between "Came for" (solid --bdg-system) and "Didn't see"
     (plain outline). Both inputs are theme-aware tokens so this auto-themes
     with no dark/light override, same trick as --wm-* below. */
  --bdg-system-soft:color-mix(in srgb, var(--bdg-system) 30%, var(--card));
  --bdg-system-line:color-mix(in srgb, var(--bdg-system) 45%, var(--card));
  /* Shows-tijdlijn (fase 9): jaartal/datum in het verleden iets lichter dan
     de titel maar iets donkerder dan een toekomstige datum (--ink-soft) —
     twee tinten grijs, geen nieuwe kleur (the owner, 2026-09-13). Theme-aware
     zonder eigen dark/light-overrides, zelfde --wm-*-truc hierboven. */
  --ink-mid:color-mix(in srgb, var(--ink) 60%, var(--ink-soft));
  /* Shows Map choropleth (TODO.md item 92) — a card→accent ramp. Both ends
     are already theme-aware tokens, so the whole scale auto-themes with no
     dark/light overrides: pale→navy in light, dark→sky-blue in dark. */
  --wm-water:color-mix(in srgb, var(--accent) 13%, var(--card));
  --wm-0:color-mix(in srgb, var(--ink) 20%, var(--card));
  --wm-border:color-mix(in srgb, var(--ink) 32%, var(--card));
  --wm-1:color-mix(in srgb, var(--accent) 34%, var(--card));
  --wm-2:color-mix(in srgb, var(--accent) 55%, var(--card));
  --wm-3:color-mix(in srgb, var(--accent) 76%, var(--card));
  --wm-4:var(--accent);
  /* "1% of the true viewport height" (TODO.md item 75 follow-up,
     2026-08-26) — not a color, so it doesn't need repeating in the
     dark/light blocks below. This `1vh` default only ever applies for the
     instant before app.js's updateViewportUnit() runs (same value `Nvh`
     itself would've resolved to, so no visible flash either way) — that
     function overwrites it with a zoom-corrected px value once the text-
     size setting can be anything other than 100%. See its own comment for
     why a plain `Nvh` isn't safe to use directly wherever this app
     actually needs to cap something to a real fraction of the screen. */
  --vh: 1vh;
}
/* Dark mode: follows the OS setting by default (prefers-color-scheme), or
   the user's explicit choice via the header toggle (data-theme, set in
   app.js and persisted to localStorage) — the attribute selectors below
   override the media query in both directions so an explicit choice always
   wins over the OS setting. Values aren't a straight inversion of the light
   palette: --accent especially needs to be lightened, since the navy from
   the light theme is nearly invisible on a near-black background. */
@media (prefers-color-scheme: dark){
  :root{
    --bg:#12161d; --bg-alt:#1a2029; --hdr-tint:#232c39; --card:#1b212b; --ink:#e7ebf2; --ink-soft:#8b96a8;
    --line:rgba(255,255,255,0.1); --accent:#6f97d6; --accent-dark:#9dbceb; --olive:#8fa8c4; --vinyl:#162130;
    --danger:#f2887a; --placeholder:#8f8478; --success:#5fd996;
    --suggest-bg:rgba(111,151,214,0.14); --suggest-border:rgba(111,151,214,0.4); --overlay-bg:rgba(27,33,43,0.82);
    --warn-bg:rgba(224,168,80,0.14); --warn-border:rgba(224,168,80,0.4); --warn-ink:#e0a850;
    --bdg-system:#4d6d96; --bdg-reltype:#3a5c92; --bdg-tag:#7d5cb0; --bdg-companion:#b06788; --bdg-count:#5c6676;
  }
}
:root[data-theme="dark"]{
  --bg:#12161d; --bg-alt:#1a2029; --hdr-tint:#232c39; --card:#1b212b; --ink:#e7ebf2; --ink-soft:#8b96a8;
  --line:rgba(255,255,255,0.1); --accent:#6f97d6; --accent-dark:#9dbceb; --olive:#8fa8c4; --vinyl:#162130;
  --danger:#f2887a; --placeholder:#8f8478; --success:#5fd996;
  --suggest-bg:rgba(111,151,214,0.14); --suggest-border:rgba(111,151,214,0.4); --overlay-bg:rgba(27,33,43,0.82);
  --warn-bg:rgba(224,168,80,0.14); --warn-border:rgba(224,168,80,0.4); --warn-ink:#e0a850;
  --bdg-system:#4d6d96; --bdg-reltype:#3a5c92; --bdg-tag:#7d5cb0; --bdg-companion:#b06788; --bdg-count:#5c6676;
}
:root[data-theme="light"]{
  --bg:#f4f6f9; --bg-alt:#e7ecf3; --hdr-tint:#dbe1ec; --card:#ffffff; --ink:#101828; --ink-soft:#64748b;
  --line:rgba(16,24,40,0.1); --accent:#16305c; --accent-dark:#0e2244; --olive:#3f5e82; --vinyl:#0e1c33;
  --danger:#b3261e; --placeholder:#a89a89; --success:#1a7a4c;
  --suggest-bg:rgba(22,48,92,0.08); --suggest-border:rgba(22,48,92,0.35); --overlay-bg:rgba(255,255,255,0.88);
  --warn-bg:rgba(180,120,20,0.1); --warn-border:rgba(180,120,20,0.4); --warn-ink:#8a5a10;
  --bdg-system:#3f5e82; --bdg-reltype:#16305c; --bdg-tag:#6b4c9a; --bdg-companion:#9c4b6b; --bdg-count:#6b7280;
}
html,body{margin:0;padding:0;box-sizing:border-box}
*,*::before,*::after{box-sizing:border-box}
body{font-family:'IBM Plex Sans',sans-serif;background:var(--bg);color:var(--ink);min-height:100vh;width:100%;transition:background .15s ease,color .15s ease}
a{color:var(--accent);text-decoration:none}
a:hover{color:var(--accent-dark);text-decoration:underline}
::placeholder{color:var(--placeholder)}
/* appearance:none stops mobile Safari from mixing in its own native
   control chrome (colors included) on buttons that don't set every
   property themselves — see .choose-option above for the bug this caused. */
button{font-family:inherit;-webkit-appearance:none;appearance:none}
h1,h2,h3{font-family:'Space Grotesk',sans-serif;font-weight:800;margin:0}
.mono{font-family:'IBM Plex Mono',monospace}
.hidden{display:none !important}

@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.fade-up{animation:fadeUp 0.25s ease}

/* header */
/* Sticky wrapper around the header + the "viewing as" bar (TODO.md item
   104) — so the bar rides flush under the header with no offset math. */
.header-stack{position:sticky;top:0;z-index:50}
.header{background:var(--bg);border-bottom:2px solid var(--ink);
  display:flex;align-items:center;gap:20px;padding:14px clamp(16px,4vw,40px);flex-wrap:wrap}
.logo{display:flex;align-items:center;gap:10px;cursor:pointer}
/* Same photo as the app icon (favicon-*.png/apple-touch-icon.png/icon-*.png,
   picked 2026-08-22), reused here at 32px instead of redrawing it. This
   replaces the old CSS-only ring+dot mark (picked 2026-08-13) that redrew
   itself from theme tokens to re-theme automatically in dark mode; a static
   photo can't do that, so it now looks the same in both themes — same
   tradeoff the icon files themselves already made. */
.logo-mark{width:32px;height:32px;border-radius:10px;overflow:hidden;flex:none}
.logo-mark img{width:100%;height:100%;object-fit:cover;display:block}
.logo-text{font-family:'Space Grotesk',sans-serif;font-weight:800;font-size:24px;letter-spacing:0.5px;line-height:1;white-space:nowrap}
.nav{display:flex;gap:2px;margin-left:6px}
/* .icon-btn (defined below) also sets display:flex at equal (single-class)
   specificity and later in the cascade, which would otherwise win over a
   plain .mobile-nav-toggle{display:none} here — the extra .icon-btn in the
   selector bumps specificity so this override actually holds outside the
   mobile media query below. */
.icon-btn.mobile-nav-toggle{display:none}
.nav-btn{background:none;border:none;cursor:pointer;font-weight:600;font-size:14px;padding:8px 12px}
.nav-btn span{border-bottom:3px solid transparent;padding-bottom:1px;color:var(--ink-soft)}
.nav-btn.active span{border-bottom-color:var(--accent);color:var(--ink)}
.header-right{flex:1;min-width:180px;display:flex;justify-content:flex-end;gap:10px;align-items:center}
.search-wrap{position:relative;display:inline-flex;align-items:center}
.search-input{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:9px 30px 9px 16px;
  font-family:'IBM Plex Sans',sans-serif;font-size:13px;color:var(--ink);width:220px;outline:none}
.search-clear{position:absolute;right:6px;background:none;border:none;cursor:pointer;color:var(--ink-soft);
  font-size:17px;line-height:1;padding:4px;display:none}
.search-clear:hover{color:var(--ink)}
.search-wrap.has-text .search-clear{display:block}
/* Reusable "input with an inline ✕" (searchFieldHTML, TODO.md item 104) —
   same look as the header search box, for the Switch / Help Center / Find a
   connection / User management search fields. */
.search-field{position:relative;display:inline-block}
.search-field > input{width:100%;padding-right:30px}
.search-field-clear{position:absolute;right:4px;top:50%;transform:translateY(-50%);
  background:none;border:none;cursor:pointer;color:var(--ink-soft);font-size:17px;
  line-height:1;padding:4px;display:none}
.search-field-clear:hover{color:var(--ink)}
.search-field.has-text .search-field-clear{display:block}
/* Cross-page search dropdown (TODO.md item 35, 2026-08-23) — #search-dropdown
   sits empty until updateSearchDropdown() fills it with a .year-filter-panel
   (app.js), same anchored-dropdown shell every other header/filter panel
   already uses. Anchored off the right edge (rather than .year-filter-panel's
   own left:0 default, which would push it mostly off the right side of the
   viewport from here). */
.gsearch-panel{left:auto;right:0;width:320px;max-width:calc(100vw - 32px)}
.gsearch-panel .year-filter-panel-scroll{max-height:420px;padding:6px 0}
.gsearch-group-label{font-family:'IBM Plex Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;color:var(--ink-soft);padding:10px 14px 5px}
.gsearch-row{display:flex;align-items:center;gap:10px;padding:7px 14px;cursor:pointer}
.gsearch-row:hover{background:var(--bg-alt)}
.gsearch-thumb{width:28px;height:28px;flex:none}
.gsearch-row-title{font-weight:600;font-size:12.5px}
.gsearch-row-sub{font-size:11px;color:var(--ink-soft);margin-top:1px}
.gsearch-more{padding:6px 14px 9px;font-size:11px;font-weight:600;color:var(--accent);cursor:pointer}
.gsearch-more:hover{color:var(--accent-dark)}
.gsearch-empty{padding:16px 14px;font-size:12px;color:var(--ink-soft);text-align:center}
.btn-add{background:var(--accent);color:#fff;border:none;border-radius:8px;padding:10px 18px;
  font-weight:600;font-size:13px;cursor:pointer;white-space:nowrap}
.btn-add:hover{background:var(--accent-dark)}
.icon-btn{background:var(--card);border:1px solid var(--line);border-radius:8px;width:36px;height:36px;
  display:flex;align-items:center;justify-content:center;flex:none;cursor:pointer}
.icon-btn svg{width:18px;height:18px;stroke:var(--ink);fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.icon-btn:hover{border-color:var(--ink)}
.icon-btn:disabled{opacity:0.5;cursor:not-allowed}
/* Header Connections icon while on that page / in a viewing session
   (TODO.md item 104) — mirrors the nav-btn active treatment. */
.icon-btn.active{border-color:var(--accent);background:var(--suggest-bg)}
.icon-btn.active svg{stroke:var(--accent)}
.icon-btn.bell{position:relative}
.icon-btn.bell.has-unread::after{content:"";position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:var(--danger);border:1.5px solid var(--card)}
/* Upward-opening variant (TODO.md item 72 follow-up, 2026-08-26) — the
   link-choice popover (see catalogLinkIconHTML()/positionLinkChoicePopover())
   is the first .year-filter-panel user whose trigger can sit anywhere near
   the bottom of its bounding box (a list row near the fold, or low in a
   scrolled modal) rather than reliably having room to open downward, so it
   picks left/right *and* up/down at open-time based on the actual measured
   space instead of a fixed per-caller class like .right above. */
.year-filter-panel.flip-up{top:auto;bottom:calc(100% + 6px)}

/* Profile page: tabs stacked vertically on the left, selected tab's content
   on the right. Falls back to a horizontal, wrapping row on mobile — a
   sidebar squeezed next to narrow content reads worse than just stacking. */
.profile-layout{display:flex;gap:32px;align-items:flex-start}
.profile-sidebar{display:flex;flex-direction:column;gap:2px;min-width:190px;flex:none}
.profile-side-btn{display:block;width:100%;text-align:left;background:none;border:none;border-radius:6px;
  padding:10px 12px;font-size:13.5px;font-weight:600;color:var(--ink-soft);cursor:pointer;text-decoration:none}
/* text-decoration:none here overrides the global a:hover{text-decoration:
   underline} rule (these are <a> tags) — an underline flashing under the
   hovered item read as a link-in-body-text affordance this is not
   (2026-08-16 feedback, caught on the Concert detail section nav but this
   class is shared with Profile/Admin, so the fix applies everywhere). */
.profile-side-btn:hover{background:var(--bg-alt);color:var(--ink);text-decoration:none}
.profile-side-btn.active{background:var(--card);border:1px solid var(--line);color:var(--ink)}
.profile-content{flex:1;min-width:0}
@media (max-width:640px){
  /* align-items:flex-start above is for the desktop row layout (sidebar and
     content shouldn't stretch to match each other's height) — on mobile the
     axes swap (flex-direction:column), so that same flex-start started
     controlling *width* instead, shrinking the sidebar row and the content
     panels down to their own content width instead of filling the screen.
     stretch here undoes that for the column layout specifically. */
  .profile-layout{flex-direction:column;gap:20px;align-items:stretch}
  .profile-sidebar{flex-direction:row;flex-wrap:wrap;min-width:0;gap:4px}
  .profile-side-btn{width:auto;padding:7px 12px}
}
/* Account tabs (TODO.md item 103) — every section is its own .panel card,
   stacked with a gap. Reused as-is by Admin (fase 8) for the same reason —
   Data integrity's per-check panels, Feedback/Updates/Audit's single panel,
   User management's Browse-collections/Users/Account-history trio. The card
   column itself is full content width — every Account tab matches every Admin tab
   (the owner, 2026-09-03) — but the *form fields* inside stay capped so a text
   input never stretches the whole page (the original item-103 complaint).
   An inline max-width on a specific field always wins over the 480 here. */
.settings-stack{display:flex;flex-direction:column;gap:16px}
.settings-stack .form-input,
.settings-stack .form-grid{max-width:480px}
/* An input wrapped in a .search-field (the ✕-clear box) — cap the wrapper, not
   the input, so the ✕ stays pinned to the input's right edge. */
.settings-stack .search-field{max-width:480px}
.settings-stack .search-field .form-input{max-width:none}
@media (max-width:640px){
  .settings-stack .form-input,
  .settings-stack .form-grid,
  .settings-stack .search-field{max-width:none}
}
/* header-actions: plain inline row on desktop (its two children are just
   flex items of .header-right, same as before this element existed) —
   turned into a fixed bottom bar under the mobile breakpoint instead, see
   the media query below. */
.header-actions{display:flex;align-items:center}
.header-actions-icons{display:flex;align-items:center;gap:10px}
.header-version-link{display:none}
.current-user-name{font-size:13px;font-weight:600;color:var(--ink-soft);white-space:nowrap}

/* auth (setup/login screens) */
.auth-screen{min-height:calc(100vh - 200px);display:flex;align-items:center;justify-content:center}
.auth-card{width:100%;max-width:360px;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:32px 28px}
.auth-card h1{font-size:22px;margin-bottom:6px}
.auth-card p{font-size:13px;color:var(--ink-soft);margin:0 0 20px}
.auth-error{background:rgba(179,38,30,0.08);color:var(--danger);border-radius:6px;padding:10px 12px;font-size:12.5px;margin-bottom:14px}
.auth-success{background:rgba(26,122,76,0.08);color:var(--success);border-radius:6px;padding:10px 12px;font-size:12.5px;margin-bottom:14px}
/* Self-registration's informational (not error) notices — "not open
   registration", "waiting for approval" (TODO.md item 8, 2026-08-23).
   Reuses the --warn-* tokens item 38 already added for the fuzzy-match
   duplicate badge, same amber "worth noting, not wrong" register. */
.auth-notice{background:var(--warn-bg);color:var(--warn-ink);border:1px solid var(--warn-border);border-radius:6px;padding:10px 12px;font-size:12.5px;line-height:1.5}
.link-btn{background:none;border:none;padding:0;cursor:pointer;color:var(--accent);font-size:12.5px;text-decoration:underline}

/* Install-the-app affordance (TODO.md item 87) — login page (stacked under
   the auth card) and Account -> Notifications. .auth-stack lets the login
   screen center a card + a panel together instead of just one card. */
.auth-stack{width:100%;max-width:360px;display:flex;flex-direction:column;gap:14px}
.install-panel{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px 15px;display:flex;gap:12px;align-items:flex-start;position:relative}
.install-panel.install-panel-steps{flex-direction:column;gap:8px}
.ip-icon{width:30px;height:30px;border-radius:8px;background:var(--bg-alt);display:flex;align-items:center;justify-content:center;flex:none}
.ip-icon svg{width:17px;height:17px;stroke:var(--accent);fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.ip-icon-inline{display:inline-flex;vertical-align:-3px;margin-right:5px}
.ip-icon-inline svg{width:15px;height:15px;stroke:var(--accent);fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.ip-body{flex:1;min-width:0}
.ip-title{font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:12.5px;color:var(--ink)}
.ip-desc{font-size:11px;color:var(--ink-soft);line-height:1.45;margin-top:2px}
.ip-btn{margin-top:9px;background:var(--accent);color:#fff;border:none;border-radius:7px;padding:6px 14px;font-size:11.5px;font-weight:700;cursor:pointer}
.ip-dismiss{position:absolute;top:8px;right:9px;background:none;border:none;color:var(--ink-soft);font-size:15px;line-height:1;cursor:pointer;padding:2px}
.install-steps{display:flex;flex-direction:column;gap:7px;margin-top:2px}
.ip-step{display:flex;gap:9px;align-items:flex-start;font-size:11px;color:var(--ink);line-height:1.45}
.ip-num{width:18px;height:18px;border-radius:50%;background:var(--bg-alt);color:var(--accent);font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;flex:none;margin-top:1px}
.ip-step svg{width:14px;height:14px;stroke:var(--accent);fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;vertical-align:-2px;margin:0 1px}
.install-hint{font-size:10.5px;color:var(--ink-soft);line-height:1.5;padding:9px 30px 9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);position:relative}
.install-hint .link-btn{font-size:10.5px}

/* main */
.main{padding:clamp(20px,4vw,40px);max-width:1180px;margin:0 auto}

/* "Viewing as" bar (TODO.md item 104) — browsing a connection's collection.
   Amber = the app's existing --warn "heads up, not an error" tint. It lives
   inside the sticky .header-stack, right under the header, so it needs no
   positioning of its own. Opaque: --warn-bg is translucent, so it's
   composited over an explicit --bg layer, otherwise releases scrolling
   under it showed straight through (the owner, 2026-09-03). Padding matches
   the header's so the exit button lines up with the header icons. */
.viewing-bar:not(.hidden){background-color:var(--bg);
  background-image:linear-gradient(var(--warn-bg),var(--warn-bg));
  border-bottom:1px solid var(--warn-border);color:var(--warn-ink)}
.viewing-bar-inner{display:flex;align-items:center;gap:14px;
  padding:9px clamp(16px,4vw,40px);font-size:13px}
.viewing-bar-eye{flex:none}
.viewing-bar-who{flex:1 1 auto;min-width:0}
.viewing-bar-who strong{font-weight:600}
.viewing-bar-exit{margin-left:auto;flex:none;font-family:'IBM Plex Mono',monospace;
  font-size:11px;font-weight:600;color:inherit;background:transparent;
  border:1px solid currentColor;border-radius:6px;padding:4px 10px;cursor:pointer}
.viewing-bar-exit:hover{background:var(--warn-border)}
/* In-bar "Switch ▾" (TODO.md item 104 Phase 2) — hop between subjects
   without leaving the view. Same amber chrome as the exit button. */
.viewing-bar-switch-wrap{flex:none;margin-left:auto}
.viewing-bar-switch-wrap + .viewing-bar-exit{margin-left:8px}
.viewing-bar-switch{font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:600;
  color:inherit;background:transparent;border:1px solid currentColor;border-radius:6px;
  padding:4px 10px;cursor:pointer}
.viewing-bar-switch:hover{background:var(--warn-border)}
/* Desktop: the dropdown hangs off the button's right edge (button sits far
   right). Mobile rules below re-anchor it to the whole bar. */
.viewing-bar-switch-wrap .view-switch-menu{left:auto;right:0}
.viewing-bar .view-switch-menu{width:250px;max-width:calc(100vw - 24px);min-width:0;
  background:var(--card);border:1px solid var(--line);border-radius:8px;
  box-shadow:0 8px 26px rgba(16,24,40,.16);padding:6px;color:var(--ink);
  max-height:min(60vh,420px);overflow-y:auto;overflow-x:hidden}
.view-switch-row{display:flex;align-items:center;gap:8px;width:100%;text-align:left;
  background:transparent;border:none;border-radius:6px;padding:8px 10px;cursor:pointer;
  font-size:12.5px;color:var(--ink)}
.view-switch-row:hover{background:var(--bg-alt)}
.view-switch-row.active{background:var(--bg-alt);font-weight:600}
.view-switch-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.view-switch-tick{flex:none;color:var(--accent);font-weight:700}
.view-switch-sep{font-family:'IBM Plex Mono',monospace;font-size:9.5px;text-transform:uppercase;
  letter-spacing:.6px;color:var(--ink-soft);padding:8px 10px 4px}
.view-switch-menu .search-field{display:block;margin:2px 0 6px}
.viewing-bar .view-switch-search{width:100%;padding:7px 28px 7px 10px;font-size:12px}
.view-switch-userlist{max-height:230px;overflow-y:auto}
.view-switch-empty{font-size:12px;color:var(--ink-soft);padding:8px 10px}
@media (max-width:640px){
  /* Two lines: (1) eye + who (+ what's shared, inline — wraps if long),
     (2) Switch + Exit. .viewing-bar-who keeps its base flex:1 so it fills
     line 1 and pushes Switch/Exit onto line 2. */
  .viewing-bar-inner{flex-wrap:wrap;gap:6px 10px}
  .viewing-bar-eye{order:1}
  .viewing-bar-who{order:1}
  .viewing-bar-switch-wrap{order:2;margin-left:0}
  .viewing-bar-exit{order:2;margin-left:auto}
  /* The dropdown, anchored to the whole bar so a left-hanging 250px panel
     doesn't run off the screen edge. The .viewing-bar .. selector beats the
     generic .year-filter{position:relative} that would otherwise make the
     small button the containing block (and the menu ~36px wide). */
  .viewing-bar:not(.hidden){position:relative}
  .viewing-bar .viewing-bar-switch-wrap{position:static}
  .viewing-bar .viewing-bar-switch-wrap .view-switch-menu{position:absolute;left:12px;right:12px;
    top:calc(100% + 6px);width:auto;max-width:none}
}
/* "Favourites only" filter has no meaning on a connection's list — their
   favourites aren't shared (TODO.md item 104). updateHeader() sets the
   .viewing-as class on <html> for the length of a viewing session. */
html.viewing-as .fav-icon-toggle{display:none}
.view-add-row{display:inline-flex;align-items:center;gap:8px}
.view-add-label{font-size:12.5px;color:var(--ink-soft)}
/* Desktop counterpart to .header-version-link (mobile) — see index.html.
   Hidden below the mobile breakpoint (that's header-version-link's job
   instead); above it, a thin bar fixed to the bottom of the viewport.
   This block must stay *after* the .main rule above — it overrides just
   .main's padding-bottom, and needs to win the cascade against that
   rule's own (shorthand) padding, not the other way round.
   `and (hover:hover) and (pointer:fine)` (TODO.md item 9, 2026-08-25) keeps
   this off on a phone in landscape too, not just portrait — plain
   min-width can't tell a landscape phone (often >640px wide) apart from an
   actual desktop window, but a phone is touch-primary regardless of which
   way it's held. See the big mobile-layout block further down for the
   exact mirrored condition (a phone gets that treatment; this bar is the
   one thing that block doesn't itself cover, since it lives outside it). */
.desktop-version-bar{display:none}
@media (min-width: 641px) and (hover:hover) and (pointer:fine){
  .desktop-version-bar:not(.hidden){display:block;position:fixed;left:0;right:0;bottom:0;z-index:40;
    background:var(--bg);border:none;border-top:1px solid var(--line);text-align:center;
    font-size:11px;color:var(--ink-soft);padding:8px 0;cursor:pointer;font-family:inherit}
  .desktop-version-bar:not(.hidden):hover{color:var(--ink)}
  /* Content needs clearance so the fixed bar never covers the last item
     on a page — same reasoning as .main's mobile padding-bottom. */
  .main{padding-bottom:52px}
}
.eyebrow{font-family:'IBM Plex Mono',monospace;text-transform:uppercase;letter-spacing:1.5px;font-size:12px;color:var(--ink-soft);margin-bottom:6px}
.page-title{font-size:clamp(28px,4vw,42px);margin:0 0 24px}
/* Artists/Collection/Shows: page title + that page's own view-toggle
   (Grid/List/Table, Timeline/List) sharing one row instead of the toggle
   living inside .filterbar below — the toggle picks how the whole page below
   renders, so it reads more like a peer of the title than one filter among
   others. The rest of the toolbar (tags, format, sort, etc.) stays in its
   own row underneath. */
.page-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:18px}
/* min-width:0 on the title+subtext block (2026-09-16, mobile walkthrough)
   — a flex item's default min-width:auto floors its shrink at its own
   content's natural width, so a longer page-sub line (Releases/Shows with
   a filter active, Artists with a longer count string) was quietly
   widening this whole block past what .page-head had room for, tipping
   flex-wrap into dropping the Grid/List/Table toggle onto its own line —
   inconsistently, since it only happened once the subtext got long enough.
   min-width:0 lets the block (and the text inside it) actually shrink
   instead of forcing the row wide. */
.page-head > div:first-child{min-width:0}
.page-head .page-title{margin:0;font-size:clamp(26px,4vw,36px)}

/* stat cards */
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:16px;margin-bottom:32px}
/* container-type so .stat-value can query its own card's width (below) —
   inline-size containment only, no visual effect on the card itself. */
.stat-card{background:var(--card);border:1px solid var(--line);border-radius:6px;padding:20px;container-type:inline-size}
.stat-value{font-family:'Space Grotesk',sans-serif;font-weight:800;font-size:40px;line-height:1;white-space:nowrap}
/* Total Playtime (TODO.md item 94): the digits match the other three stat
   cards at 40px; only the "h"/"m" units drop to 22px. The minutes (.pt-min)
   are hidden by default and shown only when the card is wide enough to fit
   "79h 44m" (or "123h 45m") on one line — on a cramped phone grid where the
   card is ~180px the value reads "79h" at the full 40px instead of wrapping
   to two lines. If container queries aren't supported the value stays "79h"
   everywhere — a safe, if less precise, fallback. */
.stat-value .stat-unit{font-size:22px;font-weight:700;margin-left:1px}
.stat-value .pt-min{display:none}
/* 170px content-box clears "123h 45m" (161px measured); a 2-column phone
   grid (card content ~140px) stays below it and shows just "79h". */
@container (min-width: 170px){ .stat-value .pt-min{display:inline} }
.stat-label{font-family:'IBM Plex Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--ink-soft);margin-top:6px}

/* panels */
.panel-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:24px;margin-bottom:32px}
.panel{background:var(--card);border:1px solid var(--line);border-radius:6px;padding:22px}
/* View all sits top-right, beside the title (reverted 2026-08-25 — the
   under-title stack from earlier the same day is gone). flex-wrap is the
   safety net for a panel too narrow to fit title + link on one line: the
   gap floor alone isn't enough, so wrap lets View all drop to its own line
   instead of crowding the title, only on genuinely narrow panels. */
.panel-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;margin-bottom:16px;gap:4px 12px}
.panel-head h2{font-size:16px;font-weight:700}
.view-all{font-size:12px;font-weight:600}

/* Dashboard → Favorites (TODO.md item 94, reworked 2026-08-31) — 0-3 cards
   (Artists / Releases / Shows, each shown only if you've favourited any).
   Each card is a one-line list (.fav-mini-list) like the breakdown cards,
   so this uses the standard .panel-grid now. No cap, no scroll, no
   min-height (the owner's call): the card is exactly as tall as the number of
   favourites — taller than the breakdown cards for many, shorter for few —
   and align-items:start lets each card size to its own content with no
   whitespace and no stretch. Replaces the earlier flex-wrap + 50%-width-cap
   treatment and the horizontal photo scrollers. */
.panel-grid.favorites-grid{align-items:start}
/* Same 20px row + 6px gap as .artist-mini-row. */
.fav-mini-list{display:flex;flex-direction:column;gap:6px}
.fav-mini-row{display:flex;align-items:center;gap:10px;cursor:pointer;border-radius:4px;margin:0 -6px;padding:0 6px}
.fav-mini-row:hover{background:var(--bg-alt)}
.fav-mini-thumb{width:20px;height:20px;flex:none}
.fav-mini-thumb .image-slot{border-radius:3px;border-width:1px}
.fav-mini-thumb.artist-cover .image-slot{border-radius:50%}
.fav-mini-thumb .slot-icon{font-size:11px}
.fav-mini-date{font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--ink-soft);flex:none;width:34px;white-space:nowrap;font-variant-numeric:tabular-nums}
.fav-mini-text{flex:1;min-width:0;font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fav-mini-sub{color:var(--ink-soft);font-weight:400}

/* Shows Map view (TODO.md item 92, Phase 1) — world choropleth. */
.wm-caption{display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-size:13px;color:var(--ink-soft);margin-bottom:10px}
.wm-caption b{color:var(--ink);font-variant-numeric:tabular-nums}
.wm-clear{border:1.5px solid var(--accent);background:var(--suggest-bg);color:var(--accent);border-radius:999px;padding:3px 11px;font:inherit;font-size:12px;font-weight:600;cursor:pointer}
.wm-clear:hover{background:var(--accent);color:var(--card)}
/* Region chips above the map — one-tap reframe (Fit to my shows / World /
   a continent you've been to). TODO.md item 92 Phase 1 follow-up. */
.wm-regions{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
.wm-rchip{border:1px solid var(--line);border-radius:999px;padding:4px 12px;font:inherit;font-size:12px;font-weight:600;cursor:pointer;background:var(--bg);color:var(--ink-soft)}
.wm-rchip:hover{border-color:var(--ink-soft);color:var(--ink)}
.wm-rchip.active{background:var(--ink);color:var(--bg);border-color:var(--ink)}
.wm-rchip:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.wm-outlier-hint{font-size:12px;color:var(--ink-soft);margin-bottom:10px;line-height:1.5}
.wm-frame{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--wm-water);width:100%}
.wm-frame svg{display:block;width:100%;height:100%}
.wm-water{fill:var(--wm-water)}
.wm-country{fill:var(--wm-0);stroke:var(--wm-border);stroke-width:0.6;vector-effect:non-scaling-stroke;stroke-linejoin:round}
.wm-b1{fill:var(--wm-1)} .wm-b2{fill:var(--wm-2)} .wm-b3{fill:var(--wm-3)} .wm-b4{fill:var(--wm-4)}
.wm-hit{cursor:pointer;transition:opacity .12s}
.wm-hit:hover,.wm-hit:focus-visible{opacity:0.82;stroke:var(--ink);stroke-width:1.5;outline:none}
.wm-legend{display:flex;align-items:center;gap:10px;margin-top:10px;font-size:12px;color:var(--ink-soft)}
.wm-swatches{display:flex;gap:3px}
.wm-swatches i{width:22px;height:12px;border-radius:2px;display:block;border:1px solid var(--line)}

/* Add-a-show "Festival" field live hint (TODO.md item 93) — "joins Rock
   Werchter 2006" / "outside the window" / "starts a new festival". */
.festival-hint{font-size:12px;line-height:1.5;border-radius:6px;padding:8px 11px;margin:-4px 0 4px;border:1px solid var(--line)}
.festival-hint b{font-weight:600}
.festival-hint-join{background:var(--feature-bg);border-color:var(--feature-border)}
.festival-hint-near{background:var(--warn-bg);border-color:var(--warn-border);color:var(--warn-ink)}
.festival-hint-new{background:var(--suggest-bg);border-color:var(--suggest-border)}

/* horizontal bar-list — dashboard breakdown panels (formats, release
   types, venues, places, show types, festivals). Every breakdown card is
   capped at a top 5 (TODO.md item 93); a shorter list reserves the full
   5-row height so the cards line up, with blank space at the bottom. */
.bar-list{display:flex;flex-direction:column;gap:11px;min-height:123px}
.bar-row{display:grid;grid-template-columns:min(38%,120px) 1fr auto;gap:10px;align-items:center}
.bar-row-label{font-size:12.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bar-row-track{background:var(--bg-alt);border-radius:4px;height:9px;overflow:hidden}
.bar-row-fill{height:100%;background:var(--olive);border-radius:4px 0 0 4px;min-width:3px}
.bar-row-count{font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--ink-soft);text-align:right;min-width:18px}
/* Dashboard-only: Formats/Release Types/Show Types bars jump to that value
   pre-filtered on the matching page (see filterFromDashboard()) — Venues/
   Cities stay plain bar-list rows, no matching filter to jump to. */
.bar-row-clickable{cursor:pointer;border-radius:4px;margin:0 -6px;padding:0 6px}
.bar-row-clickable:hover{background:var(--bg-alt)}
.bar-row-clickable:hover .bar-row-fill{background:var(--accent)}

/* Scrollbar hidden — arrow buttons (.scroll-nav, in a .nav-wrap parent, see
   further down) are the click-driven way to reach hidden cards on desktop;
   touch keeps its native swipe. A wrapping (flex-wrap) layout was tried
   here for the two rows always capped at 5 items (Most collected artists,
   Most seen live) so they'd never need to scroll at all — the owner found
   wrapping to a 2nd/3rd row uglier than just scrolling, so all .hscroll
   rows use this same single-row-plus-arrows treatment now, capped or not. */
.hscroll{display:flex;gap:14px;overflow-x:auto;padding-bottom:4px;scrollbar-width:none}
.hscroll::-webkit-scrollbar{display:none}
.mini-card{flex:none;width:120px;cursor:pointer}
.mini-cover{width:120px;height:120px}
.mini-title{font-weight:600;font-size:12.5px;margin-top:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mini-artist{font-size:11.5px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Dashboard "Most collected artists" / "Most seen live" (TODO.md item 94) —
   a compact vertical top-5 with a 20px round photo, tuned to the same
   row pitch (~26px) and min-height as the .bar-list breakdown cards beside
   it, so the .panel-grid row ends up level with no card left with blank
   space at the bottom. The photos keep the Dashboard from being all text
   and bars. Same min-height + hover affordance as .bar-row-clickable. */
.artist-mini-list{display:flex;flex-direction:column;gap:6px;min-height:123px}
.artist-mini-row{display:flex;align-items:center;gap:10px;cursor:pointer;border-radius:4px;margin:0 -6px;padding:0 6px}
.artist-mini-row:hover{background:var(--bg-alt)}
.artist-mini-thumb{width:20px;height:20px;flex:none}
.artist-mini-thumb .image-slot{border-width:1px}
.artist-mini-thumb .slot-icon{font-size:11px}
.artist-mini-name{flex:1;min-width:0;font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.artist-mini-count{font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--ink-soft);min-width:18px;text-align:right}

/* Upcoming/Already attended section labels on the Shows List view (TODO.md
   item 53, 2026-08-21) — List has no year headings today the way Timeline
   does, so this is new rather than reusing .timeline-year, but kept in the
   same quiet, label-not-heading register as the rest of the app's mono-font
   small caps (.festival-count-chip, .mini-concert-date). */
.list-section-head{font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:600;color:var(--ink-soft);
  text-transform:uppercase;letter-spacing:0.5px;padding:16px 0 4px}
.list-section-head:first-child{padding-top:0}
.mini-concert{display:flex;gap:12px;align-items:baseline;padding:10px 0;border-bottom:1px solid var(--line);cursor:pointer}
.mini-concert:last-child{border-bottom:none}
/* Dashboard Recent/Upcoming shows (TODO.md item 94) — 3 two-line rows tuned
   to the same ~124px min-height as the .bar-list breakdown cards beside
   them, by dropping the roomy 10px row padding they don't need in a 3-row
   glance card. Scoped to --compact so the concerts List view and the
   Favorites "Favorite shows" list (plain .mini-concert) are untouched. */
.mini-show-list{min-height:123px}
.mini-concert--compact{padding:4px 0}
.mini-concert-date{font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--ink-soft);flex:none;width:64px}
/* Shows-page List view (miniConcertRowHTML / miniFestivalEditionRowHTML) —
   one date per line now (a multi-day festival stacks its dates, TODO.md item
   97 follow-up), so this only needs room for a single "Sep 18, 2024", not the
   old "Jun 29 – Jul 2, 2006" range. */
.mini-concert-date--range{width:100px;white-space:nowrap}
/* A multi-day festival row: dates stack, so align everything to the top
   instead of the single-line baseline. */
.mini-concert--multiday{align-items:flex-start}
.mini-concert--multiday .mini-concert-date{line-height:1.5}
.mini-concert-info{flex:1;min-width:0}
.mini-concert-artist{font-weight:600;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mini-concert-venue{font-size:11.5px;color:var(--ink-soft);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Companion chip(s) in the List row: inline before the Type badge on desktop
   (room for it there), stacked under the venue on mobile instead — same
   dual-element-toggled-by-breakpoint approach as .header-version-link/
   .desktop-version-bar (see index.html), and the same reason .timeline-row
   already switches to flex-direction:column under the mobile breakpoint:
   without it, the fixed-width chip crowded out .mini-concert-info's
   flex:1, leaving barely any room for the show title before it ellipsised.
   inline-flex + gap (was nowrap text) since one companion per chip
   (2026-09-13) means there can be more than one element to space out now,
   not just one chip's own text. */
.mini-concert-companion-inline{display:inline-flex;flex:none;flex-wrap:wrap;gap:6px;align-items:center}
.mini-concert-companion-stacked{display:none;flex-wrap:wrap;gap:6px;margin-top:4px}

.yearbar-wrap{display:flex;align-items:flex-end;gap:14px;height:120px;overflow-x:auto;overflow-y:hidden;padding-bottom:2px;scrollbar-width:none}
.yearbar-wrap::-webkit-scrollbar{display:none}
.yearbar-col{display:flex;flex-direction:column;align-items:center;flex:none;width:30px;height:100%;justify-content:flex-end;gap:6px}
.yearbar-count,.yearbar-year{font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--ink-soft)}
.yearbar-bar{width:100%;background:var(--olive);border-radius:3px 3px 0 0;min-height:4px}
/* Dashboard-only: jumps to Shows filtered to that year (see
   filterFromDashboard()), same affordance as .bar-row-clickable above. */
.yearbar-clickable{cursor:pointer;border-radius:4px;padding:4px 2px}
.yearbar-clickable:hover{background:var(--bg-alt)}
.yearbar-clickable:hover .yearbar-bar{background:var(--accent)}

.empty-state{text-align:center;padding:60px 20px;color:var(--ink-soft);font-size:14px}

/* toolbar / segmented controls */
/* .filterbar (TODO.md item 98) — the per-page filter + sort row, a framed
   control group rather than a borderless flex row: a funnel icon, the filter
   buttons (.fbtn), the ★ toggle, an optional "Clear all", then a sort icon + the
   native sort <select> (kept together in .fb-sort). The frame + the funnel/sort
   icons are what let each .fbtn drop its ▾ caret and still read as a control,
   and what divide the filter group from the sort group (no separator rule).
   An active filter gets the --accent tint (.fbtn.is-active) and a one-click ✕
   (.filter-clear) in place of the caret. Was .toolbar. */
.filterbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:24px;
  background:var(--card);border:1px solid var(--line);border-radius:10px;padding:8px 10px}
.fb-icon{flex:none;width:20px;height:20px;color:var(--ink-soft);display:flex;align-items:center;justify-content:center;margin:0 2px 0 4px}
.fb-icon svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.fbtn{background:var(--card);border:1px solid var(--line);border-radius:7px;padding:8px 13px;
  font-size:12.5px;font-weight:600;color:var(--ink);cursor:pointer;font-family:inherit;white-space:nowrap}
.fbtn:hover{border-color:var(--ink-soft)}
/* padding-right leaves room for the absolutely-positioned .filter-clear ✕ */
.fbtn.is-active{background:var(--suggest-bg);border-color:var(--suggest-border);color:var(--accent);padding-right:31px}
.fbtn.is-active:hover{border-color:var(--accent)}
.filter-clear{position:absolute;top:50%;right:5px;transform:translateY(-50%);
  width:18px;height:18px;border:none;border-radius:50%;cursor:pointer;padding:0;line-height:1;
  display:inline-flex;align-items:center;justify-content:center;font:inherit;font-size:11px;
  background:var(--suggest-border);color:var(--accent)}
.filter-clear:hover,.filter-clear:focus-visible{background:var(--accent);color:var(--card);outline:none}
/* Sits inline right after the ★ toggle (the owner's call, item 98) — not pushed to
   the far edge, so it doesn't shove the sort control across the row as it
   appears/disappears. */
.clear-all{background:none;border:none;color:var(--ink-soft);font:inherit;font-size:12px;
  font-weight:600;cursor:pointer;padding:6px;text-decoration:underline;text-underline-offset:3px}
.clear-all:hover{color:var(--accent)}
/* Divider + sort icon + <select> travel together and wrap together on a phone.
   On desktop the group is right-aligned; on a phone it just flows after the
   filters (right-aligning it in a multi-row wrap looks stranded). */
.fb-sort{display:inline-flex;align-items:center;gap:8px;flex:none}
@media (min-width:641px){ .filterbar .fb-sort{margin-left:auto} }
/* Collection/Wishlist (Releases) and Releases/Wishlist/Shows (a connection's
   page) — TODO.md item 98 (viii) moved these from above the filter bar to
   just below it, right over the list they switch. */
.list-scope-toggle{width:fit-content;margin:0 0 18px}
.segmented{display:flex;gap:4px;flex-wrap:wrap;background:var(--card);border:1px solid var(--line);border-radius:8px;padding:3px}
.segmented button{border:none;border-radius:6px;padding:7px 14px;font-size:12.5px;font-weight:600;cursor:pointer;background:transparent;color:var(--ink-soft)}
.segmented button.active{background:var(--ink);color:var(--bg)}
.segmented button:disabled{opacity:0.4;cursor:not-allowed}
/* Full-width variant — buttons stretch to fill instead of leaving a gap
   next to the last one when the container is wider than its content
   (e.g. Manage Filters' "Include artists with" row). */
.segmented.fill button{flex:1}
/* Show Type's 5 buttons (concertFormFieldsHTML()) still didn't fit one row
   on a phone even filled — .fill only fixed the *look* of a wrapped
   leftover row, not the wrap itself (flex:1 doesn't stop a flex item
   shrinking below its own content's min-width from wrapping in the first
   place). The owner's follow-up call: scroll horizontally instead, same rail
   idiom .profile-sidebar already uses on mobile, rather than wrap at all. */
@media (max-width:640px){
  .segmented.fill{flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:none;justify-content:flex-start}
  .segmented.fill::-webkit-scrollbar{display:none}
  .segmented.fill button{flex:none;white-space:nowrap}
  /* Edge fades — the row's own "you can scroll this" hint, one or both
     edges depending on live scroll position (updateSegmentedFillFade(),
     app.js — an inline mask-image per render/scroll, not a static rule
     here, since which edge(s) fade changes as the user scrolls). Still
     mask-image rather than a gradient overlay div, so it fades the real
     button underneath instead of painting a tinted rectangle over it
     (which would mismatch light/dark and any button's own active color).
     .can-scroll itself carries no styling now — it's just a marker the JS
     also sets, kept for parity with the other overflow-toggle classes. */
}
.select{background:var(--card);border:1px solid var(--line);border-radius:7px;padding:8px 12px;font-size:12.5px;font-weight:600;color:var(--ink);cursor:pointer}
/* Best-effort color-scheme hint + <option> theming for native <select>
   dropdowns (sort selects, TODO.md item — restyled to a chip panel then
   reverted, the owner preferred the native select's click behavior). Chromium
   respects <option> background-color/color in the popup itself; Firefox and
   Safari mostly ignore it and fall back to their own OS list styling either
   way, so this only closes the gap on some browsers, not all of them. */
.select{color-scheme:light dark}
.select option{background:var(--card);color:var(--ink)}
.select option:checked{background:var(--accent);color:#fff}
.push-right{margin-left:auto}

/* year multi-select filter */
.year-filter{position:relative}
/* .year-filter-panel is the positioned box (position/anchor-edge/width —
   every .right/.gsearch-panel/mobile modifier below still targets this
   name unchanged); the actual scrolling now happens one layer in, on
   .year-filter-panel-scroll, added 2026-08-24 alongside the vertical
   scroll-nav arrows (see vscrollPanelHTML()/actions.scrollVscroll, app.js).
   It has to be a separate, non-scrolling layer: an absolutely-positioned
   child *inside* a scrolling box scrolls away with the content (tested —
   this isn't a "just add position:absolute" fix), so the arrows below live
   here instead, as this box's own children alongside the scroll box, and
   stay fixed at its top/bottom edge regardless of scroll position. */
.year-filter-panel{position:absolute;top:calc(100% + 6px);left:0;z-index:40;min-width:240px}
/* Toolbar filter dropdowns hug their chips instead of a fixed 240px, so a
   filter with only two short options (Companions → "Admin", "the owner") isn't
   mostly whitespace (the owner, 2026-09-01). width:max-content hugs a short
   filter; the cap keeps a many-option one (Years, Formats) wrapping into 2–4
   near-equal rows rather than one long row + a stub. tightenFilterPanels()
   (app.js) then pins the panel to the widest row it actually produced. */
.filterbar .year-filter-panel{min-width:0;width:max-content;max-width:min(200px,calc(100vw - 24px))}
.year-filter-panel-scroll{background:var(--card);border:1px solid var(--line);border-radius:8px;
  padding:10px;max-height:280px;overflow-y:auto;box-sizing:border-box;width:100%;
  box-shadow:0 8px 24px rgba(16,24,40,0.15);scrollbar-width:none}
.year-filter-panel-scroll::-webkit-scrollbar{display:none}
.vscroll-nav{display:none}
@media (hover:hover) and (pointer:fine){
  .vscroll-nav{display:flex;position:absolute;left:50%;transform:translateX(-50%);width:26px;height:26px;
    border-radius:50%;border:1px solid var(--line);background:var(--card);cursor:pointer;
    align-items:center;justify-content:center;opacity:0;transition:opacity .15s,border-color .15s;
    box-shadow:0 2px 8px rgba(16,24,40,0.2);z-index:1}
  .vscroll-nav:hover{border-color:var(--ink)}
  .year-filter-panel:hover .vscroll-nav, .modal-frame:hover .vscroll-nav{opacity:1}
  /* Same idea as .nav-wrap.no-scroll for the horizontal case — toggled by
     updateScrollNavVisibility() once the scroll box is measured and found
     to not actually overflow (everything already fits, nothing to scroll
     to). Both arrows hide together rather than tracking scroll position
     per-edge, matching that same simpler binary behavior. */
  .year-filter-panel.no-scroll .vscroll-nav{display:none}
  /* Modal arrows (2026-08-25) go further — per-edge, not both-together,
     since updateFadeVisibility()'s scroll listener already tracks exact
     position for the fade hint anyway (see its own comment): no point
     showing an "up" arrow when you're already at the top. */
  .modal-frame.at-top .vscroll-nav.up{display:none}
  .modal-frame.at-bottom .vscroll-nav.down{display:none}
  .vscroll-nav.up{top:-4px}
  .vscroll-nav.down{bottom:-4px}
  .vscroll-nav svg{width:13px;height:13px;stroke:var(--ink-soft);fill:none;stroke-width:2.3;stroke-linecap:round;stroke-linejoin:round}
}
.year-filter-all{display:block;width:100%;text-align:left;background:none;border:none;border-bottom:1px solid var(--line);
  padding:4px 4px 10px;margin-bottom:8px;font-size:12.5px;font-weight:700;cursor:pointer;color:var(--ink)}
.year-filter-grid{display:flex;flex-wrap:wrap;gap:6px}
/* Filter chips — mono + radius 5 to sit in the badge family (TODO.md item 97);
   still a control, so it keeps the border and the .active / .excluded states.
   Sized down 2026-09-01 (item 97 follow-up, "Fix B") from 11.5px/600/5x10 so
   an interactive chip is only a hair taller than a filled label (20px) rather
   than a clear step up (was ~26px). */
/* background var(--card), not var(--bg) (2026-09-16, mobile walkthrough)
   — same item-121 fix .form-input/.help-center-search already got; this
   one slipped through that pass since it's a filter/type-picker chip, not
   literally a search box or input field, but every usage (tag filters,
   Feedback's type picker, Views & Filters' default chips, Admin Updates'
   type picker) sits inside a white .panel/.card/dropdown the same way. */
.year-chip{font-family:'IBM Plex Mono',monospace;border:1px solid var(--line);border-radius:5px;padding:4px 9px;font-size:10.5px;font-weight:500;cursor:pointer;background:var(--card);color:var(--ink-soft)}
.year-chip.active{background:var(--ink);color:var(--bg);border-color:var(--ink)}
/* "Exclude this tag" (TODO.md item 76, 2026-08-26) — a tag chip's 3rd
   click state (see tagFilterHTML()/toggleTagFilterValue()). Solid --danger
   fill — chosen over a subtler outline+icon treatment specifically so it
   stays legible at a glance across a whole grid of chips, not just the one
   you're looking right at. */
.year-chip.excluded{background:var(--danger);color:#fff;border-color:var(--danger)}

/* Detail-page title row (2026-08-28): title text, then the favorite star, then
   the management icons, all flowing inline so they land at the end of the last
   line whether the title is one line or wrapped. The item's tag chips sit on a
   .detail-eyebrow row directly above the title (2026-09-01). Replaces the old
   .tag-manage block, which was a right-aligned flex unit that dropped to its
   own row once the title wrapped. */
.detail-title-block .detail-title{display:inline}
/* vertical-align:0.2em (not `middle`): `middle` sits the cluster ~9px below
   the text's optical centre against this heading's tall line-box — the small
   positive nudge lands it centred on the last line. */
.detail-title-adorn{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;vertical-align:0.2em;margin-left:8px}
.detail-title-adorn .favorite-star{font-size:24px}
.detail-title-adorn .icon-btn{width:32px;height:32px}
/* "← Back" line — just the link now (2026-09-01); tag chips moved to the
   .detail-eyebrow row above the title on every detail page. */
.detail-back-row{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:20px}
.detail-back-row + .detail-grid{margin-top:0}
/* Tag chips ride an eyebrow row directly above the title on every detail page
   (TODO.md item 97 follow-up, 2026-09-01 — show page first, then release +
   artist "option B"), instead of the "← Back" row. The "← Back" row is left
   empty. On release/artist this sits in the wide content column (grid col 2),
   lining up with the title, not the cover. */
.detail-eyebrow{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
/* Show-type badge (2026-09-01) — rendered twice, only one visible at a time:
   - .detail-show-type: a block on its own line under the title. Desktop
     default. The star + tag icon stay inline with the title above it.
   - .detail-title-block > .type-badge: an inline copy right after the title
     text. Mobile only (@media below) — there it flows onto its own line
     together with the star + tag icon, all three under the title. */
.detail-title-block > .type-badge{display:none}
.detail-show-type{margin:0 0 14px}
/* Show page only: one margin below the title block spaces it off whatever
   comes next — the show-type badge block, the "Day X of Y" banner, or the
   venue/date meta grid. Scoped to .concert-detail so releases/artists keep
   their own tighter title→artist-link spacing. */
.concert-detail .detail-title-block{margin-bottom:14px}
/* the tag popover: hug its own tag list instead of the filter dropdowns'
   fixed min-width — sizes to 2 tags, wraps onto more lines once there are many */
.detail-title-adorn .year-filter-panel{min-width:0;max-width:240px}

/* favorites */
.favorite-star{background:none;border:none;cursor:pointer;font-size:19px;line-height:1;color:var(--ink-soft);padding:2px;flex:none}
.favorite-star.active{color:#d4a72c}
.fav-icon-toggle{background:var(--card);border:1px solid var(--line);border-radius:7px;width:34px;height:34px;
  display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:15px;color:var(--ink-soft);flex:none}
.fav-icon-toggle.active{color:#d4a72c;border-color:#d4a72c}
.fav-icon-toggle:hover{border-color:var(--ink)}
.private-icon-toggle{background:var(--card);border:1px solid var(--line);border-radius:8px;width:36px;height:36px;
  display:flex;align-items:center;justify-content:center;cursor:pointer;flex:none}
.private-icon-toggle svg{width:16px;height:16px;stroke:var(--ink-soft);fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.private-icon-toggle:hover{border-color:var(--ink)}
.private-icon-toggle.active{border-color:var(--danger)}
.private-icon-toggle.active svg{stroke:var(--danger)}
/* "Private" marker on an always-personal section's header (TODO.md item
   104) — quiet, informational: these sections are never shared, whatever
   the item's Sharing setting. Distinct (grey, not red) from the Private
   toggle in the action row, which hides the whole item. */
/* Always-personal section mark (My note, My memory, Who did you go with?):
   a bare padlock in the section title's own colour — information, not a
   control, since this can't be changed (TODO.md item 158, option F — the
   owner, 2026-09-26). The tooltip carries the explanation. */
.personal-section-tag{display:inline-flex;align-items:center;flex:none;color:var(--ink);line-height:0}
.personal-section-tag svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* Connections page's add/remove status icon (class name kept from when this
   lived on the old Catalog page — TODO.md item 56). Circled + (not yet in
   your collection, click to add) / circled − (already have it, click to
   remove), plain ink like every other icon — the glyph carries the meaning,
   so no green/red (2026-09-07). `.unlinked` / `.linked` are still set by the
   JS but no longer style anything. */
.catalog-link-icon{background:var(--card);border:1px solid var(--line);border-radius:8px;width:32px;height:32px;
  display:flex;align-items:center;justify-content:center;cursor:pointer;flex:none}
.catalog-link-icon svg{width:15px;height:15px;fill:none;stroke:var(--ink);stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.catalog-link-icon:hover{border-color:var(--ink)}
/* Release link icon's "Add to Collection / Add to Wishlist" choice popover
   (TODO.md item 72, 2026-08-26) — reuses .year-filter/.year-filter-panel
   (the same anchored-dropdown shell every other picker in the app uses),
   just with its own compact button styling since these aren't filter rows. */
.link-choice-panel{display:flex;flex-direction:column;gap:2px}
.link-choice-option{display:flex;align-items:center;gap:9px;width:100%;background:none;border:none;border-radius:6px;
  padding:9px 10px;font-family:inherit;font-size:12.5px;font-weight:600;color:var(--ink);cursor:pointer;text-align:left}
.link-choice-option:hover{background:var(--bg-alt)}
.link-choice-dot{width:8px;height:8px;border-radius:50%;flex:none}
.link-choice-dot.owned{background:var(--success)}
.link-choice-dot.wish{background:var(--warn-ink)}
.album-card .favorite-star{position:absolute;top:6px;right:6px;z-index:2;background:var(--overlay-bg);
  border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center}

.filter-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 0;border-bottom:1px solid var(--line)}
.filter-row.wrap{align-items:flex-start}
.filter-row:first-child{padding-top:0}
/* Confirmed companions (wentWithSectionHTML) — a plain stack of names, no
   divider between them. Was inheriting .filter-row's border-bottom, which
   read as a stray line once a show had two or more confirmed companions.
   Also tightened the vertical padding — .filter-row's own 16px top+bottom
   (meant for rows with buttons/controls to tap) left names looking oddly
   far apart once there was nothing else in the row to justify the room. */
.filter-row.no-divider{border-bottom:none;padding:4px 0}
.filter-row:last-child{border-bottom:none;padding-bottom:0}
/* Admin Catalog rows (TODO.md item 56) — same hover-highlight idiom as
   .track-row.clickable/.setlist-row.clickable below, just for .filter-row. */
.filter-row.clickable{cursor:pointer}
.filter-row.clickable:hover{background:var(--bg-alt)}
.filter-row-label{font-weight:600;font-size:13.5px}
.filter-row-sub{font-size:11.5px;color:var(--ink-soft);margin-top:2px}
/* Option C — one section-heading style app-wide (the owner, 2026-09-04): a quiet
   Space Grotesk title, sentence case. The old mono uppercase eyebrow drops a
   level, to .subsection-label / .au-group-t for groups *inside* a section. */
.settings-group-title{font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:16px;letter-spacing:.2px;text-transform:none;color:var(--ink);margin:22px 0 8px}
.settings-group-title:first-child{margin-top:0}
.subsection-label{font-family:'IBM Plex Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:.7px;font-weight:500;color:var(--ink-soft);margin:16px 0 6px}
.subsection-label:first-child{margin-top:0}

/* album grid/list */
.grid-albums{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:22px}
.album-card{cursor:pointer;position:relative}
.album-cover{width:100%;aspect-ratio:1/1}
.album-card .album-title{font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:16px;margin-top:9px;line-height:1.2}
.album-card .album-artist{font-size:12.5px;color:var(--ink-soft);margin-top:2px}
.album-meta-row{display:flex;gap:6px;align-items:center;margin-top:6px;font-family:'IBM Plex Mono',monospace;font-size:11px}
.format-badges{display:inline-flex;gap:6px;flex-wrap:wrap}
/* One format badge, one colour (TODO.md item 97) — formatBadgesHTML() emits a
   single combined label ("CD + DVD", "2×DVD + CD"); the per-format
   .badge-vinyl/-cd/... classes are gone. */
.badge{font-family:'IBM Plex Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:0.5px;
  padding:3px 9px;border-radius:5px;background:var(--bdg-system);color:#fff;display:inline-block;white-space:nowrap}

/* wantlist cards — reuse .album-card's grid/title/artist/image-slot styling */
.wantlist-remove-btn{position:absolute;top:6px;left:6px;z-index:2;background:rgba(255,255,255,0.88);border:none;
  border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;cursor:pointer;
  font-size:16px;line-height:1;color:var(--ink-soft);padding:0}
.wantlist-remove-btn:hover{color:var(--danger);background:#fff}
.wantlist-cat{font-size:10.5px;color:var(--ink-soft);margin-top:5px}
.wantlist-note{font-size:11.5px;color:var(--ink-soft);font-style:italic;margin-top:6px;line-height:1.4}
.wantlist-bought-btn{width:100%;margin-top:10px;font-size:11.5px;padding:7px 0}
/* List view's own compact variant (TODO.md item 123 follow-up, 2026-09-16)
   — the Grid card's full-width button doesn't fit a .list-row; flex:none
   keeps it from stretching into .list-info's own flex:1 space. */
.wantlist-bought-btn-list{flex:none;font-size:11.5px;padding:7px 14px;white-space:nowrap}

.list-albums{display:flex;flex-direction:column}
.list-row{display:flex;align-items:center;gap:16px;padding:12px 8px;border-bottom:1px solid var(--line);cursor:pointer}
.list-cover{width:52px;height:52px;flex:none}
.list-info{flex:1;min-width:0}
.list-title{font-weight:700;font-size:14.5px}
.list-sub{font-size:12.5px;color:var(--ink-soft)}
.list-year{font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--ink-soft);flex:none}
.list-row .badge{flex:none}

/* concert timeline */
/* .timeline's left margin reserves a gutter for .timeline-year to sit in
   (pulled back into it via a matching negative margin) — puts the year to
   the left of the vertical line instead of indented same as the items.
   Widened from 62px to 120px (TODO.md item 77, 2026-08-26, Option D) —
   62px was only ever enough for right-aligned plain text; the connector +
   pill treatment needs real room for both. Desktop has the horizontal
   space to spare, unlike the phone case just below. */
.timeline{border-left:2px solid var(--line);margin-left:120px}
/* .timeline-year is the flex container for .timeline-year-connector +
   .timeline-year-pill (see the markup, renderConcerts()) — row-reverse so
   the pill sits closest to the gutter's right edge and the connector fills
   the remaining space in between it and the actual vertical line, mirrored
   from how the mobile version below reads (line → connector → pill). */
.timeline-year{margin-left:-120px;width:120px;padding:6px 0;
  display:flex;align-items:center;flex-direction:row-reverse;justify-content:flex-start}
.timeline-year-connector{width:22px;flex:none;border-top:2px dotted var(--accent);opacity:.6}
.timeline-year-pill{background:var(--accent);color:#fff;font-family:'Space Grotesk',sans-serif;
  font-weight:800;font-size:15px;padding:4px 15px;border-radius:20px;letter-spacing:.3px;white-space:nowrap}
.timeline-item{position:relative;padding:12px 28px 12px 26px;cursor:pointer}
.timeline-dot{position:absolute;left:-7px;top:18px;width:12px;height:12px;border-radius:50%;border:2px solid var(--bg);background:var(--bdg-system)}
.timeline-main{min-width:0}
/* One stacked column now (TODO.md item 97): title row (title + type + "N
   days" on the left, companion badge + favorite star pushed right), venue,
   the dated day lines tight beneath it, then tags at the bottom. The old
   right-hand .timeline-meta column is gone. */
.timeline-title-row{position:relative;display:flex;align-items:center;flex-wrap:wrap;gap:8px}
.timeline-title-row .type-badge{margin-top:0}
/* Companion + favorite, right-aligned on the title row (the owner 2026-09-01 —
   back to the pre-rework spot). margin-left:auto floats the pair right; if
   the title is long enough to wrap, the pair drops to its own line still
   right-aligned. */
.timeline-title-end{margin-left:auto;display:flex;align-items:center;gap:8px;flex:none}
.timeline-title-end .favorite-star{font-size:16px}
/* Companion, desktop-inline vs mobile-stacked swap — see timelineRowHTML's
   own comment. Desktop shows the title-row copy; mobile flips this and shows
   the row-above-the-tags copy instead (@media 640px below). inline-flex +
   gap (was plain, one merged chip needed neither) since one companion per
   chip (2026-09-13) can mean more than one element to space out. */
.timeline-companion-inline{display:inline-flex;flex-wrap:wrap;gap:6px;align-items:center}
.timeline-companion-stacked{display:none;flex-wrap:wrap;gap:6px}
/* Connector from the vertical line to the title (TODO.md item 97 — was on
   .timeline-item, which pointed at whatever sat ~24px down; now it always
   lands on the title). Shortened to leave ~7px clear of the title text,
   matching the year connector's own gap. left:-21px puts its start ~5px
   right of the dot (dot right edge = -7+12). */
.timeline-title-row::before{content:"";position:absolute;left:-21px;top:0.72em;width:13px;border-top:1px solid var(--line)}
.timeline-artist{font-weight:700;font-size:16px;line-height:1.2}
.timeline-venue{font-size:12.5px;color:var(--ink-soft);line-height:1.45;margin-top:6px}
/* Day lines sit right under the venue and close together — a multi-day
   festival otherwise stretched the whole entry out (the owner, 2026-09-01).
   The venue line's own line-height already leaves ~4px under its text, so
   the day block needs no top margin for the title->venue and venue->date
   gaps to read as equal (~8px each; verified in-app 2026-09-01). */
.timeline-days{display:flex;flex-direction:column;margin-top:0}
.timeline-day{position:relative;padding:2px 0;border-radius:6px;line-height:1.45}
.timeline-day:hover{background:var(--bg-alt)}
.timeline-day-date{font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--ink-soft)}
.timeline-day-acts{font-size:12.5px;color:var(--ink)}
.timeline-main > .chip-row{margin-top:8px}
.timeline-item.upcoming .timeline-day-acts{color:var(--ink-soft)}
.chip-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
/* Badge family (TODO.md item 97) — .chip is now a filled Tag; .chip-companion
   and .chip-artist are the two variants (grey fill / outline). Text stays
   as-typed (no text-transform), unlike the UPPERCASE system labels. */
.chip{font-family:'IBM Plex Mono',monospace;font-size:10.5px;background:var(--bdg-tag);color:#fff;border-radius:5px;padding:3px 9px;display:inline-block}
.chip-companion{background:var(--bdg-companion);color:#fff}
.chip-artist{background:transparent;color:var(--ink);border:1px solid var(--ink)}
/* Show page line-up billing + the Setlist / Tracklist switchers (TODO.md item
   97 follow-up, 2026-09-01) — a three-step fill: "Came for" solid slate,
   "Also saw" a soft slate tint, "Didn't see" a plain muted outline. The base
   .chip-artist (ink outline) stays the neutral form used elsewhere. */
.chip-artist--lead{background:var(--bdg-system);color:#fff;border-color:var(--bdg-system)}
.chip-artist--seen{background:var(--bdg-system-soft);color:var(--ink);border-color:var(--bdg-system-line)}
.chip-artist--missed{color:var(--ink-soft);border-color:var(--line);opacity:.75}
.chip-link{cursor:pointer}
button.chip{line-height:normal}
button.chip:disabled{cursor:default}
/* Show type — one slate colour for every type now (TODO.md item 97); you read
   Concert vs Festival off the word, not the hue. The per-type .concert/
   .festival/... classes are still emitted but no longer styled here. */
.type-badge{display:inline-block;margin-top:8px;font-family:'IBM Plex Mono',monospace;font-size:10.5px;
  text-transform:uppercase;letter-spacing:0.5px;color:#fff;padding:3px 9px;border-radius:5px;background:var(--bdg-system)}
/* Upcoming show (TODO.md item 53) — outline instead of filled (List view;
   Timeline hollows the dot instead). */
.type-badge.upcoming{background:none;border:1px solid var(--bdg-system);color:var(--bdg-system)}
/* Upcoming show, Timeline variant — the dot's existing 2px border (see
   .timeline-dot above, originally just a page-bg-colored ring separating it
   from the vertical line) drops the type-color fill for a hollow ring
   instead. A muted gray border rather than the type's own color (tried
   first, then dropped 2026-08-21) — a vivid colored ring read oddly next to
   the row's own dimmed text below, and gray reads as "hasn't happened yet"
   on its own without needing to also carry the type color. */
.timeline-dot.upcoming{background:var(--bg);border-color:var(--ink-soft)}
/* The dimmed row itself — a color-token swap on the title only, not
   opacity (tried first, dropped 2026-08-21: .timeline-venue already renders
   in --ink-soft, so opacity dimmed it a second time — in dark mode that
   compounded muted-on-muted read as too low-contrast to read comfortably,
   per the owner's report; opacity also blends toward whatever's behind it,
   drifting differently per theme rather than resolving to a color the
   theme has already tuned for legibility). .timeline-artist is the only
   piece of this row that normally renders at full --ink strength, so
   dropping just that to --ink-soft (the same tone the venue line already
   uses) reads as visibly dimmed without introducing any color the theme
   doesn't already account for. */
.timeline-item.upcoming .timeline-artist{color:var(--ink-soft)}

/* "4 DAYS" next to the FESTIVAL badge (TODO.md item 97) — a filled grey chip
   in the old Other/companion colour (the owner, 2026-09-01: companion moved to
   rose, this took its grey), reading as a count/modifier of the badge beside
   it, not a type of its own. */
.festival-count-chip{font-family:'IBM Plex Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:0.5px;
  background:var(--bdg-count);color:#fff;border-radius:5px;padding:3px 9px}
/* Show detail page's "Day X of Y" banner, right under the title row. The gap
   off the title comes from .concert-detail .detail-title-block's own
   margin-bottom (below) — that same margin also spaces the title off the
   meta-grid when there's no banner at all (the owner, 2026-09-01). */
.part-of-festival{display:inline-flex;align-items:center;gap:8px;background:var(--bg-alt);border:1px solid var(--line);
  border-radius:20px;padding:6px 8px 6px 14px;font-size:12px;color:var(--ink-soft);margin:0 0 14px}
.part-of-festival b{color:var(--ink);font-weight:700}
/* .pof-nav, not .nav — a bare `.nav` rule in the mobile block (the main
   nav menu's vertical dropdown) was leaking flex-direction:column + width:
   100% onto this, stacking the two day arrows and blowing the pill out to
   several lines on a phone (the owner, 2026-09-01). */
.part-of-festival .pof-nav{display:flex;flex-direction:row;gap:2px;flex:none}
.part-of-festival .pof-nav button{width:22px;height:22px;border-radius:50%;border:1px solid var(--line);background:var(--card);
  color:var(--ink-soft);font-size:11px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;flex:none}
.part-of-festival .pof-nav button:disabled{opacity:0.35;cursor:default}
.part-of-festival .pof-nav button:not(:disabled):hover{border-color:var(--ink);color:var(--ink)}

/* detail pages */
.back-link{font-size:13px;font-weight:600}
.detail-grid{display:grid;grid-template-columns:minmax(220px,280px) 1fr;gap:36px;margin-top:20px}
.detail-cover{width:100%;aspect-ratio:1/1}
/* Release cover only (.release-cover, added where imageSlotHTML renders a
   release's cover — not the artist photo, which keeps its fixed square/
   circle box). Shows the sleeve's true aspect ratio instead of force-
   cropping everything to a square, so portrait/landscape covers don't lose
   their edges. Scoped to the detail page/Catalog popover only, by design —
   grid/list thumbnails (.album-cover, .list-cover) are untouched and stay
   uniform squares. Falls back to a square dropzone while empty, since
   there's no image yet to derive a ratio from. */
.detail-cover.release-cover{aspect-ratio:auto}
.detail-cover.release-cover .image-slot:not(.has-image){aspect-ratio:1/1}
.detail-cover.release-cover .image-slot.has-image{height:auto;max-height:520px}
/* Artist photo/cover, everywhere it appears — round, not the app's usual
   rounded-square. Covers all of them with one rule: the big photo on the
   artist's own page (.detail-cover.artist-photo), the small thumbnail on
   the Artists grid/list cards (.album-cover.artist-cover / .list-cover
   .artist-cover), and the Dashboard's mini-cards (.mini-cover.artist-cover
   — Favorite artists, Most collected, Most seen live). Sizing/aspect-ratio
   is untouched everywhere — this only rounds the corners all the way to a
   circle; release covers (.release-cover above) and everything else stay
   square. */
.artist-photo .image-slot, .artist-cover .image-slot{border-radius:50%}
.detail-cover.release-cover .image-slot.has-image img{height:auto;object-fit:contain}
.detail-title{font-size:clamp(28px,4vw,40px);line-height:1.05}
.artist-link{font-size:18px;color:var(--accent);font-weight:600;margin-top:6px;cursor:pointer;display:inline-block}
.name-link{color:var(--accent);cursor:pointer}
.name-link:hover{color:var(--accent-dark);text-decoration:underline}
.badge-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:16px 0;font-family:'IBM Plex Mono',monospace;font-size:11.5px}
/* Release type — navy, UPPERCASE (TODO.md item 97), sitting next to the slate
   format badge under the release title. */
.badge-row .tag{background:var(--bdg-reltype);color:#fff;text-transform:uppercase;letter-spacing:0.5px;
  font-size:10.5px;padding:3px 9px;border-radius:5px}
.action-row{display:flex;gap:10px;margin-bottom:20px}
/* Compact icon buttons on the release / show detail pages (2026-09-04,
   The owner) — one size for the toolbar under the meta grid AND every
   per-section edit/picker icon (Tracklist, Setlist, Line-up, My memory,
   My note save, "who did you go with"), so they all read as one family.
   30x30 button / 15px glyph, vs the app-wide 36x36 / 18px .icon-btn. */
.detail-action-row .icon-btn,
.detail-action-row .private-icon-toggle,
.section-head .icon-btn{width:30px;height:30px;border-radius:7px}
.detail-action-row .icon-btn svg,
.detail-action-row .private-icon-toggle svg,
.section-head .icon-btn svg{width:15px;height:15px}
/* The Private toggle's glyph is --ink-soft and a hair heavier (1.8) app-wide
   — visibly out of step with the other toolbar icons. Match them here while
   it's inactive; the active state keeps its danger-red (own rule, above). */
.detail-action-row .private-icon-toggle svg{stroke-width:1.6}
.detail-action-row .private-icon-toggle:not(.active) svg{stroke:var(--ink)}
/* Two-tint header (TODO.md item 104, 2026-09-04 — the owner picked option A5).
   The meta grid + edit toolbar sit in a soft panel one step darker than the
   page (--hdr-tint); the toolbar is a lighter --bg-alt strip, full-bleed
   along the panel's bottom edge, rounded to match. No hairlines — the two
   tint steps do the separating. Title / artist / badges stay on the plain
   background above the panel. Scoped to the detail pages: .meta-grid is
   reused with its own borders in the read-only catalog popover. */
.detail-header-panel{background:var(--hdr-tint);border-radius:10px;
  padding:16px 16px 0;margin-bottom:20px}
.detail-header-panel .meta-grid{border:none;padding:0;margin:0}
.detail-action-row{gap:5px;align-items:center;margin:14px -16px 0;padding:8px 16px;
  background:var(--bg-alt);border-radius:0 0 10px 10px}
.detail-action-row .view-add-row{padding:2px 0}
.btn-outline{background:none;border:1px solid var(--line);border-radius:6px;padding:8px 16px;font-size:12.5px;font-weight:600;cursor:pointer;color:var(--ink)}
.btn-outline.danger{color:var(--danger)}
/* "+ Add {performer}" on the Setlist editor (setlistEditorHTML) — a
   suggested action generated from data already on the form, not a generic
   button, so it gets a tinted look to read as distinct (the owner, 2026-08-17).
   Shared with the Tracklist editor's own "+ Add {format}" buttons once
   those are built (TODO.md item 24), same reasoning either place. */
.btn-outline.suggest{background:var(--suggest-bg);border-color:var(--suggest-border);color:var(--accent)}
.btn-outline.suggest:hover{background:var(--suggest-border)}
.meta-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:14px;padding:16px 0;
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);margin-bottom:20px}
.meta-label{font-family:'IBM Plex Mono',monospace;font-size:10.5px;text-transform:uppercase;color:var(--ink-soft)}
.meta-value{font-weight:600;font-size:13.5px;margin-top:3px}
/* Option C (the owner, 2026-09-04) — was 18px/800; now the same quiet Space
   Grotesk title as .settings-group-title. Explicit font-family so a
   <div class="section-title"> matches an <h3 class="section-title">. */
.section-title{font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:16px;letter-spacing:.2px;color:var(--ink);margin:0 0 10px}
.section-head{display:flex;align-items:center;gap:8px;margin:0 0 10px}
.section-head .section-title{margin:0}
.section-count{font-family:'IBM Plex Mono',monospace;font-size:11.5px;font-weight:400;color:var(--ink-soft);margin-left:2px}
/* .filter-row's own :first-child selector (below) never actually matches
   inside the Companion section (wentWithSectionHTML) — its .filter-row rows
   sit alongside plenty of other page content before them, never literally
   first in their parent — so the 16px top padding meant for spacing
   *between* rows was also landing between the heading and the first row,
   next to the heading's own already-tight 10px margin. Zeroed here instead,
   scoped to only the row directly under the heading (an incoming request's
   banner, if any, sits between them instead and brings its own spacing). */
.section-head + .filter-row{padding-top:0}
.tracklist{margin-bottom:12px}
.track-row{display:flex;align-items:baseline;gap:12px;padding:9px 0;border-bottom:1px solid var(--line)}
/* Click-through to the Songs page (TODO.md item 54, 2026-08-21) — only rows
   tracklistItemRowHTML()/setlistItemRowHTML() found an actual match for get
   this class, so a track with nowhere to link to (a heading, or a track on
   a Wishlist-only release) stays plain rather than promising a dead click.
   Same cursor+hover-highlight idiom as every other clickable row in the app
   (.song-row, .list-row, .festival-day-row) rather than coloring the title
   like a link — the row itself is the target, not just the text. */
.track-row.clickable,.setlist-row.clickable{cursor:pointer}
.track-row.clickable:hover,.setlist-row.clickable:hover{background:var(--bg-alt)}
/* Wide enough for a disc/side-prefixed position like "DVD-B5" or "4-16"
   (see track-heading below) without wrapping to a second line. */
.track-no{font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--ink-soft);width:52px;flex:none;white-space:nowrap}
.track-title{flex:1;font-weight:600;font-size:13.5px}
/* A heading row groups the tracks below it (e.g. "CD 1", "DVD (PAL)") on a
   multi-disc/multi-format tracklist — a distinct full-width bar so it reads
   as structure, not just another track. First one skips the top margin so
   it doesn't float away from the "Tracklist" section title above it. */
.track-heading{background:var(--bg-alt);border-radius:6px;padding:6px 10px;font-size:12px;font-weight:700;
  text-transform:uppercase;letter-spacing:0.6px;color:var(--ink);margin-top:14px;margin-bottom:4px}
.track-heading:first-child{margin-top:0}
.track-version{font-weight:400;color:var(--ink-soft)}
/* A track's own credited artist, when it differs from the release's
   (Various Artists releases, 2026-09-21) — weighted like the title itself,
   not muted like Version/Featuring, since it's the actual "by" credit for
   this one track, not a side note. */
.track-artist{font-weight:600;color:var(--ink)}
.track-artist .name-link{font-weight:600}
.track-guest{font-style:italic;font-weight:400;color:var(--ink-soft)}
.track-guest .name-link{font-style:normal}
.album-edition{font-style:italic;font-weight:400;color:var(--ink-soft)}
.list-title .album-edition{font-size:12.5px}
.track-playtime{font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--ink-soft)}
.total-playtime{text-align:right;font-size:12.5px;font-weight:600;color:var(--ink-soft);margin-bottom:28px}

/* Songs page (TODO.md item, 2026-08-13/2026-08-15). A–Z rail is desktop-
   only per the mockup spec — mobile wrapping for this page is explicitly
   still open (see TODO.md's Evaluate/Test pass), so it's hidden rather than
   squeezed at the mobile breakpoint below. */
.songs-layout{display:flex;gap:24px;align-items:flex-start}
.az-rail{display:flex;flex-direction:column;gap:1px;position:sticky;top:16px;flex:none}
.az-rail button{background:none;border:none;cursor:pointer;font-size:11px;font-weight:700;padding:2px 7px;
  border-radius:4px;color:var(--ink-soft);text-align:center;font-family:'IBM Plex Mono',monospace}
.az-rail button:hover:not(:disabled){background:var(--bg-alt);color:var(--ink)}
.az-rail button:disabled{opacity:0.3;cursor:default}
/* jumpToSongLetter()'s scrollIntoView({block:"start"}) target — without
   this it lands the letter heading flush under the sticky app header
   (2026-08-17 feedback), same bug and same fix as .concert-section's
   scroll-margin-top. Scoped to the song-letter-* ids rather than the
   shared .track-heading class, which is also used in plenty of places
   that aren't a scrollIntoView target. */
[id^="song-letter-"]{scroll-margin-top:80px}
.songs-list{flex:1;min-width:0}
.song-row{display:flex;align-items:baseline;justify-content:space-between;gap:14px;padding:10px 4px;
  border-bottom:1px solid var(--line);cursor:pointer}
.song-row:hover{background:var(--bg-alt)}
.song-row-main{display:flex;align-items:baseline;gap:8px;min-width:0;flex:1;overflow:hidden}
.song-row-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.song-row-meta{font-size:12px;color:var(--ink-soft);white-space:nowrap;flex:none}
.song-row-artist{font-size:13px;color:var(--ink-soft);flex:none;white-space:nowrap}
@media (max-width:640px){
  .az-rail{display:none}
  /* A long artist name (flex:none, never shrinks) could squeeze the title
     down to a single letter on a narrow screen — .song-row-main only got
     whatever width was left over (2026-08-17 feedback). Rather than make
     the title or artist name truncate, the row itself scrolls sideways
     instead: nothing shrinks or ellipsizes, the row is just wider than the
     viewport and swipeable, same option ("2c") the owner picked from a
     preview over shrinking the artist name down to an ellipsis. */
  .song-row{overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;justify-content:flex-start}
  .song-row::-webkit-scrollbar{display:none}
  .song-row-main{flex:none;min-width:0;overflow:visible}
  .song-row-title{overflow:visible;text-overflow:clip}
}

/* Songs detail — Versions section. Name column is a fixed width so the
   cover chips line up across every version row (mockup spec), not just
   within one row. */
.song-version-row{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--line)}
.song-version-name{flex:0 0 260px;min-width:0;font-weight:600;font-size:13.5px}
/* One mini cover per release a version appears on (replaces the old
   format-badge pills, 2026-08-17) — plain flow, wraps to a second line only
   once a track has enough releases to need it (see .song-version-cover
   below for why there's no visible boundary between one release's cover
   and the next, unlike the old pills). Hovering a cover shows title +
   edition + year + release type + format(s) as a tooltip (see
   songVersionCoverTitle() in app.js) — the old pills' click-through to
   that release (openAlbum) carries over unchanged. */
.song-version-covers{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.song-version-cover{width:26px;height:26px;flex:none;cursor:pointer}
/* No border and no distinct empty-state look — a release with no cover art
   uses .image-slot's own readonly fallback (a small note icon on --bg-alt),
   but the owner explicitly didn't want that to visually stand out next to a
   real cover image (2026-08-17) the way the old bordered pills, or
   .image-slot's default dashed border, would have. */
.song-version-cover .image-slot{border:none}
.song-version-cover:hover{transform:scale(1.12)}
.song-heard-live-row{display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--line);font-size:13.5px;cursor:pointer}
@media (max-width:640px){
  .song-version-row{flex-wrap:wrap}
  .song-version-name{flex:1 1 100%}
}
.total-playtime .mono{color:var(--ink);margin-left:6px}

/* format editor (inline record edit) */
.format-editor{display:flex;flex-direction:column;gap:8px}
.format-edit-row{display:grid;grid-template-columns:1.2fr 70px 1fr auto;gap:8px;align-items:center}
.format-edit-row .form-input{padding:8px 10px;font-size:12.5px}
.format-edit-row .btn-outline{padding:6px 9px;font-size:12px;line-height:1}

/* tracklist editor (inline record edit) */
.tracklist-editor{display:flex;flex-direction:column;gap:8px}
/* Edit-page disc/artist grouping (groupEditorRows(), public/app.js) — boxes
   a format/artist marker and its rows together so a multi-disc release or
   multi-act show reads as separate groups instead of one flat list, same
   spacing rhythm as .tracklist-editor's own top-level gap so a grouped row
   and an ungrouped one (before the first marker) line up identically.
   scroll-margin-top is scrollToEditorGroup()'s jump-chip target (public/
   app.js) — without it the group lands flush under the sticky app header
   (same bug, same fix as [id^="song-letter-"] above and .concert-section's
   own scroll-margin-top; the owner, 2026-09-22). */
.editor-group{display:flex;flex-direction:column;gap:8px;border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:8px;padding:8px;scroll-margin-top:72px}
/* Drag-to-reorder (TODO.md item 12, 2026-08-17) — sits alongside the
   existing up/down arrow buttons, doesn't replace them (the owner's call:
   don't remove working functionality). The handle, not the row itself, is
   what's draggable="true" — dragging a whole grid row would fight with
   selecting text inside its inputs, since a mousedown-drag inside an
   <input> starts a text selection, not an HTML5 drag gesture. Wrapping
   div keeps each row's own internal grid (.track-edit-row etc.) untouched. */
.drag-row-wrap{display:flex;align-items:center;gap:4px}
.drag-row-wrap>.track-edit-row,.drag-row-wrap>.track-edit-heading-row,.drag-row-wrap>.track-edit-subheading-row,.drag-row-wrap>.setlist-edit-row{flex:1;min-width:0}
.drag-handle{flex:none;display:flex;align-items:center;justify-content:center;width:16px;height:100%;min-height:32px;cursor:grab;color:var(--ink-soft);touch-action:none}
.drag-handle svg{width:12px;height:16px;fill:currentColor}
.drag-handle:active{cursor:grabbing}
.drag-row-wrap.dragging{opacity:0.4}
.drag-row-wrap.drop-before{box-shadow:inset 0 2px 0 var(--accent)}
.drag-row-wrap.drop-after{box-shadow:inset 0 -2px 0 var(--accent)}
/* Drag-and-drop needs a real mouse — hide the handle on touch/coarse-pointer
   devices (mobile reordering wasn't scoped as part of this, per TODO.md's
   own note; the arrows keep working there since they're untouched). */
@media (hover:none),(pointer:coarse){.drag-handle{display:none}}
/* No. / Title / Artist (Various Artists releases) / Version / Playtime /
   Featuring / actions — Artist added 2026-09-21, same optional-field idiom
   as Version/Featuring (blank = the release's own Artist field applies). */
.track-edit-row{display:grid;grid-template-columns:52px 1.2fr 1fr 0.9fr 62px 1fr auto;gap:8px;align-items:center}
.track-edit-row .form-input{padding:8px 10px;font-size:12.5px}
.track-edit-actions{display:flex;gap:4px}
.track-edit-actions .btn-outline{padding:6px 9px;font-size:12px;line-height:1}
.track-edit-actions .btn-outline:disabled{opacity:0.35;cursor:not-allowed}
/* A heading row (see .track-heading) only ever needs one field — its label
   — so it gets its own narrower grid instead of the 5-column track one. */
.track-edit-heading-row{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;background:var(--bg-alt);border-radius:6px;padding:6px}
.track-edit-heading-row .form-input{padding:8px 10px;font-size:12.5px;font-weight:700}
/* Renumber per disc (TODO.md item 133 follow-up) — an inline panel under
   the disc's marker row, plus the read-only struck-through preview that
   stands in for a changed track's No. field while the panel is open. */
.renumber-panel{border:1px solid var(--line);border-radius:8px;background:var(--card);padding:10px;display:flex;flex-direction:column;gap:2px}
.renumber-label{font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:0.08em;text-transform:uppercase;color:var(--ink-soft);margin:0 0 4px 2px}
.renumber-opt{display:flex;gap:10px;align-items:flex-start;text-align:left;background:none;border:none;border-radius:6px;padding:6px 8px;cursor:pointer;color:var(--ink);font-family:inherit;font-size:12.5px}
.renumber-opt:hover{background:var(--bg-alt)}
.renumber-opt.active{background:var(--suggest-bg)}
.renumber-radio{flex:none;width:13px;height:13px;border-radius:50%;border:1.5px solid var(--ink-soft);margin-top:2px}
.renumber-opt.active .renumber-radio{border-color:var(--accent);box-shadow:inset 0 0 0 3px var(--card);background:var(--accent)}
.renumber-opt-label{display:block;font-weight:600}
.renumber-opt-desc{display:block;font-size:11px;color:var(--ink-soft)}
.renumber-foot{display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid var(--line);margin-top:6px;padding-top:8px;font-size:12px}
.renumber-all{display:flex;align-items:center;gap:6px;cursor:pointer}
.renumber-count{flex:1;color:var(--ink-soft)}
.renumber-foot .btn-outline{padding:6px 12px;font-size:12px;line-height:1}
/* Apply is the panel's committing action, so it gets the same solid accent
   as Save changes (.submit-btn), just compact; Renumber itself stays an
   outline button like the ↑/↓ next to it. */
/* Same 26px height as the Renumber button (.track-edit-actions .btn-outline:
   6px padding + 12px text at line-height 1 + 1px border) — hence the
   border here, which .submit-btn itself doesn't have.
   Scoped under .renumber-foot on purpose: a bare .renumber-apply has the
   same specificity as .submit-btn, which is defined further down this file
   and would win. */
.renumber-foot .renumber-apply{width:auto;padding:6px 14px;font-size:12px;line-height:1;border:1px solid var(--accent)}
.renumber-foot .renumber-apply:not(:disabled):hover{border-color:var(--accent-dark)}
.renumber-foot .renumber-actions{display:flex;gap:6px;align-items:center}
.track-no-preview{padding:8px 10px;font-size:12.5px;background:var(--suggest-bg);border-color:var(--suggest-border);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.track-no-preview s{color:var(--ink-soft);font-size:11px;margin-right:2px}
.track-edit-heading-row.renumber-drop .form-input{text-decoration:line-through;opacity:0.55}
/* A subheading (added 2026-08-16) reads inline like a track row — same
   blank leading column where a number would sit — but plain, not a full
   divider bar the way .track-edit-heading-row is. Its own 3-column grid,
   same shape as .setlist-edit-row below, since it's just no./title/actions
   either way — reusing .track-edit-row's 6-column grid with only 3 children
   would leave the trailing columns empty and misalign the action buttons. */
.track-edit-subheading-row{display:grid;grid-template-columns:56px 1fr auto;gap:8px;align-items:center}
.track-edit-subheading-row .form-input{padding:8px 10px;font-size:12.5px}
/* Setlist editor's track row — a slimmed-down .track-edit-row: a running
   number (not a stored/editable field, see setlistTrackNumber()), a title,
   and (2026-08-16) featuring — no version/playtime, meaningless for a
   setlist. Shares .track-edit-heading-row above for its heading rows and
   .track-edit-subheading-row above for its subheading rows.*/
.setlist-edit-row{display:grid;grid-template-columns:28px 1fr 1fr auto;gap:8px;align-items:center}
.setlist-edit-row .form-input{padding:8px 10px;font-size:12.5px}
.track-edit-no{font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--ink-soft);text-align:center}

/* Concert detail's Companion/Who did you see?/Setlist/Memory/My Note
   sections (TODO.md item, 2026-08-16). Went through a sidebar+card-grid
   version, a sidebar+chevron-collapsible version, a horizontal-rule-between-
   every-section version, and a version right-shifted to line up with the
   release page's cover column, before landing here (2026-08-17 feedback,
   each round): none of the card/collapse/shift ideas read as "balanced" the
   way the flat Release/Artist pages do, and the repeated rule between every
   section felt busier than it needed to; only one line, right after General
   Information, earns its place. Sections after that are set apart by
   whitespace + a bold title alone — the same idiom the Artist page's own
   Releases/Appears on/Shows sections already use, no line needed between
   each of them. */
/* Renamed from .concert-header-divider (TODO.md item 62, 2026-08-21) — now
   reused by both the release and show detail pages to bracket the
   History/Private/Edit/Unlink action-row with a rule below it too, not just
   above (the release page's .meta-grid already carries its own
   border-bottom for the "above" rule; the show page still uses this class
   above, same as before). */
.section-divider{border-top:1px solid var(--line);margin-top:20px}
.concert-section{margin-top:32px}
/* First section sits right under the two-tint header panel (item 104,
   2026-09-04) — the panel's own bottom margin is the gap. */
.concert-section:first-of-type{margin-top:0}
/* Show page "Line-up" section (TODO.md item 97 → item 104) — the bill as
   billing-coloured chips under "Headline act" / "Support act" sub-labels. */
.lineup-support-label{display:block;margin-top:14px;margin-bottom:6px}
.lineup-pick-row{display:flex;flex-direction:column;gap:6px;padding:9px 4px}
.lineup-pick-row + .lineup-pick-row{border-top:1px solid var(--line)}
.lineup-pick-name{font-size:12.5px;font-weight:600}
.lineup-pick-row .type-toggle{margin:0}
.setlist-row{display:flex;gap:12px;padding:8px 0;border-bottom:1px solid var(--line)}
.detail-photo{width:100%;aspect-ratio:4/3}
/* Memory gallery (memoryGalleryHTML) — a horizontal swipe-strip of a show's
   personal photos, one .detail-photo box per "page", native scroll-snap
   (no JS library). A dashed add-tile always sits at the end (reuses
   .image-slot's own empty-state look); each existing photo gets a small ×
   overlay to remove it. Dots track scroll position (see the capture-phase
   "scroll" listener near the bottom of app.js) and are themselves
   clickable, same as the prev/next arrows — both are desktop's click-driven
   equivalent of the swipe gesture, since a mouse/trackpad has no swipe. */
.photo-gallery{position:relative}
.memory-strip{display:flex;gap:10px;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none}
.memory-strip::-webkit-scrollbar{display:none}
.memory-strip .photo-tile{flex:0 0 100%;scroll-snap-align:center;position:relative}
/* Desktop-only fixed tile width (2026-08-17 follow-up) — flex:0 0 100%
   makes sense on a phone (one photo fills the screen, swipe for the next)
   but stretched a single photo across the entire ~1100px-wide desktop page
   before the width cap above existed, and would still look oversized at
   760px. Fixed at 220px (same 4:3 shape via .detail-photo) so several show
   at once instead of one giant one; unrelated to that width cap — this
   would matter even without it. Mobile keeps flex:0 0 100% exactly as is. */
@media (min-width:641px){
  .memory-strip .photo-tile{flex:0 0 220px}
}
.memory-remove-badge{position:absolute;top:8px;right:8px;width:26px;height:26px;border-radius:50%;
  background:rgba(9,12,18,.55);color:#fff;border:none;font-size:15px;line-height:1;cursor:pointer;
  display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px)}
.memory-remove-badge:hover{background:var(--danger)}
/* Only shown for a mouse/trackpad (hover:hover + pointer:fine) — a touch
   device already has swipe, so the arrows would just be redundant chrome
   sitting on top of every photo. */
.gallery-nav{display:none}
@media (hover:hover) and (pointer:fine){
  .gallery-nav{display:flex;position:absolute;top:50%;transform:translateY(-50%);width:30px;height:30px;
    border-radius:50%;border:none;background:rgba(9,12,18,.5);color:#fff;cursor:pointer;
    align-items:center;justify-content:center;backdrop-filter:blur(2px);opacity:0;transition:opacity .15s}
  .gallery-nav:hover{background:rgba(9,12,18,.75)}
  .photo-gallery:hover .gallery-nav, .nav-wrap:hover .gallery-nav{opacity:1}
  .gallery-nav.prev{left:8px}
  .gallery-nav.next{right:8px}
  .gallery-nav svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
  /* .no-scroll is set by updateScrollNavVisibility() (app.js) once
     .memory-strip is measured and found not to actually overflow — e.g.
     the desktop 220px tiles (2026-08-17) all fit in view already, nothing
     to scroll to. Same idea and same class name as .nav-wrap.no-scroll
     below, just scoped to .photo-gallery since the dots need hiding too,
     not just the arrows. */
  .photo-gallery.no-scroll .gallery-nav{display:none}
}
/* Generic wrapper for a horizontally-scrolling row that isn't a photo strip
   (Dashboard's Favorite artists/releases, Shows per year) — same hover-only
   desktop arrow-nav pattern as the Memory gallery above, reusing
   .gallery-nav's positioning/reveal-on-hover but with a lighter
   card-colored button (.scroll-nav) instead of the dark photo-overlay
   circle, since there's no image backdrop here for that to sit on. */
.nav-wrap{position:relative}
.scroll-nav{display:none}
@media (hover:hover) and (pointer:fine){
  .scroll-nav{display:flex;position:absolute;top:50%;transform:translateY(-50%);width:28px;height:28px;
    border-radius:50%;border:1px solid var(--line);background:var(--card);cursor:pointer;
    align-items:center;justify-content:center;opacity:0;transition:opacity .15s,border-color .15s;
    box-shadow:0 1px 4px rgba(0,0,0,.15)}
  .scroll-nav:hover{border-color:var(--ink)}
  .nav-wrap:hover .scroll-nav{opacity:1}
  /* .no-scroll is set by updateScrollNavVisibility() (app.js) once the row
     is measured and found not to actually overflow — e.g. only 1 favorite
     artist, nothing to scroll to. Wins over the hover rule above since it
     comes later and is equally specific. */
  .nav-wrap.no-scroll .scroll-nav{display:none}
  .scroll-nav.prev{left:-10px}
  .scroll-nav.next{right:-10px}
  .scroll-nav svg{width:14px;height:14px;stroke:var(--ink-soft);fill:none;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
}
.memory-dots{display:flex;gap:2px;justify-content:center;margin-top:6px}
/* Same .no-scroll gate as .gallery-nav above, but not confined to the
   hover:hover/pointer:fine media query — the dots matter on touch too
   (they're clickable, not just a scroll-position readout), so a strip that
   doesn't overflow on a touch device shouldn't show them either. */
.photo-gallery.no-scroll .memory-dots{display:none}
/* The button itself is a roomier 16px click target than the 6px dot it
   shows — a bare 6px circle is too fiddly to click precisely with a mouse. */
.memory-dots .d{position:relative;width:16px;height:16px;border:none;padding:0;background:none;cursor:pointer}
.memory-dots .d::after{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:6px;height:6px;border-radius:50%;background:var(--line);transition:background .15s,transform .15s}
.memory-dots .d.active::after{background:var(--accent);transform:translate(-50%,-50%) scale(1.3)}

/* artist detail */
.artist-sub{font-family:'IBM Plex Mono',monospace;font-size:13px;color:var(--ink-soft);margin-bottom:28px}

/* image slot */
.image-slot{position:relative;width:100%;height:100%;border-radius:8px;overflow:hidden;background:var(--bg-alt);
  border:1.5px dashed var(--line);display:flex;align-items:center;justify-content:center;cursor:pointer}
.image-slot.has-image{border-style:solid;border-color:transparent}
.image-slot img{width:100%;height:100%;object-fit:cover;display:block}
.image-slot .slot-label{font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:var(--ink-soft);text-align:center;padding:6px;pointer-events:none}
.image-slot.dragover{border-color:var(--accent);background:var(--bg)}
.image-slot input[type=file]{display:none}
.image-slot.readonly{cursor:default}
/* line-height:1 (2026-09-16) — the ♪ glyph's own ascent/descent isn't
   symmetric, so the default line-height left extra room under it inside
   the flex-centered box, reading as "not quite centered" even though the
   box itself was centered correctly. Small circular slots (the Dashboard
   rank list's #2-5 rows, its Recent/Upcoming show thumb) also need their
   own smaller size — see redesign.css — this 22px was only ever sized for
   the roomier release/artist cover contexts. */
.image-slot .slot-icon{font-size:22px;line-height:1;opacity:0.35;pointer-events:none}
/* A Memory photo tile is readonly (see memoryGalleryHTML) but is clickable
   again as of the photo-lightbox viewer (TODO.md item 23, 2026-08-21) — this
   overrides just that one readonly slot back to a pointer cursor rather than
   touching .image-slot.readonly globally, since most other readonly slots
   (list-view mini-covers etc.) still aren't clickable. */
.photo-tile .detail-photo .image-slot.readonly{cursor:pointer}
/* A release cover (or, since TODO.md item 65, 2026-08-23, an artist photo
   too) with .has-lightbox (imageSlotHTML) opens the viewer below instead of
   the file picker directly — same reasoning, explicit pointer cursor rather
   than relying on the base .image-slot rule. Applies regardless of
   .readonly, since a viewer without upload rights can still view large. */
.image-slot.has-lightbox{cursor:pointer}

/* Click-to-enlarge photo viewer (TODO.md item 23, 2026-08-21; extended to
   artist photos plus Change/Delete rights by item 65, 2026-08-23) — shared
   by Memory photos, a release cover and an artist photo, see
   renderPhotoLightbox(). Reuses the ordinary .modal/.modal-overlay shell;
   only the image stage and its prev/next arrows are new. */
.lightbox-modal{max-width:560px}
.lightbox-counter{font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--ink-soft);margin-bottom:10px}
.lightbox-stage{position:relative;background:var(--bg-alt);border-radius:8px;overflow:hidden;
  display:flex;align-items:center;justify-content:center;min-height:220px}
.lightbox-stage img{width:100%;max-height:calc(var(--vh) * 64);object-fit:contain;display:block}
/* Same chevron icon as .gallery-nav/.scroll-nav elsewhere, but always
   visible (not hover-revealed) — this sits inside a modal a mouse and a
   touch user both reach the same way, unlike the strip those two are
   hover/swipe alternatives for. */
.lightbox-nav{position:absolute;top:50%;transform:translateY(-50%);width:34px;height:34px;border-radius:50%;
  border:none;background:rgba(9,12,18,.55);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center}
.lightbox-nav:hover{background:rgba(9,12,18,.8)}
.lightbox-nav.prev{left:10px}
.lightbox-nav.next{right:10px}
.lightbox-nav svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
.lightbox-shared-badge{display:inline-flex;align-items:center;margin-top:12px;font-size:11px;font-weight:600;
  background:var(--bg-alt);color:var(--ink-soft);padding:5px 10px;border-radius:20px}

/* modal */
.modal-overlay{position:fixed;inset:0;background:rgba(28,19,13,0.55);z-index:100;display:flex;align-items:center;justify-content:center;padding:20px}
.modal-overlay.delete-overlay{background:rgba(16,24,40,0.55);z-index:110}
/* .modal-frame (added 2026-08-25) wraps every scrollable modal (everything
   except the short delete-confirm dialogs, which never overflow and so
   were left as plain .modal with no frame) — see modalFrameOpenHTML()/
   modalFrameCloseHTML() in app.js. It owns the width/max-width sizing
   .modal used to own directly (hence .modal-frame.modal-wide instead of
   .modal.modal-wide — the modifier moved up one level, same "outer keeps
   the modifiers" split the dropdown-panel round already used for .right/
   .gsearch-panel) and is the non-scrolling, position:relative anchor the
   fade/arrow overlays sit against — .modal itself stays the actual
   scrolling element (unchanged: background/border/padding/max-height/
   overflow-y), just no longer in charge of its own width. */
.modal-frame{position:relative;width:100%;max-width:520px}
.modal-frame.modal-wide{max-width:960px}
.modal-frame.help-center-frame{max-width:820px}
.modal{background:var(--card);border:2px solid var(--ink);border-radius:8px;padding:32px;width:100%;max-height:calc(var(--vh) * 86);overflow-y:auto;position:relative;
  scrollbar-width:none}
.modal::-webkit-scrollbar{display:none}
.modal.delete-modal{max-width:400px;padding:28px}
.modal-close{position:absolute;top:16px;right:16px;background:none;border:none;font-size:20px;cursor:pointer;color:var(--ink-soft);line-height:1}

/* Fade hint at a scrolled modal's top/bottom edge (raised 2026-08-25,
   alongside hiding the native scrollbar above) — visibility is scroll-
   position-aware (unlike .vscroll-nav's simpler "any overflow at all"
   toggle below), toggled by updateFadeVisibility()/the capture-phase
   scroll listener in app.js: the top one only once you've actually
   scrolled down, the bottom one only while there's still more below,
   independently of each other. Sits in .modal-frame (not inside the
   scrolling .modal) for the same reason .vscroll-nav does — see its own
   comment on why an absolutely-positioned child of a scrolling box
   scrolls away with the content instead of staying put. */
.modal-fade{position:absolute;left:2px;right:2px;height:36px;pointer-events:none;opacity:0;transition:opacity .15s;z-index:1}
.modal-fade.top{top:2px;background:linear-gradient(to bottom, var(--card), transparent);border-radius:7px 7px 0 0}
.modal-fade.bottom{bottom:2px;background:linear-gradient(to top, var(--card), transparent);border-radius:0 0 7px 7px}
.modal-fade.visible{opacity:1}
.modal h2{font-size:24px;margin:0 0 18px}

/* Help Center (TODO.md item 7) — the sidebar-categories + article-list
   layout itself is redesign.css's .profile-layout/.profile-sidebar now
   (fase 14, 2026-09-14 — same rail Account/Admin already use), reused
   as-is rather than its own bespoke grid. What's left here is the article
   content and search box, page-agnostic either way. */
.help-center-article{margin-bottom:16px}
.help-center-article:last-child{margin-bottom:0}
.help-center-article .q{font-weight:700;font-size:13.5px;color:var(--ink);margin-bottom:4px}
.help-center-article .a{font-size:12.5px;color:var(--ink-soft);line-height:1.5}
/* Cross-category search (2026-08-25) — .help-center-search reuses the same
   look .form-input inputs use elsewhere rather than pulling that class in
   (this one has no data-bind, it's wired by hand, see bindHelpCenterSearch()
   in app.js). .help-center-result-tag is the small "which category" pill a
   search result carries that a normal browsed article doesn't need. mark
   inside an article highlights the matched substring — bindHelpCenterSearch()
   and helpCenterSearchResultsHTML() both build it the same way. */
.help-center-search{width:100%;box-sizing:border-box;padding:8px 12px;border-radius:6px;border:1px solid var(--line);background:var(--bg-alt);color:var(--ink);font-size:13px}
.help-center-search:focus{outline:2px solid var(--accent);outline-offset:1px}
.help-center-search-wrap .help-center-search{padding-right:30px}
.help-center-result-tag{display:inline-block;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--accent);background:var(--bg-alt);border-radius:999px;padding:2px 8px;margin-bottom:5px}
/* Shared with Release notes' own search highlight (2026-09-14, the owner:
   "kan dit ook bij de release notes") — same .au-card family, one rule. */
.help-center-article mark, .hc-row mark{background:var(--accent);color:var(--card);border-radius:2px;padding:0 2px}
.help-center-empty{font-size:12.5px;color:var(--ink-soft);padding:10px 0}

/* Guided tour (TODO.md item 7) — positioned/sized by updateTourOverlay()
   (app.js), these just supply the look. The highlight's huge box-shadow
   spread is the spotlight trick: one box sized to the target dims the rest
   of the viewport without an SVG mask or raising the target's own z-index.
   Scrim sits just below both, catching clicks outside the highlighted
   element (skips the tour, same click-outside-closes convention every
   modal already uses) — z-index ordering here matters since all three are
   position:fixed siblings in DOM order that doesn't match paint order. */
.tour-scrim{position:fixed;inset:0;z-index:298;cursor:pointer}
.tour-highlight{position:fixed;z-index:300;border-radius:8px;box-shadow:0 0 0 3px var(--accent),0 0 0 9999px rgba(0,0,0,0.6);pointer-events:none;transition:top .15s ease,left .15s ease,width .15s ease,height .15s ease}
.tour-tooltip{position:fixed;z-index:301;background:var(--card);border:1px solid var(--line);border-radius:8px;padding:14px 16px;width:270px;box-shadow:0 8px 24px rgba(0,0,0,0.3)}
.tour-tooltip-title{font-family:'Space Grotesk',sans-serif;font-weight:800;font-size:14px;margin-bottom:4px;color:var(--ink)}
.tour-tooltip-text{font-size:12.5px;color:var(--ink-soft);line-height:1.5}
.tour-tooltip-footer{display:flex;align-items:center;justify-content:space-between;margin-top:12px}
.tour-step-count{font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--ink-soft)}
.tour-tooltip-actions{display:flex;gap:16px}
.tour-tooltip-actions button{background:none;border:none;cursor:pointer;font-family:inherit;font-size:11.5px;padding:0}
.tour-tooltip-actions .tour-skip,.tour-tooltip-actions .tour-back{color:var(--ink-soft)}
.tour-tooltip-actions .tour-next{color:var(--accent);font-weight:700}

.choose-options{display:flex;flex-direction:column;gap:12px}
/* color is set explicitly here (unlike every other themed button in the
   app, which also sets its own color) because leaving it to inherit from
   the page left mobile Safari free to fall back to its native button text
   color instead — on a phone in OS dark mode that rendered as near-white
   text on this button's light card background, unreadable. Redesigned in
   redesign.css (2026-09-14, item c) — this base rule stays as the
   fallback shape/color, the icon-row look is layered on there. */
.choose-option{text-align:left;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:18px;cursor:pointer;width:100%}

.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
.form-input{background:var(--card);border:1px solid var(--line);border-radius:6px;padding:10px 12px;
  font-family:'IBM Plex Sans',sans-serif;font-size:13px;color:var(--ink);outline:none;width:100%}
.form-input:focus{border-color:var(--accent)}
/* A "+ Add {format}"/"+ Add {performer}" marker row (Tracklist/Setlist
   editors) in place of a text input — these rows can be moved or removed
   but never retyped (TODO.md item 24 follow-up, 2026-08-17), so there's
   nothing to focus. Tinted with the same --suggest-* tokens as the button
   that creates them, to visually tie the two together. */
.form-input-static{background:var(--suggest-bg);border:1px solid var(--suggest-border);border-radius:6px;padding:10px 12px;
  font-family:'IBM Plex Sans',sans-serif;font-size:13px;font-weight:700;color:var(--accent);width:100%;display:flex;align-items:center}
.span-2{grid-column:1/3}
/* Wraps a form field (plus, on the Discogs URL row, its Import button) so a
   red import-flag asterisk — or the button — can sit right after it without
   becoming its own .form-grid cell. */
.field-flagwrap{display:flex;align-items:center;gap:6px}
.field-flagwrap.span-2{grid-column:1/3}
.field-flagwrap .form-input{flex:1;min-width:0}
.import-flag{color:var(--danger);font-weight:700;font-size:16px;line-height:1;flex:none;cursor:help;margin-left:4px}
/* Tour per act (TODO.md item 139) — one row per act in the show form. */
.tour-rows{display:flex;flex-direction:column;gap:8px}
.tour-row{display:grid;grid-template-columns:minmax(90px,160px) 1fr;gap:10px;align-items:center}
.tour-row-who{font-size:13px;font-weight:600;min-width:0;overflow-wrap:anywhere}
.tour-row-who small{display:block;font-weight:400;font-size:11.5px;color:var(--ink-soft)}
.tour-input-wrap{position:relative}
.tour-input-sm{padding:7px 10px;font-size:13px}
.tour-src-chip{flex:none;font-family:'IBM Plex Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:.4px;border:1px solid var(--suggest-border);background:var(--suggest-bg);color:var(--accent);border-radius:5px;padding:2px 7px;white-space:nowrap}
@media (max-width:560px){.tour-row{grid-template-columns:1fr;gap:4px}}
/* Required-field marker (TODO.md item 109) — deliberately distinct from
   .import-flag above (muted grey on the label itself, not a bold red one
   floating after the input) so the two asterisks are never mistaken for each
   other on a field that happens to carry both. */
.req-star{color:var(--ink-soft);font-weight:700;margin-left:1px}
.form-required-legend{font-size:11px;color:var(--ink-soft);margin:-6px 0 14px}
.type-toggle{display:flex;flex-wrap:wrap;gap:4px;background:var(--bg);border-radius:8px;padding:3px;margin-bottom:14px;width:fit-content}
.type-toggle button{border:none;border-radius:6px;padding:7px 16px;font-size:12.5px;font-weight:600;cursor:pointer;background:transparent;color:var(--ink-soft)}
.type-toggle button.active{background:var(--ink);color:var(--bg)}

/* Add Release form's Collection/Wishlist toggle (2026-08-21) — "to your"
   label above it reads as one sentence with the toggle ("Add a release to
   your Collection/Wishlist"), same idiom as the Release Date precision
   toggle's own meta-label above it. .locked variant is Mark as bought's
   locked-to-Collection state (still shows which option is active, just not
   clickable). */
.to-your{font-family:'IBM Plex Mono',monospace;font-size:10.5px;text-transform:uppercase;color:var(--ink-soft);margin-bottom:6px}
.type-toggle.locked button{cursor:not-allowed}
.type-toggle.locked button.active{background:var(--ink-soft)}
.locked-note{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--ink-soft);margin:-8px 0 16px}
.locked-note svg{flex:none}
.field-full{width:100%;margin-bottom:12px}
textarea.form-input{height:70px;resize:vertical}
.submit-btn{width:100%;background:var(--accent);color:#fff;border:none;border-radius:8px;padding:12px;font-weight:700;font-size:13.5px;cursor:pointer}
.submit-btn:disabled{opacity:0.5;cursor:not-allowed}
.submit-btn:not(:disabled):hover{background:var(--accent-dark)}
/* 2FA recovery codes, shown once after enrolment (TODO.md item 48) */
.recovery-code-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;font-family:'IBM Plex Mono',monospace;font-size:13.5px;max-width:340px;margin:6px 0}
.recovery-code-grid span{background:var(--bg);border:1px solid var(--line);border-radius:5px;padding:8px 6px;text-align:center;letter-spacing:.05em}
/* Admin -> User management: contact/2FA/role/collection sub-sections inside
   an expanded row (TODO.md items 48 + 102) — the row/toggle shell itself is
   .hc-row/.hc-row-toggle now (redesign.css, screen audit 2026-09-15), these
   four stay as the plain stacked-block content inside it. */
.au-name{font-weight:600;font-size:13.5px}
.au-sub{display:block;font-size:11px;color:var(--ink-soft);margin-top:2px;word-break:break-word}
.au-pill{margin-left:auto;flex:none;font-family:'IBM Plex Mono',monospace;font-size:9.5px;font-weight:600;letter-spacing:.4px;text-transform:uppercase;padding:3px 8px;border-radius:999px}
.au-pill.on{background:color-mix(in srgb,var(--success) 16%,transparent);color:var(--success)}
.au-pill.off{background:var(--bg-alt);color:var(--ink-soft)}
.au-group{padding:12px 0;border-bottom:1px solid var(--line)}
.au-group:last-child{border-bottom:none;padding-bottom:2px}
.au-group-t{font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.7px;text-transform:uppercase;color:var(--ink-soft);margin-bottom:8px}
/* Admin → User management's Activity group (TODO.md item 142). */
.au-kv{display:grid;grid-template-columns:max-content 1fr;gap:5px 18px;font-size:12.5px}
.au-kv span{color:var(--ink-soft)}
.au-kv b{font-weight:600;font-variant-numeric:tabular-nums}

/* Admin → Usage / System health (TODO.md item 142). One chart component
   (usageChartSVG()): thin rounded bars on the accent, weekend/secondary bars
   a lighter tint of it, hatch-free "no data" boxes, dashed version markers.
   Every colour is a theme token or a color-mix of one, so dark mode needs
   no overrides of its own. */
.uc-stats{grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin-bottom:18px}
.uc-stats .stat-card{padding:16px}
.uc-stats .stat-value{font-size:32px}
.uc-of{font-size:15px;font-weight:600;color:var(--ink-soft)}
.uc-delta{font-size:11.5px;color:var(--ink-soft);margin-top:4px}
.uc-chart-title{font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:1px;text-transform:uppercase;color:var(--ink-soft);margin-bottom:6px}
.uc-chart-head{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px}
.uc-chart-head .uc-chart-title{margin:0}
.uc-chart-head .segmented button{padding:4px 11px}
.uc-chart{display:block;width:100%;height:auto;overflow:visible}
.uc-grid{stroke:color-mix(in srgb,var(--ink) 8%,var(--card));stroke-width:1}
.uc-axis{font-family:'IBM Plex Mono',monospace;font-size:10px;fill:var(--ink-soft)}
.uc-end{font-family:'IBM Plex Mono',monospace;font-size:10.5px;font-weight:600;fill:var(--ink)}
.uc-bar{fill:var(--accent)}
.uc-bar.is-muted{fill:color-mix(in srgb,var(--accent) 55%,var(--card))}
.uc-hit{fill:transparent}
.uc-col:hover .uc-bar{fill:var(--ink)}
.uc-col:hover .uc-hit{fill:color-mix(in srgb,var(--ink) 5%,transparent)}
.uc-nodata{fill:none;stroke:var(--line);stroke-dasharray:3 3}
.uc-line{fill:none;stroke:var(--accent);stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
.uc-dot{fill:var(--accent);stroke:var(--card);stroke-width:2}
.uc-marker{stroke:var(--ink-soft);stroke-width:1;stroke-dasharray:2 3}
.uc-legend{display:flex;flex-wrap:wrap;align-items:center;gap:4px 14px;font-size:11.5px;color:var(--ink-soft);margin-top:8px}
.uc-key{display:inline-block;width:9px;height:9px;border-radius:2px;background:var(--accent);margin-right:-8px}
.uc-key.is-muted{background:color-mix(in srgb,var(--accent) 55%,var(--card))}
.uc-key.is-line{height:2px;width:14px;border-radius:0}
.uc-key.is-marker{width:0;height:11px;border-left:1px dashed var(--ink-soft);background:none;border-radius:0}
.uc-two{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:22px}
.uc-table-wrap{overflow-x:auto}
.uc-table{border-collapse:collapse;width:100%;min-width:640px;font-size:12.5px;font-variant-numeric:tabular-nums}
.uc-table th,.uc-table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top}
.uc-table th{font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.7px;text-transform:uppercase;color:var(--ink-soft);font-weight:500}
.uc-table tr:last-child td{border-bottom:none}
.uc-strong{font-weight:600}
.uc-nowrap{white-space:nowrap}
.uc-ms-row{align-items:flex-start}
.uc-ms-icon{display:inline-block;width:18px;color:var(--warn-ink)}
.uc-ms-icon.is-manual{color:var(--accent)}
.uc-ms-form{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px 12px;align-items:center;background:var(--bg-alt);border-radius:6px;padding:14px;margin:4px 0 8px}
.uc-ms-form label{font-size:12px;font-weight:600;color:var(--ink-soft)}
.uc-ms-form .form-input{max-width:420px}
.uc-ms-actions{grid-column:2;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.uc-error{font-size:12px;color:var(--danger)}
.uc-api-head{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-top:4px}
.uc-api-value{font-size:28px}
.uc-meter{height:8px;border-radius:4px;background:var(--bg-alt);overflow:hidden;margin-top:8px}
.uc-meter > div{height:100%;border-radius:4px;background:var(--accent)}
.uc-meter.is-warn > div{background:var(--warn-ink)}
.uc-meter.is-bad > div{background:var(--danger)}
.uc-pill{flex:none;display:inline-flex;align-items:center;gap:4px;font-family:'IBM Plex Mono',monospace;font-size:10px;font-weight:600;letter-spacing:.4px;text-transform:uppercase;padding:3px 8px;border-radius:999px;white-space:nowrap}
.uc-pill.is-ok{background:color-mix(in srgb,var(--success) 16%,transparent);color:var(--success)}
.uc-pill.is-warn{background:var(--warn-bg);color:var(--warn-ink)}
.uc-pill.is-bad{background:color-mix(in srgb,var(--danger) 14%,transparent);color:var(--danger)}
.uc-pill.is-idle{background:var(--bg-alt);color:var(--ink-soft)}
.uc-feature-row{padding:9px 0}
.uc-feature-row .filter-row-label{font-weight:500}
.uc-feature-n{font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:15px;font-variant-numeric:tabular-nums}
.uc-feature-n.is-zero{color:var(--ink-soft);font-weight:500}
.uc-err{padding:10px 0;border-bottom:1px solid var(--line)}
.uc-err:last-of-type{border-bottom:none}
.uc-err-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.uc-err-msg{font-family:'IBM Plex Mono',monospace;font-size:12px;margin-top:6px;word-break:break-word}
.uc-err-where{font-family:'IBM Plex Mono',monospace;margin-top:2px;word-break:break-all}
@media (max-width:600px){
  .uc-stats{grid-template-columns:1fr 1fr}
  .uc-stats .stat-value{font-size:26px}
  .uc-ms-form{grid-template-columns:1fr}
  .uc-ms-actions{grid-column:1}
}
.au-val{font-size:12.5px;margin-bottom:9px;line-height:1.5}
.au-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.modal-p{font-size:13.5px;color:var(--ink-soft);margin:0 0 22px;line-height:1.5}
.modal-actions{display:flex;gap:10px}

/* Release notes / Help Center articles (fase 15, 2026-09-14) — both now
   .hc-row rows (the same collapsed-by-default row Feedback/Admin Users use,
   redesign.css — replaced .au-card/.au-head/.au-body in the 2026-09-15
   screen audit), so only their own label text needs styling here. */
.release-note-v{font-size:13.5px;font-weight:700;color:var(--ink);display:flex;align-items:baseline;gap:8px}
.release-note-date{font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:400;color:var(--ink-soft)}
/* Search box (2026-09-14, the owner: "zoals bij Help Center") — sizing now
   comes from .panel-search-row .search-field (redesign.css, item 121
   follow-up); .search-field is inline-block by default, so without a
   width rule from one place or the other it'd shrink-wrap to its input's
   min-width instead of sharing the row with Expand all/Collapse all. */
.help-center-q{font-size:13px;font-weight:600;color:var(--ink)}
.help-center-a{font-size:12.5px;color:var(--ink-soft);line-height:1.5}
.hc-row-body ul{margin:0;padding-left:20px}
.hc-row-body li{font-size:13.5px;color:var(--ink-soft);line-height:1.5;margin-bottom:6px}
.hc-row-body li:last-child{margin-bottom:0}
.chevron-mark{display:inline-block;font-size:18px;color:var(--ink-soft);line-height:1;
  transform:rotate(0deg);transition:transform .15s ease;flex:none}
.chevron-mark.open{transform:rotate(90deg)}

/* Connections page (TODO.md item 104 decision c) */
.conn-find-row{display:flex;gap:8px;align-items:center}
.conn-find-row .search-field{flex:1;min-width:0}
.conn-find-row .btn-outline{flex:none;white-space:nowrap}
.conn-req{padding:10px 0;border-top:1px solid var(--line)}
.conn-req:first-of-type{border-top:none}
.conn-req-name{font-size:13px;font-weight:600}
.conn-req-sub{font-size:11.5px;color:var(--ink-soft);margin:2px 0 8px}
.conn-req-actions{display:flex;gap:8px;flex-wrap:wrap}
/* Sharing & discovery — cap the toggle rows on desktop so they don't span
   the whole panel (the owner, 2026-09-04); full width on a phone. No hairlines
   between rows (spacing via gap), one divider above "Default sharing" to
   split it from "Who can find you". */
.conn-disc-body{max-width:460px;display:flex;flex-direction:column;gap:14px}
.conn-toggle-row{display:flex;flex-direction:column;align-items:stretch;gap:6px}
.conn-disc-sep{border-top:1px solid var(--line);padding-top:14px;margin-top:16px}
@media (max-width:640px){
  .conn-disc-body{max-width:none}
}
.btn-danger{flex:1;background:var(--danger);color:#fff;border:none;border-radius:8px;padding:11px;font-weight:700;font-size:13.5px;cursor:pointer}
/* color set explicitly for the same reason as .choose-option (see its own
   comment) — this is the other button in the app that only set background,
   never color, and was exposed to the same native-button-text-color leak
   on mobile Safari (nearly invisible white "Cancel" text). */
.btn-cancel{flex:1;background:none;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:11px;font-weight:600;font-size:13px;cursor:pointer}

/* import review table */
.import-table-wrap{max-height:calc(var(--vh) * 52);overflow-y:auto;overflow-x:auto;border:1px solid var(--line);border-radius:6px;margin-bottom:16px}
.import-table{width:100%;border-collapse:collapse;font-size:12.5px}
.import-table th{position:sticky;top:0;background:var(--bg-alt);text-align:left;padding:9px 10px;font-family:'IBM Plex Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:0.5px;color:var(--ink-soft);border-bottom:1px solid var(--line)}
.import-table td{padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:middle}
.import-table tr:last-child td{border-bottom:none}
.import-table tr.invalid{opacity:0.45}
.import-table .import-extra{color:var(--ink-soft);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Collection Table view reuses .import-table's look (sticky mono header,
   hairline rows) via .data-table, plus sortable-header and clickable-row
   behavior that the import review table doesn't need. */
.data-table-wrap{max-height:none}
.data-table th.sortable{cursor:pointer;user-select:none}
.data-table th.sortable:hover{color:var(--ink)}
.data-table th .sort-arrow{display:inline-block;width:9px;font-size:9px;opacity:0.45;margin-left:2px}
.data-table th.sorted .sort-arrow{opacity:1;color:var(--accent)}
.data-table tbody tr.clickable{cursor:pointer}
.data-table tbody tr.clickable:hover td{background:var(--bg-alt)}
.dup-badge{display:inline-block;margin-left:6px;font-family:'IBM Plex Mono',monospace;font-size:9.5px;text-transform:uppercase;background:var(--bg-alt);color:var(--ink-soft);padding:2px 8px;border-radius:999px;white-space:nowrap}
.type-toggle.small{margin:0;padding:2px}
.type-toggle.small button{padding:5px 10px;font-size:11px}
.import-summary{font-size:12.5px;color:var(--ink-soft);margin-bottom:10px}
.import-actions{display:flex;gap:10px;justify-content:flex-end}
.import-actions .submit-btn{width:auto;padding:12px 20px}
/* "Import from Discogs/Setlist.fm" review modal (2026-09-21 follow-up,
   TODO.md item 130) — renderReimportReviewModal(). Checkbox rows for What,
   .segmented.fill (already used elsewhere) for How, then a diff row per
   field/section with a small tag on the right — same "value + a status
   pill" shape .dup-badge above already uses, just per-row instead of
   inline after a title. */
.reimport-check-group{display:flex;flex-direction:column;gap:8px;margin-top:4px}
.reimport-check{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;cursor:pointer;font-size:13px}
.reimport-check input{margin-top:2px}
.reimport-check-text{font-size:11.5px;color:var(--ink-soft);font-weight:400}
.reimport-section-label{font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.1em;text-transform:uppercase;color:var(--ink-soft);margin-bottom:6px}
.reimport-row{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--line)}
.reimport-row:last-child{border-bottom:none}
.reimport-tag{flex:none;font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:0.06em;text-transform:uppercase;padding:2px 8px;border-radius:999px;white-space:nowrap;margin-top:1px}
.reimport-tag.new{background:var(--suggest-bg);color:var(--accent);border:1px solid var(--suggest-border)}
.reimport-tag.kept{background:none;color:var(--ink-soft);border:1px solid var(--line)}
.reimport-tag.changed{background:var(--warn-bg);color:var(--warn-ink);border:1px solid var(--warn-border)}
.reimport-empty{font-size:12px;color:var(--ink-soft);font-style:italic}
/* Before → after view (TODO.md item 133): summary chips, a Field / Now /
   After import table, and the tracklist/setlist as a big "N → M" count
   with an optional per-act breakdown for a multi-act show. */
.reimport-subtitle{font-size:12.5px;color:var(--ink-soft);margin-bottom:14px}
.reimport-tag.removed{background:none;color:var(--danger);border:1px solid var(--danger)}
.reimport-chips{display:flex;flex-wrap:wrap;gap:6px}
.reimport-chip{border:1px solid var(--line);border-radius:8px;padding:5px 10px;font-size:12px;background:var(--card)}
.reimport-chip strong{font-family:'Space Grotesk',sans-serif;font-size:13.5px}
.reimport-chip.new{background:var(--suggest-bg);border-color:var(--suggest-border)}
.reimport-chip.changed{background:var(--warn-bg);border-color:var(--warn-border)}
.reimport-chip.unchanged{color:var(--ink-soft)}
.reimport-table{display:grid;grid-template-columns:minmax(80px,110px) minmax(0,1fr) minmax(0,1fr) auto;border-top:1px solid var(--line);font-size:12.5px}
.reimport-table>div{padding:7px 8px 7px 0;border-bottom:1px solid var(--line);min-width:0;overflow-wrap:anywhere}
.reimport-table>div:nth-child(4n){padding-right:0;text-align:right}
.reimport-th{font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:0.06em;text-transform:uppercase;color:var(--ink-soft)}
.reimport-k{font-weight:600}
.reimport-sub{font-size:11px;font-weight:400;color:var(--ink-soft)}
.reimport-muted{color:var(--ink-soft);font-weight:400}
.reimport-strike{text-decoration:line-through;color:var(--ink-soft)}
.reimport-cover{width:34px;height:34px;border-radius:4px;object-fit:cover;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}
.reimport-cover.empty{border:1px dashed var(--line);font-size:10px;color:var(--ink-soft);width:auto;min-width:34px;padding:0 4px}
.reimport-count{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:4px}
.reimport-big{font-family:'Space Grotesk',sans-serif;font-size:22px;font-weight:700}
.reimport-acts{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;gap:6px 14px;align-items:center;font-size:12.5px;margin:8px 0 6px}
.reimport-acts .reimport-tag{justify-self:end}
/* The "which tracks/songs?" expandable breakdown behind the Tracklist/
   Setlist summary row (2026-09-21 follow-up) — a plain scrolling list, not
   another .reimport-row (that shape assumes one row per *field*; here it's
   one row per track/song, often 10-30 of them, so a tighter/scrollable
   list reads better than the field rows' looser spacing). */
.reimport-content-list{max-height:220px;overflow-y:auto;border:1px solid var(--line);border-radius:8px;padding:4px 10px;margin-top:8px}
.reimport-content-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 0;font-size:12.5px;border-bottom:1px solid var(--line)}
.reimport-content-row:last-child{border-bottom:none}
.reimport-content-heading{font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:0.08em;text-transform:uppercase;color:var(--ink-soft);padding:8px 0 4px}
.reimport-content-heading:first-child{padding-top:6px}
.alias-actions{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:16px}
.alias-add-row{display:flex;gap:8px;margin-bottom:12px}
.alias-add-row .form-input{flex:1;min-width:0}
.alias-table td:last-child,.alias-table th:last-child{text-align:center}
.alias-added-tag{font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--ink-soft);margin-left:6px}
/* Every row in a connected-service sync review is already matched to an
   exact release (unlike xlsx import, which needs a per-row Discogs search) —
   same green as .import-flag's opposite number, --success. */
.matched{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:600;color:var(--success)}

/* Account -> Connected services (TODO.md item 130) — one card per service. */
.cs-panel.pending{border-color:var(--accent);border-width:2px}
.cs-panel.placeholder{border-style:dashed;opacity:0.6}
.cs-card{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}
.cs-card-id{display:flex;align-items:center;gap:12px}
.cs-icon{flex:none;width:40px;height:40px;border-radius:10px;background:var(--bg-alt);display:flex;align-items:center;justify-content:center}
.cs-icon svg{width:20px;height:20px;stroke:var(--ink);fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.cs-name{font-weight:700;font-size:14.5px}
.cs-status{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:600;color:var(--success);margin-top:2px}
.cs-sub{font-size:11.5px;color:var(--ink-soft);margin-top:2px}
.cs-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.cs-stats{display:flex;gap:28px;margin-top:20px;padding-top:18px;border-top:1px solid var(--line)}
.cs-stat-value{font-family:'IBM Plex Mono',monospace;font-size:20px;font-weight:700;color:var(--ink)}
.cs-stat-label{font-size:11px;color:var(--ink-soft);margin-top:2px}

/* Sync review "link existing" (TODO.md item 130, 2026-09-21) — a soft
   duplicate row gets Skip/Import new/Link existing instead of a plain
   checkbox, plus a Compare toggle that opens a two-card side-by-side
   comparison ("Your copy" vs the incoming service data) beneath the row,
   so linking is a checked decision, not a guess off a title match alone. */
.link-hint{font-size:11px;color:var(--accent);margin-top:6px;display:flex;align-items:center;gap:5px;cursor:pointer;background:none;border:none;padding:0;font-family:inherit}
.compare-row td{padding-top:0}
.compare-panel{display:flex;align-items:stretch;gap:12px;margin:2px 0 6px}
.compare-col{flex:1;min-width:0}
.compare-label{font-family:'IBM Plex Mono',monospace;font-size:9.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-soft);margin-bottom:5px}
.compare-card{display:flex;gap:10px;background:var(--card);border:1px solid var(--line);border-radius:var(--r-radius-sm);padding:10px}
.compare-card.is-mine{border-style:dashed}
.compare-cover{width:52px;height:52px;border-radius:6px;flex:none;background:var(--bg-alt);object-fit:cover}
.compare-date{width:52px;height:52px;border-radius:6px;flex:none;background:var(--bg-alt);display:flex;align-items:center;justify-content:center;text-align:center;font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:700;color:var(--ink);line-height:1.3;padding:2px}
.compare-body{min-width:0}
.compare-title{font-size:12.5px;font-weight:700;color:var(--ink)}
.compare-meta{font-size:11px;color:var(--ink-soft);margin-top:2px}
.compare-arrow{flex:none;align-self:center;color:var(--ink-soft);font-size:16px}

/* discogs search results */
.discogs-results-row td{background:var(--bg-alt);padding:12px 10px}
.discogs-results-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:8px}
/* TODO.md item 38 (2026-08-23): .discogs-result used to be one big <button>
   — the whole card committed to linking on any click. It's a plain wrapper
   now: "View details" (opens a read-only preview, see .modal further down)
   and "Link to this" (the actual commit, same attachAlbum()/
   attachConcert() as before) are separate buttons in .discogs-result-actions
   below, so looking closer at a candidate first no longer risks linking to
   the wrong one by mistake. */
.discogs-result{text-align:left;background:var(--card);border:1px solid var(--line);border-radius:6px;padding:10px 12px}
/* Same title+date (show) / title+artist (release) as what's being logged —
   the strongest possible signal this is a re-log of an existing item, so
   it's called out instead of blending in with the rest of the (much
   looser) candidate list. */
.discogs-result.exact-match{border-color:var(--accent);background:var(--bg-alt)}
/* Close but not identical — a likely typo, not a confirmed duplicate, so a
   distinct (warmer, less alarming) treatment from exact-match's accent
   border — see --warn-* tokens' own comment at the top of this file. */
.discogs-result.fuzzy-match{border-color:var(--warn-border);background:var(--warn-bg)}
.discogs-result-title{font-weight:700;font-size:12.5px}
.discogs-result-meta{font-size:11px;color:var(--ink-soft);margin-top:3px}
.discogs-result-actions{display:flex;gap:8px;margin-top:9px}
.discogs-result-actions .btn-outline{padding:7px 10px;font-size:11px}
.discogs-result-actions .submit-btn{flex:1;padding:7px 10px;font-size:11px;border-radius:6px}
.dup-badge.fuzzy-badge{background:var(--warn-bg);color:var(--warn-ink);border:1px solid var(--warn-border)}

/* Admin Updates (TODO.md — Help Center expansion, 2026-08-24) — admin-
   postable announcements, deliberately separate from RELEASE_NOTES. See
   ADMIN_TABS/renderAdminUpdates() and HELP_CENTER_CATEGORIES's "updates"
   category in app.js/help.js. Type picker reuses the app's existing
   .year-chip selectable-pill component rather than a new one. */
.update-badge{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:600;
  padding:3px 10px;border-radius:100px;border:1px solid var(--line);color:var(--ink-soft);
  background:var(--bg-alt);white-space:nowrap}
.update-badge::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0}
.update-badge.announcement{background:var(--suggest-bg);border-color:var(--suggest-border);color:var(--accent)}
.update-badge.feature{background:var(--feature-bg);border-color:var(--feature-border);color:var(--success)}
.update-badge.maintenance{background:var(--warn-bg);border-color:var(--warn-border);color:var(--warn-ink)}
.update-badge.fix{background:var(--fix-bg);border-color:var(--fix-border);color:var(--olive)}

.update-type-picker{display:flex;gap:8px;flex-wrap:wrap}

.update-list .update-row{display:flex;flex-direction:column;gap:6px;padding:14px 16px;border-bottom:1px solid var(--line)}
.update-list .update-row:last-child{border-bottom:none}
.update-row-head{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
.update-row-title-line{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.update-row-title{font-weight:700;font-size:14px}
.update-row-date{font-size:11.5px;color:var(--ink-soft);white-space:nowrap}
.update-row-msg{font-size:13px;color:var(--ink-soft);line-height:1.5;white-space:pre-wrap}
.update-row-actions{display:flex;gap:6px;flex-shrink:0}

.help-feed-item{padding:14px 0;border-bottom:1px solid var(--line)}
.help-feed-item:first-child{padding-top:0}
.help-feed-item:last-child{border-bottom:none}
.help-feed-head{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:6px}
.help-feed-title{font-weight:700;font-size:14px}
.help-feed-date{font-size:11px;color:var(--ink-soft);margin-left:auto;white-space:nowrap}
.help-feed-msg{font-size:13px;color:var(--ink-soft);line-height:1.55;white-space:pre-wrap}

/* Send Feedback to Admin (TODO.md item 19, Help Center expansion, 2026-08-24;
   reworked TODO.md items 99+100, 2026-09-03) — see feedbackCardHTML()/
   feedbackThreadHTML()/renderProfileFeedback()/renderAdminFeedback() in app.js.
   The feedback type is a system label (like a format or show-type badge), so
   it uses the same filled bdg-system slate and reads clearly as a pill next
   to the tinted status badge. .feedback-status and .status-chip.sel share the
   5-state colour map (New / In progress / Backlog / Won't fix / Resolved),
   reusing the suggest, warn and feature token families rather than inventing
   new ones.
   NOTE: keep "star-slash" pairs out of this comment block — an earlier
   version wrote the token names as "--suggest-<star>/--warn-<star>" and the
   "<star>/" closed the comment early, which silently killed the whole
   .feedback-type-tag rule for months (found 2026-09-03). */
.feedback-type-tag{display:inline-flex;align-items:center;font-family:'IBM Plex Mono',monospace;font-size:9.5px;
  font-weight:600;text-transform:uppercase;letter-spacing:0.6px;background:var(--bdg-system);border:1px solid transparent;
  color:#fff;padding:3px 9px;border-radius:100px;white-space:nowrap;line-height:1.6;vertical-align:middle}
.feedback-status{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;
  padding:3px 9px;border-radius:100px;border:1px solid var(--line);white-space:nowrap}
.feedback-status::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0}
.feedback-status.new{background:var(--suggest-bg);border-color:var(--suggest-border);color:var(--accent)}
.feedback-status.progress{background:var(--warn-bg);border-color:var(--warn-border);color:var(--warn-ink)}
.feedback-status.backlog{background:var(--bg-alt);border-color:var(--line);color:var(--ink-soft)}
.feedback-status.wontfix{background:none;border-color:var(--danger);color:var(--danger)}
.feedback-status.resolved{background:var(--feature-bg);border-color:var(--feature-border);color:var(--success)}

/* Feedback reply threads + Suggest-an-edit + Locked/Archived (TODO.md items
   99+100, 2026-09-03) — see feedbackThreadHTML()/feedbackComposerHTML()/
   feedbackCardHTML()/renderProfileFeedback()/renderSuggestEditModal().
   Both Admin → Feedback and Account → Feedback reuse the User-management
   .hc-row expandable-row look (redesign.css); only the row body is
   feedback-specific. */
.status-chip{font-family:inherit;font-size:11px;font-weight:600;padding:5px 10px;border-radius:100px;
  border:1px solid var(--line);background:var(--card);color:var(--ink-soft);cursor:pointer}
.status-chip:hover{border-color:var(--ink-soft)}
.status-chip.sel.new{background:var(--suggest-bg);border-color:var(--suggest-border);color:var(--accent)}
.status-chip.sel.progress{background:var(--warn-bg);border-color:var(--warn-border);color:var(--warn-ink)}
.status-chip.sel.backlog{background:var(--bg-alt);border-color:var(--ink-soft);color:var(--ink-soft)}
.status-chip.sel.wontfix{background:none;border-color:var(--danger);color:var(--danger)}
.status-chip.sel.resolved{background:var(--feature-bg);border-color:var(--feature-border);color:var(--success)}

.fb-date{font-size:11.5px;color:var(--ink-soft)}
/* The collapsed-card title line — type pill / status pill / flag / subject,
   spaced so they don't crowd. The subject is the readable "what is this". */
.fb-card-title{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.fb-card-subject{font-weight:600;font-size:13px;color:var(--ink)}
/* Feedback card body needs real top room — its first child is a bubble or a
   ref chip, unlike the User card's .au-group which spaces itself. And the
   composer (status + reply + Send) is set off from the thread by a rule so
   it reads as one unit. */
.hc-row-body.fb-card-body{padding-top:15px}
.fb-card-body > .fb-composer-wrap,
.fb-card-body > .fb-closed-note{border-top:1px solid var(--line);margin-top:12px;padding-top:14px}
.fb-hint.accent{color:var(--accent)}
/* The click-through chip to the release/show an item_edit is about. */
.fb-ref{display:inline-flex;align-items:center;gap:6px;font-family:inherit;font-size:11.5px;font-weight:600;
  color:var(--accent);background:var(--suggest-bg);border:1px solid var(--suggest-border);border-radius:6px;
  padding:4px 9px;cursor:pointer;width:fit-content;max-width:100%;margin-bottom:8px}
.fb-ref svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.4;flex:none}
.fb-ref-kind{font-family:'IBM Plex Mono',monospace;font-size:9px;text-transform:uppercase;letter-spacing:0.5px;opacity:0.65}
.fb-ref:hover{background:var(--suggest-border);color:#fff}
/* Locked / Archived pill. */
.fb-flag{font-family:'IBM Plex Mono',monospace;font-size:9px;text-transform:uppercase;letter-spacing:0.5px;
  padding:2px 8px;border-radius:999px;border:1px solid}
.fb-flag.locked{background:var(--bg-alt);border-color:var(--ink-soft);color:var(--ink-soft)}
.fb-flag.archived{background:rgba(179,38,30,0.09);border-color:var(--danger);color:var(--danger)}
.hc-row.fb-archived{opacity:0.7}
.fb-hint{font-family:'IBM Plex Mono',monospace;font-size:9.5px;text-transform:uppercase;letter-spacing:0.4px}
.fb-hint.warn{color:var(--warn-ink)}
/* Chat-style thread. "us" = own side (admin replies on the admin view, the
   sender's replies on the sender view). First bubble is the original message.
   The card is full width (uniform with Admin) but the conversation is capped
   to a centred ~600px column so the bubbles don't fly to opposite screen
   edges — they overlap the centred status pills instead (the owner, 2026-09-03,
   thread-bubbles mockup Option A). The cap is inert below the mobile
   breakpoint, where the thread is already screen width. The composer / status
   chips / Lock-Archive-Delete row stay full width — they're controls. */
.fb-thread{display:flex;flex-direction:column;gap:7px;margin:2px auto 10px;max-width:600px}
.fb-thread-start{align-self:center;display:flex;align-items:center;gap:6px;font-family:'IBM Plex Mono',monospace;
  font-size:9.5px;letter-spacing:0.6px;text-transform:uppercase;color:var(--ink-soft);padding:2px 4px 4px}
.fb-bubble{max-width:78%;padding:7px 11px;border-radius:11px;border:1px solid var(--line)}
.fb-bubble.them{align-self:flex-start;background:var(--bg-alt)}
.fb-bubble.us{align-self:flex-end;background:var(--suggest-bg);border-color:var(--suggest-border)}
.fb-bubble-who{font-family:'IBM Plex Mono',monospace;font-size:9px;text-transform:uppercase;letter-spacing:0.4px;
  color:var(--ink-soft);margin-bottom:3px}
.fb-bubble-body{font-size:12.5px;line-height:1.45;white-space:pre-wrap;color:var(--ink)}
/* A status change inside the thread — centred, not a bubble. */
.fb-sysline{align-self:center;display:flex;align-items:center;gap:6px;font-size:11px;color:var(--ink-soft);
  background:var(--bg);border:1px solid var(--line);border-radius:100px;padding:3px 11px}
.fb-sysline-time{opacity:0.7;font-size:10px}
@media (max-width:640px){
  /* Cap is inert here — the thread is already screen width; let bubbles use it. */
  .fb-bubble{max-width:86%}
}
.fb-composer-wrap{display:flex;flex-direction:column;gap:8px;margin-top:4px}
.fb-status-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.fb-status-row-label{font-family:'IBM Plex Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;color:var(--ink-soft);margin-right:2px}
.fb-status-hint{font-size:11px;color:var(--warn-ink)}
.fb-composer{display:flex;gap:8px;align-items:flex-start}
.fb-composer textarea{flex:1;min-height:38px;resize:vertical}
.fb-closed-note{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--ink-soft);
  background:var(--bg-alt);border:1px solid var(--line);border-radius:6px;padding:8px 11px}
.fb-closed-note svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2;flex:none}
.fb-admin-toggles{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;border-top:1px solid var(--line);padding-top:12px}
.fb-toggle{display:inline-flex;align-items:center;gap:5px;font-family:inherit;font-size:11px;font-weight:600;
  padding:5px 10px;border-radius:100px;border:1px solid var(--line);background:var(--card);color:var(--ink-soft);cursor:pointer}
.fb-toggle svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2}
.fb-toggle:hover{border-color:var(--ink-soft)}
.fb-toggle:disabled{opacity:0.4;cursor:not-allowed}
.fb-toggle.on{background:var(--bg-alt);border-color:var(--ink-soft);color:var(--ink)}
.fb-toggle.arch.on{background:rgba(179,38,30,0.09);border-color:var(--danger);color:var(--danger)}
.fb-toggle.del{color:var(--danger);margin-left:auto}
.fb-toggle.del:hover{border-color:var(--danger);background:rgba(179,38,30,0.09)}
.fb-archived-toggle{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:var(--ink-soft);cursor:pointer}
/* The Suggest-an-edit modal's item card. The action-row icon itself is a
   plain .icon-btn — same style as History / Private / Unlink beside it. */
.suggest-edit-item{display:flex;align-items:center;gap:10px;background:var(--bg);border:1px solid var(--line);
  border-radius:8px;padding:10px 12px;margin:0 0 14px}
.suggest-edit-item-ico{width:15px;height:15px;fill:none;stroke:var(--accent);stroke-width:2.4;flex:none}
.suggest-edit-item-title{font-weight:600;font-size:13px}
.suggest-edit-item-kind{font-family:'IBM Plex Mono',monospace;font-size:9.5px;text-transform:uppercase;letter-spacing:0.5px;color:var(--ink-soft);margin-top:1px}

/* Barcode-scan result rows (TODO.md item 34) — a full-width list instead of
   the compact .discogs-result grid above, since each row now carries a lot
   more info (cover, format text, barcode, Discogs popularity/link) than the
   grid's 240px cards have room for. */
.barcode-results-list{display:flex;flex-direction:column;gap:8px}
.barcode-result{background:var(--card);border:1px solid var(--line);border-radius:8px;overflow:hidden}
.barcode-result:hover{border-color:var(--accent)}
.barcode-result-main{display:flex;gap:12px;width:100%;text-align:left;background:none;border:none;
  padding:10px 12px;cursor:pointer;font-family:inherit;color:inherit}
.barcode-result-main:disabled{opacity:0.55;cursor:not-allowed}
.barcode-result-info{min-width:0;flex:1}
.barcode-result-title{font-weight:700;font-size:12.5px}
.barcode-result-meta{font-size:11px;color:var(--ink-soft);margin-top:3px;line-height:1.5}
.barcode-result-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:7px 12px;border-top:1px solid var(--line);background:var(--bg-alt)}
.barcode-result-owners{font-size:10.5px;color:var(--ink-soft)}
.barcode-result-discogs-link{font-size:10.5px;font-weight:600;color:var(--accent)}
.barcode-result-discogs-link:hover{color:var(--accent-dark);text-decoration:underline}
/* Add-time match at Discogs / Setlist.fm (TODO.md item 138). The hint sits
   under the form fields and is live-patched (refreshReleaseMatchHint()/
   refreshShowMatchHint()); the picker reuses the barcode result card plus a
   cover from GET /api/discogs/thumb/:id. */
.match-hint{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin:14px 0 12px;background:var(--suggest-bg);border:1px solid var(--suggest-border);border-radius:8px;padding:8px 12px;font-size:12.5px}
.match-hint.muted{background:none;border-color:var(--line);color:var(--ink-soft);font-size:12px}
.match-hint-basis{color:var(--ink-soft)}
.match-hint .match-hint-open{margin-top:0;font-size:12px}
.barcode-result-cover{flex:none;width:56px;height:56px;border-radius:4px;object-fit:cover;background:var(--bg-alt)}
.match-filters{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}
.match-filter-row{display:flex;flex-wrap:wrap;gap:6px}
.match-filter-chip{border:1px solid var(--line);border-radius:999px;padding:4px 11px;font-size:11.5px;font-weight:600;background:var(--card);color:var(--ink-soft);cursor:pointer;font-family:inherit}
.match-filter-chip.active{background:var(--suggest-bg);border-color:var(--suggest-border);color:var(--accent)}
.match-group-label{font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:0.08em;text-transform:uppercase;color:var(--ink-soft);margin:14px 0 6px}
.match-card{background:var(--suggest-bg);border:1px solid var(--suggest-border);border-radius:8px;padding:8px 12px;margin:14px 0 12px;font-size:12.5px}
.match-card-row{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:4px 0}
.match-card-row + .match-card-row{border-top:1px solid var(--suggest-border)}
.match-card-info{min-width:0;flex:1}
.match-card-meta{font-size:11.5px;color:var(--ink-soft)}
.match-card-actions{display:flex;align-items:center;gap:10px;flex:none}
.match-card .match-use-btn{width:auto;padding:6px 14px;font-size:12px;line-height:1;border-radius:999px}
.match-card-others{border-top:1px solid var(--suggest-border);margin-top:6px;padding-top:6px}
.match-card-other{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:3px 0}
.match-card-other .link-hint{margin-top:0;font-size:12px}

/* barcode scan (TODO.md item 34, 2026-08-18) */
.barcode-scan-btn{width:100%;display:flex;align-items:center;justify-content:center}

/* Add/Edit release form: Discogs-import panel at the top of the form, and
   the "or enter manually" divider below it (TODO.md item 34 follow-up,
   reordered 2026-08-20 — mockup approved by the owner). Redesigned in
   redesign.css (2026-09-14, item e-1 follow-up) — this stays the fallback
   shape/color; .barcode-scan-divider/.album-manual-divider themselves were
   retired in favor of one shared .form-divider (redesign.css) once both
   needed the same flanking-line look. */
.album-import-panel{background:var(--bg-alt);border:1px solid var(--line);border-radius:8px;padding:16px;margin-bottom:20px}
.album-import-label{font-family:'IBM Plex Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:0.5px;color:var(--ink-soft);margin-bottom:10px}
.barcode-scanner-wrap{display:flex;flex-direction:column;align-items:center;gap:6px}
.barcode-scanner{width:100%;max-width:320px;aspect-ratio:3/4;border-radius:16px;background:var(--vinyl);position:relative;overflow:hidden;border:4px solid var(--accent-dark)}
.barcode-video-wrap{position:absolute;inset:0}
.barcode-video-wrap video{width:100%;height:100%;object-fit:cover}
.barcode-scan-target{position:absolute;inset:22% 12%;pointer-events:none}
.barcode-corner{position:absolute;width:22px;height:22px;border-color:rgba(255,255,255,0.85)}
.barcode-corner.tl{top:0;left:0;border-top:3px solid;border-left:3px solid;border-radius:6px 0 0 0}
.barcode-corner.tr{top:0;right:0;border-top:3px solid;border-right:3px solid;border-radius:0 6px 0 0}
.barcode-corner.bl{bottom:0;left:0;border-bottom:3px solid;border-left:3px solid;border-radius:0 0 0 6px}
.barcode-corner.br{bottom:0;right:0;border-bottom:3px solid;border-right:3px solid;border-radius:0 0 6px 0}
.barcode-scan-line{position:absolute;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,#7fe3a8,transparent);top:0;animation:barcodeSweep 2.4s ease-in-out infinite}
@media (prefers-reduced-motion: reduce){.barcode-scan-line{animation:none;top:50%}}
@keyframes barcodeSweep{0%,100%{top:6%}50%{top:94%}}
.barcode-scan-caption{position:absolute;left:0;right:0;bottom:14px;text-align:center;font-family:'IBM Plex Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:1px;color:rgba(255,255,255,0.8);pointer-events:none}
.btn-text-danger{background:none;border:none;color:var(--danger);font-size:12.5px;font-weight:600;cursor:pointer;padding:8px 0}
.btn-text{background:none;border:none;color:var(--accent);font-size:12.5px;font-weight:600;cursor:pointer;padding:0;
  display:inline-flex;align-items:center;gap:4px}
.btn-text svg{width:13px;height:13px;flex:none;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.btn-text:hover{color:var(--accent-dark)}

/* Wide content on a narrow phone in portrait: the Collection Table view
   (columns reachable only via horizontal scroll) and the Shows Map view
   (choropleth cramped into a portrait sliver) both read far better in
   landscape — nudge towards that instead of leaving people to discover it.
   Hidden by default (desktop/tablet and landscape); shown only under the
   mobile-portrait media query below. Re-evaluates live on rotation, no JS
   needed. */
.table-rotate-hint{display:none}

/* TODO.md item 9 (2026-08-25) — this whole block used to be pure
   max-width, so a phone in landscape (often wider than 640px) fell back to
   the full desktop header: nav row + header-actions both inline, wrapping
   to 2 lines on a screen that's short exactly where that hurts most. Widened
   to also catch "a touch device, however wide, on a short screen" — same
   `(hover:none),(pointer:coarse)` touch-detection idiom the tracklist drag
   handle already uses (style.css, TODO.md item 11), ANDed with a height cap
   comfortably above any real phone's landscape height (~430px at most) but
   well below a landscape tablet's (768px+), so a tablet or a resized
   desktop window is unaffected either way. The owner's call after seeing a
   mockup: landscape gets *exactly* the same treatment portrait already
   has, not a separate design — this is that, applied by simply letting the
   existing mobile block's own condition cover both orientations on a
   phone. .table-rotate-hint above stays portrait-only on purpose — "rotate
   to landscape" only makes sense said from portrait. */
@media (max-width: 640px), (hover:none) and (max-height: 500px), (pointer:coarse) and (max-height: 500px){
  .detail-grid{grid-template-columns:1fr}
  /* Show detail, mobile (the owner, 2026-09-01) — the show-type badge, favorite
     star and tag icon all drop to their own line under the title (the inline
     .type-badge copy inside the title block shows here; the .detail-show-type
     block copy, used on desktop, is hidden). Desktop keeps the star + tag
     icon inline with the title instead. */
  .concert-detail .detail-title{display:block}
  .concert-detail .detail-title-block > .type-badge{display:inline-block;margin:9px 0 0;vertical-align:middle}
  .concert-detail .detail-title-block .detail-title-adorn{vertical-align:middle}
  .detail-show-type{display:none}
  .form-grid{grid-template-columns:1fr}
  .span-2{grid-column:1/2}
  .track-edit-row{grid-template-columns:1fr}
  /* 40px here dated from the initial commit, when the year was right-aligned
     plain text sitting in a left gutter on mobile too. Since the Option D
     rework (item 77) the year-pill sits to the *right* of the line (see
     .timeline-year below), so that gutter is now dead space — the List and
     Map views start flush left and the timeline shouldn't waste ~32px that
     they don't. 8px just keeps the dot (.timeline-dot, left:-7px) off the
     card's padding edge; everything (line, year-pills, titles) shifts left
     with it. */
  .timeline{margin-left:8px}
  .mini-concert-companion-inline{display:none}
  .mini-concert-companion-stacked{display:flex}
  /* Timeline companion, mobile (the owner, 2026-09-01) — off the title row (it
     was wrapping onto a cramped right-aligned second half-line under the
     title) and onto its own full-width row directly above the tags instead. */
  .timeline-companion-inline{display:none}
  .timeline-companion-stacked{display:flex;margin-top:8px}
  .mini-concert-date--range{width:96px;white-space:nowrap}
  /* Desktop pulls the year into a gutter to the *left* of the vertical
     line (negative margin — see the base rule's own comment) — on a narrow
     phone that gutter leaves too little room, so mobile drops the
     pulled-back gutter entirely and lets the year sit to the *right* of
     the line instead. Connector-then-pill order is the same real flex
     children as the desktop rule, just left-aligned and not reversed —
     reads left-to-right as line → connector → pill instead of desktop's
     pill → connector → line (TODO.md item 77, 2026-08-26, Option D — the
     actual fix this item asked for, not just consistency: the pill is what
     makes the year stand out from titles on a narrow phone, where it
     otherwise read as just another line of text).
     No left padding here (fixed same day — the first pass had one, leaving
     a gap between the actual line and where the connector started, so the
     dotted line never visually touched it) — the connector is the flex
     row's first child, so it starts flush against the line itself.
     connector width (19px) + gap (7px) = 26px, landing the *pill*'s own
     left edge at the same indent .timeline-item's own 26px padding already
     puts show titles at, same alignment the old plain-text year used to
     have. */
  .timeline-year{margin-left:0;width:auto;padding:6px 0;
    flex-direction:row;justify-content:flex-start;gap:7px}
  .timeline-year-connector{width:19px}
  .timeline-year-pill{font-size:13.5px;padding:3px 11px}

  /* 5 nav items (Catalog joined Dashboard/Artists/Releases/Shows) made the
     header itself grow taller/more crowded on a narrow phone — a first fix
     just made the row scroll horizontally instead, but that still left a
     busier header than before Catalog existed. Collapsed behind a hamburger
     instead: .nav is hidden by default here and only shown (as a full-width
     dropdown below the rest of the header, one item per row) once
     .mobile-nav-toggle is tapped — see the JS listener toggling
     .nav's .mobile-open class. Desktop is untouched either way. */
  .icon-btn.mobile-nav-toggle{display:flex}
  .nav{display:none;width:100%;order:10;flex-direction:column;gap:2px;margin-top:10px}
  .nav.mobile-open{display:flex}
  .nav .nav-btn{width:100%;text-align:left;padding:10px 12px;border-radius:6px}
  .nav .nav-btn:hover{background:var(--bg-alt)}
  .nav .nav-btn span{border-bottom:none;padding-bottom:0}
  .nav .nav-btn.active{background:var(--card);border:1px solid var(--line)}

  /* .header-right now only holds search + Add on mobile (the settings
     cluster below is pulled out into its own fixed bottom bar), so the two
     have room to sit on one line — the search box stretches to fill it
     instead of sitting at its fixed desktop width with empty space next to it. */
  .search-wrap{flex:1;min-width:0}
  .search-input{width:100%}
  /* Profile/Admin hide both search and Add (see updateHeader()'s
     showHeaderTools in app.js, TODO.md item 35), leaving .header-right
     empty — but its desktop min-width:180px
     (set above, for the search box) still applied here, forcing the empty
     box onto its own wrapped row below the hamburger + title and leaving a
     dead strip of space there. Dropped to 0 whenever app.js marks it empty
     so it collapses into the same row instead of wrapping. */
  .header-right.empty{min-width:0}

  /* header-actions: theme/profile/admin/logout move out of the header
     entirely and become a bar fixed to the bottom of the screen — thumb
     reach is better there on a phone, and it keeps the header itself down
     to just search + Add instead of a pile of icons competing for the same
     cramped row (see the search-bar-overflow fix above this block used to
     be, now superseded by this restructuring). env(safe-area-inset-bottom)
     clears the home-indicator area on iOS. */
  .header-actions{position:fixed;left:0;right:0;bottom:0;z-index:60;background:var(--bg);
    border-top:2px solid var(--ink);flex-direction:column;gap:8px;
    padding:10px 16px calc(10px + env(safe-area-inset-bottom));
    transform:translateY(0);transition:transform .28s ease}
  /* Toggled by the scroll listener in app.js — slides the bar out of the
     way while scrolling down through a list, back in on scrolling up. */
  .header-actions.hide-bar{transform:translateY(100%)}
  .header-actions-icons{justify-content:center}
  /* Version/release-notes link, mobile's counterpart to .desktop-version-bar
     — both shown on every page (see updateHeader() in app.js), not just
     Profile, which no longer has its own copy of this line. */
  .header-version-link{display:block;background:none;border:none;cursor:pointer;font-family:inherit;
    font-size:10.5px;color:var(--ink-soft);text-align:center;padding:2px}
  .header-version-link:hover{color:var(--ink);text-decoration:underline}

  /* Content needs clearance at the bottom so the fixed bar never covers
     the last item in a list. */
  .main{padding-bottom:calc(84px + env(safe-area-inset-bottom))}

}

@media (max-width: 640px) and (orientation: portrait){
  .table-rotate-hint{display:block;background:var(--card);border:1px solid var(--line);border-radius:6px;
    padding:10px 14px;font-size:12.5px;color:var(--ink-soft);margin-bottom:14px}
}
/* "Find on Setlist.fm" results in the Edit show screen (2026-09-24). */
.concert-find-results{margin-top:12px;padding-top:12px;border-top:1px solid var(--line)}
.concert-find-head{font-size:12.5px;font-weight:700;margin-bottom:2px}
.concert-find-head span{font-weight:400;color:var(--ink-soft)}
.concert-find-sub{font-family:'IBM Plex Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--ink-soft);margin:12px 0 0}
.concert-find-card{display:flex;align-items:center;gap:10px;background:var(--card);border:1px solid var(--line);border-radius:6px;padding:8px 10px;margin-top:6px}
.concert-find-info{flex:1;min-width:0;font-size:12.5px}
.concert-find-info strong{font-weight:600}
.concert-find-info small{display:block;color:var(--ink-soft);font-size:11.5px}
.concert-find-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
.concert-find-view{font-size:12px;font-weight:600;color:var(--accent);text-decoration:none;white-space:nowrap}
.concert-find-btn{padding:4px 10px;font-size:12px;white-space:nowrap}
.concert-find-empty{background:var(--card);border:1px dashed var(--line);border-radius:6px;padding:10px 12px;font-size:12.5px;color:var(--ink-soft)}
.concert-find-empty a{color:var(--accent);font-weight:600;text-decoration:none}
@media (max-width:560px){.concert-find-card{flex-direction:column;align-items:flex-start}.concert-find-actions{justify-content:flex-start}}
/* Approximate show dates (TODO.md item 140). */
.approx-chip{font-family:'IBM Plex Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:.4px;border:1px dashed var(--warn-border);background:var(--warn-bg);color:var(--warn-ink);border-radius:5px;padding:1px 6px;white-space:nowrap;vertical-align:middle}
.approx-date-grid{display:grid;grid-template-columns:1fr 1.3fr auto 1fr;gap:10px;align-items:start}
.approx-date-grid.no-range{grid-template-columns:1fr 1.3fr}
.approx-date-grid .filter-row-sub{margin-top:4px}
.approx-date-grid .form-input:disabled{opacity:.45}
.approx-or{font-family:'IBM Plex Mono',monospace;font-size:10.5px;text-transform:uppercase;letter-spacing:1px;color:var(--ink-soft);padding-top:10px}
@media (max-width:560px){.approx-date-grid{grid-template-columns:1fr 1fr}.approx-or{grid-column:1/-1;padding-top:0;text-align:center}}
.field-error-hint{margin-top:6px;font-size:12.5px;color:var(--danger);display:flex;align-items:center;gap:6px}
.field-error-hint::before{content:"!";flex:none;width:16px;height:16px;border-radius:50%;background:var(--danger);color:#fff;font-weight:700;font-size:11px;display:inline-flex;align-items:center;justify-content:center}
.field-error-hint[hidden]{display:none}
/* "Want to see live" + tour dates (TODO.md item 108, part A). The ticket
   icon sits next to the favorite star (option A): outline off, filled on. */
.want-live-icon{background:none;border:none;cursor:pointer;padding:2px;line-height:0;flex:none;color:var(--ink-soft)}
.want-live-icon svg{width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linejoin:round}
.want-live-icon.active{color:#e0843f}
.want-live-icon.active svg{fill:currentColor}
.want-live-icon.active .want-live-tear{stroke:#fff}
.rel-hero-body .want-live-icon:not(.active){color:rgba(255,255,255,0.72)}
.want-live-icon:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
.tour-date{display:grid;grid-template-columns:54px minmax(0,1fr) auto;gap:12px;align-items:center;padding:10px 0;border-top:1px solid var(--line)}
.tour-dates-panel .rel-side-label + .tour-date{border-top:none}
.tour-date-block{border:1px solid var(--line);border-radius:8px;text-align:center;padding:4px 0;line-height:1.15}
.tour-date-block .m,.tour-date-block .y{font-size:9.5px;color:var(--ink-soft)}
.tour-date-block .d{font-family:'Space Grotesk',sans-serif;font-weight:800;font-size:18px}
.tour-date-info{font-size:13.5px;min-width:0;overflow-wrap:anywhere}
.tour-date-info strong{font-weight:600}
.tour-date-info small{display:block;font-size:12px;color:var(--ink-soft);margin-top:2px}
.tour-date-info a{color:var(--accent);font-weight:600;text-decoration:none}
.tour-date-acts{display:flex;align-items:center;gap:6px;flex-wrap:wrap;justify-content:flex-end}
.tour-date-go{padding:5px 11px;font-size:12px;white-space:nowrap}
.tour-date-icon{width:26px;height:26px;border-radius:6px;font-size:16px}
.tour-date-links{font-size:12px;color:var(--ink-soft)}
/* The parts under the dates, each behind a hairline (2026-09-26, the owner —
   they ran into each other, same fix as the Add/Edit form sections). */
.tour-date-sect{border-top:1px solid var(--line);padding-top:10px;margin-top:10px}
.tour-date-sect .tour-match-line,.tour-date-sect .tour-match{margin-top:0}
.tour-date-icon svg{width:14px;height:14px}
.tour-date-links a{color:var(--accent);font-weight:600;text-decoration:none}
.tour-date-form{background:var(--bg-alt);border:1px solid var(--line);border-radius:8px;padding:12px;margin-top:10px}
.tour-date-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
.tour-date-form-grid .span-all{grid-column:1/-1}
.tour-date-form-acts{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
.want-add-row{display:flex;gap:8px;max-width:560px;margin:-4px 0 18px}
@media (max-width:560px){
  .tour-date{grid-template-columns:48px minmax(0,1fr);row-gap:6px}
  .tour-date-block{grid-row:span 2;align-self:start}
  .tour-date-acts{grid-column:2;justify-content:flex-start}
  .tour-date-form-grid{grid-template-columns:1fr}
}
/* "Did you go?" (TODO.md item 43): amber = still to answer, struck grey =
   missed. Same --warn-* family as the approximate-date marker. */
.chip-confirm,.chip-missed{display:inline-block;font-family:'IBM Plex Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:.4px;border-radius:5px;padding:2px 7px;white-space:nowrap;vertical-align:middle}
.chip-confirm{border:1px solid var(--warn-border);background:var(--warn-bg);color:var(--warn-ink)}
.chip-missed{border:1px solid var(--line);color:var(--ink-soft);text-decoration:line-through}
.confirm-btns{display:inline-flex;gap:6px;flex-wrap:wrap}
.confirm-btn{border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:6px;padding:4px 11px;font:600 12px 'IBM Plex Sans',sans-serif;cursor:pointer}
.confirm-btn.yes{border-color:var(--accent);color:var(--accent)}
.tl-confirm{margin-top:9px}
.tl-when-confirm{color:var(--warn-ink)}
.dash-confirm{border-color:var(--warn-border)}
.dash-confirm-n{color:var(--warn-ink)}
.dash-confirm-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 0;border-top:1px solid var(--line)}
.dash-confirm-row:first-of-type{border-top:none}
.dash-confirm-info{flex:1;min-width:180px;text-align:left;background:none;border:none;padding:0;cursor:pointer;color:var(--ink);font:inherit;font-size:13.5px}
.dash-confirm-info span{color:var(--ink-soft)}
.confirm-days{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.confirm-day{display:flex;justify-content:space-between;align-items:center;gap:10px;font-size:13.5px}
.confirm-opt{display:block;width:100%;text-align:left;background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin-top:8px;cursor:pointer;color:var(--ink);font:inherit;font-size:13.5px}
.confirm-opt small{display:block;color:var(--ink-soft);font-size:12px;margin-top:2px}
.confirm-opt.sel{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent) inset}
/* Setlist chip for an act with no setlist (2026-09-25) — greyed, dashed,
   not clickable. */
.chip.chip-no-setlist{opacity:.45;border-style:dashed;cursor:default}
/* Tour alerts via Ticketmaster (TODO.md item 108, part B). */
.tour-date-new{font-family:'IBM Plex Mono',monospace;font-size:9.5px;text-transform:uppercase;background:#e0843f;color:#fff;border-radius:4px;padding:1px 5px;margin-left:4px;vertical-align:middle}
.tour-match-line{margin-top:12px;font-size:12.5px;color:var(--ink-soft)}
.tour-match-line strong{color:var(--ink);font-weight:600}
.tour-match{margin-top:12px;background:var(--bg-alt);border:1px solid var(--line);border-radius:8px;padding:10px 12px}
.tour-match-row{display:flex;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--line)}
.tour-match-row:first-of-type{border-top:none}
.tour-match-info{flex:1;min-width:0;font-size:13px}
.tour-match-info small{display:block;color:var(--ink-soft);font-size:11.5px}
.tour-alert-toggle{display:flex;align-items:center;gap:8px;font-size:13.5px;margin-top:8px}
.tour-country-chips{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0 10px}
.tour-country-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);border-radius:999px;padding:3px 6px 3px 11px;font-size:12.5px;background:var(--card)}
.tour-country-chip button{border:none;background:none;cursor:pointer;color:var(--ink-soft);font-size:15px;line-height:1;padding:0 3px}
.tour-country-add{display:flex;gap:8px;max-width:420px}
