/* dimagine viewer stylesheet (app.css)
 *
 * Mobile-first, one layout at three widths (spec §3, §4):
 *   < 768px   phone:   bottom tab bar, 3-column grid
 *   768-1199  tablet:  icon rail, 4-6 columns
 *   >= 1200px desktop: pinned sidebar + inspector, 5-7 columns
 *
 * The images are the content; the chrome stays on one line per region at
 * every width, and the tiles never crop a picture (object-fit: contain).
 */

:root {
  color-scheme: light dark;

  /* Neutral greys with a slight cool bias, one accent for selection. */
  --bg: #f7f8fa;
  --surface: #ffffff;
  --surface-alt: #eef1f6;
  --border: #dde3ec;
  --text: #10151f;
  --text-muted: #5d6b80;
  --accent: #2f6fd0;
  --accent-text: #ffffff;
  --accent-soft: rgba(47, 111, 208, 0.1);
  --tile-bg: #e9edf3;
  --danger: #b4341f;
  --danger-soft: rgba(180, 52, 31, 0.08);

  /* System stack: no web font, so the viewer works offline. */
  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* The type scale is four sizes (spec §4). */
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.25rem;

  --radius: 8px;
  --radius-sm: 5px;
  --gap: 10px;
  --pad: 12px;

  --sidebar-w: 240px;
  --rail-w: 60px;
  --inspector-w: 320px;
  --tabbar-h: 58px;
  /* W45: the top bar's height, the offset the image page's pinned inspector
     sticks below it. The script measures the real thing (the bar wraps at
     520px); this default keeps a script-less page close. */
  --topbar-h: 56px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0e1116;
    --surface: #161b22;
    --surface-alt: #1d232c;
    --border: #2a323d;
    --text: #e6edf3;
    --text-muted: #9aa7b6;
    --accent: #6ea8ff;
    --accent-text: #0b1220;
    --accent-soft: rgba(110, 168, 255, 0.14);
    --tile-bg: #1b212a;
    --danger: #ff8a72;
    --danger-soft: rgba(255, 138, 114, 0.12);
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--text-base);
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
}

h1,
h2,
h3 {
  margin: 0;
  font-weight: 600;
  line-height: 1.3;
}

h1 {
  font-size: var(--text-lg);
}

h2 {
  font-size: var(--text-sm);
}

p {
  margin: 0 0 0.5em;
}

p:last-child {
  margin-bottom: 0;
}

a {
  color: var(--accent);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

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

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------------------------------------------------------- shell */

.app-shell {
  min-height: 100vh;
  /* The grid scrolls under the tab bar, so the bar has its own space. */
  padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
}

/* The no-login banner (--auth none): a full-width bar above the whole
   shell, never a column beside the rail or sidebar, so no page can be
   mistaken for a login-protected one at any width. */
.banner {
  margin: 0;
  padding: calc(var(--pad) / 2) var(--pad);
  background: var(--surface-alt);
  border-bottom: 1px solid var(--border);
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: 600;
  text-align: center;
  overflow-wrap: anywhere;
}

/* The sign-in and setup pages (spec §4b): one centred card, the
   product's own type and themes, no chrome around it. */
.auth-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 16px;
}

.auth-card {
  width: 100%;
  max-width: 380px;
  padding: calc(var(--pad) * 1.5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: var(--text-base);
}

.auth-card h2 {
  font-size: var(--text-lg);
  margin-bottom: var(--pad);
}

.auth-card label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
  margin-bottom: var(--pad);
}

.auth-card input[type="email"],
.auth-card input[type="password"],
.auth-card input[type="text"] {
  display: block;
  width: 100%;
  min-height: 44px;
  margin-top: 4px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  background: var(--bg);
}

.auth-card button {
  display: block;
  width: 100%;
  min-height: 44px;
  margin-top: var(--pad);
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--accent-text);
  font: inherit;
  font-weight: 600;
}

.auth-card label:has(input[type="checkbox"]) {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 500;
}

.auth-card p {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* The column between the side columns takes the room that is left, so a page
   with no sidebar (the image page) still fills the width. */
.main-column {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 100vh;
  min-width: 0;
}

/* ------------------------------------------------------------- top bar */

.top-bar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px var(--pad);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.top-bar-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.search-pill-container {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  min-width: 0;
  min-height: 38px;
  padding: 0 12px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

a.search-pill-container:hover {
  border-color: var(--accent);
  color: var(--accent);
  text-decoration: none;
}

/* W43: the magnifier in front of a capsule is now an SVG the page draws
   (`components::SEARCH_GLYPH`), so the label carries no glyph of its own. */

.search-pill-container input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 38px;
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--text);
  font: inherit;
  font-size: var(--text-sm);
}

.search-pill-container input:focus {
  outline: none;
}

.search-submit {
  flex: 0 0 auto;
  min-height: 38px;
  padding: 0 14px;
  background: var(--accent);
  border: 0;
  border-radius: 999px;
  color: var(--accent-text);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}

.toolbar-controls {
  display: flex;
  flex: 0 1 auto;
  align-items: center;
  gap: 8px;
  min-width: 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
  white-space: nowrap;
}

/* Below this width the field and the controls cannot share a line without one
   of them overflowing, so the search field takes its own line and the controls
   follow it. Both stay one line each, which is what the design asks for. */
@media (max-width: 520px) {
  .top-bar-row {
    flex-wrap: wrap;
  }

  .search-pill-container {
    flex: 1 1 100%;
  }

  .toolbar-controls {
    flex: 1 1 100%;
    justify-content: space-between;
  }
}

.result-count {
  font-variant-numeric: tabular-nums;
}

.sort-form {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin: 0;
}

.sort-select {
  min-height: 34px;
  max-width: 11rem;
  padding: 0 4px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: var(--text-sm);
}

/* Without the script the select needs a way to submit itself. */
.sort-submit {
  min-height: 34px;
  padding: 0 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}

.js .sort-submit {
  display: none;
}

.size-toggles {
  display: inline-flex;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.size-toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  min-height: 34px;
  padding: 0 6px;
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--text-xs);
  text-transform: uppercase;
}

.size-toggle-btn + .size-toggle-btn {
  border-left: 1px solid var(--border);
}

.size-toggle-btn:hover {
  color: var(--accent);
  text-decoration: none;
}

.size-toggle-btn.active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}

/* -------------------------------------------------------- scope chips */

/* W43: the chips live inside the search capsule now; their styling is in the
   `=== W43: chips ===` block at the end of this file. */

/* --------------------------------------------------------- breadcrumbs */

.breadcrumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  padding: 6px var(--pad);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.crumb-sep {
  color: var(--text-muted);
}

/* -------------------------------------------------------------- notices */

.notice-box {
  margin: var(--pad) var(--pad) 0;
  padding: 8px 12px;
  background: var(--surface-alt);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
}

.notice-box ul {
  margin: 0;
  padding-left: 1.2em;
}

.collection-diagnostics {
  margin: 0 var(--pad);
  padding: 8px 12px;
  background: var(--danger-soft);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
}

.error {
  color: var(--danger);
}

/* ===================== W46 · the collection page =========================
 * K27 Motion 6 owns the shape and DESIGN.md the tokens: the note's header
 * above its members — the title the tab already names, the note's own text,
 * the items and the way to the note's source — and the members in the
 * note's order, each with the caption line its embed gave it (FORMAT §5).
 * The header shares the view-heading section (`.search-section`), and the
 * note's text borrows the rendered-note component (`.note-body`, §4.6), so
 * every note this viewer renders has one typography.
 * ======================================================================= */

.collection-header h1 {
  margin-bottom: 4px;  /* --space-2: the meta line reads as its byline */
}

/* The note's prose, marked up like any rendered note but with a measure:
 * a caption-heavy header would still be a wall of text at 1440 px. */
.collection-note {
  max-width: 72ch;
  margin-bottom: 8px;  /* --space-4 */
}

.collection-meta {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.collection-meta .open-note {
  font-size: var(--text-sm);
}

/* The "Note order" the toolbar states where the library's sort menu would
 * be: a focusable label in the hint voice (W39 review L1), never a control
 * that could promise a reorder the grid does not keep. The whole sentence
 * is an attribute on the label itself. */
.search-hint.note-order {
  white-space: nowrap;
}

/* --- the members, with the captions the note gives them ----------------- */

/* A grid cell holds the tile and its caption; the tile keeps the square
 * and every behaviour of the library grid (selection, inspector, aspect
 * crop), so nothing here touches it. */
.grid-item {
  min-width: 0;
}

/* The member's caption: the line directly after the embed (FORMAT §5),
 * under the picture in the note-voice captions use (§4.6 renders a note's
 * own captions the same way) — not the hover overlay the tile's name uses
 * (§4.5). Two lines, then the whole line stays one hover away in its
 * `title` (DESIGN.md §4.1: never truncate without a way to read it all). */
.member-caption {
  display: -webkit-box;
  margin-top: 4px;  /* --space-2 */
  overflow: hidden;
  overflow-wrap: anywhere;
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: 1.35;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

/* ---------------------------------------------------------------- grid */

/* ========================= W41 · grid and tiles =========================
 * DESIGN.md §3 (grid metrics) and §4.5 (image tile): the square tile, the
 * aspect crop and its badge, the caption, selection, the loading skeleton and
 * the broken state all live in this one block, so the styling batches that own
 * the rest of the sheet merge cleanly around it. The tablet and desktop rules
 * that only set the column counts stay where they are, further down.
 *
 * DESIGN.md token values are written as literals here for the same reason —
 * the token block at the top of this sheet belongs to the phases that move it.
 * ======================================================================= */

.grid {
  display: grid;
  gap: 12px;      /* --space-5: the M and L tile gap */
  padding: 16px;  /* --space-6: the page gutter */
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.grid.size-s {
  gap: 6px;       /* --space-3: the S tile gap */
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.grid.size-l {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* --- the tile ---------------------------------------------------------- */

.tile {
  position: relative;
  display: block;
  aspect-ratio: 1;
  overflow: hidden;
  background: var(--tile-bg);
  border: 2px solid transparent;
  border-radius: 10px;  /* --radius-md */
  color: var(--text);
  /* Motion explains, never decorates (§1.8); nothing moves without the script
     and nothing moves under reduced motion. */
  transition: transform 120ms cubic-bezier(0.23, 1, 0.32, 1);
}

/* The square tile shows the whole picture: the default is never a crop. */
.tile img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Only an extreme aspect ratio is cropped (DESIGN.md §5.4): a very tall
   picture keeps its subject with a top-aligned crop, a very wide one is
   centre-cropped. `data-fit` is written by the script from the loaded
   thumbnail's natural size, so a shape nobody measured stays `contain`. */
.tile[data-fit="tall"] img {
  object-fit: cover;
  object-position: top center;
}

.tile[data-fit="wide"] img {
  object-fit: cover;
  object-position: center;
}

.tile:hover {
  transform: scale(1.005);
  text-decoration: none;
}

/* Hover is not selection: it lightens the picture, it never spends the accent
   (§1.3 — one accent, one job). */
.tile:hover img,
.tile.selected img {
  filter: brightness(1.03);
}

.tile.selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}

/* The Tall / Wide badge. It is decoration — the caption still carries the
   name — so it is aria-hidden and explains itself with a tooltip. */
.tile-badge {
  position: absolute;
  top: 6px;
  right: 6px;
  display: flex;
  width: 20px;
  height: 20px;
  align-items: center;
  justify-content: center;
  border-radius: 6px;  /* --radius-sm */
  background: rgba(255, 255, 255, 0.72);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: var(--text-muted);
  pointer-events: none;
}

.tile-badge svg {
  width: 12px;
  height: 12px;
}

/* The badge scrim is dark-tinted in the dark theme so it holds over a
   picture. `html[data-theme]` is the style tile's spelling of the theme. */
@media (prefers-color-scheme: dark) {
  .tile-badge {
    background: rgba(20, 22, 26, 0.72);
  }
}

html[data-theme="dark"] .tile-badge {
  background: rgba(20, 22, 26, 0.72);
}

html[data-theme="light"] .tile-badge {
  background: rgba(255, 255, 255, 0.72);
}

.tile-caption {
  position: absolute;
  inset: auto 0 0;
  padding: 16px 6px 4px;
  /* Dark in both themes: the name has to read over any picture. */
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.72));
  color: #fff;
  font-size: var(--text-xs);
  opacity: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: opacity 120ms cubic-bezier(0.23, 1, 0.32, 1);
}

.tile:hover .tile-caption,
.tile.selected .tile-caption,
.tile:focus-within .tile-caption {
  opacity: 1;
}

/* A pointer that cannot hover never reveals the name, so there the name is
   always on (W34 audit #3). The phone width is named as well as the coarse
   pointer: the phone layout labels its tiles whatever the device reports. */
@media (hover: none), (pointer: coarse), (max-width: 767px) {
  .tile-caption {
    opacity: 1;
  }
}

/* --- loading and broken ------------------------------------------------ */

/* A tile whose picture has not arrived holds the grid's shape: the same square
   at the same radius, tinted one step up from the background (§4.8). The only
   motion is the sheen, and under reduced motion there is none. */
.tile.loading {
  background: var(--surface-alt);
}

/* The browser paints a loading image's alt text in the tile; the skeleton is
   what belongs there instead. */
.tile.loading img {
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  .tile.loading::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(
      100deg,
      transparent 34%,
      rgba(255, 255, 255, 0.7) 50%,
      transparent 66%
    );
    background-size: 220% 100%;
    animation: tile-sheen 1.7s linear infinite;
  }
}

@media (prefers-color-scheme: dark) {
  @media (prefers-reduced-motion: no-preference) {
    .tile.loading::after {
      background-image: linear-gradient(
        100deg,
        transparent 34%,
        rgba(255, 255, 255, 0.055) 50%,
        transparent 66%
      );
    }
  }
}

@keyframes tile-sheen {
  from {
    background-position: 170% 0;
  }

  to {
    background-position: -90% 0;
  }
}

/* A picture that never arrives is a state, not an empty tile: a small
   monochrome glyph, the file's name and "Unavailable", in tertiary ink. */
.tile.broken {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;  /* --space-2 */
  padding: 8px;
  background: var(--surface-alt);
  color: var(--text-muted);
  text-align: center;
}

.tile.broken .tile-caption {
  display: none;
}

.tile-broken {
  display: flex;
  min-width: 0;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.tile-broken svg {
  width: 22px;
  height: 22px;
}

.tile-broken-name {
  display: -webkit-box;
  overflow: hidden;
  color: var(--text-muted);
  font-size: var(--text-xs);
  overflow-wrap: anywhere;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.tile-broken-note {
  color: var(--text-muted);
  font-size: var(--text-xs);
}

/* Under reduced motion the tile still answers the pointer, it just does not
   move to do it. */
@media (prefers-reduced-motion: reduce) {
  .tile:hover {
    transform: none;
  }
}

/* ------------------------------------------------------- cards & lists */

.folder-grid,
.collection-grid {
  display: grid;
  gap: var(--gap);
  padding: var(--pad);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.folder-card,
.collection-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: center;
  justify-content: center;
  aspect-ratio: auto;
  min-height: 7rem;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  text-align: center;
}

.folder-card:hover,
.collection-card:hover {
  border-color: var(--accent);
}

.card-glyph {
  width: 24px;
  height: 24px;
  fill: var(--text-muted);
  stroke: none;
}

.card-cover {
  width: 100%;
  aspect-ratio: 1;
  object-fit: contain;
  background: var(--tile-bg);
  border-radius: var(--radius-sm);
}

.card-title {
  font-size: var(--text-sm);
  font-weight: 600;
  overflow-wrap: anywhere;
}

.card-count {
  color: var(--text-muted);
  font-size: var(--text-xs);
}

.folder-tree {
  padding: var(--pad) var(--pad) 0;
}

/* The folder tree indents by depth; the indent is set after the row's own
   padding so the two do not overwrite each other. */
.sidebar-row.folder-row {
  padding-left: calc(var(--depth, 0) * 14px + 8px);
}

.depth-0 {
  --depth: 0;
}

.depth-1 {
  --depth: 1;
}

.depth-2 {
  --depth: 2;
}

.depth-3 {
  --depth: 3;
}

.depth-4 {
  --depth: 4;
}

.depth-5 {
  --depth: 5;
}

.depth-6 {
  --depth: 6;
}

.tag-list {
  display: grid;
  gap: 2px;
}

.tag-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 34px;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--text-sm);
}

.tag-row:hover {
  background: var(--surface-alt);
  color: var(--text);
  text-decoration: none;
}

.tag-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-section {
  padding: var(--pad);
  border-bottom: 1px solid var(--border);
}

.search-section h2 {
  margin-bottom: 8px;
  color: var(--text-muted);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.search-form {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
}

.search-label {
  font-size: var(--text-sm);
}

.search-hint {
  color: var(--text-muted);
  font-size: var(--text-xs);
}

.recent-views-list {
  margin: 0;
  padding-left: 1.2em;
  font-size: var(--text-sm);
}

.empty-state {
  padding: 3rem var(--pad);
  color: var(--text-muted);
  text-align: center;
}

.empty-state h3 {
  margin-bottom: 6px;
  color: var(--text);
  font-size: var(--text-lg);
}

/* W43: the tile anatomy for these states — glyph, title, sentence, one
   action — is in the `=== W43: states ===` block at the end of this file. */

.pagination {
  display: flex;
  justify-content: center;
  padding: 0 var(--pad) var(--pad);
}

.load-more-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 24px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: var(--text-sm);
}

.load-more-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
  text-decoration: none;
}

/* ---------------------------------------------------------- image page */

/* ========================= W41 · image page ============================
 * DESIGN.md §4.6. The stage takes the picture's shape: an ordinary picture is
 * shown whole and centred, and a very tall one fills the column and scrolls
 * inside a capped well instead of painting a sliver in an oversized empty box
 * (W34 #2). `data-fit` is written by the page when the note records the
 * dimensions, and by the script from the loaded image otherwise; with neither,
 * the stage keeps the whole picture. The properties are a field list, never raw
 * JSON, and the note is rendered Markdown. The DESIGN.md token values are
 * literals here so this block merges cleanly beside the other batches.
 * ==================================================================== */

/* `width: 100%` matters: the page is a centred block, and without it the page
   shrinks to the width of its own content instead of filling the column. */
.image-page {
  display: flex;
  flex-direction: column;
  gap: var(--pad);
  width: 100%;
  max-width: 1400px;
  margin: 0 auto;
  padding: var(--pad);
}

/* W45: the header holds the way back, the walk through the view, and the
   original file — one line, in the order a person reads them. The arrows and
   the position keep their shape at the ends of a view (a stub has no ink and
   no link, §4.8's quiet applied to navigation). */
.image-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--separator);
  font-size: var(--text-sm);
}

.back-link {
  flex: 0 0 auto;
}

.image-nav {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-width: 0;
  margin: 0 auto;
}

a.image-nav-prev,
a.image-nav-next {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  color: var(--accent);
}

a.image-nav-prev:hover,
a.image-nav-next:hover {
  background: var(--accent-soft);
  text-decoration: none;
}

/* A view's end stays drawn, quietly: no ink, no link, no route. */
.image-nav-prev.nav-end,
.image-nav-next.nav-end {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  color: var(--border);
}

.nav-glyph {
  width: 16px;
  height: 16px;
  fill: none;
}

.image-nav-position {
  flex: 0 0 auto;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

.original-link {
  flex: 0 0 auto;
  margin-left: auto;
}

.image-detail-layout {
  display: flex;
  flex-direction: column;
  gap: var(--pad);
}

/* The well hugs its picture (`fit-content` + centred) so a small image is not
   a small island in a full-width box, and it caps its height so a tall one is
   readable by scrolling rather than by being shrunk to a sliver. */
.image-stage {
  display: flex;
  align-self: center;
  justify-content: center;
  align-items: flex-start;
  width: fit-content;
  max-width: 100%;
  max-height: 80vh;
  /* `margin: 0`, not `0 auto`: an auto cross-axis margin absorbs the free space
     and so cancels the `align-self: stretch` below. The centring comes from
     `align-self: center` above, which the stretch then overrides. */
  margin: 0;
  padding: 16px;  /* --space-6 */
  overflow: auto;
  background: var(--tile-bg);
  border-radius: 10px;  /* --radius-md */
  overscroll-behavior: contain;
}

.image-stage img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: calc(80vh - 32px);
  border-radius: 6px;  /* --radius-sm */
}

/* A very tall picture (aspect < 0.4, DESIGN.md §5.4) is read top to bottom:
   it takes the column's width and the well scrolls. A shape nobody measured
   keeps the rule above — the whole picture, never a guess. */
.image-stage[data-fit="tall"] {
  align-self: stretch;
  width: auto;
}

.image-stage[data-fit="tall"] img {
  width: 100%;
  max-height: none;
}

.image-panel {
  display: flex;
  flex-direction: column;
  gap: 16px;      /* --space-6 */
  padding: 16px;  /* --space-6 */
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;  /* --radius-lg */
}

.image-title {
  overflow-wrap: anywhere;
}

.image-path {
  flex: 1 1 auto;
  font-family: var(--mono);
  font-size: var(--text-xs);
  overflow-wrap: anywhere;
}

.copy-btn {
  flex: 0 0 auto;
  min-height: 30px;
  padding: 0 8px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: var(--text-xs);
  cursor: pointer;
}

.copy-btn:hover {
  border-color: var(--accent);
}

.inspector-path-row {
  display: flex;
  align-items: center;
  gap: 6px;
}

.image-section {
  display: flex;
  flex-direction: column;
  gap: 12px;      /* --space-5 between sections */
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.image-section h2 {
  color: var(--text-muted);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.image-section ul {
  margin: 0;
  padding-left: 1.2em;
  font-size: var(--text-sm);
}

.note-path,
.image-source {
  color: var(--text-muted);
  font-size: var(--text-xs);
  overflow-wrap: anywhere;
}

/* --- properties: a field list, never raw JSON (DESIGN.md §4.6, W34 #8) --- */

.property-list {
  display: grid;
  margin: 0;
}

.property {
  display: grid;
  grid-template-columns: minmax(64px, 32%) 1fr;
  gap: 12px;
  align-items: baseline;
  min-height: 24px;
  padding: 4px 0;
}

.property + .property {
  border-top: 1px solid var(--border);
}

.property dt {
  color: var(--text-muted);
  font-size: var(--text-xs);
  text-align: right;
}

.property dd {
  margin: 0;
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

/* Paths, ids and sizes are machine text: monospace, tabular. */
.property dd code {
  padding: 1px 4px;
  background: var(--surface-alt);
  border-radius: 6px;  /* --radius-sm */
  font-family: var(--mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

/* The whole front matter stays reachable, collapsed. */
.properties-raw {
  margin-top: 4px;
}

.properties-raw > summary {
  color: var(--text-muted);
  font-size: var(--text-xs);
  cursor: pointer;
}

.properties-raw > summary:hover {
  color: var(--text);
}

.properties {
  max-height: 320px;
  overflow: auto;
  margin: 8px 0 0;
  padding: 12px;
  background: var(--surface-alt);
  border-radius: 8px;
  color: var(--text-muted);
  font-family: var(--mono);
  font-size: var(--text-xs);
  white-space: pre-wrap;
}

/* --- the rendered note (DESIGN.md §4.6, W34 #9) ------------------------- */

.note-body {
  color: var(--text);
  font-size: var(--text-sm);
  line-height: 1.55;
  overflow-wrap: anywhere;
}

.note-body > :first-child {
  margin-top: 0;
}

.note-body > :last-child {
  margin-bottom: 0;
}

/* A wikilink is an accent link with a dotted rule that turns solid on hover. */
.note-body a {
  color: var(--accent);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}

.note-body a:hover {
  text-decoration-style: solid;
}

.note-body :is(h1, h2, h3, h4) {
  margin: 12px 0 6px;
  font-size: var(--text-sm);
  font-weight: 600;
}

.note-body ul,
.note-body ol {
  margin: 6px 0;
  padding-left: 1.2em;
}

.note-body blockquote {
  margin: 8px 0;
  padding-left: 12px;
  border-left: 2px solid var(--border);
  color: var(--text-muted);
}

.note-body code {
  padding: 1px 4px;
  background: var(--surface-alt);
  border-radius: 6px;  /* --radius-sm */
  font-family: var(--mono);
  font-size: var(--text-xs);
}

.note-body pre {
  padding: 12px;
  overflow: auto;
  background: var(--surface-alt);
  border-radius: 8px;
}

.note-body pre code {
  padding: 0;
  background: none;
}

.note-body table {
  border-collapse: collapse;
  font-size: var(--text-xs);
}

.note-body th,
.note-body td {
  padding: 4px 8px;
  border-bottom: 1px solid var(--border);
  text-align: left;
}

/* An `![[embed]]` is a thumbnail with the caption beneath it. The renderer
   writes `<a><img alt="name"></a>`; the script lifts the alt out as the
   caption, and the link is the block that holds the two together. */
.note-body a:has(> img) {
  display: block;
  width: fit-content;
  max-width: 100%;
  margin: 8px 0;
  text-decoration: none;
}

.note-body img {
  display: block;
  max-width: 100%;
  height: auto;
  background: var(--tile-bg);
  border-radius: 6px;  /* --radius-sm */
}

.note-embed-caption {
  display: block;
  margin-top: 4px;
  color: var(--text-muted);
  font-size: var(--text-xs);
}

/* A target that resolved to nothing or to several: muted, and not a link. */
.missing-link {
  color: var(--text-muted);
  border-bottom: 1px dotted currentColor;
}

/* === W45: the pull-up sheet and the pinned inspector ===================== */

/* The tokens this block needs, by their DESIGN.md names (values verbatim):
   the sheet's duration and easing (§2.6), its elevation (§2.5) and the pill
   radius of its grip (§2.4). Motion and elevation are theme-independent. */
:root {
  --dur-slow: 260ms;
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --elev-2: 0 12px 40px rgba(0, 0, 0, 0.22);
  --radius-full: 999px;
}

/* "Appears in" states its empty case in one quiet line (§4.8's plain speech):
   no collection is a fact a person reads, not a section left off. */
.appears-in-empty {
  color: var(--text-muted);
  font-size: var(--text-xs);
}

/* The pull-up sheet's handle. Nothing without the script: the base keeps it
   out of the page, and only a phone layout under `html.js` draws it — on
   every other width the panel below the picture needs no handle at all. The
   peek it leaves is 44px, the same 44 the script drags from. */
.sheet-handle {
  display: none;
}

.sheet-grip {
  width: 36px;
  height: 4px;
  border-radius: var(--radius-full);
  background: var(--text-muted);
}

.sheet-label {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Below the tablet breakpoint (a hair under 768: a fractional viewport
   belongs to neither side of the breakpoint, so neither rule wins it), the
   picture spans the width and the panel pulls up over it. Without the script
   nothing here applies: the panel stays the plain section below the picture
   that the markup has always been. */
@media (max-width: 767.98px) {
  .image-stage {
    width: 100%;
    max-height: calc(100dvh - var(--topbar-h) - 132px);
  }

  html.js .image-page {
    padding-bottom: 44px;
  }

  html.js .image-panel {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
    z-index: 25;
    max-height: 78vh;
    max-height: 78dvh;
    overflow-y: auto;
    border-right: 0;
    border-bottom: 0;
    border-left: 0;
    border-radius: 14px 14px 0 0;
    box-shadow: var(--elev-2);
    transform: translateY(var(--sheet-shift, calc(100% - 44px)));
    transition: transform var(--dur-slow) var(--ease-in-out);
  }

  /* A drag the finger owns: the transition ends (DESIGN.md §2.6: CSS
     transitions stay interruptible), and the sheet follows the pointer. */
  html.js .image-panel.dragging {
    transition: none;
  }

  html.js .image-panel.sheet-open {
    transform: translateY(0);
  }

  html.js .sheet-handle {
    display: grid;
    justify-items: center;
    gap: 2px;
    width: 100%;
    min-height: 44px;
    margin: -16px -16px 0;
    padding: 8px 16px 4px;
    border: 0;
    border-bottom: 1px solid var(--separator);
    border-radius: 14px 14px 0 0;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: var(--text-sm);
    cursor: grab;
    touch-action: none;
  }
}

/* At the desktop breakpoint the panel is a pinned inspector (K27 motion 4):
   it stays beside the picture while the — possibly very tall — stage
   scrolls, offset below the sticky top bar by the height the script keeps
   current. The tablet keeps the plain two-column row. */
@media (min-width: 1200px) {
  .image-panel {
    position: sticky;
    top: calc(var(--topbar-h) + 8px);
    max-height: calc(100vh - var(--topbar-h) - 24px);
    max-height: calc(100dvh - var(--topbar-h) - 24px);
    overflow-y: auto;
  }
}

/* ------------------------------------------------------------- sidebar */

.desktop-sidebar,
.icon-rail,
.right-inspector {
  display: none;
}

.sidebar-brand {
  padding: 4px 8px;
  font-size: var(--text-base);
  font-weight: 700;
}

.sidebar-section {
  margin-bottom: 16px;
}

.sidebar-section-title {
  margin-bottom: 2px;
  padding: 4px 8px;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.sidebar-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 32px;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--text-sm);
}

.sidebar-row:hover {
  background: var(--surface-alt);
  color: var(--text);
  text-decoration: none;
}

.sidebar-row.active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}

.sidebar-row-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar-row-count {
  flex: 0 0 auto;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

.sidebar-row.active .sidebar-row-count {
  color: var(--accent);
}

.sidebar-empty {
  padding: 2px 8px;
  color: var(--text-muted);
  font-size: var(--text-xs);
}

/* ------------------------------------------------------- phone tab bar */

.bottom-tab-bar {
  position: fixed;
  inset: auto 0 0;
  z-index: 30;
  display: flex;
  height: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--surface);
  border-top: 1px solid var(--border);
}

.tab-item {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  min-height: 44px;
  color: var(--text-muted);
  font-size: 0.6875rem;
  text-align: center;
}

.tab-item:hover {
  color: var(--accent);
  text-decoration: none;
}

.tab-item.active {
  color: var(--accent);
  font-weight: 600;
}

.tab-icon {
  width: 20px;
  height: 20px;
  fill: currentColor;
  stroke: none;
}

/* --------------------------------------------------- tablet: 768-1199px */

@media (min-width: 768px) {
  .app-shell {
    display: flex;
    padding-bottom: 0;
  }

  .app-shell > .icon-rail {
    order: -1;
  }

  .bottom-tab-bar {
    display: none;
  }

  .icon-rail {
    position: sticky;
    top: 0;
    align-self: flex-start;
    display: flex;
    flex: 0 0 var(--rail-w);
    flex-direction: column;
    align-items: center;
    gap: 4px;
    width: var(--rail-w);
    height: 100vh;
    padding: 10px 0;
    background: var(--surface);
    border-right: 1px solid var(--border);
  }

  .rail-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    width: 100%;
    min-height: 44px;
    padding: 4px 2px;
    color: var(--text-muted);
    font-size: 0.625rem;
    text-align: center;
  }

  .rail-item:hover {
    color: var(--accent);
    text-decoration: none;
  }

  .rail-item.active {
    color: var(--accent);
    font-weight: 600;
  }

  .grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .grid.size-s {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }

  .grid.size-l {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .folder-grid,
  .collection-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .image-detail-layout {
    flex-direction: row;
    align-items: flex-start;
  }

  /* The picture takes the room that is left; the panel is a fixed column, so a
     picture that has not loaded yet cannot squeeze it out of shape. */
  .image-stage {
    flex: 1 1 0;
    min-width: 0;
  }

  .image-panel {
    flex: 0 0 min(22rem, 100%);
  }
}

/* -------------------------------------------------- desktop: 1200px and up */

@media (min-width: 1200px) {
  .icon-rail {
    display: none;
  }

  .desktop-sidebar {
    position: sticky;
    top: 0;
    align-self: flex-start;
    display: flex;
    flex: 0 0 var(--sidebar-w);
    flex-direction: column;
    width: var(--sidebar-w);
    height: 100vh;
    overflow-y: auto;
    padding: var(--pad);
    background: var(--surface);
    border-right: 1px solid var(--border);
  }

  .right-inspector {
    position: sticky;
    top: 0;
    align-self: flex-start;
    display: flex;
    flex: 0 0 var(--inspector-w);
    flex-direction: column;
    width: var(--inspector-w);
    height: 100vh;
    overflow-y: auto;
    padding: var(--pad);
    background: var(--surface);
    border-left: 1px solid var(--border);
  }

  .grid {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }

  .grid.size-s {
    grid-template-columns: repeat(7, minmax(0, 1fr));
  }

  .grid.size-l {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .folder-grid,
  .collection-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .right-inspector {
    justify-content: safe center;
  }

  .inspector-empty {
    margin: 0 auto;
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-align: center;
  }

  .inspector-preview-img {
    width: 100%;
    aspect-ratio: 1;
    margin-bottom: var(--pad);
    background: var(--tile-bg);
    border-radius: var(--radius);
    object-fit: contain;
  }

  .inspector-title {
    margin-bottom: 4px;
    font-size: var(--text-base);
    font-weight: 600;
    overflow-wrap: anywhere;
  }

  .inspector-section {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding-top: var(--pad);
    border-top: 1px solid var(--border);
  }

  .inspector-section h4 {
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  .inspector-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
  }

  .inspector-tag {
    padding: 1px 6px;
    background: var(--surface-alt);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: var(--text-xs);
  }

  .inspector-tag:hover {
    color: var(--accent);
    text-decoration: none;
  }
}

/* Without the inspector the grid gets the room instead. */
.inspector-collapsed .right-inspector {
  display: none;
}

@media print {
  .top-bar,
  .bottom-tab-bar,
  .icon-rail,
  .desktop-sidebar,
  .right-inspector,
  .pagination {
    display: none;
  }
}

/* === W43: theme === */

/* Two tokens the refined tile adds (DESIGN.md §4.3): the quiet fill a chip
   is painted with, the hairline that rims it, and the sheen a skeleton
   sweeps with (§4.8). All three are translucent, so each is stated twice —
   over light, and over dark. */
html {
  --chip-fill: rgba(0, 0, 0, 0.055);
  --separator: rgba(0, 0, 0, 0.09);
  --shimmer: rgba(255, 255, 255, 0.7);
}

@media (prefers-color-scheme: dark) {
  html:not([data-theme="light"]) {
    --chip-fill: rgba(255, 255, 255, 0.075);
    --separator: rgba(255, 255, 255, 0.1);
    --shimmer: rgba(255, 255, 255, 0.055);
  }
}

/* W34 audit #11: the theme used to be the OS's alone. A choice written on
   `<html data-theme>` paints the same palette the two `:root` blocks above
   carry, and `html[data-theme]` outranks `:root`, so the choice holds
   against the OS. `auto` is no attribute at all: the media queries stay in
   charge, and a person who changes their mind gets the OS back. The script
   applies the stored choice as its first act, before the page is painted,
   so a reload does not flash the other theme. */
html[data-theme="light"] {
  color-scheme: light;

  --bg: #f7f8fa;
  --surface: #ffffff;
  --surface-alt: #eef1f6;
  --border: #dde3ec;
  --text: #10151f;
  --text-muted: #5d6b80;
  --accent: #2f6fd0;
  --accent-text: #ffffff;
  --accent-soft: rgba(47, 111, 208, 0.1);
  --tile-bg: #e9edf3;
  --danger: #b4341f;
  --danger-soft: rgba(180, 52, 31, 0.08);

  --chip-fill: rgba(0, 0, 0, 0.055);
  --separator: rgba(0, 0, 0, 0.09);
  --shimmer: rgba(255, 255, 255, 0.7);
}

html[data-theme="dark"] {
  color-scheme: dark;

  --bg: #0e1116;
  --surface: #161b22;
  --surface-alt: #1d232c;
  --border: #2a323d;
  --text: #e6edf3;
  --text-muted: #9aa7b6;
  --accent: #6ea8ff;
  --accent-text: #0b1220;
  --accent-soft: rgba(110, 168, 255, 0.14);
  --tile-bg: #1b212a;
  --danger: #ff8a72;
  --danger-soft: rgba(255, 138, 114, 0.12);

  --chip-fill: rgba(255, 255, 255, 0.075);
  --separator: rgba(255, 255, 255, 0.1);
  --shimmer: rgba(255, 255, 255, 0.055);
}

/* The toolbar's theme switch: a segmented "raised" control (DESIGN.md §4.4)
   — the active segment is the surface lifted out of the track, not an
   accent block. */
.theme-switch {
  display: inline-flex;
  flex: 0 0 auto;
  gap: 2px;
  padding: 2px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: 999px;
}

.theme-switch button {
  min-width: 44px;
  min-height: 26px;
  padding: 0 8px;
  background: transparent;
  border: 0;
  border-radius: 999px;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--text-xs);
  cursor: pointer;
}

.theme-switch button:hover {
  color: var(--text);
}

.theme-switch button.active {
  background: var(--surface);
  color: var(--text);
  font-weight: 600;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}

/* === W43: chips === */

/* One line, always (DESIGN.md §4.3): the chips live inside the capsule and
   scroll sideways behind an edge fade rather than wrapping into a second
   row. The fade is only drawn while the row really overflows, which the
   script measures and marks with `.scrollable`. */
.scope-chips {
  display: flex;
  flex: 0 1 auto;
  align-items: center;
  gap: 6px;
  min-width: 0;
  margin: -2px 0;
  padding: 2px 0;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.scope-chips::-webkit-scrollbar {
  display: none;
}

.scope-chips.scrollable {
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent,
    #000 10px,
    #000 calc(100% - 10px),
    transparent
  );
  mask-image: linear-gradient(
    90deg,
    transparent,
    #000 10px,
    #000 calc(100% - 10px),
    transparent
  );
}

.scope-chips.scrollable.at-start {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 10px), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - 10px), transparent);
}

.scope-chips.scrollable.at-end {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 10px);
  mask-image: linear-gradient(90deg, transparent, #000 10px);
}

/* A chip is a quiet capsule: its kind is the 12px glyph, so the label is
   only the value, truncated at 22ch with the whole thing in the tooltip. */
.chip {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 5px;
  height: 22px;
  max-width: 22ch;
  padding: 0 3px 0 7px;
  overflow: hidden;
  background: var(--chip-fill);
  border-radius: 999px;
  box-shadow: inset 0 0 0 0.5px var(--separator);
  color: var(--text);
  font-size: var(--text-xs);
  letter-spacing: 0.01em;
  line-height: 1;
  white-space: nowrap;
}

.chip-kind {
  display: inline-flex;
  flex: 0 0 12px;
  align-items: center;
  justify-content: center;
  width: 12px;
  height: 12px;
  color: var(--text-muted);
}

.chip-kind svg {
  display: block;
  width: 12px;
  height: 12px;
}

.chip-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The remove target is always in the layout — only its ink waits for
   hover or focus, so the chip never reflows under a cursor. */
.chip-remove {
  display: inline-flex;
  flex: 0 0 16px;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  color: var(--text-muted);
  line-height: 1;
  opacity: 0;
  transition:
    opacity 0.12s ease,
    background 0.12s ease,
    color 0.12s ease;
}

.chip-remove svg {
  display: block;
  width: 8px;
  height: 8px;
}

.chip-remove:focus-visible {
  outline: none;
}

.chip:hover .chip-remove,
.chip:focus-within .chip-remove,
.chip:has(.chip-remove:focus-visible) .chip-remove {
  opacity: 1;
}

.chip-remove:hover {
  background: var(--text-muted);
  color: var(--surface);
  text-decoration: none;
}

.chip:has(.chip-remove:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* A touch screen has no hover to reveal the ✕ with, so it stays visible. */
@media (hover: none) {
  .chip-remove {
    opacity: 1;
  }
}

/* Exit is faster than enter (§4.3). */
.chip.removing {
  opacity: 0;
  transform: scale(0.96);
  transition:
    opacity 0.12s ease,
    transform 0.12s ease;
}

/* === W43: search === */

/* The capsule is one line at every width: the glyph, then whatever the page
   puts in it — a field, or the chips of the active scope — then the label. */
.search-glyph {
  display: block;
  flex: 0 0 16px;
  width: 16px;
  height: 16px;
  margin-right: 6px;
  color: var(--text-muted);
}

.pill-scoped {
  gap: 6px;
}

.pill-scoped:hover {
  border-color: var(--accent);
}

.pill-scoped .search-label {
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--accent);
  font-size: var(--text-sm);
}

.pill-scoped .search-label:hover {
  text-decoration: underline;
}

/* The `/search` field and the tag filter are the same capsule with a real
   input, so they take the input affordances the tile gives a field. */
.search-pill-container input {
  flex: 1 1 8ch;
  min-width: 6ch;
}

.search-pill-container:focus-within {
  background: var(--surface);
  border-color: var(--accent);
}

.tag-filter-field {
  margin-bottom: var(--pad);
}

/* W34 audit #7: two lists, not one long dump — the top tags by count beside
   the whole A–Z, each with its count, and the filter box narrows both. */
.tag-groups {
  display: grid;
  gap: 16px;
  margin-bottom: var(--pad);
}

@media (min-width: 900px) {
  .tag-groups {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
  }
}

.tag-group-title {
  margin: 0 0 6px;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

/* A row the filter hides is hidden, whatever its own display is; `hidden`
   alone loses to `display: flex`. */
.tag-row[hidden],
.tag-group[hidden] {
  display: none;
}

/* === W43: states === */

/* DESIGN.md §4.8: a small monochrome glyph, a regular-weight title, one
   sentence, and at most one text-style action. Nothing here is a block of
   colour — an error is a neutral surface whose only red thing is the glyph. */
.state-glyph {
  display: block;
  width: 28px;
  height: 28px;
  margin: 0 auto var(--pad);
  color: var(--text-muted);
}

.state-glyph svg {
  display: block;
  width: 28px;
  height: 28px;
}

.empty-state h3 {
  margin-bottom: 6px;
  color: var(--text);
  font-size: var(--text-lg);
  font-weight: 400;
  letter-spacing: -0.01em;
}

.empty-state p {
  max-width: 38ch;
  margin: 0 auto;
  color: var(--text-muted);
  font-size: var(--text-base);
}

.state-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 28px;
  margin-top: calc(var(--pad) + 2px);
  padding: 0 var(--pad);
  border: 0;
  border-radius: 999px;
  color: var(--accent);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
}

.state-action:hover {
  background: var(--accent-soft);
  text-decoration: none;
}

.state-action:active {
  transform: scale(0.98);
}

/* The one place `--danger` is allowed to land on a state. */
.state-error .state-glyph {
  color: var(--danger);
}

/* The inline form, one size down, inside a panel or under a list. */
.inline-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  max-width: none;
  padding: calc(var(--pad) * 2) var(--pad);
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: inset 0 0 0 1px var(--separator);
  text-align: center;
}

.inline-state[hidden] {
  display: none;
}

.inline-state .state-glyph {
  width: 20px;
  height: 20px;
  margin-bottom: 0;
}

.inline-state .state-glyph svg {
  width: 20px;
  height: 20px;
}

.state-title {
  margin: 0;
  color: var(--text);
  font-size: var(--text-base);
  font-weight: 500;
}

.state-sentence {
  max-width: 34ch;
  margin: 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.inline-state .state-action {
  min-height: 26px;
  margin-top: 0;
}

/* Loading (§4.8): the one place a request is visibly in flight is the
   "Load more" control, so its label is replaced by a skeleton bar in
   `--surface-alt` with the low-contrast sheen sweeping it. The grid keeps
   its tiles, so the columns never jump. */
.load-more-btn.is-loading {
  position: relative;
  overflow: hidden;
  color: transparent;
}

.load-more-btn.is-loading::after {
  content: "";
  position: absolute;
  inset: 12px 16px;
  background: var(--surface-alt);
  border-radius: var(--radius-sm);
}

.load-more-btn.is-loading::before {
  content: "";
  position: absolute;
  inset: 12px 16px;
  z-index: 1;
  background: linear-gradient(
    90deg,
    transparent,
    var(--shimmer),
    transparent
  );
  transform: translateX(-100%);
  animation: w43-sheen 1.7s ease-in-out infinite;
}

@keyframes w43-sheen {
  to {
    transform: translateX(100%);
  }
}

/* The sheen is the only motion a skeleton gets, and it is not rendered at
   all when motion is reduced — the tint stays, static. */
@media (prefers-reduced-motion: reduce) {
  .load-more-btn.is-loading::before {
    display: none;
  }

  .load-more-btn.is-loading::after {
    animation: none;
  }
}

/* === sidebar (W42) ============================================ */
/* W42 styling batch — W34 audit #4/#5/#6/#10, DESIGN.md §2–§4.2:
   the translucent, resizable sidebar with a collapsible folder tree,
   section headers on scale, and truncated rows whose full name is always
   reachable. One block, appended last, so parallel batches merge cleanly
   and its rules win over the earlier sidebar section they re-style.
   Light and dark are both defined here; nothing animates that
   prefers-reduced-motion has not already stopped. */

/* The tokens this block needs, by their DESIGN.md names (values verbatim).
   Elevation and motion tokens are theme-independent. */
:root {
  --separator: rgba(0, 0, 0, 0.09);
  --material-sidebar: rgba(246, 247, 250, 0.72);
  --elev-1: 0 1px 2px rgba(0, 0, 0, 0.06), 0 8px 24px rgba(0, 0, 0, 0.1);
  --dur-fast: 120ms;
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  /* DESIGN.md §3: default 248, min 200, max 360 — the script clamps to
     these and remembers the choice per device. */
  --sidebar-w: 248px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --separator: rgba(255, 255, 255, 0.1);
    --material-sidebar: rgba(32, 35, 42, 0.72);
  }
}

/* The theme switch (W43) picks a palette the operating system never asked
   for, and a media query only sees the operating system — so the material
   follows `<html data-theme>` as well. `html[data-theme]` outranks `:root`,
   the same way the W43 block makes it outrank `--separator`. */
html[data-theme="light"] {
  --material-sidebar: rgba(246, 247, 250, 0.72);
}

html[data-theme="dark"] {
  --material-sidebar: rgba(32, 35, 42, 0.72);
}

/* The sidebar as a material (§2.5): translucent fill, saturating blur,
   hairline on the edge facing content. */
.desktop-sidebar {
  position: sticky; /* the resizer and the sticky headers both need it */
  padding: 8px;
  background: var(--material-sidebar);
  border-right: 1px solid var(--separator);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
}

/* Solid fallbacks (§2.5): reduced transparency, no backdrop-filter support,
   and more-contrast all put the material down for a solid surface. The
   layout is correct with the material off — nothing here is sized by it. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .desktop-sidebar {
    background: var(--surface);
  }
}

@media (prefers-reduced-transparency: reduce) {
  .desktop-sidebar {
    background: var(--surface);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

@media (prefers-contrast: more) {
  .desktop-sidebar {
    background: var(--surface);
    border-right-color: var(--border);
  }
}

/* Section headers (§4.2): 11px uppercase at weight 500, secondary ink,
   24px tall, --space-5 (12px) of air above. */
.sidebar-section-title {
  display: flex;
  align-items: center;
  min-height: 24px;
  margin: 12px 0 2px;
  padding: 6px 8px 2px;
  color: var(--text-muted);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  line-height: 1.3;
  text-transform: uppercase;
}

.sidebar-brand + .sidebar-section .sidebar-section-title,
.sidebar-section:first-of-type .sidebar-section-title {
  margin-top: 4px;
}

.sidebar-section {
  margin-bottom: 0;
}

/* Sidebar rows (§4.1): 13px on 28px rows for pointers, 44px for touch,
   pressed feedback, and the selected weight at 500, not 600. */
.sidebar-row {
  position: relative;
  min-height: 28px;
  padding: 2px 8px;
  border-radius: 6px;
  color: var(--text);
  font-size: 0.8125rem;
  line-height: 1.4;
  transition: transform var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}

.sidebar-row:active {
  transform: scale(0.98);
}

.sidebar-row.active {
  font-weight: 500;
}

.sidebar-row-label {
  flex: 1 1 auto;
  min-width: 0;
}

@media (pointer: coarse) {
  .desktop-sidebar .sidebar-row {
    min-height: 44px;
  }
}

/* The folder tree (§4.1): every row reserves a 16px lane at the front of
   its depth indent for the disclosure triangle, so leaf rows and branch
   rows at the same depth align, the triangle sits at `depth × 14px`, and
   a parent's row is never pushed right of its own children (indent =
   depth × 14px on the row itself, plus the lane — never in flow, or the
   ladder inverts). Without the script the whole tree is shown with the
   original pre-W42 indent and the triangles do not exist on screen. */
.folder-node {
  position: relative;
  min-width: 0;
  margin-left: calc(var(--depth, 0) * 14px);
}

html.js .folder-node {
  margin-left: calc(var(--depth, 0) * 14px + 16px);
}

.folder-node[hidden] {
  display: none;
}

.sidebar-tree .sidebar-row.folder-row {
  padding-left: 8px;
}

.tree-triangle {
  display: none;
  position: absolute;
  top: 0;
  bottom: 0;
  /* The node already carries the depth indent plus the 16px lane, so the
     lane is exactly one step back and needs no depth arithmetic. */
  left: -16px;
  width: 16px;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 4px;
  color: var(--text-muted);
  cursor: pointer;
}

html.js .tree-triangle {
  display: inline-flex;
}

.tree-tri {
  width: 0;
  height: 0;
  border-top: 3px solid transparent;
  border-bottom: 3px solid transparent;
  border-left: 4px solid currentColor;
  transition: transform var(--dur-fast) var(--ease-out);
}

.tree-triangle[aria-expanded="true"] .tree-tri {
  transform: rotate(90deg);
}

.tree-triangle:hover {
  color: var(--text);
  background: var(--surface-alt);
}

/* The drag edge (§5 decision 2): a 6px hit area on the inner edge,
   invisible until hover, drag or keyboard focus, when a 1px hairline
   turns accent. No persistent handle. */
.sidebar-resizer {
  display: none;
}

@media (min-width: 1200px) {
  .sidebar-resizer {
    position: fixed;
    top: 0;
    bottom: 0;
    left: calc(var(--sidebar-w) - 3px);
    z-index: 25;
    display: block;
    width: 6px;
    cursor: col-resize;
    background: transparent;
  }

  .sidebar-resizer::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 2px;
    width: 1px;
    background: transparent;
    transition: background var(--dur-fast) var(--ease-out);
  }

  .sidebar-resizer:hover::after,
  .sidebar-resizer:focus-visible::after,
  .sidebar-resizer.dragging::after {
    background: var(--accent);
  }
}

/* The styled tooltip for truncated rows (§4.1): the full string rides in
   the native `title` always, and this surface fades in above the row —
   like the tile — on hover or focus of a row the script measured as
   truncated (400ms delay, instant once one is already open). Solid
   `--surface` — never a second translucent surface stacked on the
   sidebar (§2.5). */
.sidebar-tip {
  position: absolute;
  bottom: calc(100% + 4px);
  left: 8px;
  z-index: 30;
  /* Capped to the row so the sidebar's scrollport never clips the bubble;
     longer paths wrap (overflow-wrap below) instead. */
  max-width: min(280px, calc(100% - 8px));
  padding: 4px 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: var(--elev-1);
  color: var(--text);
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: normal;
  line-height: 1.35;
  text-transform: none;
  white-space: normal;
  overflow-wrap: anywhere;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out);
}

.sidebar-tip.shown {
  opacity: 1;
}

@media (hover: none) {
  .sidebar-tip {
    display: none;
  }
}

/* === W47: the filter panel ==================================== */

/* The facet list beside the grid (W47, K27 motion 3): every value the viewer can
   filter by, each with the number of images it would show *beside the filters
   already on*. On a phone and a tablet it is one row above the grid, folded
   behind its heading until it is opened; on a desktop the same lists stand open
   in a column of their own. The page carries both mounts and this sheet shows
   one of them per width, because a closed `<details>` cannot be forced open from
   the stylesheet — measured in Chrome 155, where its content is not hidden by
   `display` on the children but simply not projected, so there is nothing for
   `display` to act on. Two mounts, one list of rows, no script either way.
   Appended last, as the W42 block was, so parallel batches merge cleanly and
   these rules win over the earlier sections they extend. */

/* The one token this block adds, by its DESIGN.md name and value: §2.1's
   `--text-tertiary` — the ink for disabled labels, 4.9:1 on light and 5.2:1 on
   dark, so a dimmed row is quiet without becoming illegible. Stated the way the
   W43 theme block states them, so a chosen theme holds against the OS. */
html {
  --text-tertiary: #626c7b;
  --panel-w: 208px;
}

@media (prefers-color-scheme: dark) {
  html:not([data-theme="light"]) {
    --text-tertiary: #818c9d;
  }
}

html[data-theme="light"] {
  --text-tertiary: #626c7b;
}

html[data-theme="dark"] {
  --text-tertiary: #818c9d;
}

.refine-layout {
  min-width: 0;
}

.filter-aside {
  min-width: 0;
  background: var(--surface);
  border-bottom: 1px solid var(--separator);
}

/* The column, not the fold, appears at the desktop width — and here it is the
   fold that stands down entirely, heading and body together. */
.filter-static {
  display: none;
}

.filter-fold-summary {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 8px 16px;
  color: var(--text);
  cursor: pointer;
  font-size: 0.8125rem;
  font-weight: 600;
  /* The chevron below, not the UA marker. */
  list-style: none;
}

.filter-fold-summary::-webkit-details-marker {
  display: none;
}

.filter-glyph {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  color: var(--text-muted);
}

.filter-fold-summary::after {
  content: "";
  flex: 0 0 auto;
  margin-left: auto;
  width: 0;
  height: 0;
  border-top: 5px solid currentColor;
  border-right: 4px solid transparent;
  border-left: 4px solid transparent;
  color: var(--text-muted);
  transition: transform var(--dur-fast) var(--ease-out);
}

.filter-fold[open] .filter-fold-summary::after {
  transform: rotate(180deg);
}

/* How many filters are on: the phone reads the number with the fold shut, so a
   narrowed grid is never mistaken for the whole library. */
.filter-fold-count {
  flex: 0 0 auto;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 0.6875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.filter-fold-body {
  padding: 0 16px 12px;
}

.filter-hint {
  margin-bottom: 8px;
  color: var(--text-muted);
  font-size: 0.75rem;
  line-height: 1.4;
}

.filter-group {
  margin-bottom: 12px;
}

.filter-group:last-of-type {
  margin-bottom: 0;
}

.filter-group-title {
  margin: 8px 0 2px;
  padding: 0 8px;
  color: var(--text-muted);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  line-height: 1.3;
  text-transform: uppercase;
}

.filter-rows {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The row is the sidebar row (§4.1) with a count that means something else:
   same 28px, same 13px ink, same ellipsis with the whole value one hover away. */
.filter-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 28px;
  padding: 2px 8px;
  border-radius: 6px;
  color: var(--text);
  font-size: 0.8125rem;
  line-height: 1.4;
}

a.filter-row:hover {
  background: var(--surface-alt);
  color: var(--text);
  text-decoration: none;
}

a.filter-row:active {
  transform: scale(0.99);
}

.filter-row-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.filter-row-count {
  flex: 0 0 auto;
  color: var(--text-muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

.filter-row.is-active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 500;
}

.filter-row.is-active .filter-row-count {
  color: var(--accent);
}

/* A value the filters on leave nothing for: dimmed, and no link. The one
   convention, in every group alike — its own zero already says what clicking it
   would show, so the row keeps its place in the list (a value that vanished
   would read as the library having changed) and gives up the click. */
.filter-row.is-empty,
.filter-row.is-empty .filter-row-count {
  color: var(--text-tertiary);
  cursor: default;
}

.filter-more,
.filter-clear {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 6px;
  color: var(--accent);
  font-size: 0.75rem;
}

.filter-clear {
  margin-top: 6px;
  font-size: 0.8125rem;
}

.filter-more:hover,
.filter-clear:hover {
  background: var(--accent-soft);
  text-decoration: none;
}

.filter-more-end,
.filter-group-empty {
  margin: 2px 0 0;
  padding: 0 8px;
  color: var(--text-muted);
  font-size: 0.75rem;
  line-height: 1.4;
}

/* Touch targets (§3): 44px rows under a finger, 28px under a pointer. */
@media (pointer: coarse) {
  .filter-row {
    min-height: 44px;
  }
}

@media (min-width: 1200px) {
  .refine-layout {
    display: flex;
    align-items: flex-start;
  }

  .refine-results {
    flex: 1 1 auto;
    min-width: 0;
  }

  .filter-aside {
    position: sticky;
    top: var(--topbar-h);
    align-self: flex-start;
    flex: 0 0 var(--panel-w);
    max-height: calc(100vh - var(--topbar-h) - 16px);
    max-height: calc(100dvh - var(--topbar-h) - 16px);
    overflow-y: auto;
    padding: 4px 8px 16px;
    background: transparent;
    border-right: 1px solid var(--separator);
    border-bottom: 0;
  }

  /* The two mounts trade places here: the fold (button and all) stands down and
     the column takes its place, open by construction because a `<div>` has
     nothing to be closed. */
  .filter-fold {
    display: none;
  }

  .filter-static {
    display: block;
  }

  .filter-title {
    display: flex;
    align-items: center;
    gap: 6px;
    min-height: 24px;
    margin: 8px 0 2px;
    padding: 6px 8px 2px;
    color: var(--text-muted);
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.02em;
    line-height: 1.3;
    text-transform: uppercase;
  }

  /* The grid keeps the tile sizes its three settings were chosen for: the panel
     takes 208px off the content column, so one fewer column at each setting
     rather than five narrower tiles. */
  .refine-layout .grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .refine-layout .grid.size-s {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }

  .refine-layout .grid.size-l {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
