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

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

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

.image-header-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
  font-size: var(--text-sm);
}

.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;
}

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