/*
  styles.css — quiet, dark, library aesthetic. Ported from the legacy spike's
  styles.css: near-black ground, muted sage accents. One decision at a time means
  strong hierarchy and generous whitespace rather than dense dashboards. This file
  IS the target design — trimmed only of sections whose pages did not survive the
  port (local data viewer, metadata editor, backup restore, picker presets).

  TYPE — direction B, "Signal" (2026-09-13), replacing the Georgia-on-system-ui
  pairing the spike shipped with. Two families, and the split between them is
  SEMANTIC, not decorative:

    Space Grotesk  — prose and headings. Anything a person wrote or would read
                     as a sentence: the wordmark, section headings, titles, blurbs.
    IBM Plex Mono  — data. Anything a machine produced and a column might align:
                     dates, ratings, ids, counts, playtime figures, field labels.

  That is the whole rule. A number set in Georgia used to be indistinguishable
  from a heading set in Georgia; now the family itself says which kind of thing
  you are looking at, and the figures line up down a column as a bonus.

  Both are SIL OFL 1.1 and SELF-HOSTED from fonts/ — deliberately, not for speed:
  `font-src 'self'` in staticwebapp.config.json admits no third party, and a
  webfont fetched from Google would be a request to a third party on every page
  load, which the Privacy Policy's "no analytics" posture would then have to
  disclose. Keep them local. Licences and provenance: fonts/OFL.txt.

  The subsets are latin + latin-ext (a 1,296-game library has Ōkami and Pokémon
  in it); anything outside those ranges falls back per unicode-range, which is
  the correct degradation rather than a missing glyph.
*/

/* Space Grotesk is a single VARIABLE file spanning 300–700 — one request covers
   every weight the app uses, so do not add per-weight faces. */
@font-face {
  font-family: "Space Grotesk";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("fonts/space-grotesk-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Space Grotesk";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("fonts/space-grotesk-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Plex Mono ships static instances, so two weights means four files. 400 is the
   value weight, 500 the emphasised one; nothing needs a third. */
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/plex-mono-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/plex-mono-400-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/plex-mono-500-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/plex-mono-500-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --ground: #0b100e;
  --raised: #131b17;
  --raised-2: #16201b;
  --line: #27332d;
  --line-soft: #222d27;
  --ink: #e9eee9;
  --ink-dim: #aab5af;
  --ink-faint: #7f8e86;
  --accent: #a6d6bd;
  --accent-deep: #84b99d;
  --accent-soft: #2c4a3c;
  --surface: #131b17;
  --shadow-sm: 0 4px 10px #0006;
  --stretch: #e5b878;
  --stretch-deep: #61472b;
  --warn: #e0b070;
  --danger: #e7a2a2;
  --danger-bg: #321f1f;
  /* Notices: info banners, the toast and the sync popup (owner decision 2026-09-30).
     Yellow, so a message stands apart from the green of the app's own controls. Errors
     keep the danger colours. */
  --notice-line: #8c7a2c;
  --notice-bg: #2c2710;
  --notice-ink: #f6e8a6;

  /* The two stacks. Fallbacks are chosen for close metrics, because font-display:
     swap means the fallback is what a cold visitor reads for the first frames —
     a reflow between two similar faces is a settle, between two different ones a
     jump. */
  --sans: "Space Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", "Segoe UI Mono", Consolas, monospace;

  color-scheme: dark;
  font-family: var(--sans);
  color: var(--ink);
  background: var(--ground);
  font-synthesis: none;
}

* { box-sizing: border-box; }

/* Reserve the scrollbar's width whether or not one is showing. Without it, a
   page that grows past the viewport gains a scrollbar and shunts every column
   sideways by its width. */
html { scrollbar-gutter: stable; }
body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
  background: radial-gradient(circle at 70% -10%, #263a31 0, transparent 35rem), var(--ground);
}
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; opacity: .025;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.9'/%3E%3C/svg%3E");
}

button, input, select, textarea { font: inherit; color: inherit; }
button, select { cursor: pointer; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 50;
  padding: 10px 16px; background: var(--accent); color: var(--ground); font-weight: 700;
}
.skip-link:focus { left: 8px; top: 8px; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* ONE width for every page. The masthead lives inside <main>, so a per-view width made the
   whole header box jump 104px when you opened the library — the nav visibly resizing under
   the cursor that clicked it. 1120, not the library's old 1224: the detail page's featured
   screenshot is sized to 1120 and its thumbnail row is measured against that. */
/* The 40px that used to sit above the masthead is split with it — see .masthead's
   padding-top. Unscrolled the page looks the same; stuck, the eyebrow keeps its
   air instead of touching the top of the viewport. */
main { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 18px 0 64px; }

/* ------------------------------------------------------------------ header */

.masthead {
  position: sticky; top: 0; z-index: 10;
  display: flex; flex-wrap: wrap; gap: 20px;
  align-items: flex-end; justify-content: space-between;
  /* Top padding is load-bearing, not decoration: sticky at top:0 means the stuck
     header's own box starts at y=0, so without it the eyebrow sits flush against
     the edge of the viewport and the page scrolls up hard against the text. */
  padding-top: 22px; padding-bottom: 20px; margin-bottom: 22px;
  border-bottom: 1px solid var(--line);
  background: #0b100ef2;
  box-shadow: 0 12px 24px #0b100e99;
  backdrop-filter: blur(12px);
}
.eyebrow {
  margin: 0 0 6px; color: #96b5a5; font-size: .72rem; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase;
}
/* Mark left, wordmark right. The logo is sized off the SAME clamp as h1 so the
   two halves of the lockup scale together — one step smaller than the type,
   because a filled tile at the full font-size optically outweighs cap height. */
.masthead-title { display: flex; align-items: center; gap: 14px; }
.masthead-logo {
  flex-shrink: 0;
  width: clamp(2.2rem, 5.5vw, 3.3rem);
  height: clamp(2.2rem, 5.5vw, 3.3rem);
}
h1 {
  margin: 0;
  font: 700 clamp(2.4rem, 6vw, 3.6rem)/.9 var(--sans);
  letter-spacing: -.045em;
}
h2 { margin: 0; font: 600 1.6rem var(--sans); letter-spacing: -.025em; }
h3 { margin: 0; font-size: 1.05rem; }
h4 { margin: 0 0 6px; font-size: .95rem; }

.view-nav { display: flex; flex-wrap: wrap; gap: 4px; }
.nav-tab {
  border: 1px solid transparent; border-radius: 10px;
  padding: 9px 14px; color: var(--ink-faint); background: none;
  font-size: .88rem; font-weight: 600;
}
.nav-tab:hover { color: var(--ink); background: var(--raised); }
.nav-tab.active { color: var(--ground); background: var(--accent); border-color: var(--accent); }

/* ------------------------------------------------------------ email sign-in */

.signin-form { display: grid; gap: 12px; max-width: 34rem; }
.signin-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.signin-row .text-input { flex: 1 1 16rem; min-width: 0; }
.signin-note { margin: 0; color: var(--ink-faint); font-size: .86rem; }

.signin-sent {
  max-width: 34rem; padding: 16px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--raised);
}
.signin-sent-head { margin: 0; color: var(--ink); font-size: 1.05rem; font-weight: 700; }
.signin-sent-detail { margin: 6px 0 0; color: var(--ink-dim); font-size: .9rem; }

/* --------------------------------------------------------- account control */

/* Nav and account travel together on the right; the masthead's space-between
   then keeps the wordmark hard left. */
.masthead-actions { position: relative; display: flex; align-items: center; gap: 12px; }

/* The phone's sections button (index.html #nav-toggle). Hidden above the phone width,
   where the sections are a row of tabs. Three bars drawn in CSS: nothing fetched. */
.nav-toggle { display: none; }
.nav-toggle-bars, .nav-toggle-bars::before, .nav-toggle-bars::after {
  display: block; width: 20px; height: 2px; border-radius: 2px; background: currentColor;
}
.nav-toggle-bars { position: relative; }
.nav-toggle-bars::before, .nav-toggle-bars::after { content: ""; position: absolute; left: 0; }
.nav-toggle-bars::before { top: -6px; }
.nav-toggle-bars::after { top: 6px; }
.account { position: relative; margin-right: 8px; }

.avatar-button {
  display: grid; place-items: center;
  padding: 2px; border: 1px solid transparent; border-radius: 50%;
  background: none; cursor: pointer;
}
.avatar-button:hover { border-color: var(--line); }
.avatar-button[aria-expanded="true"] { border-color: var(--accent); }

/* The avatar itself: a tinted disc with a drawn figure. No photograph, and no
   letters — with no persona name stored there are no honest initials to show. */
.avatar {
  position: relative; display: block; overflow: hidden;
  border-radius: 50%;
  background: linear-gradient(160deg,
    hsl(var(--avatar-hue) 26% 34%),
    hsl(var(--avatar-hue) 30% 22%));
  box-shadow: inset 0 0 0 1px #ffffff14;
}
.avatar-small { width: 34px; height: 34px; }
.avatar-large { width: 46px; height: 46px; }

/* Head and shoulders, drawn in CSS so no markup is injected and nothing is fetched. */
.avatar-glyph::before,
.avatar-glyph::after {
  content: ""; position: absolute; left: 50%; transform: translateX(-50%);
  background: hsl(var(--avatar-hue) 34% 82%);
}
.avatar-glyph::before { top: 20%; width: 30%; height: 30%; border-radius: 50%; }
.avatar-glyph::after {
  top: 57%; width: 58%; height: 46%;
  border-radius: 46% 46% 0 0;
}

/* The picture controls on the Taste profile page's Account panel. */
.avatar-row { margin-top: 14px; }
.avatar-controls { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; margin-top: 6px; }
.avatar-controls > .avatar { flex: none; width: 64px; height: 64px; }
.avatar-controls-body { display: grid; gap: 10px; flex: 1 1 240px; min-width: 0; }
.avatar-controls-body .button-row { margin: 0; }
.avatar-note { margin: 0; max-width: 60ch; }

/* A chosen picture covers the glyph; if it fails to load it removes itself (avatarNode). */
.avatar-photo {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: 50%;
}

/* A Steam-backed account gets the accent ring; a dev session deliberately does not,
   so the two are never mistaken for each other at a glance. */
.avatar[data-provider="steam"] { box-shadow: inset 0 0 0 1px var(--accent-soft); }

.account-menu {
  position: absolute; right: 0; top: calc(100% + 10px); z-index: 20;
  width: min(19rem, calc(100vw - 32px));
  padding: 14px; border: 1px solid var(--line); border-radius: 12px;
  background: var(--raised); box-shadow: 0 18px 36px #0009;
}
.account-menu-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--line-soft);
}
.account-menu-brand {
  color: var(--ink-dim); font-size: .78rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
}
.account-menu-signout {
  border: 0; border-radius: 8px; padding: 4px 8px;
  background: none; color: var(--accent);
  font-size: .85rem; font-weight: 600; cursor: pointer;
}
.account-menu-signout:hover { background: var(--accent-soft); color: var(--ink); }

.account-menu-body { display: flex; align-items: center; gap: 12px; padding: 14px 0 2px; }
.account-menu-who { min-width: 0; }
.account-menu-name { margin: 0; color: var(--ink); font-size: 1rem; font-weight: 700; overflow-wrap: anywhere; }

/* ----------------------------------------------------------------- banners */

/* Pinned under the sticky masthead, so a message is on screen however far down the page
   the user has scrolled. Above the library's and picker's own sticky bars (8/9), below
   the masthead's menus and every dialog. Empty, it is zero height and covers nothing. */
.banners {
  display: grid; gap: 8px; margin-bottom: 18px;
  position: sticky; top: calc(var(--masthead-height, 90px) + 8px); z-index: 11;
}
/* Empty, it holds no space either: its margin would otherwise sit above every page heading. */
.banners:empty { margin-bottom: 0; }
.banner {
  display: flex; gap: 12px; align-items: flex-start; justify-content: space-between;
  padding: 12px 14px; border-radius: 10px; font-size: .9rem;
  border: 1px solid var(--line); background: var(--raised);
}
.banner.error { border-color: #6a3c3c; background: var(--danger-bg); color: #f3cdcd; }
.banner.info { border-color: var(--notice-line); background: var(--notice-bg); color: var(--notice-ink); }
/* Something the user must wait on or act on, but not a failure (the sync cooldown). The
   same yellow as info, set apart by weight and a brighter border now that info is yellow too. */
.banner.warning { border-color: #d9c056; background: var(--notice-bg); color: var(--notice-ink); font-weight: 600; }
.banner-close { border: 0; background: none; color: inherit; font-size: 1.2rem; line-height: 1; padding: 0 4px; }
/* The quick-confirmation popup (app.js toast): the notice colours, pinned to the
   bottom of the viewport so it shows wherever on the page the button was. It takes no
   clicks, so it can never sit over a control and swallow a tap. */
.toast {
  position: fixed; left: 50%; bottom: 24px; z-index: 50; transform: translateX(-50%);
  max-width: calc(100% - 32px); padding: 10px 18px; border: 1px solid var(--notice-line); border-radius: 999px;
  background: var(--notice-bg); color: var(--notice-ink); font-size: .9rem; font-weight: 600;
  box-shadow: var(--shadow-sm); pointer-events: none;
  animation: toast-in .16s ease-out;
}
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 8px); } }
@media (prefers-reduced-motion: reduce) { .toast { animation: none; } }
/* Steam sync progress (app.js renderSyncStatus): pinned to the bottom of the viewport
   like the toast, so it shows on every tab and anywhere on a long page, and it takes no
   clicks, so it can never sit over a control and swallow a tap. Its z-index is above
   every other layer on the page (the library footer is 110, the hover rail 100): at 45 it
   sat underneath both on the Library tab. */
.sync-status {
  position: fixed; left: 50%; bottom: 16px; z-index: 1000; transform: translateX(-50%);
  width: min(440px, calc(100% - 32px)); display: grid; gap: 7px;
  padding: 12px 16px; border: 1px solid var(--notice-line); border-radius: 12px;
  background: var(--notice-bg); color: var(--notice-ink); box-shadow: var(--shadow-sm); pointer-events: none;
}
.sync-status[hidden] { display: none; }
/* The Library pins its own footer to the bottom of the viewport; sit just above it. */
body:has(.lib-footer) .sync-status { bottom: calc(var(--lib-footer-height, 52px) + 12px); }
.sync-status-text { font-size: .9rem; font-variant-numeric: tabular-nums; }
.sync-status-note { font-size: .76rem; color: #cfc38e; }
.sync-status-track { position: relative; height: 6px; overflow: hidden; border-radius: 999px; background: #4a4220; }
.sync-status-fill { display: block; height: 100%; border-radius: inherit; background: var(--notice-ink); transition: width .4s ease-out; }
.sync-status-fill.indeterminate { width: 35%; animation: sync-slide 1.3s ease-in-out infinite; }
@keyframes sync-slide { from { transform: translateX(-100%); } to { transform: translateX(290%); } }
@media (prefers-reduced-motion: reduce) {
  .sync-status-fill { transition: none; }
  .sync-status-fill.indeterminate { animation: none; width: 100%; opacity: .5; }
}

/* ------------------------------------------------------------------ shared */

.view-root:focus { outline: none; }
.section-title {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding-bottom: 10px; margin-bottom: 18px; border-bottom: 1px solid var(--line);
}
.lede { max-width: 62ch; margin: 0 0 22px; color: var(--ink-dim); font-size: 1rem; line-height: 1.55; }
.muted { color: var(--ink-faint); }
.small { font-size: .82rem; }
.muted.small { line-height: 1.5; }
.empty-state { padding: 44px 12px; color: var(--ink-faint); text-align: center; }
.empty-state .hint { margin: 6px 0 0; font-size: .85rem; }

/* Why nothing qualified, from the recommender's own rejection reasons. */
.empty-reasons { display: inline-block; margin: 18px 0 20px; text-align: left; }
.empty-reasons-label { margin: 0 0 8px; font-size: .78rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }
.empty-reasons ul { margin: 0; padding: 0; list-style: none; }
.empty-reasons li { display: flex; gap: 10px; align-items: baseline; padding: 5px 0; font-size: .88rem; }
.empty-reasons li strong { flex: none; min-width: 2.5em; color: var(--ink); text-align: right; font-variant-numeric: tabular-nums; }
.uncertain { margin: 8px 0 0; color: var(--warn); font-size: .82rem; }
.result-count { margin: 0 0 10px; color: var(--ink-faint); font-size: .82rem; }

.panel {
  padding: 18px 20px; margin: 16px 0;
  border: 1px solid var(--line); border-radius: 14px; background: var(--raised);
}
.panel h3 { margin-bottom: 8px; }

/* A library that failed to load (app.js libraryErrorPanel): never first-run onboarding. */
.library-load-error { max-width: 46rem; }
.library-load-error h2 { margin: 0 0 8px; }
.library-load-error p { margin: 0 0 14px; color: var(--ink-dim); line-height: 1.5; }
.waking-up p { margin: 0; }

/* First-run onboarding: the three ways into an empty library, mandated order. */
.first-run-step { display: flex; gap: 14px; padding: 14px 0; border-top: 1px solid var(--line-soft); }
.first-run-number {
  flex: none; display: grid; place-items: center; width: 30px; height: 30px;
  border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink-faint); font-weight: 700; font-variant-numeric: tabular-nums;
}
.first-run-copy { display: grid; gap: 6px; min-width: 0; }
.first-run-copy p { margin: 0; }
.first-run-copy .button-row { margin-top: 2px; }

/* Signup age attestation (COPPA floor) — a claim, not a birthdate. */
.signin-block { display: grid; gap: 12px; justify-items: start; }
.age-gate { display: inline-flex; gap: 8px; align-items: center; font-size: .9rem; cursor: pointer; }
.age-gate input { width: 16px; height: 16px; accent-color: var(--accent); }
.subsection { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line-soft); }

button.primary, .primary {
  border: 1px solid var(--accent); border-radius: 10px; padding: 10px 18px;
  color: var(--ground); background: var(--accent); font-weight: 700;
}
button.primary:hover { background: #b8e2cc; }
button.primary.big { padding: 14px 26px; font-size: 1rem; }
button.secondary, .secondary {
  border: 1px solid var(--line); border-radius: 10px; padding: 9px 16px;
  color: var(--ink-dim); background: transparent; font-weight: 600;
}
button.secondary:hover { color: var(--ink); border-color: var(--accent-deep); }
/* An <a download> wearing .secondary: the CSV template is a real file link, not a
   scripted save, so it stays an anchor and only needs the bits <button> gets free. */
a.button-link { display: inline-block; text-decoration: none; line-height: normal; }
a.button-link:hover { color: var(--ink); border-color: var(--accent-deep); }
/*
  A plain text link, in the theme. The only link colour rule used to be `footer a`,
  so every link outside a <footer> — the sign-in panel's "What is Unshelve It? ·
  Privacy · Terms", the IGDB credit — fell through to the browser's default for a
  dark colour-scheme, a periwinkle #9e9eff found nowhere else in the palette.
  Unclassed only: every link wearing a class (buttons, pills, launch hand-offs)
  styles itself, and landing.css's own link rules load later and win the tie.
*/
a:not([class]) { color: var(--accent-deep); }
a:not([class]):hover { color: var(--accent); }
.csv-controls { display: inline-flex; gap: 8px; align-items: center; }
button.danger, .danger {
  border: 1px solid #623d3d; border-radius: 10px; padding: 9px 16px;
  color: var(--danger); background: transparent; font-weight: 600;
}
button.danger:hover { background: var(--danger-bg); }
button:disabled { opacity: .45; cursor: not-allowed; }
.button-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }

/*
  The padding and the equal negative margin grow the TAP target without moving anything:
  the text still sits where it did and the margin box keeps its old height, but the
  clickable box is ~26px tall instead of ~17px (WCAG 2.2 target size asks 24). History's
  game titles and the picker's "Change" were the ones a thumb kept missing.
*/
.link-button {
  border: 0; background: none; padding: 5px 0; margin: -5px 0; color: var(--accent);
  font: inherit; font-weight: 600; text-align: left; text-decoration: underline;
  text-decoration-color: #4a6a5c; text-underline-offset: 3px;
}
.link-button:hover { text-decoration-color: var(--accent); }
.link-button.small { font-size: .82rem; font-weight: 500; }
a.link { text-decoration: none; display: inline-flex; align-items: center; }

.chips { display: flex; flex-wrap: wrap; gap: 7px; }
.chip {
  border: 1px solid var(--line); border-radius: 999px; padding: 7px 14px;
  color: var(--ink-dim); background: var(--raised); font-size: .85rem; font-weight: 600;
}
.chip:hover { color: var(--ink); border-color: var(--accent-deep); }
.chip.on { color: var(--ground); background: var(--accent); border-color: var(--accent); }
.chips.small .chip { padding: 5px 11px; font-size: .78rem; }

.badge {
  border-radius: 999px; padding: 4px 11px; font-size: .74rem; font-weight: 700;
  letter-spacing: .02em; white-space: nowrap;
}
.badge.familiar { color: var(--ground); background: var(--accent); }
.badge.adjacent { color: #d8e6de; background: #2c4a3c; }
.badge.stretch { color: var(--ground); background: var(--stretch); }
.badge.quiet { color: var(--ink-faint); background: var(--raised-2); border: 1px solid var(--line); font-weight: 600; }
.badge.deck-badge { border: 0; }
.badge.deck-badge.verified { color: #bce6cf; background: #234536; }
.badge.deck-badge.playable { color: #f0d89a; background: #4a3f22; }
/* Neither green nor amber: the wild card is not a degree of familiarity, it is
   the row that did not come from the scoring at all. */
.badge.wildcard { color: #e6d8f2; background: #3d2b57; border: 1px solid #5a3f7a; }

.confidence {
  border-radius: 999px; padding: 3px 10px; font-size: .7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; white-space: nowrap;
}
.confidence.high { color: #0b100e; background: var(--accent); }
.confidence.medium { color: #d8e6de; background: #2c4a3c; }
.confidence.low { color: #e2d6bd; background: #4a412a; }
.confidence.none { color: #e6c0c0; background: #45292a; }
.confidence.user { color: #0b100e; background: #b9c9e0; }

.metadata-flag {
  padding: 3px 9px; border-radius: 6px; font-size: .7rem; font-weight: 700;
  color: #e6c0c0; background: #3a2626; white-space: nowrap;
}
.metadata-flag.user { color: #c8d6e8; background: #26313a; }

.status-pill {
  padding: 4px 11px; border-radius: 999px; font-size: .74rem; font-weight: 700;
  text-transform: capitalize; background: var(--raised-2); color: var(--ink-faint);
  border: 1px solid var(--line); white-space: nowrap;
}
.status-pill.playing { color: var(--ground); background: var(--accent); border-color: var(--accent); }
.status-pill.finished { color: #cfe6da; background: #2c4a3c; border-color: #35594a; }
.status-pill.shelved { color: #e2c9a8; background: #3a3226; border-color: #4d4130; }

/* ----------------------------------------------------------------- artwork */

.artwork {
  position: relative; display: grid; place-items: center; flex: none;
  width: 112px; aspect-ratio: 460 / 215; overflow: hidden;
  border: 1px solid var(--line); border-radius: 8px;
  color: #718078; background: linear-gradient(135deg, #1d2923, #111815);
  font: 700 1.3rem var(--sans); letter-spacing: -.02em;
}
.artwork img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; }
.artwork img.loaded { opacity: 1; }
.artwork.large { width: 200px; border-radius: 10px; }
.artwork.hero { width: 100%; max-width: 360px; border-radius: 12px; font-size: 2.4rem; }

/* ------------------------------------------------------------------ picker */

/* No top margin or padding: the masthead's own 22px bottom margin is the whole gap, as on
   the other tabs. It was 66px (26 margin + 18 padding on top of it) — owner, 2026-09-30. */
.picker-intro {
  display: flex; align-items: flex-end; justify-content: space-between; gap: 32px;
  margin: 0 0 28px; padding: 0 0 4px;
}
.picker-intro-copy { max-width: 700px; }
.picker-kicker {
  margin: 0 0 9px; color: var(--accent-deep); font-size: .72rem; font-weight: 750;
  letter-spacing: .12em; text-transform: uppercase;
}
.picker-intro h2 { font-size: clamp(2.5rem, 6vw, 4.3rem); line-height: .98; letter-spacing: -.025em; margin-bottom: 12px; }
.picker-intro .lede { max-width: 50ch; margin: 0; font-size: 1.05rem; }
.picker-qualifying { display: grid; flex: none; padding-bottom: 4px; color: var(--ink-faint); text-align: right; font-size: .78rem; }
.picker-qualifying strong { color: var(--accent); font: 500 2.15rem/1 var(--mono); letter-spacing: -.04em; font-variant-numeric: tabular-nums; }

.questions {
  display: grid; gap: 16px; padding: 20px;
  border: 1px solid var(--line); border-radius: 20px; background: var(--raised);
}
.questions > .question:first-child .chips { gap: 0; width: max-content; max-width: 100%; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; }
.questions > .question:first-child .chip { border: 0; border-right: 1px solid var(--line); border-radius: 0; }
.questions > .question:first-child .chip:last-child { border-right: 0; }
/* A fieldset's UA default is min-inline-size: min-content, so it refuses to shrink
   below its widest unbreakable child — here the one-line segmented control — and
   pushes the whole page sideways instead. Zero at every width, not just on phones:
   the gap between the two was 621–753px of horizontal scroll. */
.question { margin: 0; padding: 0; border: 0; min-inline-size: 0; }
.question legend {
  padding: 0 0 8px; color: var(--ink-faint);
  font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
}
.question-summary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; color: var(--ink-dim); font-size: .88rem; }
.more-questions { display: grid; gap: 16px; padding-top: 2px; }
.more-questions[hidden] { display: none; }
.stretch-row { display: flex; align-items: center; gap: 14px; }
.stretch-end { color: var(--ink-faint); font-size: .78rem; font-weight: 600; white-space: nowrap; }
.stretch-row input[type="range"] { flex: 1; accent-color: var(--accent); min-width: 120px; }
.stretch-value { margin: 8px 0 0; color: var(--ink-dim); font-size: .85rem; }

.picker-decision { display: flow-root; }
.picker-actions {
  position: sticky; top: var(--masthead-height, 90px); z-index: 8;
  display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
  margin: 20px 0 24px; padding: 12px 0;
  background: #0b100ef2;
  box-shadow: 0 12px 24px #0b100e99;
  backdrop-filter: blur(12px);
}
.picker-count { color: var(--ink-faint); font-size: .8rem; }

/* The shortlist. Row geometry is shared with .lib-row further down; everything
   here is what the picker adds to it. */
.pick-list { display: grid; gap: 9px; }
.pick-row.open { border-color: var(--line); background: var(--raised-2); }
.pick-rank {
  flex: none; min-width: 22px; height: 22px; display: inline-grid; place-items: center; padding: 0 6px;
  border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink-faint); background: var(--ground); font: 500 .72rem/1 var(--mono); font-variant-numeric: tabular-nums;
}
.pick-why { color: var(--ink-dim); font-size: .84rem; line-height: 1.45; }
.pick-row-actions { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
.pick-action {
  padding: 7px 12px; border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink-dim); background: var(--raised-2); font-size: .76rem; font-weight: 700; white-space: nowrap;
}
.pick-action:hover { color: var(--ink); border-color: var(--accent-deep); }
.pick-action.primary { color: var(--ground); border-color: var(--accent); background: var(--accent); }
.pick-action.primary:hover { background: #b8e2cc; }
.pick-action.quiet { color: var(--ink-faint); background: transparent; }
.pick-action.quiet.on { color: var(--ground); border-color: var(--accent); background: var(--accent); }
/* Amber, like every other "you have narrowed this yourself" mark in the app. */
.pick-lock.on { color: var(--ground); border-color: var(--stretch); background: var(--stretch); }
.pick-row.held { border-color: var(--stretch-deep); }
/* The legacy feedback bar, in the expanded breakdown: the row's own actions
   reshape the shortlist; these record a verdict on the game. */
.feedback-bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 12px; }
.feedback-spacer { flex: 1; min-width: 0; }
.feedback-btn {
  border: 1px solid var(--line); border-radius: 8px; padding: 7px 13px;
  color: var(--ink-dim); background: transparent; font-size: .82rem; font-weight: 600;
}
.feedback-btn:hover { color: var(--ink); border-color: var(--accent-deep); }
.feedback-btn.primary { color: var(--ground); background: var(--accent); border-color: var(--accent); }
.feedback-btn.danger { color: var(--danger); border-color: #553939; }
.feedback-btn.quiet { color: var(--ink-faint); border-color: transparent; }
.feedback-btn.quiet:hover { border-color: var(--line); }
/* An anchor wearing a button, because the steam:// hand-off has to stay a real
   navigation. Steam blue rather than the app's green: it leaves the app. */
.launch-steam {
  display: inline-flex; align-items: center; padding: 7px 12px;
  border: 1px solid #32698a; border-radius: 999px;
  color: #d9f2ff; background: #17344a;
  font-size: .76rem; font-weight: 700; text-decoration: none; white-space: nowrap;
}
.launch-steam:hover { border-color: #4d90bd; background: #1d4362; }
.launch-steam.primary { padding: 9px 16px; border-radius: 10px; font-size: .84rem; }
/* Spans the row: the breakdown belongs to the whole game, not to one column. */
.pick-row-breakdown {
  grid-column: 1 / -1; display: grid; gap: 10px;
  margin-top: 4px; padding-top: 12px; border-top: 1px solid var(--line-soft);
  cursor: default;
}
/* [hidden] is a UA rule and loses to the class that sets display. */
.pick-row-breakdown[hidden] { display: none; }

/* The in-game shot on the expanded card. Steam's path_full is 16:9-ish; capping the
   height keeps a tall card from pushing the factor table below the fold. */
.pick-screenshot {
  width: 100%; max-height: 260px; object-fit: cover;
  border-radius: 8px; border: 1px solid var(--line-soft); display: block;
}

.pick-stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px 18px; padding: 12px 0; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft);
}
/*
  The key/value primitive, and the place the header/data collision actually lived.
  .stat-label used to be .72rem/700 uppercase in ink-faint — which is LARGER than
  the .68rem section heading it sits under, in the same colour and the same
  all-caps micro-type. The two read as one indistinguishable band of small caps.

  Under direction B the label is mono and the heading is not, so no amount of
  size drift can make them look alike again. Mono also needs no tracking to stay
  legible in caps: the letterforms are already spaced.
*/
.stat-pair { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.stat-label {
  color: var(--ink-faint); font-family: var(--mono);
  font-size: .655rem; font-weight: 400; letter-spacing: .02em; text-transform: uppercase;
}
/* Every consumer of .stat-value holds machine data — dates, ratings, ids, tiers —
   so the value side is mono throughout, and tabular so a column of them aligns. */
.stat-value { font-family: var(--mono); font-size: .82rem; font-weight: 500; font-variant-numeric: tabular-nums; }

.breakdown { list-style: none; margin: 10px 0 0; padding: 12px 14px; border-radius: 10px; background: var(--ground); }
.breakdown li { display: flex; justify-content: space-between; gap: 14px; padding: 4px 0; font-size: .84rem; }
.factor-label { color: var(--ink-dim); }
.factor-points { font-variant-numeric: tabular-nums; font-weight: 700; white-space: nowrap; }
.factor-points.plus { color: var(--accent); }
.factor-points.minus { color: var(--danger); }
.factor-breakdown { display: grid; gap: 8px; }
.factor-breakdown .evidence-label { margin: 0; }

/* "What narrowed this?" — one row per engaged gate the relaxation math scored. */
.narrowed-by .narrowed-row {
  display: flex; gap: 12px; align-items: center; justify-content: space-between;
  padding: 7px 0; font-size: .88rem; border-top: 1px solid var(--line-soft);
}
.narrowed-by .narrowed-row:first-of-type { border-top: 0; }

/* ----------------------------------------------------------------- shelves */

.shelves { margin-top: 64px; padding-top: 28px; border-top: 1px solid var(--line); }
.shelves-heading { margin-bottom: 6px; color: var(--ink-faint); font-size: .78rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
.shelf { margin: 22px 0; }
.shelf-head { display: flex; flex-wrap: wrap; gap: 10px; align-items: baseline; margin-bottom: 10px; }
.shelf-head h4 { margin: 0; font: 600 1.05rem var(--sans); letter-spacing: -.015em; }
.shelf-blurb { color: var(--ink-faint); font-size: .82rem; }
.shelf-row {
  display: grid; grid-auto-flow: column; grid-auto-columns: 168px;
  gap: 12px; overflow-x: auto; padding-bottom: 8px; scrollbar-width: thin;
}
.shelf-item {
  display: grid; gap: 6px; padding: 8px; text-align: left;
  border: 1px solid transparent; border-radius: 12px; background: none;
}
.shelf-item:hover { border-color: var(--line); background: var(--raised); }
.shelf-item .artwork { width: 100%; }
.shelf-title { font-size: .85rem; font-weight: 600; line-height: 1.3; }
.shelf-meta { color: var(--ink-faint); font-size: .74rem; }
/* The scroll arrows, right-aligned in the shelf head rather than floated over the
   art: an overlay would sit on top of the one thing a tile is for. Sized off the
   detail header's navigation buttons so the two rows of arrows on this page match. */
.shelf-nav { display: flex; flex: none; gap: 6px; margin-left: auto; }
.shelf-arrow { width: 32px; height: 32px; font-size: 1rem; }
/* Scroll snapping, so a paged scroll lands on a tile edge instead of halfway
   through one. Proximity rather than mandatory: a mandatory axis fights a
   trackpad flick and makes a short drag feel stuck. */
.shelf-row { scroll-snap-type: x proximity; }
.shelf-item { scroll-snap-align: start; }

/* ----------------------------------------------------------------- library */

.library-intro { display: flex; align-items: end; justify-content: space-between; gap: 36px; margin: 0 0 28px; padding: 0 0 4px; }
.library-intro-copy { max-width: 620px; }
.library-intro h2 { margin: 0 0 10px; font-size: clamp(2.8rem, 6vw, 4.4rem); line-height: .95; letter-spacing: -.025em; }
.library-intro .lede { margin: 0; }
.library-summary { display: flex; gap: 30px; flex: none; padding-bottom: 5px; }
.library-summary-stat { display: grid; gap: 3px; min-width: 84px; }
.library-summary-stat strong { color: var(--stretch); font: 500 1.7rem/1 var(--mono); letter-spacing: -.04em; font-variant-numeric: tabular-nums; }
.library-summary-stat span { color: var(--ink-faint); font-family: var(--mono); font-size: .655rem; font-weight: 400; letter-spacing: .02em; text-transform: uppercase; }
.library-controls {
  position: sticky; top: var(--masthead-height, 90px); z-index: 9;
  margin-bottom: 24px; padding: 12px 0 20px;
  background: #0b100ef2;
  box-shadow: 0 12px 24px #0b100e99;
  backdrop-filter: blur(12px);
}
/*
  One row of controls that wraps rather than a fixed grid: search, status, sort
  and interest are four groups of different widths, and a grid track for each
  either squeezed the search box or pushed the last group off on its own line.
  The search box takes a share rather than the remainder — it is a text field
  among button groups, and letting it absorb every spare pixel is what left the
  row unable to hold a fourth group at all.
*/
.library-primary-controls { display: flex; flex-wrap: wrap; gap: 10px 12px; align-items: center; }
.library-primary-controls .search-box { flex: 1 1 200px; max-width: 300px; }
.library-segments { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; background: var(--raised); }
.library-segment { padding: 10px 15px; border: 0; border-right: 1px solid var(--line); color: var(--ink-dim); background: transparent; font-size: .8rem; white-space: nowrap; }
.library-segment:last-child { border-right: 0; }
.library-segment:hover { color: var(--ink); background: var(--raised-2); }
.library-segment.on { color: var(--ground); background: var(--stretch); font-weight: 750; }
/* Amber where the sort segments are green: this one hides games. */
.interest-segments .library-segment.on { color: var(--ground); background: var(--stretch); }
.filter-bar { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; }
.library-advanced {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(145px, 1fr)); gap: 8px;
  margin: 12px 0 0; padding-top: 12px; border-top: 1px solid var(--line-soft);
}
.library-advanced > .filter-select { width: 100%; padding: 8px 9px; text-overflow: ellipsis; }

/*
  The one button the advanced grid and the A–Z row now live behind. Nine
  dropdowns and twenty-eight letter chips is most of a laptop's first screen
  spent on controls, for a session that usually touches none of them.

  What makes hiding them safe is the count, so the button carries one: it wears
  the same amber as the "N shown" pill and the reset button — the three things
  on the page that mean "you are not looking at everything" — and says how many
  are on rather than merely that some are. Only the filters it actually hides
  are counted; search, status and sort stay visible and speak for themselves.

  No display of its own on .library-filter-panel, deliberately: the UA's
  [hidden] rule is then what closes it, and there is no class for it to lose to
  the way .filter-menu-panel did.
*/
.library-filter-toggle {
  display: inline-flex; gap: 9px; align-items: center;
  padding: 9px 14px; border: 1px solid var(--line); border-radius: 10px;
  color: var(--ink-dim); background: transparent; font-size: .88rem; font-weight: 600;
}
.library-filter-toggle:hover { color: var(--ink); border-color: var(--accent-deep); }
.library-filter-toggle.on {
  color: var(--stretch); border-color: var(--stretch-deep); background: #2a1f12;
  box-shadow: 0 0 0 1px #e5b87833;
}
.library-filter-toggle.on .filter-menu-caret { color: var(--stretch); }
.library-filter-toggle .filter-menu-caret { transition: transform .15s ease; }
.library-filter-toggle.open .filter-menu-caret { transform: rotate(180deg); }
.library-filter-count {
  display: inline-grid; place-items: center; min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: 999px; color: var(--ground); background: var(--stretch);
  font: 500 .68rem/1 var(--mono); font-variant-numeric: tabular-nums;
}

/* The one control that undoes every other one, so it should read as available
   the moment anything is narrowing the list, and as nothing at all when there
   is nothing to undo. Amber rather than the green accent: it matches the "N
   shown" pill, which is the other thing on screen saying the list is filtered. */
.library-reset { transition: color .15s ease, border-color .15s ease, background-color .15s ease, box-shadow .15s ease; }
.library-reset:not(:disabled) {
  color: var(--stretch); border-color: var(--stretch-deep); background: #2a1f12;
  font-weight: 750; box-shadow: 0 0 0 1px #e5b87833, 0 6px 14px #0b100e80;
}
.library-reset:not(:disabled):hover {
  color: var(--ground); border-color: var(--stretch); background: var(--stretch);
}
.library-reset:disabled { opacity: .45; cursor: default; }

/* "Add a game" — the top-up path for titles no sync can see. An inline panel
   under the primary controls rather than a dialog: it is part of tending the
   library, not an interruption to it. Suggestion rows come from the static
   catalog index, searched client-side; the last row is always the provisional
   add-by-title path. */
.library-add { margin-top: 12px; }
.library-add-head-end { display: flex; gap: 12px; align-items: center; margin-left: auto; }
.library-add-close {
  display: inline-grid; place-items: center; flex: none; width: 32px; height: 32px; padding: 0;
  border: 1px solid var(--line); border-radius: 8px;
  color: var(--ink-dim); background: transparent; font-size: 1.2rem; line-height: 1;
}
.library-add-close:hover, .library-add-close:focus-visible { color: var(--ink); border-color: var(--accent-deep); background: var(--raised); }
.library-add .search-box { width: 100%; max-width: none; }
/* Search takes the room it needs; the system picker keeps a fixed, readable width and
   drops below it when the panel is narrow. */
.library-add-controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.library-add-controls .search-box { flex: 1 1 220px; }
.library-add-controls .filter-select { flex: 0 0 auto; width: auto; min-width: 170px; }
.library-add-results { display: grid; gap: 2px; margin-top: 8px; }
.library-add-results > p { margin: 4px 2px 0; }
.library-add-row {
  display: flex; gap: 10px; align-items: center; width: 100%; padding: 8px 9px; text-align: left;
  border: 1px solid transparent; border-radius: 7px; color: var(--ink); background: transparent;
  font-size: .86rem; cursor: pointer;
}
.library-add-row:hover, .library-add-row:focus-visible { background: var(--raised); border-color: var(--accent-deep); }
.library-add-row:disabled { opacity: .45; cursor: default; }
.library-add-row .library-add-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.library-add-row .store-badge, .library-add-row .muted { flex: none; }
.library-add-manual { border-top: 1px solid var(--line-soft); border-radius: 0 0 7px 7px; }
.library-add-manual .library-add-name { white-space: normal; }

/* Multi-answer dropdown (UI.filterMenu). The closed button is styled as a
   .filter-select so it sits in the grid indistinguishably from the ones that
   take a single answer; only the panel below it differs. */
.filter-menu { position: relative; min-width: 0; }
.filter-menu-button {
  display: flex; gap: 8px; align-items: center; width: 100%; padding: 8px 9px; text-align: left;
}
.filter-menu-summary { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filter-menu-caret { flex: none; color: var(--ink-faint); font-size: .68rem; }
.filter-menu-button.on .filter-menu-caret { color: var(--stretch); }
.filter-menu-button[aria-expanded="true"] { border-color: var(--accent-deep); background: var(--raised-2); }
/*
  A panel is drawn inside .library-controls, which is z-index 9 — under the
  sticky footer's 110. So a long list ran beneath the footer and the back-to-top
  button, and the rows down there were not merely hard to read: the click landed
  on the footer, which is outside the menu, so it was swallowed and dismissed the
  menu the user was aiming at. Both of those read as "clicking the name does
  nothing".

  Lifted only while a panel is open, so the rail and the footer keep the stacking
  order the rest of the library was designed around.
*/
.library-controls:has(.filter-menu-panel:not([hidden])) { z-index: 120; }
.filter-menu-panel {
  position: absolute; z-index: 12; top: calc(100% + 6px); left: 0;
  min-width: 100%; width: max-content; max-width: 300px;
  padding: 8px; border: 1px solid var(--line); border-radius: 12px;
  background: var(--raised-2); box-shadow: 0 16px 30px #0b100ecc;
}
/* [hidden] is a UA rule and loses to any class that sets display. */
.filter-menu-panel[hidden] { display: none; }
.filter-menu-search {
  width: 100%; margin-bottom: 7px; padding: 7px 9px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--ground); color: var(--ink); font-size: .82rem;
}
.filter-menu-search:focus { outline: none; border-color: var(--accent-deep); }
.filter-menu-empty { margin: 8px 7px 2px; color: var(--ink-faint); font-size: .8rem; }
.filter-menu-options { display: grid; gap: 1px; max-height: 300px; overflow-y: auto; }
.filter-menu-option[hidden] { display: none; }
.filter-menu-option {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 9px; align-items: center;
  padding: 5px 7px; border-radius: 8px; color: var(--ink-dim); font-size: .82rem; cursor: pointer;
}
.filter-menu-option:hover { color: var(--ink); background: var(--raised); }
.filter-menu-option input { accent-color: var(--accent); width: 15px; height: 15px; }
.filter-menu-option small { color: var(--ink-faint); font-size: .7rem; font-variant-numeric: tabular-nums; }
.filter-menu-actions {
  display: flex; gap: 8px; align-items: center; justify-content: space-between;
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line-soft);
}
.filter-menu-clear, .filter-menu-done {
  padding: 5px 12px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--raised); font-size: .76rem; font-weight: 650;
}
.filter-menu-clear { color: var(--ink-faint); }
.filter-menu-clear:hover:not(:disabled) { color: var(--ink); border-color: var(--accent-deep); }
.filter-menu-clear:disabled { opacity: .45; cursor: default; }
.filter-menu-done { color: var(--ground); border-color: var(--accent); background: var(--accent); }
.library-letter-filter { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 12px; }
.library-letter {
  display: inline-grid;
  grid-template-columns: auto auto;
  gap: 4px;
  align-items: baseline;
  min-width: 34px;
  padding: 6px 8px;
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--ink-dim);
  background: var(--raised);
  font-size: .76rem;
  font-weight: 750;
}
.library-letter small { color: var(--ink-faint); font-size: .6rem; font-weight: 650; }
.library-letter:hover { color: var(--ink); border-color: var(--accent-deep); }
.library-letter.on { color: var(--ground); border-color: var(--stretch); background: var(--stretch); }
.library-letter.on small { color: #183126; }
.library-sticky-count {
  display: inline-flex;
  width: fit-content;
  margin: 14px 0 0;
  padding: 6px 12px;
  border-radius: 999px;
  color: var(--ground);
  background: var(--stretch);
  font-size: .82rem;
  font-weight: 800;
  letter-spacing: .02em;
  box-shadow: 0 4px 14px #0005;
}
.sort-field {
  display: flex; grid-column: span 2; align-items: center; gap: 8px; min-width: 290px; padding-left: 8px;
  border-left: 1px solid var(--line);
}
.sort-field .filter-select { width: 100%; padding: 8px 9px; }
.sort-label { color: var(--ink-faint); font-size: .76rem; font-weight: 700; white-space: nowrap; }
.lib-footer {
  position: sticky;
  bottom: 0;
  z-index: 110;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: #0b100ef2;
  box-shadow: 0 -12px 24px #0b100e99;
  backdrop-filter: blur(12px);
}
/* Kept deliberately low: it is pinned over the bottom of the list and over the
   hover rail, so every pixel of it is a pixel of the library you cannot see. */
.lib-footer .inline-check { font-size: .8rem; }
.library-jump { display: flex; flex: 0 0 auto; gap: 8px; }
.library-to-top, .library-to-bottom {
  width: 32px;
  height: 32px;
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--accent-deep);
  font-size: 1.05rem;
  line-height: 1;
  box-shadow: var(--shadow-sm);
}
.library-to-top:hover, .library-to-bottom:hover { border-color: var(--accent-deep); color: var(--ink); }
/* .text-input is the sign-in email field — the first input a new visitor touches. It
   had no base rule at all (only a flex size), so it wore Chromium's dark-scheme
   default: a grey slab, a 2px grey inset border and square corners, beside every
   other input on the site looking like this. */
.search-box, .filter-select, .text-input {
  border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
  background: var(--raised); color: var(--ink); font-size: .88rem; min-width: 0;
}
.text-input::placeholder { color: var(--ink-faint); }
/*
  A filter that is on, in the same amber as the "N shown" pill and the reset
  button — the three things on the page that mean "you are not looking at
  everything". A control hiding half the library used to be indistinguishable
  from one hiding nothing, which is how a filter gets left on and then blamed on
  the data.
*/
.search-box.on, .filter-select.on, .filter-menu-button.on {
  border-color: var(--stretch); color: var(--ink);
  background: #2a1f12; box-shadow: 0 0 0 2px #e5b8783d;
}
.search-box.on::placeholder { color: #c9a978; }
.search-box { flex: 1 1 220px; }
.search-box:focus, .filter-select:focus, .text-input:focus { outline: none; border-color: var(--accent-deep); box-shadow: 0 0 0 3px #84b99d22; }

/*
  align-content: start keeps the rows at their own height if the list is ever
  given more height than they fill; a grid's default, stretch, hands the extra
  to the row tracks (one result once grew to fill ~900px).
*/
.lib-list { display: grid; gap: 9px; align-content: start; }
/* The picker's shortlist is the same object as a library row and is styled with
   it, so the two lists cannot drift apart. .pick-row is deliberately not also
   .lib-row: that class is what dom.js keys the hover *rail* off, and the rail
   positions itself against the library's sticky controls, which the picker has
   none of. */
.lib-row, .pick-row {
  position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr) 150px auto auto; gap: 16px; align-items: center;
  padding: 12px 16px; border: 1px solid transparent; border-radius: 18px; background: var(--raised); cursor: pointer;
  /*
    The library renders every row it matched, on purpose — there is no hidden
    page of results and no "load more". content-visibility keeps that promise
    while letting the browser skip layout and paint for the rows currently off
    screen: seven hundred rows stay in the DOM, findable by the browser's own
    Ctrl-F, but only the visible ones cost anything to draw.

    contain-intrinsic-size supplies a placeholder height for skipped rows so the
    scrollbar does not lurch as they are realised.
  */
  content-visibility: auto;
  contain-intrinsic-size: auto 150px;
}
.health-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(145px, 1fr)); gap: 10px; margin: 18px 0; }
.health-stat { display: grid; gap: 3px; padding: 16px; border: 1px solid var(--line); border-radius: 14px; color: var(--ink); background: var(--raised); text-align: left; }
.health-stat:hover, .health-stat.active { border-color: var(--accent-deep); background: var(--raised-2); }
.health-stat strong { font: 500 1.55rem var(--mono); letter-spacing: -.04em; font-variant-numeric: tabular-nums; }
.health-stat span { color: var(--ink-faint); font-family: var(--mono); font-size: .655rem; font-weight: 400; letter-spacing: .02em; text-transform: uppercase; }
.health-controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 14px; }
.health-list { display: grid; gap: 9px; }
.health-row { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 14px; align-items: center; padding: 10px; border: 1px solid var(--line-soft); border-radius: 14px; background: var(--raised); }
.health-row .artwork { width: 90px; }
.lib-row:hover, .pick-row:hover { border-color: var(--line); background: var(--raised-2); }
.lib-row .artwork { width: 160px; border-radius: 12px; }
/*
  The library row is the cover and then one block of words, and the block is the
  button — everything a row says is inside .lib-open, so "open this game" is the
  whole block rather than a strip of title text, and a pointer anywhere on it
  finds the button rather than whichever badge it landed on.

  The old shape gave the playtime and the status a fixed column each, taken out
  of the row's width on every line: the title was left ~160px of a ~740px row
  and wrapped ordinary names like "Kingdom Come: Deliverance". Here they are
  lines inside the block instead, so the title has the whole width.
*/
.library-view .lib-row {
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto;
  column-gap: 16px;
  align-items: center;
}
.library-view .lib-row > .artwork-open { grid-column: 1; }
/*
  Inside the block: the title alone on its own line at 1.75rem, then the chips
  and the facts on the left against the hours on the right.

  Five stacked lines of small type became three, and the title stopped sharing
  its line with two badges — it was set smaller than the playtime it competed
  with. Every badge now shares one wrapping chip row, so a row missing its
  storefronts or its flag is shorter rather than carrying a reserved empty line.
*/
.library-view .lib-open {
  grid-column: 2;
  display: flex; flex-direction: column; gap: 9px;
}
.library-view .lib-title {
  font-size: 1.75rem; line-height: 1.1; letter-spacing: -.016em; text-wrap: pretty;
}
.lib-body { display: flex; gap: 18px; align-items: flex-start; }
.lib-meta { display: flex; flex-direction: column; gap: 7px; flex: 1; min-width: 0; }
.lib-chips { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
/* Both badge groups are children of the chip row now, so the top margins that
   separated them as their own lines would only push them off its baseline. */
.lib-chips .store-badges, .lib-chips .platform-badges { margin-top: 0; }
.lib-chips .review-rating { padding: 3px 8px; font-size: .7rem; }
.lib-chips .deck-badge { padding: 3px 9px; font-size: .68rem; }
/*
  The hours as the row's figure, in the Plex Mono the app sets every number in
  (.health-stat strong, .picker-qualifying strong) rather than in another line
  of prose. Its label carries the last-played date, which left the facts line to
  come here: it is a fact about playing this game, not a fact about the game.

  flex: none, so a long title or a long facts line takes the width it needs from
  the left column instead of squeezing the number.
*/
.lib-stat { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; flex: none; }
.lib-hours { color: var(--accent); font: 500 2.1rem/1 var(--mono); letter-spacing: -.045em; font-variant-numeric: tabular-nums; }
.lib-hours-unit { font-size: 1.1rem; letter-spacing: .01em; }
.lib-stat-label {
  max-width: 13ch; color: var(--ink-faint); font-size: .62rem; font-weight: 750;
  letter-spacing: .09em; text-transform: uppercase; text-align: right;
}
/*
  A tombstone's Restore is the one piece that cannot be inside the button, so it
  rides the block's top-right corner and has to be lifted over it to stay
  clickable. Only tombstone rows have one, and only the empty end of a title
  line sits under it.
*/
.library-view .lib-row > .lib-restore {
  grid-column: 2; justify-self: end; align-self: start;
  position: relative; z-index: 2;
}
/* At 1.75rem the title reaches further than it used to, so on the only rows
   that have a Restore it keeps clear of it rather than running underneath. */
.library-view .lib-row:has(> .lib-restore) .lib-title { padding-right: 96px; }
/*
  Bigger than a library row's, because this list is five games rather than a
  thousand: there is room for the cover to be something you recognise across the
  desk rather than a thumbnail you have to read the title beside.

  It also runs the full height of the row, top and bottom flush with the text
  beside it. That means dropping Steam's 460:215 header ratio and letting the
  image crop instead — the row's height is set by the words, and a tile that
  kept its own ratio would float in the middle of it with a band of background
  above and below.
*/
.pick-row .artwork-open { align-self: stretch; display: grid; align-items: center; }
/*
  The cover keeps Steam's own 460:215 rather than being cropped to whatever
  height the words happen to need. Stretching the tile to the row and letting
  object-fit: cover trim the difference took a slice off one axis or the other
  at every row height; here the image is simply the right shape, and at 300px it
  is also the tallest thing in the row, so the row takes its height from the
  picture and the two line up without either being cut.
*/
.pick-row .artwork { width: 300px; aspect-ratio: 460 / 215; border-radius: 12px; }
/*
  Two rows inside the row: the cover beside the words, and the controls on their
  own line beneath them. A single line could not hold a 300px cover, a reason
  worth reading and five controls — the reason lost, wrapping to six lines in a
  column narrower than the buttons beside it. Playtime also sizes to content
  here rather than taking the library's fixed 150px column.
*/
.pick-row { grid-template-columns: auto minmax(0, 1fr) auto auto; }
.pick-row > .artwork-open { grid-row: 1 / span 2; }
.pick-row > .lib-open, .pick-row > .lib-playtime, .pick-row > .status-pill { grid-row: 1; }
.pick-row-actions { grid-row: 2; grid-column: 2 / -1; justify-content: flex-start; }
.lib-open {
  display: grid; gap: 3px; text-align: left; border: 0; background: none; padding: 0; min-width: 0;
}
.lib-title-line { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.lib-title-line .review-rating, .lib-title-line .deck-badge { padding: 2px 7px; font-size: .64rem; }
.lib-title { font-size: 1.15rem; font-weight: 600; }
.store-badges { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 3px; }
.store-badge {
  display: inline-flex; align-items: center; min-height: 22px; padding: 3px 9px;
  border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink); background: var(--ground); font-size: .68rem; font-weight: 800;
  letter-spacing: .045em; text-transform: uppercase;
}
.store-badge.steam { color: #d9f2ff; border-color: #32698a; background: #17344a; }
.store-badge.epic { color: #fff; border-color: #686868; background: #292929; }
.store-badge.gog { color: #f1ddff; border-color: #74489a; background: #3d2457; }
/* No-appid catalog entries in the add typeahead — a real game, just not a Steam one. */
.store-badge.catalog { color: #e7f2dd; border-color: #5c7d43; background: #2c3d20; }
/* Systems, not storefronts — outlined rather than filled so the two axes stay tellable
   apart at a glance without inventing a colour per console. */
.store-badge.platform { color: var(--ink-dim); border-color: var(--line); background: transparent; }
.platform-badges { margin-top: 2px; }
/* A value that filters. Styled as text with an underline rather than as a
   button, so a row of them still reads as a sentence of facts — the affordance
   is the underline and the cursor, not a box around every genre. */
.filter-jump {
  padding: 0; border: 0; background: none; color: var(--accent);
  font: inherit; text-align: left;
  text-decoration: underline; text-decoration-color: #84b99d55;
  text-underline-offset: 3px; cursor: pointer;
}
.filter-jump:hover { color: var(--ink); text-decoration-color: var(--accent); }
.filter-jump:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

.preview-store-badges { margin: 6px 0 2px; }
.detail-store-badges { margin: 0 0 10px; }
/* Stores then systems on ONE line; the row owns the spacing its two groups used to. */
.detail-badge-row { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin: 0 0 10px; }
.detail-badge-row .detail-store-badges { margin: 0; }
.detail-store-badges .store-badge { min-height: 28px; padding: 5px 11px; font-size: .74rem; }
.lib-facts { color: var(--ink-faint); font-size: .78rem; }
/* With the hours taking a column of their own the facts line wraps sooner, and
   it used to break INSIDE a fact — an orphaned "80" under "Metacritic". Each
   fact moves to the next line whole; the separators are still break points. */
.lib-facts > span { white-space: nowrap; }
.lib-last-played {
  display: inline-block;
  padding: 2px 7px;
  border-radius: 999px;
  color: var(--ink);
  background: var(--accent-soft);
  font-weight: 750;
}
.lib-release-date { color: var(--ink-dim); font-weight: 750; letter-spacing: .015em; }
.lib-playtime { color: var(--ink); font: 500 .94rem var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.lib-footer { margin-top: 12px; padding: 7px 0; border-top: 1px solid var(--line-soft); }
.game-preview {
  position: fixed; z-index: 100;
  display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 14px;
  width: min(560px, calc(100vw - 48px)); padding: 12px;
  border: 1px solid var(--accent-deep); border-radius: 14px;
  color: var(--ink); background: var(--raised-2);
  box-shadow: 0 18px 52px #000b;
  pointer-events: none;
}
.game-preview .artwork.large { width: 200px; }
/* In-game shots in the hover card: the first spans both columns, the rest tile
   beneath it. Fed by previewImages() in dom.ts. */
/* One column, three shots, each the full width of the card — the legacy shape.
   A two-up grid made them thumbnails; at this size you can actually see the game. */
.game-preview-images {
  display: grid; grid-template-columns: 1fr; gap: 7px;
  width: 100%; align-content: start;
}
.game-preview-images img {
  display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover;
  border: 1px solid var(--line); border-radius: 8px;
}
/* Holds a shot's place while it is in flight, so the card opens at its final height instead
   of growing twice under the pointer. Same box as the images it stands in for; quiet enough
   to read as "loading", not as a broken picture. */
.game-preview-placeholder {
  width: 100%; aspect-ratio: 16 / 9;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--raised-2);
}
.game-preview-copy { display: grid; align-content: start; gap: 7px; min-width: 0; }
.game-preview-title { font: 600 1.08rem var(--sans); letter-spacing: -.015em; }
.game-preview-meta, .game-preview-description { margin: 0; }
.game-preview-meta { color: var(--ink-faint); font-size: .76rem; }
.game-preview-reviews { display: grid; gap: 6px; margin: 0; padding-left: 18px; color: var(--ink-dim); font-size: .75rem; }
.game-preview-reviews li > * { margin-right: 6px; }
.game-preview-reviews .review-rating { padding: 2px 6px; font-size: .66rem; }
.game-preview-description {
  display: -webkit-box; overflow: hidden; color: var(--ink-dim);
  font-size: .84rem; line-height: 1.45; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
}
.game-preview-tags { display: flex; flex-wrap: wrap; gap: 5px; }
.game-preview-tags .badge { padding: 3px 8px; font-size: .68rem; }
.game-preview-hint { color: var(--accent); font-size: .74rem; font-weight: 700; }
.steam-reviews { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line-soft); }
.steam-review-list { display: grid; gap: 9px; margin: 8px 0 0; padding-left: 20px; }
.steam-review-item { color: var(--ink-dim); font-size: .84rem; line-height: 1.5; }
.steam-review-item > * { margin-right: 9px; }
.steam-review-item strong { color: var(--ink); }

.review-rating { padding: 3px 8px; border-radius: 999px; font-size: .72rem; font-weight: 750; }
.review-rating.positive { color: #bce6cf; background: #234536; }
.review-rating.medium { color: #f0d89a; background: #4a3f22; }
.review-rating.negative { color: #efb3b3; background: #4b2929; }
.review-positive { color: #91d6ae; }
.review-negative { color: #e99b9b; }
.review-total { color: var(--ink-faint); }

@media (min-width: 1000px) {
  .game-preview.picker-preview {
    grid-template-columns: 400px minmax(0, 1fr);
    width: min(1000px, calc(100vw - 48px)); padding: 20px; gap: 24px;
  }
  .picker-preview .game-preview-images, .picker-preview .artwork.large { width: 400px; }
  .picker-preview .game-preview-copy { gap: 12px; }
  .picker-preview .game-preview-title { font-size: 1.45rem; }
  .picker-preview .game-preview-meta { font-size: .88rem; }
  .picker-preview .game-preview-description { font-size: 1rem; -webkit-line-clamp: 6; }
  .picker-preview .game-preview-tags .badge { padding: 5px 10px; font-size: .76rem; }
  .picker-preview .game-preview-hint { font-size: .85rem; }
}

@media (min-width: 1000px) {
  /* Everything belonging to the list column is narrowed to the list's width, so
     none of it reaches across the 380px the hover rail lives in. The letter
     filter wraps onto an extra row for it, and in exchange the rail can start
     level with the filter selects instead of below the alphabet — which
     UI.positionLibraryRail measures for itself. The footer is here for the same
     reason: it is sticky and z-index 110, so at full width it painted a bar
     straight across the rail. */
  .library-view .result-count, .library-view .lib-list, .library-view > .empty-state,
  .library-view .library-letter-filter, .library-view .lib-footer { width: calc(100% - 380px); }
  .library-view .lib-row {
    /* 300px matches the picker shortlist's own Steam-ratio (460:215) cover size
       below, so a row image reads at roughly the size Steam's own list view
       gives it, not a thumbnail scaled down for density. */
    min-height: 141px; padding: 0 12px 0 0;
    border-radius: 0; overflow: hidden;
    contain-intrinsic-size: auto 141px;
  }
  .library-view .lib-row .artwork { width: 300px; border-width: 0; border-radius: 0; }
  /* No height cap and no inner scrollbar: the rail is pinned below the filter
     controls and never slides up over them, so a tall one is simply clipped by
     the window. See UI.positionLibraryRail. It stays under the masthead (10). */
  .game-preview.library-preview {
    position: fixed; z-index: 9;
    display: grid; grid-template-columns: 1fr; gap: 10px;
    width: 368px; padding: 10px;
    overflow: hidden; border-color: var(--line); border-radius: 0;
  }
  .library-preview .game-preview-copy { grid-row: 1; }
  .library-preview .game-preview-images {
    grid-row: 2; grid-template-columns: 1fr; width: 100%; gap: 8px;
  }
  .library-preview .game-preview-images img:first-child { grid-column: auto; }
  .library-preview .game-preview-images img { border-radius: 0; }
  .library-preview .artwork.large { width: 100%; }
}
.inline-check { display: inline-flex; gap: 8px; align-items: center; color: var(--ink-dim); font-size: .85rem; }
.inline-check input { accent-color: var(--accent); width: 16px; height: 16px; }

/* ----------------------------------------------------------------- profile */

.profile-summary { display: flex; flex-wrap: wrap; gap: 28px; margin-bottom: 24px; }
.summary-stat { display: grid; gap: 2px; }
.summary-stat strong { font: 500 1.6rem var(--mono); letter-spacing: -.04em; font-variant-numeric: tabular-nums; }
.summary-stat span { color: var(--ink-faint); font-size: .72rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }

.lane-bars { display: grid; gap: 9px; }
.chart-note, .confidence-note { margin: 0 0 12px; color: var(--ink-faint); font-size: .8rem; line-height: 1.45; }
.lane-bar-row { display: grid; grid-template-columns: minmax(120px, 180px) 1fr auto; gap: 14px; align-items: center; }
.lane-bar-label { font-size: .85rem; }
.lane-bar-track { height: 8px; border-radius: 999px; background: var(--ground); overflow: hidden; }
.lane-bar-fill { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--accent-deep), var(--accent)); }
.lane-bar-value { color: var(--ink-faint); font-size: .76rem; white-space: nowrap; font-variant-numeric: tabular-nums; }

.conclusions { display: grid; gap: 14px; margin: 20px 0; }
.conclusion { padding: 18px 20px; border: 1px solid var(--line); border-radius: 14px; background: var(--raised); }
.conclusion-head { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.conclusion-text { margin: 0 0 12px; color: var(--ink-dim); line-height: 1.55; }
.evidence { padding-top: 10px; border-top: 1px solid var(--line-soft); }
.evidence-label { margin: 0 0 6px; color: var(--ink-faint); font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
.evidence ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.evidence li { display: flex; flex-wrap: wrap; gap: 10px; justify-content: space-between; font-size: .84rem; }
.evidence-detail { color: var(--ink-faint); font-variant-numeric: tabular-nums; }

/* ----------------------------------------------------------------- history */

.suppression-list, .history-list, .session-list { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 8px; }
.suppression-list li, .session-list li {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
  padding: 8px 10px; border-radius: 8px; background: var(--ground); font-size: .86rem;
}
.sup-title, .sess-title { font-weight: 600; }
.history-list li { display: grid; gap: 3px; padding: 10px; border-radius: 8px; background: var(--ground); font-size: .84rem; }
.hist-when, .sess-when { color: var(--ink-faint); font-size: .74rem; font-variant-numeric: tabular-nums; }
.hist-games { color: var(--ink-dim); }
.hist-chosen { color: var(--accent); font-weight: 600; }
.sess-note { color: var(--ink-faint); font-style: italic; }
.session-list.compact li { padding: 5px 8px; font-size: .8rem; }
/* Each list scrolls inside its own panel, so one long history doesn't push the
   panels below it (and the danger zone) a screen or more down the page. */
.history-view :is(.suppression-list, .history-list, .session-list) {
  max-height: 325px; overflow-y: auto; overscroll-behavior: contain;
  padding-right: 4px; scrollbar-gutter: stable;
}
.danger-zone { border-top-color: #4a2f2f; }

/* The Steam sync log (views/history.js syncPanel). Scrolls in its own panel like the
   lists above; one entry per run, its itemized changes folded behind a <details>. */
.sync-cooldown-note {
  margin: 10px 0 0; padding: 9px 12px; border: 1px solid var(--stretch); border-left-width: 4px;
  border-radius: 8px; background: #3a2e17; color: #f6dfb3; font-size: .84rem; font-weight: 600;
}
.sync-run-list { list-style: none; margin: 10px 0 0; padding: 0 4px 0 0; display: grid; gap: 8px;
  max-height: 325px; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
.sync-run { display: grid; gap: 4px; padding: 10px; border-radius: 8px; background: var(--ground); font-size: .86rem; }
.sync-run-head { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: space-between; }
.sync-run-when { font-weight: 600; font-variant-numeric: tabular-nums; }
.sync-run-summary { margin: 0; overflow-wrap: anywhere; }
.sync-pill { padding: 2px 9px; border: 1px solid var(--line); border-radius: 999px; color: var(--ink-dim); font-size: .72rem; font-weight: 700; }
.sync-pill.ok { color: #cfe6da; background: var(--accent-soft); border-color: #35594a; }
.sync-pill.private { color: #e2c9a8; background: #3a3226; border-color: #4d4130; }
.sync-pill:is(.failed, .interrupted) { color: #f3cdcd; background: var(--danger-bg); border-color: #6a3c3c; }
.sync-run-changes summary { cursor: pointer; color: var(--accent); font-size: .8rem; font-weight: 600; }
.sync-change-list { list-style: none; margin: 6px 0 0; padding: 0; display: grid; gap: 4px; }
.sync-change-list li { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: baseline; justify-content: space-between; font-size: .82rem; }
.sync-change-list .muted { font-variant-numeric: tabular-nums; }
.sync-change-title { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.storage-note { margin-top: 16px; }

/* ---------------------------------------------------------- detail + dialog */

dialog {
  width: min(760px, calc(100% - 24px)); max-height: 88vh; padding: 0;
  border: 1px solid var(--line); border-radius: 18px;
  color: var(--ink); background: var(--raised-2); overflow: auto;
  /* Keep wheel gestures inside the remaining modal utilities — restore and the
     raw-data viewer — rather than handing them to the page underneath. */
  overscroll-behavior: contain;
}
dialog::backdrop { background: #04070699; backdrop-filter: blur(2px); }
.dialog-body { padding: 26px; display: grid; gap: 18px; }

/* The detail as a routed PAGE rather than a modal. It sits in the normal document
   flow where #view-root would be, so it scrolls with the page and its sticky
   header pins under the masthead like every other view. */
.detail-page { width: 100%; margin: 0 auto; }
.detail-page .dialog-body { padding: 0; }
.detail-page .detail-sticky {
  margin: 0; padding: 14px 0;
  background: color-mix(in srgb, var(--ground) 94%, transparent);
}
.dialog-close {
  position: absolute; top: 14px; right: 16px; z-index: 2;
  width: 34px; height: 34px; border: 1px solid var(--line); border-radius: 8px;
  color: var(--ink-dim); background: var(--ground); font-size: 1.2rem; line-height: 1;
}
.dialog-close:hover { color: var(--ink); border-color: var(--accent-deep); }
.detail-page .detail-page-close { margin-right: 8px; }

/*
  The only pinned part of the detail page. It is TWO ROWS: cover and title, then the
  outbound links, the launch hand-off and the walk. Everything the title used to share
  its line with moved down, which is what let the cover grow from 96px to 152 — at 96,
  wedged between a heading and five controls, it was a favicon of a cover.

  Two rows cost more permanent room than one, so the bar does not keep them: see
  .detail-sticky.condensed below, which app.js applies the moment the bar is actually
  stuck under the masthead.
*/
.detail-sticky {
  position: sticky; top: var(--masthead-height, 0px); z-index: 3;
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px 16px;
  margin: -26px -26px 0; padding: 13px 26px;
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--raised-2) 94%, transparent);
  backdrop-filter: blur(10px);
}
/*
  What puts the second row BELOW rather than beside: a flex-basis of 100% cannot share
  a line with anything, so .detail-sticky-sub always breaks. The first row is elastic
  instead, so it and the close button ride line one together.
*/
.detail-sticky-main { display: flex; flex: 1 1 220px; flex-wrap: wrap; align-items: center; gap: 16px; min-width: 0; }
.detail-sticky-sub { display: flex; flex: 1 1 100%; flex-wrap: wrap; align-items: center; gap: 10px 16px; min-width: 0; }
.detail-sticky h2 { flex: 1 1 180px; margin: 0; min-width: 0; font-size: 1.75rem; }
/* Inside the dialog the sticky header pins to the dialog's own scroll box,
   not under the page masthead. */
#detail-dialog .detail-sticky { top: 0; }
/* One group: the outbound links and the launch hand-off. The auto margin that used to
   push them right is gone — on a row of their own they lead from the left, and the walk
   opposite them carries the margin instead. */
.detail-header-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.detail-header-launch { flex: none; }
/* Sized off .launch-steam beside it so the three read as one row of controls, but
   outlined in the app's own palette — Steam blue is reserved for the one action that
   actually hands off to Steam. */
.detail-header-link, .detail-header-ask {
  display: inline-flex; align-items: center; padding: 7px 12px;
  border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink-dim); background: transparent;
  font-size: .76rem; font-weight: 700; text-decoration: none; white-space: nowrap;
}
.detail-header-link:hover, .detail-header-ask:hover { color: var(--ink); border-color: var(--accent-deep); background: var(--raised); }
/* Ask AI wears the link pill but is NOT one of the links the condensed bar hides below
   1024px: since leaving the research row it has no second home on the page. */
.detail-sticky.condensed .detail-header-ask { padding: 6px 10px; }
/* The Play Order button beside Ask AI wears the same pill (the shared .po-menu-button is
   sized for a library row); its "on" state keeps the mono position so it reads as the
   Library's. It stays through the condensed fold, like Ask AI, since it is an action on
   this game and not a link away from it. */
.detail-header-actions .po-menu-button {
  min-height: 0; padding: 7px 12px; background: transparent; font-size: .76rem;
}
.detail-header-actions .po-menu-button:hover { background: var(--raised); }
.detail-header-actions .po-menu-button.on { font-size: .8rem; }
.detail-sticky.condensed .detail-header-actions .po-menu-button { padding: 6px 10px; }
/*
  WALKING THE LIST, NAMED.

  This replaced a pair of 36px ← / → icon buttons that said a next game existed without
  saying which — the destination was in the aria-label, visible to a screen reader and to
  nobody else. Styled as text with an arrow rather than as two more boxes: the sub-row
  already carries three pill controls, and a fourth and fifth box beside them would read
  as more actions on THIS game rather than as a way off it.
*/
/* Allowed to SHRINK (flex 0 1 auto, min-width 0). It was flex: none, so the row stayed
   as wide as its content — two 22ch titles and the count — and on a 390px phone the
   next-game button sat off the right edge and the page scrolled sideways. The
   flex-wrap below never got to wrap anything, because nothing ever made the row
   narrower. On a wide screen there is room, and nothing changes. */
.detail-walk { display: flex; flex: 0 1 auto; min-width: 0; max-width: 100%; flex-wrap: wrap; align-items: center; gap: 4px 14px; margin-left: auto; }
.detail-walk-step {
  display: inline-flex; align-items: center; gap: 8px; max-width: 22ch; min-width: 0;
  padding: 4px 0; border: 0; background: none;
  color: var(--ink-dim); font-size: .86rem; font-weight: 600; text-align: left;
}
.detail-walk-step:hover:not(:disabled) { color: var(--ink); }
.detail-walk-step:hover:not(:disabled) .detail-walk-arrow { color: var(--accent); }
.detail-walk-step:disabled { color: var(--ink-faint); opacity: .5; cursor: default; }
.detail-walk-arrow { flex: none; color: var(--ink-faint); font-size: 1rem; line-height: 1; }
/* The title is the point of this control, but a forty-character one would push the
   position count off the row. Clipped here; whole in the label and the tooltip.
   min-width:0 because a flex child will not shrink below its content without it. */
.detail-walk-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Data, so mono and tabular — the figure changes on every step and a proportional one
   would shift the two titles either side of it as it did. */
.detail-walk-position { color: var(--ink-faint); font-family: var(--mono); font-size: .72rem; font-variant-numeric: tabular-nums; }

/*
  CONDENSED: what the bar becomes once it is stuck under the masthead. The art gives back
  the size it was granted and the title steps down, which is what buys the room for every
  control to fold onto ONE line — all three destinations, the walk, and the close. Nothing
  is dropped: a control that disappears when you scroll is a control you have to scroll
  back to find, and the outbound links cost less room than the cover art they replace.
*/
.detail-sticky.condensed { gap: 8px 14px; padding-top: 8px; padding-bottom: 8px; }
.detail-sticky.condensed .detail-sticky-main { flex: 1 1 140px; gap: 12px; }
/* No longer 100%, so it joins the line above; a viewport too narrow to hold both still
   wraps it, which is the same graceful answer as before rather than an overflow. */
.detail-sticky.condensed .detail-sticky-sub { flex: 0 1 auto; margin-left: auto; }
/*
  The title is what gives when the line runs out of room, and it can give the most: it is
  the one thing here that is not a control, and the page under it is about this game and
  nothing else. Clipped rather than wrapped — a second line of title would take the whole
  bar with it, which is how the row used to break.
*/
.detail-sticky.condensed h2 {
  flex: 1 1 80px; font-size: 1.2rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.detail-sticky.condensed .artwork.thumb { width: 96px; }
/* The five controls now sharing this line all want the same pixels, and the two titles in
   the walk are the only things here that can give — they are the longest and the one part
   a tooltip can carry whole. */
.detail-sticky.condensed .detail-walk { gap: 4px 12px; }
.detail-sticky.condensed .detail-walk-step { max-width: 13ch; }
.detail-sticky.condensed .detail-header-actions { gap: 6px; }
.detail-sticky.condensed .detail-header-link { padding: 6px 10px; display: none; }
/*
  The outbound links ride the folded bar only at a width where all five controls
  measurably fit on one line. Measured, not guessed: at 1024 the folded bar is 62px — one
  row — and at 960 it wraps to 99px, at which point folding has stopped saving anything.

  Below that they are the right two to drop. Both destinations are also in the Identity
  block further down the page, while the walk, the launch hand-off and the close have no
  second home anywhere.
*/
@media (min-width: 1024px) {
  .detail-sticky.condensed .detail-header-link { display: inline-flex; }
}
/* Last on the merged row. Without the reorder the close button sits between the title and
   the walk, where a × reads as closing whatever it is beside. */
.detail-sticky.condensed .detail-page-close { order: 1; }

/* Now only the similar-games shelf's arrows (.shelf-arrow) — the detail header's own pair
   became .detail-walk-step above. The name is kept because the shelf wears it. */
.detail-nav-button {
  display: inline-grid; place-items: center; width: 36px; height: 36px; padding: 0;
  border: 1px solid var(--line); border-radius: 9px; color: var(--ink);
  background: var(--ground); font-size: 1.15rem; line-height: 1;
}
.detail-nav-button:hover:not(:disabled) { border-color: var(--accent-deep); background: var(--raised); }
.detail-nav-button:disabled { color: var(--ink-faint); opacity: .38; cursor: default; }
.detail-page-close { position: static; flex: none; }
/* 152px, up from 96. It is the detail banner's cover and nothing else wears .thumb. */
.artwork.thumb { width: 152px; border-radius: 8px; font-size: 1.1rem; }

/* Scrolls with everything else: the cover moved into the title bar, and pinning
   the headline facts as well cost a third of the page to say what the header
   already says. */
.detail-top { display: grid; gap: 20px; }
.detail-highlights { display: grid; grid-template-columns: minmax(150px, 1fr) minmax(260px, 2fr); gap: 10px; margin-bottom: 14px; }
.detail-highlights > :only-child { grid-column: 1 / -1; }
.last-played-highlight {
  display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: 3px 12px;
  margin: 0 0 14px; padding: 13px 15px; border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line));
  border-left: 4px solid var(--accent); border-radius: 10px;
  background: color-mix(in srgb, var(--accent) 9%, var(--raised));
}
.detail-highlights .last-played-highlight { margin: 0; }
.last-played-label {
  grid-row: 1 / span 3; align-self: center; color: var(--ink-faint);
  font-family: var(--mono); font-size: .655rem; font-weight: 400; letter-spacing: .02em; text-transform: uppercase;
}
.last-played-value { color: var(--ink); font-family: var(--mono); font-size: 1.1rem; font-weight: 500; letter-spacing: -.02em; line-height: 1.25; }
.last-played-date { color: var(--ink-dim); font-family: var(--mono); font-size: .72rem; font-variant-numeric: tabular-nums; }
.last-played-hours { color: var(--accent); font-family: var(--mono); font-size: .72rem; font-weight: 500; font-variant-numeric: tabular-nums; }
.last-played-highlight.never { border-left-color: var(--line); background: var(--raised); }
.last-played-highlight.never .last-played-value { color: var(--ink-dim); }
.release-year-highlight {
  display: grid; align-content: center; gap: 2px;
  padding: 13px 15px; border: 1px solid var(--line); border-left: 4px solid var(--accent-deep); border-radius: 10px;
  background: var(--raised);
}
.release-year-label { color: var(--ink-faint); font-family: var(--mono); font-size: .655rem; font-weight: 400; letter-spacing: .02em; text-transform: uppercase; }
.release-year-value { color: var(--ink); font: 500 1.28rem var(--mono); letter-spacing: -.03em; font-variant-numeric: tabular-nums; line-height: 1.15; }
.release-date-full { color: var(--ink-dim); font-family: var(--mono); font-size: .72rem; font-variant-numeric: tabular-nums; }
.detail-badges { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 10px; }
.detail-desc { margin: 0; color: var(--ink-dim); font-size: .9rem; line-height: 1.55; }
.detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 14px; }
.artwork-open { display: block; width: fit-content; padding: 0; border: 0; border-radius: 10px; background: transparent; text-align: inherit; }
.artwork-open:hover { box-shadow: 0 0 0 2px var(--accent); }
.artwork-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.artwork-open .artwork { pointer-events: none; }
/* "More like this", the last thing on the detail page — below Delete and Close.
   A rule and real space above it: what sits above is a form you were editing and
   the two buttons that finish with it, and a browse shelf running straight on from
   those reads as part of them.

   Four large covers a page, no captions, and no scrollbar — the arrows are the
   only way through. `overflow-x: hidden` is what removes the bar AND the drag,
   and it is deliberately not `overflow: visible`: the row must still be a scroll
   container, because scrollBy(), scroll-snap and the browser's own
   scroll-focused-element-into-view (which is how Tab still reaches tile five)
   all need one. Hidden stops the USER scrolling it; it does not stop the page. */
/* ⚠️ min-width: 0 is load-bearing. #detail-body is a GRID, and a grid item's
   default `min-width: auto` sizes to its content's intrinsic width — so a row of
   twelve column-flow tiles pushed this section to 2,148px inside a 1,120px track
   and scrolled the whole PAGE sideways instead of scrolling the shelf. Measured,
   not theorised: document.scrollWidth was 2,221 at a 1,280px viewport. */
.detail-shelf { min-width: 0; margin: 36px 0 0; padding-top: 24px; border-top: 1px solid var(--line); }
.detail-shelf .shelf-head { margin-bottom: 12px; }
/* Exactly four across: three 12px gaps come out of the width first. */
.detail-shelf .shelf-row {
  grid-auto-columns: calc((100% - 36px) / 4);
  overflow-x: hidden; padding-bottom: 0;
}
/* The cover IS the tile now — no padding to shrink it, and the frame moves onto
   the artwork so the hover edge traces the image rather than a box around it. */
.detail-shelf .shelf-item { padding: 0; border: 0; border-radius: 10px; background: none; }
.detail-shelf .shelf-item .artwork { border-radius: 10px; }
.detail-shelf .shelf-item:hover .artwork { border-color: var(--accent-deep); }
.detail-shelf .shelf-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* In-game screenshots: one featured shot with a thumbnail strip. Hotlinked from
   Steam's CDN — the bytes are never ours (architecture §7). */
.detail-gallery { margin: 0; display: grid; gap: 8px; }
/* Five across, filling the column edge to edge — the legacy proportions. A grid
   rather than fixed-width flex items: the thumbnails then divide whatever width the
   page has instead of leaving a ragged gap on the right at some viewport sizes. */
.screenshot-strip { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
.screenshot-thumb {
  padding: 0; border: 1px solid var(--line); border-radius: 8px;
  background: none; overflow: hidden; line-height: 0; cursor: pointer;
}
/* Sized to the legacy detail page: five thumbnails across the content column, big
   enough to tell two cockpit shots apart rather than being decorative dots. */
.screenshot-thumb img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
/* (featured-shot sizing lives with the other detail-screenshot rules below) */
.screenshot-thumb:hover { border-color: var(--accent-deep); }
.screenshot-thumb.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

.detail-actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-end; align-items: center; }

/* Store-sourced facts, visually separated from the user's own fields. */
.panel-head { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: space-between; margin-bottom: 10px; }
/* The in-game shot on the detail dialog — same height discipline as the pick card's. */
/* The featured shot. `max-height: 300px` lived here from when this was one small
   image below the facts; with a gallery it cropped a 16:9 frame to a letterbox
   strip. The aspect ratio governs now — the margin moved to .detail-gallery so the
   thumbnail strip sits tight beneath the image it belongs to. */
.detail-screenshot {
  width: 100%; aspect-ratio: 16 / 9; object-fit: cover;
  border-radius: 8px; border: 1px solid var(--line-soft); display: block; margin: 0;
}
.detail-gallery { margin: 0 0 14px; }
.about-text { margin: 0 0 14px; color: var(--ink-dim); font-size: .92rem; line-height: 1.6; white-space: pre-wrap; }
/*
  The store's long description (app.js aboutSection). Plain text blocks, clamped to a
  few paragraphs with a fade so a thousand-word store page cannot push the facts and
  your notes out of reach; "Show more" lifts the clamp. pre-line keeps the single
  line breaks a paragraph carries without honouring source indentation.
*/
/* Match on IGDB (a user's own unmatched game). */
.detail-igdb-link { display: grid; gap: 8px; margin: 0 0 18px; }
.igdb-link-form { display: flex; gap: 8px; flex-wrap: wrap; }
.igdb-link-query { flex: 1 1 220px; min-width: 0; }
.igdb-candidates { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.igdb-candidate { display: grid; grid-template-columns: 44px minmax(0, 1fr) auto; gap: 10px; align-items: center; }
.igdb-candidate-cover { width: 44px; height: 59px; object-fit: cover; border-radius: 4px; background: var(--surface); }
.igdb-candidate-text { display: grid; min-width: 0; }
.igdb-candidate-text strong { overflow-wrap: anywhere; }
.igdb-candidate-pick { min-height: 32px; }
.detail-about { display: grid; gap: 10px; margin: 0 0 18px; }
.detail-about[hidden] { display: none; }
.detail-about-body {
  max-height: 16rem; overflow: hidden;
  color: var(--ink-dim); font-size: .92rem; line-height: 1.6;
  /* A mask, not a coloured overlay: it fades to whatever surface the page sits on. */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 3.5rem), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 3.5rem), transparent);
}
.detail-about-body.expanded { max-height: none; -webkit-mask-image: none; mask-image: none; }
.detail-about-body p { margin: 0 0 10px; white-space: pre-line; overflow-wrap: anywhere; }
.detail-about-body h4 { margin: 14px 0 6px; color: var(--ink); font-size: .95rem; font-weight: 700; }
.detail-about-body h4:first-child { margin-top: 0; }
.detail-about-body ul { margin: 0 0 10px; padding-left: 1.2rem; }
.detail-about-body li { margin: 0 0 4px; overflow-wrap: anywhere; }
.detail-about-toggle { justify-self: start; }
.detail-about-toggle[hidden] { display: none; }
/* Still the Taste profile's fact grid (views/profile.js); the game detail's own
   facts moved to the grouped blocks below. */
.steam-facts { gap: 10px 18px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
/*
  The grouped fact blocks. What was one flat auto-fit grid is now four labelled
  groups, each its own grid — so a long cell (the tag chips) can no longer set the
  height of a row of unrelated facts, and each group can take its own shape.

  A rule between the blocks rather than around them: the groups are one continuous
  read, and four boxed cards on a page that already has a gallery and a badge row
  would be a fifth kind of container.
*/
.detail-facts { display: grid; gap: 16px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.fact-block { display: grid; gap: 11px; }
/* No border-top here any more: the heading's own hairline (.fact-block-label::after)
   is the divider now, and running both drew two rules a few pixels apart. Space
   still separates the groups — more of it, since the rule is no longer doing it. */
.fact-block + .fact-block { padding-top: 20px; }
/*
  LEVEL 1, and the fix for "the headers are hard to tell from the data".

  It is no longer a line of small caps that happens to sit above the rows — it is
  a titled RULE: a sage bar, the name, then a hairline running to the right edge.
  Three things now separate it from a field label, any one of which would do on
  its own: it is sans where labels are mono, it is sage where labels are faint,
  and it owns a horizontal edge that nothing below it has.

  The bar and the rule are ::before/::after rather than markup, so app.js still
  emits a plain <h3> and this stays a stylesheet decision.
*/
.fact-block-label {
  display: flex; align-items: center; gap: 10px;
  margin: 0; color: var(--accent);
  font-family: var(--sans);
  font-size: .74rem; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
}
.fact-block-label::before {
  content: ""; flex: none; width: 3px; height: .95em; background: var(--accent);
}
.fact-block-label::after {
  content: ""; flex: 1; height: 1px; background: var(--line-soft);
}
.detail-facts .detail-grid { gap: 12px 18px; }
/* A chip row is a whole row of its own: chips wrap, and a wrapping cell in a
   190px column would stack them one per line. */
.fact-chip-row { grid-column: 1 / -1; }
.fact-chips { gap: 6px; }
/*
  TAGS, direction B. Only lanes and mechanics are filterable (see chipRow), and
  the old design gave the other four rows the same 999px pill — so fifteen Steam
  tags arrived looking like fifteen controls, none of which did anything, and
  outweighed every real fact in the block.

  Now the shape itself carries the distinction, not just the ink:

    filterable → a SQUARE token with a sage left edge. Square corners are the
                 tell; every quiet pill elsewhere in the app is round, and the
                 lit edge is what you can see from across a scrolling page.
    everything else → was prose (.fact-run), until every "What it is" row
                 gained a Library filter (2026-09-29). A row with no filter
                 still gets no chip: add its filter first.

  Which means .static is now only the DEGRADED case: an item inside a filterable
  row whose id is missing, so it cannot be a button. It keeps the token's shape
  to hold the row together and loses the edge, because it leads nowhere.
*/
.chip.fact-chip {
  border-radius: 3px; border-color: var(--line); border-left: 2px solid var(--accent);
  padding: 4px 11px 4px 10px; background: var(--raised-2); color: var(--ink);
  font-family: var(--mono); font-size: .74rem; font-weight: 500;
}
button.chip.fact-chip:hover { color: var(--ink); border-color: var(--accent-deep); border-left-color: var(--accent); background: #1b2620; }
.chip.fact-chip.static {
  color: var(--ink-faint); background: transparent;
  border-color: var(--line-soft); border-left-color: var(--line-soft);
}
.chip.fact-chip.static:hover { color: var(--ink-faint); border-color: var(--line-soft); border-left-color: var(--line-soft); }
/* The resolved rating is the OUTPUT of the rows above it, not another input. */
/* The ladder's output: the one figure in the block that is bigger than its inputs. */
.fact-resolved { grid-column: 1 / -1; padding-top: 10px; border-top: 1px solid var(--line-soft); }
.fact-resolved .stat-value { color: var(--accent); font-size: 1.3rem; font-weight: 500; letter-spacing: -.03em; }
/* Source credit / qualifier, beside a value rather than parenthesised inside it.
   It inherits mono from .stat-value, so it drops to the lighter weight instead. */
.stat-source { margin-left: 8px; color: var(--ink-faint); font-size: .7rem; font-weight: 400; letter-spacing: 0; }
/* A peer of .fact-block-label — the heading over the user's own fields — so it
   wears the same titled rule. Its trailing note sits between the name and the
   rule, which reads as part of the heading rather than as another field. */
.section-divider {
  display: flex; align-items: center; gap: 10px;
  /* Now that it looks like a fact block's heading it needs a fact block's air —
     at the old 6px it read as one more row belonging to Identity above it. */
  margin: 22px 0 0; color: var(--accent);
  font-family: var(--sans);
  font-size: .74rem; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
}
.section-divider::before { content: ""; flex: none; width: 3px; height: .95em; background: var(--accent); }
.section-divider::after { content: ""; flex: 1; height: 1px; background: var(--line-soft); }
/* The detail dialog's editors — the legacy detail page's field styles. */
.edit-field { display: grid; gap: 6px; }
.edit-field.inline { align-content: start; }
/* A field label like any other: mono, so it cannot be confused with the heading above. */
.edit-label { color: var(--ink-faint); font-family: var(--mono); font-size: .655rem; font-weight: 400; letter-spacing: .02em; text-transform: uppercase; }
.notes {
  width: 100%; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
  background: var(--raised); color: var(--ink); font-size: .9rem; resize: vertical;
}
.notes:focus { outline: none; border-color: var(--accent-deep); }
.detail-autosave { margin-left: 12px; color: var(--ink-faint); font-weight: 500; text-transform: none; letter-spacing: 0; }
.detail-saved { margin-left: 10px; color: var(--accent); font-weight: 700; text-transform: none; letter-spacing: 0; }

.fatal { padding: 40px 20px; text-align: center; color: var(--ink-dim); }
.fatal h2 { margin-bottom: 12px; color: var(--danger); }

footer {
  margin-top: 48px; padding-top: 18px; border-top: 1px solid var(--line);
  color: var(--ink-faint); font-size: .82rem; line-height: 1.6;
}
footer p { margin: 0 0 8px; max-width: 78ch; }
footer p:last-child { margin-bottom: 0; }
footer a { color: var(--accent-deep); }
footer a:hover { color: var(--accent); }

/* --------------------------------------------------------------- signed out */

.signed-out {
  max-width: 620px; margin: 48px auto; padding: 28px 30px; text-align: left;
  border: 1px solid var(--line); border-radius: 20px; background: var(--raised);
}
.signed-out h2 { font-size: clamp(2rem, 5vw, 3rem); line-height: 1; letter-spacing: -.02em; margin-bottom: 12px; }
.signed-out .lede { margin-bottom: 20px; }
.signed-out .button-row { margin-top: 8px; }
.signed-out .privacy-note { margin: 18px 0 0; }

/* Server round-trips take visible time on a cold Function; say so. */
.picker-loading { display: grid; gap: 6px; padding: 30px 12px; color: var(--ink-dim); text-align: center; }
.picker-loading .hint { margin: 0; color: var(--ink-faint); font-size: .82rem; }
@keyframes pulse { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
.picker-loading strong { animation: pulse 1.6s ease-in-out infinite; font: 600 1.05rem var(--sans); letter-spacing: -.015em; }

/* -------------------------------------------------------------- responsive */

@media (max-width: 860px) {
  .artwork.large { width: 140px; }
  .artwork.hero { max-width: 100%; }
  /* Still the biggest cover the row can hold at this width, and still a step down to
     condense to — 64px was sized for the old one-row header and is now a stamp. */
  .artwork.thumb { width: 112px; }
  .detail-sticky.condensed .artwork.thumb { width: 72px; }
  .library-intro { align-items: flex-start; flex-direction: column; }
  /* Narrow enough that the segment groups need the full width each; the search
     box takes its own line rather than being squeezed against one of them. */
  .library-primary-controls .search-box { flex: 1 1 100%; max-width: none; }
  .library-segments { width: max-content; max-width: 100%; }
  /*
    The actions drop under the row rather than squeezing the title column, and the
    hours and the status STACK in the third column — hours over status, a small stat
    block. They used to be given the same cell (column 3, row 1, the row inherited
    from the desktop rule), which drew the status pill straight over the hours:
    "214h played" was invisible on every tablet. Each now names its own row, and the
    text and cover span both, so the stack sits beside them rather than under them.
  */
  .pick-row { grid-template-columns: auto minmax(0, 1fr) auto; }
  .pick-row .artwork { width: 170px; }
  .pick-row > .artwork-open { grid-row: 1 / span 2; }
  .pick-row > .lib-open { grid-column: 2; grid-row: 1 / span 2; }
  .pick-row > .lib-playtime { grid-column: 3; grid-row: 1; align-self: end; justify-self: end; }
  .pick-row > .status-pill { grid-column: 3; grid-row: 2; align-self: start; justify-self: end; }
  .pick-row-actions { grid-column: 1 / -1; grid-row: 3; justify-content: flex-start; }
}

/*
  The first question's segmented control folds into a two-column grid wherever its
  one-line form no longer fits. It used to fold only at 620px, but the line needs
  about 754px, so from 621px up to that the control kept its full width, the page
  scrolled sideways, and "A longer game to invest in" sat off the right edge.
  780px leaves headroom for the longest label. [tunable — tied to the chip labels]
*/
@media (max-width: 780px) {
  .questions > .question:first-child .chips {
    width: 100%; display: grid; grid-template-columns: 1fr 1fr; border-radius: 14px; overflow: hidden;
  }
  .questions > .question:first-child .chip { justify-content: center; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); white-space: normal; }
  .questions > .question:first-child .chip:nth-child(2n) { border-right: 0; }
  .questions > .question:first-child .chip:last-child { grid-column: 1 / -1; border: 0; }
}

@media (max-width: 620px) {
  /*
    The pick card STACKS on a phone: cover, then the words at full width, then hours
    and status side by side, then the actions. It used to keep the tablet's three
    columns down to 320px — a 170px cover took half the card, the reason wrapped at two
    or three words a line and ran under the status pill, and a long title pushed text
    past the card's own edge. The cover's button needs its own width: a <button>
    shrinks to its content, so the image's width: 100% would otherwise resolve against
    nothing and the cover would collapse to a sliver.
  */
  .pick-row { grid-template-columns: minmax(0, 1fr) auto; }
  .pick-row > .artwork-open { grid-column: 1 / -1; grid-row: 1; width: 100%; }
  .pick-row .artwork { width: 100%; }
  .pick-row > .lib-open { grid-column: 1 / -1; grid-row: 2; }
  .pick-row > .lib-playtime { grid-column: 1; grid-row: 3; align-self: center; justify-self: start; }
  .pick-row > .status-pill { grid-column: 2; grid-row: 3; align-self: center; }
  .pick-row-actions { grid-column: 1 / -1; grid-row: 4; }
  .detail-highlights { grid-template-columns: 1fr; }
  main { width: calc(100% - 22px); padding: 22px 0 48px; }
  .picker, .questions, .pick-list, .pick-row { min-width: 0; max-width: 100%; }
  .masthead { align-items: flex-start; }
  /*
    On a phone only the masthead pins (owner requests 2026-10-09): stuck together, the
    masthead and the library's or picker's controls took most of the screen, leaving a
    sliver of list, so the controls scroll with the page and lose their stuck-state shadow.
    The masthead stays sticky, with tighter padding so the stuck bar costs as little height
    as it can. app.js measures it into --masthead-height as at every other width, so the
    banners and the detail bar pin under it.
  */
  .library-controls, .picker-actions { position: static; box-shadow: none; }
  /*
    The sections fold into a menu button at the header's right (owner request 2026-10-09):
    as a row they wrapped to three lines and pushed the page down. The wordmark keeps the
    left, the avatar and the button the right. #view-nav is the same tablist, shown as a
    dropdown while the button says expanded (app.js toggles .nav-open on the masthead).
  */
  /* z-index 30, above the 10 it has at wider widths: its backdrop-filter makes it a stacking
     context, so the sections menu and the account menu inside it would otherwise paint UNDER
     later content, like the detail page's bar. */
  .masthead {
    z-index: 30; flex-wrap: nowrap; align-items: center;
    padding-top: 12px; padding-bottom: 12px; margin-bottom: 18px;
  }
  .masthead-text { min-width: 0; }
  /* The tagline wraps to two lines at phone width, a third of the stuck bar's height. */
  .masthead .eyebrow { display: none; }
  .masthead-logo { width: 44px; height: 44px; }
  .masthead-actions { flex: none; gap: 8px; }
  .account { margin-right: 0; }
  .nav-toggle:not([hidden]) {
    display: grid; place-items: center; width: 44px; height: 44px;
    border: 1px solid var(--line); border-radius: 12px;
    color: var(--ink); background: var(--raised); cursor: pointer;
  }
  .nav-toggle[aria-expanded="true"] { border-color: var(--accent); }
  .view-nav { display: none; }
  .masthead.nav-open .view-nav {
    position: absolute; right: 0; top: calc(100% + 10px); z-index: 20;
    display: flex; flex-direction: column; flex-wrap: nowrap; gap: 2px;
    width: min(16rem, calc(100vw - 22px)); padding: 8px;
    border: 1px solid var(--line); border-radius: 12px;
    background: var(--raised); box-shadow: 0 18px 36px #0009;
  }
  .masthead.nav-open .nav-tab { width: 100%; padding: 12px 14px; text-align: left; font-size: .95rem; }
  .picker-intro { flex-direction: column; align-items: flex-start; }
  .picker-intro-copy { min-width: 0; width: 100%; }
  .picker-qualifying { display: grid; padding: 0; text-align: left; }
  .picker-intro h2 { max-width: 8ch; font-size: clamp(2.35rem, 13vw, 3.2rem); }
  .section-title { flex-direction: column; align-items: flex-start; gap: 5px; }
  .pick-rank { display: none; }
  .artwork.large { width: 100%; }
  .library-summary { width: 100%; gap: 18px; overflow-x: auto; }
  .library-summary-stat { min-width: 76px; }
  .library-segments { overflow-x: auto; }
  .library-segment { padding: 9px 12px; }
  /* The four sort labels need ~430px on one line, so the row scrolled sideways and hid
     "Alphabetical". Here they share the row instead: a two-word label wraps onto two lines
     ("Recently / touched") and no button shrinks below its longest word. */
  .sort-segments { display: flex; width: 100%; overflow-x: visible; }
  .sort-segments .library-segment { flex: 1 1 auto; padding: 8px; white-space: normal; line-height: 1.25; text-align: center; }
  .lib-row { padding: 10px; }
  .lib-row .artwork { width: 120px; }
  /*
    A library card STACKS on a phone (owner request 2026-10-09): title, then the cover at
    the card's full width, then the chips, facts and hours at full width. Beside a 120px
    cover the words had what was left of ~360px, and the facts ran out of the card.

    The cover is a sibling of the .lib-open button, not inside it (two buttons cannot nest),
    so it cannot simply be reordered between the title and the rest. Instead the button
    spans three rows as a SUBGRID, its title in the first and its body in the third, and
    the cover sits in the middle row, over the button, so it keeps its own click. Same
    markup, same accessibility tree as every other width.
  */
  .library-view .lib-row { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto auto; row-gap: 10px; contain-intrinsic-block-size: auto 400px; }
  .library-view .lib-open { grid-column: 1; grid-row: 1 / span 3; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: subgrid; }
  /* A single fact (a genre list, "min 30 minutes · Metacritic 85") can be wider than a
     phone; whole-fact nowrap then stretched the card past the screen. Let it wrap here. */
  .library-view .lib-facts > span { white-space: normal; }
  .library-view .lib-open > .lib-title { grid-row: 1; }
  .library-view .lib-open > .lib-body { grid-row: 3; }
  .library-view .lib-row > .artwork-open { grid-column: 1; grid-row: 2; width: 100%; position: relative; z-index: 1; }
  .library-view .lib-row .artwork { width: 100%; }
  .library-view .lib-row > .lib-restore { grid-column: 1; grid-row: 1; }
  .lib-playtime { font-size: .9rem; }
  /* No room beside a 120px cover for a 2.3rem figure AND a chip row: the hours
     drop under the facts and read left-to-right with everything else. */
  .library-view .lib-title { font-size: 1.3rem; }
  .lib-body { flex-direction: column; gap: 7px; }
  .lib-stat { flex-direction: row; align-items: baseline; gap: 7px; }
  .lib-hours { font-size: 1.5rem; }
  .lib-hours-unit { font-size: .85rem; }
  .lib-stat-label { max-width: none; text-align: left; }
  .health-row { grid-template-columns: 72px minmax(0, 1fr); }
  .health-row .artwork { width: 72px; }
  .game-preview { display: none; }
  .feedback-bar { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
  .feedback-spacer { display: none; }
  .feedback-btn { min-width: 0; text-align: center; }
  .feedback-btn.danger { grid-column: 1 / -1; }
  .filter-bar > * { flex: 1 1 100%; }
  .library-advanced { grid-template-columns: 1fr; }
  .sort-field { grid-column: auto; min-width: 0; padding: 10px 0 0; border-left: 0; border-top: 1px solid var(--line); }
  .sort-field .filter-select { flex: 1; }
  .chip { max-width: 100%; white-space: normal; }
  .dialog-body { padding: 20px 16px; }
  .detail-sticky { margin: -20px -16px 0; padding: 16px 16px 14px; }
  /* Two across, not four: a quarter of a phone's width is a 96px cover, which is
     a thumbnail of a thumbnail. The arrows still page by a full row. */
  .detail-shelf .shelf-row { grid-auto-columns: calc((100% - 12px) / 2); }
  .detail-sticky h2 { font-size: 1.25rem; }
  .lane-bar-row { grid-template-columns: 1fr; gap: 4px; }
  .lane-bar-value { justify-self: start; }
}

@media (prefers-reduced-motion: no-preference) {
  button, input, select, .chip, .nav-tab, .lib-row { transition: color .15s, background-color .15s, border-color .15s, box-shadow .15s; }
  .artwork img { transition: opacity .25s; }
  /* NOTHING here may animate .detail-sticky's height — not the cover's width, not the
     title's size. syncStickyCondense() measures the bar's settled height to decide how
     far past the pin to fold, and a height caught mid-animation is smaller than the real
     one, which is what let the fold and the scroll fight each other. */
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/*
  Play Order (play-order.js, views/library.js). A row's controls ride its top-right corner,
  the corner a tombstone's Restore uses (a removed game shows Restore, never these). Placed
  ABSOLUTELY, not in the row's grid: an explicitly placed grid item is placed first and would
  push the row's own button onto a second row. They sit over that button, so they are lifted
  to stay clickable, and the title keeps clear of them.
*/
.lib-list { position: relative; }
.library-view .lib-row > .po-controls {
  position: absolute; top: 12px; right: 16px; z-index: 2;
  display: flex; gap: 6px; align-items: center;
}
.library-view .lib-row:has(> .po-controls) .lib-title { padding-right: 132px; }
/* In the Play Order sort a chosen game's card gives its right edge to the drag handle, top to
   bottom; the card's content and its corner controls move in by the handle's width (22px). */
.library-view .lib-row:has(> .po-handle) { padding-right: 38px; }
.library-view .lib-row:has(> .po-handle) > .po-controls { right: 38px; }
.po-menu-button {
  min-height: 32px; padding: 5px 11px; border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink-dim); background: var(--surface); font-size: .78rem; font-weight: 700; white-space: nowrap;
}
.po-menu-button:hover { color: var(--ink); border-color: var(--accent-deep); }
.po-menu-button.on { border-color: var(--accent-deep); color: var(--accent); font-family: var(--mono); font-size: .9rem; }
.po-handle {
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 2;
  display: grid; place-items: center; width: 22px; padding: 0;
  border: 0; border-left: 1px solid var(--line); border-radius: 0 17px 17px 0; background: var(--surface);
  color: var(--ink-dim); font-size: 1rem; line-height: 1; cursor: grab;
  /* The handle owns the gesture: a touch that starts here drags, it never scrolls the page. */
  touch-action: none; user-select: none; -webkit-user-select: none;
}
.po-handle:hover, .po-handle:focus-visible { color: var(--ink); border-color: var(--accent-deep); background: var(--raised-2); }
.po-menu-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Inset, so the ring is not clipped by a card that hides its overflow. */
.po-handle:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
body.po-drag-active, body.po-drag-active * { cursor: grabbing !important; user-select: none; }
body.po-drag-active .game-preview { display: none; }
.lib-row.po-dragging { opacity: .55; outline: 2px dashed var(--accent-deep); outline-offset: -2px; }
.po-drop-indicator {
  position: absolute; left: 0; right: 0; height: 4px; z-index: 5; border-radius: 2px;
  background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); pointer-events: none;
}
.po-drop-indicator[hidden] { display: none; }
.po-section-head {
  margin: 18px 0 2px; padding-top: 14px; border-top: 1px solid var(--line);
  color: var(--ink-faint); font: 700 .72rem var(--sans); letter-spacing: .09em; text-transform: uppercase;
}
.po-empty {
  margin: 0; padding: 14px 16px; border: 1px dashed var(--line); border-radius: 14px;
  color: var(--ink-dim); background: var(--raised); line-height: 1.45;
}
.po-empty strong { color: var(--ink); }
.po-menu {
  position: fixed; z-index: 1100; display: grid; min-width: 210px; padding: 6px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--raised-2); box-shadow: var(--shadow-sm);
}
.po-menu-heading { margin: 0; padding: 6px 10px 8px; color: var(--ink-faint); font-size: .74rem; font-weight: 700; }
.po-menu-item {
  padding: 9px 10px; border: 0; border-radius: 8px; color: var(--ink); background: transparent;
  font-weight: 600; text-align: left;
}
.po-menu-item:hover:not([disabled]), .po-menu-item:focus-visible { background: var(--surface); outline: none; }
.po-menu-item[disabled] { color: var(--ink-faint); cursor: default; }
/* Saving state: quiet while it works, and it stays put (with its buttons) when it did not. */
.po-status {
  position: fixed; left: 16px; bottom: 16px; z-index: 1050; max-width: min(460px, calc(100% - 32px));
  display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center;
  padding: 9px 14px; border: 1px solid var(--line); border-radius: 12px;
  background: var(--raised-2); color: var(--ink-dim); box-shadow: var(--shadow-sm); font-size: .86rem; font-weight: 600;
}
.po-status[hidden] { display: none; }
body:has(.lib-footer) .po-status { bottom: calc(var(--lib-footer-height, 52px) + 12px); }
.po-status.failed, .po-status.conflict { border-color: var(--notice-line); background: var(--notice-bg); color: var(--notice-ink); }
.po-status-actions { display: flex; gap: 8px; }
.po-status-actions button { padding: 6px 12px; color: inherit; }
.detail-play-order .po-menu-button { justify-self: start; }
.po-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 10px; }
/*
  Bulk selection (2026-10-10): a checkbox over each row's top-left corner, big enough for a
  thumb, and a toolbar under the controls that counts and acts. The selected row is tinted so
  the selection reads at a glance when scrolling.
*/
.lib-select {
  position: absolute; top: 10px; left: 10px; z-index: 3; display: grid; place-items: center;
  width: 36px; height: 36px; border-radius: 9px; background: #000b; cursor: pointer;
}
.lib-select input { width: 20px; height: 20px; margin: 0; cursor: pointer; accent-color: var(--accent); }
.lib-row.lib-selected { box-shadow: inset 0 0 0 2px var(--accent); }
.lib-bulk {
  position: sticky; bottom: calc(var(--lib-footer-height, 52px) + 8px); z-index: 109;
  display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; margin: 12px 0 8px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--raised-2); box-shadow: var(--shadow-sm);
}
@media (min-width: 1000px) { .lib-bulk { width: calc(100% - 380px); } }
.lib-bulk-count { font-weight: 700; font-variant-numeric: tabular-nums; }
.lib-bulk-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.lib-bulk-progress, .lib-bulk-report {
  display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; flex-basis: 100%; margin: 0;
  color: var(--ink-dim); font-size: .84rem;
}
.lib-bulk-report.has-failed { color: var(--notice-ink); }
.lib-bulk-report button { padding: 5px 10px; }
/* The picker's scope and its empty Play Order */
.picker-empty-order-actions { margin-top: 10px; }
/* The taste profile, with its working (views/profile.js) */
.taste-lanes { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.taste-lane-summary {
  display: grid; grid-template-columns: auto minmax(110px, 170px) 1fr auto auto auto; gap: 10px; align-items: center;
  width: 100%; padding: 9px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface);
  color: var(--ink); text-align: left; cursor: pointer;
}
.taste-mechanic .taste-lane-summary, .taste-opted-out .taste-lane-summary { grid-template-columns: 1fr auto auto auto; }
.taste-lane-summary:hover, .taste-lane-summary:focus-visible { border-color: var(--accent); outline: none; }
.taste-lane-rank { color: var(--ink-faint); font: 600 .74rem var(--mono); }
.taste-lane-label { font-weight: 700; }
.taste-lane-recorded { color: var(--ink-dim); font-size: .8rem; white-space: nowrap; }
.taste-lane-influence { color: var(--accent); font: 700 .8rem var(--mono); white-space: nowrap; }
.taste-lane-caret { color: var(--ink-faint); }
.taste-lane-detail { padding: 10px 12px 6px; }
.taste-lane-explain { margin: 0 0 10px; color: var(--ink-dim); font-size: .86rem; }
.taste-games { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.taste-game { padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px; background: var(--raised); }
.taste-game-head { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: baseline; }
.taste-game-title { padding: 0; font-weight: 700; }
.taste-game-recorded, .taste-game-share { color: var(--ink-dim); font-size: .8rem; }
.taste-game-parts { margin: 4px 0 6px; color: var(--ink-faint); font-size: .78rem; }
.taste-game-toggle { font-size: .82rem; }
@media (max-width: 620px) {
  .taste-lane-summary { grid-template-columns: auto 1fr auto; }
  .taste-lane-summary .lane-bar-track { grid-column: 1 / -1; }
  .taste-lane-summary .taste-lane-recorded { grid-column: 1 / 3; }
  .taste-mechanic .taste-lane-summary, .taste-opted-out .taste-lane-summary { grid-template-columns: 1fr auto; }
  .taste-mechanic .taste-lane-summary .taste-lane-influence { grid-column: 1 / 2; }
  .lib-bulk { padding: 8px 10px; }
}
.po-sort-summary { color: var(--ink-dim); font-size: .84rem; font-weight: 600; }
/* "Start over" is the quiet one wherever it appears: a text button, never the primary action. */
.po-sort-restart {
  padding: 4px 8px; border: 0; background: transparent; color: var(--ink-faint); font-size: .8rem; text-decoration: underline;
}
.po-sort-restart:hover, .po-sort-restart:focus-visible { color: var(--ink); }

/*
  Sort by choosing (play-order-sort.js): a dialog over the whole page, two covers side by
  side, "or" between them. The dialog carries its own save line with the same answers the
  Play Order panel offers (Retry / Undo, Use that version / Keep this one), so the panel
  stays out of the way while it is open: two Retry buttons for one failure, one of them
  outside the focus trap, is worse than none.
*/
body.po-sorting { overflow: hidden; }
body.po-sorting .po-status { display: none; }
.po-sort {
  position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 16px;
  background: #0009; overflow-y: auto;
}
.po-sort-panel {
  width: min(760px, 100%); max-height: calc(100vh - 32px); overflow-y: auto; padding: 18px 20px 20px;
  border: 1px solid var(--line); border-radius: 16px; background: var(--raised-2); box-shadow: var(--shadow-sm);
}
.po-sort-head { display: flex; gap: 12px; align-items: flex-start; justify-content: space-between; margin-bottom: 14px; }
.po-sort-head h2 { margin: 0 0 4px; font-size: 1.3rem; }
.po-sort-hint { margin: 0; color: var(--ink-dim); font-size: .9rem; }
.po-sort-close { flex: none; }
.po-sort-progress { margin: 0 0 6px; color: var(--ink-dim); font-size: .84rem; font-weight: 600; }
.po-sort-bar { height: 4px; margin-bottom: 6px; border-radius: 2px; background: var(--raised); overflow: hidden; }
.po-sort-estimate { margin: 0 0 14px; color: var(--ink-faint); font-size: .76rem; }
/* The stopping point after a batch: the same quiet block as "done", with its two ways on. */
.po-sort-stop { display: grid; gap: 10px; justify-items: start; padding: 8px 0; }
.po-sort-stop h3 { margin: 0; }
.po-sort-stop p { margin: 0; color: var(--ink-dim); }
.po-sort-stop-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.po-sort-bar span { display: block; height: 100%; background: var(--accent); transition: width .2s; }
.po-sort-pair { display: grid; grid-template-columns: 1fr auto 1fr; gap: 12px; align-items: start; }
.po-sort-option { display: grid; gap: 8px; justify-items: center; }
.po-sort-card {
  display: grid; gap: 8px; width: 100%; padding: 10px; border: 1px solid var(--line); border-radius: 14px;
  background: var(--surface); color: var(--ink); text-align: center; cursor: pointer;
}
.po-sort-card:hover, .po-sort-card:focus-visible { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); outline: none; }
.po-sort-cover {
  position: relative; display: grid; place-items: center; width: 100%; max-width: 240px; margin: 0 auto;
  aspect-ratio: 2 / 3; overflow: hidden; border: 1px solid var(--line); border-radius: 10px;
  background: var(--raised); color: var(--ink-faint); font-size: 2.4rem; font-weight: 700;
}
.po-sort-cover img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity .25s; }
.po-sort-cover img.loaded { opacity: 1; }
.po-sort-title { font-weight: 700; line-height: 1.25; }
.po-sort-meta { color: var(--ink-dim); font-size: .8rem; line-height: 1.35; }
.po-sort-remove {
  padding: 4px 8px; border: 0; background: transparent; color: var(--ink-faint); font-size: .76rem; text-decoration: underline;
}
.po-sort-remove:hover, .po-sort-remove:focus-visible { color: var(--ink); }
.po-sort-middle { display: grid; gap: 10px; justify-items: center; align-self: center; padding-top: 40%; }
.po-sort-or { color: var(--ink-faint); font: 700 .78rem var(--sans); letter-spacing: .08em; text-transform: uppercase; }
.po-sort-either { padding: 7px 12px; border: 1px solid var(--line); border-radius: 999px; background: var(--raised); color: var(--ink-dim); font-size: .82rem; font-weight: 600; }
.po-sort-either:hover, .po-sort-either:focus-visible { color: var(--ink); border-color: var(--accent-deep); }
.po-sort-done { display: grid; gap: 10px; justify-items: start; padding: 8px 0; }
.po-sort-done h3 { margin: 0; }
.po-sort-done p { margin: 0; color: var(--ink-dim); }
/* What a resume found changed, once, above the first question. */
.po-sort-note { margin: 0 0 10px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px; background: var(--raised); color: var(--ink-dim); font-size: .84rem; }
/*
  The save line: quiet while saving and saved, loud (the notice colours, with its answers)
  when the server does not have the last choice. The cards wait behind a failure.
*/
.po-sort-foot { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; justify-content: space-between; margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--line); }
.po-sort-save { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; flex: 1 1 240px; min-height: 28px; color: var(--ink-faint); font-size: .82rem; }
.po-sort-save.saving { color: var(--ink-dim); }
.po-sort-save.saved { color: var(--ink-dim); }
.po-sort-save.failed, .po-sort-save.conflict {
  padding: 8px 10px; border: 1px solid var(--notice-line); border-radius: 10px; background: var(--notice-bg); color: var(--notice-ink);
}
.po-sort-save-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.po-sort-save-actions button { padding: 6px 12px; color: inherit; }
.po-sort-blocked .po-sort-card, .po-sort-blocked .po-sort-either, .po-sort-blocked .po-sort-remove { opacity: .55; cursor: not-allowed; }
@media (max-width: 620px) {
  .po-sort { padding: 8px; }
  .po-sort-panel { padding: 14px 12px 16px; max-height: calc(100vh - 16px); }
  .po-sort-head h2 { font-size: 1.1rem; }
  .po-sort-pair { gap: 6px; }
  .po-sort-card { padding: 6px; }
  .po-sort-middle { padding-top: 30%; }
  .po-sort-cover { font-size: 1.8rem; }
  .po-sort-remove { font-size: .72rem; }
}

@media (max-width: 620px) {
  .library-view .lib-row > .po-controls { top: 10px; right: 10px; }
  /* The title line is the controls' row here: tall enough for them, so they never hang over the cover. */
  .library-view .lib-row:has(> .po-controls) .lib-open > .lib-title { min-height: 44px; }
  .library-view .lib-row:has(> .po-controls) .lib-title { padding-right: 112px; }
  .library-view .lib-row:has(> .po-handle) { padding-right: 32px; }
  .library-view .lib-row:has(> .po-handle) > .po-controls { right: 32px; }
  .po-status { left: 16px; right: 16px; max-width: none; }
}
/* Beside the hover rail the row has no top padding (the cover runs flush), so the controls step down. */
@media (min-width: 1000px) {
  .library-view .lib-row > .po-controls { top: 10px; right: 12px; }
  .library-view .lib-row:has(> .po-handle) { padding-right: 34px; }
  .library-view .lib-row:has(> .po-handle) > .po-controls { right: 34px; }
  .po-handle { border-radius: 0; }   /* the cards are square-cornered beside the rail */
}
