/* =========================================================================
   Spotify AI Analytics — Component set (B-1)
   =========================================================================
   One shared, token-driven component vocabulary, built ONLY from the custom
   properties in design-tokens.css — no colour/spacing literals. Namespaced
   `ui-` so none of these selectors collide with the legacy classes in
   style.css. Later items copy this layer's patterns; per-page ad-hoc CSS
   gets deleted as pages migrate onto it.
   ========================================================================= */

/* ── Buttons (≥44px tap targets) ─────────────────────────────────────── */
.ui-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: var(--tap-min);
  padding: 0 var(--space-md);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  line-height: 1;
  cursor: pointer;
  text-decoration: none;
  transition:
    background-color var(--transition),
    color var(--transition),
    border-color var(--transition);
}

.ui-btn svg {
  flex-shrink: 0;
}

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

.ui-btn-primary {
  background-color: var(--color-accent);
  /* Not --color-text-inverse: that token is for text on inverted *surfaces*, and
     on the accent fill it measured 3.2:1 (WCAG AA wants 4.5). */
  color: var(--color-on-accent);
  font-weight: var(--font-weight-semibold);
}

.ui-btn-primary:hover:not(:disabled) {
  background-color: var(--color-accent-hover);
}

.ui-btn-secondary {
  background-color: transparent;
  border-color: var(--border-strong);
  color: var(--color-text);
}

.ui-btn-secondary:hover:not(:disabled) {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.ui-btn-danger {
  background-color: transparent;
  border-color: var(--color-danger);
  color: var(--color-danger);
}

.ui-btn-danger:hover:not(:disabled) {
  background-color: var(--color-danger-dim);
}

.ui-btn-ghost {
  background-color: transparent;
  color: var(--color-accent);
}

.ui-btn-ghost:hover:not(:disabled) {
  background-color: var(--color-accent-dim);
}

.ui-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ── Cards ───────────────────────────────────────────────────────────── */
.ui-card {
  background-color: var(--color-surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  padding: var(--space-md);
  box-shadow: var(--shadow-sm);
  transition:
    background-color var(--transition),
    box-shadow var(--transition);
}

.ui-card-hoverable:hover {
  background-color: var(--color-surface-card-hover);
  box-shadow: var(--shadow-md);
}

.ui-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}

.ui-card-title {
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  line-height: var(--line-height-tight);
}

/* ── Discovery hub card (E-1) ────────────────────────────────────────── */

/* The whole card is one link, so a tap anywhere on it opens the surface and
   the hub's five entry points are one tap each.  The anchor carries the card's
   own ``ui-card`` skin; ``display: flex`` is what lays it out as the block the
   grid places, instead of an inline link with a box around it.  Tokens only —
   the component layer states no literal of its own. */
.discovery-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-height: var(--tap-comfortable);
  color: inherit;
  text-decoration: none;
}

.discovery-card:focus-visible {
  outline: var(--focus-ring) solid var(--color-accent);
  outline-offset: var(--space-3xs);
}

.discovery-card-icon {
  color: var(--color-accent);
}

.discovery-card-summary {
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
}

/* ── Badges / pills ──────────────────────────────────────────────────── */
.ui-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs) var(--space-xs);
  border-radius: var(--radius-full);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  line-height: var(--line-height-tight);
  background-color: var(--color-surface-input);
  color: var(--color-text-secondary);
}

.ui-badge-accent {
  background-color: var(--color-accent-dim);
  color: var(--color-accent);
}

.ui-badge-danger {
  background-color: var(--color-danger-dim);
  color: var(--color-danger);
}

.ui-badge-warning {
  background-color: var(--color-warning-dim);
  color: var(--color-warning);
}

/* ── Banners ─────────────────────────────────────────────────────────── */
/* G-7 · the ingest-staleness mark. Token-only: this file is exempt from the
   colour/spacing ratchets, and it stays that way so the mark costs the ratchet
   nothing. */
.ui-banner {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  padding: var(--space-md);
  margin-bottom: var(--space-md);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  background-color: var(--color-surface-card);
  color: var(--color-text);
}

.ui-banner p {
  margin: 0;
  color: var(--color-text-secondary);
}

.ui-banner a {
  color: var(--color-accent);
}

.ui-banner-warning {
  border-color: var(--color-warning-dim);
  background-color: var(--color-warning-dim);
}

.ui-banner-warning strong {
  color: var(--color-warning);
}

/* ── Form fields ─────────────────────────────────────────────────────── */
.ui-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.ui-label {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-secondary);
}

.ui-input,
.ui-select,
.ui-textarea {
  min-height: var(--tap-min);
  padding: 0 var(--space-sm);
  background-color: var(--color-surface-input);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  transition: border-color var(--transition);
  appearance: none;
}

.ui-textarea {
  min-height: calc(var(--tap-min) * 2);
  padding-top: var(--space-xs);
  padding-bottom: var(--space-xs);
  resize: vertical;
}

.ui-input:focus,
.ui-select:focus,
.ui-textarea:focus {
  outline: none;
  border-color: var(--color-accent);
}

.ui-input::placeholder,
.ui-textarea::placeholder {
  color: var(--color-text-muted);
}

.ui-input:focus-visible,
.ui-select:focus-visible,
.ui-textarea:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

/* ── Layout ──────────────────────────────────────────────────────────── */
.ui-stack {
  display: flex;
  flex-direction: column;
}

.ui-stack-gap-xs { gap: var(--space-xs); }
.ui-stack-gap-sm { gap: var(--space-sm); }
.ui-stack-gap-md { gap: var(--space-md); }
.ui-stack-gap-lg { gap: var(--space-lg); }

.ui-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.ui-row-wrap {
  flex-wrap: wrap;
}

.ui-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
}

@media (min-width: 480px) {
  .ui-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 768px) {
  .ui-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ── Text utilities ──────────────────────────────────────────────────── */
.ui-text-secondary { color: var(--color-text-secondary); }
.ui-text-muted { color: var(--color-text-muted); }
.ui-text-accent { color: var(--color-accent); }

/* ── Loading / feedback ──────────────────────────────────────────────── */
.ui-skeleton {
  background-color: var(--skeleton-base);
  border-radius: var(--radius-sm);
  animation: ui-skeleton-pulse 1.5s ease-in-out infinite;
}

@keyframes ui-skeleton-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

.ui-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  padding: var(--space-xl) var(--space-md);
  text-align: center;
  color: var(--color-text-secondary);
}

.ui-empty-title {
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* ── Track action row (B-6) ──────────────────────────────────────────── */
/* One shared row for every surface that lists a track: the primary action
   (add to queue while the playback scope is granted, otherwise open on
   Spotify), open on Spotify, open context.  It renders inside the narrow
   info column of a ranked list, so it wraps instead of widening the page —
   B-5's 380 px no-horizontal-scroll guarantee is measured by
   tests/test_mobile_layout.py. */
.ui-track-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  min-width: 0;
  max-width: 100%;
  margin-top: var(--space-2xs);
}

.ui-track-actions > a,
.ui-track-actions > button {
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  padding: 0 var(--space-sm);
  font-size: var(--font-size-xs);
  white-space: nowrap;
}

/* ── B-8 · Focus, skip link, motion preference ───────────────────────── */
/*
   Focus states
   ------------
   The ring is declared once, here, for every focusable element the app
   renders — the legacy sheet's `.chat-input:focus`, `.search-input:focus`,
   `.explore-select:focus` and the like set `outline: none` and replace it with
   nothing, so before this rule a keyboard user could not see where they were.
   `:where()` keeps the specificity at zero, so a component that wants its own
   ring still wins without an `!important`.
*/
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--focus-ring) solid var(--color-accent);
  outline-offset: var(--focus-ring);
}

/* A ring drawn outside the box is clipped by a container with `overflow:
   hidden`; such a row opts into the inset variant instead of losing the ring. */
.ui-focus-inset:focus-visible {
  outline-offset: calc(-1 * var(--focus-ring));
}

/*
   Skip link
   ---------
   The first focusable element in `<body>`, visible only while it holds focus:
   a keyboard user can leave the twelve sidebar links with one keystroke.  It
   targets `#main-content`, the `<main>` landmark, which carries
   `tabindex="-1"` so focus actually lands there.
*/
.skip-link {
  position: absolute;
  left: var(--space-sm);
  top: calc(-1 * var(--space-3xl));
  z-index: var(--z-overlay);
  /* M3 · Android tap-target floor: the skip link is a real control, so it
     honours the same >= 44px hit area as every other interactive element. */
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
  padding: var(--space-xs) var(--space-md);
  border-radius: var(--radius-md);
  background-color: var(--color-accent);
  color: var(--color-text-inverse);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  text-decoration: none;
  transition: top var(--transition-fast);
}

.skip-link:focus {
  top: var(--space-sm);
}

/* Visually hidden, still announced: chat has no visible page title, and every
   document still needs its `<h1>` in the outline. */
.ui-visually-hidden {
  position: absolute;
  inline-size: var(--hairline);
  block-size: var(--hairline);
  padding: 0;
  margin: calc(-1 * var(--hairline));
  overflow: hidden;
  white-space: nowrap;
  border: 0;
  clip-path: inset(50%);
}

/* ── F-2 · the context chip ───────────────────────────────────────────── */
/*
   What the sheet is currently speaking about.  The chip is server-rendered
   inside the sheet (`partials/_chat_sheet.html`) and reads as one line: the
   kind, the entity, and the control that clears it.  It is placed above the
   chat layer's own block so that block's token audit still slices exactly the
   text it was written against.

   `[hidden]` is re-stated for the same reason the sheet re-states it: an
   author `display: flex` outranks the user agent's `display: none`, and a
   chip that cannot be hidden is a chip that claims a context the page never
   named.
*/
.ui-chat-context {
  display: flex;
  align-self: flex-start;
  align-items: center;
  gap: var(--space-xs);
  max-inline-size: 100%;
  padding: var(--space-2xs) var(--space-sm);
  border: var(--hairline) solid var(--border-subtle);
  border-radius: var(--radius-full);
  background: var(--color-surface-input);
}

.ui-chat-context[hidden] {
  display: none;
}

.ui-chat-context-name {
  min-inline-size: 0;
  overflow: hidden;
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ui-chat-context-clear {
  min-height: var(--tap-min);
  font-size: var(--font-size-xs);
}

/* ── F-1 · the chat layer ─────────────────────────────────────────────── */
/*
   The chat is a layer, not a destination: the shell renders
   `partials/_chat_sheet.html` on every page, this is what makes it a bottom
   sheet, and `js/chat-sheet.js` owns the open/close, the gesture and the
   scroll it gives back.  The transcript inside it reuses the chat bubble
   classes style.css already draws (`.chat-msg`, `.chat-sql-block`,
   `.chat-input`), so the layer and /chat cannot drift apart.

   Every value here is a token from design-tokens.css — this block states no
   colour and no spacing literal of its own (tests/test_chat_sheet.py pins
   that), and it lives in the component layer rather than in style.css, whose
   B-2 block is read by tests/test_tab_nav.py.

   `[hidden]` has to be re-stated: an author `display: flex` outranks the user
   agent's `display: none` for the hidden attribute, and a sheet that cannot be
   hidden is a sheet that covers the page it belongs to.
*/
.ui-chat-sheet {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: var(--z-overlay);
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  max-block-size: 70vh;
  padding: var(--space-sm) var(--space-md) var(--space-md);
  border-top: var(--hairline) solid var(--border-color);
  border-top-left-radius: var(--radius-lg);
  border-top-right-radius: var(--radius-lg);
  background: var(--color-surface-raised);
  box-shadow: var(--shadow-lg);
}

.ui-chat-sheet[hidden] {
  display: none;
}

.ui-chat-sheet-head {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* The drag affordance.  Drawn, not borrowed from an image: the thickness is
   the smallest step of the spacing scale. */
.ui-chat-sheet-grip {
  position: absolute;
  top: var(--space-2xs);
  left: 50%;
  inline-size: var(--space-2xl);
  block-size: var(--space-3xs);
  transform: translateX(-50%);
  border-radius: var(--radius-full);
  background: var(--border-strong);
}

.ui-chat-sheet-title {
  margin: 0;
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
}

.ui-chat-sheet-tools {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  margin-inline-start: auto;
}

.ui-chat-thread-list {
  max-inline-size: var(--space-3xl);
}

.ui-chat-sheet-messages {
  flex: 1 1 auto;
  overflow-y: auto;
  /* Scrolling the transcript must not scroll the page behind it. */
  overscroll-behavior: contain;
  padding: var(--space-xs) 0;
}

.ui-chat-welcome-title {
  margin: 0;
  font-size: var(--font-size-md);
}

.ui-chat-sheet-input {
  padding-top: var(--space-xs);
  border-top: var(--hairline) solid var(--border-subtle);
}

/* ── LLM-1 · the runtime model panel ─────────────────────────────────────
   One collapsible block above the composer, on both surfaces.  Collapsed it
   is a single line — what is answering right now — so it costs the transcript
   no room until someone opens it. */
.ui-chat-model {
  margin: 0 var(--space-md) var(--space-xs);
  border: var(--hairline) solid var(--border-color);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

.ui-chat-model[open] {
  padding-bottom: var(--space-xs);
}

.ui-chat-model-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  min-block-size: var(--tap-min);
  padding: 0 var(--space-xs);
  cursor: pointer;
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}

.ui-chat-model-label {
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

.ui-chat-model-current {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ui-chat-model-form {
  display: grid;
  gap: var(--space-2xs);
  padding: var(--space-xs);
  border-top: var(--hairline) solid var(--border-subtle);
}

.ui-chat-model-field {
  display: grid;
  gap: var(--space-3xs);
  font-size: var(--font-size-2xs);
  color: var(--color-text-secondary);
}

.ui-chat-model-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.ui-chat-model-status {
  min-block-size: var(--space-md);
  margin: 0;
  font-size: var(--font-size-2xs);
  color: var(--color-text-muted);
}

.ui-chat-model-status[data-tone="ok"] {
  color: var(--color-success);
}

.ui-chat-model-status[data-tone="error"] {
  color: var(--color-danger);
}

.ui-chat-stop {
  align-self: flex-end;
}

/* U-6 · per-message controls.  Always drawn rather than revealed on hover: a
   control that only exists under a pointer is not a control. */
.ui-chat-msg-actions {
  display: flex;
  gap: var(--space-3xs);
  margin-top: var(--space-2xs);
}

.ui-chat-action {
  min-height: var(--tap-min);
  font-size: var(--font-size-xs);
}

.ui-chat-stopped {
  margin-inline-start: var(--space-2xs);
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}

/* F-1 · the track-row half: the same act a swipe performs, for anyone without
   a pointer. */
.ui-track-ask {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--tap-min);
  min-block-size: var(--tap-min);
}

/* The sheet's opener is the header's Ask button (see studio.css).  The old
   fixed bottom-right button lived over the page's own controls, so its rules
   are gone; this is the one remaining hook for anything that still asks. */
.ui-chat-fab {
  display: none;
}

@media (min-width: 768px) {
  .ui-chat-sheet {
    right: var(--space-md);
    bottom: var(--space-md);
    left: var(--space-md);
    max-inline-size: calc(var(--space-3xl) * 12);
    margin-inline: auto;
    border: var(--hairline) solid var(--border-color);
    border-radius: var(--radius-lg);
  }
}

/*
   Motion preference
   -----------------
   "Honoured" means the animation is not run, not that it runs faster: under
   `prefers-reduced-motion: reduce` every animation and transition collapses to
   a single frame and smooth scrolling stops.  `animation-iteration-count: 1`
   is what stops the looping ones (the skeleton pulse, the chat typing cursor);
   a duration of 0.01ms alone would still loop them forever.
   Measured with a `reduced_motion="reduce"` browser context in
   `tests/e2e/test_a11y.py`, and pinned as text by `tests/test_accessibility.py`.
*/
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Playback history (C-5) ──────────────────────────────────────────── */
/* The rows of a history page and of the track context page reuse the ranked
   list's frame; what is added here is the meta line — the server-rendered
   relative time, the genre tag, where the play came from — and the pager's
   own row.  Built from tokens only: this layer states no colour or spacing
   literal of its own, so the ratchet in tests/test_design_tokens.py cannot
   move for it. */
.history-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  margin-top: var(--space-2xs);
}

.history-time,
.history-genre {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--space-2xs);
  border-radius: var(--radius-full);
  background: var(--color-surface-input);
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  line-height: var(--line-height-normal);
}

.history-genre {
  background: var(--color-accent-dim);
  color: var(--color-text);
}

.history-source,
.history-platform {
  font-size: var(--font-size-xs);
}

.history-row .ranked-info {
  min-width: 0;
}

.history-more {
  display: flex;
  justify-content: center;
  padding: var(--space-sm) 0;
}

.history-end {
  font-size: var(--font-size-xs);
  text-align: center;
}

.feature-name {
  flex: 1 1 auto;
}

.feature-value {
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
   Memory browser and learning journal — one page, two filters (F-11)

   The page is rendered from `memory.html` plus the two panels: the journal,
   server-rendered night by night, and the memory list, fetched from
   `/api/memory`.  Everything here is built from the design tokens rather than
   from literals in the template, because B-1's ratchet counts a spacing value
   written in a page as restyling debt that can only be paid twice.

   This file is one of the two token files the ratchet exempts, so the values
   below are the single home for this page's spacing, size and colour.
   --------------------------------------------------------------------------- */

.ui-facts {
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}

.ui-filter-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin: var(--space-md) 0;
}

/* ── The journal ──────────────────────────────────────────────────────────── */

.ui-journal {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.ui-journal-days {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.ui-journal-entry {
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  padding: var(--space-md);
}

/* The night the per-day filter selected. */
.ui-journal-entry[aria-current="true"] {
  border-color: var(--color-accent);
}

.ui-journal-day {
  margin: 0;
  font-size: var(--font-size-sm);
}

.ui-journal-quiet {
  color: var(--color-text-muted);
  font-weight: 400;
}

.ui-journal-narrative {
  margin: var(--space-xs) 0 var(--space-sm);
  font-size: var(--font-size-sm);
  line-height: 1.55;
}

.ui-journal-figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.ui-journal-figures li {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

.ui-journal-figures strong {
  color: var(--color-text);
}

.ui-journal-figures code,
.ui-journal-source {
  font-size: var(--font-size-2xs);
  color: var(--color-text-secondary);
}

.ui-journal-foot {
  margin: var(--space-sm) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* ── A memory row ─────────────────────────────────────────────────────────── */

.ui-memory-item {
  padding: var(--space-md);
}

.ui-memory-body {
  display: flex;
  gap: var(--space-md);
  align-items: flex-start;
}

.ui-memory-main {
  flex: 1;
  min-width: 0;
}

.ui-memory-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  align-items: center;
  margin-bottom: var(--space-xs);
}

.ui-memory-date {
  margin-left: auto;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

.ui-memory-text {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.5;
}

.ui-memory-provenance {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

.ui-memory-meta {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

.ui-memory-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-sm);
}

/* A retired memory stays on the page so it can be restored — dimmed, and the
   badge beside it says what "dimmed" means. */
.ui-memory-retired {
  opacity: 0.62;
}

.ui-memory-delete {
  flex-shrink: 0;
  color: var(--color-danger);
}

/* One badge per memory type: the type is the per-type filter's vocabulary, so
   its colour is a token here rather than a literal emitted by the script. */
.ui-badge-type-preference { color: var(--color-accent); }
.ui-badge-type-observation { color: var(--color-warning); }
.ui-badge-type-behavioral-pattern { color: var(--color-purple); }

/* ── The inline editor the row's Edit button opens ────────────────────────── */

.ui-memory-editor {
  display: none;
  margin-top: var(--space-sm);
}

.ui-memory-editor-open {
  display: block;
}

.ui-memory-editor-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-xs);
}

.ui-memory-editor-field {
  flex: 1 1 8rem;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* Full width, for a thumb: the textarea spans the card, and the three short
   fields share a row and wrap to their own line on a phone. */
.ui-memory-editor textarea,
.ui-memory-editor input,
.ui-memory-editor select {
  width: 100%;
  box-sizing: border-box;
  margin-top: var(--space-2xs);
  padding: var(--space-xs);
  border-radius: var(--radius-sm);
}

.ui-memory-editor textarea {
  resize: vertical;
}

.ui-memory-editor-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-sm);
}

.ui-memory-editor-actions .ui-btn {
  flex: 1 1 6rem;
}

.ui-memory-pager {
  justify-content: center;
  align-items: center;
  margin-top: var(--space-lg);
}

/* ── B-4 · the connection state ──────────────────────────────────────── */
/* Token-only, like the rest of this file: the ratchet's ceiling does not move
   for a status line.  `offline` is the state that matters, so it is the one
   that changes colour — a green dot on a healthy connection is the default and
   carries no information. */
.ui-connection-status {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  padding: var(--space-2xs) var(--space-sm);
  margin-bottom: var(--space-md);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  background-color: var(--color-surface-card);
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
}

.ui-connection-dot {
  flex: none;
  width: var(--space-2xs);
  height: var(--space-2xs);
  border-radius: var(--radius-full);
  background-color: var(--color-success);
}

.ui-connection-label {
  color: var(--color-text);
  font-weight: var(--font-weight-medium);
}

.ui-connection-detail,
.ui-connection-noscript {
  color: var(--color-text-muted);
}

.ui-connection-status[data-connection-state="offline"] {
  border-color: var(--color-warning-dim);
  background-color: var(--color-warning-dim);
}

.ui-connection-status[data-connection-state="offline"] .ui-connection-dot {
  background-color: var(--color-warning);
}

.ui-connection-status[data-connection-state="offline"] .ui-connection-label {
  color: var(--color-warning);
}

/* A surface whose figures predate the connection.  Marked at the edge rather
   than dimmed: the numbers are still the last true reading, only no longer
   current, and the status line above says so in words. */
[data-surface][data-stale="true"] {
  border-left: var(--space-3xs) solid var(--color-warning);
}

/* ── B-4 · the four-state contract ───────────────────────────────────── */
/* One shape for `loading`, `empty` and `error`, so a region that cannot show
   data always shows a reason instead of collapsing to nothing. */
.ui-surface-state {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  padding: var(--space-sm) 0;
  color: var(--color-text-secondary);
}

.ui-surface-state-title {
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

.ui-surface-state-message {
  margin: 0;
  color: var(--color-text-secondary);
}

.ui-surface-state--empty {
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-xl) var(--space-md);
}

.ui-surface-state--loading .ui-surface-state-title {
  color: var(--color-text-muted);
}

.ui-surface-state--error .ui-surface-state-title {
  color: var(--color-danger);
}

/* B-4 · the offline shell's list of what cannot be read (drawn by sw.js's
   ``renderOfflineShell()``, which is why it carries no framework class of its
   own — it is one page, not a component). */
.offline-surfaces {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-sm);
  margin: 0;
  padding: 0;
  list-style: none;
}

.offline-surface {
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
}

/* =========================================================================
   Chat rail bar (LLM-1) + in-chat memory browser (F-6b)
   =========================================================================
   The page surface's header: a two-state view switch and one chip per LLM
   rail.  Health is painted on the chip dot from ``/api/system/llm/health``;
   the tooltip is the only place the resolved endpoint and active model are
   spelled out, so a hover answers "which rail am I on, and is it alive?".
   Everything here is token-driven — no colour literals.
   ========================================================================= */

.chat-railbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg);
  border-bottom: 1px solid var(--border-subtle);
  background: var(--color-surface);
}

.chat-tabs {
  display: inline-flex;
  gap: var(--space-3xs);
  padding: var(--space-3xs);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  background: var(--color-surface-input);
}

.chat-tab {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--color-text-muted);
  font: inherit;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  min-height: var(--tap-min);
  padding: var(--space-3xs) var(--space-md);
  border-radius: var(--radius-full);
  cursor: pointer;
}

.chat-tab:hover {
  color: var(--color-text);
  background: var(--color-surface-card-hover);
}

.chat-tab.is-active {
  color: var(--color-text-inverse);
  background: var(--color-accent);
}

.chat-rails {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.chat-rail-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  min-height: var(--tap-min);
  padding: var(--space-3xs) var(--space-sm);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  background: var(--color-surface-input);
  color: var(--color-text-secondary);
  font: inherit;
  font-size: var(--font-size-sm);
  cursor: pointer;
}

.chat-rail-chip:hover {
  border-color: var(--border-strong);
  color: var(--color-text);
}

.chat-rail-chip.is-active {
  border-color: var(--color-accent);
  color: var(--color-text);
  box-shadow: var(--shadow-sm);
}

.chat-rail-dot {
  flex: none;
  width: var(--space-xs);
  height: var(--space-xs);
  border-radius: var(--radius-full);
  background: var(--color-text-muted);
}

.chat-rail-dot[data-state="ok"] {
  background: var(--color-success);
}

.chat-rail-dot[data-state="auth"] {
  background: var(--color-warning);
}

.chat-rail-dot[data-state="unreachable"],
.chat-rail-dot[data-state="error"] {
  background: var(--color-danger);
}

.chat-rail-tooltip {
  position: absolute;
  top: calc(100% + var(--space-2xs));
  right: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  width: max-content;
  max-width: 36ch;
  padding: var(--space-sm);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
  box-shadow: var(--shadow-lg);
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
}

.chat-rail-tooltip[hidden] {
  display: none;
}

.chat-rail-tooltip-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  color: var(--color-text);
  font-weight: var(--font-weight-semibold);
}

.chat-rail-tooltip-state {
  color: var(--color-text-muted);
  font-size: var(--font-size-2xs);
  text-transform: uppercase;
}

.chat-rail-tooltip-state[data-state="ok"] {
  color: var(--color-success);
}

.chat-rail-tooltip-state[data-state="auth"] {
  color: var(--color-warning);
}

.chat-rail-tooltip-state[data-state="unreachable"],
.chat-rail-tooltip-state[data-state="error"] {
  color: var(--color-danger);
}

.chat-rail-tooltip-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
}

.chat-rail-tooltip-row span:first-child {
  color: var(--color-text-muted);
}

.chat-rail-tooltip-row code {
  font-family: var(--font-mono);
  font-size: var(--font-size-2xs);
  overflow-wrap: anywhere;
}

.chat-rail-tooltip-note {
  margin: var(--space-3xs) 0 0;
  color: var(--color-text-muted);
  font-size: var(--font-size-2xs);
}

/* ── The memory view ─────────────────────────────────────────────────── */

.chat-layout[data-chat-view="memory"] .chat-messages,
.chat-layout[data-chat-view="memory"] .ui-chat-model,
.chat-layout[data-chat-view="memory"] .chat-input-bar {
  display: none;
}

.chat-memory {
  display: flex;
  flex: 1;
  min-height: 0;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
}

.chat-memory[hidden] {
  display: none;
}

.chat-memory-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.chat-memory-field {
  flex: 1 1 auto;
  min-width: 0;
}

.chat-memory-search {
  width: 100%;
}

.chat-memory-status {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}

.chat-memory-status[data-state="error"] {
  color: var(--color-danger);
}

.chat-memory-list {
  display: flex;
  flex: 1;
  min-height: 0;
  flex-direction: column;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-y: auto;
}

.chat-memory-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-sm);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-surface-card);
}

.chat-memory-row-main {
  min-width: 0;
}

.chat-memory-row-text {
  margin: 0;
  color: var(--color-text);
  overflow-wrap: anywhere;
}

.chat-memory-row-meta {
  margin: var(--space-3xs) 0 0;
  color: var(--color-text-muted);
  font-size: var(--font-size-2xs);
}

.chat-memory-delete {
  flex: none;
}
