/* ============================================================
   DeuelLab Notebook — application stylesheet.

   Loaded after design/tokens.css, which owns every colour, size and
   density value. Nothing here invents a hex except the two ramps
   declared at the top, both of which are justified in place.
   ============================================================ */

/* ---- fonts -------------------------------------------------
   Self-hosted, never the Google CDN: it transmits visitor IPs to a
   third party. Files come from design/fonts/fetch-fonts.sh; if they
   are absent the stack falls back to system-ui and the interface is
   merely less tight, not broken.
   ------------------------------------------------------------ */

@font-face {
  font-family: "Fira Sans";
  src: url("../design/fonts/woff2/FiraSans-Regular.05f26db4e8fd.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Fira Sans";
  src: url("../design/fonts/woff2/FiraSans-Italic.9a68d78532b5.woff2") format("woff2");
  font-weight: 400; font-style: italic; font-display: swap;
}
@font-face {
  font-family: "Fira Sans";
  src: url("../design/fonts/woff2/FiraSans-Medium.7342c1ed135b.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Fira Sans";
  /* The scale asks for 650 in places. Fira has no 650 — it maps here, to
     600 SemiBold. The browser must never synthesise the missing weight. */
  src: url("../design/fonts/woff2/FiraSans-SemiBold.bf7123c08d0a.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("../design/fonts/woff2/JetBrainsMono[wght].570921a7dc7c.woff2") format("woff2");
  font-weight: 100 800; font-style: normal; font-display: swap;
}

:root {
  /* ---- interface danger ------------------------------------
     Deliberately NOT --retracted. That token means "PubMed lists a
     retraction notice for this paper" — a scientific fact. A failed
     password field is an interface state. Sharing one token would
     make the loudest colour in the system mean two unrelated things,
     and the retraction badge is the one that must never be routine.
     UZH Berry, which clears 6.5:1 on white.
     ------------------------------------------------------------ */
  --danger:      #BF0D3E;
  --danger-dim:  #FBC6D4;

  /* ---- identity --------------------------------------------
     Avatar discs. Also not the semantic tokens: if --signed also
     means "Anna", it reliably means neither. All six carry white
     text at 4.5:1 or better, and they are theme-independent by
     design — a saturated fill reads on either ground.
     ------------------------------------------------------------ */
  --id-blue:   #0028A5;
  --id-berry:  #8F0A2E;
  --id-apple:  #536B18;
  --id-cyan:   #147082;
  --id-orange: #7E2601;
  --id-slate:  #4D4D4D;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --danger:     #F3537F;
    --danger-dim: #3D0417;
  }
}
:root[data-theme="dark"] {
  --danger:     #F3537F;
  --danger-dim: #3D0417;
}

/* ============================================================
   Reset and base
   ============================================================ */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-2); }

/* Native <button> chrome (grey buttonface, 3D outset border) was never
   reset here, so any button relying only on this base rule — the toolbar's
   icon buttons, .icon-btn — rendered with the browser's own default button
   styling instead of the app's flat/modern look. .btn and its variants set
   their own background/border already, so this is additive there, not a
   behaviour change. */
button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

code, kbd, samp, .mono { font-family: var(--font-mono); }

/* ============================================================
   The lockup — university mark, hairline, group name.
   Institution first. Appears once in the shell, never on a page.
   ============================================================ */

.lockup {
  display: flex;
  align-items: center;
  gap: 0.62rem;
  font-weight: 600;
  font-size: 0.9rem;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.lockup img,
.lockup svg { height: 23px; width: auto; display: block; }
.lockup .bar { width: 1px; height: 18px; background: var(--rule); flex: none; }
@media (max-width: 900px) { .lockup img, .lockup svg { height: 20px; } }

/* The mark is a single path with no fill attribute, so it inherits.
   Loaded via <img> it cannot inherit, so dark mode inverts it instead. */
:root[data-theme="dark"] .lockup img,
:root:not([data-theme="light"]) .lockup img { filter: invert(1) brightness(1.6); }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .lockup img { filter: none; }
}

/* ============================================================
   Auth pages — a single sheet on the desk
   ============================================================ */

.auth {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.4rem;
  padding: 2rem 1rem 4rem;
  background: var(--ground);
}

.auth__sheet {
  width: 100%;
  max-width: 27rem;
  background: var(--surface);
  border-radius: var(--radius-page);
  box-shadow: var(--shadow-page);
  padding: 2rem 2.1rem 2.2rem;
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}

.auth__sheet--wide { max-width: 34rem; }

.auth h1 {
  margin: 0;
  font-size: var(--text-entry-title);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.auth p {
  margin: 0;
  font-size: var(--text-ui);
  line-height: var(--leading-ui);
  color: var(--ink-2);
}

.auth__foot {
  font-size: var(--text-ui);
  color: var(--ink-3);
  text-align: center;
  max-width: 27rem;
}

/* ============================================================
   Forms
   ============================================================ */

.field { display: flex; flex-direction: column; gap: 0.3rem; }
/* `display: flex` above outranks the UA's [hidden] { display: none }, so a
   `.hidden = true` on one of these containers would otherwise do nothing —
   passkeys.js hides the password field and a button row that way. */
.field[hidden], .btn-row[hidden] { display: none; }

.field > label,
.field__label {
  font-size: var(--text-label);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="number"],
.field input[type="tel"],
.field select,
.field textarea {
  width: 100%;
  font: inherit;
  font-size: var(--text-ui);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.5rem 0.6rem;
  transition: border-color 0.12s, box-shadow 0.12s;
}
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--ink-3); }

/* ---- filter bar ------------------------------------------------
   The "search + a couple of narrow selects + Filter/Clear" row above
   every library table. It lived in samples.css and, byte-identically,
   in tasks.css, whose own comment admitted the copy ("tasks.css carries
   its own copy of the same rules rather than pulling in an unrelated
   app's stylesheet") — the reason that argument never worked is that
   app.css is the stylesheet every page already loads, so this is where
   a shared component belongs. */

.filter-bar {
  display: flex;
  gap: 0.6rem;
  flex-wrap: wrap;
  align-items: flex-end;
}
.filter-bar .field { flex: none; min-width: 9rem; }
.filter-bar .field--grow { flex: 1; min-width: 12rem; }

/* additional_specs.md: "choose 'my Team' as default (should be
   configurable by the user)" — a small, quiet second control, never
   competing visually with the actual search/filter form above it. */
.filter-bar--default {
  margin-top: -0.3rem;
  gap: 0.4rem;
}
.filter-bar--default .field { min-width: 0; }
.filter-bar--default label { font-size: 0.7rem; color: var(--ink-3); }
.filter-bar--default select {
  font-size: 0.76rem;
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink-2);
}

/* A checkbox and its label on one line. Also formerly duplicated —
   tasks.css and sequences.css each defined it, and samples/item_detail
   .html uses the class while loading neither, so the "link a plasmid to
   a new sequence" form's Circular checkbox rendered unstyled. */
.field--checkbox {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--text-ui);
  color: var(--ink-2);
  cursor: pointer;
}
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-wash);
  outline: none;   /* replaced by the ring above, not removed */
}
.field--error input { border-color: var(--danger); }

/* A six-digit code is data, not prose. Mono, spaced, tabular. */
.field input[name$="token"],
.field input[name$="otp_token"],
.field input[inputmode="numeric"] {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 1.1rem;
  letter-spacing: 0.28em;
  text-align: center;
  padding-inline: 0.4rem;
}

/* two_factor renders the code as an IntegerField, so browsers add spinner
   arrows. Nobody nudges a one-time code up by one. */
.field input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
.field input[type="number"]::-webkit-outer-spin-button,
.field input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* static/js/date_today.js wraps every <input type="date"> / type=
   "datetime-local"> in one of these and appends a "Today" button as a
   sibling, so this has to work whether or not the input's ancestor is
   .field (cellines/forms.py's DateTimeInput widget isn't guaranteed one).
   `flex: 1` (flex-basis: 0%) governs the input's main-axis size
   independently of its `width` -- mobile/30-sheet.css sets
   `.field input[type="date"] { width: 100% }` at phone width, and that
   still applies (width feeds intrinsic sizing elsewhere) without winning
   a specificity fight against this rule, because the two rules set
   different properties. */
.date-today-wrap {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.date-today-wrap input[type="date"],
.date-today-wrap input[type="datetime-local"] {
  flex: 1;
  min-width: 0;
}
.btn--today {
  flex: none;
  padding: 0.3rem 0.6rem;
  font-size: var(--text-label);
}

.field__help { font-size: 0.78rem; color: var(--ink-3); line-height: 1.45; }
.field__error { font-size: 0.78rem; color: var(--danger); font-weight: 500; }

.form-stack { display: flex; flex-direction: column; gap: 0.95rem; }

/* ============================================================
   Buttons
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  font-size: var(--text-ui);
  font-weight: 600;
  padding: 0.5rem 0.9rem;
  border-radius: var(--radius);
  border: 1px solid var(--rule);
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.btn:hover { background: var(--surface-2); color: var(--ink); }
.btn[disabled] { opacity: 0.45; cursor: not-allowed; }

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #FFFFFF;
}
.btn--primary:hover { background: var(--accent-2); border-color: var(--accent-2); color: #FFFFFF; }

.btn--quiet { border-color: transparent; background: none; color: var(--ink-3); }
.btn--quiet:hover { background: var(--surface-2); color: var(--ink); }

.btn--danger { border-color: var(--danger); color: var(--danger); }
.btn--danger:hover { background: var(--danger-dim); color: var(--danger); }

.btn-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
}
.btn-row--split { justify-content: space-between; }

/* ============================================================
   Messages — colour plus icon plus word, never colour alone
   ============================================================ */

.msg {
  display: flex;
  gap: 0.55rem;
  align-items: flex-start;
  padding: 0.65rem 0.8rem;
  border-radius: var(--radius);
  font-size: var(--text-ui);
  line-height: var(--leading-ui);
}
.msg[hidden] { display: none; }
.msg svg { width: 15px; height: 15px; flex: none; margin-top: 0.18rem; }
.msg--info      { background: var(--accent-wash);   color: var(--accent); }
.msg--success   { background: var(--signed-dim);    color: var(--signed); }
.msg--warning   { background: var(--unverified-dim); color: var(--unverified); }
.msg--error     { background: var(--danger-dim);    color: var(--danger); }
.msg--retracted { background: var(--danger-dim);    color: var(--danger); }

/* ============================================================
   Enrolment: QR, secret, backup tokens
   ============================================================ */

.qr {
  display: grid;
  place-items: center;
  padding: 0.9rem;
  /* The QR is black modules on white and stays that way in dark mode —
     inverting it is a scan failure, not a style choice. */
  background: #FFFFFF;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.qr img { display: block; width: 190px; height: 190px; image-rendering: pixelated; }

.secret {
  font-family: var(--font-mono);
  font-size: var(--text-data);
  word-break: break-all;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.55rem 0.65rem;
  color: var(--ink-2);
}

.tokens {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-data);
}
.tokens li {
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.35rem 0.5rem;
  text-align: center;
  color: var(--ink-2);
}

/* ============================================================
   Chips — a state is a colour AND a shape AND a word
   ============================================================ */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.28rem;
  font-size: 0.7rem;
  font-weight: 600;
  padding: 0.16rem 0.42rem;
  border-radius: 3px;
  white-space: nowrap;
}
.chip svg { width: 0.9em; height: 0.9em; flex: none; }
.chip--draft   { background: var(--surface-2); color: var(--ink-2); border: 1px solid var(--rule); }
.chip--sealed  { background: var(--unverified-dim); color: var(--unverified); }
.chip--signed  { background: var(--signed-dim); color: var(--signed); }
.chip--unver   { background: var(--unverified-dim); color: var(--unverified); }
.chip--agent   { background: var(--agent-dim); color: var(--agent); }

/* ============================================================
   Avatars
   ============================================================ */

.avatar {
  width: 26px; height: 26px;
  border-radius: 50%;
  display: grid; place-items: center;
  font-size: 0.62rem; font-weight: 700;
  color: #FFFFFF;
  flex: none;
  user-select: none;
  /* An uploaded photo is layered *inside* the same disc rather than
     replacing it, so a <span> monogram and a <button> avatar in the topbar
     take one identical rule. The colour stays underneath as the fallback
     while the image loads or if it 404s. */
  position: relative;
  overflow: hidden;
}
.avatar__img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: 50%;
}
/* The profile page's own preview, big enough to judge a crop by. */
.avatar--lg { width: 72px; height: 72px; font-size: 1.5rem; }
.avatar--blue   { background: var(--id-blue); }
.avatar--berry  { background: var(--id-berry); }
.avatar--apple  { background: var(--id-apple); }
.avatar--cyan   { background: var(--id-cyan); }
.avatar--orange { background: var(--id-orange); }
.avatar--slate  { background: var(--id-slate); }
/* A BioArt illustration, not a photo: pale watercolour on a transparent
   ground, which vanishes on a saturated identity fill. Last of the
   .avatar--* rules on purpose — same specificity as the six colours, so
   source order is what decides, and this must win. */
.avatar--art { background: var(--surface-2); }
/* The profile picker's swatch size. Big enough to tell a monocyte from a
   macrophage, small enough that all 88 fit without a scroll box. */
.avatar--pick { width: 40px; height: 40px; font-size: 0.8rem; }
/* In the picker the disc *is* the <img>, not a box with one inside it, so
   it needs the fit rule .avatar__img would otherwise have carried. contain,
   not cover: the illustrations already ship with their own margin baked in
   (scripts/fetch_bioart_avatars.py), and cover would crop it back off. */
img.avatar { object-fit: contain; }
/* The initials over a BioArt illustration. `position: relative` (not just
   a plain text node) is load-bearing: `.avatar__img` is absolutely
   positioned, and CSS paints positioned descendants after in-flow ones
   regardless of source order, so without this the picture would always
   paint over the initials once it loads. `ink` is chosen per illustration
   by scripts/fetch_bioart_avatars.py from that image's own pixels, not
   guessed from its colour name — these are real watercolour fills of
   varying lightness, not uniformly pale ones. Fixed shades rather than the
   theme's --ink tokens: those are calibrated against the page background,
   and this has to contrast the artwork instead, which does not change
   with the theme. */
/* 1.2em (not a fixed size): the span inherits .avatar's font-size, so one
   rule enlarges the monogram by a fifth on the 26px disc, the 40px picker
   swatch and the 72px profile preview alike. The halo is a text-shadow ring
   rather than -webkit-text-stroke + paint-order, which Safari still paints
   over the glyph instead of behind it; eight offsets, because four leaves
   visible notches on the diagonals, and em rather than px so the ring keeps
   its weight relative to the letter at all three sizes. Each ink shade gets
   the opposite halo — white behind the dark letters, near-black behind the
   light ones — so both read over a mid-tone watercolour. */
.avatar__initials {
  position: relative;
  font-size: 1.2em;
  text-shadow:
    -0.08em -0.08em 0 var(--avatar-halo), 0 -0.08em 0 var(--avatar-halo), 0.08em -0.08em 0 var(--avatar-halo),
    -0.08em 0 0 var(--avatar-halo), 0.08em 0 0 var(--avatar-halo),
    -0.08em 0.08em 0 var(--avatar-halo), 0 0.08em 0 var(--avatar-halo), 0.08em 0.08em 0 var(--avatar-halo);
}
.avatar__initials--dark  { --avatar-halo: rgba(255, 255, 255, 0.85); color: rgba(0, 0, 0, 0.78); }
.avatar__initials--light { --avatar-halo: rgba(0, 0, 0, 0.55); color: rgba(255, 255, 255, 0.92); }

/* ============================================================
   Application shell — one thin bar, and it does not compete
   ============================================================ */

.shell { min-height: 100vh; display: flex; flex-direction: column; background: var(--ground); }

.topbar {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0 0.9rem;
  height: 46px;
  flex: none;
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
}
.topbar__right { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }

.topbar__search {
  margin-left: 0.6rem;
  flex: 1;
  max-width: 22rem;
  background: var(--surface-2);
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 0.32rem 0.6rem;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--ink-3);
}
.topbar__search:focus-within { border-color: var(--accent); background: var(--surface); }
.topbar__search svg { width: 14px; height: 14px; flex: none; }
.topbar__search input {
  border: 0; background: none; font: inherit; font-size: var(--text-ui);
  color: var(--ink); width: 100%; outline: none;
}
.topbar__search input::placeholder { color: var(--ink-3); }

.icon-btn {
  width: 28px; height: 28px;
  border-radius: var(--radius);
  display: grid; place-items: center;
  color: var(--ink-3);
  flex: none;
}
.icon-btn:hover { background: var(--surface-2); color: var(--ink); }
.icon-btn svg { width: 16px; height: 16px; }
/* An icon button that is a toggle currently in its "on" position (the
   docbar star). Colour, not a background, so it reads as state on the
   glyph itself rather than as a hover that got stuck. */
.icon-btn--on { color: var(--accent); }
.icon-btn--on:hover { color: var(--accent); }

/* fixes.md: Security/Admin/Sign out live under the initials avatar now,
   not as loose buttons in the topbar — same floating-panel look as
   .swatch-popup (position: fixed, toggled by static/js/topbar.js). */
.user-menu { position: relative; }
.user-menu > .avatar { border: 0; cursor: pointer; }
.user-menu > .avatar:hover, .user-menu > .avatar:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.user-menu__popup {
  position: fixed;
  z-index: 65;
  display: none;
  flex-direction: column;
  min-width: 12rem;
  padding: 0.35rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  box-shadow: var(--shadow-pop);
}
.user-menu__popup.is-open { display: flex; }
.user-menu__name {
  margin: 0.15rem 0.5rem 0.35rem;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--rule);
  font-weight: 600;
  font-size: var(--text-ui);
  color: var(--ink);
}
.user-menu__popup a,
.user-menu__popup button {
  display: block;
  width: 100%;
  padding: 0.4rem 0.5rem;
  border-radius: var(--radius);
  color: var(--ink-2);
  font: inherit;
  font-size: var(--text-ui);
  text-align: left;
  text-decoration: none;
  background: none;
  border: 0;
  cursor: pointer;
}
.user-menu__popup a:hover,
.user-menu__popup button:hover { background: var(--surface-2); color: var(--ink); }
.user-menu__popup form { margin: 0; }

.shell__body { flex: 1; padding: 2rem 1rem 4rem; }

/* ============================================================
   Left sidebar — fixes.md: "make more use of the left menu."
   A fixed column, sibling to .shell__body (see _sidebar.html for why it's
   a sibling include rather than nested markup) — :has() lets the body's
   margin react to the sidebar's mere presence without every template
   needing its own layout class.
   ============================================================ */

/* .notes-layout (notes/shell.html's rail+canvas row) needs the same
   offset as .shell__body — both are .sidebar's flow-sibling on their
   respective pages, and .sidebar being position:fixed takes it out of
   flow entirely, so without this its first ~208px sits UNDER the fixed
   sidebar (invisible, and — worse — unclickable, since the sidebar still
   paints and hit-tests above static content there). */
.shell:has(.sidebar) .shell__body,
.shell:has(.sidebar) .notes-layout { margin-left: 208px; }

.sidebar {
  position: fixed;
  top: 47px; /* .topbar's height + its 1px border */
  left: 0;
  bottom: 0;
  width: 208px;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 0.9rem 0.7rem;
  background: var(--surface);
  border-right: 1px solid var(--rule);
  overflow-y: auto;
}

.sidebar__nav { list-style: none; margin: 0.4rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.1rem; }

.sidebar__group-label {
  margin: 0.9rem 0.5rem 0.2rem;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-3);
}

.sidebar__nav a {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.42rem 0.55rem;
  border-radius: var(--radius);
  color: var(--ink-2);
  font-size: var(--text-ui);
  text-decoration: none;
}
.sidebar__nav a svg { width: 16px; height: 16px; flex: none; }
.sidebar__nav a:hover { background: var(--surface-2); color: var(--ink); }
.sidebar__nav a.is-active { background: var(--accent-wash); color: var(--accent); font-weight: 600; }

.sidebar__toc { margin-top: 0.6rem; padding-top: 0.6rem; border-top: 1px solid var(--rule); }
.sidebar__toc ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.05rem; }
.sidebar__toc a {
  display: block;
  padding: 0.3rem 0.55rem;
  border-radius: var(--radius);
  color: var(--ink-3);
  font-size: 0.78rem;
  text-decoration: none;
  line-height: 1.3;
}
.sidebar__toc a:hover { background: var(--surface-2); color: var(--ink); }
.sidebar__toc a[data-level="3"] { padding-left: 1.1rem; }

.sidebar__recent { margin-top: 0.6rem; padding-top: 0.6rem; border-top: 1px solid var(--rule); }
.sidebar__recent ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.05rem; }
.sidebar__recent a {
  display: block;
  padding: 0.3rem 0.55rem;
  border-radius: var(--radius);
  color: var(--ink-3);
  font-size: 0.78rem;
  text-decoration: none;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sidebar__recent a:hover { background: var(--surface-2); color: var(--ink); }
.sidebar__recent a.is-active { color: var(--accent); font-weight: 600; }

@media (max-width: 900px) {
  /* Renamed from 860px to the mobile layer's shared 900px compact
     breakpoint (css/mobile/00-contract.css) so this and the drawer's own
     media query flip in lockstep — they used to disagree by 40px, which
     would have left a 208px gutter of dead space between them with no
     visible sidebar in it.

     The sidebar itself no longer disappears here: css/mobile/10-shell.css
     (loaded after this file, and after every theme) turns it into an
     off-canvas drawer instead — position:fixed, translateX, an .is-open
     toggle driven by static/js/mobile_nav.js — so a phone user still has
     the left menu, just off-canvas rather than a column. What still
     belongs in this file is what does NOT change with the drawer:
     because the drawer is off-canvas (out of flow, slid in over the
     content) rather than a column that takes space, .shell__body and
     .notes-layout still need their 208px offset collapsed to zero —
     there's nothing left in the flow to make room for. */
  .shell:has(.sidebar) .shell__body,
  .shell:has(.sidebar) .notes-layout { margin-left: 0; }
}

.sheet {
  background: var(--surface);
  max-width: 38rem;
  margin: 0 auto;
  padding: 2.2rem 2.4rem 2.6rem;
  border-radius: var(--radius-page);
  box-shadow: var(--shadow-page);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.sheet h1 {
  margin: 0;
  font-size: var(--text-entry-title);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
.sheet h2 {
  margin: 0.6rem 0 0;
  font-size: var(--text-body-h);
  font-weight: 600;
  letter-spacing: -0.01em;
}
.sheet p { margin: 0; color: var(--ink-2); max-width: var(--measure); }
.sheet--wide { max-width: 72rem; }

/* ---------------------------------------------------------------------
   Dashboard (templates/projects/dashboard.html). Four stacked sections —
   starred notes, the current week, outstanding tasks, project cards — all
   built from the tokens in design/tokens.css and nothing else.
   --------------------------------------------------------------------- */
.dash-section { display: flex; flex-direction: column; gap: 0.55rem; }
/* A heading with a right-aligned "more" link on the same baseline. */
.dash-section__head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.dash-section__head h2 { margin: 0.6rem 0 0; }
.dash-section__more { font-size: var(--text-data); text-decoration: none; color: var(--accent); flex: none; }
.dash-section__more:hover { text-decoration: underline; }

/* Starred notes — a list, not cards: a hand-picked handful must not
   outweigh the project grid below it. */
.starred-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.starred-list__row {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.4rem 0; border-bottom: 1px solid var(--rule-soft);
}
.starred-list__row:last-child { border-bottom: 0; }
.starred-list__title {
  font-weight: 600; color: var(--accent); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.starred-list__title:hover { text-decoration: underline; }
.starred-list__meta { font-size: var(--text-data); color: var(--ink-3); flex: none; margin-left: auto; }
.starred-list__unstar { flex: none; display: flex; }

/* The current week: seven equal columns that fall back to a wrapping
   grid on a narrow window rather than scrolling sideways. */
.dash-week__range { font-size: var(--text-data); }
.dash-week {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.4rem;
}
@media (max-width: 60rem) {
  .dash-week { grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); }
}
.dash-week__day {
  display: flex; flex-direction: column; gap: 0.3rem;
  min-height: 6rem; padding: 0.5rem;
  border: 1px solid var(--rule); border-radius: var(--radius);
  background: var(--surface-3);
}
.dash-week__day--today { border-color: var(--accent-dim); background: var(--accent-wash); }
.dash-week__label { display: flex; align-items: baseline; gap: 0.35rem; }
.dash-week__weekday {
  font-size: var(--text-label); text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-3);
}
.dash-week__date { font-size: var(--text-data); font-weight: 600; color: var(--ink-2); }
.dash-week__day--today .dash-week__date { color: var(--accent); }
.dash-week__holiday {
  font-size: var(--text-label); padding: 0.15rem 0.3rem; border-radius: var(--radius);
  background: var(--unverified-dim); color: var(--unverified);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dash-week__task {
  font-size: var(--text-data); line-height: 1.25;
  padding: 0.2rem 0.35rem; border-radius: var(--radius);
  text-decoration: none; color: var(--ink); background: var(--surface-2);
  border-left: 3px solid var(--rule);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dash-week__task:hover { background: var(--accent-dim); }
/* Status stripe, same vocabulary the task list uses. */
.dash-week__task--in_execution { border-left-color: var(--accent); }
.dash-week__task--planned { border-left-color: var(--ink-3); }
.dash-week__task--done_success { border-left-color: var(--signed); }
.dash-week__task--done_fail { border-left-color: var(--retracted); }
.dash-week__task--cancelled { border-left-color: var(--rule); text-decoration: line-through; }

/* Outstanding tasks — the dashboard's five-row peek at tasks/list.html. */
.dash-tasks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.dash-tasks__row {
  display: flex; align-items: baseline; gap: 0.6rem;
  padding: 0.4rem 0; border-bottom: 1px solid var(--rule-soft);
}
.dash-tasks__row:last-child { border-bottom: 0; }
.dash-tasks__title {
  color: var(--accent); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dash-tasks__title:hover { text-decoration: underline; }
.dash-tasks__status {
  flex: none; font-size: var(--text-label); text-transform: uppercase; letter-spacing: 0.04em;
  padding: 0.1rem 0.35rem; border-radius: var(--radius);
  background: var(--surface-2); color: var(--ink-3);
}
.dash-tasks__status--in_execution { background: var(--accent-wash); color: var(--accent); }
.dash-tasks__meta { flex: none; margin-left: auto; font-size: var(--text-data); color: var(--ink-3); }

/* Dashboard project cards. */
.dash-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0.9rem;
}
.dash-card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface-3);
  /* projects/list.html and accounts/team_list.html wrap the whole card
     in <a class="dash-card"> (the dashboard's own cards are a plain
     <div>, link only on .dash-card__title). A descendant's own
     `text-decoration: none` does NOT stop an ancestor anchor's default
     underline from painting through it — .dash-card__title already set
     that and still rendered underlined — so it has to be killed here,
     on the anchor itself. A no-op for the dashboard's <div> cards. */
  text-decoration: none;
}
.dash-card__head { display: flex; flex-direction: column; gap: 0.1rem; }
.dash-card__title { font-weight: 600; color: var(--ink); text-decoration: none; }
.dash-card__title:hover { color: var(--accent); text-decoration: underline; }
.dash-card__meta { font-size: 0.78rem; color: var(--ink-3); }
.dash-card__list { list-style: none; margin: 0; padding: 0.4rem 0 0; border-top: 1px solid var(--rule-soft); display: flex; flex-direction: column; gap: 0.35rem; }
.dash-card__list li { display: flex; justify-content: space-between; gap: 0.6rem; font-size: 0.82rem; }
.dash-card__item-title { color: var(--ink-2); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-card__item-title:hover { color: var(--accent); text-decoration: underline; }
.dash-card__item-meta { color: var(--ink-3); flex: none; }
.dash-card__empty { margin: 0.4rem 0 0; color: var(--ink-3); font-size: 0.82rem; }
.dash-card__avatars { display: flex; flex-wrap: wrap; gap: 0.3rem; padding: 0.4rem 0 0; border-top: 1px solid var(--rule-soft); }

/* Record rows — projects/detail.html (Notes/Members/Teams) and
   accounts/team_detail.html (Roster). Same "quiet row, not a bordered
   table" language as the dashboard's .dash-card__list/.dash-tasks
   above, reused here because these lists need room for an avatar, a
   role chip or a trailing Remove/Promote form that the dashboard rows
   never carry — a plain .data-table read as underlines under every
   title, name and date (per-cell border-bottom plus the bare <a>'s
   default text-decoration). */
.record-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.record-list__row {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.5rem 0; border-bottom: 1px solid var(--rule-soft);
}
.record-list__row:last-child { border-bottom: 0; }
/* The row's own title — a note or team link, or (no href) a plain
   member/roster name. Ink at rest, accent on hover, same as
   .dash-card__title — page content, not a shouted-blue link. The
   hover rule is scoped to `a` so a non-link name never fakes that
   affordance. */
.record-list__title {
  font-weight: 600; color: var(--ink); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
a.record-list__title:hover { color: var(--accent); text-decoration: underline; }
.record-list__meta { font-size: var(--text-data); color: var(--ink-3); flex: none; margin-left: auto; white-space: nowrap; }
/* Trailing action (Remove/Promote/Demote forms) — pinned to the row's
   right edge the same way .record-list__meta is, for rows that carry
   an action instead of meta text. */
.record-list__action { flex: none; margin-left: auto; display: flex; justify-content: flex-end; }

/* Generic list table — additional_specs.md's several inventory apps
   (projects, samples, reagents, cell lines, sequences) all need
   "rows of records with a few columns," so this lives once here rather
   than once per app's own stylesheet. */
.data-table { width: 100%; border-collapse: collapse; font-size: var(--text-ui); }
.data-table th {
  text-align: left; font-weight: 600; color: var(--ink-2); font-size: var(--text-label);
  padding: 0.4rem 0.6rem; border-bottom: 1px solid var(--rule);
}
.data-table td { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
.data-table tr:last-child td { border-bottom: none; }
.data-table--scroll { overflow-x: auto; }

.meta { font-size: 0.74rem; color: var(--ink-3); display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
/* A .meta line's own links (Planner, docx export, "All checkpoints" …)
   read as part of the quiet header line, not an underlined blue link —
   same no-underline-until-hover treatment as .dash-section__more. */
.meta a { text-decoration: none; }
.meta a:hover { text-decoration: underline; }

.rule { border: 0; border-top: 1px solid var(--rule-soft); margin: 0.3rem 0; }

.stack { display: flex; flex-direction: column; gap: 0.6rem; }
.row { display: flex; gap: 0.6rem; align-items: center; }

.list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.list li {
  display: flex; align-items: center; gap: 0.6rem;
  min-height: var(--row-data);
  padding: 0.35rem 0;
  border-bottom: 1px solid var(--rule-soft);
  font-size: var(--text-ui);
  color: var(--ink-2);
}
.list li:last-child { border-bottom: 0; }

.muted { color: var(--ink-3); }

/* Note word/character count (static/js/word_count.js) — a
   quiet stat line under the page, same width as .page so it reads as the
   document's own footer rather than app chrome. */
.word-count {
  width: 100%;
  max-width: 58rem;
  margin: -0.6rem auto 0;
  padding: 0 5.2rem;
  font-size: var(--text-label);
  font-variant-numeric: tabular-nums;
}

.mentions-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.7rem; }
.mentions-list li { padding: 0.6rem 0; border-bottom: 1px solid var(--rule-soft); }
.mentions-list li:last-child { border-bottom: 0; }
.mentions-list__title { font-weight: 600; text-decoration: none; color: var(--accent); }
.mentions-list__title:hover { text-decoration: underline; }
.mentions-list__meta { margin-left: 0.5rem; font-size: 0.74rem; color: var(--ink-3); }
.mentions-list__excerpt { margin: 0.25rem 0 0; color: var(--ink-2); font-size: 0.85rem; }

/* ============================================================
   The notebook — rail (navigation) and canvas (one document)
   ============================================================ */

/* The notebook shell is the one page that scrolls INSIDE itself: .docbar and
   the formatting .toolbar stay put while .canvas__scroll moves (fixes.md:
   "keep the menubar with the formatting options sticky at the top").

   That only works if the chain from the viewport down to .canvas__scroll has
   a DEFINITE height, and `.shell`'s `min-height: 100vh` is not one. An
   auto-height column flex container sizes to its content's max-content
   contribution, and per CSS Flexbox §9.9.1 a `flex: 1 1 0` item still
   contributes its full content size there — `min-height: 0` governs how the
   item may shrink once the container's height is known, not how big the
   container decides to be. So a long entry made .shell (and the window) grow
   to the document's full height, .canvas__scroll never overflowed, and the
   header scrolled away with the text — exactly what it was meant not to do.
   Pinning the height makes the free space finite and hands it to
   .canvas__scroll, which then scrolls on its own.

   Scoped with :has() rather than a class on the element (same technique as
   .shell:has(.sidebar) above) so the sheet-based pages in _page.html, which
   are supposed to scroll the window normally, keep their auto height. dvh
   second so browsers that have it use the one that accounts for a mobile
   browser's collapsing address bar. */
.shell:has(.notes-layout) { height: 100vh; height: 100dvh; }

.notes-layout { flex: 1; display: flex; min-height: 0; }

/* The entry list. It lives inside the .panel--rail slide-over now (it was
   a permanent 230px column between the app sidebar and the document), so
   it carries no width, no ground and no border of its own — the panel owns
   all three. What is left is the stack of rows. */
.rail {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.4rem 0.35rem 1rem;
}
.panel--rail .panel-b { overflow-y: auto; }
.rail__label {
  font-size: var(--text-label);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 0.7rem 0.55rem 0.35rem;
}
.rail__item {
  width: 100%;
  text-align: left;
  padding: 0.42rem 0.55rem;
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  gap: 0.16rem;
  text-decoration: none;
  color: inherit;
  transition: background 0.12s;
}
.rail__item:hover { background: var(--surface-2); }
.rail__item--current { background: var(--accent-wash); box-shadow: inset 2px 0 0 var(--accent); }
.rail__title {
  font-size: 0.79rem; font-weight: 550; color: var(--ink); line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail__meta { font-size: 0.66rem; color: var(--ink-3); display: flex; align-items: center; gap: 0.35rem; }
.rail__empty { padding: 0.7rem 0.55rem; font-size: var(--text-ui); }

/* min-height: 0 lets .canvas shrink to the space .notes-layout actually
   has, instead of a flex item's default min-height:auto refusing to
   shrink below its content's height — without it, a long note pushes the
   whole page taller and forces the outer .shell to scroll instead of
   .canvas__scroll alone, taking .docbar/.toolbar along with it even
   though they're meant to stay put (fixes.md: "keep the menubar with the
   formatting options sticky at the top" when scrolling a note). */
.canvas { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--ground); }

/* ============================================================
   The notebook desk — the landing page at /n/ with nothing open.
   One .binder per accessible project (plus Personal), each a horizontal
   rack of .page-card "pages". See notes/shell.html for the brief.
   ============================================================ */
.desk {
  display: flex;
  flex-direction: column;
  gap: 1.6rem;
  padding: 0.4rem 0 2.5rem;
  /* .canvas__scroll centres its children with align-items:center, so a
     flex item there is sized by its CONTENT — and a rack of a dozen page
     cards is very wide content. Without an explicit width the desk grew
     to max-content and pushed the whole shell into a horizontal scroll,
     sliding the binders out under the app sidebar. Width first, then
     min-width:0 down the chain so the rack is the thing that scrolls. */
  width: 100%;
  max-width: 68rem;
  min-width: 0;
}
.desk__searchnote { margin: 0; }

.binder {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-page);
  /* The board a binder's pages are clipped into: a heavier rule down the
     left edge, in the accent, so the row reads as a bound object rather
     than as another card. Colour is doing decoration, not state, which is
     the one job design/README.md leaves the accent free for besides
     interaction. */
  border-left: 3px solid var(--accent);
  padding: 0.75rem 0 0.9rem;
  min-width: 0;
}
.binder--empty { box-shadow: none; border-left-color: var(--rule); }

.binder__spine {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0 0.95rem 0.6rem;
}
.binder__label { font-size: 0.92rem; font-weight: 650; margin: 0; }
.binder__label a { color: inherit; text-decoration: none; }
.binder__label a:hover { color: var(--accent); text-decoration: underline; }
.binder__meta { font-size: var(--text-label); color: var(--ink-3); }
.binder__nav { margin-left: auto; display: flex; gap: 0.15rem; }
/* `display: flex` outranks the UA's `[hidden] { display: none }`, so
   notes_desk.js hiding a rack's arrows had no visible effect without
   this — the classic hidden-vs-display collision. */
.binder__nav[hidden] { display: none; }
.binder__arrow[disabled] { opacity: 0.3; pointer-events: none; }

.binder__rack {
  display: flex;
  gap: 0.7rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding: 0.2rem 0.95rem 0.5rem;
  scrollbar-width: thin;
  min-width: 0;
}
.binder__rack > * { scroll-snap-align: start; }

.page-card {
  flex: none;
  width: 15rem;
  min-height: 8.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.7rem 0.75rem;
  text-decoration: none;
  color: inherit;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  /* A sheet of paper, not a tile: the ruled left margin and the shadow
     lifting off the binder board are the whole "this is a page" cue. */
  box-shadow: var(--shadow-1, 0 1px 2px rgba(0, 0, 0, 0.06));
  transition: transform 0.12s, box-shadow 0.12s;
}
.page-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-pop); }
.page-card__title {
  font-size: 0.83rem; font-weight: 600; color: var(--ink); line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.page-card__excerpt {
  flex: 1;
  font-size: 0.72rem; color: var(--ink-3); line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.page-card__foot { display: flex; align-items: center; gap: 0.4rem; font-size: 0.66rem; color: var(--ink-3); }
.page-card__date { margin-left: auto; font-variant-numeric: tabular-nums; }

.page-card--new {
  align-items: center;
  justify-content: center;
  gap: 0.15rem;
  text-align: center;
  width: 9.5rem;
  border-style: dashed;
  border-color: var(--rule);
  box-shadow: none;
  color: var(--ink-3);
}
.page-card--new:hover { border-color: var(--accent); color: var(--accent); box-shadow: none; }
.page-card__plus { font-size: 1.3rem; line-height: 1; font-weight: 300; }
.page-card__newlabel { font-size: 0.78rem; font-weight: 600; }
.page-card__newin { font-size: 0.66rem; }

.page-card--more {
  width: 7rem;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  color: var(--accent);
  box-shadow: none;
}

@media (max-width: 640px) {
  .page-card { width: 12.5rem; }
}

.docbar {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0 0.9rem;
  height: 42px;
  flex: none;
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
}
/* Docbar disclosure menu — user's ask: "in the top menu add an icon to
   the 'move to different project' tools." The trigger is a plain .icon-btn
   so it sits in the same row of glyphs as archive/history/export with no
   second button style; the panel is absolutely positioned out of a 42px
   bar that must not grow, and z-indexed over .canvas__scroll below it. */
/* The entry-list toggle is the last thing in the bar in both states, so
   it does not move when an entry is opened or closed — with an entry open
   the run of icon buttons before it already pushes it right; with nothing
   open there is only the title, hence the auto margin. */
.docbar__rail-toggle { margin-left: auto; flex: none; }

.docbar__menu { position: relative; flex: none; }
.docbar__menu > summary { list-style: none; cursor: pointer; }
.docbar__menu > summary::-webkit-details-marker { display: none; }
.docbar__menu > summary::marker { content: ""; }
.docbar__menu[open] > summary { background: var(--surface-2); color: var(--ink); }
/* Anchored to its RIGHT edge, not its left. This trigger lives in the
   run of icon buttons at the far right of the docbar, so a left-anchored
   panel opens *outwards*, off the window — and since the panel is nowrap
   and the <select> grows to fit the longest "Project · Team" it holds,
   the Move button was the part that ended up past the right edge:
   invisible and unclickable the moment a real project name was longer
   than the scratch data ("the button actually performing the action is
   not visible"). Right-anchored it grows inwards instead, and the two
   caps below keep it inside the window even at 22rem of project name:
   the panel can never be wider than the viewport, and it is the select —
   never the button — that gives up the width. */
.docbar__menu-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  left: auto;
  z-index: 60;
  max-width: calc(100vw - 1.8rem);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  box-shadow: var(--shadow-pop);
  font-size: var(--text-label);
  color: var(--ink-3);
  white-space: nowrap;
}
.docbar__menu-panel > label { flex: none; }
.docbar__menu-panel > .btn { flex: none; }
.docbar__menu-panel select {
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.2rem 0.4rem;
  max-width: 22rem;
  /* min-width:0 is what actually lets a flex item shrink below the width
     of its longest option — without it the select refuses, and the panel
     grows past max-width instead of the select shrinking. */
  min-width: 0;
  flex: 0 1 auto;
}

.doc-title {
  font-size: 0.82rem; font-weight: 650; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Click-to-rename — user's ask: "Change the title of a document directly
   in the menubar when the document is open." Only rendered interactive
   (data-editable) when this session may currently edit; a plain span
   otherwise. */
.doc-title[data-editable] {
  cursor: text;
  border-radius: 4px;
  padding: 0.15rem 0.35rem;
  margin: 0 -0.35rem;
}
.doc-title[data-editable]:hover, .doc-title[data-editable]:focus-visible {
  background: var(--surface-2);
  outline: none;
}
.doc-title__input {
  font: inherit;
  font-weight: 650;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--accent);
  border-radius: 4px;
  padding: 0.05rem 0.3rem;
  width: min(28rem, 60vw);
  max-width: 100%;
}
.doc-title__input:focus { outline: none; }
/* An exclamation in a circle, inlined so it needs no network request and
   no build step. Used masked, so the fill colour comes from currentColor. */
:root {
  --icon-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.5A6.5 6.5 0 1 0 8 14.5 6.5 6.5 0 0 0 8 1.5Zm-.75 3h1.5v5h-1.5v-5Zm0 6.25h1.5v1.5h-1.5v-1.5Z'/%3E%3C/svg%3E");
}

/* Text for assistive tech only. Not `display:none` — that would remove it
   from the accessibility tree, which is the opposite of the point. Used
   wherever a state is shown graphically (a colour, a glyph) and still needs
   a name: samples/box_detail.html's expiry markers, for instance. */
.u-visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

.doc-saved {
  margin-left: auto; font-size: 0.7rem; color: var(--ink-3);
  font-variant-numeric: tabular-nums; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 0.3rem;
}
/* There is no save button, so this indicator is the whole save contract.
   A failure must not read the same as "All changes saved" — it gets the
   danger colour, weight, and a glyph, and stops being nowrap so the reason
   is legible rather than clipped. */
.doc-saved--warn { color: var(--ink-2); }
.doc-saved--error {
  color: var(--danger); font-weight: 600; font-size: var(--text-ui);
  white-space: normal;
}
.doc-saved--error::before,
.doc-saved--warn::before {
  content: ""; flex: none; width: 0.85em; height: 0.85em;
  background-color: currentColor;
  /* Masked rather than an <img>: it inherits currentColor, so the same glyph
     reads correctly in the warn and error colours and in both themes. */
  -webkit-mask: var(--icon-alert) center / contain no-repeat;
  mask: var(--icon-alert) center / contain no-repeat;
}

.canvas__scroll { flex: 1; overflow-y: auto; padding: 1.5rem 1rem 4rem; display: flex; flex-direction: column; gap: 0.8rem; align-items: center; }

/* The page itself — prose density, not UI density. This is what
   design/README.md means by "the page is an object": a fixed 38rem sheet on
   the desk, not a form that happens to hold text. */
.page {
  /* "Make the page width correspond to a pdf for notes, so its true
     WYSIWYG." A matching CONTAINER width (50rem, tracking the PDF's real
     793.7px A4-at-96dpi total) was not enough on its own: this app's own
     printed stylesheet (templates/notes/pdf.html) sets body font-size to
     10.5pt (=14px), while --text-body here is 1.0625rem (=17px) — same
     font (Fira Sans, loaded both places), but ~21% bigger on screen, so
     the same pixel-wide column fits fewer words per line and wraps
     earlier than the PDF, confirmed by a real side-by-side ("mit, DNA,
     Unterdrückung..." in the PDF vs "zum, Prognose. ist..." on screen at
     the same line). Shrinking the screen font to 14px to match would
     make on-screen reading uncomfortably small, so instead the column
     itself is widened by that same ~1.214 ratio: the PDF's real content
     width (793.7 - 2*2.2cm margin = 627.4px) times 17/14 is ~761.9px: at
     the still-2.2cm-derived 5.2rem side padding below, that means an
     outer width of ~928px = 58rem (wider than the PDF's own physical
     page — line-wrap parity is the actual goal here, physical-page-
     silhouette parity is not, once the two font sizes differ). Paragraphs
     below intentionally do NOT cap at --measure inside .page — a printed
     page's text fills its whole margin-bound column, so the on-screen one
     should too.

     fixes2.md round 2: "The width of the note is better, but still not
     exactly the same than the PDF. It is ever so slightly to small" —
     confirmed with a real side-by-side (a German paragraph, live editor
     vs. its own PDF export): at the math-derived 928px, the editor wraps
     one word earlier than the PDF on every line of a several-line
     paragraph (e.g. "...empfindlich auf" on screen vs "...empfindlich auf
     replikativen" in the PDF). The 627.4px content-width figure above is
     exactly reproducible; what it doesn't capture is that the PDF is set
     with WeasyPrint (Pango/HarfBuzz text shaping) while the screen is
     whatever browser the reader has open — two different shaping engines
     laying out the *same* Fira Sans file at the *same* effective size
     still don't measure a given string identically (kerning/hinting
     rounding), and that gap compounds over a ~60-character line. Measured
     directly in-browser (Range.getClientRects() per character, .page's
     own max-width swept 928px-1000px, comparing each line's last word
     against the PDF's own line breaks for the same content): the two wrap
     identically only from 931px to 935px — below that the editor drops
     the PDF's line-ending word to the next line (this bug report's exact
     symptom), above it the editor starts fitting a word *more* per line
     than the PDF does. 933px (58.3rem) sits in the middle of that
     measured window, i.e. --page-width is not the pure ~928px math above
     but that figure plus a small empirically-measured shaping-engine
     buffer. design/tokens.css's --page-width is the one place this
     number lives — discussions.css's .thread and citations.css's
     .bibliography read the same token instead of their own copy, per
     this round's other note: "The references and Discussion boxes dont
     have the same width than the note itself." */
  background: var(--surface);
  width: 100%;
  max-width: var(--page-width);
  margin: 0 auto;
  padding: 5.4rem 5.2rem 5.9rem;
  border-radius: var(--radius-page);
  box-shadow: var(--shadow-page);
  /* editor.js's .img-margin-btn (the hover "annotate" shortcut in this
     padding's own blank left gutter) is positioned absolute against .page,
     not the viewport, so it scrolls with the image it points at for free. */
  position: relative;
}
.page__meta { font-size: var(--text-label); color: var(--ink-3); margin: 0 0 1.2rem; }
.page p, .page li {
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--ink);
}
.page h1, .page h2, .page h3, .page h4 { font-weight: 650; letter-spacing: -0.01em; line-height: 1.3; }
/* Sub/superscript (editor.js's makeVerticalAlignMark, rendering.py's
   MARK_TAGS). `line-height: 0` is the standard fix for the browser default,
   which otherwise pushes any line holding an H2O or a 10^6 further from its
   neighbours than every other line in the paragraph. The citation marker is
   a <sup> too and is explicitly excluded: `.page sup` is (0,1,1) and would
   otherwise beat citations.css's own (0,1,0) `.cite` size. */
.page sub:not(.cite), .page sup:not(.cite) { font-size: 0.75em; line-height: 0; }
.page img { max-width: 100%; border-radius: var(--radius); display: block; margin: 0.4rem 0; }

/* editor.js: the hover "annotate" shortcut that lives in .page's own blank
   left padding, vertically centred on whichever image the pointer is over.
   Absolutely positioned (never in flow) so showing/hiding it can never
   shift the page, and opacity/pointer-events (not display) drive
   visibility so editor.js's hover-grace-period dance has something to fade.
   One singleton element, repositioned per hover, not one per image. */
.img-margin-btn,
.table-margin-btn {
  position: absolute;
  width: 28px;
  height: 28px;
  border-radius: var(--radius);
  display: grid;
  place-items: center;
  background: var(--surface);
  border: 1px solid var(--rule);
  color: var(--ink-3);
  box-shadow: var(--shadow-pop);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.img-margin-btn.is-visible,
.table-margin-btn.is-visible { opacity: 1; pointer-events: auto; }
.img-margin-btn svg,
.table-margin-btn svg { width: 16px; height: 16px; }
.img-margin-btn:hover,
.table-margin-btn:hover { background: var(--surface-2); color: var(--ink); }
.img-margin-btn { left: 1.4rem; }
/* The table sibling sits in the RIGHT gutter, level with the top of the
   table the caret is in — user's ask: "the table options button should show
   up in the right border when my cursor is in the table". Selection-driven
   rather than hover-driven (editor.js's syncTableMarginBtn), so unlike the
   image button it needs no fade-out grace period; it shares the fade only
   because it shares everything else. */
.table-margin-btn { right: 1.4rem; }
.page .msg { margin-bottom: 0.4rem; }
.page pre {
  background: var(--surface-2, var(--ground));
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.7rem 0.85rem;
  overflow-x: auto;
  font-size: var(--text-label);
  line-height: 1.5;
}
.page pre code { background: none; padding: 0; border-radius: 0; }
.page code { background: var(--surface-2, var(--ground)); padding: 0.05em 0.3em; border-radius: 3px; }

/* fixes.md: "I did not find the specified table editor and cant enter a
   table to a note." @tiptap/extension-table's node tree
   (table > tableRow > tableHeader|tableCell), both here (editing + the
   read-only canvas) and in templates/notes/pdf.html's own copy. A table
   is data, not prose — deliberately not capped at --measure like
   paragraphs, since rows commonly need the full sheet width; the
   scrollbar is the fallback for whatever still doesn't fit. */
.page table {
  width: 100%;
  max-width: 100%;
  border-collapse: collapse;
  margin: 0.4rem 0 0.8rem;
  font-size: var(--text-body);
  /* User's ask: "There need to be more styling options: Column widths".
     Column widths only exist as a concept under `fixed` — with the default
     `auto`, the browser re-derives every column from its content and a
     width the user dragged (or typed into the table popup) is a
     suggestion the next long cell overrides. This is also what
     prosemirror-tables' column-resizing plugin assumes; its <colgroup> is
     inert without it. The cost is that a table nobody has sized now gets
     equal columns rather than content-derived ones — which is the trade
     every WYSIWYG makes the moment column widths become a thing you can
     set, and the predictable half of it. */
  table-layout: fixed;
}
.page th, .page td {
  border: 1px solid var(--rule);
  /* User's ask: "There is far too much padding around the text." Most of
     it was never this padding — it was the paragraph INSIDE each cell
     keeping its default 0.08em block margins, so a one-line cell carried
     ~2.4rem of dead vertical space that no table rule mentioned. That is
     zeroed below; this is trimmed to match, and the leading tightened,
     because a table is dense data and should not be set like body prose. */
  padding: 0.18rem 0.4rem;
  text-align: left;
  vertical-align: top;
  /* The resize handle is absolutely positioned against the cell. */
  position: relative;
}
.page th > p, .page td > p, .page th > ul, .page td > ul, .page th > ol, .page td > ol {
  margin: 0;
}
.page th > p, .page td > p, .page th li, .page td li { line-height: 1.35; }
.page th > p + p, .page td > p + p { margin-top: 0.3em; }
.page th { background: var(--surface-2); font-weight: 650; }
.page .ProseMirror td.selectedCell, .page .ProseMirror th.selectedCell { background: var(--accent-wash); }

/* Column resizing (Table.configure({ resizable: true })). ProseMirror wraps
   a resizable table in .tableWrapper and paints .column-resize-handle on
   the border under the pointer; both need styling from us, the extension
   ships no CSS. The wrapper is also what keeps a table wider than the page
   scrollable instead of bleeding over the sheet edge — .page table used to
   claim `overflow-x: auto`, which does nothing on a <table> box. */
.page .tableWrapper { overflow-x: auto; margin: 0.4rem 0 0.8rem; }
.page .tableWrapper table { margin: 0; }
.page .ProseMirror .column-resize-handle {
  position: absolute;
  right: -2px;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--accent);
  pointer-events: none;
  z-index: 20;
}
.page .ProseMirror.resize-cursor { cursor: col-resize; }

/* Inline record mentions (samples, and later reagents) — additional_specs.md
   "Inline mentioning of a vial/sample with a link to the storage position."
   A tag/pill, deliberately not styled like .cite's superscript bracket —
   a mention names a physical thing, it isn't numbered prose. */
.mention {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  padding: 0.05em 0.5em 0.05em 0.35em;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  color: var(--ink);
  font-size: 0.85em;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
.mention:hover, .mention:focus-visible { border-color: var(--ink-3); }
.mention svg { width: 0.85em; height: 0.85em; flex: none; }
.mention__meta { font-weight: 400; color: var(--ink-3); }

/* Sequence-mention embed — the chip's toggle-to-a-map (disc-mate-annotation
   branch). Only a "sequence"-kind mention ever gets a .mention-embed
   wrapper (frontend/src/editor.js's addNodeView / notes/rendering.py's
   render_node) — the chip itself (.mention above) is completely unchanged,
   collapsed or not, and a sample/reagent mention never gets a wrapper at
   all, so this whole block is additive. */
.mention-embed {
  display: inline-flex;
  align-items: flex-start;
  gap: 0.3em;
  max-width: 100%;
}
.mention-embed.is-open {
  /* Breaks out of the inline flow to fill .page's own content column, the
     same way .page img already does (see design/README.md's page anatomy,
     and .page's "paragraphs don't cap at --measure" note) — the node
     itself stays a plain inline atom in the document; ProseMirror doesn't
     care what its NodeView paints, only this CSS display changes. */
  display: block;
  width: 100%;
  margin: 0.5rem 0;
}
.mention-embed.is-open .mention { margin-bottom: 0.4rem; }

/* Editor-only in practice (the read-only canvas never renders this button —
   notes/rendering.py's render_node() has nothing to persist a click into),
   but there is nothing here that *requires* JS: it is a plain <button>, so a
   note whose expanded state was saved from the editor still shows the
   right chevron if this ever does end up on a page with an editor but a
   fully hydrated read-only view. */
.mention__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6em;
  height: 1.6em;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-3);
  cursor: pointer;
  flex: none;
}
.mention__toggle:hover, .mention__toggle:focus-visible { border-color: var(--accent); color: var(--accent); }
.mention__toggle svg { width: 0.8em; height: 0.8em; transition: transform 0.15s ease; }
.mention-embed.is-open .mention__toggle svg { transform: rotate(-180deg); }

/* The expanded block itself — hidden entirely while collapsed (both by the
   NodeView's own `embed.hidden` and, belt-and-braces for the read-only
   canvas which never renders a collapsed one of these at all, here). */
.seq-embed {
  display: none;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: 0.9rem 1rem;
}
.mention-embed.is-open .seq-embed { display: block; }
.seq-embed__status { font-size: var(--text-ui); color: var(--ink-3); }
.seq-embed__status--error { color: var(--retracted); }
.seq-embed__note {
  display: block;
  margin-top: 0.5rem;
  font-size: var(--text-label);
  color: var(--unverified);
}
.seq-embed__map { display: block; }
.seq-embed__svg { display: block; width: 100%; height: auto; }
.seq-embed__svg--circular { max-width: 22rem; margin: 0 auto; }

.seq-embed-backbone { stroke: var(--rule); stroke-width: 2; fill: none; }
.seq-embed-intron { stroke: var(--ink-3); stroke-width: 1.5; opacity: 0.6; }
.seq-embed-arc__label {
  font: 600 8px var(--font-mono);
  fill: var(--ink);
  text-anchor: middle;
  dominant-baseline: middle;
}
.seq-embed-arc__label--outside { text-anchor: start; }
.seq-embed-center__name { font: 650 12px var(--font-sans); fill: var(--ink); text-anchor: middle; }
.seq-embed-center__len { font: 400 9px var(--font-mono); fill: var(--ink-3); text-anchor: middle; }
.seq-embed-center__name--linear, .seq-embed-center__len--linear { text-anchor: start; }
.seq-embed-center__len--linear { text-anchor: end; }

/* Same per-kind ramp as backend/static/css/sequences.css's own
   .seq-map-arc--*/.seq-map-head--* (gene/cds/promoter/primer/
   restriction_site/regulatory/misc), reproduced here rather than shared —
   see frontend/src/editor.js's EMBED_KIND_CLASSES comment for why. Split
   into the same three rule groups that file uses (arc stroke, rect/polygon
   fill, arrowhead fill) rather than one combined stroke+fill rule: an arc
   is drawn as an unfilled <path>, and a plain `fill` rule would otherwise
   win over its `fill="none"` attribute (a CSS class always outranks a bare
   SVG presentation attribute) and paint a solid wedge over the ring. */
.seq-embed-arc--gene       { stroke: var(--accent); }
.seq-embed-arc--cds        { stroke: var(--signed); }
.seq-embed-arc--promoter   { stroke: var(--unverified); }
.seq-embed-arc--primer     { stroke: var(--agent); }
.seq-embed-arc--restriction_site { stroke: var(--retracted); }
.seq-embed-arc--regulatory { stroke: var(--correction); }
.seq-embed-arc--misc       { stroke: var(--ink-3); }
rect.seq-embed-arc--gene, polygon.seq-embed-arc--gene       { fill: var(--accent); }
rect.seq-embed-arc--cds, polygon.seq-embed-arc--cds         { fill: var(--signed); }
rect.seq-embed-arc--promoter, polygon.seq-embed-arc--promoter { fill: var(--unverified); }
rect.seq-embed-arc--primer, polygon.seq-embed-arc--primer   { fill: var(--agent); }
rect.seq-embed-arc--restriction_site, polygon.seq-embed-arc--restriction_site { fill: var(--retracted); }
rect.seq-embed-arc--regulatory, polygon.seq-embed-arc--regulatory { fill: var(--correction); }
rect.seq-embed-arc--misc, polygon.seq-embed-arc--misc       { fill: var(--ink-3); }
.seq-embed-head--gene       { fill: var(--accent); }
.seq-embed-head--cds        { fill: var(--signed); }
.seq-embed-head--promoter   { fill: var(--unverified); }
.seq-embed-head--primer     { fill: var(--agent); }
.seq-embed-head--restriction_site { fill: var(--retracted); }
.seq-embed-head--regulatory { fill: var(--correction); }
.seq-embed-head--misc       { fill: var(--ink-3); }

/* Generic file attachment chip — additional_specs.md "File upload": "Allow
   upload of non-editable documents to notes as attachements (images, raw
   data files)." A PDF/spreadsheet/raw-data drop has no inline render the
   way an image does, so it becomes a downloadable chip instead — same
   pill shape as .mention (both name a thing rather than read as prose)
   but its own icon/colour so a file attachment is never mistaken for a
   record mention. */
.file-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.15em 0.6em 0.15em 0.4em;
  border-radius: 8px;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  color: var(--ink);
  font-size: 0.85em;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  max-width: 100%;
}
.file-chip:hover, .file-chip:focus-visible { border-color: var(--accent); color: var(--accent); }
.file-chip svg { width: 0.08em; height: 0.08em; flex: none; color: var(--ink-3); }
.file-chip:hover svg, .file-chip:focus-visible svg { color: var(--accent); }
.file-chip__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-chip__size { font-weight: 400; color: var(--ink-3); flex: none; }

/* The editor mount is a ProseMirror contenteditable — same prose rules as
   the read-only render above apply through .page's descendant selectors,
   so only the editor-specific bits (no default outline, placeholder text)
   need adding here. */
.page .ProseMirror { outline: none; }
.page .ProseMirror p.is-editor-empty:first-child::before {
  content: attr(data-placeholder);
  float: left;
  height: 0;
  color: var(--ink-3);
  pointer-events: none;
}

/* ---------- formatting toolbar ----------
   A permanent bar directly under the title (docbar), not a
   selection-triggered pop-up — themed like the rest of the app's chrome
   (surface + ink tokens), not a floating dark chip, so it reads as part of
   the page rather than a tooltip. */
.toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.2rem;
  padding: 0.4rem 0.9rem;
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
  /* This bar is pinned (see .shell:has(.notes-layout)), so its height is
     taken straight out of the reading area — and it wraps, so a narrow
     canvas can turn one 42px row into many. Cap it and let it scroll
     itself: every button stays reachable without the header ever crowding
     out the entry. Inert at any normal window width (one or two rows), and
     the popovers it opens are position: fixed (.swatch-popup, .cite-popup),
     so an overflow container here clips nothing. */
  max-height: 40vh;
  overflow-y: auto;
}
.toolbar.is-disabled { opacity: 0.5; pointer-events: none; }
.toolbar button {
  width: 28px;
  height: 28px;
  border-radius: var(--radius);
  display: grid;
  place-items: center;
  color: var(--ink-2);
  font-size: 0.78rem;
}
.toolbar button:hover { background: var(--surface-2); color: var(--ink); }
.toolbar button.is-active { background: var(--accent-wash); color: var(--accent); }
.toolbar button svg { width: 15px; height: 15px; }
.toolbar .sep { width: 1px; height: 18px; background: var(--rule); margin: 0 0.3rem; }
.toolbar select {
  height: 28px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink-2);
  font-size: 0.78rem;
  padding: 0 0.4rem;
}
.toolbar select:hover { background: var(--surface-2); }
#tb-block-type { min-width: 7rem; }
#tb-font-size { min-width: 5.5rem; }

/* Text-colour / highlight swatch popover — reuses .cite-popup's fixed/
   positioned/click-outside-closes mechanics (citations.css) but a grid of
   dots instead of a search list, so it isn't duplicated here. */
.swatch-popup {
  position: fixed;
  z-index: 65;
  display: none;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.5rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  box-shadow: var(--shadow-pop);
}
.swatch-popup.is-open { display: flex; }
.swatch-popup__grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.3rem; }
.swatch-popup__swatch {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--swatch-color);
  border: 1px solid var(--rule);
}
.swatch-popup__swatch:hover { outline: 2px solid var(--accent); outline-offset: 1px; }
.swatch-popup__clear {
  font-size: 0.72rem;
  color: var(--ink-3);
  text-align: left;
  padding: 0.15rem 0.2rem;
}
.swatch-popup__clear:hover { color: var(--ink); }

/* Table options popover — user's ask: "There need to be more styling
   options: Column widths, row heights, add row, add column, remove row,
   remove column." Eight-plus actions is too many to spend on the toolbar
   itself (which already wraps and self-scrolls at narrow widths), and they
   are all meaningless unless the caret is in a table, so they live behind
   one button here. Same fixed/click-outside-closes mechanics as
   .swatch-popup above; a labelled grid rather than a swatch grid, because
   "delete column" is not a thing anyone should recognise by glyph alone. */
.table-popup {
  position: fixed;
  z-index: 65;
  display: none;
  flex-direction: column;
  gap: 0.55rem;
  padding: 0.6rem;
  min-width: 15.5rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  box-shadow: var(--shadow-pop);
}
.table-popup.is-open { display: flex; }
.table-popup__group { display: flex; flex-direction: column; gap: 0.25rem; }
.table-popup__label {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
}
.table-popup__row { display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; }
.table-popup__btn {
  flex: 1 1 auto;
  min-height: 26px;
  padding: 0.2rem 0.45rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink-2);
  font-size: 0.74rem;
  white-space: nowrap;
}
.table-popup__btn:hover { background: var(--surface-2); color: var(--ink); }
.table-popup__btn--danger { color: var(--danger); }
.table-popup__btn--danger:hover { background: var(--danger-dim); color: var(--danger); }
.table-popup__size { display: flex; align-items: center; gap: 0.3rem; }
.table-popup__size input {
  width: 4.6rem;
  height: 26px;
  padding: 0 0.4rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 0.74rem;
}
.table-popup__unit { font-size: 0.72rem; color: var(--ink-3); }
.table-popup__rule { height: 1px; background: var(--rule-soft); margin: 0.1rem 0; }
/* Nothing in here can act on a caret that is outside a table, so the
   trigger says so rather than opening an inert panel. */
/* Shown only by mobile/20-editor.css's (pointer: coarse) block. */
.toolbar #insert-photo-btn { display: none; }
.toolbar button.is-unavailable { opacity: 0.35; pointer-events: none; }

/* ============================================================
   Version history panel — design/README.md: "Panels slide over,
   never dock." A fixed overlay, not a permanent sidebar, so it
   never steals width from the document measure.
   ============================================================ */

.panel {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: min(430px, 92vw);
  background: var(--surface);
  border-left: 1px solid var(--rule);
  box-shadow: var(--shadow-pop);
  transform: translateX(102%);
  /* visibility is not interpolable, so it is stepped: on close it flips to
     hidden only after the 0.22s slide has finished, preserving the animation;
     on open (below) it flips immediately so the panel is there to slide in. */
  transition: transform 0.22s cubic-bezier(0.3, 0.8, 0.4, 1),
              visibility 0s linear 0.22s;
  display: flex;
  flex-direction: column;
  z-index: 70;
  /* transform alone leaves a closed panel in the tab order — keyboard
     users tabbed from the document straight into invisible off-screen
     history rows and the reference search field. visibility:hidden
     removes it from focus while still allowing the slide transition. */
  visibility: hidden;
}
.panel.is-open {
  transform: translateX(0);
  visibility: visible;
  transition: transform 0.22s cubic-bezier(0.3, 0.8, 0.4, 1), visibility 0s linear 0s;
}

.panel-h {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 0.85rem;
  border-bottom: 1px solid var(--rule);
  flex: none;
}
.panel-h h2 { font-size: 0.86rem; font-weight: 650; margin: 0; }
.panel-h__close { margin-left: auto; }

.panel-b { flex: 1; overflow-y: auto; }

.vrow {
  width: 100%;
  text-align: left;
  padding: 0.5rem 0.85rem;
  border: 0;
  border-bottom: 1px solid var(--rule-soft);
  background: none;
  display: flex;
  gap: 0.6rem;
  align-items: baseline;
  font-size: 0.76rem;
  cursor: pointer;
}
.vrow:hover { background: var(--surface-3); }
.vrow[aria-current="true"] { background: var(--accent-wash); }
.vrow .vn {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--ink-3);
  width: 2.2rem;
  flex: none;
  font-variant-numeric: tabular-nums;
}
.vrow .vw {
  flex: 1;
  min-width: 0;
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vrow .agent-tag { color: var(--agent); }
.vrow .vt {
  color: var(--ink-3);
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.diff { padding: 0.85rem; font-size: 0.82rem; line-height: 1.6; border-top: 1px solid var(--rule); }
.diff .dh {
  display: block;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 0.5rem;
}
.diff p { margin: 0; color: var(--ink-2); }
.diff .diff-meta { margin: 0.7rem 0 0; font-size: 0.72rem; color: var(--ink-3); }
.diff .diff-meta .mono { font-family: var(--font-mono); }
.diff .diff-empty { color: var(--ink-3); font-style: italic; }
.diff .diff-amend { margin: 0 0 0.6rem; font-size: 0.76rem; color: var(--unverified); }
.ins { background: var(--signed-dim); color: var(--signed); border-radius: 2px; padding: 0 0.1em; }
.del { background: var(--retracted-dim); color: var(--retracted); text-decoration: line-through; border-radius: 2px; padding: 0 0.1em; }

.vrow-signed { color: var(--signed); width: 12px; height: 12px; flex: none; }

/* ============================================================
   Sealing & signing — PLAN.md §4.2/§4.3
   ============================================================ */

.amend-bar { align-items: center; }
.amend-bar__body { display: flex; flex-direction: column; gap: 0.5rem; width: 100%; }
.amend-bar input {
  width: 100%;
  max-width: 26rem;
  font: inherit;
  font-size: var(--text-ui);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.4rem 0.6rem;
}
.amend-bar input:focus {
  border-color: var(--unverified);
  box-shadow: 0 0 0 3px var(--unverified-dim);
  outline: none;
}

/* Sign dialog. position: fixed, not the mockup's position: absolute — the
   real shell has no bounded .app container to clip an absolute overlay to,
   same reasoning as .panel's own fixed positioning. */
.scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.34);
  z-index: 80;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}
.scrim.is-open { display: flex; }
.dialog {
  background: var(--surface);
  border-radius: 6px;
  box-shadow: var(--shadow-pop);
  width: min(420px, 100%);
  padding: 1.2rem 1.3rem 1.3rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  /* A dialog must never outgrow the viewport. .scrim centres it with
     align-items: center, and a flex item taller than its container overflows
     equally off the top *and* the bottom with nothing to scroll — so the
     button row becomes unreachable rather than merely below the fold. That is
     how signing an entry with a long pinned-citations list left no way out of
     the dialog at all. dvh, with vh first as the fallback, because mobile
     browser chrome makes 100vh taller than what is actually on screen. */
  max-height: calc(100vh - 2rem);
  max-height: calc(100dvh - 2rem);
  overflow-y: auto;
}
/* Keep the fixed furniture — heading, hash, code field, buttons — at its
   natural size so only the long list gives way. */
.dialog > * { flex-shrink: 0; }
/* The one part of the sign dialog with unbounded length becomes the scroller,
   which keeps "Sign entry" on screen no matter how many references are pinned.
   min-height stops it collapsing to nothing on a short window; min-width: 0 is
   what actually lets a flex item shrink below its content size. */
.dialog .pinned-citations {
  flex: 1 1 auto;
  min-height: 4.5rem;
  min-width: 0;
  overflow-y: auto;
}
.dialog h2 { margin: 0; font-size: 1.02rem; font-weight: 650; }
.dialog p { margin: 0; font-size: 0.83rem; color: var(--ink-2); line-height: 1.55; }
.hashbox {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 0.5rem 0.6rem;
  color: var(--ink-3);
  word-break: break-all;
}

/* Discrepancy resolver (frontend/src/editor.js's makeConflictResolver) —
   fixes.md's "similar to git, but don't overcomplicate things": one row
   per conflicting block-range, base for context + two side-by-side picks. */
.conflict-dialog { width: min(640px, 100%); max-height: 85vh; }
.conflict-dialog__list {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  overflow-y: auto;
  max-height: 55vh;
  padding-right: 0.2rem;
}
.conflict-row {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.6rem 0.7rem;
}
.conflict-row__base {
  margin: 0 0 0.5rem;
  font-size: 0.76rem;
  color: var(--ink-3);
  text-decoration: line-through;
}
.conflict-row__options { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
.conflict-row__options button {
  text-align: left;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 0.5rem 0.6rem;
  font-size: 0.82rem;
  color: var(--ink);
  cursor: pointer;
}
.conflict-row__options button:hover { border-color: var(--ink-3); }
.conflict-row__options button.is-chosen { border-color: var(--accent); background: var(--accent-wash); }
.conflict-row__label {
  display: block;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-3);
  margin-bottom: 0.2rem;
}
.agent-warn {
  display: flex;
  gap: 0.5rem;
  background: var(--agent-dim);
  border-radius: 4px;
  padding: 0.6rem 0.7rem;
  font-size: 0.78rem;
  color: var(--agent);
  line-height: 1.45;
  align-items: flex-start;
}
.agent-warn svg { width: 15px; height: 15px; flex: none; margin-top: 0.1rem; }
#sign-otp { max-width: 10rem; }

/* ============================================================
   Account pages — profile, and the staff user list
   ============================================================ */

/* The identity block at the top of the profile: photo/monogram beside the
   address it belongs to. */
.profile-identity {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding-bottom: 0.4rem;
}
.profile-identity__email { font-weight: 600; margin: 0; }
.profile-identity__hint { font-size: 0.78rem; color: var(--ink-3); margin: 0.15rem 0 0; }

/* Identity-colour picker. Distinct from .swatch-popup, which is the
   note-highlight palette in a floating popup — this one sits in the page,
   is a radio group, and has to show which option is currently chosen. */
.swatch-row { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.swatch { cursor: pointer; line-height: 0; }
/* The real radio stays in the DOM for keyboard and screen-reader users and
   is what :has() below reads; it is only visually replaced by the dot. */
.swatch input { position: absolute; opacity: 0; width: 0; height: 0; }
.swatch__dot {
  display: block;
  width: 26px; height: 26px;
  border-radius: 50%;
  border: 2px solid transparent;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.swatch:has(input:checked) .swatch__dot { border-color: var(--ink); }
.swatch:has(input:focus-visible) .swatch__dot { outline: 2px solid var(--accent); outline-offset: 2px; }
.swatch--blue   .swatch__dot { background: var(--id-blue); }
.swatch--berry  .swatch__dot { background: var(--id-berry); }
.swatch--apple  .swatch__dot { background: var(--id-apple); }
.swatch--cyan   .swatch__dot { background: var(--id-cyan); }
.swatch--orange .swatch__dot { background: var(--id-orange); }
.swatch--slate  .swatch__dot { background: var(--id-slate); }

/* Avatar picker. One radio group of 88 options, drawn as a row of colours
   per artwork — the grouping is presentation only (accounts.forms
   .ProfileForm.avatar_art_groups), the inputs are one flat group so that
   choosing any picture unchooses every other. No JavaScript: :has() does
   the selected state, and the page works with scripting off. */
.art-picker { display: flex; flex-direction: column; gap: 0.7rem; }
.art-group__title {
  margin: 0 0 0.25rem;
  font-size: var(--text-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 600;
}
.art-group__title a { color: inherit; text-decoration: none; border-bottom: 1px dotted var(--rule); }
.art-group__title a:hover { color: var(--ink); }
.art-group__row { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.art-opt { cursor: pointer; line-height: 0; display: block; }
.art-opt--wide { display: flex; align-items: center; gap: 0.5rem; line-height: normal; }
.art-opt__label { font-size: 0.82rem; color: var(--ink-2); }
/* Same trick as .swatch: the real radio stays in the DOM for keyboard and
   screen-reader users and is what :has() reads. */
.art-opt input { position: absolute; opacity: 0; width: 0; height: 0; }
.art-opt .avatar { border: 2px solid transparent; transition: border-color 80ms linear; }
.art-opt:hover .avatar { border-color: var(--rule); }
.art-opt:has(input:checked) .avatar { border-color: var(--ink); }
.art-opt:has(input:focus-visible) .avatar { outline: 2px solid var(--accent); outline-offset: 2px; }

.checkbox { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; }
.checkbox input { width: auto; }

.plain-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 0.35rem; font-size: 0.85rem; }
.plain-list .field__help { display: block; }
.mono-meta { font-family: var(--font-mono); font-size: 0.72rem; color: var(--ink-3); margin-right: 0.4rem; }

/* Staff user list. A table rather than cards: the columns (name, teams,
   authenticator state) are what an administrator scans down. */
.user-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.user-table th {
  text-align: left;
  font-size: var(--text-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 600;
  padding: 0.4rem 0.5rem;
  border-bottom: 1px solid var(--rule);
}
.user-table td { padding: 0.5rem; border-bottom: 1px solid var(--rule); vertical-align: middle; }
.user-table__who { display: flex; align-items: center; gap: 0.5rem; }
.user-table__actions { display: flex; gap: 0.35rem; flex-wrap: wrap; justify-content: flex-end; }

/* ============================================================
   Evidence — EVIDENCE_PLAN.md §5, A5. The version-history panel's
   published/pending mark, the link out to it, and the two full-page
   tables (/n/<pk>/evidence/, /evidence/).
   ============================================================ */

/* The history panel's link through to the full chain of evidence. A plain
   <a> (blue is reserved for "you can click this", design/README.md), sized
   to sit quietly in the 0.86rem panel header beside the h2 without
   competing with it. */
.panel-h__evidence {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-left: 0.6rem;
  font-size: 0.72rem;
  font-weight: 600;
  white-space: nowrap;
}
.panel-h__evidence svg { width: 12px; height: 12px; flex: none; }

/* Per-row published/pending mark in the history panel, same footprint as
   the existing .vrow-signed tick beside it. Colour reuses the semantic
   state tokens rather than inventing new ones: --signed for "a checkpoint
   now covers this", --unverified for "written and linked, not yet
   published" — which is exactly what that token already means elsewhere
   ("checksum not confirmed"). Shape carries the rest of the meaning
   (design/README.md: colour is never alone) — a chain-link mark and a
   clock, not the checkmark .vrow-signed already owns, so the two can never
   be mistaken for each other on the same row. */
.vrow-evidence { width: 12px; height: 12px; flex: none; }
.vrow-evidence--published { color: var(--signed); }
.vrow-evidence--pending { color: var(--unverified); }

/* /n/<pk>/evidence/ and /evidence/ — both data-density audit tables, so
   they lean on .data-table rather than inventing a second table style. */
.evidence-checkpoint {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  margin-top: 0.3rem;
}
.evidence-checkpoint .field__help { font-size: 0.72rem; }
