:root {
  /* ── Surfaces ──────────────────────────────────────────────────────────
     The three surfaces and the border are plain values: nothing derives them
     and nothing needs to. */
  --bg: #0d0f10;
  --panel: #14171a;
  --panel-alt: #1a1e21;
  --border: #2a2e31;

  /* ── Foregrounds ───────────────────────────────────────────────────────
     Every colour that lands on top of a surface is declared TWICE: a `-base`
     that a theme authors by hand, and the token the rest of the file spends,
     which defaults to it.

     appearance.js reads the `-base` values, measures them against the three
     surfaces, and writes the spent token back onto <html> as an inline style —
     nudged along OKLCh lightness if and only if it missed its WCAG target.
     For a well-authored theme that is a no-op and the two are identical. The
     indirection is what makes "a user cannot configure an unreadable app" a
     property of the system rather than a promise about the palette.

     If JS never runs, the `-base` values are what you get, so the app is still
     themed and still legible on a dead script. */
  --text-base: #e6e2d6;
  --muted-base: #8f958c;
  --accent-base: #c99a4b;
  --danger-base: #e0796a;

  --text: var(--text-base);
  --muted: var(--muted-base);
  --accent: var(--accent-base);
  --danger: var(--danger-base);

  /* Derived accent tones. appearance.js overwrites all five; these fallbacks
     keep the file self-consistent without it. --accent-fill is the accent as a
     BLOCK (3:1, a UI boundary), --on-accent is what may be printed on top of
     that block (4.5:1 against it), --accent-subtle is a wash quiet enough that
     body text still clears 4.5:1 over it, --focus is the ring. */
  --accent-dim: color-mix(in srgb, var(--accent) 68%, var(--muted));
  --accent-fill: var(--accent);
  --accent-hover: var(--accent);
  --on-accent: #101010;
  --accent-subtle: color-mix(in srgb, var(--accent) 12%, var(--panel));
  --accent-wash-2: color-mix(in srgb, var(--accent) 22%, var(--panel));
  --focus: var(--accent);

  --mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;

  /* ── Design tokens ──────────────────────────────────────────────────────
     Added in the design pass. Everything below this line in the file, and
     everything in the DESIGN PASS section at the foot of it, spends these
     rather than inventing a number at the call site. That is the whole point
     of having them: a spacing scale you can deviate from silently is a
     spacing scale that stops meaning anything by the third feature. */

  /* Two type families, with a job each. Monospace is the notebook's own
     voice — your words, timestamps, code, safety numbers — and it stays. The
     UI stack is for chrome: labels, buttons, explanatory copy. Setting a
     paragraph of safety guidance in monospace looks like a terminal error;
     setting a note in a UI font makes it look like someone else's app. */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui,
             "Helvetica Neue", Arial, sans-serif;

  /* The font the NOTEBOOK is set in — your notes, the composer, message
     bubbles. Monospace is the default and the house voice, but a page of it is
     genuinely hard work for some people, so the Type setting repoints this one
     variable (see the [data-type] blocks). Nothing else moves: safety numbers,
     keys and code stay monospace whatever is chosen, because there the
     fixed width is carrying information. */
  --font-note: var(--mono);
  --font-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia,
                "Times New Roman", serif;
  /* No webfont is fetched for this — nothing in this app fetches anything. If
     the reader has installed OpenDyslexic or Atkinson Hyperlegible we use it;
     otherwise we fall back to the widest-apertured faces that ship with the
     major operating systems, and lean on the tracking below. */
  --font-dyslexic: "OpenDyslexic", "Atkinson Hyperlegible", "Lexie Readable",
                   "Comic Sans MS", Verdana, Tahoma, sans-serif;

  /* A 4px base, because every touch target and gap in this app is a multiple
     of it, and half-steps are where layouts start looking accidental.
     --sp-mult is the DENSITY control: one number, set on <html>, that moves
     the whole scale together. That is the difference between a density setting
     and thirty ad-hoc overrides — nothing can drift, because there is only one
     place to drift from. */
  --sp-mult: 1;
  --sp-1: calc(0.25rem * var(--sp-mult)); --sp-2: calc(0.5rem * var(--sp-mult));
  --sp-3: calc(0.75rem * var(--sp-mult)); --sp-4: calc(1rem * var(--sp-mult));
  --sp-5: calc(1.5rem * var(--sp-mult));  --sp-6: calc(2rem * var(--sp-mult));

  /* Line height travels with density, because spacing that grows while the
     text stays welded together is not "spacious", it is just a bigger box
     around the same cramped paragraph. */
  --lh-b: 1.55;        /* set by density */
  --lh-x: 1;           /* multiplied by the type setting */
  --lh: calc(var(--lh-b) * var(--lh-x));
  --lh-tight: calc(var(--lh-b) * 0.92 * var(--lh-x));

  /* Radii: a theme sets the `-b` bases (its own shape), the Corner setting
     sets the multiplier. Square is a multiplier of 0, which is why every
     radius in the file goes through these and not through a literal. */
  --r-mult: 1;
  --r-sm-b: 6px; --r-md-b: 10px; --r-lg-b: 14px; --r-pill-b: 999px;
  --r-sm: calc(var(--r-sm-b) * var(--r-mult));
  --r-md: calc(var(--r-md-b) * var(--r-mult));
  --r-lg: calc(var(--r-lg-b) * var(--r-mult));
  --r-pill: calc(var(--r-pill-b) * var(--r-mult));

  /* How wide a column of prose is allowed to get. A notebook maximised on a
     32" monitor should not set 200-character lines. */
  --content-max: 48rem;

  /* Motion. Two durations only. Anything that needs a third duration is
     usually a thing that should not be animating. */
  --dur-1: 90ms;    /* state: hover, press, focus */
  --dur-2: 170ms;   /* presence: something arriving or leaving */
  --ease: cubic-bezier(.22, .68, .28, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);

  /* Elevation, as one ramp, tinted per theme so a light theme does not get a
     black shadow (which is what makes light themes look cheap). */
  --shadow-1: 0 1px 2px rgba(0,0,0,.28);
  --shadow-2: 0 6px 20px rgba(0,0,0,.34);
  --shadow-3: 0 18px 50px rgba(0,0,0,.48);

  /* The smallest thing a thumb should have to hit. 44px is Apple's number and
     Android's is 48; 44 with generous padding around it clears both. */
  --tap: 44px;

  /* Safe areas, resolved once so no rule has to remember the env() dance. */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
}

/* iOS Safari zooms the page on focusing any input under 16px — this is
   the fix, applied once instead of chasing it per-input */
input, select, textarea {
  font-size: max(16px, 1rem);
}

/* Two more small iOS courtesies, same spirit as the 16px rule — fix the
   platform reflex without touching the user's own controls:
   - the grey flash iOS paints over anything tapped fights our own :active
     states; the app's feedback should be the app's, not the platform's.
   - Safari "helpfully" inflates text when the phone rotates to landscape,
     which re-wraps every note mid-read. `none` keeps type at the size the
     appearance settings chose. Neither rule affects pinch-zoom. */
* { -webkit-tap-highlight-color: transparent; }
html { -webkit-text-size-adjust: none; text-size-adjust: none; }

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--mono);
  overscroll-behavior: none;   /* stop pull-to-refresh bounce swallowing the app */
}

/* dynamic viewport height — accounts for mobile browser chrome
   (address bar) resizing instead of the stale 100% */
@supports (height: 100dvh) {
  html, body { height: 100dvh; }
}

body {
  display: flex;
  flex-direction: column;
}

button, input, select, a {
  touch-action: manipulation;   /* removes the double-tap-zoom delay on tappable elements */
}

/* ---------- top bar ---------- */
header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: calc(0.6rem + env(safe-area-inset-top)) 0.9rem 0.6rem;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  flex-wrap: nowrap;             /* one row, always — never wrap the ⋯ button off */
}

header .brand {
  color: var(--accent);
  letter-spacing: 0.06em;
  font-weight: 600;
  white-space: nowrap;           /* don't break the product name across two lines */
  flex-shrink: 0;
}

header .brand::before { content: "▤ "; }

.header-controls {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: nowrap;             /* keep search/⌘/messages/⋯ on one row */
  flex: 1 1 auto;
  min-width: 0;                  /* allow the row (and the select) to shrink */
  justify-content: flex-end;
}

.header-controls .icon-btn,
.header-controls .more-menu { flex-shrink: 0; }   /* icons keep their size; the select yields */

.header-controls button {
  font-size: 0.72rem;
  padding: 0.5rem 0.6rem;
  min-height: 38px;
}

/* unified icon buttons in the top bar */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 38px; min-height: 38px; padding: 0.4rem;
  font-size: 1.05rem; line-height: 1;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 9px;
  cursor: pointer; transition: border-color .15s ease, background .15s ease, transform .05s ease;
}
.icon-btn:hover { border-color: var(--accent-dim); background: var(--panel-alt); }
.icon-btn:active { transform: scale(0.94); }
#palette-btn { font-family: var(--mono); font-size: 0.95rem; }

.menu-sec {
  font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted); padding: 0.5rem 0.7rem 0.2rem; margin-top: 0.15rem;
}
.menu-sec:first-child { margin-top: 0; }
.menu-danger { color: var(--danger) !important; }

#mic-btn {
  padding: 0.4rem 0.55rem;
  font-size: 0.95rem;
}
#mic-btn.listening { border-color: var(--danger); color: var(--danger); }

.more-menu {
  position: relative;
}
.more-menu summary {
  list-style: none;
  cursor: pointer;
  background: var(--panel-alt);
  color: var(--accent);
  border: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  padding: 0.5rem 0.7rem;
  min-height: 38px;
  display: flex;
  align-items: center;
  border-radius: 2px;
}
.more-menu summary::-webkit-details-marker { display: none; }
.more-menu summary::after { content: ""; }
.more-menu[open] summary { border-color: var(--accent-dim); }
.more-menu-items {
  position: absolute;
  right: 0;
  top: calc(100% + 0.4rem);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  width: max-content;
  min-width: 190px;
  max-width: calc(100vw - 1.4rem);     /* never spill off the screen edge */
  max-height: calc(100vh - 5rem);      /* long menu scrolls instead of overflowing */
  max-height: calc(100dvh - 5rem);     /* ...measured against the REAL viewport on
                                          mobile: 100vh includes the space behind
                                          Safari's collapsing URL bar, which turned
                                          the menu's last items unreachable */
  overflow-y: auto;
  z-index: 40;
  box-shadow: 0 10px 34px rgba(0,0,0,.5);
}
.more-menu-items button {
  width: 100%; text-align: left; white-space: nowrap;
  background: transparent; border: 1px solid transparent; border-radius: 7px;
  padding: 0.5rem 0.6rem; color: var(--text);
}
.more-menu-items button:hover { background: var(--panel-alt); border-color: var(--border); }

/* ---------- chat stream ---------- */
main {
  flex: 1;
  overflow-y: auto;
  padding: 1rem 1.25rem 0.5rem;
  max-width: 760px;
  width: 100%;
  margin: 0 auto;
}

/* Rich-style bordered panel with title sitting on the top border line */
.panel {
  position: relative;
  border: 1px solid var(--border);
  border-radius: 2px;
  background: var(--panel);
  padding: 0.9rem 1.1rem 0.8rem;
  margin-top: 1rem;
  white-space: pre-wrap;
  line-height: 1.45;
  font-size: 0.92rem;
}
.panel-title {
  position: absolute;
  top: -0.68em;
  left: 0.9rem;
  background: var(--bg);
  padding: 0 0.5em;
  color: var(--accent);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  user-select: none;   /* selecting/copying panel text should never include the label */
}

.panel.recall {
  border-style: dashed;
  color: var(--muted);
  font-size: 0.8rem;
}
.panel.recall .panel-title { color: var(--muted); }

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

.panel a {
  color: var(--accent);
  text-decoration: underline;
}
.panel a:hover { color: var(--accent-dim); }

.entry-btn {
  float: right;
  background: transparent;
  border: none;
  font-size: 0.95rem;
  padding: 0.35rem 0.5rem;
  margin: -0.35rem -0.1rem;
  min-width: 36px;
  min-height: 36px;
  opacity: 0.55;
  cursor: pointer;
}
.entry-btn:hover { opacity: 1; border-color: transparent; }
.delete-btn:hover { color: var(--danger); }

/* ---------- input bar ---------- */
footer {
  border-top: 1px solid var(--border);
  padding: 0.85rem 1.25rem calc(1.1rem + env(safe-area-inset-bottom));
  flex-shrink: 0;
}

form#composer {
  max-width: 760px;
  margin: 0 auto;
  display: flex;
  gap: 0.6rem;
  align-items: center;
}

form#composer .prompt-glyph { color: var(--accent-dim); }

#msg-input {
  flex: 1 1 auto;
  min-width: 0;              /* let the field shrink instead of shoving Send off-screen */
  background: transparent;
  border: none;
  color: var(--text);
  font-family: var(--mono);
  font-size: max(16px, 0.95rem);
  outline: none;
  caret-color: var(--accent);
}

/* keep the action controls fixed-width so they never get pushed off the
   viewport on a narrow phone — only the text field flexes */
form#composer .prompt-glyph,
form#composer #note-btn,
form#composer #mic-btn,
form#composer button[type="submit"] { flex: 0 0 auto; }
#note-btn { padding: 0.4rem 0.5rem; font-size: 0.95rem; }

#msg-input::placeholder { color: var(--muted); }

button {
  background: var(--panel-alt);
  color: var(--accent);
  border: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 0.8rem;
  letter-spacing: 0.05em;
  padding: 0.45rem 0.9rem;
  min-height: 38px;
  border-radius: 2px;
  cursor: pointer;
}
button:hover { border-color: var(--accent-dim); }
button:disabled { opacity: 0.4; cursor: default; }

/* ---------- gate screen (passphrase) ---------- */
#gate {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
#gate .panel { width: min(320px, 90vw); text-align: left; }
#gate input {
  width: 100%;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: var(--mono);
  padding: 0.5rem 0.6rem;
  margin: 0.7rem 0 0.9rem;
  border-radius: 2px;
}
#gate p { color: var(--muted); font-size: 0.8rem; margin: 0; }

.hidden { display: none !important; }

/* ---------- modal layers ----------
   z-index here is only the FLOOR. The real value is written by the modal stack
   in app.js (MODAL_Z_BASE + position), because a modal opened from another
   modal has to paint above its opener, and a single shared number made that
   depend on the order the elements happen to appear in index.html — which is
   how "share my invite", opened from Messages, ended up underneath it.
   Everything here stays far below #privacy-veil (9999). */
.modal {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: var(--z-modal, 400);
}
.modal-panel {
  width: min(440px, 92vw);
  max-height: 80vh;
  overflow-y: auto;
  background: var(--panel);
  white-space: normal;   /* the panel base uses pre-wrap; structured modal markup must not */
}
.modal-close {
  position: absolute;
  top: 0.2rem;
  right: 0.2rem;
  border: none;
  background: transparent;
  font-size: 1.2rem;
  min-width: 40px;
  min-height: 40px;
  padding: 0;
}
.groups-modal .row,
#groups-modal .row {
  display: flex;
  gap: 0.4rem;
  margin: 0.7rem 0;
  flex-wrap: wrap;
}
#groups-select {
  flex: 1;
  background: var(--panel-alt);
  color: var(--text);
  border: 1px solid var(--border);
  font-family: var(--mono);
  font-size: max(16px, 0.85rem);
  padding: 0.35rem 0.5rem;
  border-radius: 2px;
}
.member-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}
.member-row .member-name { color: var(--accent); min-width: 90px; }
.member-row .member-hash {
  color: var(--muted);
  font-size: 0.72rem;
  flex: 1;
  word-break: break-all;
}
.member-row button { font-size: 0.7rem; padding: 0.4rem 0.6rem; }
.add-member-row input {
  flex: 1;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: var(--mono);
  font-size: max(16px, 0.82rem);
  padding: 0.4rem 0.5rem;
  border-radius: 2px;
  min-width: 100px;
}
#groups-modal .hint { color: var(--muted); font-size: 0.72rem; margin-top: 0.6rem; }

@media (prefers-reduced-motion: no-preference) {
  .cursor-blink { animation: blink 1.1s steps(1) infinite; }
}
@keyframes blink { 50% { opacity: 0; } }

@media (max-width: 600px) {
  main, form#composer { padding-left: 0.9rem; padding-right: 0.9rem; }
  .panel { font-size: 0.88rem; }
  footer { padding-left: 0.9rem; padding-right: 0.9rem; }
  form#composer { gap: 0.4rem; }
  form#composer button { padding: 0.45rem 0.6rem; }   /* slimmer so Save always fits */
}

/* ── Batch 1 additions: busy indicator, diary hints, gate import ───────── */

/* Animated processing indicator — shown during any async op */
#busy {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 4px 10px;
  font-size: 0.8rem;
  color: var(--gold, #e5b567);
  opacity: 0.85;
}
#busy.hidden { display: none; }
.busy-dots::after {
  content: "";
  animation: busydots 1.2s steps(1, end) infinite;
}
@keyframes busydots {
  0%   { content: ""; }
  25%  { content: "."; }
  50%  { content: ".."; }
  75%  { content: "..."; }
  100% { content: ""; }
}

/* Gate: import-a-key-file affordance */
.gate-import { margin-top: 10px; }
.linkish {
  background: none;
  border: none;
  color: var(--gold, #e5b567);
  text-decoration: underline;
  cursor: pointer;
  font-size: 0.8rem;
  padding: 0;
}
.linkish:hover { opacity: 0.8; }

/* Rendered rule from a markdown --- (instead of literal dashes) */
.panel hr {
  border: none;
  border-top: 1px solid var(--dim, #444);
  margin: 8px 0;
}
.panel code {
  background: rgba(255,255,255,0.08);
  padding: 1px 4px;
  border-radius: 3px;
  font-size: 0.9em;
}

/* ── Composer redesign: focus-to-expand, woops-back, hidden context ──────── */

#app { position: relative; }
main { transition: opacity 0.2s ease, filter 0.2s ease; }
footer { transition: box-shadow 0.2s ease; }
form#composer { transition: transform 0.22s cubic-bezier(.34,1.56,.64,1); }
#msg-input { transition: min-height 0.18s ease, font-size 0.18s ease; }

/* focused compose surface: dim the backlog, lift + enlarge the input bar */
#app.composing main { opacity: 0.28; filter: saturate(0.6); pointer-events: none; }
#app.composing footer { box-shadow: 0 -12px 40px rgba(0,0,0,0.6); }
#app.composing form#composer { transform: translateY(-4px); }  /* no scale: it overflowed and clipped Send */
#app.composing #msg-input { min-height: 2.4rem; font-size: max(16px, 1.06rem); }

/* ── Settings modal: segmented controls + toggles ────────────────────────── */
.setting-block { margin: 0.9rem 0; }
.setting-block + .setting-block { border-top: 1px solid var(--border); padding-top: 0.9rem; }
.setting-label {
  color: var(--accent);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 0.5rem;
}
.segmented { display: flex; gap: 0; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
.segmented button {
  flex: 1 1 0;
  border: none;
  border-right: 1px solid var(--border);
  border-radius: 0;
  background: var(--panel-alt);
  color: var(--muted);
  padding: 0.5rem 0.4rem;
  font-size: 0.72rem;
}
.segmented button:last-child { border-right: none; }
.segmented button.active { background: var(--accent); color: var(--bg); }
.segmented button:hover { color: var(--text); }
.setting-row {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  margin: 0.55rem 0;
  color: var(--text);
  font-size: 0.85rem;
  cursor: pointer;
}
.setting-row input[type="checkbox"] { margin-top: 0.15rem; flex: 0 0 auto; }
.setting-block .hint { color: var(--muted); font-size: 0.72rem; margin: 0.4rem 0 0; }

.mini-btn { padding: 0.3rem 0.6rem; font-size: 0.7rem; min-height: 0; flex: 0 0 auto; }

/* modal titles in-flow so the notch label isn't clipped by the panel's scroll */
.modal-panel { padding-top: 1.1rem; }
.modal-panel > .panel-title {
  position: static;
  display: block;
  background: transparent;
  padding: 0;
  margin: 0 0 0.8rem;
}

/* ═══════════════ v2: themes, palette, search, sync, reply actions ═══════════ */

/* --- Themes: override the SURFACES and the `-base` foregrounds per
       <html data-theme>. A theme never sets --accent/--text/--muted directly:
       those are the tokens appearance.js writes after it has checked them.
       The shape and texture of each theme lives further down the file. --- */
[data-theme="midnight"] {
  --bg:#0b1020; --panel:#131b30; --panel-alt:#1a2440; --border:#26324e;
  --text-base:#dce6f5; --muted-base:#93a5c4; --accent-base:#6ea8ff; --danger-base:#f0857c;
}
[data-theme="matrix"] {
  --bg:#061109; --panel:#0b1a11; --panel-alt:#0f2417; --border:#153322;
  --text-base:#b9f5c6; --muted-base:#71a880; --accent-base:#4ee66a; --danger-base:#ef8377;
}
[data-theme="parchment"] {
  --bg:#efe7d3; --panel:#f7f1e2; --panel-alt:#eae0c8; --border:#d3c4a0;
  --text-base:#2e2a20; --muted-base:#6b6148; --accent-base:#7f591b; --danger-base:#9c3f2c;
}
[data-theme="rose"] {
  --bg:#150f1b; --panel:#1f1729; --panel-alt:#291d35; --border:#3d2e4a;
  --text-base:#efe1ee; --muted-base:#bda2c0; --accent-base:#e8a3c0; --danger-base:#f08a80;
}
/* DAYLIGHT — the plain light theme. Parchment is a mood; this is a lit room.
   Neutral greys, a white card on an off-white page, grey-not-black shadows. */
[data-theme="daylight"] {
  --bg:#f5f6f8; --panel:#ffffff; --panel-alt:#eceef2; --border:#d3d8e0;
  --text-base:#1b1f24; --muted-base:#5c6470; --accent-base:#2a63cc; --danger-base:#b3261e;
}
/* HIGH CONTRAST (dark) — black page, white text: 21:1, comfortably past the
   AAA bar of 7:1. No gradients, no washes, no soft shadows; every boundary is
   a hard 2px line, because a low-vision user navigating by edges cannot use an
   edge made of a 12%-opacity glow. */
[data-theme="contrast"] {
  --bg:#000000; --panel:#000000; --panel-alt:#101010; --border:#ffffff;
  --text-base:#ffffff; --muted-base:#d8d8d8; --accent-base:#ffd447; --danger-base:#ff8f7a;
}
/* HIGH CONTRAST (light) — the same contract with the polarity flipped, which
   matters: a lot of people with low vision find light-on-dark harder, not
   easier, and offering only the dark one is offering half an option. */
[data-theme="contrast-light"] {
  --bg:#ffffff; --panel:#ffffff; --panel-alt:#f0f0f0; --border:#000000;
  --text-base:#000000; --muted-base:#333333; --accent-base:#0b4bbf; --danger-base:#a3000f;
}
html, body, header, footer, main, .panel, .modal-panel, .palette-card { transition: background-color .25s ease, color .25s ease, border-color .25s ease; }

/* theme picker swatches */
.theme-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:.5rem; }
.theme-swatch {
  display:flex; align-items:center; gap:.5rem; justify-content:flex-start;
  background:var(--panel-alt); border:1px solid var(--border); border-radius:4px;
  padding:.5rem .6rem; color:var(--text); font-size:.78rem; text-align:left;
}
.theme-swatch.active { border-color:var(--accent); }
.theme-swatch .sw { width:1.1rem; height:1.1rem; border-radius:50%; flex:0 0 auto; border:1px solid var(--border); }
.sw[data-theme="amber"]     { background:linear-gradient(135deg,#0d0f10 50%,#c99a4b 50%); }
.sw[data-theme="midnight"]  { background:linear-gradient(135deg,#0b1020 50%,#6ea8ff 50%); }
.sw[data-theme="matrix"]    { background:linear-gradient(135deg,#061109 50%,#4ee66a 50%); }
.sw[data-theme="parchment"] { background:linear-gradient(135deg,#efe7d3 50%,#7f591b 50%); }
.sw[data-theme="rose"]      { background:linear-gradient(135deg,#150f1b 50%,#e8a3c0 50%); }
.sw[data-theme="daylight"]  { background:linear-gradient(135deg,#ffffff 50%,#2a63cc 50%); }
.sw[data-theme="contrast"]  { background:linear-gradient(135deg,#000000 50%,#ffd447 50%); }
.sw[data-theme="contrast-light"] { background:linear-gradient(135deg,#ffffff 50%,#0b4bbf 50%); }

/* header icon buttons */
#search-btn, #palette-btn { font-size:.8rem; padding:.5rem .55rem; min-height:38px; }
#palette-btn { letter-spacing:.02em; }

/* --- Command palette + search overlays --- */
.overlay {
  position:fixed; inset:0; z-index: var(--z-modal, 400); background:rgba(0,0,0,.55);
  display:flex; align-items:flex-start; justify-content:center; padding:8vh 1rem 1rem;
}
.overlay.hidden { display:none; }
.palette-card {
  width:min(560px,94vw); background:var(--panel); border:1px solid var(--accent-dim);
  border-radius:8px; box-shadow:0 24px 70px rgba(0,0,0,.6); overflow:hidden;
  animation:overlayIn .16s ease;
}
@keyframes overlayIn {
  from { opacity: 0; transform: scale(0.96) translateY(6px); }
  to   { opacity: 1; transform: none; }
}
#palette-input, #search-input {
  width:100%; background:transparent; border:none; border-bottom:1px solid var(--border);
  color:var(--text); font-family:var(--mono); font-size:max(16px,1rem); padding:1rem 1.1rem; outline:none;
}
.palette-list { max-height:52vh; overflow-y:auto; }
.palette-row {
  padding:.6rem 1.1rem; cursor:pointer; font-size:.85rem; color:var(--text);
  border-left:2px solid transparent; display:flex; flex-direction:column; gap:.15rem;
}
.palette-row:hover, .palette-row.sel { background:var(--panel-alt); border-left-color:var(--accent); }
.palette-empty { padding:1rem 1.1rem; color:var(--muted); font-size:.8rem; }
.sr-meta { color:var(--accent-dim); font-size:.68rem; text-transform:uppercase; letter-spacing:.05em; }
.sr-text { color:var(--text); }

/* --- quiet secondary buttons (messages, invite) --- */
.ghost-btn { background:transparent; border:1px solid transparent; color:var(--muted); padding:.4rem .5rem; }
.ghost-btn:hover { color:var(--text); border-color:var(--border); }

/* --- cloud sync --- */
#set-sync-pass { width:100%; background:var(--panel-alt); color:var(--text);
  border:1px solid var(--border); border-radius:3px; padding:.5rem; margin-top:.5rem; }
#sync-status { color:var(--muted); }

/* --- jump-to-message flash --- */
.flash { animation:flashHl 1.5s ease; }
@keyframes flashHl {
  0%,100% { box-shadow:0 0 0 0 transparent; }
  20% { box-shadow:0 0 0 3px var(--accent); }
}

/* a note in the feed — the ordinary case now that the feed IS your notes */
.panel.note {
  border-style: dashed;
  border-color: var(--accent-dim);
  background: var(--accent-subtle);
}
.panel.note .panel-title { color: var(--accent); }
.panel.shared-note .panel-title { color: var(--muted); }
#note-text {
  width: 100%;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: var(--mono);
  font-size: max(16px, 0.95rem);
  border-radius: 3px;
  padding: 0.6rem;
  line-height: 1.4;
  resize: vertical;
}

/* ── Accessibility + delights ──────────────────────────────────────────── */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}

/* empty-state greeting */
.empty-state {
  max-width: 30ch; margin: 18vh auto 0; text-align: center; color: var(--muted); line-height: 1.6;
}
.empty-state .empty-glyph { font-size: 2.6rem; color: var(--accent-dim); margin-bottom: 0.6rem; }
.empty-state b { color: var(--accent); }

/* insights */
.stat-row {
  display: flex; justify-content: space-between; align-items: baseline;
  border-bottom: 1px solid var(--border); padding: 0.55rem 0; font-size: 0.9rem;
}
.stat-row b { color: var(--accent); font-size: 1.05rem; }
.bar-row { display: flex; align-items: center; gap: 0.6rem; margin: 0.45rem 0; font-size: 0.82rem; }
.bar-label { flex: 0 0 8.5rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar { flex: 1 1 auto; height: 0.7rem; background: var(--panel-alt); border-radius: 4px; overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--accent); }
.bar-n { flex: 0 0 auto; color: var(--muted); width: 2rem; text-align: right; }

/* ── mic on/off states + direct-message inputs ─────────────────────────── */
#mic-btn { position: relative; opacity: 0.7; }              /* idle = muted look */
#mic-btn::after {                                            /* the "/" slash when off */
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 1.35em; height: 2px; background: currentColor; opacity: 0.85;
  transform: translate(-50%, -50%) rotate(-45deg); border-radius: 2px; pointer-events: none;
}
#mic-btn.listening { opacity: 1; color: var(--danger); border-color: var(--danger); animation: micPulse 1s ease-in-out infinite; }
#mic-btn.listening::after { display: none; }                /* remove slash while active */
@keyframes micPulse { 50% { opacity: 0.55; } }

#dm-to, #dm-text {
  width: 100%; background: var(--panel-alt); border: 1px solid var(--border);
  color: var(--text); font-family: var(--mono); font-size: max(16px, 0.9rem);
  border-radius: 3px; padding: 0.5rem; margin-top: 0.5rem;
}
#dm-text { resize: vertical; line-height: 1.4; }
#dm-status { color: var(--muted); }

/* ---- toast: transient status (read-receipts, invite copied, decrypt notes) ---- */
#toast-host {
  /* Above the composer, not on it: at bottom:1.1rem every toast sat exactly
     over the Save button — pointer-events:none meant taps went through, but a
     control you cannot READ while a confirmation shows is still a blocked
     control. 5.4rem clears the footer at every density. */
  position: fixed; left: 50%; bottom: calc(5.4rem + var(--safe-b, 0px)); transform: translateX(-50%) translateY(1.5rem);
  /* Above the whole modal stack (400+), because a toast is fired from inside
     modals — "Sent.", "Copied", "Marked verified" — and a confirmation you
     cannot see is a confirmation that did not happen. Still far below
     #privacy-veil at 9999: nothing outranks the veil. */
  max-width: min(92vw, 30rem); z-index: 9000;
  background: var(--panel, #1b1b1f); color: var(--text, #eee);
  border: 1px solid var(--border, #3a3a42); border-radius: .7rem;
  padding: .7rem .95rem; font-size: .92rem; line-height: 1.35;
  box-shadow: 0 8px 30px rgba(0,0,0,.45);
  opacity: 0; pointer-events: none; transition: opacity .2s ease, transform .2s ease;
}
#toast-host.show { opacity: 1; transform: translateX(-50%) translateY(0); }
@media (prefers-reduced-motion: reduce) { #toast-host { transition: opacity .2s ease; transform: translateX(-50%); } }

/* ---- Messages / inbox ---- */
.has-badge { position: relative; }
.msg-badge {
  display: inline-block; min-width: 1.1rem; height: 1.1rem; padding: 0 .3rem;
  margin-left: .35rem; border-radius: .8rem; background: var(--danger, #e5484d);
  color: #fff; font-size: .72rem; line-height: 1.1rem; text-align: center; vertical-align: middle;
}
/* corner badge when it sits on the header Messages icon */
.icon-btn.has-badge .msg-badge {
  position: absolute; top: -5px; right: -5px; margin: 0;
  box-shadow: 0 0 0 2px var(--bg);
}
.messages-panel { display: flex; flex-direction: column; max-height: 80vh; width: min(94vw, 34rem); }
.messages-head { display: flex; align-items: center; gap: .5rem; }
.messages-head .panel-title { flex: 1; }
.msg-threads { overflow-y: auto; margin-top: .5rem; }
.thread-row {
  display: flex; flex-direction: row; align-items: center; gap: .6rem; width: 100%;
  text-align: left; padding: .65rem .5rem; border: none; border-bottom: 1px solid var(--border);
  background: transparent; color: var(--text); cursor: pointer; border-radius: 8px;
}
.thread-row:hover { background: var(--panel-alt); }
.thread-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.thread-top { display: flex; align-items: baseline; gap: .5rem; }
.thread-top b { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thread-when { color: var(--muted); font-size: .7rem; flex-shrink: 0; }
.thread-snip { color: var(--muted); font-size: .82rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* contact avatar chips */
.avatar-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex-shrink: 0; border-radius: 50%;
  border: 1px solid var(--border); color: #fff; font-size: .78rem; font-weight: 600;
  letter-spacing: .02em;
}
.msg-thread { display: flex; flex-direction: column; min-height: 0; flex: 1; }
.msg-thread-head { display: flex; align-items: center; gap: .5rem; padding: .4rem 0 .6rem; border-bottom: 1px solid var(--border); }
#msg-thread-name { flex: 1; display: flex; align-items: center; gap: .5rem; min-width: 0; }
#msg-thread-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-thread-body { flex: 1; overflow-y: auto; padding: .8rem .2rem; display: flex; flex-direction: column; gap: .45rem; min-height: 6rem; }
.bubble { max-width: 82%; padding: .5rem .7rem; border-radius: 14px; font-size: .93rem; line-height: 1.38; word-wrap: break-word; display: flex; flex-direction: column; gap: .15rem; }
.bubble.in { align-self: flex-start; background: var(--panel-alt); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
.bubble.out { align-self: flex-end; background: var(--accent-dim); color: #14100a; border-bottom-right-radius: 4px; }
.bubble-meta { font-size: .64rem; opacity: .75; align-self: flex-end; white-space: nowrap; }
.bubble.in .bubble-meta { color: var(--muted); }
.bubble .tick { margin-left: .3rem; }
.msg-reply-row { display: flex; gap: .4rem; margin-top: .5rem; }
.msg-reply-row input { flex: 1; min-width: 0; }

/* QR / invite */
.qr-box {
  display: flex; align-items: center; justify-content: center;
  background: #e6e2d6; border-radius: 12px; padding: 14px; margin: .4rem auto .7rem;
  width: fit-content; min-height: 180px; min-width: 180px;
}
.qr-box img, .qr-box canvas { display: block; }
#invite-url {
  width: 100%; background: var(--panel-alt); border: 1px solid var(--border); color: var(--muted);
  border-radius: 6px; padding: .5rem .6rem; font-size: .78rem; font-family: var(--mono);
}

/* ═══════════════ Total recall: grouped results, chips, insights ═══════════ */

/* Day headers sit between result rows so a long list reads as a timeline
   rather than a wall. Sticky, because scrolling a month of results without
   knowing which day you're in is the thing that makes search feel like work. */
.recall-day {
  position: sticky; top: 0; z-index: 1;
  padding: .5rem 1.1rem .3rem;
  font-size: .66rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted); background: var(--panel);
  border-bottom: 1px solid var(--border);
}
.recall-row .sr-text { line-height: 1.45; }
.recall-row mark {
  background: color-mix(in srgb, var(--accent) 34%, transparent);
  color: inherit; border-radius: 2px; padding: 0 .1em;
}
/* Parchment is a light theme: the same 34% tint reads as a smear there, so the
   highlight goes darker and thinner instead of brighter. */
[data-theme="parchment"] .recall-row mark {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
}

/* The active filters, echoed back. Recall parses plain language, so it has to
   show its work — otherwise a query that silently matched nothing looks like
   an empty notebook rather than a misread date. */
.recall-chips { display: flex; flex-wrap: wrap; gap: .35rem; padding: .7rem 1.1rem .2rem; }
.recall-chip {
  font-size: .68rem; letter-spacing: .04em;
  border: 1px solid var(--accent-dim); color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border-radius: 999px; padding: .12rem .55rem;
  /* A chip echoes back whatever the parser understood, and one of those values
     can be an address. Capped and ellipsised so a long one is a chip rather
     than a line that pushes every other chip off the row. */
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Insights, in the place you'll actually see them: the empty state of search. */
.recall-stats {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem;
  padding: 1rem 1.1rem .6rem;
}
.recall-stat {
  display: flex; flex-direction: column; gap: .1rem;
  background: var(--panel-alt); border: 1px solid var(--border);
  border-radius: 8px; padding: .55rem .6rem; min-width: 0;
}
.recall-stat.wide { grid-column: span 3; flex-direction: row; align-items: baseline; justify-content: space-between; gap: .5rem; }
.recall-stat b { color: var(--accent); font-size: 1.15rem; line-height: 1.1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recall-stat.wide b { font-size: .92rem; }
.recall-stat span { color: var(--muted); font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; }

/* "27 results · best match first" — the same sticky header a timeline uses, so
   the two orderings read as one feature. A ranked list that does not say it is
   ranked reads as a broken chronological one. */
.recall-ranked-head { color: var(--accent); }

/* "More like this", inside a result row. Sits under the snippet and stops the
   row's own click, so it is a second action rather than a trap. */
.sr-related {
  align-self: flex-start; margin-top: .35rem;
  background: transparent; border: 1px dashed var(--border); color: var(--muted);
  border-radius: 6px; padding: .1rem .45rem; min-height: 0; font-size: .68rem;
}
.sr-related:hover { border-style: solid; border-color: var(--accent); color: var(--accent); }

/* THE ZERO-RESULTS STATE. It used to be the words "no matches" and nothing
   else; it is now a diagnosis, so it needs room to be one. */
.recall-none { padding: 1rem 1.1rem 1.2rem; }
.recall-none-lead { margin: 0 0 .5rem; color: var(--text); font-size: .92rem; }
.recall-none .hint { margin: .35rem 0; }
.recall-relax { display: flex; flex-direction: column; gap: .4rem; margin-top: .5rem; }
.recall-relax-btn {
  text-align: left; width: 100%;
  background: var(--panel-alt); color: var(--accent);
  border: 1px solid var(--border); border-radius: 8px;
  padding: .5rem .7rem; min-height: var(--tap); font-size: .78rem;
}
.recall-relax-btn:hover { border-color: var(--accent); }

.recall-help { padding: .2rem 1.1rem 1rem; color: var(--muted); font-size: .74rem; line-height: 2; }
.recall-help div { margin-bottom: .4rem; line-height: 1.5; }
.recall-help code {
  background: var(--panel-alt); border: 1px solid var(--border);
  border-radius: 4px; padding: .12rem .4rem; margin-right: .25rem;
  font-size: .72rem; color: var(--text);
}

/* ═══════════════ Link cards ══════════════════════════════════════════════ */

/* Every card shares one shell so a video, an article and a bare link read as
   the same object at a glance. Colours come only from theme variables — there
   is no hardcoded white card here, because two of the five themes are light
   and three are dark and a card must look deliberate in all of them. */
.link-card {
  display: block; margin: .6rem 0 .1rem;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--panel-alt); overflow: hidden;
  text-decoration: none; color: var(--text);
  box-shadow: 0 2px 10px rgba(0,0,0,.18);
}
[data-theme="parchment"] .link-card { box-shadow: 0 2px 10px rgba(90,70,30,.12); }
.link-card .card-body { padding: .65rem .8rem .75rem; display: flex; flex-direction: column; gap: .3rem; }
.link-card .card-meta {
  color: var(--muted); font-size: .66rem; letter-spacing: .09em; text-transform: uppercase;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.link-card .card-title {
  color: var(--text); font-size: .92rem; font-weight: 600; line-height: 1.32;
  white-space: normal;
}
.link-card .card-excerpt {
  margin: .1rem 0 0; color: var(--muted); font-size: .8rem; line-height: 1.5;
  white-space: normal;
}
.link-card .card-link {
  align-self: flex-start; margin-top: .15rem;
  color: var(--accent); font-size: .74rem; text-decoration: none;
  border-bottom: 1px solid var(--accent-dim); padding-bottom: 1px;
}
.link-card .card-link:hover { color: var(--text); border-bottom-color: var(--text); }

.card-article .card-image {
  display: block; width: 100%; max-height: 190px; object-fit: cover;
  background: var(--panel); border-bottom: 1px solid var(--border);
}

/* The facade. A button, not a div, so it is reachable by keyboard and
   announced as something you can activate — the whole privacy story depends
   on the tap being a real, deliberate act. */
.card-video .card-facade {
  position: relative; display: block; width: 100%; padding: 0; margin: 0;
  border: none; border-radius: 0; background: #000; cursor: pointer;
  min-height: 0; line-height: 0; overflow: hidden;
}
.card-video .card-facade.no-thumb { min-height: 120px; background: var(--panel); }
.card-video .card-facade img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.card-video .card-play {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 3.2rem; height: 3.2rem; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,.62); border: 1px solid rgba(255,255,255,.35);
  color: #fff; font-size: 1.2rem; line-height: 1; padding-left: .18rem;
  transition: transform .12s ease, background .12s ease;
}
.card-video .card-facade:hover .card-play { background: var(--accent); color: var(--bg); transform: translate(-50%,-50%) scale(1.06); }
.card-video .card-frame { display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; background: #000; }

/* A degraded card (ok:false) is just a link, and should look like one — not
   like a broken card. */
.card-plain {
  padding: .55rem .8rem; font-size: .82rem; color: var(--accent);
  word-break: break-all; white-space: normal;
}
.card-plain:hover { border-color: var(--accent-dim); }

/* Cards inside a chat bubble: edge to edge, and never wider than the bubble. */
.bubble .link-card { margin: .4rem -.2rem .15rem; max-width: 100%; }
.bubble.out .link-card { background: color-mix(in srgb, var(--panel) 88%, var(--accent) 12%); color: var(--text); }
.bubble.out .link-card .card-title { color: var(--text); }
.card-preview:not(:empty) { margin-top: .6rem; }
.card-preview .link-card { margin-top: 0; }

/* ═══════════════ Invite codes ════════════════════════════════════════════ */
.gate-invite { margin: .9rem 0 .2rem; }
.gate-invite label { display: block; color: var(--accent); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; }
#gate-invite-code { text-transform: uppercase; letter-spacing: .12em; text-align: center; }
#gate-invite-status { color: var(--muted); font-size: .75rem; margin: .3rem 0 0; }
.gate-or { border: none; border-top: 1px solid var(--border); margin: 1rem 0 .7rem; }

.invite-list { margin-top: .5rem; }
.invite-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: .4rem;
  padding: .5rem 0; border-bottom: 1px solid var(--border); font-size: .82rem;
}
.invite-row .invite-code { color: var(--accent); letter-spacing: .1em; font-size: .84rem; background: none; }
.invite-row .invite-state { color: var(--muted); font-size: .68rem; text-transform: uppercase; letter-spacing: .08em; flex: 1; }
.invite-row.used .invite-code { color: var(--muted); text-decoration: line-through; }


/* --- disappearing messages -------------------------------------------------
   The timer is a header control, not a menu item, because it is state: if
   this conversation deletes itself, that fact should be legible without
   opening anything. The system line is centred and quiet so a timer change
   reads as an event in the conversation rather than as somebody's message. */
.msg-timer {
  padding: 0.25rem 0.4rem;
  font-size: 0.8rem;
  max-width: 8.5rem;
}
.msg-timer-note {
  margin: 0.2rem 0.6rem 0;
  font-size: 0.75rem;
  opacity: 0.8;
}
.msg-system {
  align-self: center;
  margin: 0.35rem auto;
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  font-size: 0.75rem;
  text-align: center;
  opacity: 0.75;
  background: rgba(127, 127, 127, 0.14);
}

/* ---- privacy screen ------------------------------------------------------
   Opaque, fixed, above everything including modals, and painted with the
   theme's own background so it reads as "the app, idle" rather than "the app,
   crashed". No transition on the way IN: the OS grabs its task-switcher
   thumbnail within a frame or two of the page being hidden, and any fade at
   all is a fade the screenshot can catch halfway. Coming back is instant too,
   because a privacy screen you have to wait out is one people turn off. */
#privacy-veil {
  position: fixed; inset: 0; z-index: 9999;
  display: none; flex-direction: column; align-items: center; justify-content: center;
  gap: .6rem; background: var(--bg); color: var(--accent-dim);
}
html.privacy-screen #privacy-veil { display: flex; }
#privacy-veil .veil-mark { font-size: 1.4rem; letter-spacing: .18em; text-transform: uppercase; }
#privacy-veil .veil-hint { color: var(--muted); font-size: .74rem; }

/* ---- contact verification ------------------------------------------------
   Quiet by default (a small tick that does not compete with the name), loud
   when broken — that asymmetry is the point: "verified" is reassurance and
   should sit still, "verification broken" is the one thing in this app that
   has to interrupt. */
.verify-badge {
  font-size: .62rem; letter-spacing: .06em; text-transform: uppercase;
  padding: .05rem .3rem; border-radius: 4px; white-space: nowrap; flex-shrink: 0;
}
.verify-badge.ok { color: var(--accent-dim); border: 1px solid var(--accent-dim); opacity: .85; }
.verify-badge.broken { color: var(--danger); border: 1px solid var(--danger); opacity: 1; }
.thread-top .verify-badge { margin-left: .35rem; }
.verify-warning {
  margin: .5rem 0 0; padding: .55rem .7rem; border-radius: 6px;
  border: 1px solid var(--danger); color: var(--danger);
  font-size: .78rem; line-height: 1.45;
}
.verify-row { margin-top: .7rem; padding-top: .7rem; border-top: 1px solid var(--border); }
.verify-row .hint { margin: .4rem 0 0; }
/* The safety number itself: monospace and wide-tracked, because 60 digits read
   aloud only works if the groups are visually separate. */
#invite-url { font-variant-numeric: tabular-nums; }

/* ---- auto-lock / duress settings ---------------------------------------- */
#set-lock-timeout, #set-duress-pass {
  background: var(--panel-alt); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px; padding: .45rem .5rem;
}
#set-duress-pass { flex: 1; min-width: 8rem; }

/* ===========================================================================
   ============================= DESIGN PASS ================================

   Everything below this line is the interface pass. It sits at the end of the
   file deliberately: it refines what came before rather than rewriting it, so
   the diff is readable and so nothing that was carefully reasoned about
   earlier (the privacy veil, the reduced-motion contract, the focus rings)
   gets quietly lost in a rewrite.

   THE THREE RULES IT FOLLOWS

   1. Hierarchy comes from SIZE, WEIGHT AND SPACE before it comes from colour.
      This app has five themes and two of them are light; a design that leans
      on colour to say "this is the important one" falls apart in at least one
      of them. Space is theme-independent.

   2. Motion is state feedback, never decoration. Two durations, both under
      200ms, and every one of them collapses to nothing under
      prefers-reduced-motion — which is enforced by the blanket rule further
      up this file, kept and now strengthened rather than replaced.

   3. THE PRIVACY VEIL IS NOT PART OF THIS PASS. Its opacity and its
      no-transition-inbound behaviour are a security control (the OS
      task-switcher screenshot), not styling. Nothing here touches
      #privacy-veil, and nothing here may introduce a global transition that
      would give it one by accident — which is exactly why the transition
      rules below name their properties instead of using `transition: all`.
=========================================================================== */

/* ---- typography ----------------------------------------------------------
   The notebook speaks in monospace; the app speaks in the system font. The
   split is what stops the whole thing reading as a terminal emulator, while
   keeping the one texture that actually belongs to this product. */
body { font-family: var(--font-ui); -webkit-font-smoothing: antialiased; }

/* Your words, in the notebook's own voice. */
.panel.note, .panel.note .panel-title, .bubble-text, .capsule-open-text,
.sp-text, #note-text, #capsule-text, #msg-input, #dm-text,
header .brand, code, pre, .invite-code, #invite-url {
  font-family: var(--mono);
}

/* Chrome, in the system voice, at a size that is meant to be read rather than
   squinted at. The old UI ran at 0.72rem in a lot of places, which is fine for
   a label and hostile for a sentence. */
button, input, select, textarea, .hint, .setting-row, .menu-sec,
.more-menu-items button, .palette-row, .cal-item, .tag-row {
  font-family: var(--font-ui);
}
.hint { font-size: 0.8rem; line-height: 1.55; letter-spacing: 0.005em; }
p { line-height: 1.55; }

/* Section labels: small, wide, quiet. They are signposts, not headings, and
   the only place tracking this loose is justified. */
.setting-label, .menu-sec, .panel-title, .sr-meta, .cal-dow,
.recall-day, .recall-stat span {
  font-family: var(--font-ui);
  letter-spacing: 0.1em;
  font-weight: 600;
}
.panel.note .panel-title { letter-spacing: 0.08em; font-weight: 500; }

/* ---- spacing, rhythm and the safe area -----------------------------------
   The notch case: the header already paid for the top inset. The sides and
   the bottom matter too — a phone held in landscape puts the camera cutout
   over the left edge of the feed, and the home indicator sits on top of the
   composer's Save button. */
main {
  padding: var(--sp-5) calc(var(--sp-5) + var(--safe-l)) var(--sp-3)
           calc(var(--sp-5) + var(--safe-r));
  scroll-padding-bottom: var(--sp-6);
}
header {
  padding-left: calc(var(--sp-4) + var(--safe-l));
  padding-right: calc(var(--sp-4) + var(--safe-r));
}
footer {
  padding: var(--sp-3) calc(var(--sp-5) + var(--safe-r))
           calc(var(--sp-4) + var(--safe-b)) calc(var(--sp-5) + var(--safe-l));
}
.modal { padding: calc(var(--sp-3) + var(--safe-t)) var(--sp-3) calc(var(--sp-3) + var(--safe-b)); }
.modal-panel { border-radius: var(--r-lg); box-shadow: var(--shadow-3); }
.wide-panel { width: min(560px, 94vw); }

/* ---- touch targets -------------------------------------------------------
   Everything tappable clears 44px, and the header row gets real gaps so two
   adjacent icons are two targets rather than one ambiguous one. On a phone
   the top row is also the hardest place to reach, which is why the things you
   do constantly (write, search) are at the bottom and in the middle. */
.icon-btn { min-width: var(--tap); min-height: var(--tap); border-radius: var(--r-md); }
.header-controls { gap: var(--sp-2); }
.header-controls button, .more-menu summary { min-height: var(--tap); }
.more-menu summary { border-radius: var(--r-md); padding: 0 var(--sp-3); justify-content: center; min-width: var(--tap); }
.more-menu-items button { min-height: var(--tap); padding: var(--sp-2) var(--sp-3); }
button { min-height: var(--tap); border-radius: var(--r-sm); font-weight: 500; font-size: 0.84rem; letter-spacing: 0.01em; }
.mini-btn { min-height: 36px; border-radius: var(--r-sm); }
.modal-close { min-width: var(--tap); min-height: var(--tap); border-radius: var(--r-md); }

/* ---- motion --------------------------------------------------------------
   Named properties only. `transition: all` on a shared class is how an
   element that must appear instantly (the privacy veil) ends up with a fade
   nobody asked for. */
.icon-btn, button, .thread-row, .cal-cell, .tag-row, .cal-item,
.palette-row, .theme-swatch, .tag-chip, .note-link, .backlinks {
  transition: background-color var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease);
}
button:active, .icon-btn:active, .cal-cell:active { transform: scale(0.97); }

/* Something arriving. 170ms and a 4px rise — enough to read as "this came
   from somewhere", short enough that it is never something you wait for. */
@media (prefers-reduced-motion: no-preference) {
  .modal:not(.hidden) > .modal-panel { animation: panelIn var(--dur-2) var(--ease-out); }
  .panel.note { animation: noteIn var(--dur-2) var(--ease-out); }
  main .empty-state { animation: panelIn 260ms var(--ease-out); }
}
@keyframes panelIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes noteIn  { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* The reduced-motion contract, restated and widened. The blanket rule earlier
   in this file kills durations; this also kills the transforms, so an element
   whose animation is neutralised does not get stuck mid-rise. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  button:active, .icon-btn:active, .cal-cell:active { transform: none; }
}

/* ---- focus ---------------------------------------------------------------
   Kept from the accessibility work and made louder. A 2px ring on a busy
   background is easy to lose, so it gets a contrasting halo underneath it —
   which also means the ring stays visible on the parchment theme, where the
   accent is a mid-tone brown against a light page. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 22%, transparent);
  border-radius: var(--r-sm);
}
/* Rows that are their own tap target announce focus as a filled state rather
   than a floating ring, which reads better inside a scrolling list. */
.palette-row:focus-visible, .thread-row:focus-visible, .cal-item:focus-visible,
.tag-row:focus-visible {
  outline-offset: -2px;
  background: var(--panel-alt);
}

/* ---- the feed ------------------------------------------------------------ */
.panel { border-radius: var(--r-md); padding: var(--sp-4) var(--sp-4) var(--sp-3); }
.panel.note { border-radius: var(--r-md); box-shadow: var(--shadow-1); }
main .panel + .panel { margin-top: var(--sp-4); }

/* Empty states TEACH. The old one apologised and pointed at one shortcut;
   this one is the only screen a new user reads, so it spends its space on the
   three things that make the notebook worth keeping. */
.empty-state { max-width: 34ch; margin: 14vh auto 0; }
.empty-state .empty-glyph { font-size: 3rem; opacity: 0.5; margin-bottom: var(--sp-3); }
.empty-lead { color: var(--text); font-size: 0.95rem; line-height: 1.6; margin: 0 0 var(--sp-4); }
.empty-tips {
  list-style: none; margin: 0; padding: 0; text-align: left;
  display: flex; flex-direction: column; gap: var(--sp-2);
}
.empty-tips li {
  color: var(--muted); font-size: 0.82rem; line-height: 1.5;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--panel) 60%, transparent);
}
.empty-tips code {
  color: var(--accent); background: none; border: none;
  font-family: var(--mono); padding: 0; margin-right: 0.35em;
}
.empty-inline { color: var(--muted); font-size: 0.85rem; line-height: 1.6; padding: var(--sp-3) 0; }
.empty-inline b { color: var(--text); }
.empty-inline code {
  font-family: var(--mono); color: var(--accent);
  background: var(--panel-alt); border: 1px solid var(--border);
  border-radius: 4px; padding: 0.05rem 0.35rem;
}

/* ---- tags, note links, backlinks ---------------------------------------- */
.tag-chip {
  display: inline; text-decoration: none; cursor: pointer;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-radius: var(--r-sm);
  padding: 0.05em 0.32em;
  font-weight: 500;
  box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
.tag-chip:hover { background: color-mix(in srgb, var(--accent) 24%, transparent); }
[data-theme="parchment"] .tag-chip { background: color-mix(in srgb, var(--accent) 16%, transparent); }

/* A note link is styled as a link to somewhere INSIDE the notebook — a dotted
   underline rather than a solid one, so a glance can tell it apart from an
   http link that will leave the app. */
.note-link {
  color: var(--accent); text-decoration: none; cursor: pointer;
  border-bottom: 1px dashed var(--accent-dim);
}
.note-link::before { content: "⇢ "; opacity: 0.65; }
.note-link:hover { color: var(--text); border-bottom-color: var(--text); }

.backlinks {
  display: block; margin: var(--sp-3) 0 0;
  width: auto; min-height: 32px;
  background: none; border: none; border-top: 1px solid var(--border);
  border-radius: 0; padding: var(--sp-2) 0 0;
  color: var(--muted); font-size: 0.74rem; text-align: left; letter-spacing: 0.02em;
}
.backlinks:hover { color: var(--accent); }

/* ═══════════════ A note whose words are not yours ════════════════════════
   A reply kept out of a conversation with a model is an ordinary note in every
   mechanical sense, and must not LOOK like one. The badge is above the text,
   not below it: a label that arrives after you have already read something as
   your own has failed at the only job it had. The left rule carries the same
   fact down the whole panel for a long reply where the badge has scrolled off. */
.note-kept { border-left: 3px solid var(--accent-dim); }
.kept-badge {
  display: block; margin: 0 0 var(--sp-2);
  color: var(--accent); font-size: .7rem; letter-spacing: .04em;
}

/* ═══════════════ A link nobody has fetched yet ═══════════════════════════
   Rendered under a URL that arrived from something that is not a person. It is
   a button and a sentence, never a card: the whole point is that no request
   has been made and none will be until this is pressed. */
.unfurl-offer {
  display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  margin-top: var(--sp-2);
}
.unfurl-btn {
  background: transparent; border: 1px dashed var(--border); color: var(--muted);
  border-radius: 6px; padding: .2rem .5rem; min-height: 0; font-size: .7rem;
}
.unfurl-btn:hover { border-style: solid; border-color: var(--accent); color: var(--accent); }
.unfurl-btn[disabled] { opacity: .6; }
.unfurl-note { color: var(--muted); font-size: .66rem; line-height: 1.4; flex: 1 1 10rem; min-width: 0; }

/* The autocomplete strip above recall's results. */
.tag-suggest {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  padding: var(--sp-3) 1.1rem var(--sp-2);
  border-bottom: 1px solid var(--border);
}
.tag-suggest-chip {
  min-height: 34px; padding: 0.2rem 0.6rem;
  border: 1px solid var(--accent-dim); border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent); font-size: 0.78rem; font-family: var(--mono);
}
.tag-suggest-chip i { font-style: normal; opacity: 0.6; margin-left: 0.4em; font-size: 0.9em; }
.tag-suggest-chip:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }

/* ---- the tag index ------------------------------------------------------- */
.tags-body { margin-top: var(--sp-3); max-height: 58vh; overflow-y: auto; }
.tag-row {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  min-height: 42px; padding: var(--sp-2) var(--sp-2);
  background: none; border: none; border-bottom: 1px solid var(--border);
  border-radius: var(--r-sm); color: var(--text); text-align: left;
}
.tag-row:hover { background: var(--panel-alt); }
.tag-name { font-family: var(--mono); color: var(--accent); flex: 0 0 auto; min-width: 7rem; font-size: 0.85rem; }
.tag-bar { flex: 1 1 auto; height: 6px; border-radius: var(--r-pill); background: var(--panel-alt); overflow: hidden; }
.tag-bar i { display: block; height: 100%; background: var(--accent); border-radius: var(--r-pill); }
.tag-n { flex: 0 0 2.5rem; text-align: right; color: var(--muted); font-size: 0.76rem; }

/* ---- calendar ------------------------------------------------------------
   Density shading is built from the theme accent with color-mix, so the grid
   is legible on the two light themes as well as the three dark ones. A fixed
   green ramp would have looked correct in exactly one of them. */
.cal-head { display: flex; align-items: flex-start; gap: var(--sp-2); }
.cal-head .panel-title { flex: 1; }
.cal-nav {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: var(--sp-2) 0 var(--sp-2); flex-wrap: wrap;
}
.cal-label {
  flex: 1 1 auto; text-align: center; min-width: 8rem;
  color: var(--text); font-size: 0.95rem; font-weight: 600; letter-spacing: 0.02em;
}
#calendar-totals { margin: 0 0 var(--sp-3); text-align: center; }

.cal-grid {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px;
  margin: var(--sp-3) 0 var(--sp-2);
}
.cal-dow {
  text-align: center; font-size: 0.6rem; color: var(--muted);
  padding-bottom: var(--sp-1); text-transform: uppercase;
}
.cal-cell {
  position: relative; aspect-ratio: 1 / 1; min-height: 38px; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; padding: 0;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--panel-alt); color: var(--muted); font-size: 0.72rem;
}
.cal-cell.blank { border-color: transparent; background: none; }
.cal-cell[data-level="0"] { background: color-mix(in srgb, var(--panel-alt) 70%, transparent); }
.cal-cell[data-level="1"] { background: color-mix(in srgb, var(--accent) 14%, var(--panel-alt)); color: var(--text); }
.cal-cell[data-level="2"] { background: color-mix(in srgb, var(--accent) 30%, var(--panel-alt)); color: var(--text); }
.cal-cell[data-level="3"] { background: color-mix(in srgb, var(--accent) 52%, var(--panel-alt)); color: var(--text); }
.cal-cell[data-level="4"] { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.cal-cell:hover { border-color: var(--accent-dim); }
.cal-cell.today { outline: 2px solid var(--accent-dim); outline-offset: -2px; font-weight: 700; }
.cal-cell.selected { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 40%, transparent); }
.cal-num { line-height: 1; }
.cal-dot { display: none; }

.cal-legend {
  display: flex; align-items: center; justify-content: flex-end; gap: 4px;
  color: var(--muted); font-size: 0.62rem; margin-bottom: var(--sp-3);
}
.cal-legend .cal-swatch {
  width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border);
  display: inline-block;
}
.cal-swatch[data-level="0"] { background: var(--panel-alt); }
.cal-swatch[data-level="1"] { background: color-mix(in srgb, var(--accent) 14%, var(--panel-alt)); }
.cal-swatch[data-level="2"] { background: color-mix(in srgb, var(--accent) 30%, var(--panel-alt)); }
.cal-swatch[data-level="3"] { background: color-mix(in srgb, var(--accent) 52%, var(--panel-alt)); }
.cal-swatch[data-level="4"] { background: var(--accent); }

/* "On this day" leads the screen — bordered on the accent so it reads as the
   feature rather than as a header above the real content. */
.cal-otd {
  border: 1px solid var(--accent-dim); border-radius: var(--r-md);
  background: color-mix(in srgb, var(--accent) 7%, var(--panel));
  padding: var(--sp-3); margin-bottom: var(--sp-4);
}
.cal-otd.empty { border-style: dashed; background: none; }
.cal-otd-kicker {
  color: var(--accent); font-size: 0.66rem; letter-spacing: 0.16em;
  text-transform: uppercase; font-weight: 700;
}
.cal-otd-year {
  color: var(--muted); font-size: 0.68rem; letter-spacing: 0.06em;
  margin: var(--sp-3) 0 var(--sp-1); text-transform: uppercase;
}
.cal-otd-none { color: var(--muted); font-size: 0.8rem; margin: var(--sp-2) 0 0; line-height: 1.55; }

.cal-item {
  display: flex; flex-direction: column; gap: 2px; width: 100%; text-align: left;
  min-height: 0; padding: var(--sp-2); margin-bottom: 2px;
  background: none; border: none; border-radius: var(--r-sm); color: var(--text);
}
.cal-item:hover { background: var(--panel-alt); }
.cal-item-meta {
  color: var(--accent-dim); font-size: 0.62rem; letter-spacing: 0.06em;
  text-transform: uppercase; font-weight: 600;
}
.cal-item-text {
  color: var(--text); font-size: 0.84rem; line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.cal-day { margin-top: var(--sp-3); }
.cal-day-head {
  color: var(--accent); font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase;
  font-weight: 700; padding-bottom: var(--sp-2); border-bottom: 1px solid var(--border);
  margin-bottom: var(--sp-2);
}
.cal-day-none { color: var(--muted); font-size: 0.82rem; }

/* ---- export -------------------------------------------------------------- */
.danger-note {
  border: 1px solid var(--danger); border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--danger) 9%, transparent);
  color: var(--text); font-size: 0.8rem; line-height: 1.6;
  padding: var(--sp-3); margin: 0 0 var(--sp-3);
}
.danger-note b { color: var(--danger); }
#archive-pass {
  flex: 1; min-width: 9rem;
  background: var(--panel-alt); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm); padding: var(--sp-2);
}

/* ---- time capsule -------------------------------------------------------- */
#capsule-text {
  width: 100%; background: var(--panel-alt); border: 1px solid var(--border);
  color: var(--text); font-size: max(16px, 0.95rem); border-radius: var(--r-sm);
  padding: var(--sp-3); line-height: 1.5; resize: vertical;
}
#capsule-date {
  flex: 1; min-width: 9rem; background: var(--panel-alt); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm); padding: var(--sp-2);
}
.capsule-ceremony { text-align: center; }
.capsule-ceremony .panel-title { text-align: center; }
.capsule-seal-mark {
  font-size: 3.2rem; color: var(--accent); line-height: 1;
  margin: var(--sp-2) 0 var(--sp-3);
}
@media (prefers-reduced-motion: no-preference) {
  /* One flourish in the whole app, on the one screen that is a small
     occasion. It runs once, for half a second, and then sits still. */
  .modal:not(.hidden) .capsule-seal-mark { animation: sealRise 620ms var(--ease-out); }
}
@keyframes sealRise {
  0%   { opacity: 0; transform: translateY(14px) scale(0.86); }
  60%  { opacity: 1; transform: translateY(-3px) scale(1.04); }
  100% { opacity: 1; transform: none; }
}
.capsule-open-text {
  text-align: left; white-space: pre-wrap; line-height: 1.6; font-size: 0.95rem;
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--panel-alt); padding: var(--sp-4); margin: var(--sp-3) 0;
  max-height: 40vh; overflow-y: auto;
}

/* ---- year in review ------------------------------------------------------ */
.yr-card {
  border: 1px solid var(--accent-dim); border-radius: var(--r-lg);
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--accent) 10%, var(--panel)) 0%,
      var(--panel) 55%);
  padding: var(--sp-4); margin: var(--sp-2) 0 var(--sp-3);
}
.yr-head { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--sp-4); }
.yr-kicker {
  color: var(--muted); font-size: 0.62rem; letter-spacing: 0.18em;
  text-transform: uppercase; font-weight: 700;
}
.yr-year { color: var(--accent); font-family: var(--mono); font-size: 2.6rem; line-height: 1; font-weight: 700; }
.yr-figures { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-3); margin-bottom: var(--sp-4); }
.yr-fig { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.yr-fig b { font-family: var(--mono); font-size: 1.5rem; color: var(--text); line-height: 1.1; }
.yr-fig span { color: var(--muted); font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase; }
.yr-months {
  display: flex; align-items: flex-end; gap: 3px; height: 56px;
  padding: 0 1px; margin-bottom: 3px;
}
.yr-bar { flex: 1 1 0; background: var(--accent-dim); border-radius: 2px 2px 0 0; min-width: 0; }
.yr-monthlabels {
  display: flex; gap: 3px; color: var(--muted); font-size: 0.55rem;
  margin-bottom: var(--sp-4); padding: 0 1px;
}
.yr-monthlabels span { flex: 1 1 0; text-align: center; }
.yr-lines { display: flex; flex-direction: column; gap: 0; }
.yr-line {
  display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-3);
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); font-size: 0.8rem;
}
.yr-line span { color: var(--muted); }
.yr-line b { color: var(--text); font-family: var(--mono); font-size: 0.84rem; text-align: right; }
.yr-tags { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3); }
.yr-tag {
  font-family: var(--mono); font-size: 0.75rem; color: var(--accent);
  border: 1px solid var(--accent-dim); border-radius: var(--r-pill);
  padding: 0.1rem 0.55rem;
}
.yr-tag i { font-style: normal; opacity: 0.6; }
.yr-quote {
  margin: var(--sp-4) 0 0; padding: var(--sp-3) 0 0 var(--sp-3);
  border-left: 2px solid var(--accent-dim);
  color: var(--text); font-size: 0.85rem; line-height: 1.6; font-style: italic;
}
.yr-quote-label {
  display: block; font-style: normal; color: var(--muted);
  font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: var(--sp-1);
}
.yr-foot { color: var(--muted); font-size: 0.62rem; margin-top: var(--sp-4); letter-spacing: 0.04em; }
#review-year {
  background: var(--panel-alt); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-3); min-height: 40px;
}

/* ---- surprise ------------------------------------------------------------ */
.sp-body { margin-top: var(--sp-3); }
.sp-when {
  color: var(--accent); font-size: 0.66rem; letter-spacing: 0.12em;
  text-transform: uppercase; font-weight: 700; margin-bottom: var(--sp-3);
}
.sp-text {
  white-space: pre-wrap; line-height: 1.65; font-size: 0.95rem;
  border-left: 2px solid var(--accent-dim); padding-left: var(--sp-4);
  max-height: 46vh; overflow-y: auto;
}
.sp-back { margin-top: var(--sp-3); color: var(--muted); font-size: 0.74rem; }

/* ===========================================================================
   THE FIVE THEMES, MADE DELIBERATE

   A theme was five hex values swapped on <html data-theme>. That makes five
   tints of one design, and the difference between "Amber" and "Rosewood" was
   the hue of a border. Each of these now changes its own SHAPE and TEXTURE as
   well as its colour — corner radius, background treatment, shadow character,
   and how hard the type contrasts — so picking one is picking a room to write
   in rather than picking a highlighter.

   The constraint every one of them respects: contrast has to survive. Two of
   the five are light themes, so nothing here may depend on a dark background,
   and the density shading, the highlight colour and the focus ring are all
   built with color-mix against the theme's own surfaces rather than hardcoded.
=========================================================================== */

/* AMBER — lamplight on a desk at night. Warm near-black, a single pool of
   light from above, soft corners. The default, and the quietest. */
[data-theme="amber"], :root:not([data-theme]) {
  --r-sm-b: 6px; --r-md-b: 10px; --r-lg-b: 14px;
}
/* The wash goes on BODY, not on the html element the theme attribute sits on:
   body carries an opaque `background: var(--bg)` from the top of this file, so
   anything painted on html would be covered by it and silently do nothing. */
[data-theme="amber"] body, html:not([data-theme]) body {
  background-image:
    radial-gradient(120% 60% at 50% -10%,
      color-mix(in srgb, var(--accent) 9%, transparent) 0%, transparent 60%);
  background-attachment: fixed;
}

/* MIDNIGHT — cold, deep, and a long way from the desk lamp. A vertical
   gradient rather than a flat field, so the top of the screen recedes. */
[data-theme="midnight"] {
  --r-sm-b: 8px; --r-md-b: 12px; --r-lg-b: 18px;
  --shadow-2: 0 6px 22px rgba(4, 10, 28, .55);
  --shadow-3: 0 20px 56px rgba(3, 8, 24, .7);
}
[data-theme="midnight"] body {
  background-image:
    linear-gradient(180deg, #101a36 0%, var(--bg) 42%),
    radial-gradient(80% 40% at 12% 0%, rgba(110, 168, 255, .13) 0%, transparent 70%);
  background-attachment: fixed;
}
[data-theme="midnight"] .panel { backdrop-filter: none; }

/* MATRIX — a terminal, and it should look like one. Square corners, flat
   surfaces, tighter tracking, and a very faint scanline grid that is felt
   rather than seen. No soft shadows: a CRT does not have them. */
[data-theme="matrix"] {
  --r-sm-b: 2px; --r-md-b: 2px; --r-lg-b: 3px; --r-pill-b: 3px;
  --shadow-1: none;
  --shadow-2: 0 0 0 1px rgba(78, 230, 106, .14);
  --shadow-3: 0 0 40px rgba(78, 230, 106, .1), 0 0 0 1px rgba(78, 230, 106, .2);
}
[data-theme="matrix"] body {
  background-image: repeating-linear-gradient(180deg,
    rgba(78, 230, 106, .035) 0px, rgba(78, 230, 106, .035) 1px, transparent 1px, transparent 4px);
  background-attachment: fixed;
}
[data-theme="matrix"] body, [data-theme="matrix"] button,
[data-theme="matrix"] .hint, [data-theme="matrix"] .setting-row { font-family: var(--mono); }
[data-theme="matrix"] .panel.note { border-style: solid; }
[data-theme="matrix"] .yr-year, [data-theme="matrix"] .cal-label { letter-spacing: 0.04em; }

/* PARCHMENT — a light theme that has to look like paper rather than like a
   dark theme with the lights on. Warm layered wash, brown-tinted shadows
   (a black shadow on cream is the single thing that makes a light theme look
   cheap), and a slightly looser line height because dark-on-light needs air. */
[data-theme="parchment"] {
  --r-sm-b: 5px; --r-md-b: 9px; --r-lg-b: 12px;
  --shadow-1: 0 1px 2px rgba(120, 96, 48, .14);
  --shadow-2: 0 6px 20px rgba(120, 96, 48, .16);
  --shadow-3: 0 18px 46px rgba(110, 88, 44, .24);
}
[data-theme="parchment"] body {
  background-image:
    radial-gradient(100% 55% at 50% 0%, rgba(255, 252, 242, .9) 0%, transparent 65%),
    linear-gradient(180deg, #f4ecd9 0%, var(--bg) 55%);
  background-attachment: fixed;
}
[data-theme="parchment"] .panel { line-height: 1.55; }
[data-theme="parchment"] .modal { background: rgba(60, 48, 26, .34); }
[data-theme="parchment"] .bubble.out { color: #2b2618; }
[data-theme="parchment"] .avatar-chip { color: #fff; }

/* ROSEWOOD — the soft one. Rounder than everything else, a warm glow from the
   lower right, lower contrast on purpose: this is the theme for writing at
   the end of the day rather than for scanning a list. */
[data-theme="rose"] {
  --r-sm-b: 10px; --r-md-b: 16px; --r-lg-b: 22px;
  --shadow-2: 0 8px 26px rgba(26, 10, 32, .5);
  --shadow-3: 0 22px 60px rgba(22, 8, 28, .62);
}
[data-theme="rose"] body {
  background-image:
    radial-gradient(90% 60% at 95% 105%, rgba(224, 143, 176, .16) 0%, transparent 62%),
    radial-gradient(70% 40% at 0% 0%, rgba(176, 101, 136, .12) 0%, transparent 70%);
  background-attachment: fixed;
}
[data-theme="rose"] .bubble { border-radius: 20px; }
[data-theme="rose"] .bubble.in { border-bottom-left-radius: 6px; }
[data-theme="rose"] .bubble.out { border-bottom-right-radius: 6px; }

/* The theme swatches gain the same treatment, so the picker previews the
   shape of a theme and not only its hue. */
.theme-swatch { border-radius: var(--r-md); min-height: var(--tap); padding: var(--sp-2) var(--sp-3); }
.theme-swatch.active { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 30%, transparent); }
.sw[data-theme="amber"]     { border-radius: 50%; }
.sw[data-theme="midnight"]  { border-radius: 50%; }
.sw[data-theme="matrix"]    { border-radius: 2px; }
.sw[data-theme="parchment"] { border-radius: 30%; }
.sw[data-theme="rose"]      { border-radius: 50%; }

/* ---- narrow phones -------------------------------------------------------
   The bottom of the screen is where the thumb lives, so the composer keeps
   its full target size at every width; it is the header that gives up space
   first, because it is the thing you reach for least. */
@media (max-width: 600px) {
  main { padding-left: calc(var(--sp-4) + var(--safe-l)); padding-right: calc(var(--sp-4) + var(--safe-r)); }
  footer { padding-left: calc(var(--sp-4) + var(--safe-l)); padding-right: calc(var(--sp-4) + var(--safe-r)); }
  header { gap: var(--sp-1); }
  .header-controls { gap: 6px; }
  .header-controls .icon-btn { min-width: 40px; }
  form#composer button[type="submit"] { min-height: var(--tap); padding: 0 var(--sp-3); }
  .yr-figures { grid-template-columns: repeat(2, 1fr); }
  .cal-cell { min-height: 34px; font-size: 0.68rem; }
  .modal { padding: calc(var(--sp-2) + var(--safe-t)) var(--sp-2) calc(var(--sp-2) + var(--safe-b)); }
  .modal-panel { max-height: 88vh; }
}

/* Very wide: the feed stays a column. A 1400px-wide line of prose is not a
   feature, and this is a notebook, not a dashboard. */
@media (min-width: 900px) {
  main { padding-top: var(--sp-6); }
}

/* ---- the privacy veil, restated ------------------------------------------
   NOT restyled. Repeated here only so that anyone editing this section sees
   the constraint without having to find the original rule: it is OPAQUE, it
   has NO transition on the way in, and it sits above everything. Those are
   security properties (the OS task-switcher takes a real screenshot within a
   frame or two of the app being hidden), not visual ones. The `transition`
   declarations in this pass all name their properties precisely so none of
   them can reach this element. */
#privacy-veil { transition: none; }

/* The palette and recall overlays get the safe area too — on a notched phone
   in landscape the search field otherwise starts underneath the camera. */
.overlay {
  padding: max(8vh, calc(var(--safe-t) + var(--sp-4)))
           calc(var(--sp-4) + var(--safe-r))
           calc(var(--sp-4) + var(--safe-b))
           calc(var(--sp-4) + var(--safe-l));
}
.palette-card { border-radius: var(--r-lg); box-shadow: var(--shadow-3); }
.palette-row { padding: var(--sp-3) 1.1rem; min-height: 44px; justify-content: center; }
#palette-input, #search-input { padding: var(--sp-4) 1.1rem; }

/* ---- fixes found by looking at the thing -------------------------------
   Everything in this block came out of screenshotting the app at 420x880 and
   reading the result rather than the CSS. Two of them predate this pass. */

/* 1. Modal titles were being CLIPPED at the top edge of the panel — "MESSAGES"
      and "YEAR IN REVIEW" rendered as half a line of text sitting above their
      own panel. The rule that pulls a title back into the flow inside a modal
      was a CHILD selector (`.modal-panel > .panel-title`), so it stopped
      applying the moment a title was wrapped in a header row for layout —
      which Messages has done since it was built. Descendant, not child. */
.modal-panel .panel-title {
  position: static;
  display: block;
  background: transparent;
  padding: 0;
  margin: 0 0 var(--sp-3);
  color: var(--accent);
}

/* 2. The close button is absolutely positioned in the panel's top-right, so a
      header row that fills its full width runs underneath it. Reserve the
      corner rather than moving the button: it wants to be in the same place
      on every modal, which is most of the point of a close button. */
.modal-panel > .panel-title,
.messages-head, .cal-head { padding-right: calc(var(--tap) + var(--sp-1)); }
.cal-head { align-items: center; }

/* 3. The reply field was the browser's default white box in all five themes —
      the one input in the app that never got themed. */
.msg-reply-row input, #msg-reply-input {
  background: var(--panel-alt); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-3); min-height: var(--tap);
}
.msg-reply-row input::placeholder { color: var(--muted); }

/* 4. A thread header with a timer, a verify button and a block button left
      four characters for the person's name ("Sar…"). The name is the most
      important thing on that row, so it now takes a full line of its own and
      the controls wrap underneath, where there is room for them to be legible
      instead of truncated. */
/* Was `flex-wrap: wrap` with the name forced onto a row of its own and five
   controls wrapping under it — 143px of an 774px panel. One row now: who you
   are talking to (and their verification badge, which is state you read every
   time) plus ⋯ for the actions, which are pressed once each or never. */
.msg-thread-head { flex-wrap: nowrap; position: relative; }
#msg-thread-name { flex: 1 1 auto; min-width: 0; font-size: 1rem; }
.msg-timer { max-width: 9.5rem; border-radius: var(--r-sm); background: var(--panel-alt); color: var(--text); }

/* 5. The composer placeholder was being clipped mid-word on a 420px screen
      ("write a note…  (try"). A placeholder that does not fit is worse than a
      short one: the hint it was carrying now lives in the empty state, where
      there is room to say it properly. */
#msg-input::placeholder { text-overflow: ellipsis; }

/* Bubbles get the theme radius and a little more breathing room. */
.bubble { padding: var(--sp-3) var(--sp-3); border-radius: var(--r-lg); }
.bubble.in { border-bottom-left-radius: var(--r-sm); }
.bubble.out { border-bottom-right-radius: var(--r-sm); }
.thread-row { border-radius: var(--r-md); min-height: var(--tap); }

/* The invite/QR and settings fields, brought onto the same input treatment as
   everything else rather than each carrying its own three declarations. */
#gate input, #set-sync-pass, #set-username, #dm-to, #dm-text, #note-text,
#invite-url, #gate-invite-code, #gate-note, #member-name, #member-hash,
#groups-select, #set-lock-timeout, #set-duress-pass {
  border-radius: var(--r-sm);
}
#gate .panel { width: min(360px, 92vw); padding: var(--sp-5); }
#gate input { min-height: var(--tap); padding: var(--sp-2) var(--sp-3); }

/* The header glyphs (⌕ ⌘ ⋯) are typographic symbols, not emoji, and the
   system UI stack renders several of them at a fraction of the size of the
   emoji sitting next to them. They were mono before this pass because the
   whole app was; keeping them there is what makes the row read as one set of
   controls rather than as four unrelated marks. */
.icon-btn, .more-menu summary, .prompt-glyph, .empty-tips code { font-family: var(--mono); }
#search-btn { font-size: 1.35rem; }
#palette-btn { font-size: 1.05rem; }
#inbox-btn { font-size: 1rem; }
.more-menu summary { font-size: 1.2rem; letter-spacing: 0; }

/* The note wash used to be a hardcoded 18% of the accent, with a hand-tuned
   14% for parchment and 7% for matrix because a saturated accent at 18% made
   the feed read as inverted. All three are gone: the wash is --accent-subtle,
   which is computed per theme per accent to the strength at which BOTH --text
   and --muted still clear 4.5:1 over it. That is the same tuning those three
   numbers were doing by eye, done by measurement, and it now works for an
   accent nobody has seen yet. */

/* ===========================================================================
   ========================= THE APPEARANCE SYSTEM ==========================

   Everything a person can choose about how this app looks resolves here, and
   it resolves into TOKENS. The rule this section is written to obey, and the
   reason it is one block rather than eight:

     a new option may add ROWS to a token table. It may not add a per-theme
     special case.

   Eight themes x three densities x four type settings x three corner styles is
   288 combinations; the only way that is maintainable is if the combinations
   never meet. A density knows nothing about a theme, a theme knows nothing
   about a corner style, and the accent knows only the three surface colours it
   has to survive against.

   The five per-theme exceptions that survive further up this file (matrix's
   note wash, rose's bubble tails, parchment's line height) are SHAPE and
   TEXTURE decisions, which is the one axis a token cannot carry.
=========================================================================== */

/* ---- 1. The two new themes' texture --------------------------------------
   Every other theme in this file earns its identity from a background wash, a
   shadow character and a radius. These two are no different — except that the
   high-contrast pair earns it by having none of those things, deliberately. */

/* DAYLIGHT. A light theme fails in one of two ways: it uses black shadows
   (which look like dirt on white) or it uses no shadows at all (and then
   nothing has an edge). This uses a cool grey shadow at low opacity, a flat
   page, and a white card that sits a half-step above it. */
[data-theme="daylight"] {
  --r-sm-b: 7px; --r-md-b: 11px; --r-lg-b: 16px;
  --shadow-1: 0 1px 2px rgba(24, 34, 54, .07);
  --shadow-2: 0 4px 16px rgba(24, 34, 54, .10);
  --shadow-3: 0 18px 44px rgba(24, 34, 54, .16);
}
[data-theme="daylight"] body {
  background-image: linear-gradient(180deg, #ffffff 0%, var(--bg) 30%);
  background-attachment: fixed;
}
[data-theme="daylight"] .panel { box-shadow: var(--shadow-1); }

/* HIGH CONTRAST, both polarities. No wash, no shadow, no translucency, and a
   2px border on everything that is a thing — for a low-vision reader the
   boundary between two surfaces has to be a line, not a 4% luminance step. */
[data-theme="contrast"], [data-theme="contrast-light"] {
  --r-sm-b: 3px; --r-md-b: 4px; --r-lg-b: 5px; --r-pill-b: 4px;
  --shadow-1: none; --shadow-2: none; --shadow-3: none;
}
[data-theme="contrast"] body, [data-theme="contrast-light"] body { background-image: none; }
[data-theme="contrast"] .panel, [data-theme="contrast-light"] .panel,
[data-theme="contrast"] .modal-panel, [data-theme="contrast-light"] .modal-panel,
[data-theme="contrast"] .palette-card, [data-theme="contrast-light"] .palette-card,
[data-theme="contrast"] button, [data-theme="contrast-light"] button,
[data-theme="contrast"] .icon-btn, [data-theme="contrast-light"] .icon-btn {
  border-width: 2px;
}
[data-theme="contrast"] .panel.recall, [data-theme="contrast-light"] .panel.recall { border-style: solid; }
/* Underlining links rather than colouring them is the same argument: colour is
   the one channel a low-vision or colour-blind reader may not have. */
[data-theme="contrast"] .panel a, [data-theme="contrast-light"] .panel a,
[data-theme="contrast"] .tag-chip, [data-theme="contrast-light"] .tag-chip,
[data-theme="contrast"] .note-link, [data-theme="contrast-light"] .note-link {
  text-decoration: underline;
}
[data-theme="contrast"] :focus-visible, [data-theme="contrast-light"] :focus-visible {
  outline-width: 3px;
}

/* ---- 2. Accent tokens, spent ---------------------------------------------
   These four rules are the entire reason the derivation exists. Before this,
   the app printed text on --accent-dim and hoped; two themes needed a
   hand-written text colour to survive it, and a custom accent would have had
   nowhere to get one. --on-accent is computed against the exact fill it will
   sit on, in every theme, for every colour. */
.bubble.out { background: var(--accent-fill); color: var(--on-accent); }
.segmented button.active { background: var(--accent-fill); color: var(--on-accent); }
.tag-bar i { background: var(--accent-fill); }
/* ...which retires this. */
[data-theme="parchment"] .bubble.out { color: var(--on-accent); }

/* The timestamp and read tick inside a bubble were quietened with opacity, and
   an outgoing bubble is a solid block of accent, so the quietening landed on a
   surface that changes with the user's colour. Each side of the conversation
   now takes the foreground that belongs to ITS OWN background: --muted on the
   incoming panel, --on-accent on the outgoing fill. Small type carries the
   "this is secondary", which is what size is for. */
.bubble-meta, .bubble .tick { opacity: 1; }
.bubble.in .bubble-meta, .bubble.in .tick { color: var(--muted); }
.bubble.out .bubble-meta, .bubble.out .tick { color: var(--on-accent); }

/* The contact avatar is the one colour in the app that is NOT a theme colour:
   it is generated from a hash of the person's key so the same person is the
   same colour everywhere, and it is generated at a fixed low lightness
   precisely so that one foreground works on all of them. Deriving it per theme
   would defeat the point (recognising someone at a glance), so it keeps its
   own pair. */
.avatar-chip { color: #fff; border-color: rgba(255,255,255,.35); }

/* The subtle wash, used wherever accent sits BEHIND text. Same story: it is
   computed so that --text still clears 4.5:1 over it, rather than picked. */
/* A chip lives inside a note, and a note is painted in --accent-subtle, so a
   chip takes the step BEYOND it — otherwise the pill is invisible on exactly
   the surface it appears on most. */
.tag-chip, .tag-suggest-chip { background: var(--accent-wash-2); }
.tag-chip:hover, .tag-suggest-chip:hover {
  background: color-mix(in srgb, var(--accent-fill) 26%, var(--accent-wash-2));
}
[data-theme="parchment"] .tag-chip { background: var(--accent-wash-2); }

/* The focus ring gets its own token so it can be louder than the accent
   without dragging the accent up with it. */
:focus-visible {
  outline-color: var(--focus);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--focus) 26%, transparent);
}

/* ---- 3. Density -----------------------------------------------------------
   Three rows. Everything else in the file already spends --sp-* and --lh. */
[data-density="compact"]     { --sp-mult: 0.78; --lh-b: 1.38; }
[data-density="comfortable"] { --sp-mult: 1;    --lh-b: 1.55; }
[data-density="spacious"]    { --sp-mult: 1.3;  --lh-b: 1.78; }

/* The rules that had a line height baked in, brought onto the token. This is
   the "coherently through the token scale" part: spacious does not mean
   "bigger gaps around the same tight paragraph". */
body, p, .hint, .empty-lead, .empty-inline { line-height: var(--lh); }
.panel { line-height: var(--lh-tight); }
.panel.note, .bubble-text, .sp-text, .capsule-open-text { line-height: var(--lh); }
[data-theme="parchment"] .panel { line-height: var(--lh); }
main .panel + .panel { margin-top: var(--sp-4); }
.more-menu-items { gap: var(--sp-1); padding: var(--sp-2); }
.setting-block + .setting-block { margin-top: var(--sp-5); }
footer { padding-top: var(--sp-3); padding-bottom: calc(var(--sp-4) + var(--safe-b)); }

/* ---- 4. Type --------------------------------------------------------------
   The Type setting repoints ONE variable, --font-note, which is what the
   notebook's own voice is set in. Monospace is still the default and still the
   house style; it is also genuinely tiring to read a thousand words of, and
   "the app has opinions" is not a reason to make somebody's diary hard work. */
[data-type="mono"]  { --font-note: var(--mono); }
[data-type="sans"]  { --font-note: var(--font-ui); }
[data-type="serif"] { --font-note: var(--font-serif); }
[data-type="dyslexic"] {
  --font-note: var(--font-dyslexic);
  /* The dyslexia-friendly option is the one setting here that reaches beyond
     the notebook into the app's own chrome, because a reader who needs it
     needs it for the buttons too. The tracking and word spacing do most of
     the work when no dyslexia-specific face is installed — letter and word
     separation is the part of those fonts with actual evidence behind it. */
  --font-ui: var(--font-dyslexic);
  letter-spacing: 0.035em;
  word-spacing: 0.14em;
  /* Multiplied INTO the density's line height rather than replacing it, so
     "compact + dyslexia-friendly" is still compact and still airier than
     compact alone. Two settings that overwrite each other's token are two
     settings that quietly cancel. */
  --lh-x: 1.14;
}
/* Never repointed, whatever is chosen: a safety number, a key, a code and a
   block of code are all things where the fixed width IS the information. */
code, pre, .invite-code, #invite-url, .icon-btn, .more-menu summary,
.prompt-glyph, .empty-tips code, #member-hash, .sr-meta {
  font-family: var(--mono);
}
.panel.note, .panel.note .panel-title, .bubble-text, .capsule-open-text,
.sp-text, #note-text, #capsule-text, #msg-input, #dm-text, #msg-reply-input {
  font-family: var(--font-note);
}

/* ---- 5. Corners -----------------------------------------------------------
   One multiplier over the theme's own radii, so "square" squares a rounded
   theme and "rounded" rounds a square one without either forgetting which
   theme it is. */
[data-corners="square"]  { --r-mult: 0; }
[data-corners="soft"]    { --r-mult: 1; }
[data-corners="rounded"] { --r-mult: 1.7; }
/* Two shapes that are round because of what they ARE, not because of a theme:
   an avatar and a colour swatch. Squaring those is a bug, not a preference. */
.avatar-chip, .theme-swatch .sw, .accent-swatch i { border-radius: 50%; }
[data-corners="square"] .avatar-chip, [data-corners="square"] .theme-swatch .sw,
[data-corners="square"] .accent-swatch i { border-radius: 2px; }

/* ---- 6. Reading width -----------------------------------------------------
   main had a 760px cap already; the composer and the header did not, so on a
   wide window the notebook sat in a neat column under a header stretched to
   2000px. All three now share one token. */
[data-width="narrow"] { --content-max: 38rem; }
[data-width="normal"] { --content-max: 48rem; }
/* "Full" is a very large number rather than `none`, because every rule below
   feeds this token to calc() and calc(100% - none) is not a length — it is a
   dropped declaration and a silently broken layout. */
[data-width="full"]   { --content-max: 400rem; }

main { max-width: var(--content-max); }
form#composer { max-width: var(--content-max); }
/* The header and the footer stay full-bleed — their border is the line that
   separates the app's chrome from the page, and a 48rem-wide rule floating in
   the middle of a 2000px window looks like a mistake. It is their CONTENT that
   is centred, by growing the side padding instead of capping the box. */
header {
  padding-left: max(calc(0.9rem + var(--safe-l)), calc((100% - var(--content-max)) / 2));
  padding-right: max(calc(0.9rem + var(--safe-r)), calc((100% - var(--content-max)) / 2));
}
footer {
  padding-left: max(calc(1.25rem + var(--safe-l)), calc((100% - var(--content-max)) / 2));
  padding-right: max(calc(1.25rem + var(--safe-r)), calc((100% - var(--content-max)) / 2));
}

/* ---- 7. Contrast repairs -------------------------------------------------
   Found by measuring, not by looking: every one of these was text diluted by
   an `opacity` that put it under 4.5:1 no matter how good the palette was.
   Opacity is a fine way to say "this is quieter" and a terrible way to say it
   about text, because it multiplies against a background the author cannot
   see from where the rule is written. --muted is already measured; use it. */
.entry-btn { opacity: 1; color: var(--muted); }
.entry-btn:hover { color: var(--accent); }
.delete-btn:hover { color: var(--danger); }
.msg-system { opacity: 1; color: var(--muted); background: var(--panel-alt); }
.msg-timer-note { opacity: 1; color: var(--muted); }
.verify-badge.ok { opacity: 1; color: var(--accent-dim); }
.tag-suggest-chip i { opacity: 1; color: var(--muted); }
.yr-tag i { opacity: 1; color: var(--muted); }
#mic-btn { opacity: 1; color: var(--muted); }
#mic-btn.listening { color: var(--danger); }
.linkish:hover { opacity: 1; color: var(--accent-hover); }
/* Disabled controls are explicitly exempt from WCAG 1.4.3, and dimming them is
   how a person knows they are disabled. Left alone, and marked so as not to be
   "fixed" by a later pass. */
button:disabled { opacity: 0.45; }

/* ---- 8. The Appearance screen -------------------------------------------- */
.appearance-body { display: flex; flex-direction: column; gap: var(--sp-5); }
.ap-group { display: flex; flex-direction: column; gap: var(--sp-2); }
.ap-label {
  font-family: var(--font-ui); font-size: 0.66rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--muted); font-weight: 600;
}
.ap-note { color: var(--muted); font-size: 0.78rem; line-height: var(--lh); margin: 0; }

/* Presets: a whole look in one tap, for the large majority of people who want
   the app to look nicer and do not want to be given a colour picker. */
.preset-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.preset-btn {
  flex: 1 1 9rem; text-align: left; padding: var(--sp-3);
  background: var(--panel-alt); border: 1px solid var(--border);
  border-radius: var(--r-md); color: var(--text); min-height: var(--tap);
  display: flex; flex-direction: column; gap: 2px;
}
.preset-btn b { color: var(--accent); font-size: 0.84rem; font-weight: 600; }
.preset-btn span { color: var(--muted); font-size: 0.72rem; line-height: 1.35; }
.preset-btn:hover { border-color: var(--accent-dim); }

/* Accent swatches. A colour picker on its own is a trap — most people want a
   good answer, not a colour wheel — so the curated set comes first and the
   picker sits at the end of the same row for the people who want their own. */
.accent-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.accent-swatch {
  min-width: var(--tap); min-height: var(--tap); padding: var(--sp-1);
  background: var(--panel-alt); border: 1px solid var(--border);
  border-radius: var(--r-md); display: inline-flex; align-items: center;
  justify-content: center; position: relative;
}
.accent-swatch i {
  display: block; width: 1.4rem; height: 1.4rem;
  border: 1px solid var(--border);
}
.accent-swatch.active { border-color: var(--focus); box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus) 34%, transparent); }
.accent-swatch[data-accent="auto"] i {
  background: linear-gradient(135deg, var(--accent-fill) 50%, var(--panel-alt) 50%);
}
.accent-custom { display: flex; align-items: center; gap: var(--sp-2); }
.accent-custom input[type="color"] {
  width: var(--tap); height: var(--tap); padding: 2px; cursor: pointer;
  background: var(--panel-alt); border: 1px solid var(--border);
  border-radius: var(--r-md);
}

/* The honesty panel. It says what the derivation did and what the numbers came
   out at — partly so a user can trust it, and mostly because a system that
   silently corrects you is indistinguishable from a system that ignores you. */
.ap-verdict {
  border: 1px solid var(--border); border-left: 3px solid var(--accent-fill);
  border-radius: var(--r-sm); padding: var(--sp-3);
  background: var(--panel-alt); font-size: 0.78rem; line-height: var(--lh);
}
.ap-verdict.changed { border-left-color: var(--danger); }
.ap-verdict p { margin: 0 0 var(--sp-2); }
.ap-verdict p:last-child { margin-bottom: 0; }
.ap-ratios { display: grid; grid-template-columns: 1fr auto auto; gap: 2px var(--sp-3); }
.ap-ratios span { font-size: 0.72rem; color: var(--muted); }
.ap-ratios b { font-family: var(--mono); font-size: 0.72rem; color: var(--text); font-weight: 500; }
.ap-ratios .ok { color: var(--accent); }
.ap-ratios .no { color: var(--danger); }

/* The live sample. The app behind the modal is already the real preview —
   everything applies instantly — but the modal covers most of it, so this is
   the bit that shows a note, a chip, a button and a solid accent block
   together, which is where a bad accent shows up first. */
.ap-preview {
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--panel); padding: var(--sp-4); display: flex;
  flex-direction: column; gap: var(--sp-3);
}
.ap-preview .ap-pv-note {
  font-family: var(--font-note); font-size: 0.9rem; line-height: var(--lh);
  color: var(--text); margin: 0;
}
.ap-preview .ap-pv-muted { color: var(--muted); font-size: 0.78rem; margin: 0; }
.ap-pv-row { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }
.ap-pv-fill {
  background: var(--accent-fill); color: var(--on-accent);
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm);
  font-size: 0.78rem; font-weight: 600;
}
.ap-pv-fill:hover { background: var(--accent-hover); }

.segmented { border-radius: var(--r-sm); }
.segmented button { border-radius: 0; min-height: var(--tap); }
.ap-seg-wide button { flex: 1 1 auto; font-size: 0.76rem; padding: 0 var(--sp-2); }

.ap-auto-pair { display: none; gap: var(--sp-3); flex-wrap: wrap; }
[data-theme-mode="auto"] .ap-auto-pair { display: flex; }
[data-theme-mode="auto"] #ap-theme-single { display: none; }
.ap-auto-pair label { flex: 1 1 10rem; display: flex; flex-direction: column; gap: var(--sp-1); }
.ap-auto-pair select {
  background: var(--panel-alt); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-2); min-height: var(--tap);
}

.ap-footer { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* ---- 9. The privacy veil, one more time ----------------------------------
   Nothing above may touch it, and nothing above does. Restated as an explicit
   floor rather than as a comment, so that a future theme, density, corner
   style or accent cannot make it translucent, cannot give it a radius, and
   cannot give it a transition. It is the only rule in this file that uses
   !important, and that is what it is for: this is a security control that
   happens to be made of CSS. The OS takes a real screenshot of this app within
   a frame or two of it being backgrounded; a veil that is 98% opaque, or that
   is 40% of the way through a fade, is a veil that shows the messages. */
#privacy-veil {
  background: var(--bg) !important;
  opacity: 1 !important;
  transition: none !important;
  animation: none !important;
  backdrop-filter: none !important;
  border-radius: 0 !important;
  color: var(--text);
}
#privacy-veil .veil-hint { color: var(--muted); }

/* ---- 10. The assistant (ai.js, §9b) ---------------------------------------
   Design brief: it must read as part of this app for the handful of people who
   have it, and be absent for everyone else. So it borrows the vocabulary
   already here — the modal panel, the thread row, the message bubble, the 4px
   scale, the 44px tap target — and adds almost no new shapes. Every colour is
   a theme token, so the eight themes and any accent the contrast engine
   produces carry it for free; there is not one hard-coded hex below.

   There is also no animation here beyond the durations already in the token
   pair, so `prefers-reduced-motion` needs no new exception, and nothing in
   this block can reach #privacy-veil (every transition names its properties). */

/* THE PANEL FILLS THE SCREEN, AND THE TRANSCRIPT GETS WHAT IS LEFT.
   Before this the panel was content-sized inside a centred modal and #ai-body
   carried `max-height: 52vh`, so on a 420x880 phone the conversation — the
   only reason anybody opens this — got about a third of the panel while the
   controls, the quota line and two permanent paragraphs of disclosure took the
   rest. A cap on the ONE part that should absorb slack is backwards: the
   chrome is what should be fixed, and the transcript is what should grow. */
#ai-modal { align-items: stretch; }
.ai-panel {
  display: flex; flex-direction: column; position: relative;
  /* height stays AUTO and the stretch above does the work. `height: 100%`
     resolves against the modal's content box and then adds the panel's own
     margin on top of it, which put the bottom of the panel 8px below the
     bottom of the screen — the composer's last row was off the edge on a
     short phone. Stretch subtracts the margins for us. */
  min-height: 0; max-height: none; height: auto;
  /* The shared .panel margin centred a short card; a full-height sheet does
     not want it, and left it 16px lower at the top than at the bottom. */
  margin: 0;
}
/* On a wide screen a full-height column of chat is not better, it is just
   long, so the panel goes back to a centred card there. */
@media (min-width: 720px) {
  #ai-modal { align-items: center; }
  .ai-panel { height: min(88vh, 48rem); }
}
/* The close button is absolutely positioned in the panel's top-right corner
   (that is how every modal here works), so the head row leaves it a lane
   rather than letting ＋ New slide underneath it. */
.ai-head { display: flex; align-items: center; gap: var(--sp-2); padding-right: 2.4rem; }
.ai-head .panel-title { flex: 1; }

.ai-quota {
  margin: 0 0 var(--sp-2); color: var(--muted); font-size: 0.76rem;
  font-family: var(--font-ui, inherit);
}
.ai-picker {
  background: var(--panel-alt); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-2); min-height: var(--tap);
  margin-bottom: var(--sp-2); width: 100%;
}

/* flex: 1 with min-height: 0 is the whole trick — without min-height a flex
   item refuses to shrink below its content, so a long transcript pushed the
   composer off the bottom instead of scrolling. */
.ai-body { flex: 1 1 auto; min-height: 4rem; max-height: none; }
.ai-bubble { max-width: 88%; }
.ai-thinking { color: var(--muted); font-style: italic; }
/* The bridge drives a real browser and honestly takes half a minute. After a
   few seconds the bubble grows a second line saying so, on its own row, so a
   long wait reads as "this is how long it takes" rather than "this is stuck". */
.ai-thinking-sub {
  display: block; font-style: normal; font-size: 0.72rem;
  opacity: 0.75; margin-top: 2px;
}

/* The plaintext-queue disclosure, in bridge mode only. Deliberately NOT styled
   as an error: it is a true and permanent property of this engine, not a fault
   and not a transient state, and red would train people to dismiss it. It gets
   the same quiet left rule an attached note gets — the visual language this app
   already uses for "read this carefully, it is about what leaves your device". */
.ai-plain-note {
  border-left: 3px solid var(--accent-fill);
  background: var(--accent-subtle);
  border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-3);
  margin-top: var(--sp-2);
  text-align: left;
}

/* An attached note is drawn as neither a message nor a reply: it is a quoted
   document, and it should look like one so nobody mistakes it for something
   they typed. Left rule, wash, and the label says where it came from. */
.ai-attached {
  align-self: stretch; max-width: 100%;
  background: var(--accent-subtle); color: var(--text);
  border: 1px solid var(--border); border-left: 3px solid var(--accent-fill);
  border-radius: var(--r-sm);
}
.ai-attached-label {
  font-size: 0.68rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--accent); margin-bottom: var(--sp-1);
}
.ai-attached .bubble-text { white-space: pre-wrap; }

.ai-empty { text-align: center; padding: var(--sp-4) var(--sp-3); color: var(--muted); }
.ai-empty-glyph { font-size: 1.6rem; color: var(--accent); margin-bottom: var(--sp-2); }
.ai-empty-lead { color: var(--text); margin: 0 0 var(--sp-2); }
.ai-empty .hint { text-align: left; }

.ai-note { color: var(--danger); font-size: 0.8rem; margin: var(--sp-2) 0 0; }
.ai-calm { color: var(--muted); }
.ai-foot { margin: var(--sp-2) 0 0; color: var(--muted); font-size: 0.74rem; }

.ai-attach-row {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  margin-top: var(--sp-2); padding: var(--sp-2);
  background: var(--accent-subtle); border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
.ai-attach-chip { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.ai-attach-chip > b { font-family: var(--font-note); font-size: 0.86rem; line-height: 1.4; }
.ai-attach-sub { color: var(--muted); font-size: 0.72rem; line-height: 1.45; }

/* THE COMPOSER IS ONE ROW THAT CANNOT WRAP.
   It used to be `flex-wrap: wrap` around a textarea and SIX buttons, which on
   a 420px phone laid itself out over three lines and ate 148px of a screen
   whose scarcest resource is vertical space. Three things stay: the box you
   type in, ⋯ (everything occasional, one tap away) and Send. `nowrap` plus a
   textarea that is allowed to shrink to nothing means the row is one line at
   320px as surely as at 768px — it is not a width that happens to fit. */
.ai-compose {
  display: flex; gap: var(--sp-2); margin-top: var(--sp-2);
  flex-wrap: nowrap; align-items: flex-end;
  /* The overflow popup anchors to THIS row, not to the ⋯ button. Anchored to
     the button it could only ever grow leftwards from a point 60px in from the
     right edge, and at 320px that put its left edge 7px outside the window. */
  position: relative;
}
.ai-compose > textarea { flex: 1 1 0; min-width: 0; }

/* THE OVERFLOW POPUP — one implementation, used by the assistant's composer
   and by the conversation header. A real <details>, so it opens on Enter and
   on a tap, closes on Escape, and is announced as a disclosure — none of which
   is true of a div with a click handler and aria-expanded painted on
   afterwards.
   The popup anchors to a positioned ANCESTOR ROW rather than to the ⋯ button:
   anchored to the button it can only grow leftwards from a point 60px in from
   the right edge, which put its left edge outside a 320px window. */
.pop-more { flex: 0 0 auto; }
.pop-more > summary {
  list-style: none; display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--tap); min-height: var(--tap); padding: 0 var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--panel-alt); color: var(--text); cursor: pointer;
}
.pop-more > summary::-webkit-details-marker { display: none; }
.pop-more > summary:hover { border-color: var(--accent); }
/* A dot when something is armed, so "behind a menu" never means "out of
   sight": the ⋯ itself says there is state under it. The armed thing also
   still shows as its own chip row above the composer. */
.pop-more[data-armed="1"] > summary { border-color: var(--accent); color: var(--accent); }
/* Drops DOWN by default, because most anchors are near the top of their
   panel — the conversation header's menu opened upwards for one release and
   every item but the last was off the top of the screen. The composer sits at
   the bottom, so it opts into the other direction below. */
.pop-more-items {
  position: absolute; right: 0; top: calc(100% + var(--sp-1));
  width: max-content; min-width: min(14rem, 100%); max-width: 100%;
  display: flex; flex-direction: column; gap: 2px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--shadow-3);
  padding: var(--sp-1); z-index: 3;
}
.pop-more-items button, .pop-more-items select {
  width: 100%; text-align: left; justify-content: flex-start;
  background: transparent; border: 1px solid transparent; color: var(--text);
  border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-3); min-height: var(--tap);
}
.pop-more-items button:hover { background: var(--panel-alt); border-color: var(--border); }
/* The longest label is "⌕ Notes about this". It is allowed to wrap rather than
   push the menu past the edge of a 320px screen. */
.pop-more-items button { white-space: normal; }
.pop-more-items select { border-color: var(--border); background: var(--panel-alt); }
.pop-more-label {
  color: var(--muted); font-size: 0.7rem; letter-spacing: 0.04em;
  text-transform: uppercase; padding: var(--sp-2) var(--sp-3) 0;
}

.pop-more-items button[data-armed="1"] { color: var(--accent); }
.ai-compose .pop-more-items { top: auto; bottom: calc(100% + var(--sp-1)); }


/* THE STANDING DISCLOSURE.
   Two permanent paragraphs — the bridge plaintext explanation and the persona
   note — used to sit under the composer on every single view, costing about a
   third of the screen forever. Not one word of them is gone. They are shown in
   full when they are new and whenever the engine or the persona changes, and
   they collapse to a line that still STATES the fact rather than hinting at
   it, so the collapsed form is a summary and not a euphemism. */
.ai-disclose { margin: 0; }
.ai-disclose > summary {
  cursor: pointer; color: var(--muted); font-size: 0.74rem; line-height: 1.4;
  padding: var(--sp-1) 0; list-style: none;
  display: flex; gap: var(--sp-2); align-items: baseline;
}
.ai-disclose > summary::-webkit-details-marker { display: none; }
.ai-disclose > summary:hover { color: var(--text); }
.ai-disclose > summary .ai-disclose-more {
  margin-left: auto; flex: 0 0 auto; color: var(--accent); white-space: nowrap;
}
.ai-disclose[open] > summary { color: var(--text); }
.ai-disclose-body { padding-bottom: var(--sp-1); }
.ai-disclose-body > p { margin: var(--sp-2) 0 0; }
.ai-compose input {
  flex: 1 1 8rem; min-width: 0; background: var(--panel-alt); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-3); min-height: var(--tap);
}
.ai-compose input::placeholder { color: var(--muted); }
.ai-compose .ghost-btn, .ai-compose button { min-height: var(--tap); flex: 0 0 auto; }

.ai-setup { margin-top: var(--sp-2); }
.ai-setup input, .ai-setup select {
  width: 100%; margin-top: var(--sp-2); background: var(--panel-alt); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-3); min-height: var(--tap);
}

/* The sheet: the attach picker and the "what will be sent" review share it,
   because they are two halves of one act and the second must feel like the
   continuation of the first rather than a different screen. */
.ai-sheet {
  position: absolute; inset: 0; z-index: 2;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  display: flex; align-items: flex-end; justify-content: center;
  padding: var(--sp-2);
}
.ai-sheet.hidden { display: none; }
.ai-sheet-card {
  width: 100%; max-height: 100%; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: var(--sp-3); box-shadow: var(--shadow-2);
}
.ai-sheet-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.ai-sheet-head b { flex: 1; }
/* ...but the sheet's own close belongs in the sheet, not flung up to the
   panel corner where it would land on top of the panel's close button. */
.ai-sheet-head .modal-close { position: static; flex: 0 0 auto; }
.ai-sheet-card input[type="text"] {
  width: 100%; background: var(--panel-alt); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-3); min-height: var(--tap);
}

.ai-pick-list { margin-top: var(--sp-2); max-height: 42vh; overflow-y: auto; }
.ai-pick-row {
  display: flex; flex-direction: column; gap: var(--sp-1); width: 100%;
  text-align: left; background: transparent; border: 1px solid transparent;
  border-radius: var(--r-md); padding: var(--sp-2) var(--sp-2); min-height: var(--tap);
  color: var(--text);
}
.ai-pick-row:hover { background: var(--panel-alt); border-color: var(--border); }
/* A note in this list is the user's own writing, so it is set in the
   notebook's voice, not the chrome's. */
.ai-pick-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--font-note); font-size: 0.86rem; }
.ai-pick-meta { color: var(--muted); font-size: 0.7rem; }

/* The review list. Deliberately plain and deliberately verbatim: this is the
   screen whose whole job is to be believed, so it shows the text at full
   length in the notebook's own face rather than a tidy summary of it. */
.ai-rev-new { margin: var(--sp-2) 0; }
.ai-rev-list { max-height: 34vh; overflow-y: auto; margin: var(--sp-2) 0 0; }
.ai-rev-prior > summary {
  cursor: pointer; color: var(--muted); font-size: 0.76rem; line-height: 1.5;
  padding: var(--sp-2) 0; min-height: var(--tap); display: list-item;
}
.ai-rev-prior > summary:hover { color: var(--text); }
.ai-rev-part {
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--sp-2); margin-bottom: var(--sp-2); background: var(--panel-alt);
}
.ai-rev-attached { border-left: 3px solid var(--accent-fill); background: var(--accent-subtle); }
.ai-rev-label {
  font-size: 0.66rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--accent); margin-bottom: var(--sp-1);
}
.ai-rev-text { white-space: pre-wrap; word-wrap: break-word; font-family: var(--font-note); font-size: 0.84rem; }

/* The row in the conversation list. Same shape as a contact's row so it does
   not read as an advert, one chip so it does not read as a person. */
.ai-thread-row .ai-avatar { background: var(--accent-fill); border-color: var(--accent); color: var(--on-accent); }
.ai-chip {
  font-size: 0.6rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--accent); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 0 var(--sp-1); flex: 0 0 auto;
}

/* ===========================================================================
   GROUPS, RICH MESSAGING, ONBOARDING

   Everything here is built out of the existing tokens — the 4px spacing
   scale, the radius scale, the two motion durations, the 44px tap floor, the
   safe-area insets. Nothing introduces a new colour, a new duration or a
   hard-coded pixel margin, because the appearance system's guarantee (an
   accent is contrast-fitted against the theme's own surfaces) only holds for
   rules that go through the tokens.

   Every transition below names its properties. `transition: all` is how the
   privacy veil ends up with a fade it must never have.
=========================================================================== */

/* ---- conversation list --------------------------------------------------- */
.thread-sub {
  display: block; color: var(--muted); font-size: 0.72rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.avatar-chip.group-avatar {
  background: color-mix(in srgb, var(--accent) 22%, var(--panel));
  border-color: var(--accent-dim);
  font-size: 0.95rem;
}
/* A long display name must not push the timestamp off the row, and an
   address-only title (64 hex) must not wrap to four lines. */
.thread-row b, .thread-snip, .thread-sub { min-width: 0; }
.thread-main { overflow: hidden; }

/* ---- the thread header --------------------------------------------------- */
/* The name takes a full line and the controls wrap under it — the fix from
   the interface pass, extended to the group-info button. */
#msg-thread-name {
  display: flex; align-items: center; gap: var(--sp-2);
  flex-wrap: wrap; width: 100%; min-width: 0;
}
.thread-name-text {
  font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 100%;
}
.thread-name-sub {
  color: var(--muted); font-size: 0.72rem; font-weight: 400;
  flex-basis: 100%;
}

/* ---- day and unread separators ------------------------------------------- */
/* A hairline with the label sitting in it. Built with a flex row and two
   grown rules rather than a background gradient, so it survives any theme's
   surface colour and any corner style. */
.day-sep, .unread-sep {
  display: flex; align-items: center; gap: var(--sp-3);
  margin: var(--sp-3) 0 var(--sp-2);
  font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase;
}
.day-sep::before, .day-sep::after,
.unread-sep::before, .unread-sep::after {
  content: ""; flex: 1; height: 1px; background: var(--border);
}
.day-sep span { color: var(--muted); }
.unread-sep::before, .unread-sep::after { background: var(--accent-dim); }
.unread-sep span {
  color: var(--on-accent); background: var(--accent-fill);
  border-radius: var(--r-sm); padding: 0.1rem var(--sp-2); font-weight: 600;
}

.thread-more {
  align-self: center; margin-bottom: var(--sp-2);
  background: var(--panel-alt); color: var(--muted);
  border: 1px solid var(--border); border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-4); font-size: 0.78rem;
}
.thread-more:hover { color: var(--text); }

/* ---- bubbles ------------------------------------------------------------- */
/* The wrap exists so a reaction row and the ⋯ button can sit beside/under a
   bubble without being inside it — a reaction inside the bubble inherits the
   outgoing bubble's fill and becomes unreadable. */
.bubble-wrap {
  display: flex; flex-direction: column; max-width: 88%;
  position: relative;
}
.bubble-wrap.in  { align-self: flex-start; align-items: flex-start; }
.bubble-wrap.out { align-self: flex-end;   align-items: flex-end; }
.bubble-wrap .bubble { max-width: 100%; }

/* Long words, pasted URLs and 64-hex keys must wrap rather than force the
   whole panel wider than the screen. `anywhere` rather than `break-all` so
   ordinary prose still breaks at spaces. */
.bubble-text { overflow-wrap: anywhere; white-space: pre-wrap; }

.bubble-sender {
  font-size: 0.72rem; font-weight: 600; color: var(--accent);
  margin-bottom: 0.1rem; overflow-wrap: anywhere;
}
.bubble.out .bubble-sender { color: inherit; opacity: 0.8; }
/* A name somebody gave themselves, marked as exactly that. Quiet, because it
   is the normal case in a group; present, because it is the difference
   between a name and a claim. */
.claimed-name { font-weight: 400; opacity: 0.75; font-size: 0.68rem; }

/* The quoted message above a reply. A button, because it jumps. */
.bubble-quote {
  display: flex; flex-direction: column; gap: 0.1rem; text-align: left;
  width: 100%; min-height: 0;
  border: none; border-left: 3px solid var(--accent-dim);
  background: color-mix(in srgb, var(--panel) 70%, transparent);
  border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-2);
  font-size: 0.76rem; line-height: 1.35;
}
.bubble.out .bubble-quote {
  background: color-mix(in srgb, #000 12%, transparent);
  border-left-color: color-mix(in srgb, currentColor 55%, transparent);
}
.quote-who { font-weight: 600; color: var(--accent); }
.bubble.out .quote-who { color: inherit; opacity: 0.85; }
.quote-text {
  color: var(--muted); overflow: hidden; text-overflow: ellipsis;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.bubble.out .quote-text { color: inherit; opacity: 0.8; }

/* A message that has not been accepted yet reads as provisional rather than
   as broken: same bubble, less committed. */
.bubble.sending { opacity: 0.62; }
.bubble.failed {
  border: 1px solid var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, var(--panel));
  color: var(--text);
}
.bubble .tick.warn { color: var(--danger); }
.bubble-retry {
  align-self: flex-end; margin-top: var(--sp-2);
  min-height: 32px; padding: 0 var(--sp-3); font-size: 0.74rem;
}

/* ---- reactions ----------------------------------------------------------- */
.reaction-row {
  display: flex; flex-wrap: wrap; gap: var(--sp-1);
  margin: calc(var(--sp-1) * -1) 0 var(--sp-1);
  padding: 0 var(--sp-2);
}
.reaction-chip {
  min-height: 26px; padding: 0 var(--sp-2);
  font-size: 0.76rem; line-height: 1;
  border-radius: 999px;
  border: 1px solid var(--border); background: var(--panel-alt); color: var(--text);
}
.reaction-chip.mine {
  border-color: var(--accent); background: var(--accent-subtle); color: var(--accent);
}

/* ---- the per-message affordance ------------------------------------------
   Always in the DOM, never hover-only: on a touch device a control that
   appears on hover does not exist. It is small and quiet, and it grows to a
   full 44px hit area through padding rather than through size. */
.bubble-more {
  position: absolute; top: 0; min-height: 0;
  width: 26px; height: 26px; padding: 0; line-height: 1;
  border-radius: 999px; font-size: 0.9rem;
  background: var(--panel-alt); color: var(--muted);
  border: 1px solid var(--border);
}
.bubble-wrap.in  .bubble-more { right: -6px; }
.bubble-wrap.out .bubble-more { left: -6px; }
.bubble-more:hover, .bubble-more:focus-visible { color: var(--text); }
/* This control was originally quietened with `opacity: .55`, which the
   appearance suite immediately failed at 2.2–3.2:1 against every theme. That
   is the right catch and the wrong fix would have been to exempt it: opacity
   is a contrast reduction wearing a costume, and the whole guarantee of the
   appearance system is that no combination of theme and accent can produce
   unreadable text. It is quiet now because it is SMALL and set in `--muted`,
   which fitForegrounds() holds at 4.5:1 in every theme — the hierarchy comes
   from size and weight before colour, which is rule 1 of the interface pass. */
@media (pointer: coarse) {
  /* Thumbs, not cursors: the visible dot stays 26px, the target is 44. */
  .bubble-more::after {
    content: ""; position: absolute; inset: calc((var(--tap) - 26px) / -2);
  }
}

/* ---- the message menu (a bottom sheet on a phone, a card on a desktop) ---- */
.overlay.sheet { align-items: flex-end; }
.sheet-card {
  width: 100%; max-width: 34rem; margin: 0 auto;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: var(--sp-4) calc(var(--sp-4) + var(--safe-r))
           calc(var(--sp-4) + var(--safe-b)) calc(var(--sp-4) + var(--safe-l));
  display: flex; flex-direction: column; gap: var(--sp-2);
  box-shadow: var(--shadow-2);
}
.sheet-card > button { justify-content: flex-start; text-align: left; }
.mm-preview {
  margin: 0 0 var(--sp-1); color: var(--muted); font-size: 0.8rem;
  overflow: hidden; text-overflow: ellipsis; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  border-left: 3px solid var(--border); padding-left: var(--sp-3);
}
.mm-emoji { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-2); }
.mm-emoji-btn {
  min-width: var(--tap); min-height: var(--tap);
  font-size: 1.3rem; padding: 0;
  border-radius: var(--r-md); background: var(--panel-alt); border: 1px solid var(--border);
}
.mm-emoji-btn:hover { background: var(--accent-subtle); }
@media (min-width: 700px) {
  .overlay.sheet { align-items: center; }
  .sheet-card { border-radius: var(--r-lg); }
}
@media (prefers-reduced-motion: no-preference) {
  .overlay.sheet:not(.hidden) .sheet-card { animation: sheetIn var(--dur-2) var(--ease-out); }
}
@keyframes sheetIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* ---- the "replying to" chip --------------------------------------------- */
.reply-chip {
  display: flex; align-items: center; gap: var(--sp-2);
  border-left: 3px solid var(--accent-dim);
  background: var(--panel-alt); border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-2);
  font-size: 0.78rem; min-width: 0;
}
.reply-chip .quote-text { flex: 1; min-width: 0; -webkit-line-clamp: 1; }
.quote-cancel {
  min-height: 32px; min-width: 32px; padding: 0; flex-shrink: 0;
  background: none; border: none; color: var(--muted); font-size: 1.1rem;
}
.quote-cancel:hover { color: var(--text); }

/* ---- groups -------------------------------------------------------------- */
.groups-list {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: var(--sp-2) var(--sp-3);
}
.groups-list .empty-state { grid-column: 1 / -1; }
.group-row { width: 100%; }
.group-manage { white-space: nowrap; }
.member-list { display: flex; flex-direction: column; gap: var(--sp-2); margin: var(--sp-2) 0; }
.member-row {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--panel-alt); min-width: 0;
}
.member-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.member-hash {
  font-family: var(--mono); font-size: 0.7rem; color: var(--muted);
  flex-shrink: 0;
}
@media (max-width: 460px) {
  /* On a narrow phone the address is the first thing to give up its line. */
  .member-row { flex-wrap: wrap; }
  .member-hash { flex-basis: 100%; }
}
.group-honesty { margin-top: var(--sp-4); }

/* ---- the gate's first-run explainer -------------------------------------- */
.gate-firstrun { margin-bottom: var(--sp-4); }
.gate-lead { font-size: 0.88rem; line-height: 1.6; margin: 0 0 var(--sp-3); }
.gate-facts {
  list-style: none; margin: 0 0 var(--sp-3); padding: 0;
  display: flex; flex-direction: column; gap: var(--sp-2);
}
.gate-facts li {
  font-size: 0.82rem; line-height: 1.55; color: var(--muted);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--panel-alt) 70%, transparent);
}
.gate-facts li b { color: var(--text); }
/* The one fact that costs somebody their notebook gets the danger treatment
   rather than being the second bullet of three identical ones. */
.gate-facts li:nth-child(2) {
  border-color: color-mix(in srgb, var(--danger) 45%, var(--border));
  background: color-mix(in srgb, var(--danger) 8%, transparent);
}
.gate-facts li:nth-child(2) b { color: var(--danger); }

/* ---- the tour ------------------------------------------------------------ */
.tour-panel { text-align: center; max-width: 30rem; }
.tour-glyph { font-size: 2.6rem; color: var(--accent); margin-bottom: var(--sp-2); }
.tour-body {
  font-size: 0.9rem; line-height: 1.65; color: var(--text);
  margin: var(--sp-3) 0 var(--sp-4); text-align: left;
}
.tour-dots { display: flex; justify-content: center; gap: var(--sp-2); margin-bottom: var(--sp-4); }
.tour-dot {
  width: 7px; height: 7px; border-radius: 999px;
  background: var(--border);
  transition: background-color var(--dur-1) var(--ease);
}
.tour-dot.on { background: var(--accent); }
.tour-actions { justify-content: center; }

/* ---- empty states, extended ---------------------------------------------- */
/* The feed's empty state is centred in a tall column; the ones inside a modal
   are not, and inheriting a 14vh top margin there looks like a bug. */
.empty-inbox, .empty-thread, .empty-inline-block { margin: var(--sp-5) auto 0; }
.empty-thread { margin-top: auto; margin-bottom: auto; }
.empty-actions { justify-content: center; margin-top: var(--sp-4); flex-wrap: wrap; }

/* ---- jump-to-message flash ----------------------------------------------- */
.bubble-wrap.flash .bubble {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .bubble-wrap.flash .bubble { animation: bubbleFlash 1.6s var(--ease); }
}
@keyframes bubbleFlash {
  0%, 100% { outline-color: transparent; }
  15%, 60% { outline-color: var(--accent); }
}

/* ===========================================================================
   FIXES FOUND BY OPENING THE SCREENSHOTS

   Three of these were invisible in the source and obvious in the picture,
   which is the whole argument for shooting the thing and looking at it.
=========================================================================== */

/* 1. THE GATE RENDERED ITS OWN INDENTATION.
      `.panel` sets `white-space: pre-wrap` — correct for the feed, where a
      note's own line breaks are the user's. The modal panels already opted
      out (see the `white-space: normal` rule above). The GATE panel never
      did, because until now it held one paragraph on one source line. The
      moment it held structured markup, every newline and every leading
      indent in index.html turned into visible whitespace mid-sentence. */
#gate .panel, #gate p, #gate li, #gate label, #gate .gate-firstrun {
  white-space: normal;
}

/* 2. THE PASSPHRASE FIELD WENT BELOW THE FOLD.
      `#gate` centres its panel and never scrolled, which was fine for a
      120px panel and puts the input off the bottom of a 420x880 phone once
      there is an explainer above it — with no way to reach it, because there
      was nothing to scroll. `safe center` centres while there is room and
      falls back to the start edge when there is not, so the top of the panel
      is always reachable; the overflow gives it somewhere to go. */
#gate {
  overflow-y: auto;
  align-items: safe center;
  padding: calc(var(--safe-t) + var(--sp-4)) calc(var(--sp-3) + var(--safe-r))
           calc(var(--safe-b) + var(--sp-4)) calc(var(--sp-3) + var(--safe-l));
}
/* Denser than the body copy default: this is a checklist to scan, not an
   essay to read, and every line of it costs the field below it. */
.gate-facts li { font-size: 0.78rem; line-height: 1.5; padding: var(--sp-2) var(--sp-3); }
.gate-lead { font-size: 0.84rem; line-height: 1.55; }
.gate-firstrun { margin-bottom: var(--sp-3); }

/* 3. THE ⋯ BUTTON SAT ON TOP OF THE FIRST WORD.
      It is absolutely positioned against the bubble wrap, and the wrap was
      exactly as wide as the bubble — so "outside the bubble" and "inside the
      wrap" were the same place, and the button covered the first character of
      every message. The wrap now reserves a gutter on the side the button
      lives on. */
.bubble-wrap { max-width: 92%; }
.bubble-wrap.in  { padding-right: 30px; }
.bubble-wrap.out { padding-left: 30px; }
.bubble-wrap.in  .bubble-more { right: 0; }
.bubble-wrap.out .bubble-more { left: 0; }

/* 4. A TRUNCATED ADDRESS WRAPPED ONTO A SECOND LINE.
      `…bbbbb / b` — one stray character on its own row, in every member row.
      The ellipsised form is a fixed 17 characters and should never wrap. */
.member-hash { white-space: nowrap; }
/* And the add-member row was three controls fighting over 320px, which
   clipped the placeholder that tells you what to type. Two rows on a phone,
   one on anything wider. */
.add-member-row { flex-wrap: wrap; }
.add-member-row input { flex: 1 1 12rem; min-width: 0; }
.add-member-row button { flex: 0 0 auto; }
.tour-step { text-align: center; margin: 0 0 var(--sp-3); }
/* The group-info button carries the member count now, so it can be wider than
   the other header controls and must not squash them. */
#msg-group-info { flex: 0 1 auto; min-width: 0; }

/* ===========================================================================
   Storage shield — the data-safety indicator, banner and sheet.

   The dot is a ::after on the header button so the state survives any future
   change of glyph, and the colours resolve against theme tokens where a token
   exists (warn = accent, risk = danger). "ok" gets its own green because no
   theme defines a success colour, and it must read as "fine" on the light
   themes too — hence the darker fallback mixed toward the theme's own text.
=========================================================================== */
:root {
  --shield-ok: #4e9d68;
  --shield-warn: var(--accent);
  --shield-risk: var(--danger);
}
#shield-btn { position: relative; }
#shield-btn::after {
  content: "";
  position: absolute;
  top: 5px; right: 5px;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--muted);
  /* A 1px ring of the panel colour keeps the dot legible when it sits on an
     emoji rather than beside it. */
  box-shadow: 0 0 0 2px var(--panel);
}
#shield-btn.shield-ok::after   { background: var(--shield-ok); }
#shield-btn.shield-warn::after { background: var(--shield-warn); }
#shield-btn.shield-risk::after { background: var(--shield-risk); }

/* The banner is quiet on purpose: it is persistent, so it must be livable —
   an alarm-red strip that never leaves would train people to ignore warnings,
   which is the opposite of its job. */
#shield-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 0.45rem 0.75rem;
  background: var(--accent-subtle);
  border-bottom: 1px solid var(--border);
  font-size: 0.78rem;
  color: var(--text);
  flex: 0 0 auto;
}
#shield-banner.hidden { display: none; }

/* ===========================================================================
   The messaging flag (CIPHERJOT_MESSAGING, config.js). app.js puts
   `no-messaging` on <html> before first paint when the flag is off; this one
   rule is the whole cosmetic half. The functional half is the guard inside
   every messaging choke point in app.js — hiding a button is not a control,
   refusing to poll is.
=========================================================================== */
html.no-messaging .msg-only { display: none !important; }

/* ===========================================================================
   Recovery kit. The canvas is drawn at print resolution (1000×1414, the A-series
   ratio) and scaled down for the preview; it is deliberately paper-coloured in
   every theme, because its destination is a printer, and a dark card is a
   wasted ink cartridge and an unreadable photocopy.
=========================================================================== */
#kit-canvas {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-sm, 6px);
  background: #f7f4ec;
}

/* ===========================================================================
   Checklists — `- [ ]` in any note is a live checkbox. The box is an inline
   control inside prose, so it deliberately opts out of the big-button rules:
   its hit area is padded, not sized, and its feedback is the glyph itself.
=========================================================================== */
.todo-box {
  background: none;
  border: none;
  padding: 0.1em 0.3em;
  margin: 0 0 0 -0.3em;
  font: inherit;
  font-size: 1.15em;
  line-height: 1;
  color: var(--accent);
  cursor: pointer;
  min-height: 0;
  vertical-align: -0.05em;
}
.todo-box.done { color: var(--muted); }
.todo-box:hover { color: var(--accent-hover); }
.todo-done { color: var(--muted); text-decoration: line-through; }

/* ===========================================================================
   Pinned notes — the sticky strip above the feed. Chips scroll horizontally
   on a phone rather than wrapping into a second header.
=========================================================================== */
#pin-strip {
  display: flex;
  gap: 0.5rem;
  overflow-x: auto;
  padding: 0.45rem 0.75rem;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
  flex: 0 0 auto;
  scrollbar-width: none;
}
#pin-strip::-webkit-scrollbar { display: none; }
#pin-strip.hidden { display: none; }
.pin-chip {
  flex: 0 0 auto;
  max-width: 70vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.78rem;
  padding: 0.3rem 0.6rem;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: var(--r-sm, 6px);
  color: var(--text);
  cursor: pointer;
}
.pin-chip:hover { border-color: var(--accent-dim); }
/* An unpinned pin is an OFFER, not a state — it should whisper. The emoji is
   desaturated so seven notes in a row do not read as seven red pushpins;
   pinning restores full colour, which is the state change doing its own
   announcing. */
.pin-btn { opacity: 0.3; filter: grayscale(1); }
.pin-btn:hover { opacity: 0.7; filter: grayscale(0.5); }
.pin-btn.pinned { opacity: 1; filter: none; color: var(--accent); }

/* ===========================================================================
   Passphrase strength (vault creation only). A meter, not a gatekeeper: the
   app never refuses a passphrase — it makes the cost of a weak one legible
   and asks once. Colours ride the shield tokens so "weak" reads as the same
   red family everywhere danger does.
=========================================================================== */
#gate-strength { margin: 0.5rem 0 0.2rem; }
.strength-track {
  height: 6px;
  border-radius: 3px;
  background: var(--panel-alt);
  border: 1px solid var(--border);
  overflow: hidden;
}
#strength-fill {
  height: 100%;
  width: 0;
  border-radius: 3px;
  background: var(--shield-risk);
  transition: width var(--dur-2, 170ms) ease, background var(--dur-2, 170ms) ease;
}
#gate-strength.s1 #strength-fill { width: 25%; background: var(--shield-risk); }
#gate-strength.s2 #strength-fill { width: 50%; background: var(--shield-warn); }
#gate-strength.s3 #strength-fill { width: 75%; background: var(--shield-warn); }
#gate-strength.s4 #strength-fill { width: 100%; background: var(--shield-ok); }
#gate-strength .hint { margin-top: 0.35rem; }

/* ===========================================================================
   THE WORLD-CLASS PASS.

   1 — The gate. The first screen anyone ever sees leads with the mark and
   the field; the teaching stays visible on first run (a disclosure behind a
   click is a disclosure skipped) but stops shouting: the boxes are gone, an
   accent hairline carries the list, and a returning user gets the mark, one
   quiet line, and their field.
=========================================================================== */
#gate { display: flex; align-items: center; justify-content: center; }
#gate .panel { border-style: solid; }
.gate-brand { text-align: center; margin-top: 0.4rem; }
.gate-glyph { font-size: 2.6rem; color: var(--accent); line-height: 1; }
.gate-name {
  text-align: center;
  font-size: 1.5rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text);
  margin: 0.5rem 0 1.1rem;
  font-weight: 700;
}
.gate-returning { text-align: center; color: var(--muted); font-size: 0.9rem; margin: 0 0 1rem; }
.gate-facts { list-style: none; padding: 0; margin: 0.8rem 0; }
.gate-facts li {
  border: none !important;
  border-left: 2px solid var(--accent-dim) !important;
  border-radius: 0 !important;
  background: transparent !important;
  padding: 0.15rem 0 0.15rem 0.8rem !important;
  margin: 0.55rem 0;
}
#gate-input {
  width: 100%;
  text-align: center;
  letter-spacing: 0.08em;
  padding: 0.8rem 0.9rem;
  font-size: max(16px, 1.05rem);
}
#gate-input:focus { border-color: var(--accent); outline: none; }
#gate-submit { width: 100%; margin-top: 0.6rem; padding: 0.7rem; font-size: 1rem; }

/* 2 — Note actions surface on INTENT. At rest the feed is words and dates.
   Desktop: hover or keyboard focus raises them. Touch: swipe a note left
   (wireNotebook) and its tray appears; swipe right or tap elsewhere puts it
   away. A pinned pin stays faintly visible everywhere — it is state, not an
   action, and state does not hide. */
@media (hover: hover) and (pointer: fine) {
  .panel .entry-btn { opacity: 0; transition: opacity var(--dur-1, 90ms) ease; }
  .panel:hover .entry-btn, .panel:focus-within .entry-btn { opacity: 0.55; }
  .panel:hover .pin-btn:not(.pinned), .panel:focus-within .pin-btn:not(.pinned) { opacity: 0.35; }
  .panel .entry-btn:hover { opacity: 1; }
  .panel .pin-btn.pinned { opacity: 0.85; }
  .panel:hover .pin-btn.pinned { opacity: 1; }
}
@media (pointer: coarse) {
  .panel .entry-btn { opacity: 0; pointer-events: none; transition: opacity var(--dur-2, 170ms) ease, transform var(--dur-2, 170ms) ease; transform: translateX(6px); }
  .panel.actions-open .entry-btn { opacity: 0.8; pointer-events: auto; transform: none; }
  .panel.actions-open .pin-btn.pinned { opacity: 1; }
  .panel .pin-btn.pinned { opacity: 0.7; transform: none; }
}

/* 3 — The composer grows in place. Enter saves, Shift+Enter breaks the
   line; the field rises with the thought instead of jumping to a modal. */
#msg-input {
  resize: none;
  overflow-y: auto;
  max-height: 9.5rem;
  line-height: 1.45;
}
footer form { align-items: flex-end; }

/* 4 — One date, one day. The feed groups consecutive notes under a single
   day line instead of stamping every panel with its own date chip. */
.day-sep {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  margin: 1.3rem 0 0.5rem;
  color: var(--muted);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.day-sep::before, .day-sep::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--border);
}

/* 5 — Sheets arrive, they do not snap. The palette's entrance, everywhere,
   collapsing under reduced motion exactly as the motion rules demand. */
.modal .modal-panel { animation: overlayIn var(--dur-2, 170ms) ease; }
@media (prefers-reduced-motion: reduce) {
  .modal .modal-panel { animation: none; }
}
