:root{
  --bg:#f4f6f9; --bg-alt:#e7ecf3; --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);
}
/* 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; --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);
  }
}
:root[data-theme="dark"]{
  --bg:#12161d; --bg-alt:#1a2029; --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);
}
:root[data-theme="light"]{
  --bg:#f4f6f9; --bg-alt:#e7ecf3; --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);
}
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 */
.header{position:sticky;top:0;z-index:50;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}
/* Matches the app icon (favicon.svg/apple-touch-icon.png/icon-*.png,
   picked 2026-08-13 from the design-preview options) — light background,
   navy ring+dot — rather than the old dark-square version those assets
   used to share. Built from the same theme tokens as everything else
   instead of a static image, so (unlike the icon files) it re-themes
   automatically: light bg + navy dot in light mode, dark-card bg + the
   lighter dark-mode --accent in dark mode. */
.logo-mark{width:32px;height:32px;border-radius:10px;background:var(--bg-alt);position:relative;flex:none}
.logo-ring{position:absolute;inset:5px;border-radius:7px;border:1px solid var(--accent);opacity:.3}
.logo-dot{position:absolute;inset:0;margin:auto;width:9px;height:9px;border-radius:50%;background:var(--accent)}
.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}
.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}
.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)}
/* Right-aligned variant of .year-filter-panel — used by header-right
   dropdowns (notifications, Profile's "More") that hang off the right edge
   of their trigger instead of the left, so they don't overflow the viewport
   on the right side of the screen. */
.year-filter-panel.right{left:auto;right:0}

/* 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}
}
/* 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}
.link-btn{background:none;border:none;padding:0;cursor:pointer;color:var(--accent);font-size:12.5px;text-decoration:underline}

/* main */
.main{padding:clamp(20px,4vw,40px);max-width:1180px;margin:0 auto}
/* 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. */
.desktop-version-bar{display:none}
@media (min-width: 641px){
  .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 .toolbar 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}
.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}
.stat-card{background:var(--card);border:1px solid var(--line);border-radius:6px;padding:20px}
.stat-value{font-family:'Space Grotesk',sans-serif;font-weight:800;font-size:40px;line-height:1}
/* Text-shaped values (e.g. "38h 12m") run wider than a plain number at the
   same size — sized down so they don't wrap inside a narrow stat card. */
.stat-value.compact{font-size:26px}
.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}
.panel-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:16px}
.panel-head h2{font-size:20px}
.view-all{font-size:12px;font-weight:600}

/* horizontal bar-list — dashboard breakdown panels (formats, release
   types, venues, places, show types) */
.bar-list{display:flex;flex-direction:column;gap:11px}
.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 — Sander 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}

.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}
.mini-concert-date{font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--ink-soft);flex:none;width:64px}
.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 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. */
.mini-concert-companion-inline{flex:none;white-space:nowrap}
.mini-concert-companion-stacked{display:none;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 */
.toolbar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:24px}
.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)}
/* 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}
.select{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:9px 14px;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, Sander 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{position:absolute;top:calc(100% + 6px);left:0;z-index:40;background:var(--card);
  border:1px solid var(--line);border-radius:8px;padding:10px;min-width:240px;max-height:280px;overflow-y:auto;
  box-shadow:0 8px 24px rgba(16,24,40,0.15)}
.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}
.year-chip{border:1px solid var(--line);border-radius:6px;padding:5px 10px;font-size:12px;font-weight:600;cursor:pointer;background:var(--bg);color:var(--ink-soft)}
.year-chip.active{background:var(--ink);color:var(--bg);border-color:var(--ink)}
/* Display-only chip (Companion/Who did you see?, TODO.md item 16,
   2026-08-17) — same look as an active .year-chip, but not a control:
   add/remove now happens through a manage popover instead of clicking the
   chip itself, so this overrides the inherited pointer cursor. */
.year-chip.static{cursor:default}

/* tag icon + its chips on record/show/artist detail pages — travel as one
   unit so they wrap together onto their own line when space is tight */
.tag-manage{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;flex:none}
.tag-manage .chip-row{margin-top:0}
/* 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 */
.tag-manage .year-filter-panel{min-width:0;max-width:240px}
/* one tag per row instead of wrapping chips — every row is the same width
   regardless of tag name length, which reads a lot tidier stacked */
.tag-manage-list{display:flex;flex-direction:column;gap:2px}
.tag-manage-list .year-chip{width:100%;text-align:left;box-sizing:border-box}

/* 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:8px;width:36px;height:36px;
  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)}
/* Catalog's link/unlink status icon — green (not yet in your collection,
   clickable to add) or red (already have it, clickable to unlink) instead
   of a text "+ Add" button / "In your collection" chip. */
.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-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.catalog-link-icon.unlinked{border-color:var(--success)}
.catalog-link-icon.unlinked svg{stroke:var(--success)}
.catalog-link-icon.linked{border-color:var(--danger)}
.catalog-link-icon.linked svg{stroke:var(--danger)}
.toolbar-sep{color:var(--ink-soft);opacity:0.45;font-size:14px}
.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}
/* Direct child only (2026-08-18 fix, TODO.md) — a plain single-day
   .timeline-item's own star sits right after .timeline-dot as a direct
   child, which this positions top-right of the whole card. The festival
   group card (festivalGroupCardHTML) nests one star per day several levels
   deep instead (.festival-day-row, its own row) — a descendant selector
   here used to catch those too, stacking every day's star on top of each
   other in the same spot instead of leaving them in their own row. */
.timeline-item > .favorite-star{position:absolute;top:10px;right:0}
.detail-title + .favorite-star{font-size:24px}

.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}
.filter-row-label{font-weight:600;font-size:13.5px}
.filter-row-sub{font-size:11.5px;color:var(--ink-soft);margin-top:2px}
.settings-group-title{font-family:'IBM Plex Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--ink-soft);margin:24px 0 4px}
.settings-group-title: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}
.badge{padding:2px 7px;border-radius:5px}
.badge-vinyl{background:#0e1c33;color:#fff;border:1px solid var(--line)}
.badge-cd{background:transparent;color:var(--ink);border:1px solid var(--ink)}
.badge-cassette{background:#7a5230;color:#fff;border:1px solid var(--line)}
.badge-dvd{background:#3f5e82;color:#fff;border:1px solid var(--line)}
.badge-digital{background:transparent;color:var(--olive);border:1px solid var(--olive)}
.badge-book{background:#6b2737;color:#fff;border:1px solid var(--line)}

/* 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-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{font-family:'IBM Plex Mono',monospace;font-size:11px;flex:none;padding:3px 9px}

/* 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. */
.timeline{border-left:2px solid var(--line);margin-left:62px}
.timeline-year{font-family:'Space Grotesk',sans-serif;font-weight:800;font-size:20px;
  margin-left:-62px;width:50px;padding:4px 12px 4px 0;text-align:right}
.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)}
/* Title/venue on the left; the type badge sits inline next to the title
   (colour-matched with the dot) while tags are pushed all the way right,
   next to the favorite star — keeps each entry compact without burying the
   type indicator far from the rest of the entry. */
.timeline-row{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}
.timeline-main{min-width:0}
.timeline-meta{display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex:none;padding-top:2px}
.timeline-meta .chip-row{margin-top:0;justify-content:flex-end}
.timeline-date{font-family:'IBM Plex Mono',monospace;font-size:11.5px}
.timeline-title-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px}
.timeline-title-row .type-badge{margin-top:0}
.timeline-artist{font-weight:700;font-size:16px}
.timeline-venue{font-size:12.5px;color:var(--ink-soft);margin-top:2px}
.chip-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
.chip{font-family:'IBM Plex Mono',monospace;font-size:10.5px;background:var(--bg-alt);border-radius:5px;padding:3px 9px}
.chip-link{cursor:pointer;border-radius:6px;padding:5px 12px;font-size:12px}
.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}
.type-badge.concert{background:var(--accent)}
.type-badge.festival{background:var(--olive)}
.type-badge.musical{background:#6b4c9a;border:1px solid var(--line)}
.type-badge.cabaret{background:#9c4b6b;border:1px solid var(--line)}
.type-badge.other{background:#6b7280;border:1px solid var(--line)}
.timeline-dot.concert{background:var(--accent)}
.timeline-dot.festival{background:var(--olive)}
.timeline-dot.musical{background:#6b4c9a}
.timeline-dot.cabaret{background:#9c4b6b}
.timeline-dot.other{background:#6b7280}

/* Multi-day festivals (TODO.md item 18) — festivalGroupCardHTML's Shows-
   list card: every day's own lineup stays visible as its own row instead of
   behind a click (see the card's own file comment for why). */
.festival-day-list{display:flex;flex-direction:column;gap:8px;margin-top:10px}
.festival-day-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:6px 8px;border-radius:6px;cursor:pointer}
.festival-day-row:hover{background:var(--bg-alt)}
.festival-day-label{font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--ink-soft);flex:none;width:88px}
.festival-count-chip{font-family:'IBM Plex Mono',monospace;font-size:10.5px;background:var(--suggest-bg);color:var(--accent);border-radius:5px;padding:3px 9px}
/* Show detail page's "Part of {festival} — day X of Y" banner, right under
   the badge row. */
.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-bottom:14px}
.part-of-festival b{color:var(--ink);font-weight:700}
.part-of-festival .nav{display:flex;gap:2px}
.part-of-festival .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}
.part-of-festival .nav button:disabled{opacity:0.35;cursor:default}
.part-of-festival .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}
.badge-row .tag{background:var(--bg-alt);padding:4px 10px;border-radius:5px}
.badge-row .chip-row{margin:0 0 0 auto}
.action-row{display:flex;gap:10px;margin-bottom:20px}
.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 (Sander, 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}
.section-title{font-size:18px;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}
.chevron-btn{background:var(--card);border:1px solid var(--line);border-radius:8px;width:26px;height:26px;flex:none;
  display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
.chevron-btn:hover{border-color:var(--ink)}
.chevron-btn svg{width:13px;height:13px;stroke:var(--ink-soft);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}
.chevron-btn.collapsed svg{transform:rotate(-90deg)}
/* .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)}
/* 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)}
.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") Sander 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 Sander 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}
/* Drag-to-reorder (TODO.md item 12, 2026-08-17) — sits alongside the
   existing up/down arrow buttons, doesn't replace them (Sander'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}}
.track-edit-row{display:grid;grid-template-columns:56px 1.4fr 1fr 70px 1.4fr 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}
/* 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. */
.concert-header-divider{border-top:1px solid var(--line);margin-top:20px}
.concert-section{margin-top:32px}
.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}
.image-slot .slot-icon{font-size:22px;opacity:0.35;pointer-events:none}

/* 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{background:var(--card);border:2px solid var(--ink);border-radius:8px;padding:32px;width:100%;max-width:520px;max-height:86vh;overflow-y:auto;position:relative}
.modal.delete-modal{max-width:400px;padding:28px}
.modal.modal-wide{max-width:960px}
.modal-close{position:absolute;top:16px;right:16px;background:none;border:none;font-size:20px;cursor:pointer;color:var(--ink-soft);line-height:1}
.modal h2{font-size:24px;margin:0 0 18px}

/* Help Center (TODO.md item 7) — sidebar categories + selected category's
   articles. Reuses .modal for the card itself (see renderHelpCenterModal's
   inline max-width override), just these two new pieces for the layout. */
.help-center-layout{display:grid;grid-template-columns:170px 1fr;gap:22px}
.help-center-categories{display:flex;flex-direction:column;gap:2px}
.help-center-cat-btn{text-align:left;border:none;background:transparent;border-radius:6px;padding:8px 10px;font-size:12.5px;font-weight:600;color:var(--ink-soft);cursor:pointer}
.help-center-cat-btn.active{background:var(--ink);color:var(--bg)}
.help-center-replay{margin-top:10px;text-align:left;background:none;border:1px dashed var(--line);border-radius:6px;padding:8px 10px;font-size:11.5px;font-weight:600;color:var(--accent);cursor:pointer}
.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}
@media (max-width:640px){
  .help-center-layout{grid-template-columns:1fr}
  .help-center-categories{flex-direction:row;flex-wrap:wrap}
  .help-center-replay{width:100%}
}

/* 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{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. */
.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%}
.choose-option .title{font-weight:700;font-size:16px}
.choose-option .desc{font-size:12.5px;color:var(--ink-soft);margin-top:3px}

.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
.form-input{background:var(--bg);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}
.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)}
.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)}
.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-note-entry{border-bottom:1px solid var(--line)}
.release-note-entry:last-child{border-bottom:none}
/* Collapsed by default (see renderReleaseNotesModal) — the whole row is the
   toggle, not just a small chevron hit-target, so it's easy to tap on
   mobile. */
.release-note-head{display:flex;align-items:center;gap:8px;width:100%;background:none;border:none;
  cursor:pointer;padding:14px 0;text-align:left;font-family:inherit;color:inherit}
.release-note-entry h3{font-size:15px;margin:0;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)}
.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)}
.release-note-entry ul{margin:0 0 16px;padding-left:20px}
.release-note-entry li{font-size:13.5px;color:var(--ink-soft);line-height:1.5;margin-bottom:6px}
.release-note-entry li:last-child{margin-bottom:0}
.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:52vh;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 6px;border-radius:4px;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}
.alias-actions{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top: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}
.discogs-result{text-align:left;background:var(--card);border:1px solid var(--line);border-radius:6px;padding:10px 12px;cursor:pointer}
.discogs-result:hover{border-color:var(--accent)}
/* Same title+date as what's being logged — the strongest possible signal
   this is a re-log of an existing show, 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)}
.discogs-result:disabled{opacity:0.55;cursor:not-allowed}
.discogs-result:disabled:hover{border-color:var(--line)}
.discogs-result.exact-match:disabled:hover{border-color:var(--accent)}
.discogs-result-title{font-weight:700;font-size:12.5px}
.discogs-result-meta{font-size:11px;color:var(--ink-soft);margin-top:3px}
.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}
.btn-text:hover{color:var(--accent-dark)}

.release-notes-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:6px}
.release-notes-head h2{margin:0}
.release-notes-expand-controls{display:flex;align-items:center;gap:8px;color:var(--ink-soft);font-size:12.5px}

/* Collection Table view on a narrow phone in portrait: most columns are
   still reachable via the table's own horizontal scroll, but landscape
   fits far more of them at once without scrolling — nudge towards that
   instead of just leaving people to discover the scroll on their own.
   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}

@media (max-width: 640px){
  .detail-grid{grid-template-columns:1fr}
  .form-grid{grid-template-columns:1fr}
  .span-2{grid-column:1/2}
  .track-edit-row{grid-template-columns:1fr}
  .timeline-row{flex-direction:column}
  .timeline-meta{align-items:flex-start;padding-top:8px}
  .timeline-meta .chip-row{justify-content:flex-start}
  .timeline{margin-left:40px}
  .mini-concert-companion-inline{display:none}
  .mini-concert-companion-stacked{display:block}
  /* Desktop pulls the year into a gutter to the *left* of the vertical
     line (negative margin, right-aligned text — see the base rule's own
     comment) — on a narrow phone that gutter left too little room and read
     as the year sitting flush against the line. Simpler on mobile: drop
     the pulled-back gutter entirely and let the year sit to the *right* of
     the line instead, at the same indent as the entries below it (matches
     .timeline-item's own 26px left padding), like a heading over its group. */
  .timeline-year{margin-left:0;width:auto;font-size:16px;padding:6px 0 6px 26px;text-align:left}

  /* 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)}

  /* The notification bell lives inside .header-actions' fixed bottom bar on
     mobile (see below) — .year-filter-panel's normal position:absolute,
     top:100% would open the dropdown below the button, i.e. off the bottom
     of the screen. Anchored to the viewport instead (position:fixed) and
     opened *upward*, clear of the bar. */
  .notif-filter .year-filter-panel{position:fixed;left:12px;right:12px;top:auto;
    bottom:calc(78px + env(safe-area-inset-bottom));width:auto;max-height:50vh;overflow-y:auto}

  /* .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%}
  /* Dashboard/Profile/Admin hide both search and Add (see updateHeader() in
     app.js), 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))}

  /* Trailing separator between the filter controls and the sort/view
     controls in page toolbars (Artists/Collection/Shows) — meaningful
     mid-row on desktop, but left dangling at the end of a line once the
     toolbar wraps here. */
  .toolbar-sep{display:none}
}

@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}
}
