:root {
  color-scheme: light dark;
  --bg: #f5f5f7;
  --surface: rgba(255, 255, 255, 0.78);
  --surface-strong: rgba(255, 255, 255, 0.94);
  /* FULLY OPAQUE banner colour for the sticky .detail-meta strip. It is
     deliberately solid (not --surface-strong, which is 0.94 alpha) so
     body text scrolling UNDER the sticky band can't ghost through. Value
     ≈ the frosted panel's apparent colour with the old 3.5% banner tint
     baked in, so it still reads as a subtle header strip, just opaque.
     The .detail-meta::after fade dissolves the body text FROM this same
     colour, so the text fades into the header cleanly. */
  --meta-band: #f4f4f5;
  --line: rgba(0, 0, 0, 0.08);
  --text: #1d1d1f;
  --muted: #6e6e73;
  --soft: #86868b;
  --accent: #3f94ba;
  --accent-soft: rgba(63, 148, 186, 0.11);
  /* Tight, low-spread shadow — was 0 18px 50px / 0.08 alpha which
     made each .panel cast a 50px-wide soft halo. With three panels
     adjacent, those halos overlapped into a single perceived darker
     "container" behind the row. Pulling the spread down to 6px
     keeps the halos local to each panel so they don't reach across
     the gap. */
  --shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
  --radius: 20px;
  /* Code surface — distinct from --surface-strong (which is near-white
     in light mode and disappears against white cards). A subtle gray
     tint in light mode and a subtle white lift in dark mode keep code
     chips visibly differentiated from surrounding text in both themes. */
  --code-bg: rgba(0, 0, 0, 0.06);
  --code-border: rgba(0, 0, 0, 0.08);
  /* Single source of truth for the "delete / danger / destructive"
     red across the dashboard — used by the close-X traffic-light
     button, the danger-menu hover state, the danger-button icon,
     etc. Same value in both light and dark mode (the colour is vivid
     enough to work in both); the alpha-tinted variants below let
     soft hover backgrounds compose against it. */
  --danger: #fe5f63;
  --danger-soft: rgba(254, 95, 99, 0.12);
  --danger-soft-strong: rgba(254, 95, 99, 0.22);
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="dark"]) {
    --bg: #000000;
    --surface: rgba(28, 28, 30, 0.76);
    --surface-strong: rgba(44, 44, 46, 0.94);
    --meta-band: #202022;
    --line: rgba(255, 255, 255, 0.12);
    --text: #f5f5f7;
    --muted: #a1a1a6;
    --soft: #8e8e93;
    --accent: #3f94ba;
    --accent-soft: rgba(63, 148, 186, 0.18);
    /* Same shadow-shrink as light mode — tight, low-spread so the
       halos stay local to each panel and don't merge across the gap. */
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
    --code-bg: rgba(255, 255, 255, 0.10);
    --code-border: rgba(255, 255, 255, 0.14);
    --danger: #fe5f63;
    --danger-soft: rgba(254, 95, 99, 0.16);
    --danger-soft-strong: rgba(254, 95, 99, 0.28);
  }
}

:root[data-theme="dark"] {
    --bg: #000000;
    --surface: rgba(28, 28, 30, 0.76);
    --surface-strong: rgba(44, 44, 46, 0.94);
    --meta-band: #202022;
    --line: rgba(255, 255, 255, 0.12);
    --text: #f5f5f7;
    --muted: #a1a1a6;
    --soft: #8e8e93;
    --accent: #3f94ba;
    --accent-soft: rgba(63, 148, 186, 0.18);
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
    --code-bg: rgba(255, 255, 255, 0.10);
    --code-border: rgba(255, 255, 255, 0.14);
    --danger: #fe5f63;
    --danger-soft: rgba(254, 95, 99, 0.16);
    --danger-soft-strong: rgba(254, 95, 99, 0.28);
  }

* {
  box-sizing: border-box;
}

/* While the page is in its initial-load phase (HTML loaded but JS init
   not yet finished), suppress every CSS transition/animation across the
   tree. Without this, JS-triggered class changes during init — adding
   .resizable to .content-grid, wiring up stored pane widths, etc. —
   cause flex/grid properties to transition between their pre- and
   post-init values, which the user sees as a brief panel "opening
   and closing". JS removes the class on the next frame after init,
   so post-init user interactions still get the smooth animations. */
.page-loading,
.page-loading *,
.page-loading *::before,
.page-loading *::after {
  transition: none !important;
  animation-duration: 0s !important;
  animation-delay: 0s !important;
}

/* The earlier `.theme-transitioning *` global cross-fade was removed:
   forcing transitions on every element + pseudo-element for 380ms
   was visibly laggy on the Pi (and not great on desktop either).
   Theme swaps are instant again — the dropdown's cascade + click
   spring carry the "I did something" feedback by themselves. */

/* Reduce motion (user preference, independent from OS-level
   prefers-reduced-motion). Toggled via html.reduce-motion class,
   applied by applyReduceMotion() in app.js from the Settings
   preference.

   ALL motion is suppressed:
   • Every CSS animation is forced to 0.001s duration (still fires
     animationend so JS cleanup paths — waterfall class removal,
     theme-menu close timing, etc. — keep working). Affects:
     drawer-card-waterfall, drawer-card-click-pop, detail-slide-
     in-forward/-backward, detail-body-rise, detail-meta-fade,
     rooms-nav-slide-in-forward/-backward, menuPop, confirm-pop-
     in/out, confirm-backdrop-in/out, detail-panel-btn-click-pop,
     theme-option-drop/-rise/-spring, and any future @keyframes.
   • Every CSS transition is forced to 0.001s duration. Hover
     lifts, panel resize when enlarging/minimising the detail
     panel, sheet slide-ins, edit/delete floating-button slide-up,
     all snap instantly. transitionend still fires.
   • Infinite/ambient animations (search prism rotation, settings
     gear spin) are killed outright — 0.001s × infinite would
     loop at warp speed.

   The user opts into this; non-opting users get the full motion
   language. */
html.reduce-motion *,
html.reduce-motion *::before,
html.reduce-motion *::after {
  animation-duration: 0.001s !important;
  animation-delay: 0s !important;
  transition-duration: 0.001s !important;
  transition-delay: 0s !important;
}

/* Transient "no animations" class. Added to the root by the boot
   sequence to prevent transitions from playing during initial
   state restoration (e.g. when ?e=1 in the URL hash drops the user
   straight into the maximized-detail view, we don't want the
   maximize animation to play — the user wasn't toggling it, they
   were restoring an existing state). Also wrapped around any
   runtime class-toggle that could re-trigger CSS animations on
   already-mounted elements (notably the reduce-motion toggle,
   where removing the .reduce-motion class would otherwise replay
   the settings panel's open animation while the panel is already
   visible). The class is removed after two RAFs so the suppression
   only covers the immediate paint, not the whole session. */
html.apricity-no-anim,
html.apricity-no-anim *,
html.apricity-no-anim *::before,
html.apricity-no-anim *::after {
  animation: none !important;
  transition: none !important;
}

/* Boot-time visibility gate — now a SAFETY-NET FALLBACK, not the
   primary mechanism. app.js's primeGridLayoutFromHash() removes
   `.page-loading` synchronously at parse time in every case (after
   priming the content-grid layout class from the hash so the panel
   frames already paint in the correct arrangement — see that IIFE).
   So in normal operation this rule is off before first paint and the
   skeleton frames appear immediately in the right layout, then fill
   with content as render() runs. This rule only takes effect if that
   JS throws or never runs (e.g. JS disabled), in which case keeping
   the content hidden until boot()'s tail clears the class is the
   safer degraded behavior than showing a frozen static skeleton. */
html.page-loading .content-grid,
html.page-loading .facts-panel {
  visibility: hidden;
}

/* Infinite/ambient animations: kill the animation entirely instead
   of letting it loop instantly (would peg the CPU). */
html.reduce-motion .search::before,
html.reduce-motion .settings-fab-icon {
  animation: none !important;
}

body {
  margin: 0;
  min-height: 100vh;
  /* Flat single-color page background. The previous gradient
     (#fbfbfd top → var(--bg) bottom + a radial blue tint) created
     two competing greys and a perceived "darker box" — the user
     wanted one clean surface. The brighter grey is now reserved
     for the .topbar so the header still visually divides from the
     content area beneath. */
  background: var(--bg);
  color: var(--text);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body {
    background: var(--bg);
  }
}

:root[data-theme="dark"] body {
    background:
      radial-gradient(circle at top left, rgba(63, 148, 186, 0.18), transparent 34rem),
      linear-gradient(180deg, #1c1c1e 0%, var(--bg) 100%);
  }

button,
input,
select,
textarea {
  font: inherit;
}

button {
  appearance: none;
}

:root {
  --sidebar-w: 280px;
  --rooms-w: minmax(200px, 0.55fr);
  --drawers-w: minmax(320px, 0.95fr);
  --resizer-w: 14px;  /* grab target only — no visible line, see .col-resizer */
}

/* Single-column shell — the old left sidebar (brand / wings / settings)
   was dissolved: brand moved into the top bar, wings became a horizontal
   pill row beneath it, and settings became a body-level floating fab.
   The .main panel's own 32px padding now plays the role of the web-app
   left/right page margin. */
.shell {
  min-height: 100vh;
}

/* Vertical drag handle between adjacent panels. 14px-wide hit target with
   no visible divider line — the col-resize cursor on hover is the only
   affordance, which keeps the chrome quiet between adjacent panels. */
.col-resizer {
  position: relative;
  cursor: col-resize;
  user-select: none;
  background: transparent;
  z-index: 5;
}

/* Focus-visible outline only — for keyboard nudging. No hover/drag chrome. */
.col-resizer:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -4px;
  border-radius: 2px;
}

/* When the user is dragging, freeze cursor/selection across the whole
   document so we don't get awkward I-beams during the drag. */
body.col-resizing {
  cursor: col-resize !important;
  user-select: none;
}
body.col-resizing * {
  cursor: col-resize !important;
}

/* Kill panel flex transitions during a manual drag. The drag handler
   (makeResizer in app.js) updates --rooms-w / --drawers-w on every
   pointermove (60×/sec), and the resizable-mode flex shorthand on each
   panel ties flex-basis to those CSS variables. Without this override,
   the base `.content-grid > .drawers-panel` and `.detail-panel` rules
   apply a 0.4s flex-basis transition to every drag step, so the panel
   visually trails the cursor by ~400ms — drags feel sluggish. With
   transitions off mid-drag, panel widths track the cursor instantly;
   the transition still applies for class-driven state changes
   (detail-hidden toggle, initial layout) because those happen outside
   the drag. */
body.col-resizing .content-grid > .rooms-panel,
body.col-resizing .content-grid > .drawers-panel,
body.col-resizing .content-grid > .detail-panel,
body.col-resizing .content-grid > .col-resizer {
  transition: none !important;
}

/* .sidebar / .sidebar-* and the in-sidebar settings button styles were
   removed when the left sidebar was dissolved. Brand lives in the top
   bar now (compact .brand below), settings is a floating fab
   (.settings-fab further down). */

/* Brand lockup: logo + name + tagline. Sits at the left of the top bar
   as the page's primary visual anchor. Sized larger than the search
   bar / pill chips so the hierarchy reads brand > nav > content.
   Width is pinned to var(--rooms-w) so the brand block aligns
   vertically with the Rooms panel directly below it — and follows
   when the user drags the rooms/drawers column resizer. */
.brand {
  display: flex;
  align-items: center;
  gap: 14px;
  flex: 0 0 var(--rooms-w, 300px);
  /* No padding here — the parent .topbar handles its own spacing. */
}

.brand-mark {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: 0;
  overflow: visible;
  background: transparent;
  box-shadow: none;
  flex-shrink: 0;
}

.brand-text {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
  min-width: 0;
}

.brand-logo {
  display: block;
  width: 56px;
  height: 56px;
  object-fit: cover;
  /* No mix-blend-mode — the Apricity sphere is a deliberately
     designed RGBA gradient and renders cleanly on any surface. The
     old multiply/screen overrides existed only to bleed white-halo
     residue out of the legacy MemPalace transparent PNG. */
}

.brand-name {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text);
}

.brand-subtitle {
  font-size: 13px;
  color: var(--muted);
  letter-spacing: -0.005em;
}

.panel-header p,
.drawer-card span,
.detail-meta span {
  color: var(--muted);
}

/* On narrow viewports collapse to logo-only so the search keeps
   breathing room. Threshold lifted to 900px since the brand text is
   now substantial enough to push the search past comfort below that. */
@media (max-width: 900px) {
  .brand-text {
    display: none;
  }
}

.nav {
  display: grid;
  gap: 8px;
}

/* The .nav.nav-horizontal pill-row rules (and a brief intermediate
   wings-grouped accordion) were both retired when the wing navigation
   moved into the Rooms-panel drill-down — see the "Rooms panel:
   drill-down navigation" block lower in this file. The .nav-row /
   .nav-item / .nav-toolbar rules below are still used by other
   consumers and by the renaming inline forms. */

.nav-row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 4px;
  border-radius: 12px;
}

.nav-row .menu-wrap,
.room-row .menu-wrap {
  opacity: 0;
  transition: opacity 0.18s ease;
}

.nav-row:hover .menu-wrap,
.nav-row:focus-within .menu-wrap,
.room-row:hover .menu-wrap,
.room-row:focus-within .menu-wrap {
  opacity: 1;
}

.menu-wrap:has(.action-menu:not(.hidden)) {
  opacity: 1;
}

.nav-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  min-height: 44px;
  border: 0;
  border-radius: 12px;
  padding: 0 12px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  text-align: left;
}

.nav-row:hover .nav-item,
.nav-row.active .nav-item {
  background: var(--surface-strong);
}

.nav-row.active .nav-item {
  box-shadow: inset 0 0 0 1px var(--line);
}

.nav-item strong {
  color: var(--soft);
  font-size: 13px;
}

.nav-item:disabled {
  cursor: default;
  opacity: 0.85;
}

.nav.editing .nav-item strong,
.room-nav.editing .room-item strong {
  display: none;
}

.nav-toolbar,
.room-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 4px 6px;
  min-height: 28px;
}

.nav-toolbar-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.nav-toolbar-action {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--accent);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
}

.nav-toolbar-action:hover {
  background: var(--surface-strong);
  border-color: var(--accent);
}

.nav-toolbar-action svg {
  width: 13px;
  height: 13px;
}

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

.edit-toggle {
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition:
    background 160ms ease,
    border-color 160ms ease,
    color 160ms ease;
}

.edit-toggle svg {
  width: 14px;
  height: 14px;
}

.edit-toggle:hover {
  background: var(--accent-soft);
  color: var(--accent);
}

.edit-toggle.active {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}

.row-actions {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding-right: 4px;
}

.row-action {
  display: inline-grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  color: var(--soft);
  cursor: pointer;
  transition:
    background 160ms ease,
    border-color 160ms ease,
    color 160ms ease;
}

.row-action svg {
  width: 15px;
  height: 15px;
}

.row-action:hover {
  background: var(--accent-soft);
  color: var(--accent);
}

.row-action.danger:hover {
  background: rgba(254, 95, 99, 0.12);
  color: #fe5f63;
}

.row-action.accent {
  color: var(--accent);
}

.row-action.accent:hover {
  background: var(--accent-soft);
}

.rename-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 4px;
  width: 100%;
  padding: 4px 6px;
  border-radius: 12px;
  background: var(--surface-strong);
  box-shadow: inset 0 0 0 1px var(--line);
}

.rename-input {
  width: 100%;
  min-width: 0;
  height: 32px;
  padding: 0 10px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  font: inherit;
  outline: none;
}

.rename-input:focus {
  background: var(--surface);
  box-shadow: inset 0 0 0 1px rgba(63, 148, 186, 0.45);
}

.rename-input.rename-error {
  box-shadow: inset 0 0 0 1px rgba(254, 95, 99, 0.45);
  color: #fe5f63;
}

.main {
  /* Constrain to a comfortable max width and centre — same pattern as
     Linear, Vercel's dashboard, GitHub Issues, Stripe's dashboard.
     Without max-width the top-bar items (brand on the left, action
     cluster on the right) drift apart on wide monitors until they
     read as unrelated, and the 3-panel content row stretches into
     uncomfortably long line lengths in the memory previews. 1440px
     keeps the layout cohesive on ultrawides while still giving the
     three-column content grid room to breathe. */
  max-width: 1440px;
  margin: 0 auto;
  padding: 24px 32px;
  overflow: hidden;
}

/* Topbar is the page's header chrome — sits OUTSIDE .main, spans
   100% viewport width with its own brighter-grey background and a
   hairline bottom border. The inner content (brand · search ·
   actions) is constrained by .topbar-inner below to the same
   max-width + padding as .main, so the layout still aligns with
   the content grid beneath. */
.topbar {
  background: var(--surface-strong);
  border-bottom: 1px solid var(--line);
  margin-bottom: 24px;
}

.topbar-inner {
  display: flex;
  align-items: center;
  gap: 16px;
  /* Same max-width + horizontal padding as .main so brand/search
     align vertically with the rooms + drawers panels below. */
  max-width: 1440px;
  margin: 0 auto;
  padding: 18px 32px;
  /* No `justify-content: space-between` here — the brand and search
     each pin to the panel widths below them (via flex-basis below),
     and `.top-actions` uses margin-left:auto to slide to the far
     right. space-between would re-distribute leftover space between
     items, breaking the vertical alignment with the content grid. */
}

.top-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
  /* Push the action cluster to the far right of the top bar (aligns
     it visually with the detail-panel column below). */
  margin-left: auto;
}

h1,
h2,
h3,
p {
  margin: 0;
}

.topbar h1 {
  font-size: 42px;
  line-height: 1.05;
  letter-spacing: 0;
}

.topbar p {
  margin-top: 8px;
  color: var(--muted);
}

.search {
  /* position:relative + isolation:isolate makes the rotating-ring
     ::before (below) live in this element's own stacking context, so
     the conic-gradient doesn't bleed onto adjacent siblings in the
     top bar. */
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  gap: 12px;
  /* Width tracks var(--drawers-w) (the memories panel below) with a
     +60px extension so the search reaches a touch past the memories
     panel's right edge — keeps the column alignment intuitive while
     giving the input a slightly more comfortable proportion. Still
     follows the drawers/detail column resizer in real time. */
  flex: 0 0 calc(var(--drawers-w, 480px) + 60px);
  min-width: 0;
  max-width: calc(var(--drawers-w, 480px) + 60px);
  height: 44px;
  padding: 0 12px 0 16px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  backdrop-filter: blur(20px);
  transition:
    border-color 160ms ease,
    box-shadow 160ms ease;
}

.search:focus-within {
  border-color: rgba(63, 148, 186, 0.45);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

/* Rotating conic-gradient ring around the search bar — ported from the
   active-memory-card glow. Same xor-mask trick: a 2px padding band
   sits as a strictly-defined ring outside the .search silhouette, no
   inner bleed, no edge feather. Uses the established palette (white-
   cyan peak → mid teal → deep navy → mirror back) and the same slow
   15s linear rotation rhythm. Always on — this is the "Apricity is
   alive and listening" affordance now that the search bar is the
   centre of the top bar. */
.search::before {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: 16px; /* search's 14px + 2px outset */
  padding: 2px;
  background: conic-gradient(
    from var(--glow-angle),
    #dcfcfe,
    #9dd7e8 12%,
    #5ea8c8 28%,
    #234464 50%,
    #5ea8c8 72%,
    #9dd7e8 88%,
    #dcfcfe
  );
  -webkit-mask:
    linear-gradient(#fff 0 0) content-box,
    linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  animation: drawer-card-glow-rotate 15s linear infinite;
  pointer-events: none;
  z-index: -1;
}

@media (prefers-reduced-motion: reduce) {
  .search::before {
    animation: none;
  }
}

.search-icon {
  width: 18px;
  height: 18px;
  color: var(--soft);
  flex-shrink: 0;
}

.search .hint {
  flex-shrink: 0;
}

.write-open,
.primary-action,
.secondary-action,
.icon-button {
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  backdrop-filter: blur(20px);
  color: var(--text);
  cursor: pointer;
  transition:
    background 160ms ease,
    border-color 160ms ease,
    box-shadow 160ms ease;
}

.write-open {
  height: 44px;
  min-width: 84px;
  padding: 0 18px;
  border-color: rgba(63, 148, 186, 0.62);
  background: #3f94ba;
  color: white;
  box-shadow: 0 10px 22px rgba(63, 148, 186, 0.18);
  font-weight: 700;
  backdrop-filter: none;
}

.write-open:hover {
  border-color: rgba(63, 148, 186, 0.78);
  background: #3887aa;
  box-shadow: 0 0 0 4px rgba(63, 148, 186, 0.18);
}

/* Drafts button — distinguished from the rest of the toolbar pill
   cluster by a thin Apricity-blue border. Drafts are work-in-progress
   writes (the half-step before the primary Write CTA next to it), so
   we tint the border to mark that kinship without escalating Drafts
   into a second filled button. Translucent alpha keeps it subordinate
   to .write-open's solid fill. Sized to match .write-open exactly
   (height / padding / font) so the two buttons read as a paired
   set — the .toolbar-button base sizing was tuned for the smaller
   icon-buttons (Trash, Theme, Settings) and looked visually
   undersized next to Write. Font-weight stays at 600 (vs Write's
   700) to preserve the primary/secondary hierarchy without
   shrinking the button. */
#draftsBtn {
  border-color: rgba(63, 148, 186, 0.55);
  height: 44px;
  min-height: 44px;
  min-width: 84px;
  padding: 0 18px;
  font-size: 16px;
  font-weight: 600;
}

#draftsBtn:hover {
  border-color: rgba(63, 148, 186, 0.78);
}

.primary-action:hover,
.secondary-action:hover,
.icon-button:hover {
  background: var(--surface-strong);
  border-color: rgba(63, 148, 186, 0.28);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

/* Universal hover-enlarge + click-pop for top-row action buttons.
   Mirrors the .detail-panel-edit / .detail-panel-delete vocabulary
   (scale 1.08 + brightness on hover, .clicked-pop overshoot on
   click, scale-down on active) so the top toolbar feels cohesive
   with the floating detail-panel actions.

   Stacks with the existing .icon-button:hover halo (background tint
   + 4px accent-soft ring) — together they read as "lit AND lifted"
   instead of two competing signals. The halo gets carried along by
   the scale (box-shadow scales with the element), so the whole
   "button + glow" lifts as one piece.

   Settings gear (rotating SVG) and Notifications bell (swinging SVG
   when has-unseen) compose with the outer-frame transform without
   conflict — the inner-SVG animations target different elements
   than the button itself, so the gear keeps spinning inside the
   enlarged frame and the bell keeps swinging inside the popped frame.

   Scoped to .top-actions descendants so the pattern doesn't leak
   into trash row delete buttons, draft list actions, inline editor
   toolbar, etc. — those have their own contextual feedback and
   uniform scale would over-trigger in cramped contexts. */
.top-actions .write-open,
.top-actions .icon-button {
  transition:
    background 160ms ease,
    border-color 160ms ease,
    box-shadow 160ms ease,
    transform 200ms cubic-bezier(0.34, 1.45, 0.64, 1),
    filter 160ms ease;
}

/* Kill the visible 1px ring around top-row icon buttons in every
   state. The inherited `.icon-button { border: 1px solid var(--line) }`
   draws a soft neutral hairline that reads as a coloured outline on
   the toolbar at glance distance — even though `--line` is a low-alpha
   black, the cluster of circular pills picks up the surrounding
   surface tint and looks like a chromatic ring. Switching to a
   transparent border (keeping the 1px so layout doesn't shift between
   theme changes that might re-add it) matches the
   .detail-panel-edit / .detail-panel-delete approach: no border, just
   filled background + drop-shadow on hover.

   Scoped to .icon-button only — .write-open keeps its tinted blue
   border because that's part of the filled-CTA identity, not a
   neutral toolbar outline.

   PLUS: bare-at-rest, materialize-on-hover. At rest the icons sit
   naked over the page background — transparent background, transparent
   border, no shadow — giving a clean toolbar that weights only the
   icons themselves. On hover/focus/aria-expanded the existing
   var(--surface-strong) background (from .icon-button:hover and
   .menu-button.toolbar-button.icon-only:hover further down the file)
   fades in via the 160ms transition already on the rule. The button
   "materializes" the moment you reach for it — the lift + brightness
   + drop-shadow then have a visible surface to operate on instead
   of reading as a ghost ring appearing from nothing. Apple-toolbar /
   Linear / Vercel pattern: at rest the icons are semantically what
   they are (icons), at hover they become semantically what they are
   then (buttons). */
.top-actions .icon-button {
  border-color: transparent;
  background: transparent;
}

.top-actions .write-open:hover,
.top-actions .write-open:focus-visible,
.top-actions .icon-button:hover,
.top-actions .icon-button:focus-visible {
  transform: scale(1.08);
  filter: brightness(1.06);
  /* Override the inherited hover box-shadow on .icon-button:hover
     (4px var(--accent-soft) ring) and .write-open:hover (4px blue
     ring) with the neutral drop-shadow the .detail-panel-edit /
     .detail-panel-delete buttons use. The colored ring read as
     visually busy on small toolbar buttons — a soft drop-shadow
     gives the same "I'm lifted" signal without the chromatic
     accent fighting the scale + brightness lift. Same shadow
     values as detail-panel-edit/delete so the button family
     looks coherent. */
  box-shadow:
    0 6px 16px rgba(0, 0, 0, 0.16),
    0 2px 4px rgba(0, 0, 0, 0.08);
}

/* Inherited `.icon-button:hover` also sets border-color to the
   blue accent tint — keep it transparent on hover too so the ring
   doesn't pop back in mid-interaction. */
.top-actions .icon-button:hover,
.top-actions .icon-button:focus-visible {
  border-color: transparent;
}

/* Bell + Theme button specialcase: both carry `.menu-button` which
   triggers higher-specificity rules later in the file
   (.menu-button.toolbar-button.icon-only at 0,3,0 base + 0,4,0 hover)
   that win over the .top-actions .icon-button overrides above purely
   by source order. To beat them, we bump our selectors with the
   .top-actions descendant prefix on the same three-class chain,
   landing at 0,4,0 base + 0,5,0 hover — strictly higher specificity
   regardless of source order. Without this override, the bell + theme
   buttons keep the soft accent-tinted ring at rest AND the blue halo
   box-shadow on hover, while their trash/settings neighbours sit
   cleanly bordered. Cohesion requires all four match. */
.top-actions .menu-button.toolbar-button.icon-only,
.top-actions .menu-button.toolbar-button.icon-only:hover,
.top-actions .menu-button.toolbar-button.icon-only:focus-visible,
.top-actions .menu-button.toolbar-button.icon-only[aria-expanded="true"] {
  border-color: transparent;
}

/* Bare-at-rest for bell + theme — needs the same .top-actions
   .menu-button.toolbar-button.icon-only specificity to beat the
   .menu-button.toolbar-button.icon-only base rule (0,3,0) that sets
   `background: var(--surface)`. The hover/focus/aria-expanded rules
   below + the inherited menu-button hover styles supply the
   materialize background, so this only zeroes out the rest state. */
.top-actions .menu-button.toolbar-button.icon-only {
  background: transparent;
}

.top-actions .menu-button.toolbar-button.icon-only:hover,
.top-actions .menu-button.toolbar-button.icon-only:focus-visible,
.top-actions .menu-button.toolbar-button.icon-only[aria-expanded="true"] {
  /* Replace the inherited 4px accent-soft halo with the same neutral
     drop-shadow the icon-button hover override uses, so the press-
     down feedback is identical across the four icon buttons. */
  box-shadow:
    0 6px 16px rgba(0, 0, 0, 0.16),
    0 2px 4px rgba(0, 0, 0, 0.08);
}

/* :active scale-down — fires the moment the mouse goes down, before
   the click handler runs. Gives an immediate "I'm being pressed"
   signal that's distinct from the hover lift (lifted UP) vs press
   (pushed DOWN). Faster transition than hover so the press feels
   crisp instead of mushy. */
.top-actions .write-open:active,
.top-actions .icon-button:active {
  transform: scale(0.96);
  filter: brightness(0.96);
  transition:
    transform 80ms ease,
    filter 80ms ease;
}

/* .clicked-pop — JS adds this class on click for an overshoot
   keyframe that plays AFTER the :active release. Sequence is:
   press (scale 0.96) → release (scale 1.08 hover) → pop (1 → 1.12
   → 1 via this keyframe) → settle. The springy cubic-bezier gives
   the bounce-out feel. Overshoot is 1.12 (vs detail-panel's 1.15)
   because top-row buttons are smaller and a bigger pop would read
   as exaggerated next to their compact size. */
@keyframes top-action-click-pop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.12); }
  100% { transform: scale(1); }
}

.top-actions .write-open.clicked-pop,
.top-actions .icon-button.clicked-pop {
  animation: top-action-click-pop 0.32s cubic-bezier(0.34, 1.45, 0.64, 1);
}

@media (prefers-reduced-motion: reduce) {
  .top-actions .write-open,
  .top-actions .icon-button {
    transform: none !important;
    filter: none !important;
  }
  .top-actions .write-open.clicked-pop,
  .top-actions .icon-button.clicked-pop {
    animation: none;
  }
}

/* Global select — styled as a macOS-style popup button so every
   <select> in the app reads as native chrome instead of the OS-
   default dropdown. appearance:none strips the OS chevron + bezel;
   we paint our own chevron via background-image (SVG data URI),
   reserve right-side padding to keep the label clear of it, and
   keep the glass surface + accent focus halo the rest of the app
   uses. Individual variants (.inline-field select, .settings-
   select-control) only override sizing / chevron placement — they
   don't need to redo the appearance:none + chevron themselves. */
select {
  appearance: none;
  -webkit-appearance: none;
  height: 44px;
  padding: 0 36px 0 12px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background-color: var(--surface);
  backdrop-filter: blur(20px);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><path d='m3 5 3 3 3-3' fill='none' stroke='%236e6e73' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 12px;
  color: var(--text);
  cursor: pointer;
  outline: none;
  transition:
    border-color 160ms ease,
    box-shadow 160ms ease,
    background-color 160ms ease;
}

select:hover {
  border-color: rgba(63, 148, 186, 0.28);
  background-color: var(--surface-strong);
}

select:focus {
  border-color: rgba(63, 148, 186, 0.45);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

/* Dark-mode chevron: brighter gray so the glyph stays legible on
   the darker surface. The :not selector lives inside the @media so
   Auto mode only flips to the dark glyph when the SYSTEM actually
   prefers dark — same Auto+light gotcha that bit the settings
   switch earlier. Explicit data-theme="dark" override below covers
   the forced-dark case in either system setting. */
:root[data-theme="dark"] select {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><path d='m3 5 3 3 3-3' fill='none' stroke='%23a1a1a6' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) select {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><path d='m3 5 3 3 3-3' fill='none' stroke='%23a1a1a6' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  }
}

.search input {
  flex: 1;
  min-width: 0;
  height: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text);
  outline: none;
  font: inherit;
  font-size: 14px;
}

.search input::placeholder {
  color: var(--soft);
}

.search input::-webkit-search-cancel-button {
  appearance: none;
}

.panel {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  backdrop-filter: blur(24px);
}

/* Flexbox (not grid) for the content area so we can animate the
   detail-panel collapse smoothly. grid-template-columns animation is
   technically supported in modern browsers, but browsers can't reliably
   interpolate between mixed-unit column tracks (minmax, fr, px) — the
   transition just snaps. flex-grow + flex-basis transitions are
   universally smooth because they're simple length/number interpolations.
*/
.content-grid {
  display: flex;
  /* Wider gap (was 16px) gives each panel breathing room around it
     so the three don't visually merge into one big container — they
     read as three distinct islands floating on the page surface. */
  gap: 24px;
  align-items: stretch;
}

/* Animate `gap` only in non-resizable mode (where gap actually
   changes from 16 → 0 on detail-enlarged). Animating `gap` is
   expensive — it forces a layout recalc of every flex child every
   frame — so we restrict the transition to the one configuration that
   needs it. In `.resizable` mode `.content-grid` already has `gap: 0`,
   so there's nothing to animate. */
.content-grid:not(.resizable) {
  transition: gap 0.4s cubic-bezier(0.32, 0.72, 0, 1);
}

.content-grid > .rooms-panel {
  flex: 0.55 1 200px;
  min-width: 200px;
  /* Lean transition list — only the properties that actually change
     during detail-enlarged. min-width snaps instantly (it was 200px,
     enlarged sets 0): flex-basis owns the shrinking animation, and the
     min-width snap is invisible because flex-basis hasn't started
     transitioning yet at frame 0. Removed margin transitions because
     rooms-panel margins never change. Fewer layout-affecting transitions
     = less per-frame reflow work on the Pi during the 0.4s animation. */
  transition:
    flex-grow 0.4s cubic-bezier(0.32, 0.72, 0, 1),
    flex-basis 0.4s cubic-bezier(0.32, 0.72, 0, 1),
    opacity 0.3s ease,
    transform 0.42s cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* FLEX properties use a non-overshoot curve so the panel widths don't
   oscillate (which would cause line-clamped .drawer-preview text to
   reflow at every overshoot frame — visible "stutter"). The bouncy
   spring is preserved on the detail-panel's transform (translateX +
   scale pop) — transform doesn't affect siblings' layout, so the
   visual bounce never touches the drawers panel's width. The drawers
   panel matches the detail panel's 0.4s layout duration so both panes
   feel coordinated; the visible reflow on the side is softened by the
   right-edge fade-mask on .drawer-preview below (Apple's trick on
   scrolling list edges — reflowing characters dissolve into the right
   margin instead of getting hard-cropped). */
.content-grid > .drawers-panel {
  flex: 0.95 1 320px;
  min-width: 0;
  /* Same trim as rooms-panel: drop margin transitions (margins never
     change on this panel) so per-frame layout work is minimized. */
  transition:
    flex-grow 0.4s cubic-bezier(0.32, 0.72, 0, 1),
    flex-basis 0.4s cubic-bezier(0.32, 0.72, 0, 1),
    opacity 0.3s ease,
    transform 0.42s cubic-bezier(0.34, 1.3, 0.64, 1);
}

.content-grid > .detail-panel {
  flex: 1.5 1 380px;
  min-width: 0;
  transform-origin: right center;
  transition:
    flex-grow 0.4s cubic-bezier(0.32, 0.72, 0, 1),
    flex-basis 0.4s cubic-bezier(0.32, 0.72, 0, 1),
    opacity 0.3s ease,
    margin-left 0.4s cubic-bezier(0.32, 0.72, 0, 1),
    transform 0.42s cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* Empty-state mode: detail panel collapses to 0 width AND slides off
   the right edge via translateX, so the slide-out is visually obvious
   even if flex-basis animation happens to skip frames. The drawers
   panel grows into the freed space simultaneously (its flex-grow
   bumps from 0.95 → 2.45 = 0.95 + 1.5 to claim detail's old share).
   The negative margin-left on detail absorbs the 16px gap that would
   otherwise leave dead space between drawers and the collapsed panel. */
.content-grid.detail-hidden > .drawers-panel {
  flex-grow: 2.45;
}

.content-grid.detail-hidden > .detail-panel {
  flex-grow: 0;
  flex-basis: 0;
  margin-left: -16px;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  /* Visible "pop-in" spring lives here, on transform — not flex. The
     panel slides 64px from the right AND scales up from 0.92 so when
     it returns to translateX(0) scale(1) with the bouncy curve below
     (cubic-bezier(0.34, 1.3, 0.64, 1)), the user sees a clearly
     elastic pop instead of a flat slide. Transform doesn't trigger
     reflow on siblings, so this overshoot is "free" — the drawers
     panel's line-clamped previews don't restyle. */
  transform: translateX(64px) scale(0.92);
}

/* ----- Enlarged state ----- The mirror of detail-hidden: instead of
   detail collapsing while rooms+drawers stay visible, rooms+drawers
   collapse off the LEFT edge while detail expands to fill the entire
   content row. Same flex-basis-collapses-to-0 trick, with a translateX
   slide so the panels visibly move out (not just shrink). Container
   gap is also collapsed to 0 so the 16px×3 inter-panel gaps don't leave
   dead whitespace on the left edge of the now-full-width detail panel.
   (Earlier draft used negative margins on rooms+drawers to close the
   gaps — but negative margins add to the flex free-space calculation,
   so detail-panel grew ~64px past the container right edge.) */
.content-grid.detail-enlarged {
  gap: 0;
}

.content-grid.detail-enlarged > .rooms-panel,
.content-grid.detail-enlarged > .drawers-panel {
  flex-grow: 0;
  flex-basis: 0;
  min-width: 0;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  transform: translateX(-40px);
  /* GPU layer hint — promotes these panels to their own composite
     layer while they slide out, so transform/opacity changes are
     handled on the GPU rather than re-painting from main thread. On a
     Pi this is the difference between buttery and choppy. */
  will-change: transform, opacity;
}

/* Snap, don't animate: when virtualization just swapped the cards
   under the cursor, any new card landing in the hovered position
   would otherwise fire its 220ms hover-spring from scratch and
   produce visible flicker. This class (added for exactly one paint
   frame in renderDrawerWindow) freezes the spring so the card
   appears already-hovered without a transition. Removed on the next
   rAF so deliberate hovers after scroll stops still get the spring. */
.drawer-list.no-card-transition .drawer-card,
.drawer-list.no-card-transition .drawer-card * {
  transition: none !important;
}

/* Click-pop tactile feedback. Earlier the same "pop" effect happened
   as a side effect of the click triggering a full re-render — when
   the freshly-rendered card with .active class landed under the
   cursor, its hover spring transform fired from scratch and read as
   a bounce. The current renderer doesn't re-render on click (we just
   flip the .active class via classList for perf), so we restore the
   tactile feedback explicitly via this keyframe: a quick scale up to
   1.04 with overshoot ease, then back to the resting state. Sits on
   TOP of any hover transform (hover stays in effect) because the
   animation is one-shot and reverts on completion. */
@keyframes drawer-card-click-pop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.04); }
  100% { transform: scale(1); }
}

.drawer-card.clicked-pop {
  animation: drawer-card-click-pop 0.32s cubic-bezier(0.34, 1.45, 0.64, 1);
}

/* Don't run the pop for users who've opted out of UI animation. */
@media (prefers-reduced-motion: reduce) {
  .drawer-card.clicked-pop {
    animation: none;
  }
}

/* Waterfall stagger — runs whenever the memory list's *content* changes
   (wing/room switch, search query, sort change, fresh data load). Each
   card drops in from slightly above with a small scale-in, lands with
   the same spring overshoot used by every other tactile chrome motion
   (cubic-bezier(0.34, 1.3, 0.64, 1)). The per-card delay is set inline
   by renderDrawerWindow so the visible window fills top-to-bottom in a
   brief cascade. Scroll-only re-renders DON'T add this class (see
   state._waterfallNext gating in renderDrawerWindow) so scrolling stays
   buttery — only genuine list changes trigger the cascade.

   The animation runs on a wrapper element (.drawer-card-wrap) NOT on
   the card itself — because animations win over transitions on the
   same property, animating .drawer-card's transform would block the
   .drawer-card:hover lift's transition for the duration of the
   cascade. With the wrap as a separate transform context, the wrap
   handles the drop-in and the card inside it handles hover lifting
   independently. The two transforms stack naturally because they're
   on different elements. */
.drawer-card-wrap {
  display: contents;
}
.drawer-card-wrap.waterfall {
  display: block;
  animation: drawer-card-waterfall 0.32s cubic-bezier(0.34, 1.3, 0.64, 1) both;
  will-change: transform, opacity;
}

@keyframes drawer-card-waterfall {
  from {
    opacity: 0;
    transform: translateY(-14px) scale(0.97);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .drawer-card-wrap.waterfall {
    animation: none;
  }
}

.content-grid.detail-enlarged > .detail-panel {
  /* Claim everything: every other panel's flex-grow is 0 in this
     state, so detail gets all the free space. Sum-of-grows: detail's
     10 vs others' 0 → detail spans the full content row. */
  flex-grow: 10;
}

.content-grid.detail-enlarged > .col-resizer {
  flex-basis: 0;
  opacity: 0;
  pointer-events: none;
  transition: flex-basis 0.4s cubic-bezier(0.32, 0.72, 0, 1), opacity 0.3s ease;
}

/* Resizable variant: user has explicit widths via CSS vars (set by the
   col-resizer drag handles). Same flex model, but rooms/drawers get
   fixed flex-basis from the vars instead of the default fr ratios. */
.content-grid.resizable {
  gap: 0;
}

.content-grid.resizable > .rooms-panel {
  flex: 0 0 var(--rooms-w);
}

.content-grid.resizable > .drawers-panel {
  flex: 0 0 var(--drawers-w);
}

.content-grid.resizable > .detail-panel {
  flex: 1 1 0;
}

.content-grid.resizable > .col-resizer {
  flex: 0 0 var(--resizer-w);
}

.content-grid.resizable.detail-hidden > .drawers-panel {
  flex-grow: 1;
  flex-basis: 0;
}

.content-grid.resizable.detail-hidden > .detail-panel {
  flex: 0 0 0;
}

/* Resizable-mode counterparts for .detail-enlarged: the base
   `.content-grid.resizable > .rooms-panel { flex: 0 0 var(--rooms-w) }`
   pins panels at their user-resized pixel widths, which would otherwise
   override the single-class .detail-enlarged collapse and leave the
   detail panel unable to grow (panels invisible via transform/opacity
   but still occupying layout space). These double-class rules zero out
   flex-basis so the layout space actually frees up. */
.content-grid.resizable.detail-enlarged > .rooms-panel,
.content-grid.resizable.detail-enlarged > .drawers-panel {
  flex: 0 0 0;
}

.content-grid.resizable.detail-enlarged > .detail-panel {
  flex: 1 1 0;
}

.content-grid.resizable.detail-enlarged > .col-resizer {
  flex: 0 0 0;
}

/* ---------- Browse mode (Memories panel maximized) ----------
   Mirror of .detail-enlarged but with .drawers-panel claiming the
   freed space. Triggered by the new traffic-light capsule on the
   Memories panel header (state.drawersEnlarged → updateGridLayout).
   The two enlarged states are mutually exclusive — opening Detail
   forces browse off, entering browse forces detail-enlarged off
   (see updateGridLayout in app.js). In this state Rooms slides off
   the left edge using the same translate+fade as detail-enlarged's
   rooms collapse, Drawers expands to claim the full content row, and
   the .drawer-list switches to a 4-column grid so the user can scan
   many memories at once. */
.content-grid.drawers-enlarged {
  gap: 0;
}

.content-grid.drawers-enlarged > .rooms-panel {
  flex-grow: 0;
  flex-basis: 0;
  min-width: 0;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  transform: translateX(-40px);
  will-change: transform, opacity;
}

.content-grid.drawers-enlarged > .drawers-panel {
  /* Same flex-grow:10 outvote trick as detail-enlarged: every other
     panel's flex-grow is 0 in this state, so drawers gets the row. */
  flex-grow: 10;
}

.content-grid.drawers-enlarged > .detail-panel {
  /* Symmetric exit on the right: same translate+fade as Rooms uses on
     the left, just mirrored to translateX(+40px) so Detail slides OUT
     to the right edge. State.selectedDrawerId persists during browse
     mode, so when the user exits browse, this panel slides back from
     translateX(40px)→0 with the previously-open memory still in view
     — no need to re-select. */
  flex-grow: 0;
  flex-basis: 0;
  min-width: 0;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  transform: translateX(40px);
  will-change: transform, opacity;
}

.content-grid.drawers-enlarged > .col-resizer {
  flex-basis: 0;
  opacity: 0;
  pointer-events: none;
  transition: flex-basis 0.4s cubic-bezier(0.32, 0.72, 0, 1), opacity 0.3s ease;
}

/* Resizable-mode counterpart — same logic as .resizable.detail-enlarged
   but for the drawers-enlarged state. Without these the user-saved
   pixel widths from col-resizer drags would override the collapse and
   leave panels occupying layout space. */
.content-grid.resizable.drawers-enlarged > .rooms-panel,
.content-grid.resizable.drawers-enlarged > .detail-panel {
  flex: 0 0 0;
}

.content-grid.resizable.drawers-enlarged > .drawers-panel {
  flex: 1 1 0;
  flex-grow: 10;
}

.content-grid.resizable.drawers-enlarged > .col-resizer {
  flex: 0 0 0;
}

/* 4-column grid for the memory cards in browse mode. minmax(0, 1fr)
   lets columns shrink below content width so we never horizontally
   overflow during the panel-width transition. The vertical gap stays
   on the existing .drawer-list rule so single-col and browse layouts
   share the same row rhythm. In browse mode the JS renderer bypasses
   virtualization (see renderDrawerWindow) and dumps all cards into
   the grid; for libraries under a few hundred memories this is
   trivially fast, and it sidesteps the columns-aware-virtualization
   refactor that a true windowed grid would need. */
.content-grid.drawers-enlarged .drawer-list {
  /* Column count comes from a JS-set custom property so the grid can
     adapt to the filtered drawer count when the adaptive_browse pref
     is on (see adaptiveBrowseCols in app.js — 11-24 cards = 2 cols,
     25-50 = 3, 51+ = 4; below 11 the maximize toggle is hidden
     outright so browse mode isn't reachable). Fallback of 4 matches
     the original fixed layout for any path that doesn't set the
     property (e.g. adaptive_browse=false). */
  grid-template-columns: repeat(var(--browse-cols, 4), minmax(0, 1fr));
}

/* The Memories actions capsule mirrors Detail's: floating top-left
   chrome, red close + green/yellow maximize. Same visual class as
   Detail (.detail-panel-actions) for shared styling, plus a modifier
   class (.drawers-actions) for browse-mode-specific overrides. */
.drawers-actions {
  /* Inherits position:absolute, top:-10px, left:-10px, etc. from the
     base .detail-panel-actions rule — no overrides needed for the
     capsule itself. This rule exists to anchor browse-mode behavior. */
}

/* Maximize button on Memories flips green → yellow when browse mode
   is active, identical visual to Detail's enlarged state. Scoped to
   `.drawers-panel` ancestor so this rule doesn't accidentally style
   Detail's maximize button (the two panels' buttons share class
   names by design for visual consistency). */
.content-grid.drawers-enlarged .drawers-panel .detail-panel-maximize {
  background: #fac800;
  color: #7e640c;
}

/* Adaptive browse: when the filtered drawer count is below the hide
   threshold (see adaptiveBrowseCols in app.js), the toggle isn't a
   useful affordance — single-column scroll handles a short list fine.
   Fade out the button AND its surrounding capsule together so the
   memories-panel corner is clean (the .drawers-actions capsule only
   ever contains this one button, so when the button is hidden the
   capsule itself is empty chrome). Uses :has() to keep the JS to a
   single classList.toggle on the button; the capsule reacts via CSS.
   On browsers without :has() (very old Safari / Firefox <121) the
   capsule shows as a tiny empty pip — not broken, just slightly ugly. */
.drawers-panel .detail-panel-maximize.is-hidden {
  opacity: 0;
  pointer-events: none;
  transform: scale(0.8);
  transition:
    opacity 0.18s ease,
    transform 0.22s cubic-bezier(0.34, 1.3, 0.64, 1);
}
.drawers-panel .drawers-actions:has(.detail-panel-maximize.is-hidden) {
  opacity: 0;
  pointer-events: none;
  transform: scale(0.8);
  transition:
    opacity 0.18s ease,
    transform 0.22s cubic-bezier(0.34, 1.3, 0.64, 1);
}

.content-grid.drawers-enlarged .drawers-panel .detail-panel-maximize .icon-maximize {
  opacity: 0;
  transform: scale(0.6);
}

.content-grid.drawers-enlarged .drawers-panel .detail-panel-maximize .icon-minimize {
  opacity: 1;
  transform: scale(1);
}

.content-grid.detail-hidden .col-resizer:last-of-type {
  flex-basis: 0;
  pointer-events: none;
  opacity: 0;
  transition: flex-basis 0.4s cubic-bezier(0.32, 0.72, 0, 1), opacity 0.3s ease;
}

@media (prefers-reduced-motion: reduce) {
  .content-grid > .drawers-panel,
  .content-grid > .detail-panel,
  .content-grid .col-resizer {
    transition: none;
  }
}

.panel {
  min-width: 0;
  overflow: hidden;
}

.rooms-panel,
.drawers-panel,
.detail-panel {
  height: clamp(560px, calc(100vh - 286px), 760px);
  /* position:relative anchors the absolute-positioned floating close X
     (.detail-panel-close) to this corner instead of letting it fall
     through to the next-positioned ancestor (the body). */
  position: relative;
}

.rooms-panel,
.drawers-panel,
.detail-panel {
  display: flex;
  flex-direction: column;
}

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 18px;
  /* Locked to the same content height + box model as .detail-meta on
     the sibling Detail panel (content-box, 41px content, 18px padding,
     1px bottom border) so the two dividers line up horizontally across
     panels that start at the same Y. The header's content (h2 title +
     count line, or the Wings/Rooms drill title) fits inside 41px; the
     count line's own min-height is set on .panel-header p. */
  box-sizing: content-box;
  height: 41px;
  border-bottom: 1px solid var(--line);
}

.panel-header h2 {
  font-size: 17px;
}

.panel-header p {
  margin-top: 4px;
  font-size: 13px;
  /* Reserve the count line's vertical space even while it's empty.
     #drawerCount / #roomCount start blank and get filled by JS
     ("95 visible", "5 wings") on first render — without a reserved
     height the empty <p> collapses to 0, then expands when the text
     lands, pushing the header's bottom border + the Sort control +
     the whole list DOWN. Pinning line-height + min-height keeps the
     header height identical before and after the count populates, so
     nothing shifts on reload. */
  line-height: 1.3;
  min-height: calc(1.3em);
}

.room-nav {
  display: grid;
  grid-template-columns: 1fr;
  align-content: start;
  gap: 4px;
  padding: 12px;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.room-row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 4px;
  /* Persistent virtualization — same content-visibility:auto trick as
     .drawer-card. Rooms-panel often has 30+ rooms; off-screen rooms
     get fully skipped, keeping per-frame layout cost flat regardless
     of room count. The 46px is .room-item's 42px min-height + the
     4px row gap, matching the rendered row size so the rooms-panel
     scrollbar stays accurate. */
  content-visibility: auto;
  contain-intrinsic-size: auto 46px;
}

.room-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 42px;
  min-width: 0;
  border: 1px solid transparent;
  border-radius: 12px;
  padding: 0 12px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  text-align: left;
  /* Font comes from the global "button { font: inherit }" reset.
     No explicit size/weight here — the iOS drill-down wings use
     the inherited body font naturally, and the active state
     highlight does the visual work without needing typographic
     bumps. */
  /* Hover / active swaps animate the background gently so the row
     doesn't snap. Earlier diagnosis blamed this transition for the
     "phantom highlight on the row above" bug, but the actual cause
     was renderRooms's innerHTML wipe destroying live DOM mid-click
     — fixed in the fast-path branch of renderRooms (see app.js).
     With the wipe gone for activation-only changes, animating
     background is safe again and the row tints feel less jumpy. */
  transition:
    background 0.14s ease,
    border-color 0.14s ease,
    transform 0.16s cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* Hover uses --accent-soft (theme-aware: 11% accent in light, 18% in
   dark) instead of --surface-strong, which was nearly invisible
   against the panel's own translucent surface in light mode. The
   accent tint reads clearly in both themes. */
.room-row:hover .room-item,
.room-item:hover {
  background: var(--accent-soft);
}

.room-row.active .room-item,
.room-item.active {
  background: var(--accent-soft);
  border-color: rgba(63, 148, 186, 0.32);
}

/* :active press feedback — fires while the click is being registered,
   before the panel slide kicks in. The brief scale-down + stronger
   accent flash gives an "I got it" beat so taps don't feel dead.
   color-mix gives a stronger accent tint than --accent-soft without
   needing a separate token. */
.room-item:active {
  transform: scale(0.985);
  background: color-mix(in srgb, var(--accent) 22%, transparent);
}

.room-item span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.room-item strong {
  flex: 0 0 auto;
  color: var(--soft);
  font-size: 13px;
}

/* ---------- Tunnel indicator on room rows ----------
   Inert visual badge inside .room-item, sitting just before the memory-
   count <strong>. Indicates "this room has tunnels — open it to see them
   on the right". The actual tap target is the parent .room-item button;
   renderDetail's dispatch handles the rest. */
.room-item-end {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: 0 0 auto;
}

.room-tunnel-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  color: var(--accent);
  pointer-events: none;
}

.room-tunnel-icon svg {
  width: 14px;
  height: 14px;
}

.room-nav.editing .room-tunnel-icon {
  display: none;
}

/* ---------- Rooms panel: drill-down navigation ----------
   The Rooms panel renders one of two views at a time:

     • WINGS view (state.selectedWing === "all"):
       "All Memory" header row + each wing as a tappable drill-in row
       (right-chevron hints "tap to drill in"). Tapping a wing fires
       the iOS-style push transition that swaps the panel content for
       the rooms-of-wing view.

     • ROOMS-of-WING view (state.selectedWing !== "all"):
       "← Back to wings" bar at top (same affordance as the tunnel
       inspector's back), wing name + count as a subtitle, "All rooms"
       pseudo-row, then the per-room list. Tapping back fires the
       inverse pop transition.

   The wing list / room list both use the existing .room-row / .room-item
   shells — only the per-row trailing affordance (count + drill chevron
   vs count + tunnel indicator) differs.

   Sliding uses the same cubic-bezier curve as the detail-panel push-pop
   (see .detail.detail-enter-forward / -backward in the detail-pane
   animations block further down) so the motion language is uniform. */

/* All Memory / Wings / Rooms typography — all share .room-item's
   font-size:14 + weight:500 base. Hierarchy is communicated via:
   • Indent (rooms sit 22px further right than wing headers)
   • Disclosure triangle on wings (rooms don't have one)
   • Active highlight (accent-soft bg + border)
   • Muted label colour on rooms (subtler text)
   Following Apple Mail/Notes/Finder — uniform sidebar typography,
   structure-only hierarchy. */

/* "All Memory" pseudo-row — always-active disclosure showing the
   total drawer count, sits at the top of the Wings view. Disabled
   button (can't be re-clicked) so the visual active treatment
   doesn't invite an action that wouldn't change anything. */
.all-memory-row {
  margin-bottom: 4px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
}
.all-memory-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  min-height: 42px;
  border: 1px solid transparent;
  border-radius: 12px;
  padding: 0 12px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  text-align: left;
  font-weight: 600;
}
.all-memory-item:hover,
.all-memory-row.active .all-memory-item {
  background: var(--accent-soft);
}
.all-memory-row.active .all-memory-item {
  border-color: rgba(63, 148, 186, 0.32);
}
.all-memory-item strong {
  flex: 0 0 auto;
  color: var(--soft);
  font-size: 13px;
  font-weight: 600;
}
.all-memory-row.active .all-memory-item strong {
  color: var(--accent);
}
.all-memory-item:disabled {
  cursor: default;
}

/* Wing drill rows (wings view only): chrome identical to .room-item
   plus a trailing chevron-right hint signalling "tap to drill in". */
.wing-drill-item .wing-drill-chevron {
  display: inline-block;
  color: var(--soft);
  font-size: 14px;
  line-height: 1;
  margin-left: 3px;
  transform: translateY(-1px);
  /* Subtle nudge-right on hover reinforces "this leads to a sub-view"
     — same gesture vocabulary as iOS UINavigationController push. */
  transition: color 0.14s ease, transform 0.18s cubic-bezier(0.34, 1.3, 0.64, 1);
}
.wing-drill-item:hover .wing-drill-chevron {
  color: var(--accent);
  transform: translate(1px, -1px);
}
.wing-drill-item:active .wing-drill-chevron {
  transform: translate(3px, -1px);
}

/* Back-bar at the top of the rooms-of-wing view. Same eyebrow
   typography as the detail-pane's back bar — uppercase, tracked,
   accent-coloured — so the affordance reads as part of the same
   navigation system. */
.room-nav-back-bar {
  padding: 0 4px 6px;
}
.room-nav-back-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px 4px 2px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: opacity 0.12s ease, transform 0.18s cubic-bezier(0.34, 1.3, 0.64, 1);
}
.room-nav-back-btn:hover { opacity: 0.7; }
.room-nav-back-btn:active { transform: translateX(-3px); }
.room-nav-back-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.room-nav-back-arrow {
  display: inline-block;
  font-size: 16px;
  font-weight: 600;
  line-height: 1;
}

/* The wing-name subtitle that sits under the back bar in rooms view. */
.room-nav-wing-header {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 6px 10px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--line);
}
.room-nav-wing-header strong {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
}
.room-nav-wing-header span {
  font-size: 12px;
  color: var(--muted);
}

/* ---------- Drill-down slide animation ----------
   Forward slide (drill into a wing): content enters from the right.
   Backward slide (back to wings): content enters from the left.
   Same easing curve + duration as detail-slide-in-forward/backward
   in the detail-pane block — UIKit kCAMediaTimingFunctionEaseInEaseOut. */
@keyframes rooms-nav-slide-in-forward {
  from { transform: translateX(20px); opacity: 0; }
  to   { transform: translateX(0);    opacity: 1; }
}
@keyframes rooms-nav-slide-in-backward {
  from { transform: translateX(-20px); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}
.room-nav.rooms-nav-enter-forward {
  animation: rooms-nav-slide-in-forward 0.28s cubic-bezier(0.32, 0.72, 0, 1);
}
.room-nav.rooms-nav-enter-backward {
  animation: rooms-nav-slide-in-backward 0.28s cubic-bezier(0.32, 0.72, 0, 1);
}
@media (prefers-reduced-motion: reduce) {
  .room-nav.rooms-nav-enter-forward,
  .room-nav.rooms-nav-enter-backward {
    animation: none;
  }
}

.drawer-list {
  display: grid;
  align-content: start;
  gap: 10px;
  flex: 1;
  min-height: 0;
  overflow: auto;
  /* Roomier horizontal inset than the rooms panel — cards visually shrink
     in from the panel edge, AND the hover scale(1.015) lift has clear
     headroom before the card silhouette gets uncomfortably close to the
     rounded panel border. Vertical stays compact so the list still fits
     plenty of cards in the visible window. */
  padding: 12px 20px;
}

.drawer-card {
  position: relative;
  display: grid;
  /* Single column — the per-card dot-menu slot is gone now that edit
     and delete (via Backspace) both live in the right-pane chrome.
     Fixed grid-template-rows so the kicker / title / preview slots
     are predictable, and the card's total height is exact (matching
     the VIRT_CARD_HEIGHT constant in app.js). */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto 1fr;
  gap: 8px;
  width: 100%;
  /* Fixed height so the JS virtualization math (VIRT_CARD_HEIGHT =
     140) maps row index → pixel offset exactly. Without this, real
     card heights drift from the placeholder and the scrollbar
     misaligns / cards jitter during fast scroll. The title is
     forced to a single line (see .drawer-card strong below) and the
     preview is line-clamped to 3 lines, so 140px reliably fits:
     padding (16) + kicker (~16) + gap (8) + title (~22) + gap (8)
     + 3-line preview (~60) = 130, with 10px slack. */
  height: 140px;
  overflow: hidden;
  border: 1px solid transparent;
  border-radius: 16px;
  padding: 8px;
  /* Fully opaque background + a small close-drop shadow gives each
     card a clear "floating" silhouette in light mode where 58%-alpha
     white blended into the near-white panel background. Subtle by
     default; the .active state still owns the accent glow. */
  background: #ffffff;
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.04),
    0 4px 12px rgba(15, 15, 30, 0.06);
  color: var(--text);
  text-align: left;
  /* `will-change: transform` hints the GPU to keep the card on its own
     compositor layer so the hover lift stays buttery — no layout
     recalculation per frame. */
  will-change: transform;
  /* Spring-out easing on transform makes the lift feel tactile rather
     than mechanical — the card "settles" into the lifted position. */
  transition:
    box-shadow 0.18s ease,
    background 0.14s ease,
    border-color 0.14s ease,
    transform 0.22s cubic-bezier(0.34, 1.3, 0.64, 1);
}

.drawer-select {
  display: grid;
  gap: 8px;
  min-width: 0;
  border: 0;
  border-radius: 12px;
  padding: 6px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  text-align: left;
}

/* Hover = card physically lifts (translateY + small scale) and the
   shadow strengthens to sell the elevation. Spring easing on transform
   (set on .drawer-card base) gives it a tactile settle. The lift
   COMPOSES with --waterfall-y (the cascade offset) via calc, so
   hovering a card mid-cascade smoothly stacks the hover lift on top
   of the current animated position rather than fighting it. */
.drawer-card:hover {
  background: white;
  transform: translateY(-2px) scale(1.015);
  box-shadow:
    0 4px 10px rgba(0, 0, 0, 0.06),
    0 14px 32px rgba(15, 15, 30, 0.12);
}

/* Custom @property so we can animate the angle of a conic-gradient
   on the active card's glow pseudo-element. Registered properties
   are required for transitioning/animating CSS values that aren't
   normally animatable (like an angle inside a gradient stop). Chrome
   85+, Safari 16.4+, Firefox 128+. */
@property --glow-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

/* Selected card: simple static accent halo. The rotating prism ring
   was moved off the card and onto the search bar — having it on every
   selected card AND the search at once was too much motion in view.
   The card keeps a quiet 4px accent-soft shadow as its "I'm selected"
   signal; the search bar carries the alive/listening rotation. */
.drawer-card.active {
  border-color: rgba(63, 148, 186, 0.22);
  background: #ffffff;
  box-shadow: 0 0 0 4px var(--accent-soft);
}

/* @keyframes for the conic-gradient rotation, now consumed by the
   .search::before ring in the top bar. */
@keyframes drawer-card-glow-rotate {
  to { --glow-angle: 360deg; }
}

/* Hover on the already-selected card: don't re-lift via the hover
   transform. The active card has settled into its "I am chosen"
   state and bouncing it on every cursor pass undoes the click-pop's
   clean ending — the pop animation finishes at scale(1) and the
   cursor still being there would otherwise flip it to scale(1.015),
   reading as a buggy jump. The rotating ring is the selected-state
   signal; the card stays put. */
.drawer-card.active:hover {
  transform: none;
}

/* Respect users who've opted out of UI animation — the lift is
   feedback, not essential, so just skip the transform. */
@media (prefers-reduced-motion: reduce) {
  .drawer-card:hover,
  .drawer-card.active:hover {
    transform: none;
  }
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .drawer-card {
    background: rgba(44, 44, 46, 0.62);
  }

  /* Dark-mode hover: heavier black shadow because 6% black against a
     dark panel reads as essentially no shadow. The lift transform
     comes from the light-mode :hover rule (transform is theme-
     independent, no need to redeclare). */
  :root:not([data-theme="light"]) .drawer-card:hover {
    background: rgba(58, 58, 60, 0.92);
    box-shadow:
      0 4px 12px rgba(0, 0, 0, 0.36),
      0 16px 36px rgba(0, 0, 0, 0.28);
  }

  /* Dark-mode active card: brighter surface (matches hover bg) plus a
     subtle accent border so the selection reads against the dark
     panel. The accent-soft shadow halo is theme-aware via the dark
     --accent-soft token. */
  :root:not([data-theme="light"]) .drawer-card.active {
    background: rgba(58, 58, 60, 0.92);
    border-color: rgba(63, 148, 186, 0.42);
  }
}

:root[data-theme="dark"] .drawer-card {
  background: rgba(44, 44, 46, 0.62);
}

:root[data-theme="dark"] .drawer-card:hover {
  background: rgba(58, 58, 60, 0.92);
  box-shadow:
    0 4px 12px rgba(0, 0, 0, 0.36),
    0 16px 36px rgba(0, 0, 0, 0.28);
}

:root[data-theme="dark"] .drawer-card.active {
  background: rgba(58, 58, 60, 0.92);
  border-color: rgba(63, 148, 186, 0.42);
}

.menu-wrap {
  position: relative;
  display: grid;
  align-self: center;
}

.drawer-menu-wrap {
  align-self: start;
}

.menu-button {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border: 1px solid transparent;
  border-radius: 10px;
  background: transparent;
  color: var(--soft);
  cursor: pointer;
  line-height: 1;
  transition: background 0.18s ease, color 0.18s ease, transform 0.18s ease;
}

/* When .menu-button is combined with .toolbar-button.icon-only (the
   theme toggle in the top bar), let the toolbar sizing/shape WIN
   over the compact menu-button defaults — otherwise the theme
   button would be a 32×32 transparent square while its trash/
   settings neighbours are 44×44 surface-filled pills with the icon
   visually inside a circle. Raising specificity with three classes
   beats the single-class .menu-button rule above. */
.menu-button.toolbar-button.icon-only {
  width: 44px;
  height: 44px;
  min-height: 44px;
  padding: 0;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--text);
}
/* And when it's open / pressed, fall back to the same accent-soft
   hover tint the other toolbar icon-only buttons use (instead of
   the compact menu-button's accent-soft + bare-transparent treatment). */
.menu-button.toolbar-button.icon-only:hover,
.menu-button.toolbar-button.icon-only:focus-visible,
.menu-button.toolbar-button.icon-only[aria-expanded="true"] {
  background: var(--surface-strong);
  border-color: rgba(63, 148, 186, 0.28);
  box-shadow: 0 0 0 4px var(--accent-soft);
  color: var(--text);
  outline: none;
}

/* Theme-button icon: both sun + moon ship in static HTML (#themeIcon);
   these rules show exactly ONE based on the RESOLVED theme, so the
   correct glyph is present at first paint with no JS innerHTML pop-in
   (that empty-svg-filled-by-JS was the theme-button reload flicker).
   #themeIcon is an inline-flex box so the chosen svg centers in the
   button the same way the old single svg did. */
#themeIcon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
#themeIcon svg {
  width: 18px;
  height: 18px;
}
/* Base (explicit light, AND auto-with-light-system): sun shown. */
#themeIcon .theme-icon-sun { display: block; }
#themeIcon .theme-icon-moon { display: none; }
/* Explicit dark → moon. */
:root[data-theme="dark"] #themeIcon .theme-icon-sun { display: none; }
:root[data-theme="dark"] #themeIcon .theme-icon-moon { display: block; }
/* Auto (no data-theme attr) + dark system → moon. The head script only
   sets data-theme for explicit light/dark, so `:not([data-theme])`
   precisely matches auto mode and defers to the OS preference here. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) #themeIcon .theme-icon-sun { display: none; }
  :root:not([data-theme]) #themeIcon .theme-icon-moon { display: block; }
}

.menu-button.compact {
  width: 30px;
  height: 30px;
}

.menu-button .menu-dots {
  width: 18px;
  height: 18px;
  fill: currentColor;
}

.menu-button:hover,
.menu-button:focus-visible {
  background: var(--accent-soft);
  color: var(--accent);
  outline: none;
}

.menu-button[aria-expanded="true"] {
  background: var(--accent-soft);
  color: var(--accent);
}

.action-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 15;
  min-width: 184px;
  padding: 6px;
  display: grid;
  gap: 2px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface-strong);
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.05),
    0 12px 32px rgba(0, 0, 0, 0.14);
  backdrop-filter: blur(24px);
  transform-origin: top right;
  animation: menuPop 0.14s ease-out;
}

@keyframes menuPop {
  from {
    opacity: 0;
    transform: scale(0.96) translateY(-2px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

.action-item {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 36px;
  padding: 0 10px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  cursor: pointer;
  text-align: left;
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
  transition: background 0.14s ease, color 0.14s ease;
}

.action-item .action-icon {
  width: 17px;
  height: 17px;
  flex: 0 0 17px;
  color: var(--muted);
  transition: color 0.14s ease;
}

.action-item:hover,
.action-item:focus-visible {
  background: var(--accent-soft);
  color: var(--accent);
  outline: none;
}

.action-item:hover .action-icon,
.action-item:focus-visible .action-icon {
  color: var(--accent);
}

/* Destructive action styling — all "delete" / "remove forever" / etc
   surfaces use --danger (#fe5f63) so the meaning reads consistently
   across the dashboard. Soft alpha variants for hover backgrounds
   come from --danger-soft. Same color in light and dark mode (the
   value is vivid enough to work in both); the .danger-soft alpha
   bumps slightly in dark mode for adequate contrast on dark
   surfaces. */
.danger-menu,
.danger-menu .action-icon {
  color: var(--danger);
}

.danger-menu:hover,
.danger-menu:focus-visible {
  background: var(--danger-soft);
  color: var(--danger);
}

.danger-menu:hover .action-icon,
.danger-menu:focus-visible .action-icon {
  color: var(--danger);
}

.drawer-kicker {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  /* Same single-line reason as the title — keep the kicker exactly
     one row tall so the card's fixed 140px height holds. The two
     inner spans (location + date) each get their own ellipsis so
     either side can truncate independently. */
  white-space: nowrap;
  overflow: hidden;
}
.drawer-kicker > span {
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.drawer-kicker > .drawer-date {
  flex-shrink: 0;
}

/* Recently-updated card: accent-tinted gradient overlay along the
   right edge. Subtle, doesn't compete with content, signals "this
   one is fresh" at a glance. Pseudo-element overlay (not background
   change) so we can transform it independently. pointer-events
   none so clicks pass through. The gradient peaks at the right
   edge and dissolves leftward by ~70% across the card, so the
   muted text in the kicker (always near the right edge) has its
   contrast tuned by the tint opacity. */
.drawer-card.is-updated::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    to left,
    color-mix(in srgb, var(--accent) 24%, transparent) 0%,
    color-mix(in srgb, var(--accent) 12%, transparent) 35%,
    transparent 70%
  );
  /* Drain animation: when .is-fading flips on, the overlay slides
     rightward off the card edge over 0.45s. Easing is decelerate
     so it starts slow (still tinted), accelerates as it leaves —
     reads as "the colour is draining out" rather than fading. */
  transform: translateX(0);
  opacity: 1;
  transition:
    transform 0.45s cubic-bezier(0.4, 0, 0.2, 1),
    opacity 0.45s ease;
  z-index: 1;
}
.drawer-card.is-updated.is-fading::after {
  transform: translateX(60%);
  opacity: 0;
}
/* Fallback for engines without color-mix() — slightly translucent
   --accent-soft, close enough to the gradient feel. */
@supports not (background: color-mix(in srgb, red 24%, transparent)) {
  .drawer-card.is-updated::after {
    background: linear-gradient(
      to left,
      rgba(63, 148, 186, 0.26) 0%,
      rgba(63, 148, 186, 0.13) 35%,
      transparent 70%
    );
  }
}
/* Dark mode: drop the tint intensity a touch so the muted timestamp
   text directly above retains clean contrast against the tinted
   background (in dark mode the accent + dark surface composite is
   already higher visual weight than in light mode, so the same
   percentages read louder). */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .drawer-card.is-updated::after {
    background: linear-gradient(
      to left,
      color-mix(in srgb, var(--accent) 18%, transparent) 0%,
      color-mix(in srgb, var(--accent) 9%, transparent) 35%,
      transparent 70%
    );
  }
}
:root[data-theme="dark"] .drawer-card.is-updated::after {
  background: linear-gradient(
    to left,
    color-mix(in srgb, var(--accent) 18%, transparent) 0%,
    color-mix(in srgb, var(--accent) 9%, transparent) 35%,
    transparent 70%
  );
}
/* Make sure the button's content stays above the tint overlay. */
.drawer-card.is-updated > .drawer-select {
  position: relative;
  z-index: 2;
}

/* Failed-save tint — mirrors .is-updated's right-edge gradient pattern
   but in --danger red, applied to drawer cards whose drawer_id is in
   state.failedSaves. Unlike .is-updated, this DOES NOT auto-fade on
   view — failure is unresolved until the user successfully retries
   (the retry button on the detail panel POSTs and clears the failure
   on success) or manually dismisses via Mark-all-as-seen. The
   persistent tint makes it visually obvious in the cards list that
   "this memory has an unsaved attempted edit." */
.drawer-card.is-failed::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    to left,
    color-mix(in srgb, var(--danger) 28%, transparent) 0%,
    color-mix(in srgb, var(--danger) 14%, transparent) 35%,
    transparent 70%
  );
  z-index: 1;
}
@supports not (background: color-mix(in srgb, red 24%, transparent)) {
  .drawer-card.is-failed::after {
    background: linear-gradient(
      to left,
      rgba(254, 95, 99, 0.28) 0%,
      rgba(254, 95, 99, 0.14) 35%,
      transparent 70%
    );
  }
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .drawer-card.is-failed::after {
    background: linear-gradient(
      to left,
      color-mix(in srgb, var(--danger) 22%, transparent) 0%,
      color-mix(in srgb, var(--danger) 11%, transparent) 35%,
      transparent 70%
    );
  }
}
:root[data-theme="dark"] .drawer-card.is-failed::after {
  background: linear-gradient(
    to left,
    color-mix(in srgb, var(--danger) 22%, transparent) 0%,
    color-mix(in srgb, var(--danger) 11%, transparent) 35%,
    transparent 70%
  );
}
.drawer-card.is-failed > .drawer-select {
  position: relative;
  z-index: 2;
}
/* If both states fire at once (drawer had a failed save AND is in the
   recently-updated window), failure takes precedence visually — the
   :not() chains keep the .is-updated overlay from compositing on top
   of the red tint, which would muddle the colour into a brownish mush. */
.drawer-card.is-failed.is-updated::after {
  background: linear-gradient(
    to left,
    color-mix(in srgb, var(--danger) 28%, transparent) 0%,
    color-mix(in srgb, var(--danger) 14%, transparent) 35%,
    transparent 70%
  );
}

.drawer-card strong {
  font-size: 15px;
  /* Single-line + ellipsis so the card's fixed height (set on
     .drawer-card above) reliably accommodates the title. Wrapping
     titles would push the preview down past the 140px overflow clip,
     hiding it entirely on long titles. Reads cleaner in a dense list
     anyway — the user can hover/click to see the full title. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Body preview — adaptive truncation via line-clamp. The JS feeds a
   generous 600-char slice with the redundant `# Title` line already
   stripped (see stripRepeatedTitle in app.js); CSS clamps the visible
   text to N lines so wider cards naturally fit more characters per line
   while card height stays predictable. Box-orient/line-clamp is the
   well-supported WebKit/Blink/Firefox combo; the unprefixed `line-clamp`
   is included for future browser versions. */
.drawer-card .drawer-preview {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  line-height: 1.45;
  /* `word-break: break-word` keeps overly long unbroken tokens (URLs,
     drawer ids, code identifiers) from forcing the line wider than the
     card and causing horizontal overflow inside the clamp box. */
  word-break: break-word;
  /* Soft right-edge fade so characters near the right boundary
     dissolve into transparency instead of getting cleaved at a hard
     line. During the drawers-panel resize, this turns the most-
     visible reflow zone (rightmost ~8% of the preview) into a smooth
     alpha gradient — the eye reads "text fading into the edge" rather
     than "text jumping". Apple uses this on scrolling list edges and
     the Music app's lyric pane. Always-on; the effect is subtle when
     the layout is static and only becomes noticeable when text shifts. */
  -webkit-mask-image: linear-gradient(to right, black 88%, transparent 100%);
  mask-image: linear-gradient(to right, black 88%, transparent 100%);
}


/* Inline code chip — backticked tokens like `added_by` or `~/.config`
   render as soft monospace badges instead of bare backtick literals.
   --code-bg gives subtle but visible contrast in BOTH themes (using
   --surface-strong was invisible in light mode because it's near-white
   on a white card). */
.inline-code {
  display: inline;
  padding: 1px 6px;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.88em;
  background: var(--code-bg);
  border-radius: 5px;
  color: var(--text);
  /* The card preview is line-clamped, so we explicitly drop any line-
     height inheritance that would push individual code chips taller
     than the surrounding text line — keeps the 3-line clamp accurate. */
  line-height: inherit;
}

/* Wrapper for fenced code blocks. Holds the <pre> AND its copy
   button as siblings so the button can stay pinned to the visible
   top-right corner — if the button lived inside the <pre>, it'd
   scroll horizontally with the code when lines exceed the pane width.
   The wrapper is position:relative so the absolute-positioned button
   anchors to the wrapper's edges, which don't scroll. */
.markdown .code-block-wrap {
  position: relative;
  margin: 14px 0;
}

/* Fenced code block (```lang … ```) — proper code window with mono
   font, scrollable horizontally when lines exceed pane width. Used
   inside the markdown body (e.g. systemd unit files, JSON snippets,
   config samples). data-lang is set by markdownLite when the opening
   fence includes a language hint; surfaced via an optional eyebrow.
   Subtle border supplements --code-bg so the block reads as a window
   even in light mode where the tinted background alone is faint.
   Margin moved up to .code-block-wrap so the wrapper owns vertical
   spacing in the markdown flow. */
.markdown pre.code-block {
  margin: 0;
  padding: 14px 16px;
  background: var(--code-bg);
  border: 1px solid var(--code-border);
  border-radius: 12px;
  overflow-x: auto;
  font-size: 13.5px;
  line-height: 1.55;
}

/* Clipboard copy button parked in the top-right of every fenced code
   block. Subtle by default (low opacity, transparent background) so it
   doesn't distract from the code itself; lights up on hover or when
   the user is hovering the parent block. The .copied state swaps the
   clipboard glyph for a checkmark via the two stacked <svg>s below —
   one visible at a time depending on .copied. Toggled by the click
   handler in renderDrawerDetail; auto-clears after 1.4s. */
.code-copy-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  display: inline-grid;
  place-items: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--code-border);
  border-radius: 8px;
  background: var(--code-bg);
  color: var(--muted);
  cursor: pointer;
  opacity: 0;
  transform: scale(0.92);
  transition:
    opacity 0.18s ease,
    transform 0.18s cubic-bezier(0.34, 1.3, 0.64, 1),
    background 0.14s ease,
    color 0.14s ease,
    border-color 0.14s ease;
}

/* Show the button as soon as the user enters the code area OR
   focuses/hovers the button itself. Touch devices have no hover, so
   the button is ALSO permanently visible at lower opacity on coarse
   pointers (rule below). */
.code-block-wrap:hover .code-copy-btn,
.code-block-wrap:focus-within .code-copy-btn,
.code-copy-btn:focus-visible {
  opacity: 1;
  transform: scale(1);
}

.code-copy-btn:hover,
.code-copy-btn:focus-visible {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: transparent;
  outline: none;
}

/* Coarse pointers (touch): no hover available, so keep the button
   permanently visible. Slightly dimmer than the hovered state so it
   doesn't dominate the code visually. */
@media (hover: none) {
  .code-copy-btn {
    opacity: 0.65;
    transform: scale(1);
  }
}

.code-copy-btn svg {
  width: 16px;
  height: 16px;
  position: absolute;
  transition: opacity 0.18s ease, transform 0.22s cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* Default state — clipboard visible, checkmark hidden. */
.code-copy-btn .code-copy-icon-done {
  opacity: 0;
  transform: scale(0.6);
}

/* Copied state — swap to checkmark, suppress the clipboard. The
   button also flashes to the accent color so the success state is
   obvious even at a glance. */
.code-copy-btn.copied {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: transparent;
  opacity: 1;
  transform: scale(1);
}

.code-copy-btn.copied .code-copy-icon-copy {
  opacity: 0;
  transform: scale(0.6);
}

.code-copy-btn.copied .code-copy-icon-done {
  opacity: 1;
  transform: scale(1);
}

.markdown pre.code-block code {
  display: block;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  color: var(--text);
  white-space: pre;
}

/* Hide the OS scrollbar for code blocks too (matches the dashboard's
   overall no-scrollbar treatment for scrollable surfaces). */
.markdown pre.code-block {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.markdown pre.code-block::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

.detail-panel {
  min-height: 0;
  /* Override .panel's overflow:hidden so the floating .detail-panel-close
     can sit overlapping the panel's top-left corner (negative top/left
     offsets escape the panel's bounding box). The inner .detail article
     has its own overflow:auto, so scrollable content is still contained. */
  overflow: visible;
}

/* Same overflow-visible override for the Memories panel — its new
   .drawers-actions capsule (the close + maximize traffic-light)
   floats at top:-10px;left:-10px and would otherwise be clipped by
   the panel's default overflow:hidden. The inner .drawer-list
   handles its own scroll via overflow:auto, so memory cards remain
   contained inside the panel chrome. */
.drawers-panel {
  overflow: visible;
}

.detail {
  flex: 1;
  min-height: 0;
  overflow: auto;
  /* Match the panel's rounded shape so the .detail-meta / .tunnel-detail
     backgrounds (semi-transparent fills) don't poke past the panel's
     rounded corners. Without this clip the rectangular .detail corners
     show through whenever the floating .detail-panel-close /
     .detail-panel-edit buttons become semi-transparent on hover. */
  border-radius: var(--radius);
  /* Bottom-edge soft fade only. The TOP fade is intentionally omitted
     here: the sticky, opaque .detail-meta band sits at the top of the
     scroll viewport, so a top mask would alpha-fade the metadata
     HEADER itself (DRAWER/DATE/SOURCE/…) — not what we want. Instead
     the body-text fade is anchored just BELOW the meta's divider line
     via .detail-meta::after (see below), matching the memories panel
     where the header is a non-scrolling sibling above the fade. The
     bottom fade is unchanged — content still dissolves into the panel
     chrome at the bottom edge instead of cleaving hard. */
  -webkit-mask-image: linear-gradient(to bottom, black 0, black calc(100% - 18px), transparent 100%);
  mask-image: linear-gradient(to bottom, black 0, black calc(100% - 18px), transparent 100%);
}

/* Body-text fade sitting just BELOW the meta strip's divider line.
   The old top mask on .detail faded the whole scroll-viewport top,
   which dimmed the metadata HEADER itself — the user wanted the fade
   moved under the divider so only the BODY text dissolves into the
   line (matching the memories panel, whose header is a non-scrolling
   sibling above the list fade). This overlay is an ::after on the
   meta strip: it renders right under the divider and fades the body's
   first lines from the panel surface to transparent over ~22px. The
   meta is position:sticky (a positioned box) so it — and this pseudo —
   pin to the scroll-viewport top AND paint ABOVE the in-flow body
   content; z-index makes that explicit. Top colour is --meta-band, the
   same fully-opaque colour as the now-sticky band above it, so the body
   text dissolves into the band's colour at the divider in both themes,
   not a coloured wash. pointer-events:none so it never eats clicks. */
.detail-meta {
  z-index: 6;
}
.detail-meta::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* +1px past 100% so the fade starts just BELOW the band's 1px
     border-bottom divider. Without the offset the opaque --meta-band top
     edge paints directly over the divider and hides it. The 1px clears
     exactly the border so the divider line stays visible above the fade. */
  transform: translateY(calc(100% + 1px));
  height: 22px;
  background: linear-gradient(to bottom, var(--meta-band), transparent);
  pointer-events: none;
  z-index: 4;
}

/* Same top + bottom fade on the rooms-panel scroll container and the
   memories-panel scroll container. Both have rounded panel chrome
   above them; the fade keeps scrolled content from cleaving hard
   against the panel header bar (top) or bottom edge. */
.room-nav,
.drawer-list {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 18px, black calc(100% - 18px), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, black 18px, black calc(100% - 18px), transparent 100%);
}

/* Hide the OS scrollbar across the three main scrolling panels of the
   dashboard — rooms list, memories list, and detail pane. The content is
   still fully scrollable via wheel / trackpad / keyboard / touch; we just
   suppress the visible scroll thumb because it doesn't match the rest of
   the soft glassmorphism chrome (it renders as a dark rectangle pinned
   to the rounded panel edge, which looks out of place). This is the
   standard iOS / macOS overlay-scrollbar pattern. */
.room-nav,
.drawer-list,
.detail {
  scrollbar-width: none;          /* Firefox */
  -ms-overflow-style: none;       /* legacy Edge / IE */
}
.room-nav::-webkit-scrollbar,
.drawer-list::-webkit-scrollbar,
.detail::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;                  /* Chrome / Safari / WebKit */
}

/* ---------- Detail-pane navigation animations ----------
   iOS-style push/pop: forward (new view enters from the right), backward
   (older view re-enters from the left). cubic-bezier mirrors UIKit's
   default `kCAMediaTimingFunctionEaseInEaseOut` push curve. Applied via
   applyDetailTransition() after each render that changed the view kind. */
@keyframes detail-slide-in-forward {
  from { transform: translateX(24px); opacity: 0; }
  to   { transform: translateX(0);    opacity: 1; }
}

@keyframes detail-slide-in-backward {
  from { transform: translateX(-24px); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}

.detail.detail-enter-forward {
  animation: detail-slide-in-forward 0.28s cubic-bezier(0.32, 0.72, 0, 1);
}

.detail.detail-enter-backward {
  animation: detail-slide-in-backward 0.28s cubic-bezier(0.32, 0.72, 0, 1);
}

/* Same-kind data updates: only the value cells animate. Labels, section
   headers, action buttons, and overall layout stay put — they didn't
   change semantically, so animating them would create the "false motion"
   Apple HIG warns against.

   Two different motions for two different roles:

   • Body content (.markdown for drawers, the label paragraph for tunnels)
     rises bottom-to-top — this is "new content arriving" so vertical
     motion reads as a fresh page slotting into view, like a news feed.

   • Top-bar metadata (drawer ID / source / filed / author cells, tunnel
     endpoint values) just CROSS-FADES — no slide. The labels above
     these values don't move, so animating the value with translateY
     would create awkward inconsistency between label and value. Pure
     opacity keeps the row geometrically still while signaling change.
     Direction-agnostic: forward and backward both use the same fade. */
@keyframes detail-body-rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes detail-meta-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Body content — drawer markdown + tunnel "About this connection" body */
.detail.detail-values-forward .markdown.value-changed,
.detail.detail-values-backward .markdown.value-changed,
.detail.detail-values-forward .tunnel-detail-label p.value-changed,
.detail.detail-values-backward .tunnel-detail-label p.value-changed {
  animation: detail-body-rise 0.32s cubic-bezier(0.32, 0.72, 0, 1);
}

/* Metadata cells — cross-fade only, no positional motion */
.detail.detail-values-forward .value-changed,
.detail.detail-values-backward .value-changed {
  animation: detail-meta-fade 0.24s ease-out;
}

/* Re-target the body-rise keyframe on top — selector specificity is
   the same, so source order wins; body content uses rise, everything
   else falls through to fade. */
.detail.detail-values-forward .markdown.value-changed,
.detail.detail-values-backward .markdown.value-changed,
.detail.detail-values-forward .tunnel-detail-label p.value-changed,
.detail.detail-values-backward .tunnel-detail-label p.value-changed {
  animation: detail-body-rise 0.32s cubic-bezier(0.32, 0.72, 0, 1);
}

@media (prefers-reduced-motion: reduce) {
  .detail.detail-enter-forward,
  .detail.detail-enter-backward,
  .detail.detail-values-forward .value-changed,
  .detail.detail-values-backward .value-changed {
    animation: none;
  }
}

/* Back affordance above .detail-meta when a drawer was peeked into from
   the tunnel inspector. Same eyebrow typography (uppercase, tracked,
   accent-colored) as `FROM` / `TO` / `ABOUT THIS CONNECTION` so it
   visually reads as part of the inspector's chrome, not a UIKit import.
   See renderDrawerDetail(backToTunnel:…) and the click handler. */
.detail-back-bar {
  padding: 18px 24px 0;
}

.detail-back-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px 4px 2px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: opacity 0.12s ease, transform 0.18s cubic-bezier(0.34, 1.3, 0.64, 1);
}

.detail-back-btn:hover { opacity: 0.7; }

.detail-back-btn:active { transform: translateX(-3px); }

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

.detail-back-arrow {
  display: inline-block;
  font-size: 16px;
  font-weight: 600;
  line-height: 1;
  transform: translateY(-1px);
}

/* ---------- Confirm-modal pop-in physics ----------
   Spring-overshoot scale-up on open: from a touch-smaller, slightly-down
   resting position, the panel "pops" into place. cubic-bezier with the
   third control point above 1 gives the overshoot you'd get from SwiftUI's
   `.spring(response:dampingFraction:)` defaults. Backdrop fades in flat. */
@keyframes confirm-pop-in {
  0%   { transform: scale(0.86) translateY(8px); opacity: 0; }
  60%  { transform: scale(1.02) translateY(0);   opacity: 1; }
  100% { transform: scale(1)    translateY(0);   opacity: 1; }
}

@keyframes confirm-backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.confirm-sheet:not(.hidden) .confirm-panel,
.confirm-sheet:not(.hidden) .write-panel,
.write-sheet:not(.hidden) .write-panel,
.write-sheet:not(.hidden) .lab-panel,
.edit-sheet:not(.hidden) .edit-panel {
  animation: confirm-pop-in 0.42s cubic-bezier(0.34, 1.45, 0.64, 1);
  transform-origin: center;
}

.confirm-sheet:not(.hidden) .confirm-backdrop,
.write-sheet:not(.hidden) .write-backdrop,
.edit-sheet:not(.hidden) .edit-backdrop {
  animation: confirm-backdrop-in 0.18s ease-out;
}

/* Symmetric spring exit: the entry pops UP past its resting size before
   settling; the exit should pop UP past the resting size first too, then
   shrink and fade away. Keyframes mirror the entry — entry peaks at 60%
   on the way in, so exit peaks at 40% on the way out (100 - 60). Same
   cubic-bezier as the entry so the physics feel like one consistent
   spring acting in both directions. `forwards` holds the invisible end
   state in place during the JS-side gap before .hidden is applied. */
@keyframes confirm-pop-out {
  0%   { transform: scale(1)    translateY(0);    opacity: 1; }
  40%  { transform: scale(1.04) translateY(-2px); opacity: 1; }
  100% { transform: scale(0.86) translateY(8px);  opacity: 0; }
}

@keyframes confirm-backdrop-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

.confirm-sheet.closing .confirm-panel,
.confirm-sheet.closing .write-panel,
.write-sheet.closing .write-panel,
.write-sheet.closing .lab-panel,
.edit-sheet.closing .edit-panel {
  /* Close runs faster than open (≈ 2/3 of the .42s entry) because
     dismissal is a "you've moved on" gesture — equal-duration close
     reads as the UI making you wait. iOS HIG / Material both prescribe
     shorter exits for this reason. Same spring cubic-bezier so the
     pop-bigger-then-shrink still feels like one consistent physics
     object, just acted out more decisively. */
  animation: confirm-pop-out 0.28s cubic-bezier(0.34, 1.45, 0.64, 1) forwards;
  transform-origin: center;
}

.confirm-sheet.closing .confirm-backdrop,
.write-sheet.closing .write-backdrop,
.edit-sheet.closing .edit-backdrop {
  /* Scaled to match the new panel duration: short fade in the second
     half of the panel animation so the backdrop doesn't outlast the
     panel or vanish mid-bounce. */
  animation: confirm-backdrop-out 0.14s ease-in forwards;
  animation-delay: 0.14s;
}

@media (prefers-reduced-motion: reduce) {
  .confirm-sheet:not(.hidden) .confirm-panel,
  .confirm-sheet:not(.hidden) .write-panel,
  .write-sheet:not(.hidden) .write-panel,
  .write-sheet:not(.hidden) .lab-panel,
  .edit-sheet:not(.hidden) .edit-panel,
  .confirm-sheet:not(.hidden) .confirm-backdrop,
  .write-sheet:not(.hidden) .write-backdrop,
  .edit-sheet:not(.hidden) .edit-backdrop,
  .confirm-sheet.closing .confirm-panel,
  .confirm-sheet.closing .write-panel,
  .write-sheet.closing .write-panel,
  .write-sheet.closing .lab-panel,
  .edit-sheet.closing .edit-panel,
  .confirm-sheet.closing .confirm-backdrop,
  .write-sheet.closing .write-backdrop,
  .edit-sheet.closing .edit-backdrop {
    animation: none;
  }
}

.detail-meta {
  /* Sticky so the metadata band — and the body-text fade pinned just
     below its divider (.detail-meta::after) — stay glued to the top of
     the .detail scroll viewport as the body scrolls UNDER them, instead
     of scrolling away with the content (the earlier position:relative
     made the fade ride the content up and out, so it felt redundant).
     Matches the memories panel, whose header is a non-scrolling sibling
     above the list fade. Requires the OPAQUE background set below so the
     body text can't bleed through the band as it scrolls beneath it.
     position:sticky is still a positioned box, so the absolute ::after
     keeps anchoring to it. */
  position: sticky;
  top: 0;
  display: grid;
  /* Known-good layout (restored). Content-sized columns evenly spread
     across the strip: `auto` columns shrink/share so all four cells fit
     the normal-width panel without clipping AUTHOR, space-evenly gives
     the original even padding, and the dotted drawer-id underline spans
     exactly its hash. The repeat(N,max-content) + center + clamp-gap
     experiments broke the padding, over-extended the dots, and clipped
     in normal view — all reverted. */
  grid-template-columns: auto auto auto auto;
  justify-content: space-evenly;
  align-items: center;
  padding: 18px;
  /* Fixed content height so the strip — and its bottom divider — never
     shift vertically regardless of cell content (dotted drawer-id
     underline, optional "Updated" dot cell, the blank skeleton, real
     values). Box-sizing content so the 18px padding adds predictably.
     The 41px value is chosen to MATCH the sibling .panel-header's
     content height (h2 ~20px + count line ~21px), since both panels
     start at the same Y and share padding:18px + a 1px bottom border —
     so their dividers line up horizontally across the panels. Without
     the match the detail divider sat ~13px higher than the Memories /
     Wings panel-header divider. */
  box-sizing: content-box;
  height: 41px;
  border-bottom: 1px solid var(--line);
  /* Subtle banner background to distinguish the metadata strip from
     the markdown body below. Earlier light-mode value
     `rgba(255, 255, 255, 0.52)` was white-on-white-translucent and
     invisible against the panel surface (which is itself
     near-white). Switched to a tiny additive dark overlay
     (rgba(0,0,0,0.035)) — same approach as macOS sidebar headers:
     slight darkening that reads as a banner without competing with
     content. The band is FULLY OPAQUE via --meta-band (a solid colour,
     theme-aware) — NOT --surface-strong, which is 0.94 alpha and let the
     body text ghost through the sticky band (it scrolls underneath),
     making the header read as semi-transparent. The user wants a solid
     header; only the body text should fade. The ::after fade dissolves
     the body FROM this same --meta-band colour. */
  background: var(--meta-band);
}

.detail-meta div {
  display: grid;
  gap: 4px;
  min-width: 0;
}

/* "Updated" meta cell uses an accent-coloured value + leading dot
   so it matches the card-list recently-updated marker visually.
   Inline-flex on the strong puts the dot before the relative-time
   text. The cell only renders at all when the drawer was recently
   updated AND the update time differs from the filed time (so we
   don't surface "Updated 2m ago" right after a memory is created).
   Cell renders normally next to Drawer / Date / Source / Author. */
.detail-meta strong.meta-recent {
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.detail-meta strong.meta-recent::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent);
}
@supports not (background: color-mix(in srgb, red 20%, transparent)) {
  .detail-meta strong.meta-recent::before {
    box-shadow: 0 0 4px rgba(63, 148, 186, 0.4);
  }
}

/* Tunnels meta cell: graph-edge glyph inline before the count.
   Same vocabulary as the rooms-panel tunnel indicator so the user
   reads the icon as "this is linked through tunnels" at a glance. */
.detail-meta strong .meta-tunnel-glyph {
  display: inline-flex;
  align-items: center;
  width: 12px;
  height: 14px;
  margin-right: 6px;
  color: var(--accent);
  vertical-align: middle;
}
.detail-meta strong .meta-tunnel-glyph svg {
  width: 12px;
  height: 14px;
}

.detail-meta span {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
}

.detail-meta strong {
  overflow: hidden;
  font-size: 13px;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Muted instead of full --text contrast — the metadata values
     (drawer hash, date, source, author) are reference info, not the
     focus of the panel. Pulling them down to --muted (#6e6e73 light /
     #a1a1a6 dark) keeps them readable but lets the markdown body
     content above them claim the visual hierarchy. Theme-aware via
     the CSS variable so it adapts to dark mode automatically. */
  color: var(--muted);
  /* Regular weight (vs the browser default bold for <strong>) — the
     uppercase labels above each value carry the visual emphasis, so
     the values themselves can sit at a thinner weight. Reads more
     like polished spec sheet, less like a stack of bold headers. */
  font-weight: 400;
}

/* Drawer hash is itself the Copy-ID affordance. The permanent dotted
   underline (always visible — small round dots, like dictionary apps)
   is the classic "click for action" signal. The user shouldn't have to
   hover to discover it's interactive. Click triggers a small "Copied"
   pill toast that floats above the hash. No hover color change: cursor
   is the only transient feedback so the metadata block stays visually
   stable. */
.detail-meta strong.drawer-id-copy {
  cursor: pointer;
  /* Round dots via radial-gradient background — `border-style: dotted`
     renders as squares in some browsers (Firefox, certain WebKit builds)
     and as round in others (Chrome). The background-image trick gives
     consistent round dots everywhere: a tiny radial-gradient circle,
     tiled horizontally as a 2px-tall strip pinned to the bottom of the
     element. padding-bottom reserves room below the baseline so the
     dots sit clear of the descenders. */
  /* inline-block so the dotted background tracks the TEXT width, not
     the grid cell — justify-self:start keeps it left-anchored. The
     earlier max-width:100% / width:fit-content combo (paired with the
     max-content columns) let the dots tile across the full cell;
     dropped, since with auto columns the cell is already content-sized
     and the dots span exactly the hash. */
  display: inline-block;
  background-image: radial-gradient(circle, var(--soft) 1px, transparent 1.5px);
  background-size: 5px 2px;
  background-position: 0 100%;
  background-repeat: repeat-x;
  padding-bottom: 5px;
  vertical-align: baseline;
}

/* Traffic-light control cluster overlaying the detail panel's top-left
   corner — close + maximize wrapped in a single rounded-rectangle
   capsule, macOS-style. The capsule itself is the floating chrome
   (white in light mode, dark gray in dark mode); the two inner
   circles carry the colored actions: red close, green maximize.
   The edit pencil on the top-right stays as its own standalone
   rounded-rect button (it's a separate semantic group — actions on
   the content vs. controls for the window). */
.detail-panel-actions {
  position: absolute;
  top: -10px;
  left: -10px;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px;
  border-radius: 14px;
  background: #ffffff;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.06);
  transition: opacity 0.2s ease, background 0.18s ease;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .detail-panel-actions {
    background: #2c2c2e;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
  }
}

:root[data-theme="dark"] .detail-panel-actions {
  background: #2c2c2e;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
}

/* Hover-reveal for panel chrome controls. By default the maximize +
   close pill sits invisible at rest (opacity 0, pointer-events off so
   the empty corner doesn't intercept hover-triggers from cards
   underneath), and fades in over the existing 0.2s opacity transition
   the moment the cursor enters the parent panel. `:focus-within` is
   ANDed in so a keyboard user tabbing into one of the buttons still
   makes the wrapper visible — without that, the buttons would be
   reachable by Tab but invisible, which is a usability fail.

   The user can opt OUT via Settings → Display → "Always show panel
   controls": that toggle adds the body.apricity-panel-controls-always
   class which forces opacity:1 regardless of hover. Default is
   hover-only (less visual clutter at rest; macOS-traffic-light
   pattern); the toggle exists for users who prefer persistent
   affordances or do a lot of maximize/close cycling.

   Scoped to `.panel > .detail-panel-actions` (direct child) so the
   trigger is the panel itself, not bubbling hovers from nested
   children that could keep the controls visible while the cursor
   has actually left the panel chrome. */
.detail-panel-actions {
  opacity: 0;
  pointer-events: none;
}

.panel:hover > .detail-panel-actions,
.panel:focus-within > .detail-panel-actions {
  opacity: 1;
  pointer-events: auto;
}

body.apricity-panel-controls-always .detail-panel-actions {
  opacity: 1;
  pointer-events: auto;
}

/* Touch devices (no hover capability) — always show the panel
   controls. Without this, mobile users could never reach maximize
   or close affordances because there's no cursor to hover with,
   leaving the panels operationally trapped. Matches the same
   `@media (hover: none)` accommodation we use on .code-copy-btn. */
@media (hover: none) {
  .detail-panel-actions {
    opacity: 1;
    pointer-events: auto;
  }
}

/* Inner action circles — solid color bg, contrast-color icon. Sizes
   chosen so the pill (4px padding + 22px circle + 6px gap + 22px circle
   + 4px padding = 58px wide) sits cleanly in the corner. */
.detail-panel-close,
.detail-panel-maximize {
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: transform 0.18s cubic-bezier(0.34, 1.3, 0.64, 1), filter 0.14s ease;
}

.detail-panel-close {
  background: var(--danger);
  color: #7f2f30;
}

.detail-panel-maximize {
  background: #34c759;
  color: #1e632d;
  /* Background/icon transition so the green → yellow swap on enlarge
     glides instead of jumping. Same easing as the rest of the
     traffic-light hover transforms. */
  transition:
    background 0.18s ease,
    color 0.18s ease,
    transform 0.18s cubic-bezier(0.34, 1.3, 0.64, 1),
    filter 0.14s ease;
}

/* When the detail panel is maximized, the button's role flips from
   "expand" to "collapse" — recolor to yellow so the user reads the
   change at a glance (and the minimize glyph swap below isn't the
   only signal). Picked-color palette matching macOS yellow traffic
   light + a dark-yellow icon for contrast.

   Scoped to `.detail-panel .detail-panel-maximize` (not just
   `.detail-panel-maximize`) so the rule fires ONLY on Detail's button.
   Since browse mode now adds an identically-classed traffic-light
   capsule on the Memories panel, an unscoped selector would turn that
   button yellow too whenever Detail was enlarged. The descendant
   constraint keeps the two states independent. */
.content-grid.detail-enlarged .detail-panel .detail-panel-maximize {
  background: #fac800;
  color: #7e640c;
}

.detail-panel-close:hover,
.detail-panel-close:focus-visible,
.detail-panel-maximize:hover,
.detail-panel-maximize:focus-visible {
  outline: none;
  /* Slight brightness bump + scale; keeps the icon colors intact
     (filter would have shifted them). */
  transform: scale(1.08);
  filter: brightness(1.05);
}

.detail-panel-close svg,
.detail-panel-maximize svg {
  width: 12px;
  height: 12px;
}

/* Maximize button stacks two icons (maximize / minimize) absolutely so
   they occupy the same spot — only one is opacity:1 at a time. The
   swap is driven by .content-grid.detail-enlarged below; same
   cross-fade-with-scale pattern as the code-block copy → checkmark
   swap, so the two states feel like one button changing role rather
   than two separate controls. */
.detail-panel-maximize {
  position: relative;
}

.detail-panel-maximize svg {
  position: absolute;
  transition: opacity 0.18s ease, transform 0.22s cubic-bezier(0.34, 1.3, 0.64, 1);
}

.detail-panel-maximize .icon-minimize {
  opacity: 0;
  transform: scale(0.6);
}

.content-grid.detail-enlarged .detail-panel .detail-panel-maximize .icon-maximize {
  opacity: 0;
  transform: scale(0.6);
}

.content-grid.detail-enlarged .detail-panel .detail-panel-maximize .icon-minimize {
  opacity: 1;
  transform: scale(1);
}

/* Floating action buttons (edit + delete) — only visible in the
   enlarged detail view. Independent larger circles hovering at the
   bottom-right INSIDE the detail panel (not overflowing the corner
   like the traffic-light cluster on the top-left). 52px touch
   targets — comfortably above the Apple HIG 44px minimum and
   appropriate for primary actions in the focused enlarged workflow.
   Hidden by default → slide up + fade in when
   .content-grid.detail-enlarged is on. */
.detail-panel-edit,
.detail-panel-delete,
.detail-panel-retry {
  position: absolute;
  bottom: 24px;
  z-index: 8;
  display: inline-grid;
  place-items: center;
  width: 52px;
  height: 52px;
  padding: 0;
  /* Border is always present but transparent in view mode — keeps the
     button's box-size stable and lets `border-color` animate cleanly
     in/out of edit mode (the delete button morphs into a bordered
     grey "cancel" capsule when editing). Without this, `border: 0`
     in view mode and `border: 1px solid var(--line)` in edit mode
     means the border PROPERTY pops in instantly when entering edit,
     and pops out instantly on exit — jarring during the morph back. */
  border: 1px solid transparent;
  box-sizing: border-box;
  border-radius: 50%;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  /* Hidden-state transform: dropped below the visible position +
     slightly scaled down. The visible-state rule below restores to
     translateY(0) scale(1), so the transition reads as "rise up
     from below and pop into place" on enlarge, and the inverse
     "drop down and shrink away" on minimize. translateY does the
     sliding; scale adds a touch of flare. */
  transform: translateY(24px) scale(0.94);
  /* Shadow gives the floating button "lift" off the panel content
     below — similar weight to the traffic-light cluster's elevation. */
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.16), 0 2px 4px rgba(0, 0, 0, 0.08);
  /* EXIT transition (applies when .detail-enlarged class is removed).
     Opacity has a 0.10s delay before it starts fading, so the slide
     is actually visible — earlier version had opacity racing the
     slide to 0 in 0.16s, hiding the buttons before the user could
     see them move. Now: first 100ms is full-opacity slide-down,
     then opacity fades over 0.22s while the slide continues to its
     final position. Slide curve is a slow-start accelerator
     (cubic-bezier(0.4, 0, 1, 0.5)) so the buttons settle for a tick
     before accelerating away — feels like "let me think… nope,
     I'm out" rather than a panicked drop. Entry's bouncy spring is
     overridden below.

     background / color / border-color / box-shadow are also
     transitioned (0.24s ease) so the edit→view morph (delete
     button red→grey→red, edit button color flips, border fades in
     and out) animates as one cohesive shape-shift instead of
     snapping each property instantly while only the icon fades. */
  transition:
    opacity 0.22s ease 0.10s,
    transform 0.32s cubic-bezier(0.4, 0, 1, 0.5),
    background 0.24s ease,
    color 0.24s ease,
    border-color 0.24s ease,
    box-shadow 0.24s ease,
    filter 0.14s ease;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .detail-panel-edit,
  :root:not([data-theme="light"]) .detail-panel-delete,
  :root:not([data-theme="light"]) .detail-panel-retry {
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.3);
  }
}

:root[data-theme="dark"] .detail-panel-edit,
:root[data-theme="dark"] .detail-panel-delete,
:root[data-theme="dark"] .detail-panel-retry {
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.3);
}

/* Positions: delete on the far right (48px), edit just left of it
   (112px), retry to the left of edit (176px). 12px gap preserved
   between every adjacent 52px circle (52 + 12 = 64 stride). The
   retry button appears only when the drawer has a cached failedSave
   entry, so its slot at 176px is empty most of the time. */
.detail-panel-delete { right: 48px; }
.detail-panel-edit { right: 112px; }
.detail-panel-retry { right: 176px; }

/* Edit = blue (non-destructive interactive action). --accent body +
   a darker shade of the same hue for the icon — same shade family
   per the user's earlier spec. */
.detail-panel-edit {
  background: var(--accent);
  color: #194757;
}

/* Delete = same standard --danger red as the close-X (reverted from
   the darker burgundy experiment). Icon matches the close-X dark-red
   too so the destructive semantic is uniform. */
.detail-panel-delete {
  background: var(--danger);
  color: #7f2f30;
}

/* Retry = matches the danger family because the trigger is a FAILED
   save — same red semantic as the .is-failed card tint + the
   notification bell entry. The refresh-arrow icon distinguishes the
   intent (try again) from delete (remove). Same dark-red icon colour
   as delete + close for visual cohesion of the red action family. */
.detail-panel-retry {
  background: var(--danger);
  color: #7f2f30;
}

.detail-panel-edit svg,
.detail-panel-delete svg,
.detail-panel-retry svg {
  width: 22px;
  height: 22px;
}

/* Lock each icon to a FIXED colour rather than inheriting via
   `currentColor` from the button's animated `color` attribute.
   Without this, when the edit button morphed back to view mode,
   the still-visible-but-fading pencil briefly rendered in
   intermediate transition colours (washed-out pale blue) as the
   button's `color` animated from white → dark-blue over 0.24s.
   The trash button hid the same effect because its background
   was ALSO animating red↔grey, drowning out the icon-colour
   transition. Pinning each icon to its target colour means the
   morph is purely icon-swap + button-bg (where applicable),
   never icon-colour-flash. */
.icon-edit-pencil { color: #194757; }
.icon-edit-check { color: white; }
.icon-delete-trash { color: #7f2f30; }
.icon-delete-cancel { color: var(--muted); }

/* ---------- inline-edit button morph ----------
   The pencil/trash buttons each carry TWO stacked SVGs (default +
   editing-state icon). Only one is visible at a time, swapped by
   the .content-grid.detail-editing class. Same icon-stack pattern
   used for the maximize/minimize toggle — keeps the click target
   stable while the icon role flips.

   Earlier iteration added `position: relative` here for the inner
   absolutely-positioned SVGs to anchor to — that was wrong. The
   buttons are ALREADY `position: absolute` (their bottom-right
   floating placement requires it), and absolute IS a positioning
   context for descendants, so the inner SVGs already anchor to
   the button without any override. The accidental relative override
   was pulling the buttons out of their corner anchor and into
   normal flow at the end of .detail-panel — which is why both
   buttons "disappeared" into a small element at the bottom-left
   AND a large gap appeared below the article content. */

.detail-panel-edit .icon-edit-check,
.detail-panel-delete .icon-delete-cancel {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 22px;
  height: 22px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
}
.detail-panel-edit .icon-edit-pencil,
.detail-panel-delete .icon-delete-trash {
  transition: opacity 0.18s ease;
}

/* Editing on: swap icons, recolor buttons. The edit button takes
   on the Write-button blue (already var(--accent) by default, just
   stays). The delete button transitions to a hollow grey "cancel"
   capsule — borderless light pill with a muted X glyph. */
.content-grid.detail-editing .detail-panel-edit .icon-edit-pencil,
.content-grid.detail-editing .detail-panel-delete .icon-delete-trash {
  opacity: 0;
}
.content-grid.detail-editing .detail-panel-edit .icon-edit-check,
.content-grid.detail-editing .detail-panel-delete .icon-delete-cancel {
  opacity: 1;
}

/* Edit button stays blue (already accent) but the icon color flips
   to pure white so the check pops against the blue fill. */
.content-grid.detail-editing .detail-panel-edit {
  background: var(--accent);
  color: white;
}

/* Delete → cancel: hollow grey with subtle border. No fill, no
   shadow flourish — cancel is the "quiet exit" action, opposite
   of the loud red destructive trash it replaces. Only `border-color`
   (not the full `border` shorthand) is set here so the base
   transparent-border baseline still drives the box-size, and the
   color animates in/out as part of the morph rather than the border
   property snapping into existence. box-sizing was promoted to the
   base rule so the +1px transparent border doesn't shift the
   button against its still-borderless edit/save sibling. */
.content-grid.detail-editing .detail-panel-delete {
  background: var(--surface);
  color: var(--muted);
  border-color: var(--line);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.content-grid.detail-editing .detail-panel-delete:hover {
  background: var(--surface-strong);
  color: var(--text);
  border-color: rgba(0, 0, 0, 0.18);
}

/* One-shot check draw animation. Total path length is ~21 units,
   dash 30 keeps it on the safe side of "fully erased at offset 30,
   fully drawn at offset 0". Animation runs once per entry into
   edit mode (animation-fill-mode: forwards holds the drawn end-
   state). Re-runs naturally whenever the class is re-added because
   removing+re-adding the class restarts the animation.

   Duration tuned to match the rest of the button-morph clock (the
   trash↔X swap finishes in ~0.24s). Was 0.42s — that made the
   edit-button morph perceptibly slower than the delete-button
   morph because the check kept "drawing" after everything else
   had settled. 0.26s keeps the flourish character but lands on
   roughly the same beat as the colour/border transitions. */
.content-grid.detail-editing .icon-edit-check-path {
  stroke-dasharray: 30;
  stroke-dashoffset: 30;
  animation: edit-check-draw 0.26s cubic-bezier(0.65, 0, 0.35, 1) forwards;
}
@keyframes edit-check-draw {
  to { stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .content-grid.detail-editing .icon-edit-check-path {
    animation: none;
    stroke-dashoffset: 0;
  }
}
html.reduce-motion .content-grid.detail-editing .icon-edit-check-path {
  animation: none;
  stroke-dashoffset: 0;
}

/* ============================================================
   Tunnel-bind indicator on the memory detail panel
   ============================================================
   Replaces the old TUNNELS meta cell. Two paired affordances:
     • A floating chip at bottom-LEFT (mirroring the
       edit/delete cluster on bottom-right) with the graph-
       edge glyph + count, click = open the tunnel inspector.
       Edit mode adds an inline × unbind sibling.
     • A wide low-opacity horizontal graph-edge SVG painted
       across the bottom of the panel as a watermark — the
       subliminal "this is connected" cue replacing the cramped
       text "↕ 1" indicator that used to share the meta strip.
   Both are gated on the .has-tunnel class JS toggles on the
   .detail-panel when the rendered drawer is tunnel-bound. */
/* LEFT-side floating column, top-anchored at the same vertical
   position as the editor toolbar's wing/room chips (top: 170px,
   left: 24px). Per the user's spec: this lives "where the
   wing/room sections appear in edit mode". In edit mode the
   wing/room + format chips slide DOWN to start below the tunnel
   chip (see .detail-editor-chips override below) so they share
   the column without overlapping. */
.detail-panel-tunnel-info {
  position: absolute;
  /* The `top` here is only a FALLBACK for the first frame before
     positionTunnelInfo() (app.js) measures the WING pill's real
     rendered position and sets an inline `top` so the box's bottom
     edge sits ~16px above WING. The box is absolute-to-panel while
     WING lives in the scrolling .detail flow, so a static value
     can't stay adjacent across content sizes — JS measurement is
     the source of truth. JS computes the SAME top in view + edit
     mode, so the box never jumps when edit toggles. */
  top: 300px;
  left: 48px;
  z-index: 8;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  width: 210px;
  /* overflow: visible (NOT auto) — an auto/scroll container clips
     child box-shadows into a hard grey rectangle at the chip's
     edge, which was the "ugly grey box" artifact. Visible lets the
     soft elevation shadow render in full. */
  overflow: visible;
  opacity: 0;
  pointer-events: none;
  transform: translateX(-16px) scale(0.96);
  transform-origin: left top;
  transition:
    opacity 0.22s ease 0.10s,
    transform 0.32s cubic-bezier(0.4, 0, 1, 0.5);
}

/* The editor wing/room + format toolbar keeps its native top:170px
   anchor in all cases — no push. positionTunnelInfo() places the
   tunnel box in the empty gutter ABOVE the WING pill (there's
   ~170px of clearance below the meta strip), so the editor toolbar
   never has to shift to make room. */

.detail-panel-tunnel-info.hidden {
  display: none;
}

/* Visible only when the panel is enlarged — same gating as the
   editor format-chips column, so view+edit modes stay symmetric.
   Position is identical in both modes (set in the base rule); only
   the entrance fade/slide differs here. */
.content-grid.detail-enlarged .detail-panel-tunnel-info {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(0) scale(1);
  transition:
    opacity 0.28s ease 0.06s,
    transform 0.36s cubic-bezier(0.34, 1.45, 0.64, 1);
}

/* Each chip is a vertical card: graph-edge glyph on top, linked
   memory title underneath. Wrap holds the chip + an absolutely-
   positioned corner × that appears in edit mode for unbind.
   The hover POP scale lives HERE on the wrap (not the chip) so the
   whole unit — card AND the corner × — scales together. With the
   scale on the chip alone, the × (a sibling) stayed put while the
   card grew, making the × visually detach during the pop. */
.detail-panel-tunnel-chip-wrap {
  position: relative;
  transform: scale(1);
  transform-origin: center;
  transition: transform 0.18s cubic-bezier(0.34, 1.3, 0.64, 1);
}

.detail-panel-tunnel-chip-wrap:hover {
  transform: scale(1.04);
}

.detail-panel-tunnel-chip-wrap:active {
  transform: scale(0.98);
}

.detail-panel-tunnel-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 16px 14px 14px;
  /* SOLID surface, NO border, NO box-shadow. The earlier
     border+shadow combo produced the visible "grey sharp box"
     artifact behind the corner × — shadows under translucent
     pseudo-states and the border's pixel-rounded corners both
     show through at the × area in light mode. A single solid
     fill with a subtle ring gives the chip enough presence
     without any layered chrome that could ghost through. */
  border: 0;
  border-radius: 16px;
  background: var(--surface-strong);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  text-align: center;
  /* Soft drop-shadow elevation (same floating-card family as the
     edit/delete buttons) PLUS a faint inset ring for edge
     definition. The drop-shadow is what makes the WHOLE box read
     as a distinct card: without it the surface-strong fill is
     nearly identical to the panel background, so scaling the chip
     only appeared to move the text/glyph (the only high-contrast
     bits). The shadow gives the box a visible silhouette that
     clearly scales as one unit on hover. */
  box-shadow:
    0 4px 14px rgba(0, 0, 0, 0.10),
    0 1px 3px rgba(0, 0, 0, 0.06),
    inset 0 0 0 1px color-mix(in srgb, var(--text) 6%, transparent);
  /* No transform here — the scale POP lives on the wrap so the ×
     scales with the card. The chip only animates its shadow. */
  transition: box-shadow 0.18s ease;
}

/* Deepen the chip's elevation shadow while the wrap is hovered, so
   the whole card reads as lifting. Keyed off the WRAP hover (not
   the chip's own) so it stays in sync with the wrap's scale. */
.detail-panel-tunnel-chip-wrap:hover .detail-panel-tunnel-chip {
  box-shadow:
    0 10px 26px rgba(0, 0, 0, 0.14),
    0 3px 8px rgba(0, 0, 0, 0.08),
    inset 0 0 0 1px color-mix(in srgb, var(--text) 6%, transparent);
}

/* Dark-mode elevation — deeper shadows to register against the
   dark panel surface, matching the edit/delete button treatment. */
:root[data-theme="dark"] .detail-panel-tunnel-chip {
  box-shadow:
    0 4px 14px rgba(0, 0, 0, 0.45),
    0 1px 3px rgba(0, 0, 0, 0.30),
    inset 0 0 0 1px color-mix(in srgb, var(--text) 10%, transparent);
}
:root[data-theme="dark"] .detail-panel-tunnel-chip-wrap:hover .detail-panel-tunnel-chip {
  box-shadow:
    0 10px 26px rgba(0, 0, 0, 0.55),
    0 3px 8px rgba(0, 0, 0, 0.35),
    inset 0 0 0 1px color-mix(in srgb, var(--text) 10%, transparent);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .detail-panel-tunnel-chip {
    box-shadow:
      0 4px 14px rgba(0, 0, 0, 0.45),
      0 1px 3px rgba(0, 0, 0, 0.30),
      inset 0 0 0 1px color-mix(in srgb, var(--text) 10%, transparent);
  }
  :root:not([data-theme="light"]) .detail-panel-tunnel-chip-wrap:hover .detail-panel-tunnel-chip {
    box-shadow:
      0 10px 26px rgba(0, 0, 0, 0.55),
      0 3px 8px rgba(0, 0, 0, 0.35),
      inset 0 0 0 1px color-mix(in srgb, var(--text) 10%, transparent);
  }
}

.detail-panel-tunnel-chip-glyph {
  width: 22px;
  height: 22px;
  color: var(--accent);
  flex-shrink: 0;
}

/* Linked memory title — soft accent label feel, two-line clamp so
   long titles don't blow the chip's vertical rhythm. */
.detail-panel-tunnel-chip-label {
  font-size: 13px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
  letter-spacing: -0.005em;
}

/* Inline unbind × — tucked INSIDE the chip's top-right corner
   (not hanging off it) so it never creates the corner-overlap
   artifact that the earlier outside-the-edge placement did.
   Always in the DOM, gated via CSS — fades + scales in when the
   panel enters edit mode, instead of popping in via a JS
   re-render. Borderless and outline-free so it sits cleanly on
   the chip's surface; the muted text-colour + red-on-hover give
   enough contrast without chrome. */
/* Aggressive native-button reset — the earlier text-character
   "×" with the default <button> rendering pipeline (system font
   metrics, default appearance, default background) was the
   actual source of the grey rectangle the user kept seeing in
   light mode. Replaced with an inline SVG × inside a fully
   reset button so nothing the browser supplies by default
   shows through. */
.detail-panel-tunnel-unbind {
  position: absolute;
  top: 6px;
  right: 6px;
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  margin: 0;
  border: 0;
  outline: 0;
  border-radius: 50%;
  background: transparent;
  background-color: transparent;
  background-image: none;
  background-clip: padding-box;
  color: color-mix(in srgb, var(--text) 55%, transparent);
  font: inherit;
  line-height: 0;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transform: scale(0.78);
  transform-origin: center;
  /* Kill native button rendering on every platform that has it. */
  -webkit-appearance: none;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
  box-shadow: none;
  filter: none;
  transition:
    opacity 0.24s ease,
    transform 0.34s cubic-bezier(0.34, 1.45, 0.64, 1),
    background 0.14s ease,
    color 0.14s ease;
}

.detail-panel-tunnel-unbind svg {
  width: 12px;
  height: 12px;
  pointer-events: none;
}

.content-grid.detail-editing .detail-panel-tunnel-unbind {
  opacity: 1;
  pointer-events: auto;
  transform: scale(1);
}

.detail-panel-tunnel-unbind:hover {
  transform: scale(1.10);
  background: color-mix(in srgb, var(--danger) 16%, transparent);
  color: var(--danger);
}

.detail-panel-tunnel-unbind:active {
  transform: scale(0.94);
}

.detail-panel-tunnel-unbind:focus {
  outline: 0;
  background: transparent;
}
.detail-panel-tunnel-unbind:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Watermark — wide, low-opacity horizontal graph-edge SVG painted
   across the META STRIP background (NOT the panel-wide background
   it was on before). Lives INSIDE .detail-meta so it scrolls with
   the strip and is visible at non-enlarged sizes — the user's
   "you would not know there is a linked memory unless in full
   screen mode" was the prior bug. Position absolute + low z-index
   so it sits behind the metadata cells; pointer-events: none so it
   never hijacks clicks on the meta cells. */
.detail-meta-tunnel-watermark {
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(70%, 520px);
  height: auto;
  transform: translate(-50%, -50%);
  color: var(--accent);
  opacity: 0.08;
  pointer-events: none;
  z-index: 0;
}

/* Lift the meta cells above the watermark so the text never
   gets visually muddied by the SVG sitting under it. The cells
   are direct grid children of .detail-meta; bumping their stacking
   context above 0 is enough without changing layout. */
.detail-meta > div {
  position: relative;
  z-index: 1;
}

/* Watermark dimming in dark mode so it doesn't punch through the
   dark surface too brightly. */
:root[data-theme="dark"] .detail-meta-tunnel-watermark {
  opacity: 0.12;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .detail-meta-tunnel-watermark {
    opacity: 0.12;
  }
}

@media (prefers-reduced-motion: reduce) {
  .detail-panel-tunnel-info,
  .detail-panel-tunnel-unbind {
    transition: none;
  }
}
html.reduce-motion .detail-panel-tunnel-info,
html.reduce-motion .detail-panel-tunnel-unbind {
  transition: none;
}

/* ---------- inline WYSIWYG editor ----------
   The editing surface IS the rendered markdown — same .markdown
   CSS, same H1, same callouts, same code blocks. Contenteditable
   on the .markdown div makes every element editable in place; the
   markdown source is reconstructed on each input via htmlToMarkdown
   so the buffer stays in sync. Wing/Room live as small chips at
   the bottom since they aren't part of the markdown body. The old
   form-style editor (textarea + title input below) is gone — its
   styles below are now dead code and can be deleted in a sweep. */

/* The contenteditable .markdown surface looks visually identical
   to read-only mode (inherits all .markdown rules unchanged). The
   only deltas: cursor:text (placeable caret) and a barely-visible
   accent ring on focus so the editable affordance is discoverable
   without breaking the "this IS the rendered view" promise. */
/* No focus halo, no border, no chrome — edit mode is visually
   indistinguishable from view mode until the user starts typing.
   The only delta is cursor:text so the caret is placeable inside
   the prose. */
.markdown-editing {
  outline: none;
  cursor: text;
}
/* Re-enable selection inside the editor (drawer-meta block disables
   it for the read-only view's metadata chrome). */
.markdown-editing,
.markdown-editing * {
  user-select: text;
}
/* The .callout-remove button (absolutely-positioned X) is appended
   as the last DOM child of every callout in edit mode. That breaks
   the base ".markdown blockquote > p:last-child { margin-bottom: 0 }"
   rule that view mode relies on — the last content <p> is now
   second-to-last in the DOM, so :last-child no longer matches and
   the <p> keeps its default 6px margin-bottom. Visible as "extra
   space at the bottom of the callout box, shifting everything
   below it down" the moment edit mode kicks in. Re-zero the margin
   on the true last content child (whichever block type it is), but
   exclude .callout-label so its intentional 6px label gap is
   preserved when the callout has no body yet. */
.markdown-editing blockquote.callout > *:nth-last-child(2):not(.callout-label) {
  margin-bottom: 0;
}

/* Default (non-enlarged) chip layout: sticky horizontal bar at the
   top of the editable body, glass backdrop. Used when the panel
   is too narrow for the side-floating layout (enlarged mode below
   overrides this with a grid-positioned left column). */
.detail-editor-chips {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 12px 32px;
  background: var(--surface-strong);
  backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--line);
}

/* Enlarged mode: chips move into a sticky left column floating in
   the markdown's left padding area. Grid centers the markdown at
   max-width 760px and the chips sit in the empty space to its
   left. align-self:start + position:sticky keeps them anchored to
   the top of the visible viewport as the user scrolls — same
   sidebar-metadata pattern macOS Notes / iOS Pages uses. */
.content-grid.detail-enlarged .detail-edit-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 760px) minmax(0, 1fr);
  column-gap: 16px;
  align-items: start;
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-chips {
  grid-column: 1;
  justify-self: end;
  align-self: start;
  position: sticky;
  /* The column must STAY put and float in place as the user scrolls — no
     ride-up. position:sticky pins from the very first pixel only when the
     element's at-rest position EQUALS `top` (otherwise it rides, or gets
     pushed down). #detail has NO padding, so:
       at-rest = sticky .detail-meta band height (~78px) + margin-top (162px)
               = 240px
     So `top: 240px` == at-rest → pinned at 240px the instant it renders,
     no drift. The floating tunnel box is JS-positioned just ABOVE this
     column (box bottom = metaBottom + 170 − 16 ≈ 232), so 240 sits the
     wing/room pills ~8px directly UNDER the box — a tight stack, not the
     ~56px gap the earlier 288 (a wrong +40 #detail-padding assumption)
     produced. The box's vertical anchor in app.js (metaBottom + 170) and
     this 240/162 must stay coupled: box-bottom = metaBottom+154, chips-top
     = metaBottom+162, so the 8px gap = 162 − 154. The chips live in the
     LEFT grid gutter (column 1), never over the centred title/body. */
  top: 240px;
  margin-top: 162px;
  /* Strip the top-bar styling — in side-floating layout the chips
     are individual pills, not a backdrop bar. */
  background: transparent;
  backdrop-filter: none;
  border: 0;
  padding: 0;
  /* margin-right bumped 8 → 24 to nudge the whole tool column a
     hair LEFT — the previous 8px landed it right against the body
     edge and the tool buttons read as crowded against the prose
     gutter. 24 gives them visible breathing room without losing
     their "attached to the body" relationship. */
  margin-right: 24px;
  /* Stack vertically (Wing on top, Room below, then tool groups).
     Fixed 260px column so the two chip pills are forced to the same
     width regardless of their input text length — otherwise "Wing
     claude" overflows wider than "Room rules" and the column looks
     ragged. align-items: stretch makes each child (chips + tool
     rows) fill that 260px so chips and tool-button rows line up
     under the same left/right extents. */
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: stretch;
  gap: 10px;
  width: 210px;
  z-index: 5;
}

/* In the enlarged side-floating layout, force both chip pills to fill
   the column width (inline-flex doesn't stretch by intrinsic-sizing
   rules even when the parent has align-items: stretch) and let the
   input field take all remaining space inside each chip after its
   label. Result: Wing and Room pills are visually identical width,
   their text fields expand to match. */
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-chip {
  width: 100%;
  flex: 0 0 auto;
  box-sizing: border-box;
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-chip-field {
  flex: 1;
  min-width: 0;
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-chip input {
  width: 100%;
}

/* Muted hairline separator between the chips and the first formatting
   group. Stretches across the chips column width via align-self:stretch
   so it visually divides the metadata zone from the writing chrome. */
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-toolbar-sep {
  height: 1px;
  margin: 6px 6px;
  background: var(--line);
  align-self: stretch;
}

/* Tool-group row — NOT a surrounding pill. The group is a transparent
   container holding a row of self-contained circle buttons aligned
   to the left edge of the chips column with a uniform gap. Each
   button is its own circle (border + surface) so the family reads
   as "these icons belong together by row" without needing a
   wrapping pill. flex-start + fixed gap means a 2-button row sits
   adjacent (callout next to link) instead of spreading edge-to-
   edge like space-between would. */
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-group {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
  padding: 0;
  background: transparent;
  border: 0;
  width: 100%;
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool {
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  font-family: inherit;
  line-height: 1;
  cursor: pointer;
  position: relative;
  /* Subtle drop shadow — matches the floating button family
     (.detail-panel-edit / -delete / -tunnel-chip) so the editor
     toolbar reads as "lifted off the page" instead of sitting flat
     on the panel. Lighter than the main-action circles' shadow
     since the chips are smaller and there are many of them — too
     much elevation would visually crowd the column. */
  box-shadow:
    0 3px 8px rgba(0, 0, 0, 0.08),
    0 1px 2px rgba(0, 0, 0, 0.04);
  transition:
    background 0.14s ease,
    color 0.14s ease,
    border-color 0.14s ease,
    box-shadow 0.18s ease,
    transform 0.14s cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* Dark-mode chip shadow lift — same delta as the main buttons. */
:root[data-theme="dark"] .content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool {
  box-shadow:
    0 3px 8px rgba(0, 0, 0, 0.38),
    0 1px 2px rgba(0, 0, 0, 0.22);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool {
    box-shadow:
      0 3px 8px rgba(0, 0, 0, 0.38),
      0 1px 2px rgba(0, 0, 0, 0.22);
  }
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool:hover {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool:active {
  transform: scale(0.9);
}
/* Inline-code button's <> glyph wants monospace + slightly smaller
   so the angle brackets don't crowd the circle. */
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-mono {
  font-family: -apple-system, BlinkMacSystemFont, "SF Mono", Menlo, monospace;
  font-size: 12px;
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool svg {
  width: 17px;
  height: 17px;
}

/* Wrapper around the callout button + its menu. Exists so the menu
   can sit in a positioning context that is NOT inside the button —
   otherwise the button's :active scale(0.9) tap-feedback transform
   propagates into the menu (transforms apply to all descendants,
   even absolutely-positioned ones) and the popover visually
   shrinks while the user is mid-click. Width auto-collapses to
   the button's size, so it lays out as a normal flex item next
   to the link button. */
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-callout-wrap,
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-link-wrap {
  position: relative;
  display: inline-flex;
}

/* Inline URL input popover for the link button. Now globally-
   scoped (selectors NOT prefixed by .content-grid.detail-enlarged
   .detail-edit-grid) because the popover is appended to document.
   body and positioned with position:fixed near the user's
   selection rather than anchored to the tiny chips-column button
   — so the popover follows the text being hyperlinked, not the
   distant toolbar button. Container is heavily rounded (20px) to
   echo the close/maximize circular buttons' family of curves. */
.detail-editor-link-popover {
  min-width: 260px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 20px;
  background: var(--surface-strong);
  backdrop-filter: blur(20px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.05);
  z-index: 200;
  display: flex;
  gap: 6px;
  align-items: center;
}
.detail-editor-link-input {
  flex: 1;
  min-width: 0;
  height: 30px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font-size: 12px;
  font-family: inherit;
  outline: none;
  transition: border-color 0.14s ease, background 0.14s ease;
}
.detail-editor-link-input:focus {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.detail-editor-link-apply {
  height: 30px;
  padding: 0 14px;
  border: 0;
  border-radius: 999px;
  background: var(--accent);
  color: white;
  font-size: 12px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  transition: filter 0.14s ease, transform 0.14s ease;
}
.detail-editor-link-apply:hover {
  filter: brightness(1.05);
}
.detail-editor-link-apply:active {
  transform: scale(0.96);
}

/* "Fake" selection overlay rendered while the link popover is open.
   Mimics the native ::selection highlight so the user can SEE which
   text they're about to hyperlink even though the editor body has
   lost focus to the popover's URL input. Each div is one visual
   line of the captured Range (one DOMRect from getClientRects).
   pointer-events:none so it doesn't intercept clicks; z-index just
   above the editor surface but well below the popover (100). */
.detail-editor-selection-overlay {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 60;
}
.detail-editor-selection-rect {
  position: fixed;
  background: color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius: 2px;
  pointer-events: none;
}
/* Fallback for engines without color-mix() — slightly translucent
   --accent-soft, close enough to the system selection tint. */
@supports not (background: color-mix(in srgb, red 30%, transparent)) {
  .detail-editor-selection-rect {
    background: var(--accent-soft);
  }
}

/* Callout type popover — appears to the RIGHT of the callout button
   (button is now the LAST tool in the row so the menu opens into
   empty padding space with no sibling for it to cover). One row per
   type, accent-tinted hover, glass surface matching the chip auto-
   complete popover. Positioned relative to the wrapper, which
   shares the button's bounding box. */
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-callout-menu {
  position: absolute;
  top: 0;
  left: calc(100% + 6px);
  min-width: 110px;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-strong);
  backdrop-filter: blur(20px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.05);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-callout-menu.hidden {
  display: none;
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-callout-menu button {
  display: block;
  width: 100%;
  padding: 6px 10px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-align: left;
  cursor: pointer;
  font-family: inherit;
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-callout-menu button:hover {
  background: var(--accent-soft);
  color: var(--accent);
}

/* Lift the callout wrapper (and its popover descendant) above any
   adjacent toolbar element while the dropdown is open. Now that
   callout is the rightmost tool in its group there's no sibling to
   the right for the menu to cover, but this still guards against
   any future re-arranging. :has() is well-supported in all modern
   browsers as of 2024. */
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-callout-wrap:has(.detail-editor-callout-menu:not(.hidden)) {
  z-index: 100;
}

/* Side-floating chips + tool groups — both entrance AND exit are
   CSS transitions (no keyframe animations). That's the only way to
   get clean bidirectional interruption: the user can click cancel
   mid-entry or click edit mid-exit and the chips smoothly reverse
   from their current visual state. Keyframe animations can't be
   interrupted-and-reversed cleanly — the animation overrides
   specified styles until it finishes, which is why earlier (with
   entry-as-animation + exit-as-transition) a fast cancel-during-
   entry made the chips "just disappear" instead of gracefully
   reversing.

   Default state = invisible + off-left. JS adds `.is-entered`
   after mount (forcing a reflow first) which transitions the chips
   into their resting position. Adding `.is-leaving` overrides
   `.is-entered` with the leaving state again. Both classes can
   coexist while interrupting — `.is-leaving` wins via specificity
   ordering (declared later in the cascade). */
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-chip,
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-group {
  opacity: 0;
  transform: translateX(-24px);
  /* Entry duration tightened 0.42 → 0.28 to match the snappy
     panel-transition pace elsewhere in the UI. Spring overshoot
     curve preserved — it's the satisfying landing feel, not the
     duration, that makes the spring read as "Apple-native". */
  transition:
    opacity 0.28s cubic-bezier(0.34, 1.3, 0.64, 1),
    transform 0.28s cubic-bezier(0.34, 1.3, 0.64, 1);
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-toolbar-sep {
  opacity: 0;
  transition: opacity 0.18s ease;
}

/* Entered state — JS adds .is-entered after mount + reflow so the
   transitions fire. Stagger delays are set INLINE by JS (not in
   this rule) because they need to differ by direction: entering
   uses the cascade, but REVERSING from an in-flight exit back to
   entered should be instant (no per-chip delay) so the user gets
   immediate feedback when they change their mind. Keeping delays
   in inline style lets JS clear them on reverse. */
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-chip.is-entered,
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-group.is-entered {
  opacity: 1;
  transform: translateX(0);
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-toolbar-sep.is-entered {
  opacity: 1;
}

/* Leaving state — JS adds .is-leaving. Declared AFTER .is-entered
   so it overrides via the cascade (both rules have same specificity
   so source order decides). Faster decelerator curve for the
   "decisive close" feel, vs the bouncy spring used for entry.
   Stagger delays also set inline by JS so the reverse path can
   clear them. */
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-chip.is-leaving,
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-group.is-leaving {
  opacity: 0;
  transform: translateX(-24px);
  /* Exit duration tightened 0.32 → 0.24 to keep the "decisive
     close" beat that's a touch faster than entry. */
  transition:
    opacity 0.24s cubic-bezier(0.4, 0, 0.2, 1),
    transform 0.24s cubic-bezier(0.4, 0, 0.2, 1);
}
.content-grid.detail-enlarged .detail-edit-grid .detail-editor-toolbar-sep.is-leaving {
  opacity: 0;
  transition: opacity 0.16s ease;
}

/* Reduce-motion: skip all transitions AND override the default-
   hidden state so chips appear instantly at full opacity (the new
   base state is opacity:0/translateX(-24px) so chips can transition
   in — without this override, reduce-motion users would see
   permanently invisible chips). .is-leaving still works (snaps to
   opacity:0 instantly without a transition). */
@media (prefers-reduced-motion: reduce) {
  .content-grid.detail-enlarged .detail-edit-grid .detail-editor-chip,
  .content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-group,
  .content-grid.detail-enlarged .detail-edit-grid .detail-editor-toolbar-sep {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .content-grid.detail-enlarged .detail-edit-grid .detail-editor-chip.is-leaving,
  .content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-group.is-leaving,
  .content-grid.detail-enlarged .detail-edit-grid .detail-editor-toolbar-sep.is-leaving {
    opacity: 0;
    transition: none;
  }
}
html.reduce-motion .content-grid.detail-enlarged .detail-edit-grid .detail-editor-chip,
html.reduce-motion .content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-group,
html.reduce-motion .content-grid.detail-enlarged .detail-edit-grid .detail-editor-toolbar-sep {
  opacity: 1;
  transform: none;
  transition: none;
}
html.reduce-motion .content-grid.detail-enlarged .detail-edit-grid .detail-editor-chip.is-leaving,
html.reduce-motion .content-grid.detail-enlarged .detail-edit-grid .detail-editor-tool-group.is-leaving,
html.reduce-motion .content-grid.detail-enlarged .detail-edit-grid .detail-editor-toolbar-sep.is-leaving {
  opacity: 0;
  transition: none;
}
.content-grid.detail-enlarged .detail-edit-grid .markdown-editing {
  grid-column: 2;
  max-width: none;
  margin: 0;
}
.detail-editor-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  padding: 0 4px 0 10px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--line);
  /* Subtle drop shadow matching the .detail-editor-tool family so
     the wing/room pills lift off the panel the same way the format
     chips do. */
  box-shadow:
    0 3px 8px rgba(0, 0, 0, 0.08),
    0 1px 2px rgba(0, 0, 0, 0.04);
}

:root[data-theme="dark"] .detail-editor-chip {
  box-shadow:
    0 3px 8px rgba(0, 0, 0, 0.38),
    0 1px 2px rgba(0, 0, 0, 0.22);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .detail-editor-chip {
    box-shadow:
      0 3px 8px rgba(0, 0, 0, 0.38),
      0 1px 2px rgba(0, 0, 0, 0.22);
  }
}
.detail-editor-chip > span {
  text-transform: uppercase;
  color: var(--muted);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
}
.detail-editor-chip input {
  width: 130px;
  height: 26px;
  padding: 0 10px;
  border: 0;
  background: transparent;
  color: var(--text);
  font-size: 12px;
  font-weight: 600;
  outline: none;
  border-radius: 999px;
  transition: background 0.14s ease;
}
.detail-editor-chip input:focus {
  background: var(--accent-soft);
}

/* Wraps the input + the popover-style autocomplete menu so the menu
   can position itself relative to the input. The menu itself uses
   fixed positioning (set via JS from input.getBoundingClientRect)
   to avoid being clipped by the article's overflow:auto. */
.detail-editor-chip-field {
  position: relative;
  display: inline-flex;
  align-items: center;
}

/* Popover-style autocomplete menu — appears just below the chip
   input when the user is typing or focused. Mirrors macOS Spotlight
   / Safari URL suggestions / Notes tag autocomplete: glass surface,
   rounded corners, soft shadow, accent-tinted highlight for the
   active row.
   Position is absolute relative to the chip-field (which is
   position:relative). Earlier `position: fixed` with JS coord
   calculation broke because ancestor elements (.detail's transient
   slide animations) created their own positioning context — fixed
   started anchoring to the transformed parent instead of the
   viewport, dumping the menu mid-page. Absolute relative to the
   chip-field is rock-solid because there's no animated wrapper in
   between. */
.detail-editor-chip-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 160px;
  max-width: 280px;
  max-height: 240px;
  overflow-y: auto;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface-strong);
  backdrop-filter: blur(20px);
  box-shadow:
    0 12px 32px rgba(0, 0, 0, 0.16),
    0 2px 6px rgba(0, 0, 0, 0.06);
  /* Above pretty much everything else in the panel chrome. */
  z-index: 100;
  scrollbar-width: thin;
}
.detail-editor-chip-menu.hidden {
  display: none;
}
.detail-editor-chip-option {
  display: block;
  width: 100%;
  padding: 7px 12px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  font-size: 12.5px;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.1s ease, color 0.1s ease;
}
.detail-editor-chip-option:hover,
.detail-editor-chip-option.is-active {
  background: var(--accent-soft);
  color: var(--accent);
}
.detail-editor-chip-option:focus-visible {
  outline: none;
}

/* ---------- legacy form-editor (no longer used) ----------
   Kept below for reference only — pencil click now mounts a
   contenteditable .markdown body instead. Safe to delete in a
   cleanup pass. */
/* Padding mirrors .markdown (32px on all sides) so the visual frame
   doesn't shift between view and edit modes — same content origin,
   same right/bottom inset. Without this match, switching into edit
   nudged the whole content block ~28px to the left and looked like
   two different boxes. */
.detail-editor {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 32px;
}

/* Title input — borderless by default, accent underline on focus.
   Font-size, line-height, and weight all mirror .markdown h1
   (32px / 1.12) so the title's optical size in edit mode is
   identical to the rendered H1 the user just clicked through. */
.detail-editor-title {
  width: 100%;
  padding: 6px 8px;
  margin: -6px -8px 6px;
  border: 0;
  border-bottom: 1px solid transparent;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 32px;
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -0.01em;
  outline: none;
  transition: border-color 0.14s ease, background 0.14s ease;
}
.detail-editor-title:focus,
.detail-editor-title:hover {
  border-bottom-color: var(--accent);
  background: var(--accent-soft);
  border-radius: 6px 6px 0 0;
}

/* Wing + room paired inputs — compact, side by side, smaller than
   the title. Labelled inline so the role is clear ("Wing: ___"). */
.detail-editor-meta-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.detail-editor-meta-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.detail-editor-meta-field > span {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.detail-editor-meta-field input {
  height: 34px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
  outline: none;
  transition: border-color 0.14s ease, box-shadow 0.14s ease;
}
.detail-editor-meta-field input:focus {
  border-color: rgba(63, 148, 186, 0.45);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Content textarea — borderless, full-width. Font-size + line-height
   match .markdown (16px / 1.62) so the body text in edit mode has
   the same optical density as the rendered prose. Negative margin
   pulls the textarea to the editor's edge so the hover/focus halo
   doesn't visually inset away from the markdown body's origin. */
.detail-editor-content {
  width: 100%;
  min-height: 200px;
  padding: 10px 8px;
  margin: 0 -8px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 16px;
  line-height: 1.62;
  resize: none;
  outline: none;
  transition: background 0.14s ease, box-shadow 0.14s ease;
}
.detail-editor-content:hover,
.detail-editor-content:focus {
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--line);
}

.detail-editor-hint {
  margin: 0;
  padding: 0 8px;
  font-size: 11.5px;
  color: var(--soft);
  font-style: italic;
}

.detail-editor-error {
  margin: 0;
  padding: 8px 12px;
  border-radius: 8px;
  background: var(--danger-soft);
  color: var(--danger);
  font-size: 13px;
  font-weight: 500;
}

/* Reveal the floating actions on the drawer detail when enlarged.
   updateGridLayout writes data-edit-drawer-id on the edit button (and
   shapes the delete button's dataset) only when a drawer is in view,
   so the attribute selector keeps the buttons hidden in non-drawer
   detail states even if somehow enlarged.
   ENTRY transition (overrides the base "exit" transition): bouncy
   spring on the transform so the buttons rise up + pop into place
   with a satisfying overshoot. The base rule's snappy exit curve
   kicks back in when the class is removed (different transition for
   each direction means entry feels punchy AND exit feels clean). */
.content-grid.detail-enlarged .detail-panel-edit[data-edit-drawer-id],
.content-grid.detail-enlarged .detail-panel-delete[data-drawer-id],
.content-grid.detail-enlarged .detail-panel-retry[data-retry-drawer-id] {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0) scale(1);
  transition:
    opacity 0.26s ease,
    transform 0.4s cubic-bezier(0.34, 1.3, 0.64, 1),
    filter 0.14s ease;
}

/* Tunnel detail: the floating delete button is ALWAYS visible — no
   enlarged state to gate it behind (the tunnel inspector has a fixed
   compact layout, see the .content-grid.detail-kind-tunnel maximize-
   hiding rule below). Same arrival transition as the drawer's
   enlarged-state reveal so the affordance feels consistent even
   though it appears without a maximize toggle. */
.content-grid.detail-kind-tunnel .detail-panel-delete[data-tunnel-id] {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0) scale(1);
  transition:
    opacity 0.26s ease,
    transform 0.4s cubic-bezier(0.34, 1.3, 0.64, 1),
    filter 0.14s ease;
}

/* Tunnel detail: hide the maximize / minimize toggle. The inspector is
   already a focused short layout (meta strip + label + two endpoint
   cards) — enlarging it just adds whitespace without revealing more
   content. Removing the affordance avoids dead-end UI.

   Scoped to #detailPanelMaximize (the detail panel's specific ID), NOT
   .detail-panel-maximize — the drawers panel's browse-mode toggle
   shares the .detail-panel-maximize class for visual parity, and a
   broader selector would hide that button too whenever a tunnel was
   open. A prior version of this rule did exactly that, dropping the
   drawers-panel maximize (and shifting the panel-header chrome that
   sat beneath it) the moment a tunnel detail came into view. */
.content-grid.detail-kind-tunnel #detailPanelMaximize {
  display: none;
}

/* Entry stagger — drawer delete pops 90ms AFTER edit; retry (when
   visible) pops 180ms after edit so the three buttons ripple in
   from right to left. Tunnel view only has the delete button (no
   edit pencil since tunnels have an inline label-edit affordance in
   the body), so the stagger only matters in drawer view. */
.content-grid.detail-enlarged .detail-panel-delete[data-drawer-id] {
  transition-delay: 0.09s;
}
.content-grid.detail-enlarged .detail-panel-retry[data-retry-drawer-id] {
  transition-delay: 0.18s;
}

.content-grid.detail-enlarged .detail-panel-edit[data-edit-drawer-id]:hover,
.content-grid.detail-enlarged .detail-panel-delete[data-drawer-id]:hover,
.content-grid.detail-enlarged .detail-panel-retry[data-retry-drawer-id]:hover,
.content-grid.detail-kind-tunnel .detail-panel-delete[data-tunnel-id]:hover,
.content-grid.detail-enlarged .detail-panel-edit[data-edit-drawer-id]:focus-visible,
.content-grid.detail-enlarged .detail-panel-delete[data-drawer-id]:focus-visible,
.content-grid.detail-enlarged .detail-panel-retry[data-retry-drawer-id]:focus-visible,
.content-grid.detail-kind-tunnel .detail-panel-delete[data-tunnel-id]:focus-visible {
  outline: none;
  transform: translateY(0) scale(1.08);
  filter: brightness(1.06);
}

/* Click pop — keyframe runs on .clicked-pop class, added by the
   click handlers in app.js. Same pattern as the memory cards'
   click feedback, with a slightly larger overshoot (1.15 vs 1.04)
   because these buttons are bigger and the tactile feedback can be
   more pronounced. */
@keyframes detail-panel-btn-click-pop {
  0%   { transform: translateY(0) scale(1); }
  45%  { transform: translateY(0) scale(1.15); }
  100% { transform: translateY(0) scale(1); }
}

.detail-panel-edit.clicked-pop,
.detail-panel-delete.clicked-pop,
.detail-panel-retry.clicked-pop {
  animation: detail-panel-btn-click-pop 0.34s cubic-bezier(0.34, 1.45, 0.64, 1);
}

@media (prefers-reduced-motion: reduce) {
  .detail-panel-edit.clicked-pop,
  .detail-panel-delete.clicked-pop,
  .detail-panel-retry.clicked-pop {
    animation: none;
  }
}

/* Enlarged-mode reading layout — follows standard typography guidelines
   for comfortable reading width (60-75 characters per line). At our
   16px body font this works out to ~720px of text column. Without a
   constraint the markdown would stretch across the full enlarged
   panel (>1200px wide), producing unreadably long lines.
   Approach:
   - Centered max-width column with auto margins on both sides — the
     "breathing room" on either side is the empty panel area.
   - Keep modest internal padding so text doesn't touch the column
     edge (still readable, just a tight column).
   - Bottom padding generously clears the floating edit/delete buttons
     (44px circles at bottom:20px right:20px → need ~120px clear).
   - Detail-meta gets a wider max-width because it's a 4-cell tabular
     header, not flowing prose; readability rules are different.
   - tunnel-detail mirrors the markdown column for the same reading
     comfort. */
/* Metadata bar spans the FULL panel width in enlarged mode — no
   max-width constraint. The reading-column max-width applies only
   to the body (.markdown / .tunnel-detail), which is flowing prose
   that benefits from constrained line length. The metadata strip is
   a tabular header — it reads better edge-to-edge with cells
   spreading naturally via justify-content: space-evenly, matching
   the panel's top border. */

/* Drawer-id short/full swap: in normal-width view we show only the
   short hash suffix (one segment, fits the narrow column); in
   enlarged view we show the full drawer_id (plenty of horizontal
   room now). The strong's data-copy-drawer-id still holds the full
   id either way, so clicking always copies the full thing. */
.drawer-id-full {
  display: none;
}
.content-grid.detail-enlarged .drawer-id-short {
  display: none;
}
.content-grid.detail-enlarged .drawer-id-full {
  display: inline;
}

/* Enlarged-only metadata cell (e.g. Location) — hidden in normal
   view to keep the 4-cell layout compact, revealed when enlarged
   where the extra horizontal room comfortably fits a 5th cell. */
.detail-meta .meta-cell-enlarged {
  display: none;
}
.content-grid.detail-enlarged .detail-meta {
  /* Known-good (restored): five content-sized columns evenly spread
     across the wider panel — same behaviour as the base rule, just
     with the extra Location cell. */
  grid-template-columns: repeat(5, auto);
}
.content-grid.detail-enlarged .detail-meta .meta-cell-enlarged {
  display: grid;
}

/* Re-center the dotted-underline background under the full id when
   enlarged so the dotted "click-to-copy" hint tracks the visible
   text. (Background-size + repeat means the dots naturally fill
   whatever width the strong takes — nothing extra needed.) */

.content-grid.detail-enlarged .markdown {
  max-width: 760px;
  margin-left: auto;
  margin-right: auto;
  /* 40 top / 40 bottom — symmetric, matches view-mode feel. The
     floating action buttons (52px tall, 24px from panel bottom)
     do overlap the last ~36px of content when scrolled to bottom,
     but the .detail mask-image fade (see below) softens that
     visually so it reads as content gracefully dissolving into
     the buttons rather than being chopped off. Same pattern iOS
     uses for content under a translucent tab bar. */
  padding: 40px 32px;
}

.content-grid.detail-enlarged .tunnel-detail {
  max-width: 760px;
  margin-left: auto;
  margin-right: auto;
  padding-bottom: 40px;
}

/* When the panel is enlarged, increase the bottom mask-fade from
   the default 18px to ~80px so the last lines of content fade
   gracefully behind the floating action buttons instead of being
   visually clipped by them. BOTTOM fade only — the TOP fade was
   removed (it used to be `transparent 0, black 18px`, which dimmed
   the top of the sticky .detail-meta band, showing as a useless
   gradient over the metadata header in maximized view). Mirrors the
   non-enlarged .detail rule, which is also bottom-only; the body-text
   fade under the divider is handled by .detail-meta::after, not a top
   mask. Applied in BOTH view and edit modes — earlier
   :not(.detail-editing) scope inadvertently dropped the fade
   when entering edit mode, which looked like a bug (the cap
   suddenly appeared sharp again where it had been soft moments
   before). */
.content-grid.detail-enlarged .detail {
  -webkit-mask-image: linear-gradient(to bottom, black 0, black calc(100% - 80px), transparent 100%);
  mask-image: linear-gradient(to bottom, black 0, black calc(100% - 80px), transparent 100%);
}

/* ---------- Tunnel inspector (right pane) ----------
   Activated by clicking a tunnel entry box in the room expansion. The
   layout is a stack: header → route (two endpoint cards + arrow) → label →
   metadata footer → actions. Endpoint cards are buttons so they read as
   tappable; the directional arrow between them is a CSS-shrinkable SVG so
   the route collapses gracefully in a narrow detail pane. */

.tunnel-detail {
  position: relative;
  display: grid;
  gap: 18px;
  padding: 24px;
  /* container so the @container query below can target this pane's width
     rather than the viewport — the detail panel is user-resizable. */
  container-type: inline-size;
}

/* Tunnel-detail header chrome (.tunnel-detail-header / -eyebrow / -id-row /
   .tunnel-detail-meta / .tunnel-detail-actions) removed — tunnel id,
   created, updated, and kind now render in a .detail-meta strip at the
   top of the panel, matching the memory detail's layout. The inline
   "Copy ID" button is gone too; the id itself is click-to-copy via
   data-copy-tunnel-id. Delete is a floating .detail-panel-delete (same
   affordance as the memory detail), wired via data-delete-scope. */

/* The route is the visual centerpiece: source card → graph-edge → target
   card. Stacked vertically rather than side-by-side because in the
   resizable detail pane (especially in enlarged mode) horizontal
   placement left a wide band of dead space below the cards while
   forcing each card to be too narrow for its content. Vertical stack
   uses the available column the same way a Mail conversation thread
   does — one item above the other with a thin connector between, and
   the long axis of the panel actually doing visual work. */
.tunnel-route {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  align-items: stretch;
  justify-items: stretch;
}

.tunnel-endpoint {
  display: grid;
  gap: 6px;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface);
  align-content: start;
  min-width: 0;
}

/* Header row inside an endpoint card: FROM/TO eyebrow on the left,
   "Open memory →" link on the right when the endpoint is drawer-bound.
   The link takes the place of the old card-bottom button so the action
   sits inside the card chrome instead of claiming its own row + push-
   ing the "About this connection" block below the viewport fold. */
.tunnel-endpoint-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.tunnel-endpoint-eyebrow {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.tunnel-endpoint-wing {
  font-size: 12px;
  font-weight: 600;
  color: var(--soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tunnel-endpoint-room {
  font-size: 19px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.005em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tunnel-endpoint-stat {
  margin-top: 2px;
  font-size: 12px;
  color: var(--muted);
}

.tunnel-endpoint-drawer {
  margin-top: 6px;
  padding: 8px 10px;
  border: 1px dashed var(--line);
  border-radius: 10px;
  background: var(--surface-strong);
  display: grid;
  gap: 2px;
  min-width: 0;
}

.tunnel-endpoint-drawer-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.tunnel-endpoint-drawer strong {
  font-size: 13px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Compact text-link version — sits in the endpoint-head row to the right
   of the FROM/TO eyebrow. No border, no big padded button surface; just
   inline text + arrow with an accent-color hover state. Keeps the card
   chrome lean so the route+label fit comfortably in one viewport. */
.tunnel-endpoint-open {
  margin: 0;
  padding: 4px 6px 4px 2px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: opacity 0.12s ease, transform 0.18s cubic-bezier(0.34, 1.3, 0.64, 1);
}

.tunnel-endpoint-open:hover {
  opacity: 0.7;
}

.tunnel-endpoint-open:active {
  transform: translateX(2px);
}

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

/* Neutral connector between the stacked endpoint cards — the same
   graph-edge glyph as the rooms-panel tunnel indicator (vertical
   node-line-node) so the visual vocabulary is consistent. In the
   vertical layout it sits in its own row, centered, with a generous
   height so the line reads as a real "tunnel" link rather than a
   tiny chip squeezed between two cards. */
.tunnel-route-connector {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 44px;
  color: var(--accent);
}
.tunnel-route-connector svg {
  width: 32px;
  height: 32px;
}

.tunnel-detail-label {
  display: grid;
  gap: 8px;
  padding: 14px 16px;
  border-radius: 14px;
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.tunnel-detail-label.tunnel-detail-label-empty {
  background: var(--surface);
  border: 1px dashed var(--line);
}

.tunnel-detail-label p {
  margin: 0;
  font-size: 14px;
  line-height: 1.52;
  color: var(--text);
}

.tunnel-detail-label-empty p {
  color: var(--muted);
  font-style: italic;
}

.tunnel-detail-section-label {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

/* Label head: section title on the left, edit pencil on the right. Pencil
   only appears on hover/focus of the surrounding label section to keep the
   chrome quiet until the user is looking at it. */
.tunnel-detail-label-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.tunnel-detail-edit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--soft);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.14s ease, background 0.12s ease, color 0.12s ease;
}

.tunnel-detail-label:hover .tunnel-detail-edit,
.tunnel-detail-edit:focus-visible {
  opacity: 1;
}

.tunnel-detail-edit:hover,
.tunnel-detail-edit:focus-visible {
  background: var(--surface-strong);
  color: var(--accent);
  outline: none;
}

.tunnel-detail-edit svg {
  width: 14px;
  height: 14px;
}

.tunnel-detail-label-editor {
  width: 100%;
  min-height: 96px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  line-height: 1.52;
  resize: vertical;
}

.tunnel-detail-label-editor:focus {
  outline: none;
  border-color: var(--accent);
}

.tunnel-detail-label-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

/* Bind-a-memory inline picker. <details>/<summary> so the disclosure state
   is browser-managed; the <select> handles the actual choice. */
.tunnel-endpoint-bind {
  margin-top: 4px;
}

.tunnel-endpoint-bind > summary {
  list-style: none;
  cursor: pointer;
  padding: 6px 0;
  color: var(--soft);
  font-size: 12px;
  font-weight: 600;
  transition: color 0.12s ease;
}

.tunnel-endpoint-bind > summary::-webkit-details-marker { display: none; }

.tunnel-endpoint-bind > summary::before {
  content: "+ ";
  color: var(--accent);
  font-weight: 700;
}

.tunnel-endpoint-bind > summary:hover,
.tunnel-endpoint-bind[open] > summary {
  color: var(--accent);
}

.tunnel-endpoint-bind select {
  width: 100%;
  margin-top: 4px;
  padding: 7px 8px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 12px;
}

/* Unbind × sits inside the .tunnel-endpoint-drawer chip in the top-right
   corner. Visible on hover/focus of the chip so it isn't visual noise when
   you're just reading the binding. */
.tunnel-endpoint-drawer {
  position: relative;
  padding-right: 30px;
}

.tunnel-endpoint-unbind {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.14s ease, background 0.12s ease, color 0.12s ease;
}

.tunnel-endpoint-drawer:hover .tunnel-endpoint-unbind,
.tunnel-endpoint-unbind:focus-visible {
  opacity: 1;
}

.tunnel-endpoint-unbind:hover,
.tunnel-endpoint-unbind:focus-visible {
  background: var(--surface);
  color: var(--accent);
  outline: none;
}

/* showStatus() flips this class on for errors so the message looks distinct
   from a neutral "Saving…" indicator. */
.write-status.status-error {
  color: #b00020;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .write-status.status-error {
    color: #ff6b7a;
  }
}

:root[data-theme="dark"] .write-status.status-error {
  color: #ff6b7a;
}

/* .tunnel-detail-meta and .tunnel-detail-actions removed — tunnel
   metadata now renders in the shared .detail-meta strip at the top
   of the panel (same selectors the memory detail uses), and the
   inline action buttons (swap + delete) are replaced by the floating
   .detail-panel-delete corner button. */

.markdown {
  padding: 32px;
  line-height: 1.62;
  font-size: 16px;
  /* Default arrow cursor in read-only view so the panel doesn't
     telegraph "you can type here" when actually the body is locked.
     Selection still works via click-and-drag; we're just changing
     the hover affordance, not the interaction. The .markdown-editing
     override below restores the text caret once edit mode is on. */
  cursor: default;
}
/* Editor mode: text caret so the user knows they can click in and
   start typing. Declared after .markdown so it wins for same-
   specificity matches on the dual-class .markdown.markdown-editing
   element used by the inline editor. */
.markdown-editing {
  cursor: text;
}

.markdown h1 {
  margin-bottom: 18px;
  font-size: 32px;
  line-height: 1.12;
}

.markdown h2 {
  margin: 18px 0 8px;
  font-size: 20px;
}

.markdown h3 {
  margin: 16px 0 8px;
}

.markdown p {
  margin-bottom: 9px;
}

/* Real list elements with native bullet / number markers. Replaces the
   old `.bullet` paragraph that just indented text without any glyph in
   front. `list-style-position: outside` keeps the marker in the gutter
   so wrapped lines align under the first character (not under the
   bullet). Tinted marker color via ::marker — supported in all current
   browsers (Chrome 86+, Safari 11.1+, Firefox 68+); falls back to the
   inherited text colour on older engines, which is also fine. */
.markdown ul,
.markdown ol {
  margin: 6px 0;
  padding-left: 24px;
}
.markdown ul {
  list-style: disc outside;
}
.markdown ol {
  list-style: decimal outside;
}
.markdown li {
  margin: 2px 0;
}
.markdown li::marker {
  color: var(--muted);
}
/* Nested lists (rare, but possible inside drawers): tighter top margin
   and a different marker so two levels visually separate. */
.markdown ul ul {
  list-style-type: circle;
  margin-top: 0;
}
.markdown ol ol {
  list-style-type: lower-alpha;
  margin-top: 0;
}

/* Blank-line spacer emitted by markdownLite for any blank source line.
   Height is 0 because the natural margin-bottom on <p> / <h*> elements
   already provides the right amount of separation between blocks. A
   visible spacer on top of those margins compounded into a sparse,
   exhausting-to-scan layout (especially in drawers authored with
   classic double-blank-line markdown habits). Writers are asked to use
   the tight single-return format (see CLAUDE.md / the canonical
   attribution drawer); this rule keeps the UI compact even when a
   stray blank line slips through. */
.space {
  height: 0;
}

/* Horizontal rule (`---` / `___` / `***` on its own line) — thin
   accent-tinted divider, more elegant than the default browser <hr>. */
.markdown .markdown-hr {
  border: 0;
  height: 1px;
  margin: 22px 0;
  background: var(--line);
}

/* Plain blockquote — `> some quoted text`. Left-rail with a soft
   colour, subtle inset background using the same --code-bg pattern
   so blockquotes and code blocks feel like a related family of
   "out-of-flow" callout chrome. Inner <p> margins are tightened so
   short quotes don't have awkward whitespace. */
.markdown blockquote {
  margin: 14px 0;
  padding: 10px 14px;
  border-left: 3px solid var(--soft);
  border-radius: 4px;
  background: var(--code-bg);
  color: var(--text);
}

.markdown blockquote > p {
  margin: 0 0 6px;
}

.markdown blockquote > p:last-child {
  margin-bottom: 0;
}

/* GitHub-style admonition: a `> [!NOTE]` (or TIP/IMPORTANT/WARNING/
   CAUTION) on the first line of a blockquote run promotes it to a
   coloured callout with a bold uppercase label. The accent colour for
   each type is parameterised via the local custom property `--c`, so
   the body styling below stays DRY — only the variant rules below
   change `--c`. color-mix() composes the subtle tinted background
   from the same `--c`, works in all current browsers (Chrome 111+,
   Safari 16.4+, Firefox 113+) and degrades to the default --code-bg
   on older engines because the .callout selector doesn't override
   the base blockquote `background` until the @supports check kicks
   in below. */
.markdown blockquote.callout {
  border-left-width: 3px;
  border-left-color: var(--c, var(--soft));
}

@supports (background: color-mix(in srgb, red 50%, transparent)) {
  .markdown blockquote.callout {
    background: color-mix(in srgb, var(--c, var(--soft)) 9%, transparent);
  }
}

.markdown blockquote.callout .callout-label {
  margin-bottom: 6px;
  color: var(--c, var(--soft));
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
}

.markdown blockquote.callout-note      { --c: #3f94ba; } /* info — matches --accent */
.markdown blockquote.callout-tip       { --c: #34a853; } /* positive / success */
.markdown blockquote.callout-important { --c: #8e51d8; } /* attention */
.markdown blockquote.callout-warning   { --c: #d49a00; } /* caution */
.markdown blockquote.callout-caution   { --c: #fe5f63; } /* danger — matches --danger token */

/* In-editor remove button — sits in the top-right corner of each
   callout while editing. The button is injected (contenteditable=
   false) so the caret can't land in it, and is stripped from
   serialization via the HTML→MD walker's skip-class set. Visually
   it's hidden by default and fades in on callout hover (Apple
   "reveal on intent" pattern — same vocabulary as the macOS
   "delete file" affordance in Finder previews). Click → removes the
   entire callout + syncs the buffer. */
.markdown-editing blockquote.callout {
  position: relative;
}
.markdown-editing blockquote.callout .callout-remove {
  position: absolute;
  top: 8px;
  right: 8px;
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--surface);
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
  font-family: inherit;
  cursor: pointer;
  opacity: 0;
  transition:
    opacity 0.16s ease,
    background 0.14s ease,
    color 0.14s ease,
    transform 0.14s cubic-bezier(0.34, 1.3, 0.64, 1);
}
.markdown-editing blockquote.callout:hover .callout-remove,
.markdown-editing blockquote.callout:focus-within .callout-remove,
.markdown-editing blockquote.callout .callout-remove:focus-visible {
  opacity: 1;
}
.markdown-editing blockquote.callout .callout-remove:hover {
  background: var(--danger, #fe5f63);
  color: white;
}
.markdown-editing blockquote.callout .callout-remove:active {
  transform: scale(0.9);
}

/* Markdown links + autolinked bare URLs — rendered by inlineFormat
   in app.js. Subtle underline with the accent colour so links read
   as interactive without competing visually with the body text.
   Wiki-links (.wiki-link) keep their own styling — they're internal
   navigation, while these are external. */
.markdown a:not(.wiki-link) {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-color 0.14s ease;
}

.markdown a:not(.wiki-link):hover,
.markdown a:not(.wiki-link):focus-visible {
  text-decoration-color: var(--accent);
  outline: none;
}

/* Markdown tables — rendered by markdownLite when it detects a pipe-
   row run with a `| --- |` separator in row 2. Rounded outer border
   so the table reads as one cohesive surface; header row tinted via
   --code-bg (same neutral darker tint we use for inline code chips
   and the metadata bar). border-collapse:separate + border-spacing:0
   lets the border-radius actually clip the corner cells, which
   collapse mode wouldn't allow. */
.markdown .md-table {
  width: 100%;
  margin: 18px 0;
  font-size: 14px;
  border: 1px solid var(--line);
  border-radius: 10px;
  border-collapse: separate;
  border-spacing: 0;
  overflow: hidden;
}

.markdown .md-table th,
.markdown .md-table td {
  padding: 10px 14px;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--line);
}

.markdown .md-table th {
  font-weight: 600;
  color: var(--text);
  background: var(--code-bg);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.markdown .md-table td {
  color: var(--text);
  line-height: 1.5;
}

/* Last row: no bottom border (the table's outer border closes it). */
.markdown .md-table tbody tr:last-child td {
  border-bottom: none;
}

/* Row hover removed — in viewing mode a memory's markdown body is a
   document surface, not an interactive list. The accent-tint flash on
   every cursor pass-by made the page feel twitchy without helping
   readers track rows (which is what the original "subtle hover for
   long tables" intent was). Tables are usually short (3-8 rows) so
   the eye doesn't need a row tracker anyway. Removed 2026-05-28.
   If hover-track is wanted back for a specific surface — say, the
   editor's table-edit affordances — scope it to .markdown-editing
   only so viewing-mode bodies stay quiet. */

.facts-panel {
  margin-top: 16px;
}

.facts {
  display: grid;
  gap: 8px;
  max-height: 300px;
  overflow: auto;
  padding: 12px;
}

.fact {
  display: grid;
  grid-template-columns: minmax(120px, 1fr) minmax(140px, 0.8fr) minmax(160px, 1.3fr) auto;
  gap: 10px;
  /* align-items:start (was center) so each cell wraps independently
     and the row's End button stays anchored to the top of the row
     when cells expand vertically — keeps the layout readable when
     facts have long object strings. */
  align-items: start;
  border-radius: 14px;
  padding: 12px;
  background: rgba(255, 255, 255, 0.62);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .fact {
    background: rgba(44, 44, 46, 0.64);
  }
}

:root[data-theme="dark"] .fact {
    background: rgba(44, 44, 46, 0.64);
  }

.fact strong {
  color: var(--accent);
}

/* Allow long subject/predicate/object values to wrap onto multiple
   lines instead of truncating with ellipsis. overflow-wrap:anywhere
   handles strings without spaces (long identifiers, URLs) by
   breaking at any character; word-break:break-word keeps natural
   word boundaries where they exist. line-height bumped slightly so
   wrapped text stays readable. */
.fact span,
.fact strong {
  overflow-wrap: anywhere;
  word-break: break-word;
  line-height: 1.4;
}

.fact em {
  color: var(--muted);
  font-size: 12px;
  font-style: normal;
}

.empty-list,
.fatal {
  padding: 24px;
  color: var(--muted);
}

.write-sheet,
.edit-sheet {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 24px;
}

.confirm-sheet {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: 24px;
}

.confirm-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.24);
  backdrop-filter: blur(18px);
}

.confirm-panel {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 12px;
  width: min(420px, 100%);
  border: 1px solid var(--line);
  border-radius: 24px;
  padding: 24px;
  background: var(--surface-strong);
  box-shadow: 0 28px 90px rgba(0, 0, 0, 0.24);
  text-align: center;
}

.confirm-icon {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: 999px;
  background: rgba(254, 95, 99, 0.1);
  color: #fe5f63;
  font-size: 24px;
  font-weight: 800;
}

.confirm-panel h2 {
  font-size: 22px;
}

.confirm-panel p {
  color: var(--text);
  line-height: 1.45;
}

.confirm-warning {
  max-width: 100%;
  overflow-wrap: anywhere;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.4;
}

/* Empty warning shouldn't take vertical space — collapse the div
   entirely when there's no edge-case note to show. :empty matches
   an element with no children (textContent="" qualifies). */
.confirm-warning:empty {
  display: none;
}

/* Static recovery hint — small faded line that reassures the user
   the action isn't catastrophic. Always present in the panel
   markup. Uses opacity on top of --muted instead of color-mix —
   opacity applies reliably in every browser (color-mix sometimes
   wasn't taking effect, leaving the hint at default text color).
   Result is a hint at ~35% effective opacity vs --muted's
   baseline gray, which is distinctly lighter than the surrounding
   body and warning text. */
.confirm-hint {
  margin: 0;
  font-size: 11px;
  line-height: 1.4;
  color: var(--muted);
  opacity: 0.55;
  font-weight: 400;
  letter-spacing: 0.01em;
}
/* Permanent-deletion variant — used by Empty Recently-Deleted and any
   future flow where the standard "Recoverable from the trash bin"
   reassurance is replaced by a permanence warning. Reads in the
   --danger red and at higher opacity so it actually registers. */
.confirm-hint.danger {
  color: var(--danger);
  opacity: 0.85;
  font-weight: 500;
}

.confirm-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  width: 100%;
  margin-top: 4px;
}

.danger-action {
  min-height: 44px;
  border: 1px solid rgba(254, 95, 99, 0.18);
  border-radius: 999px;
  padding: 0 18px;
  background: #fe5f63;
  color: white;
  cursor: pointer;
  font-weight: 700;
}

.danger-action:hover {
  background: #bd0013;
  box-shadow: 0 0 0 4px rgba(254, 95, 99, 0.12);
}

.danger-action:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

.write-sheet.hidden,
.edit-sheet.hidden,
.hidden {
  display: none !important;
}

.write-backdrop,
.edit-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.24);
  backdrop-filter: blur(18px);
}

.write-panel,
.edit-panel {
  position: relative;
  display: grid;
  gap: 14px;
  width: min(720px, 100%);
  max-height: min(820px, calc(100vh - 48px));
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: 24px;
  padding: 22px;
  background: var(--surface-strong);
  box-shadow: 0 28px 90px rgba(0, 0, 0, 0.24);
}

.write-header {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: 18px;
  padding-bottom: 4px;
}

.write-header h2 {
  font-size: 24px;
}

.write-header p,
.write-panel label span,
.edit-panel label span {
  /* .commit-confirm span dropped 2026-05-28 alongside .commit-confirm —
     same dead-element cleanup. The remaining three selectors are live. */
  color: var(--muted);
  font-size: 13px;
}

.icon-button {
  min-height: 34px;
  padding: 0 14px;
}

.write-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.write-panel label,
.edit-panel label {
  display: grid;
  gap: 7px;
}

.combo-field {
  position: relative;
}

.combo {
  position: relative;
}

.combo-toggle {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  display: inline-grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--soft);
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease, transform 160ms ease;
}

.combo-toggle svg {
  width: 16px;
  height: 16px;
}

.combo-toggle:hover {
  background: var(--accent-soft);
  color: var(--accent);
}

.combo[data-open="true"] .combo-toggle {
  color: var(--accent);
  transform: translateY(-50%) rotate(180deg);
}

.combo input[data-combo-input] {
  padding-right: 42px;
}

.combo-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 30;
  max-height: 240px;
  padding: 6px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: var(--shadow);
  backdrop-filter: blur(24px);
}

.combo-menu.hidden {
  display: none;
}

.combo-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  border: 0;
  border-radius: 10px;
  padding: 8px 10px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  text-align: left;
  font-size: 14px;
}

.combo-option:hover,
.combo-option[aria-selected="true"] {
  background: var(--accent-soft);
  color: var(--accent);
}

.combo-option strong {
  font-weight: 500;
}

.combo-option .combo-option-slug {
  font-size: 12px;
  color: var(--soft);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

.combo-option-hint {
  padding: 8px 10px;
  font-size: 12px;
  color: var(--muted);
}

.combo-option-create {
  border-top: 1px solid var(--line);
  margin-top: 6px;
  padding-top: 12px;
}

.combo-option-create strong {
  color: var(--accent);
}

.write-panel input,
.write-panel textarea,
.write-panel select,
.edit-panel textarea {
  width: 100%;
  border: 1px solid var(--line);
  border-radius: 14px;
  background-color: rgba(255, 255, 255, 0.78);
  color: var(--text);
  outline: none;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .write-panel input,
  :root:not([data-theme="light"]) .write-panel textarea,
  :root:not([data-theme="light"]) .write-panel select,
  :root:not([data-theme="light"]) .edit-panel textarea {
    background-color: rgba(28, 28, 30, 0.88);
  }
}

:root[data-theme="dark"] .write-panel input,
:root[data-theme="dark"] .write-panel textarea,
:root[data-theme="dark"] .write-panel select,
:root[data-theme="dark"] .edit-panel textarea {
    background-color: rgba(28, 28, 30, 0.88);
  }

.write-panel input,
.write-panel select {
  height: 44px;
  padding: 0 13px;
}

.write-panel select {
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position:
    calc(100% - 18px) 19px,
    calc(100% - 13px) 19px;
  background-repeat: no-repeat;
  background-size: 5px 5px;
  padding-right: 34px;
}

.write-panel textarea,
.edit-panel textarea {
  min-height: 210px;
  resize: vertical;
  padding: 13px;
  line-height: 1.5;
}

.edit-panel textarea {
  min-height: min(460px, 54vh);
}

.write-panel input:focus,
.write-panel textarea:focus,
.write-panel select:focus,
.edit-panel textarea:focus {
  border-color: rgba(63, 148, 186, 0.45);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

.write-warning {
  border: 1px solid rgba(255, 149, 0, 0.28);
  border-radius: 14px;
  padding: 12px;
  background: rgba(255, 149, 0, 0.1);
  color: var(--text);
  font-size: 13px;
}

.write-actions {
  display: grid;
  grid-template-columns: auto minmax(160px, 1fr) auto;
  gap: 12px;
  align-items: end;
}

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

.save-memory {
  justify-self: end;
}

.primary-action,
.secondary-action {
  min-height: 44px;
  padding: 0 18px;
  font-weight: 700;
}

.primary-action {
  background: var(--accent);
  color: white;
}

.primary-action:disabled {
  cursor: not-allowed;
  opacity: 0.45;
}

/* .commit-confirm removed 2026-05-28 — was a layout class for an
   inline "are you sure?" block that was never wired up. No matching
   element in index.html or any app.js template literal. */

.write-status {
  min-height: 20px;
  color: var(--muted);
  font-size: 13px;
}

.write-status.success {
  color: var(--text);
}

.write-status.error {
  color: #fe5f63;
}

.draft-list {
  display: grid;
  gap: 8px;
}

.draft-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 44px;
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 0 12px;
  background: rgba(255, 255, 255, 0.58);
  color: var(--text);
  cursor: pointer;
  text-align: left;
}

.draft-item span {
  color: var(--muted);
  font-size: 12px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .draft-item {
    background: rgba(44, 44, 46, 0.62);
  }
}

:root[data-theme="dark"] .draft-item {
    background: rgba(44, 44, 46, 0.62);
  }

@media (max-width: 980px) {
  .shell {
    grid-template-columns: 1fr;
  }

  .sidebar {
    position: static;
    height: auto;
  }

  .nav {
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  }

  .main {
    padding: 20px;
  }

  .topbar {
    flex-wrap: wrap;
  }

  .topbar .search {
    flex-basis: 100%;
    max-width: none;
  }

  .content-grid {
    grid-template-columns: 1fr;
    display: grid;
  }

  .top-actions {
    flex: 1 1 auto;
    justify-content: flex-end;
    flex-wrap: wrap;
  }

  .rooms-panel,
  .drawers-panel,
  .detail-panel {
    height: auto;
    min-height: 360px;
  }

  .rooms-panel {
    min-height: 240px;
  }

  .room-nav,
  .drawer-list,
  .detail {
    max-height: none;
  }

  .search {
    min-width: 0;
  }

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

  .stat:nth-child(2n + 1)::before {
    display: none;
  }

  .stat:nth-child(n + 3)::after {
    content: "";
    position: absolute;
    left: 14px;
    right: 14px;
    top: 0;
    height: 1px;
    background: var(--line);
  }

  .fact {
    grid-template-columns: 1fr;
  }
}

/* ---------- v13 additions: toolbar, sort/author selects, drafts/trash/fact sheets, [[links]] ---------- */

.top-actions {
  flex-wrap: wrap;
}

.toolbar-button {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 14px;
  font-weight: 600;
  font-size: 14px;
}

/* Both .toolbar-button and .view-toggle have explicit display:inline-flex
   that beats the user-agent [hidden]{display:none} on specificity.
   These targeted overrides restore the semantic that setting an
   element's `hidden` attribute (or .hidden=true in JS) actually hides
   it. Without them, e.g. `labBtn.hidden = !getShowTools()` had no
   visible effect — the Tools button stayed in the row even with the
   Settings toggle off. Same fix applies to the KG view-toggle. */
.toolbar-button[hidden],
.view-toggle[hidden] {
  display: none;
}

.badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--accent);
  color: white;
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
}

.hint {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-strong);
  color: var(--soft);
  font-family: -apple-system, BlinkMacSystemFont, monospace;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.panel-tools {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* The inline "Mark read" pill was removed 2026-05-28 in favor of the
   notification bell in the top bar. Styles below cover the bell + its
   badge + dropdown panel + avatar + item rows. */

/* Notification bell button. Reuses .icon-button + .menu-button base
   chrome; the .notifications-bell specifier gives us hooks for the
   badge positioning AND the swing animation on the inner svg. The
   svg's transform-origin is set to top-centre so the bell rocks on
   its hanger like a real one, not pivoting from its centre. */
.notifications-bell {
  position: relative;
}
.notifications-bell svg {
  transform-origin: top center;
}
/* Badge — white-on-red pill, picking up the new --danger token
   (#fe5f63). Earlier versions wrapped this in a 2px --bg ring
   (box-shadow: 0 0 0 2px var(--bg)) to separate the pill from the
   bell outline; removed per user preference — the badge now sits
   directly against whatever's behind it. */
.notifications-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--danger);
  color: white;
  font-size: 10px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  pointer-events: none;
}
.notifications-badge.hidden { display: none; }
/* Bell-swing animation — runs continuously while there are unseen
   updates so the bell feels "alive" / unanswered. Apple's Reminders
   and Mail use steady gentle motion for unread state rather than a
   one-shot pulse; matches that vocabulary. The swing arcs ±15° from
   the top-centre pivot, peaks four times in the first half of the
   cycle (like a physical bell ringing), then sits still for the
   second half so the motion doesn't read as frantic. 2.4s cycle —
   fast enough to register as "active alert," slow enough not to nag.
   When the count drops to zero, .has-unseen comes off the bell and
   the svg returns to its static rest state. */
@keyframes notifications-bell-swing {
  0%, 50%, 100% { transform: rotate(0deg); }
  10%           { transform: rotate(15deg); }
  20%           { transform: rotate(-12deg); }
  30%           { transform: rotate(9deg); }
  40%           { transform: rotate(-6deg); }
}
.notifications-bell.has-unseen svg {
  animation: notifications-bell-swing 2.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .notifications-bell.has-unseen svg { animation: none; }
}
html.reduce-motion .notifications-bell.has-unseen svg { animation: none; }

/* Dropdown panel. Anchored under the bell via the existing
   .menu-wrap positioning system. Wider than the theme dropdown
   because notification items need horizontal room for the avatar
   + title + relative time. Max-height + internal scroll so a big
   batch of updates doesn't push the panel off the viewport. */
.notifications-menu {
  width: 340px;
  max-height: 480px;
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.notifications-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 12px 14px 8px;
  border-bottom: 1px solid var(--line);
}
.notifications-head-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.005em;
}
.notifications-head-count {
  font-size: 11px;
  color: var(--muted);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.notifications-list {
  flex: 1 1 auto;
  overflow-y: auto;
  /* Suppress the native scrollbar — same uncluttered-focus reason
     as the settings sheet. Wheel/touch scroll still works. */
  scrollbar-width: none;
}
.notifications-list::-webkit-scrollbar { width: 0; display: none; }
.notifications-empty {
  padding: 28px 14px;
  text-align: center;
  font-size: 12.5px;
  color: var(--muted);
}
.notifications-empty.hidden { display: none; }
/* Mark-all-as-seen footer button — full-width, pinned at the bottom
   of the panel via flex order, separated from the list by a divider. */
.notifications-mark-all {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 11px 14px;
  border: 0;
  border-top: 1px solid var(--line);
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.14s ease;
}
.notifications-mark-all:hover { background: var(--accent-soft); }
.notifications-mark-all:active { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.notifications-mark-all:focus-visible { outline: none; background: var(--accent-soft); }
.notifications-mark-all svg { width: 13px; height: 13px; display: block; }
.notifications-mark-all.hidden { display: none; }

/* Individual notification entry. Avatar + body block + time. The
   whole row is the click target so hover-tint should feel like a
   tappable list row (Mail / Notes / Reminders convention). */
.notif-item {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  padding: 10px 14px;
  border: 0;
  background: transparent;
  text-align: left;
  font: inherit;
  cursor: pointer;
  transition: background 0.14s ease;
  /* Separator hairline between rows. Last row's bottom border is
     handled by the .notifications-mark-all top-border above, so no
     need for a :last-child trim here. */
  border-bottom: 1px solid var(--line);
}
.notif-item:last-child { border-bottom: 0; }
.notif-item:hover { background: var(--accent-soft); }
.notif-item:focus-visible { outline: none; background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
.notif-avatar {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: inline-grid;
  place-items: center;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.01em;
  /* Inline background-color / gradient comes from the JS-built spec
     (NOTIFICATION_AVATARS) so the brand hue is per-actor. The human
     fallback path uses the same shape with a deterministic hsl()
     background + the actor's initial as text. */
  overflow: hidden;
}
/* Brand-mark avatars compose: the .notif-avatar circle holds a
   brand-coloured background (set inline by JS — solid for Anthropic
   /OpenAI/xAI, gradient for Google Gemini) and the inner <img>
   points at the MONOCHROME lobe-icons SVG (black on transparent).
   The `filter: brightness(0) invert(1)` chain forces every visible
   pixel of the SVG to white — brightness(0) first crushes any non-
   transparent pixel to pure black, then invert(1) flips it to pure
   white. Net effect: the mark renders white regardless of its
   source colour, so the brand-coloured background stays dominant
   and the white mark reads cleanly on it in both light and dark
   themes. No per-theme override needed — white-on-brand works on
   any surface. The mark sits at ~55% of the circle diameter (16
   inside 30) to leave a healthy ring of brand colour around it —
   same proportion native iOS app icons use to keep the chrome
   reading as the dominant element. */
.notif-avatar-brand {
  overflow: hidden;
}
.notif-avatar-brand img {
  width: 16px;
  height: 16px;
  display: block;
  object-fit: contain;
  filter: brightness(0) invert(1);
}
.notif-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.notif-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.005em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.notif-meta {
  font-size: 11.5px;
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 4px;
}
.notif-author {
  color: var(--soft);
}
.notif-when {
  color: var(--muted);
}

/* Failure entry — visually distinguished from info entries so the
   user can tell at a glance "this isn't a heads-up, this is a
   problem." Left accent bar in --danger gives a strip of red along
   the row's leading edge (matches the macOS Mail unread blue-bar
   idiom but in red for errors). Avatar circle takes --danger-soft
   as bg + --danger as icon colour — a warning triangle inside, not
   a brand mark, since the actor here is "the dashboard / your save
   pipeline" rather than a model.

   On hover the row picks up --danger-soft instead of --accent-soft
   so the error semantic stays consistent through interaction. */
.notif-item-failure {
  box-shadow: inset 3px 0 0 var(--danger);
}
.notif-item-failure:hover,
.notif-item-failure:focus-visible {
  background: var(--danger-soft);
}
.notif-avatar-failure {
  background: var(--danger-soft);
  color: var(--danger);
}
.notif-avatar-failure svg {
  width: 18px;
  height: 18px;
  display: block;
}

/* Edit-event avatar — used when a drawer was UPDATED (vs. created)
   and the actual editor is unknown (MemPalace's update path doesn't
   capture an actor field, so we can't honestly attribute the edit
   to anyone). Neutral slate circle with a pencil glyph signals "an
   edit happened" without falsely claiming WHO did it. The colour is
   --muted on a --surface-strong background so it reads quieter than
   the brand-color creation avatars but quieter still than the red
   failure avatar — fits the "informational, no specific actor"
   register. */
.notif-avatar-edit {
  background: var(--surface-strong);
  color: var(--muted);
  border: 1px solid var(--line);
  box-sizing: border-box;
}
.notif-avatar-edit svg {
  width: 16px;
  height: 16px;
  display: block;
}
/* Header row inside .notif-body for failure entries — title on the
   left flexes to fill, timestamp pinned to the right (no shrink).
   Title still ellipsises if it's longer than the available width.
   This frees the second line (.notif-failure-reason) to be a clean
   full-width wrapping block instead of competing inline with the
   timestamp like the regular .notif-meta layout does. */
.notif-header-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.notif-header-row .notif-title {
  flex: 1 1 auto;
  min-width: 0;
}
.notif-header-row .notif-when {
  flex: 0 0 auto;
  font-size: 11.5px;
  color: var(--muted);
  white-space: nowrap;
}
/* Failure reason gets its own full-width line below the header row.
   Block display + relaxed line-height so multi-sentence errors wrap
   into a readable paragraph rather than a cramped single-line strip
   competing with the timestamp on the right. Top margin breathes it
   off the header row. */
.notif-failure-reason {
  display: block;
  margin-top: 4px;
  color: var(--danger);
  font-weight: 500;
  font-size: 12.5px;
  line-height: 1.45;
  white-space: normal;
}

/* Settings → "Mark all memories as seen" action row. Same general
   shape as settings-card-row-toggle / -select, but with an action
   button (the Clear pill) on the right instead of a switch/select.
   Row-wide hover tint removed 2026-05-28 per the cohesion rule
   established for toggle rows: only the actionable element on the
   right (the Clear pill) should advertise "click me," not the entire
   row. The pill carries its own hover state via .secondary-action
   / .icon-button base styles, so the tactile signal hasn't been
   lost — just narrowed to where it belongs. */

.inline-field {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.inline-field select {
  height: 34px;
  padding: 0 28px 0 10px;
  border-radius: 10px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
  /* Tighter chevron offset to match the smaller pill — the global
     12px right offset would push the glyph too far in this 34px-
     tall version. Background-image, repeat, and size are inherited
     from the global `select` rule above. */
  background-position: right 10px center;
  background-size: 11px;
}

.drawer-kicker {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.drawer-date {
  color: var(--soft);
  font-size: 11px;
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
  white-space: nowrap;
}

/* Anchored to the top-right of the metadata block as a vertical stack:
   Copy ID → Edit. Both buttons share the same width so they read as one
   column of controls. Uppercase / tracked typography matches the rest
   of the inspector chrome (FROM / TO / SOURCE / FILED labels). */
.detail-meta-actions {
  position: absolute;
  top: 12px;
  right: 12px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  z-index: 1;
}

.detail-meta-actions .icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: 88px;
  min-height: 28px;
  padding: 0 10px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* Theme-aware text + icons via var(--text). In dark mode this resolves
     to near-white (#f5f5f7); in light mode to near-black (#1d1d1f). Both
     read crisp against the matching translucent .detail-meta background. */
  color: var(--text);
}

.detail-meta-actions .icon-button:hover {
  color: var(--text);
}

.detail-meta-actions .icon-button svg {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
}

/* Trash glyph is the only thing in red — the button frame stays neutral
   and the "DELETE" label stays theme-text. SVG paths use
   stroke="currentColor" so retargeting `color` on the <svg> flips only
   the glyph. Apple-system red works on both light and dark surfaces. */
.detail-meta-actions .icon-button[data-delete-scope] svg {
  color: #fe5f63;
}

/* Same tunnel glyph as the room-row indicator. Shown when the displayed
   memory lives in a room with at least one tunnel; tap jumps to the other
   end (see renderDetail). */
.detail-tunnel-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--soft);
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

.detail-tunnel-icon:hover,
.detail-tunnel-icon:focus-visible {
  background: var(--surface-strong);
  color: var(--accent);
  border-color: var(--accent);
  outline: none;
}

.detail-tunnel-icon svg {
  width: 16px;
  height: 16px;
}

a.wiki-link {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px dashed currentColor;
  padding-bottom: 1px;
  cursor: pointer;
}

a.wiki-link:hover {
  border-bottom-style: solid;
}

.edit-id-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 0 0;
}

.edit-id {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--muted);
  font-size: 12px;
  overflow: hidden;
}

.edit-id code {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: ui-monospace, "SF Mono", monospace;
  font-size: 12px;
  color: var(--text);
}

.field-hint {
  margin-top: 4px;
  color: var(--muted);
  font-size: 12px;
}

.required-marker {
  color: var(--soft);
  margin-left: 2px;
  font-weight: 500;
}

.field-error {
  display: block;
  margin-top: 6px;
  color: #fe5f63;
  font-size: 12px;
  font-weight: 500;
}

.settings-section {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.settings-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

/* ---------- view toggle ---------- */
.view-toggle {
  display: inline-flex;
  align-items: center;
  padding: 3px;
  gap: 2px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
}

.view-toggle-btn {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 7px;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease;
}

.view-toggle-btn:hover {
  color: var(--text);
}

.view-toggle-btn.active {
  background: var(--surface-strong);
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--line);
}

/* ---------- knowledge graph viz ---------- */
.facts-graph {
  padding: 14px;
}

.kg-graph-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.kg-stage {
  position: relative;
  border-radius: 18px;
  overflow: hidden;
  background:
    radial-gradient(120% 80% at 30% 20%, rgba(63, 148, 186, 0.10), transparent 65%),
    radial-gradient(100% 80% at 70% 90%, rgba(63, 148, 186, 0.06), transparent 70%),
    var(--surface);
  border: 1px solid var(--line);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.04),
    0 12px 30px rgba(0, 0, 0, 0.18);
}

.kg-svg {
  display: block;
  width: 100%;
  height: 64vh;
  min-height: 480px;
  max-height: 760px;
  cursor: grab;
  touch-action: none;
}

.kg-svg.kg-panning { cursor: grabbing; }
.kg-svg.kg-dragging { cursor: grabbing; }

/* ----- edges ----- */
.kg-edge {
  stroke-width: 1.4;
  fill: none;
  transition: stroke 220ms ease, stroke-opacity 220ms ease, stroke-width 220ms ease;
  pointer-events: none;
}

.kg-edge.intra {
  stroke: var(--kg-color, var(--line));
  stroke-opacity: 0.42;
}

.kg-edge.bridge {
  stroke: var(--soft);
  stroke-opacity: 0.22;
}

.kg-edge.highlight {
  stroke: var(--kg-color, var(--accent));
  stroke-opacity: 0.92;
  stroke-width: 2.4;
}

.kg-edge.dim {
  stroke-opacity: 0.08;
}

.kg-edge-label {
  fill: var(--muted);
  font-size: 17px;
  font-weight: 500;
  letter-spacing: 0.01em;
  pointer-events: none;
  text-anchor: middle;
  opacity: 0;
  paint-order: stroke;
  stroke: var(--bg);
  stroke-width: 6;
  stroke-linejoin: round;
  transition: opacity 220ms ease, fill 220ms ease;
}

.kg-edge-label.highlight {
  opacity: 1;
  fill: var(--text);
}

.kg-edge-label.dim {
  opacity: 0;
}

/* ----- nodes ----- */
.kg-node {
  cursor: pointer;
  transition: opacity 240ms ease;
}

.kg-node-halo {
  fill: var(--kg-color, var(--accent));
  opacity: 0;
  filter: url(#kgGlow);
  transition: opacity 260ms ease;
  pointer-events: none;
}

.kg-node-ring {
  fill: none;
  stroke: var(--kg-color, var(--accent));
  stroke-width: 1.6;
  opacity: 0;
  transition: opacity 220ms ease, stroke-width 220ms ease;
  pointer-events: none;
}

.kg-node-fill {
  fill: var(--kg-color, var(--surface-strong));
  fill-opacity: 0.16;
  stroke: var(--kg-color, var(--line));
  stroke-opacity: 0.55;
  stroke-width: 1.6;
  transition:
    fill-opacity 240ms ease,
    stroke-opacity 240ms ease,
    stroke-width 240ms ease;
}

.kg-node-overlay {
  pointer-events: none;
  opacity: 0.55;
  transition: opacity 240ms ease;
}

.kg-node-label {
  fill: var(--text);
  font-size: 19px;
  font-weight: 500;
  letter-spacing: 0.005em;
  pointer-events: none;
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--bg);
  stroke-width: 6;
  stroke-linejoin: round;
  opacity: 0;
  transition: fill 240ms ease, font-weight 240ms ease, opacity 240ms ease;
}

/* Hubs (top-degree nodes) act as cluster anchors — labels stay visible. */
.kg-node.hub .kg-node-label {
  opacity: 0.95;
}

.kg-node.hub .kg-node-fill {
  fill-opacity: 0.32;
  stroke-opacity: 0.85;
  stroke-width: 1.8;
}

/* On hover or focus, reveal the label for that node + its neighbourhood. */
.kg-node:hover .kg-node-label,
.kg-node.hover .kg-node-label,
.kg-node.focus .kg-node-label,
.kg-node.connected .kg-node-label {
  opacity: 1;
}

/* When zoomed in past the threshold, show every label. */
.kg-svg.kg-zoomed .kg-node-label {
  opacity: 0.88;
}
.kg-svg.kg-zoomed .kg-node.dim .kg-node-label {
  opacity: 0.18;
}

.kg-node:hover .kg-node-fill,
.kg-node.hover .kg-node-fill {
  fill-opacity: 0.45;
  stroke-opacity: 0.95;
  stroke-width: 2;
}

.kg-node.connected .kg-node-fill {
  fill-opacity: 0.28;
  stroke-opacity: 0.85;
}

.kg-node.focus .kg-node-fill {
  fill-opacity: 1;
  stroke-opacity: 1;
  stroke-width: 2.2;
}

.kg-node.focus .kg-node-label {
  font-weight: 600;
  fill: var(--text);
}

.kg-node.focus .kg-node-ring {
  opacity: 1;
  stroke-width: 1.8;
}

.kg-node.focus .kg-node-halo {
  opacity: 0.55;
}

.kg-node.focus .kg-node-overlay {
  fill: url(#kgFocusGrad);
  opacity: 1;
}

.kg-node.dim {
  opacity: 0.18;
}

.kg-node.dim .kg-node-label {
  opacity: 0;
}

/* ----- chrome ----- */
.kg-hint {
  font-size: 11px;
  font-weight: 500;
  color: var(--soft);
  letter-spacing: 0.02em;
  padding: 0 4px;
}

.kg-legend {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 12px;
  color: var(--muted);
  padding: 0 4px;
}

.kg-reset {
  appearance: none;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 999px;
  transition: background 160ms ease, border-color 160ms ease, color 160ms ease;
}

.kg-reset:hover {
  background: var(--surface-strong);
  border-color: rgba(63, 148, 186, 0.4);
  color: var(--accent);
}

.settings-section-head {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.settings-section-head strong {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}

.settings-section-head span {
  font-size: 12px;
  color: var(--muted);
}

.settings-export {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  align-self: flex-start;
  min-height: 36px;
  padding: 0 16px;
  font-size: 13px;
  font-weight: 600;
}

.settings-export svg {
  width: 16px;
  height: 16px;
}

.settings-export:disabled {
  opacity: 0.6;
  cursor: progress;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .field-error {
    color: #fe5f63;
  }
}

:root[data-theme="dark"] .field-error {
  color: #fe5f63;
}

input.invalid-mismatch {
  border-color: rgba(254, 95, 99, 0.55);
  box-shadow: 0 0 0 4px rgba(254, 95, 99, 0.12);
}

.login-panel {
  width: min(420px, 100%);
  gap: 16px;
  padding: 28px 28px 24px;
}

.login-logo {
  width: 84px !important;
  height: 84px !important;
  border-radius: 0;
}

.login-panel h2 {
  font-size: 28px;
  margin-top: -4px;
}

.login-panel > p {
  color: var(--muted);
}

.login-form {
  display: grid;
  gap: 12px;
  width: 100%;
}

.login-form label {
  display: grid;
  gap: 6px;
  text-align: left;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
}

.login-form input[type="text"],
.login-form input[type="password"],
.login-form input:not([type]) {
  height: 44px;
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 0 14px;
  background: var(--surface-strong);
  color: var(--text);
  outline: none;
  transition:
    border-color var(--dur-2, 180ms) ease,
    box-shadow var(--dur-2, 180ms) ease;
  font-size: 14px;
  font-weight: 500;
}

.login-form input:focus {
  border-color: rgba(63, 148, 186, 0.45);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

.checkbox-row {
  display: flex !important;
  flex-direction: row !important;
  align-items: center;
  gap: 10px;
  color: var(--text) !important;
  font-weight: 500 !important;
  cursor: pointer;
}

.checkbox-row input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
  cursor: pointer;
}

.login-form .primary-action {
  margin-top: 4px;
  min-height: 44px;
}

.fact {
  grid-template-columns: minmax(120px, 1fr) minmax(140px, 0.8fr) minmax(160px, 1.3fr) auto auto;
}

.fact.expired {
  opacity: 0.55;
}

.fact.expired strong {
  text-decoration: line-through;
}

.fact-actions {
  display: flex;
  align-items: center;
  gap: 6px;
}

.fact-actions .icon-button {
  min-height: 28px;
  padding: 0 10px;
  font-size: 12px;
}

.draft-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  min-height: 56px;
  padding: 12px 14px;
}

.draft-item > .draft-info {
  display: grid;
  gap: 4px;
  min-width: 0;
}

.draft-item strong {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.draft-item .primary-action {
  min-height: 36px;
  padding: 0 14px;
  font-size: 13px;
}

.draft-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.trash-footer {
  display: flex;
  justify-content: flex-start;
}

.write-status:empty {
  display: none;
}

.draft-actions .icon-button {
  min-height: 36px;
  padding: 0 12px;
  font-size: 13px;
}

/* Tunnel snapshots in Recently deleted. Same layout as drawer snapshots
   but the title slot becomes a source ↔ target route, and an optional
   label preview line sits below the meta line. */
.trash-tunnel-row .trash-tunnel-arrow {
  color: var(--accent);
  font-weight: 700;
  margin: 0 4px;
}

.trash-tunnel-row .trash-tunnel-label {
  display: block;
  margin-top: 6px;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.45;
}

/* Trash bin: section-split refactor reverted 2026-05-28 — the trash
   is now a flat list again. Going forward only actual deletions
   (delete + delete_tunnel) produce snapshot entries; the dashboard
   no longer writes update-before / rename-before rows, so there are
   no "version history" entries to group. See the openTrashSheet
   render path + the server's log_version call sites for the
   policy. */

.icon-button.icon-only {
  display: inline-grid;
  place-items: center;
  width: 36px;
  height: 36px;
  min-height: 36px;
  padding: 0;
}

.icon-button.icon-only svg {
  width: 18px;
  height: 18px;
}

.toolbar-button.icon-only {
  width: 44px;
  height: 44px;
  min-height: 44px;
  padding: 0;
}

.toolbar-button.icon-only svg {
  width: 20px;
  height: 20px;
}

/* Destructive icon-button (trash, "delete forever") — same --danger
   token as the danger-menu rule above, so red reads identically across
   chrome and inline actions. */
.icon-button.danger-button {
  border-color: var(--danger-soft-strong);
  color: var(--danger);
}

.icon-button.danger-button:hover {
  border-color: var(--danger);
  background: var(--danger-soft);
  box-shadow: 0 0 0 3px var(--danger-soft);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .hint {
    background: rgba(58, 58, 60, 0.92);
  }
}

:root[data-theme="dark"] .hint {
    background: rgba(58, 58, 60, 0.92);
  }

@media (max-width: 980px) {
  .panel-tools {
    margin-top: 8px;
    flex-basis: 100%;
  }
}

@media (max-width: 560px) {
  .topbar h1 {
    font-size: 34px;
  }

  .stats {
    grid-template-columns: 1fr;
  }

  .stat + .stat::before {
    display: none;
  }

  .stat + .stat::after {
    content: "";
    position: absolute;
    left: 14px;
    right: 14px;
    top: 0;
    height: 1px;
    background: var(--line);
  }

  .write-grid,
  .write-actions {
    grid-template-columns: 1fr;
  }

  .detail-meta {
    grid-template-columns: 1fr;
  }
}

/* ---------- footer ---------- */
/* Footer is OUTSIDE .main, spans 100% viewport width with its own
   subtle grey overlay + a hairline top border that runs edge to
   edge. The inner content (brand line + stats + system-info) is
   constrained by .footer-inner to the same max-width + padding as
   .main so the text columns align with the content grid above. */
.footer {
  margin-top: 32px;
  border-top: 1px solid var(--line);
  background: var(--surface-strong);
}

.footer-inner {
  max-width: 1440px;
  margin: 0 auto;
  padding: 24px 32px;
}

.footer-brand {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 18px;
  margin-bottom: 18px;
  border-bottom: 1px solid var(--line);
}

.footer-brand > div {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.footer-brand strong {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}

.footer-brand span {
  font-size: 12px;
  color: var(--muted);
}

.footer-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  text-decoration: none;
  font-size: 13px;
  font-weight: 600;
  transition:
    background 160ms ease,
    border-color 160ms ease,
    color 160ms ease;
}

.footer-link svg {
  width: 16px;
  height: 16px;
}

.footer-link:hover {
  background: var(--surface);
  border-color: rgba(63, 148, 186, 0.28);
  color: var(--text);
}

.footer-info {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0 28px;
}

/* When the stats info sits above the system info, add a hairline
   divider between them so the two cell-rows read as related-but-
   distinct sections (palace totals on top, host runtime on bottom).
   :has() is well-supported in current browsers; the rule falls back
   gracefully (no divider) on older engines. */
.footer-info + .footer-info {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

.footer-cell {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 4px 0;
  min-width: 0;
}

.footer-cell + .footer-cell::before {
  content: "";
  position: absolute;
  left: -14px;
  top: 8px;
  bottom: 8px;
  width: 1px;
  background: var(--line);
}

.footer-cell-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}

.footer-cell-value {
  font-size: 13px;
  font-weight: 500;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 960px) {
  .footer-info {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .footer-cell:nth-child(2n + 1)::before {
    display: none;
  }

  .footer-cell:nth-child(n + 3)::after {
    content: "";
    position: absolute;
    left: 14px;
    right: 14px;
    top: 0;
    height: 1px;
    background: var(--line);
  }
}

@media (max-width: 560px) {
  .footer-info {
    grid-template-columns: 1fr;
  }

  .footer-cell + .footer-cell::before {
    display: none;
  }

  .footer-cell + .footer-cell::after {
    content: "";
    position: absolute;
    left: 14px;
    right: 14px;
    top: 0;
    height: 1px;
    background: var(--line);
  }

  .footer-brand {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
}
/* ---------- Lab ---------- */
.lab-panel {
  width: min(960px, 100%);
  max-height: min(880px, calc(100vh - 32px));
}

.lab-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 4px;
  background: var(--accent-soft);
  border-radius: 14px;
}

.lab-tab {
  flex: 1 1 auto;
  min-width: 110px;
  padding: 8px 14px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.lab-tab:hover {
  color: var(--text);
}

.lab-tab.active {
  background: var(--surface-strong);
  color: var(--text);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

.lab-body {
  display: grid;
  gap: 16px;
}

.lab-pane[hidden] {
  display: none;
}

.lab-pane h3 {
  margin: 0;
  font-size: 18px;
}

.lab-hint {
  margin: -4px 0 4px;
  color: var(--muted);
  font-size: 13px;
}

.lab-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
}

.lab-pane label {
  display: grid;
  gap: 4px;
  font-size: 13px;
}

.lab-pane label > span {
  color: var(--muted);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.lab-pane input,
.lab-pane select,
.lab-pane textarea {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
}

.lab-pane textarea {
  min-height: 90px;
  resize: vertical;
}

.lab-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.lab-actions-row {
  justify-content: flex-start;
  flex-wrap: wrap;
}

.lab-output {
  display: grid;
  gap: 8px;
  padding: 10px;
  border: 1px dashed var(--line);
  border-radius: 12px;
  background: var(--surface);
  min-height: 44px;
  max-height: 400px;
  overflow: auto;
}

.lab-output:empty::before {
  content: "Results appear here.";
  color: var(--muted);
  font-size: 12px;
}

.lab-loading {
  color: var(--muted);
  font-style: italic;
}

.lab-error {
  color: #b00020;
  background: rgba(176, 0, 32, 0.08);
  padding: 8px 10px;
  border-radius: 8px;
  font-size: 13px;
}

.lab-empty {
  color: var(--muted);
  font-size: 13px;
}

.lab-summary {
  font-size: 12px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.lab-json,
.lab-spec {
  margin: 0;
  padding: 8px;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
  background: var(--bg);
  border-radius: 8px;
}

.lab-block {
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 10px 12px;
  background: var(--surface);
  display: grid;
  gap: 10px;
}

.lab-block summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 14px;
  list-style: none;
  user-select: none;
}

.lab-block summary::marker,
.lab-block summary::-webkit-details-marker { display: none; }

.lab-block summary::before {
  content: "▸";
  display: inline-block;
  margin-right: 6px;
  color: var(--muted);
  transition: transform 0.15s ease;
}

.lab-block[open] > summary::before {
  transform: rotate(90deg);
}

.lab-fact,
.lab-dup,
.lab-timeline-row {
  padding: 8px 10px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  display: grid;
  gap: 4px;
}

.lab-fact-spo {
  font-size: 14px;
}

.lab-fact-spo em {
  color: var(--muted);
  font-style: normal;
}

.lab-fact-meta,
.lab-dup-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--muted);
}

.lab-transitional {
  display: inline-block;
  margin-top: 2px;
  padding: 1px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.lab-pill {
  display: inline-block;
  padding: 1px 8px;
  background: var(--accent-soft);
  color: var(--accent);
  border-radius: 999px;
  font-size: 11px;
  font-weight: 500;
}

.lab-pill-warn {
  background: rgba(176, 0, 32, 0.12);
  color: #b00020;
}

.lab-timeline-row {
  grid-template-columns: 110px 1fr;
  align-items: center;
}

.lab-timeline-date {
  font-size: 12px;
  color: var(--muted);
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

.lab-tax-wing {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 8px 10px;
  background: var(--surface);
}

.lab-tax-wing summary {
  cursor: pointer;
  list-style: none;
}

.lab-tax-wing ul {
  list-style: none;
  padding: 6px 0 0 12px;
  margin: 6px 0 0;
  display: grid;
  gap: 4px;
  font-size: 13px;
}

.lab-dup-body {
  margin: 0;
  padding: 6px 8px;
  background: var(--bg);
  border-radius: 8px;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
  white-space: pre-wrap;
  word-break: break-word;
}

body.lab-open {
  overflow: hidden;
}

/* "Copied" pill toast — small black pill that floats above the clicked
   element. Spring pop-in (overshoot scale) on entry; flat fade on
   exit. position:absolute (not fixed) + document/page coordinates so
   the toast scales and translates with the document under pinch-zoom
   — fixed-positioned elements anchor to the layout viewport and get
   pushed out of the visual viewport when the user zooms in. */
.copied-toast {
  position: absolute;
  z-index: 100;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--text);
  color: var(--surface);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, calc(-100% - 4px)) scale(0.85);
  transition: opacity 0.22s ease, transform 0.32s cubic-bezier(0.34, 1.45, 0.64, 1);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
}

.copied-toast-visible {
  opacity: 1;
  transform: translate(-50%, calc(-100% - 6px)) scale(1);
}

.copied-toast-dismissing {
  opacity: 0;
  transform: translate(-50%, calc(-100% - 10px)) scale(0.95);
  transition: opacity 0.22s ease-in, transform 0.22s ease-in;
}

.copied-toast-error {
  background: #fe5f63;
  color: #fff;
}

@media (prefers-reduced-motion: reduce) {
  .copied-toast {
    transition: opacity 0.12s linear;
    transform: translate(-50%, -100%);
  }
  .copied-toast-visible,
  .copied-toast-dismissing {
    transform: translate(-50%, -100%);
  }
}

/* Visual separator between the content-action cluster (Tools/Drafts/
   Write) and the chrome-action cluster (Trash/Theme/Settings) in the
   top bar. Thin vertical hairline + small horizontal breathing room
   so the two groups read as distinct purposes. */
.top-actions-separator {
  display: inline-block;
  width: 1px;
  height: 24px;
  background: var(--line);
  margin: 0 6px;
  flex-shrink: 0;
}

/* Slow rotation on the gear icon — same "Apricity is alive" register
   as the search-bar prism ring, just paced slower so the two ambient
   motions don't compete. The animation is on the SVG (not the
   button) so the button frame stays put while the gear spins inside
   it; reads as "the gear is alive inside its housing". */
.settings-fab-icon {
  display: block;
  transform-origin: 50% 50%;
  animation: settings-fab-spin 20s linear infinite;
}

@keyframes settings-fab-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .settings-fab-icon {
    animation: none;
  }
}

/* Pause the ambient gear spin while the notification bell is
   actively swinging — two simultaneous animations in the top-actions
   cluster compete for attention and dilute the bell's "you have
   unread updates" semantic. The gear's spin is purely decorative;
   the bell's swing carries information. So during unseen-alert
   periods the gear sits still, and the moment the user clears the
   bell (count → 0, .has-unseen comes off) the gear quietly resumes
   its slow rotation — a "dashboard exhales, returning to baseline"
   beat. :has() looks up from .top-actions to find the bell's
   .has-unseen state and conditionally suppresses the animation. */
.top-actions:has(.notifications-bell.has-unseen) .settings-fab-icon {
  animation: none;
}

/* ---------- theme dropdown (top-bar menu) ---------- */
/* Wraps the icon button + its action-menu so the menu can position
   itself relative to the button. inline-flex (not inline-block) so
   the wrapped button aligns to the SAME vertical baseline as its
   trash / settings siblings in the .top-actions flex row — inline-
   block introduced a subtle baseline shift that made the theme
   button sit a few pixels lower than the others. */
.theme-menu-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
}

/* The theme dropdown isn't really a "menu" any more — it's a stack
   of independent glass pills, each its own self-contained card
   floating with its own border + shadow. iOS Shortcuts widget
   layout / Control Center module rhythm. The outer .action-menu
   container is reduced to a positioning + spacing shell (transparent
   background, no border, no shadow), and each option carries its
   own visual chrome. */
.theme-menu {
  min-width: 196px;
  padding: 0;
  /* Reset the base .action-menu chrome — pills carry their own. */
  background: transparent;
  border: none;
  box-shadow: none;
  backdrop-filter: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* No container animation — each pill cascades in on its own. */
  animation: none;
  transform-origin: top right;
}

/* Each theme option is a standalone pill: glass background, its own
   border + shadow, rounded corners. Stacks vertically with the
   container's 6px gap separating them.

   Cascade entry: each pill drops in from above with a small spring
   landing, staggered 60ms each. Short, snappy, no global side
   effects — should not feel laggy. */
.theme-menu .action-item.theme-option {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  column-gap: 12px;
  padding: 10px 18px;
  min-height: 42px;
  /* Self-contained card visual. Fully pill-shaped (border-radius
     clamps to half the pill height on a 42px row → ~21px) so the
     curve matches the macOS traffic-light circles in spirit — the
     "white background on close and maximize buttons" the user asked
     for, just on a rectangle that has to be long enough to hold a
     row of content. */
  background: var(--surface-strong);
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.04),
    0 6px 18px rgba(0, 0, 0, 0.08);
  backdrop-filter: blur(20px);
  /* Each pill animates in independently. */
  animation: theme-option-drop 0.26s cubic-bezier(0.34, 1.3, 0.64, 1) both;
}
.theme-menu .action-item.theme-option:nth-child(1) { animation-delay: 0ms;   }
.theme-menu .action-item.theme-option:nth-child(2) { animation-delay: 60ms;  }
.theme-menu .action-item.theme-option:nth-child(3) { animation-delay: 120ms; }

@keyframes theme-option-drop {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Close animation — mirrors the open cascade in reverse but at a
   noticeably faster pace: exits should feel snappier than entries.
   Each pill rises + fades, with the BOTTOM row leaving first and
   the top last (wave receding upward), ease-in so the exit feels
   "pulled into the void" rather than spring-bouncy. The .closing
   class is added by closeMenus() before the actual .hidden class
   gets toggled, so the animation has time to play. */
.theme-menu.closing .action-item.theme-option {
  animation: theme-option-rise 0.13s ease-in both;
}
.theme-menu.closing .action-item.theme-option:nth-child(3) { animation-delay: 0ms;  }
.theme-menu.closing .action-item.theme-option:nth-child(2) { animation-delay: 30ms; }
.theme-menu.closing .action-item.theme-option:nth-child(1) { animation-delay: 60ms; }

@keyframes theme-option-rise {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(-6px); }
}

@media (prefers-reduced-motion: reduce) {
  .theme-menu.closing .action-item.theme-option {
    animation: none;
  }
}

/* Hover: subtle accent border + accent-tinted icon. No background
   change — the pill is already a card; tinting the whole card
   competes with the active-state signal. */
.theme-menu .action-item.theme-option:hover {
  background: var(--surface-strong);
  border-color: rgba(63, 148, 186, 0.4);
}
.theme-menu .action-item.theme-option:hover .action-icon {
  color: var(--accent);
}

/* :active scales the whole pill down a touch — instant feedback
   while the click is held. */
.theme-menu .action-item.theme-option:active {
  transform: scale(0.97);
  transition: transform 0.1s cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* Spring pop on click — visible for the brief window between click
   and menu close (the close is delayed in JS so this plays). */
.theme-menu .action-item.theme-option.clicked-pop {
  animation: theme-option-spring 0.28s cubic-bezier(0.34, 1.45, 0.64, 1);
}
@keyframes theme-option-spring {
  0%   { transform: scale(1); }
  35%  { transform: scale(0.94); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .theme-menu .action-item.theme-option,
  .theme-menu .action-item.theme-option.clicked-pop {
    animation: none;
  }
}

.theme-option-label {
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
  transition: color 0.14s ease;
}

/* Right-side checkmark — real SVG glyph (not a text character), only
   visible on the active row. The icon column on the left always
   reserves space so all three rows line up. */
.theme-option-check {
  width: 16px;
  height: 16px;
  color: var(--accent);
  opacity: 0;
  justify-self: end;
  flex-shrink: 0;
  transition: opacity 0.14s ease;
}

/* Active state: pill gets accent border + accent-soft tint, icon +
   label + checkmark all go accent. This is the "you are here"
   signal — distinct enough from hover that the two don't collide. */
.theme-menu .action-item.is-active {
  background: var(--accent-soft);
  border-color: rgba(63, 148, 186, 0.55);
}
.theme-menu .action-item.is-active .theme-option-check {
  opacity: 1;
}
.theme-menu .action-item.is-active .theme-option-label {
  color: var(--accent);
  font-weight: 600;
}
.theme-menu .action-item.is-active .action-icon {
  color: var(--accent);
}

/* ---------- settings checkbox toggle row ---------- */
.settings-toggle {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 12px;
  cursor: pointer;
  padding: 8px 0;
}
.settings-toggle input[type="checkbox"] {
  margin-top: 4px;
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
  cursor: pointer;
  flex-shrink: 0;
}
.settings-toggle-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.settings-toggle-text strong {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.settings-toggle-text span {
  font-size: 12px;
  color: var(--muted);
  line-height: 1.45;
}
.settings-toggle-text em {
  font-style: normal;
  color: var(--text);
  font-weight: 500;
}

/* ---------- settings select row ---------- */
/* Same visual rhythm as .settings-toggle but with a select on the
   right instead of a checkbox on the left. Used for choice
   preferences (auto-delete-after, etc.) where a checkbox can't
   express the range. Title + description on the left, select on the
   right, vertically centred. */
.settings-select {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 16px;
  padding: 8px 0;
}
.settings-select-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.settings-select-text strong {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.settings-select-text span {
  font-size: 12px;
  color: var(--muted);
  line-height: 1.45;
}
.settings-select-text em {
  font-style: normal;
  color: var(--text);
  font-weight: 500;
}
.settings-select select {
  /* Inherits the global select rule (height, border, padding,
     accent-soft focus halo). Just give it a sensible min-width so
     "Off (keep forever)" doesn't truncate. */
  min-width: 168px;
}

/* ---------- settings theme row ---------- */
/* Three-pill theme picker that mirrors the top-bar dropdown's
   Auto/Light/Dark options but lays them out as inline buttons
   inside the Settings sheet. Same data-theme-option semantics as
   the top-bar dropdown — clicks route through the existing handler
   and the .is-active class is toggled by applyTheme(), so both UIs
   stay in sync automatically. */
.settings-theme-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 16px;
  padding: 8px 0;
}
.settings-theme-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.settings-theme-text strong {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.settings-theme-text span {
  font-size: 12px;
  color: var(--muted);
  line-height: 1.45;
}
.settings-theme-options {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}
.settings-theme-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition:
    background 0.14s ease,
    border-color 0.14s ease,
    color 0.14s ease,
    box-shadow 0.14s ease;
}
.settings-theme-pill svg {
  width: 16px;
  height: 16px;
}
.settings-theme-pill:hover,
.settings-theme-pill:focus-visible {
  outline: none;
  background: var(--surface-strong);
  border-color: rgba(63, 148, 186, 0.28);
  color: var(--text);
}
.settings-theme-pill.is-active {
  background: var(--accent-soft);
  border-color: rgba(63, 148, 186, 0.55);
  color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* ---------- settings keyboard shortcut row ---------- */
/* Same left-text / right-control rhythm as .settings-toggle and
   .settings-select, but the right-side control is a "key cap"
   button that doubles as the click target for re-binding. Click
   → enters .listening state (accent border + pulsing halo), the
   next non-modifier keydown is captured and saved via
   updatePreferences({ shortcuts: {action: key} }). Esc cancels
   recording and restores the prior binding. */
.settings-shortcut-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 16px;
  padding: 8px 0;
}
.settings-shortcut-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.settings-shortcut-text strong {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.settings-shortcut-text span {
  font-size: 12px;
  color: var(--muted);
  line-height: 1.45;
}
.settings-shortcut-key {
  min-width: 64px;
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-strong);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "SF Mono", monospace;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  cursor: pointer;
  text-align: center;
  white-space: nowrap;
  transition:
    background 0.14s ease,
    border-color 0.14s ease,
    box-shadow 0.14s ease;
}
.settings-shortcut-key:hover,
.settings-shortcut-key:focus-visible {
  outline: none;
  border-color: rgba(63, 148, 186, 0.4);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.settings-shortcut-key.listening {
  background: var(--accent-soft);
  border-color: rgba(63, 148, 186, 0.55);
  color: var(--accent);
  /* Subtle pulse so the user knows the field is "hot" and waiting. */
  animation: settings-shortcut-pulse 1.4s ease-in-out infinite;
}
@keyframes settings-shortcut-pulse {
  0%, 100% { box-shadow: 0 0 0 3px var(--accent-soft); }
  50%      { box-shadow: 0 0 0 6px var(--accent-soft); }
}
@media (prefers-reduced-motion: reduce) {
  .settings-shortcut-key.listening { animation: none; }
}
html.reduce-motion .settings-shortcut-key.listening { animation: none; }

/* ============================================================
   Settings sheet — new layout (macOS Settings-style)
   ------------------------------------------------------------
   Two-column shell (sidebar nav + content pane) replacing the
   old single-column form. Each pane is a grouped-card list
   modeled after iOS/macOS Settings: rounded cards with rows
   inside, hairline dividers, generous whitespace.

   The .settings-overlay class plays the role .write-sheet used
   to play for this sheet (modal container). The backdrop is
   the shared .write-backdrop. The .settings-shell is the
   resizable card. Animation rules mirror the write-sheet's
   spring pop-in / pop-out so the entry/exit feels identical
   to every other sheet in the app.
   ============================================================ */

.settings-overlay {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 24px;
}

/* Pop-in / pop-out animations, parallel to .write-sheet's. Different
   selectors so we don't have to edit the existing rule list — the
   settings sheet now has its own dedicated animation block.
   :not(.is-opened) gates the entrance animation to the very first
   render after .hidden is removed; JS adds .is-opened once the
   animation completes so subsequent class flips (notably toggling
   the reduce-motion preference) don't re-trigger the open animation
   on a sheet that's already visible — that was being perceived as
   a "flicker and pop" by the user. */
/* Animation target lifted from .settings-shell to .settings-shell-wrap
   so the close pill (sibling of the shell inside the wrap) scales
   and translates IN UNISON with the shell during pop-in/close. With
   the animation pinned to just the shell, the pill stayed static
   while the shell scaled — visually they looked like two unrelated
   floating elements. Animating the wrap moves both as one rigid
   unit, identical to how .detail-panel-actions stays glued to its
   detail panel's corner. */
.settings-overlay:not(.hidden):not(.is-opened) .settings-shell-wrap {
  animation: confirm-pop-in 0.42s cubic-bezier(0.34, 1.45, 0.64, 1);
  transform-origin: center;
}
.settings-overlay:not(.hidden):not(.is-opened) .write-backdrop {
  animation: confirm-backdrop-in 0.18s ease-out;
}
.settings-overlay.closing .settings-shell-wrap {
  animation: confirm-pop-out 0.28s cubic-bezier(0.34, 1.45, 0.64, 1) forwards;
  transform-origin: center;
}
.settings-overlay.closing .write-backdrop {
  animation: confirm-backdrop-out 0.14s ease-in forwards;
  animation-delay: 0.14s;
}
@media (prefers-reduced-motion: reduce) {
  .settings-overlay:not(.hidden) .settings-shell-wrap,
  .settings-overlay:not(.hidden) .write-backdrop,
  .settings-overlay.closing .settings-shell-wrap,
  .settings-overlay.closing .write-backdrop { animation: none; }
}
html.reduce-motion .settings-overlay:not(.hidden) .settings-shell-wrap,
html.reduce-motion .settings-overlay:not(.hidden) .write-backdrop,
html.reduce-motion .settings-overlay.closing .settings-shell-wrap,
html.reduce-motion .settings-overlay.closing .write-backdrop { animation: none; }

/* Two-column shell: sidebar nav on the left, content pane on the
   right. Fixed max dimensions; the content pane scrolls internally,
   not the whole shell, so the floating close stays anchored to the
   shell corner regardless of how far the user has scrolled. */
.settings-shell {
  position: relative;
  display: grid;
  grid-template-columns: 220px 1fr;
  width: min(960px, 100%);
  height: min(680px, calc(100vh - 48px));
  border: 1px solid var(--line);
  border-radius: 24px;
  background: var(--surface-strong);
  box-shadow: 0 28px 90px rgba(0, 0, 0, 0.24);
  overflow: hidden;
}

/* Floating close pill — overlaps the shell's top-right CORNER (negative
   offsets escape the frame's bounding box). The shell has overflow:
   hidden so an inside-shell pill would have to sit fully inside the
   border-radius; placing the pill as a sibling of the shell inside the
   non-clipping frame lets it sit ON TOP of the shell corner instead —
   identical pattern to .detail-panel-actions on the Detail panel
   (top:-10px;left:-10px there; mirrored to top:-10px;right:-10px here
   for the right-corner placement). z-index lifts it above the shell. */
.settings-shell-actions {
  position: absolute;
  top: -10px;
  right: -10px;
  z-index: 10;
  display: inline-flex;
  align-items: center;
  padding: 4px;
  border-radius: 14px;
  background: #ffffff;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.06);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .settings-shell-actions {
    background: #2c2c2e;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
  }
}
:root[data-theme="dark"] .settings-shell-actions {
  background: #2c2c2e;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
}

.settings-shell-close {
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--danger);
  color: #7f2f30;
  cursor: pointer;
  transition:
    transform 0.18s cubic-bezier(0.34, 1.3, 0.64, 1),
    filter 0.14s ease;
}
.settings-shell-close:hover,
.settings-shell-close:focus-visible {
  outline: none;
  transform: scale(1.08);
  filter: brightness(1.05);
}
.settings-shell-close svg {
  width: 12px;
  height: 12px;
}

/* Frame wraps the shell-wrap + the dismissal hint so the overlay's
   place-items:center centers them as a single column unit. Block-
   level by default → fills the grid cell width, so children are
   centered via align-items:center. */
.settings-shell-frame {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  max-width: 100%;
}

/* Shell-wrap is the positioning ancestor for the floating close pill.
   Sized to the viewport (not the parent frame) so the wrap's width
   is deterministic and can't shrink unexpectedly. The earlier
   width:min(960px, 100%) created a circular sizing dependency: the
   wrap wanted 100% of the frame, the frame's intrinsic width came
   from the wrap's content, and depending on layout timing the
   browser sometimes resolved the wrap (and therefore the shell
   inside it) to a sub-960px width — the "settings sometimes appears
   narrower" bug. Using `calc(100vw - 48px)` references the viewport
   directly (minus the .settings-overlay's 24px padding each side),
   which has no circular reference. The shell inside still uses
   min(960px, 100%) but its 100% is now anchored to a stable wrap. */
.settings-shell-wrap {
  position: relative;
  width: min(960px, calc(100vw - 48px));
}

/* Dismissal hint sits as the second flex item, right under the
   shell with a 10px gap (the frame's `gap`). Fixed off-white color
   in BOTH themes — the settings overlay always shows a dimmed
   blurred backdrop (rgba(0,0,0,0.24) + backdrop-filter blur), which
   uniformly darkens the surface enough that off-white reads as a
   quiet hint against it. Using var(--muted) here made the hint
   invert to dark grey in light mode, where it then sat as nearly-
   invisible dark-on-dark on the dimmed backdrop. */
.settings-overlay-hint {
  margin: 0;
  font-size: 11.5px;
  color: rgba(255, 255, 255, 0.55);
  letter-spacing: 0.02em;
  text-align: center;
  pointer-events: none;
  /* Fade in with the rest of the overlay — matches the existing
     .settings-shell entry animation duration so the hint arrives
     in sync. Translate-only transform (no left:50% to undo) since
     the flex parent already handles horizontal centering. */
  animation: settings-overlay-hint-in 0.32s ease 0.18s both;
}
@keyframes settings-overlay-hint-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.settings-overlay.closing .settings-overlay-hint {
  animation: settings-overlay-hint-out 0.18s ease both;
}
@keyframes settings-overlay-hint-out {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(4px); }
}
/* X marker — fully inherits the hint's color + weight so it reads
   as ordinary inline text. The kbd element is kept for semantic
   correctness (this is a keyboard key being referenced) without
   any visual emphasis — no pill, no bolder weight, no different
   color, no italic. The surrounding "press" / "to close" context
   carries the meaning. */
.settings-overlay-hint kbd {
  display: inline;
  padding: 0;
  margin: 0 1px;
  border: 0;
  background: transparent;
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  font-style: normal;
  color: inherit;
}
@media (prefers-reduced-motion: reduce) {
  .settings-overlay-hint,
  .settings-overlay.closing .settings-overlay-hint { animation: none; }
}
html.reduce-motion .settings-overlay-hint,
html.reduce-motion .settings-overlay.closing .settings-overlay-hint { animation: none; }

/* ---------- sidebar ---------- */
.settings-nav-pane {
  display: flex;
  flex-direction: column;
  padding: 26px 14px 16px;
  border-right: 1px solid var(--line);
  /* Subtle tint so the sidebar reads as chrome distinct from the
     content area, like macOS Sonoma's two-tone sidebar. */
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.015) 0%,
    rgba(0, 0, 0, 0.03) 100%
  );
}
:root[data-theme="dark"] .settings-nav-pane,
:root:not([data-theme="light"]) .settings-nav-pane {
  /* Dark-mode sidebar uses a darker tint of the surface to match the
     same "step-down" appearance the light theme has. */
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .settings-nav-pane {
    background: linear-gradient(
      180deg,
      rgba(255, 255, 255, 0.015) 0%,
      rgba(255, 255, 255, 0.03) 100%
    );
  }
}
:root[data-theme="dark"] .settings-nav-pane {
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.015) 0%,
    rgba(255, 255, 255, 0.03) 100%
  );
}

.settings-nav-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 10px 16px;
}
.settings-nav-eyebrow {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
}
.settings-nav-title {
  font-size: 19px;
  font-weight: 700;
  color: var(--text);
  margin: 0;
}

.settings-nav-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow-y: auto;
  /* No mask-image fade here — the sidebar list is short (5 items)
     and a top-edge fade was clipping the first item's highlight
     instead of doing anything useful. Plain crisp edges read better. */
  scrollbar-width: none;
}
.settings-nav-list::-webkit-scrollbar { display: none; }

.settings-nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--text);
  font-size: 13.5px;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
  transition: background 0.14s ease, color 0.14s ease;
}
.settings-nav-item:hover {
  background: var(--accent-soft);
}
.settings-nav-item:focus-visible {
  outline: none;
  background: var(--accent-soft);
  box-shadow: 0 0 0 2px var(--accent);
}
.settings-nav-item.is-active {
  background: var(--accent);
  color: white;
}
.settings-nav-item.is-active .settings-nav-icon {
  background: rgba(255, 255, 255, 0.18);
  color: white;
}

/* Coloured icon "chiclet" — macOS Settings borrows this pattern,
   gives each section its own colored square so the sidebar reads as
   a navigable list of distinct things rather than a wall of text.
   Different background colour per section, white glyph in front. */
.settings-nav-icon {
  display: inline-grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 6px;
  color: white;
  flex-shrink: 0;
  transition: background 0.14s ease;
}
.settings-nav-icon svg {
  width: 16px;
  height: 16px;
}
/* Chiclet palette — tuned for the Apricity light theme. The iOS-
   default saturations (#ff9500 / #5856d6 / #34c759) felt cartoony
   against the pale --bg (#f5f5f7); these slightly desaturated tones
   sit better with the rest of the chrome while keeping each section
   distinct at a glance. Dark mode reads fine with the same values
   because they're still far from black and the white glyph stays
   high-contrast against any of them. */
.settings-nav-icon-display       { background: #e88a2b; }  /* warm amber */
.settings-nav-icon-notifications { background: #af52de; }  /* Apple purple — distinct from amber Display and indigo Shortcuts */
.settings-nav-icon-shortcuts     { background: #5ac8fa; }  /* Apple cyan — keys + signal, cooler than the old indigo so it doesn't collide with the new purple */
.settings-nav-icon-trash         { background: var(--danger); }  /* on-brand red */
.settings-nav-icon-backup        { background: #4cae6a; }  /* muted green */
.settings-nav-icon-account       { background: #8e8e93; }  /* neutral gray (left untouched per spec) */
.settings-nav-icon-about         { background: var(--accent); }  /* Apricity teal */

/* ---------- content pane ---------- */
.settings-content-pane {
  position: relative;
  overflow-y: auto;
  /* Generous breathing room so the cards have space to feel like
     intentional groupings instead of crammed form rows. */
  padding: 32px 36px 36px;
  /* Suppress the native scrollbar entirely on every platform.
     Earlier this pane shipped with a slim Apple-y scrollbar, but the
     dashboard's right edge already has the sheet boundary + the back-
     drop blur, so the scrollbar read as an extra chrome line cluttering
     the otherwise clean settings surface. Content still scrolls (wheel,
     trackpad, touch, keyboard PgUp/PgDn) — only the visible track and
     thumb are hidden. Matches the writing sheet which also hides its
     scrollbar for the same "uncluttered focus" reason. */
  scrollbar-width: none;          /* Firefox + standards-track */
  -ms-overflow-style: none;       /* legacy Edge / IE */
}
.settings-content-pane::-webkit-scrollbar {
  /* WebKit (Safari, Chrome, modern Edge): zero out both track and
     thumb so nothing paints. Width:0 on its own suppresses the
     vertical bar; display:none belt-and-braces in case a future
     WebKit update renders width:0 as a 1px line. */
  width: 0;
  height: 0;
  display: none;
}

.settings-pane {
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.settings-pane.hidden { display: none; }

.settings-pane-head {
  margin-bottom: 4px;
}
.settings-pane-head h3 {
  margin: 0 0 4px;
  font-size: 22px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.01em;
}
.settings-pane-head p {
  margin: 0;
  font-size: 13px;
  color: var(--muted);
  line-height: 1.5;
}

/* Grouped card — rounded background, soft border, internal rows
   separated by hairline dividers. The classic iOS Settings rhythm. */
.settings-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  overflow: hidden;
}

.settings-card-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 18px;
  padding: 14px 16px;
  min-height: 50px;
  cursor: default;
}
/* Row-wide hover tints removed 2026-05-28 across every Settings row
   type (select, shortcut, action, toggle). Rationale: only the
   actionable element on the right edge of each row is interactive —
   the select dropdown, the shortcut-rebind chip, the action button,
   the toggle switch. Tinting the whole row on hover falsely advertised
   "click anywhere on this row" when in fact clicking on the label
   text or the description span does nothing. Each interactive
   element carries its own hover state (select focus halo, chip
   border tint + box-shadow, button hover lift, switch surface
   shift), so the tactile signal lives where it belongs. The earlier
   toggle-rows-don't-hover special-case is now the universal rule. */

.settings-card-row-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
/* Title row: direct-child > strong only. Earlier this was a plain
   descendant selector (.settings-card-row-text strong), which also
   matched any <strong> nested INSIDE the description <span> — and
   var(--text) is near-white in dark mode, so highlighted terms in
   the description prose ("Home Assistant", "API, TV") blew up
   bright white and competed visually with the title above. Scoping
   to direct child fixes that without losing the title's emphasis. */
.settings-card-row-text > strong {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.settings-card-row-text span {
  font-size: 12.5px;
  color: var(--muted);
  line-height: 1.45;
}
/* Inline emphasis (em / strong) INSIDE the description span: render
   with a softer lift than the title, sitting between --muted and
   --text. Pure --text was visually shouting in dark mode; pure
   --muted would lose the emphasis entirely. The 60% mix reads as
   "this term is the noun in the sentence" without becoming a
   second-tier headline. font-style stays roman (em italics in tight
   12.5px UI prose are too noisy at small sizes). */
/* em / strong inside description spans fully inherit the span's
   weight + color so noun-mentions don't visually pop relative to
   the surrounding prose. Earlier the rule lifted weight to 500 +
   shifted the colour toward var(--text) — that produced uneven
   emphasis where every term that happened to be inside <em> or
   <strong> read as bolder than its neighbours. Stripping the
   overrides also removes the italic default on <em> (which would
   otherwise add a second axis of visual noise at 12.5px UI sizes). */
.settings-card-row-text span em,
.settings-card-row-text span strong {
  font-style: normal;
  font-weight: inherit;
  color: inherit;
}

.settings-card-divider {
  height: 1px;
  margin: 0 16px;
  background: var(--line);
}

/* iOS-style switch. Hides the native checkbox and styles ::before
   as the knob inside the colored track. Uses the Apricity accent
   for the on-state to stay on-brand (instead of iOS green). */
.settings-switch {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: 44px;
  height: 26px;
  border-radius: 13px;
  background: rgba(0, 0, 0, 0.12);
  cursor: pointer;
  transition: background 0.18s ease;
  flex-shrink: 0;
  margin: 0;
}
/* Dark-mode track: white-transparent so the knob has contrast.
   The :not([data-theme="light"]) branch is wrapped in @media so
   Auto mode only flips to the dark track when the SYSTEM actually
   prefers dark — without the @media wrap, Auto + system-light was
   getting the white track on a white card and the switch read as
   "just a floating knob" instead of a proper pill. */
:root[data-theme="dark"] .settings-switch {
  background: rgba(255, 255, 255, 0.16);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .settings-switch {
    background: rgba(255, 255, 255, 0.16);
  }
}
.settings-switch::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: white;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22), 0 0 0 0.5px rgba(0, 0, 0, 0.05);
  transition: transform 0.22s cubic-bezier(0.34, 1.3, 0.64, 1);
}
.settings-switch:checked {
  background: var(--accent);
}
/* Dark-mode + light-theme overrides: the unchecked-state dark-mode
   rule above (:root[data-theme="dark"] .settings-switch) sits at
   specificity (0,2,1), one weight heavier than the bare
   .settings-switch:checked (0,2,0). Without these matching-
   specificity :checked variants the accent fill never wins in dark
   mode — the track stays the translucent-white "off" colour even
   when the input is checked. Mirrors the same theme-bracketing
   pattern used by every other --accent-bound surface. */
:root[data-theme="dark"] .settings-switch:checked,
:root[data-theme="light"] .settings-switch:checked {
  background: var(--accent);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .settings-switch:checked {
    background: var(--accent);
  }
}
.settings-switch:checked::before {
  transform: translateX(18px);
}
.settings-switch:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Styled select for the in-card select rows. Mirrors the global
   select look but tightened to the card row height. */
.settings-select-control {
  height: 32px;
  padding: 0 28px 0 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-strong);
  color: var(--text);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  /* Custom chevron — matches the disclosure indicator vibe of the
     macOS Settings popover-buttons. */
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><path d='m3 5 3 3 3-3' fill='none' stroke='%236e6e73' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 12px;
  min-width: 168px;
  transition: border-color 0.14s ease, box-shadow 0.14s ease;
}
.settings-select-control:focus {
  outline: none;
  border-color: rgba(63, 148, 186, 0.45);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Action row — a button styled as a full-width card row, used in
   the Backup pane. The chevron icon on the right gives it a clear
   "this does something" affordance, similar to disclosure rows in
   iOS Settings. */
.settings-action-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 18px;
  padding: 14px 16px;
  min-height: 50px;
  border: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
  color: var(--text);
  transition: background 0.14s ease;
}
.settings-action-row:hover,
.settings-action-row:focus-visible {
  outline: none;
  background: rgba(63, 148, 186, 0.06);
}
.settings-action-icon {
  width: 20px;
  height: 20px;
  color: var(--accent);
}

/* Legacy form-field classes kept for backward-compat — the Account
   pane was refactored 2026-05-28 to use the .settings-card-row-input
   pattern below (left text + right inline input), matching the rest
   of the settings rows. These rules are dead in the current HTML but
   kept defined to avoid layout breakage if anything still references
   them through external customisations. */
.settings-card-form { gap: 14px; padding: 18px; }
.settings-form-field { display: flex; flex-direction: column; gap: 6px; }
.settings-form-field > span:first-child {
  font-size: 12px; font-weight: 600; color: var(--muted);
  letter-spacing: 0.02em; text-transform: uppercase;
}
.settings-form-field input {
  height: 38px; padding: 0 12px;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface-strong); color: var(--text); font-size: 14px;
  transition: border-color 0.14s ease, box-shadow 0.14s ease;
}
.settings-form-field input:focus {
  outline: none; border-color: rgba(63, 148, 186, 0.5);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.settings-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

/* Account-pane bottom action bar — Sign out (danger pill, left) and
   Save credentials (primary CTA, right) on opposite ends so the
   destructive action stays spatially distant from the affirmative one
   (same convention as macOS Settings, GitHub account settings, etc.).
   margin-top breathes the bar away from the password card / match-
   error above it. */
.settings-account-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 18px;
}

/* Sign out — danger pill matching .primary-action's footprint
   (44px height, same 0 18px padding, same 700 weight) so the two
   buttons read as a paired set with consistent dimensions. Outlined
   instead of filled so the destructive intent is clear but doesn't
   compete visually with the primary save button. */
.settings-sign-out {
  min-height: 44px;
  padding: 0 18px;
  border: 1px solid rgba(254, 95, 99, 0.34);
  border-radius: 999px;
  background: transparent;
  color: var(--danger);
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.14s ease, border-color 0.14s ease;
}
.settings-sign-out:hover {
  background: var(--danger-soft);
  border-color: rgba(254, 95, 99, 0.55);
}

/* Inline input control — sits on the right of an .settings-card-row-
   input row, mirroring .settings-select-control's height/border/radius
   so password + select rows share one visual rhythm. min-width 240px
   gives passwords enough breathing room while still letting the left-
   side label text get its full description width on a 960px shell. */
.settings-input-control {
  height: 32px;
  min-width: 240px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-strong);
  color: var(--text);
  font-size: 13px;
  font-weight: 500;
  font-family: inherit;
  transition: border-color 0.14s ease, box-shadow 0.14s ease;
}
.settings-input-control:focus {
  outline: none;
  border-color: rgba(63, 148, 186, 0.45);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Account password-mismatch error — sits between the password card
   and the Session card. Tighter spacing than the form-bottom version
   it replaces; small + danger-coloured so it reads as a row-level
   validation note rather than a separate status block. */
.settings-pane-error {
  display: block;
  margin: 8px 4px 12px;
  font-size: 12px;
  color: var(--danger);
  letter-spacing: 0.01em;
}

/* Hide the divider that precedes the current-password row when the
   row itself is hidden (no existing account → no "current password"
   prompt). Without this, a stranded divider would float between
   Username and New password, breaking the card's visual rhythm.
   Uses :has() — supported in all modern browsers. The [data-current-
   divider] attribute marks the specific divider; the .hidden class
   on the adjacent row is the trigger. */
.settings-card-divider[data-current-divider]:has(+ .settings-card-row.hidden) {
  display: none;
}

/* Single-line helper text below a settings card — used for terse
   requirements (e.g. "Password must be at least 8 characters") that
   don't deserve per-row description chrome. Sits closer to the card
   than .settings-pane-error, less weight than the pane head's
   subtitle. */
.settings-pane-helper {
  display: block;
  margin: 8px 4px 0;
  font-size: 12px;
  color: var(--muted);
  letter-spacing: 0.01em;
}

/* Status text (success / error / saving…) shown below action panes.
   Slightly smaller than the legacy write-status to fit the tighter
   visual rhythm. */
.settings-pane-status:not(:empty) {
  margin-top: 8px;
  padding: 8px 12px;
  border-radius: 8px;
  font-size: 13px;
}

/* In the new card layout the existing .settings-shortcut-key sits
   directly inside .settings-card-row instead of the old wrapper —
   reset the listening-pulse box-shadow so it doesn't overflow the
   card boundary on top/bottom. The pulse uses spread rather than
   y-offset so it still reads as a halo. */
.settings-card .settings-shortcut-key {
  min-width: 72px;
}

/* Subsection heading inside a Settings pane — smaller than the
   pane head, uppercase, muted. Separates "Global" from "Editing
   mode" shortcut groups. line-height:1.2 tightens the line box so
   the heading and its note below read as a title+subtitle pair
   rather than two separate paragraphs — the loose default 1.5
   line-height made the head's box ~16px tall, leaving visible
   whitespace below the cap-height that pushed the note further
   down visually than its 4px margin alone would. */
.settings-subsection-head {
  margin: 18px 0 0;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}
.settings-subsection-note {
  margin: 6px 0 10px;
  font-size: 12px;
  color: var(--muted);
  line-height: 1.5;
}
.settings-subsection-note em {
  font-style: normal;
  color: var(--text);
  font-weight: 500;
}

/* Wraps a subsection's <h4> head + adjacent <p> note into a single
   .settings-pane flex child so the pane's `gap: 18px` doesn't drive
   a giant chasm between the heading and its description. Inside the
   wrapper, the h4 and p use their own tight margins so they read as
   a title+subtitle pair. Without this wrapper, every h4+p pair was
   visually two paragraphs separated by a card-sized gap. */
.settings-subsection-titlepair {
  display: block;
}

/* Read-only key display — same visual as the rebindable key
   button but inert (no hover lift, no listening pulse, no cursor
   pointer, no focus halo). Used for editing-mode shortcuts that
   are universal conventions and not user-remappable.

   ALIGNMENT FIX: the readonly variant is a <span>, which defaults to
   `display: inline` and therefore IGNORES min-width / min-height from
   the base .settings-shortcut-key rule. That made the chip render at
   just its text-height (~18px) instead of the intended 34px, sitting
   visually too high (vertically off-center) inside its 50px row —
   the "indented upward" bug the user spotted in the Editing section.
   inline-grid + place-items: center honours both min dimensions AND
   centers the symbol both axes, so the readonly chip now matches the
   button variant's physical footprint exactly. */
.settings-shortcut-key-readonly {
  display: inline-grid;
  place-items: center;
  cursor: default;
  pointer-events: none;
  /* Tighter padding because the symbol strings (⌘⇧X) are typically
     shorter than the named keys (Backspace) and need less room. */
  padding: 0 12px;
}
.settings-shortcut-key-readonly:hover,
.settings-shortcut-key-readonly:focus-visible {
  border-color: var(--line);
  box-shadow: none;
}

/* Action rows can be either <button> (Backup pane: import/export) or
   <a target="_blank"> (About pane: external links). The <a> variant
   needs text-decoration:none so the row body doesn't show an underline
   on hover the way a stock anchor does. */
a.settings-action-row,
.settings-action-link {
  text-decoration: none;
  color: inherit;
}

/* About pane: identity row (name + tagline + version pill) and the
   compact brand block to its left. Lays out as a left-aligned group
   (logo + text stack) with the version chip pinned to the right of
   the row — same .settings-card-row grid the toggle/select rows use,
   just with a richer left column than the other rows. */
.settings-about-identity {
  cursor: default;
}
.settings-about-identity:hover {
  background: transparent;
}
.settings-about-brand {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}
.settings-about-logo {
  width: 36px;
  height: 36px;
  border-radius: 9px;
  flex-shrink: 0;
}
.settings-about-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.settings-about-text strong {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.01em;
}
.settings-about-text span {
  font-size: 12.5px;
  color: var(--muted);
  line-height: 1.45;
}
.settings-about-text em {
  font-style: normal;
  color: var(--text);
  font-weight: 500;
}

/* Version chip — small monospace pill, accent-tinted so it reads as
   metadata rather than a clickable element. Pattern lifted from
   GitHub's release badges and macOS About panels. */
.settings-version-badge {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-family: -apple-system, BlinkMacSystemFont, "SF Mono", monospace;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  flex-shrink: 0;
}

/* Wraps the installed-version pill + the optional "update available"
   link as a tight inline group so they read as one visual unit. */
.settings-about-version-group {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

/* "Update available" affordance — only visible when the server's
   /api/version response includes a latest_github tag higher than
   the installed version. Soft accent-bordered pill with an up arrow
   so the meaning ("new release exists, click to view") reads at a
   glance. Same monospace + size as the installed-version badge so
   the two pills sit cleanly next to each other. Hover lifts to the
   filled accent so the click target is unmistakable. */
.settings-version-update {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px 4px 8px;
  border: 1px solid rgba(63, 148, 186, 0.55);
  border-radius: 999px;
  background: transparent;
  color: var(--accent);
  font-family: -apple-system, BlinkMacSystemFont, "SF Mono", monospace;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
.settings-version-update:hover,
.settings-version-update:focus-visible {
  outline: none;
  background: var(--accent);
  color: white;
  border-color: var(--accent);
}
.settings-version-update svg {
  width: 11px;
  height: 11px;
}

/* ============================================================
   ===== MOBILE RESPONSIVENESS =====
   Everything below lives inside @media blocks — desktop layout
   is unchanged. Tablet (≤768px) collapses the 3-panel content
   grid to a single stacked column and shrinks the top bar.
   Phone (≤480px) tightens padding further and re-anchors
   floating overlays so they don't overflow the viewport edge.
   ============================================================ */

/* ----- TABLET / LANDSCAPE PHONE — ≤768px -----
   The 3-panel content row was designed for ≥1024px. Below 768px
   the columns shrink past their min-width floors and the layout
   either horizontally scrolls or visually breaks. Switch to
   flex-column so the panels stack vertically, full width each.
   Top bar tightens its padding + gap so it doesn't dominate the
   short viewport. Settings shell drops the 220px sidebar — its
   2-column grid becomes a single column with the nav as a
   horizontal scroll strip across the top. */
@media (max-width: 768px) {
  /* Top bar: tighten padding, drop visual weight. The brand-text
     was already hidden at 900px (logo-only mode); here we also
     pull the gap down so the search has more room next to the
     icons. */
  .topbar-inner {
    padding: 12px 16px;
    gap: 10px;
  }
  /* Search shouldn't claim 480px+ on a 768px viewport — let it
     flex-shrink to the available space minus brand + actions. */
  .search {
    flex: 1 1 0;
    min-width: 0;
  }

  /* Content grid: stack panels vertically. Each panel takes full
     width. The default flex-row layout had hard min-width floors
     that forced horizontal overflow at this size. */
  .content-grid {
    flex-direction: column;
    gap: 16px;
  }
  .content-grid > .rooms-panel,
  .content-grid > .drawers-panel,
  .content-grid > .detail-panel {
    flex: 0 0 auto;
    width: 100%;
    max-width: 100%;
    min-height: 0;
  }
  /* Rooms panel: cap height so the user doesn't have to scroll
     past a giant wing/room list to reach memories. The wings list
     inside the panel scrolls within this cap. */
  .content-grid > .rooms-panel {
    max-height: 220px;
  }
  /* Memories panel: room enough to show a few cards without
     dominating the viewport. */
  .content-grid > .drawers-panel {
    min-height: 50vh;
  }

  /* Settings shell: drop the 2-column nav+content grid. The
     220px sidebar leaves nearly nothing for content on tablet-
     width. Use a single column with the nav as a horizontal
     scroll strip across the top. */
  .settings-shell {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
    height: min(680px, calc(100vh - 32px));
  }
  .settings-nav-pane {
    border-right: 0;
    border-bottom: 1px solid var(--line);
    padding: 12px 14px;
    /* Convert the vertical nav list to a horizontal scroll strip. */
    overflow-x: auto;
    overflow-y: hidden;
  }
  .settings-nav {
    flex-direction: row;
    flex-wrap: nowrap;
    gap: 6px;
  }
  .settings-nav-item {
    flex: 0 0 auto;
    white-space: nowrap;
  }
  /* Hide the "APRICITY / Settings" header in the sidebar at this
     size — the pane title is already redundant with the overlay
     context (you opened settings) and takes vertical space the
     content needs. */
  .settings-nav-head {
    display: none;
  }
  /* Content pane: tighter horizontal padding so each card uses
     the available width. */
  .settings-content-pane {
    padding: 20px 18px 24px;
  }

  /* Notification dropdown + every .action-menu: clamp width so
     they never overflow the right edge of the viewport. */
  .action-menu {
    max-width: calc(100vw - 24px);
  }

  /* Floating action buttons: drop the right-edge inset so the
     three 52px circles fit comfortably in a narrower panel. */
  .detail-panel-delete { right: 24px; }
  .detail-panel-edit { right: 88px; }
  .detail-panel-retry { right: 152px; }

  /* Footer info: stack stats vertically + tighten padding so the
     bottom edge doesn't claim half the short viewport. */
  .footer {
    padding: 12px 16px;
  }
}

/* ----- PHONE PORTRAIT — ≤480px -----
   Further tightening: drop the brand text gap, reduce top-action
   spacing, shrink the floating button trio so they all fit on a
   ~320px-wide detail panel. */
@media (max-width: 480px) {
  /* Top bar: smaller gap between actions so all six icons fit
     even on a 360px screen. The brand-text is already hidden
     from the 900px breakpoint. */
  .topbar-inner {
    padding: 10px 12px;
    gap: 8px;
  }
  .top-actions {
    gap: 6px;
  }
  /* Brand logo a touch smaller so it doesn't dominate. */
  .brand-logo,
  .brand-mark {
    width: 40px;
    height: 40px;
  }

  /* Detail-panel buttons: even tighter inset + smaller circle.
     52px → 44px (Apple HIG minimum). At 44px stride 56 (44 + 12
     gap), retry needs right = 16 + 56 + 56 = 128. */
  .detail-panel-edit,
  .detail-panel-delete,
  .detail-panel-retry {
    width: 44px;
    height: 44px;
  }
  .detail-panel-delete { right: 16px; }
  .detail-panel-edit { right: 72px; }
  .detail-panel-retry { right: 128px; }
  .detail-panel-edit svg,
  .detail-panel-delete svg,
  .detail-panel-retry svg {
    width: 18px;
    height: 18px;
  }

  /* Settings shell: minimum padding so it fills the screen
     instead of floating awkwardly with margin around it. */
  .settings-overlay {
    padding: 12px;
  }
  .settings-shell {
    border-radius: 18px;
    height: min(680px, calc(100vh - 24px));
  }
  .settings-pane-head h3 {
    font-size: 19px;
  }
  /* Settings input rows: stack label above input on portrait
     phone — the right-side 240px input crowds the text on a
     360px shell width. */
  .settings-card-row.settings-card-row-input {
    grid-template-columns: 1fr;
    gap: 6px;
  }
  .settings-card-row.settings-card-row-input .settings-input-control {
    width: 100%;
    min-width: 0;
  }
  /* Settings select rows: same stacking treatment. */
  .settings-card-row.settings-card-row-select {
    grid-template-columns: 1fr;
    gap: 8px;
  }
  .settings-card-row.settings-card-row-select .settings-select-control {
    width: 100%;
    min-width: 0;
  }

  /* Notification dropdown: anchor right with a small inset so it
     doesn't kiss the viewport edge. Wider than the 184px desktop
     min so notification text can wrap into the available width. */
  .notifications-menu {
    right: 8px !important;
    min-width: calc(100vw - 32px);
    max-width: calc(100vw - 16px);
  }

  /* Trash + drafts + write sheets: edge-to-edge on phone since
     the floating shell pattern wastes horizontal pixels at this
     size. */
  .write-panel,
  .edit-panel {
    width: calc(100vw - 16px);
    max-width: calc(100vw - 16px);
  }

  /* Footer: hide the system-info row entirely on phone — it
     dominates and isn't useful at this size. Counts stay. */
  .footer-system {
    display: none;
  }
}

/* Defensive: prevent any rogue horizontal overflow at any width.
   Mobile layouts are full-width by design; horizontal scroll
   below 768px is always a bug, not a feature. */
@media (max-width: 768px) {
  body {
    overflow-x: hidden;
  }
}
