/*
 * Go Scribi.
 *
 * No colour is written literally anywhere below — every one comes from a custom
 * property set on <html> by the layout, from Theme. That is precisely why a
 * user's own hex values work as well as a built-in preset: there is nothing else
 * to change.
 *
 * That sentence was false in twenty-six places for most of this file's life: a
 * hardcoded red in fifteen rules, two `background: #fff` (one of them the note
 * editor, which is a white slab on any dark theme), and a `var(--danger)` with a
 * fallback to a DIFFERENT red because --danger did not exist. It held anyway,
 * because every theme was light with a white surface. A test greps this file now
 * rather than trusting the comment.
 */

:root {
  /* The sidebar's default width, and the ONE place that says 20rem.
     Defined rather than read with a `var(--sidebar-width, 20rem)` fallback: a
     property only ever set by JavaScript is one `stylesheet_test` rightly calls
     undefined, and a fallback carrying the default is a second place for the
     default to live.

     On :root and not on `.layout`, which is the mechanism rather than a
     preference — the head script and the controller set this property INLINE on
     <html>, and an inline style outranks a stylesheet rule on the same element
     but loses to a rule on a nearer one. Put here it is a default that a stored
     width overrides; put on `.layout` it would have won, and no width would ever
     have been remembered. */
  --sidebar-width: 20rem;

  /* Shape and motion. NOT theme roles — these are the same whichever palette is
     loaded, which is why they live here and not in Theme.
     --radius was used in four rules and defined nowhere at all before this
     block existed. Those four elements had square corners the whole time and
     nobody noticed, which is the argument for the grep test in one sentence. */
  --radius: 12px;
  --radius-sm: 8px;
  --radius-pill: 999px;
  --ease: 150ms cubic-bezier(0.2, 0, 0.2, 1);

  /* A shape, not a colour: it is used as a mask, so what is painted through it
     is currentColor. Kept here with the other things every theme shares. */
  --icon-clear: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 3l10 10M13 3L3 13' stroke='%23000' stroke-width='2' stroke-linecap='round' fill='none'/%3E%3C/svg%3E");
}

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

/* The `hidden` attribute is display:none in the USER-AGENT stylesheet, and any
   author rule that sets display beats it no matter how weak the selector. So
   `.note__editor { display: grid }` rendered a form marked hidden fully visible
   — the note showed as its own source underneath itself, with correct markup
   the whole time. Nothing but looking at the page finds that one.

   !important is the documented fix for exactly this, and the reason `hidden`
   is otherwise unreliable in any stylesheet that uses flex or grid. */
[hidden] { display: none !important; }

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

/* color-scheme used to be `light dark` here, declared statically. It comes from
   the theme now (see themes.css), which reaches <html> via the sync script.
   Without JavaScript it stays on <body>, so form controls inside the page match
   and only the scrollbar does not — the tradeoff the partial documents. */

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 15px;
  line-height: 1.55;
}

a { color: var(--accent); }
a:hover { text-decoration: none; }

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

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

/* --- top bar ------------------------------------------------------------- */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 1.1rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  /* WRAPS, so the last item drops to a second line instead of hanging 2px off
     the side. It looked as though it already wrapped on a phone — the text
     inside each item was breaking — while the row itself squeezed everything
     and then overflowed anyway. Two pixels is still scrolling in two
     dimensions, which is the whole of WCAG 1.4.10. Measured while fitting the
     drawer: the last thing keeping every page 392px wide against a 390px
     viewport was Sign out. */
  flex-wrap: wrap;
}

.topbar__brand {
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--text);
  text-decoration: none;
}
.topbar__brand:hover { color: var(--accent); }

.topbar__account { display: flex; align-items: center; gap: 0.75rem; }
.topbar__email { color: var(--muted); font-size: 0.9rem; }
.topbar form { margin: 0; }

/* --- layout -------------------------------------------------------------- */

.main { padding: 1.5rem 1.1rem; max-width: 60rem; margin: 0 auto; }

.entry {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 2rem 1.1rem;
}

.card {
  width: 100%;
  max-width: 26rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1.75rem;
}

.card__title { margin: 0 0 1.25rem; font-size: 1.35rem; }
.card__footer { margin: 1.25rem 0 0; font-size: 0.9rem; }

.facts { margin: 1rem 0 0; padding-left: 1.1rem; color: var(--muted); }
.facts li { margin-bottom: 0.35rem; }

/* --- forms --------------------------------------------------------------- */

.form { display: grid; gap: 1rem; margin: 0; }
.field { display: grid; gap: 0.3rem; }
.field__label { font-size: 0.85rem; font-weight: 600; }
.field__hint { color: var(--muted); font-size: 0.82rem; line-height: 1.45; }

.input {
  width: 100%;
  padding: 0.6rem 0.7rem;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}

.input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

.input--code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.6rem;
  letter-spacing: 0.4em;
  text-align: center;
}

/* --- buttons ------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.6rem 1rem;
  font: inherit;
  font-weight: 600;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  text-decoration: none;
}

.btn:hover { border-color: var(--accent); }

.btn--primary {
  background: var(--accent);
  color: var(--accent-text);
  border-color: var(--accent);
}

.btn--ghost { background: none; }

.btn--block { width: 100%; }

/* button_to renders a form; without this the block buttons do not fill. */
form.button_to { margin: 0; }
form.button_to .btn--block { width: 100%; }

/* ...but zeroing that margin also leaves a button_to flush against whatever
   precedes it, which is how "Save password" and "I prefer magic links" ended up
   touching. Any button_to that is not the first thing in a card gets its space
   back. Adjacent margins collapse, so this does not double up where a .divider
   has already provided the gap. */
.card > * + form.button_to { margin-top: 1rem; }

.btn--google { width: 100%; }

.btn__glyph {
  display: inline-grid;
  place-items: center;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: var(--text);
  color: var(--surface);
  font-weight: 700;
  font-size: 0.8rem;
}

/* --- bits ---------------------------------------------------------------- */

.divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 1.1rem 0;
  color: var(--muted);
  font-size: 0.85rem;
}
.divider::before, .divider::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--border);
}

.identity {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  margin: 0 0 1.25rem;
  padding: 0.55rem 0.7rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.identity__email { font-weight: 600; overflow-wrap: anywhere; }
.identity__change { font-size: 0.85rem; margin-left: auto; }

.flash {
  padding: 0.7rem 0.9rem;
  margin: 0 0 1rem;
  border-radius: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
}
.flash--alert { border-left-color: var(--danger); }

.entry .flash { max-width: 26rem; width: 100%; }

/* --- development mailbox ------------------------------------------------- */

.mailbox { width: 100%; max-width: 60rem; }
.mailbox__item {
  display: grid;
  gap: 0.2rem;
  padding: 0.8rem 0;
  border-bottom: 1px solid var(--border);
}
.mailbox__subject { font-weight: 600; }
.mailbox__meta { color: var(--muted); font-size: 0.85rem; }
.mailbox__code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 700;
  letter-spacing: 0.2em;
}
.mailbox__frame {
  width: 100%;
  min-height: 32rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

/* --- workspace switcher --------------------------------------------------- */

/*
 * A <details> element, not a scripted dropdown. It opens, closes, and is
 * reachable from the keyboard with no JavaScript, which is the right trade for
 * a menu of links.
 */
.switcher { position: relative; }

.switcher__button {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
  list-style: none;
}
.switcher__button::-webkit-details-marker { display: none; }
.switcher__button:hover { border-color: var(--accent); }

.switcher__caret { color: var(--muted); font-size: 0.7rem; }

.switcher__panel {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 0;
  z-index: 20;
  min-width: 14rem;
  padding: 0.3rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-md);
}

.switcher__item {
  display: block;
  padding: 0.45rem 0.6rem;
  border-radius: 6px;
  color: var(--text);
  text-decoration: none;
}
.switcher__item:hover { background: var(--bg); }
.switcher__item.is-current { background: var(--accent); color: var(--accent-text); }

.switcher__item--new {
  margin-top: 0.3rem;
  border-top: 1px solid var(--border);
  border-radius: 0 0 6px 6px;
  color: var(--muted);
}

/* --- danger --------------------------------------------------------------- */

.card--danger { margin-top: 1.5rem; border-color: var(--danger); }

/* Outlined rather than filled.
   A solid --danger block is the loudest thing on any page it appears on, and on
   a dark theme --danger is a LIGHT colour, so Delete was shouting louder than
   Edit in the note toolbar. A destructive action should be findable and
   unmistakable, not the first thing the eye lands on — the confirmation is what
   makes it safe, not the shade.

   Filled on hover, where the pointer is already committed. */
.btn--danger {
  background: transparent;
  border-color: var(--danger);
  color: var(--danger);
}
.btn--danger:hover {
  background: var(--danger);
  color: var(--surface);
}

.input--emoji { max-width: 8rem; }

/* Cards stack in the main region. */
.main .card + .card { margin-top: 1.5rem; }

/* --- swatch pickers ------------------------------------------------------- */

/*
 * Presets, with the text field left alive beside them. Asking someone to
 * "figure out what to type" into a decoration field is not a reasonable
 * request; asking them to pick from sixteen, or type if they want something
 * else, is.
 */
.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-bottom: 0.15rem;
}

.swatch {
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--swatch, var(--surface));
  cursor: pointer;
  line-height: 1;
}

.swatch:hover { transform: scale(1.12); }

/* The chosen one gets a ring in the text colour rather than a colour of its
   own, so it reads against every swatch including ones close to the theme. */
.swatch.is-chosen {
  outline: 2px solid var(--text);
  outline-offset: 1px;
}

/* "None" reads as a symbol rather than as a colour: a circle with a line through
   it, which is what an empty swatch would otherwise be mistaken for — white. */
.swatch--none {
  border-radius: 50%;
  background:
    linear-gradient(
      to bottom left,
      transparent calc(50% - 1px), var(--muted) calc(50% - 1px),
      var(--muted) calc(50% + 1px), transparent calc(50% + 1px)
    );
}

.swatch--none.is-chosen { outline-offset: 2px; }

.swatch--emoji {
  width: 2rem;
  height: 2rem;
  font-size: 1.05rem;
  background: var(--bg);
}
.swatch--emoji:hover { background: var(--surface); }

.swatches--emoji { max-width: 100%; }

.color-row { display: flex; gap: 0.5rem; align-items: center; }

.color-row__native {
  width: 2.6rem;
  height: 2.35rem;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  cursor: pointer;
  flex: none;
}

/* Cards inside the app are wider than the sign-in box. 26rem is right for a
   login form and cramped for a settings page — it wrapped forty emoji onto five
   rows and sixteen colours onto two. */
.main .card { max-width: 38rem; }

/* --- the workspace's home page ------------------------------------------- */

/* docs/WORKSPACE-HOME-PLAN.md. The cap above is right for a form and wrong for
   a page of sections: a card holding a two-column list of facts inside an 832px
   column has 224px of it standing empty to the right. Lifted HERE rather than
   raised for everybody, because the settings form is the reason 38rem exists. */
.home .card { max-width: none; }

.home__summary { color: var(--muted); margin: 0 0 1.2rem; }

.home__section + .home__section { margin-top: 1.1rem; }

/* The title and the way to the whole list, on one line. */
.home__section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.home__more { font-size: 0.9rem; font-weight: 600; text-decoration: none; white-space: nowrap; }
.home__more:hover { text-decoration: underline; }

.home__recent { list-style: none; margin: 0.6rem 0 0; padding: 0; }

/* Each row is its own grid, with the last column a fixed width so the times at
   least line up: "31 minutes ago" and "1 day ago" are very different lengths,
   and a column that sizes to its own row puts six of them in six places. */
.home__note {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto 7.5rem;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0;
  border-top: 1px solid var(--border);
}

/* AND WHERE SUBGRID IS UNDERSTOOD, the whole list shares one set of tracks, so
   the folder column lines up too and no width has to be guessed at.

   The same shape as the pricing toggle's `:has()` a few thousand lines down:
   modern CSS with the fallback written out rather than assumed. Without this
   block the rule above still holds the list together — with subgrid alone it
   would not, because an unsupported `grid-template-columns: subgrid` is an
   invalid declaration and the row would fall back to stacking every cell.

   `display: contents` would share the tracks without @supports and cannot be
   used: a row that is not a box cannot carry the line above it. */
@supports (grid-template-columns: subgrid) {
  .home__recent {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
  }

  .home__note {
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
  }
}

.home__note:first-child { border-top: 0; }
.home__note-kind { display: inline-flex; color: var(--muted); }
.home__note-title { text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.home__note-title:hover { text-decoration: underline; }
.home__note-where, .home__note-when { font-size: 0.85rem; white-space: nowrap; }
.home__note-when { text-align: right; }

/* --- the tiles ------------------------------------------------------------ */

/* `auto-fill` rather than `auto-fit`: with auto-fit, a workspace showing four
   tiles stretches them across the whole card and the same page looks like a
   different design from one showing eight. Fill keeps the track width, so the
   grid grows downwards and a tile is the same size everywhere. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 0.6rem;
  margin-top: 0.9rem;
}

/* The whole tile is the link, so the target is the box rather than the word in
   it — a 44px hit area without a rule about hit areas. */
.tile {
  display: grid;
  grid-template-rows: auto auto auto;
  align-content: start;
  gap: 0.15rem;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg);
  color: inherit;
  text-decoration: none;
}

.tile:hover { border-color: var(--accent); }
.tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.tile__icon { display: inline-flex; color: var(--muted); }
.tile:hover .tile__icon { color: var(--accent); }
.tile__label { font-weight: 600; }
.tile__detail { color: var(--muted); font-size: 0.85rem; }

/* Two columns rather than one on a phone. A single column of eight tiles is a
   list wearing borders, and the labels are short enough to sit side by side. */
@media (max-width: 30rem) {
  .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Two columns where there is room for two. `auto` on the term rather than a
   fixed width: "Reading pictures" and "You" are very different lengths, and a
   guessed column either clips one or leaves a gap after the other. */
.home__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: 0.15rem 1.6rem;
  margin: 0.8rem 0 0;
}

.home__facts > div {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.6rem;
  padding: 0.35rem 0;
  align-items: baseline;
}

.home__facts dt { color: var(--muted); font-size: 0.85rem; font-weight: 600; }
.home__facts dd { margin: 0; }
.home__fact-link { font-size: 0.85rem; margin-left: 0.3rem; }

/* One column on a narrow window: two columns of a two-column list is four
   columns, and at 420px that is nothing anybody can read. */
@media (max-width: 40rem) {
  .home__note { grid-template-columns: auto minmax(0, 1fr) auto; }
  .home__note-where { display: none; }
}

/* --- the tree ------------------------------------------------------------- */

.layout {
  /* Clamped HERE as well as in the controller, so a stored value from a wider
     screen — or a hand-edited one — cannot make a column narrower than the tree
     can be read in or wider than the note can spare. The handle reads the same
     property, so the two cannot drift apart. */
  --sidebar-w: clamp(15rem, var(--sidebar-width), 30rem);
  position: relative;
  display: grid;
  /* minmax(0, 1fr) rather than 1fr: a grid track's automatic minimum is its
     CONTENT, so anything wider than the page — a map, a wide table, a long line
     of code — stretches the column instead of scrolling inside it. The map is
     where this first showed up: its frame grew to the full width of the drawing
     and never scrolled at all. */
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  align-items: start;
  min-height: calc(100dvh - 3.2rem);
}

.sidebar {
  position: relative;
  padding: 0.9rem 0.6rem;
  border-right: 1px solid var(--border);
  background: var(--surface);
  min-height: calc(100dvh - 3.2rem);
}

/* On the sidebar's own right edge, so it tracks the width without being told
   what the width is. Inside the sidebar rather than beside it, which is also
   what makes Esc from the handle mean "back to the note": the pane switch asks
   whether focus is inside the sidebar. */
.sidebar-handle {
  position: absolute;
  top: 0;
  right: -3px;
  width: 6px;
  height: 100%;
  cursor: col-resize;
  /* A thin strip, so the map's rule applies rather than the board's exception:
     nothing here needs to scroll under a finger. */
  touch-action: none;
}

.sidebar-handle:hover,
body.is-resizing-sidebar .sidebar-handle {
  background: var(--accent);
  opacity: 0.35;
}

.sidebar-handle:focus-visible,
.sidebar-handle:focus {
  outline: none;
  background: var(--accent);
  opacity: 0.75;
}

/* A drag that leaves the strip must not select the page it crosses. */
body.is-resizing-sidebar {
  cursor: col-resize;
  user-select: none;
}

.sidebar__actions { display: flex; gap: 0.4rem; margin-bottom: 0.8rem; }
.sidebar__actions .btn { flex: 1; text-align: center; padding: 0.4rem 0.5rem; font-size: 0.85rem; }

.main--with-sidebar { margin: 0; max-width: 52rem; }

/* PUT AWAY, above the breakpoint — docs/SIDEBAR-PLAN.md Phase 2.

   The class is on <html> and is written there before the first paint, so a
   sidebar you hid yesterday is not drawn today and then taken away again.

   `display: none` rather than a zero-width column: hidden means hidden, and it
   also means the tree cannot be tabbed into or focused, which a 0px column with
   its contents still in it would happily allow. The drawer rules below undo this
   deliberately — see the note there. */
html.sidebar-hidden .layout { grid-template-columns: minmax(0, 1fr); }
html.sidebar-hidden .sidebar { display: none; }

/* And the reading column CENTRES rather than staying hard against the left edge
   with the sidebar's width as blank space beside it. `.main--with-sidebar` caps
   itself at 52rem on purpose — prose set to the full width of a monitor is
   worse, not better — so hiding the sidebar gives a Markdown note no more room
   and every reason to look lopsided. A map, a board and a checklist carry
   `main--fill`, which lifts the cap, and those really do take the space. */
html.sidebar-hidden .main--with-sidebar { width: 100%; margin-inline: auto; }

/* `width: 100%` is not redundant beside `margin-inline: auto`, which is the
   trap. A grid item stretches to its track by default — but AUTO MARGINS TURN
   STRETCHING OFF, because the margins are what absorb the free space, and the
   item then sizes itself to its CONTENT. Measured: a note capped at 832px came
   out 681px wide, which is the width of its longest line. The width puts the
   stretch back, the max-width caps it, and the auto margins centre what is
   left. */

/* --- big enough to hit ------------------------------------------------------

   WCAG 2.2 SC 2.5.8 asks for 24 by 24 CSS pixels. Six controls were under it —
   docs/UI-IMPROVEMENTS.md §14 measured them at 13×18, 14×15, 19×16 and three at
   about 21 square — and two of the standard's exceptions were checked and did
   not apply: on a checklist row the twisty, the tick and the marker are within a
   few pixels of each other, so the SPACING exception fails, and `n` and `Space`
   are keyboard shortcuts rather than "a different control on the same page", so
   EQUIVALENT fails too.

   THE HIT AREA GROWS AND THE GLYPH DOES NOT. Nothing in a screenshot changes.
   The tick's circle is the one that could not simply be made bigger — it is a
   drawn thing with a suite of its own measuring it — so it keeps its size and
   gains an invisible 24px target around it. */
:root {
  /* 24px. `stylesheet_test` scans for a definition at the start of a line, and
     this was written on one line with its selector — so the property it defines
     was reported as used-but-never-defined, which is exactly the bug that test
     exists for and exactly the shape it cannot see. */
  --target: 1.5rem;
}

/* min- rather than width, because both of these set their own width further
   down the file and a later rule of equal specificity wins. A minimum cannot be
   argued with by a later `width`, which is the property that says "at least
   this" and is the one this rule means. */
.checklist__twisty,
.tree__action {
  min-width: var(--target);
  min-height: var(--target);
}

.checklist__has-note,
.board-card__has-note {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--target);
  min-height: var(--target);
  padding: 0;
}

.map__has-note {
  display: grid;
  place-items: center;
  top: 0;
  right: 0;
  width: var(--target);
  height: var(--target);
  padding: 0;
}

/* The circle stays 1.3rem, which `checkbox-geometry.mjs` measures and the design
   chose. The target around it is this: transparent, centred, and belonging to
   the input, so a press anywhere in it is a press on the checkbox. */
.task-list__checkbox { position: relative; }

.task-list__checkbox::before {
  content: "";
  position: absolute;
  /* Sized and centred explicitly rather than with `inset`. The checkbox is an
     `inline-grid`, so an absolutely positioned child resolves its offsets
     against the GRID AREA — which for `place-content: center` with no tracks is
     a zero-size box at the middle. `inset` measured out to nothing; this only
     needs the origin to be right. */
  left: 50%;
  top: 50%;
  width: var(--target);
  height: var(--target);
  transform: translate(-50%, -50%);
}

/* --- the sidebar as a drawer ---------------------------------------------

   docs/SIDEBAR-PLAN.md Phase 1. `.layout` above is a two-column grid and there
   was no media query anywhere in this stylesheet that touched it, so at 390px
   the sidebar took 320 of them and the note was left 70 — on every page, all
   four note types. docs/UI-IMPROVEMENTS.md §1 has the measurements.

   Above the breakpoint none of this exists: the sidebar is simply part of the
   page, and the three controls below are drawn only where there is something
   for them to do. A control that does nothing is worse than no control. */
.sidebar__close,
.sidebar-scrim { display: none; }

.pane-toggle { display: inline-flex; }

/* A row of its own, at the leading edge of the content and hard against the
   sidebar's edge. Slim: it holds one small control, and on a phone every row
   costs something. */
/* NO ROW OF ITS OWN — reported. A whole line for one small button pushed every
   page's title and every note's controls down a level. It is the first item in
   the header's own flex row now, which also means it lines up with the title
   rather than with an offset guessed from three paddings that have nothing to
   do with each other. */

.pane-toggle,
.sidebar__close {
  align-items: center;
  justify-content: center;
  /* 24px, so it is not one of the six targets docs/UI-IMPROVEMENTS.md §14
     found under the line. This one is new, so it starts on the right side of
     it. */
  min-width: 2.25rem;
  min-height: 2.25rem;
  padding: 0;
  /* A REAL BORDER. A bare glyph on the page background reads as decoration —
     NN/g's finding about hamburger menus is that the problem is low salience,
     and "a small icon is simply harder to notice regardless of labeling". This
     is the same border every other button in the application wears, so it looks
     like the pressable thing it is. */
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--muted);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.pane-toggle:hover,
.sidebar__close:hover { background: var(--bg); color: var(--text); }

.pane-toggle:focus-visible,
.sidebar__close:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.sidebar__close { margin: 0 0 0.4rem auto; }

/* Points LEFT while the sidebar is showing — press it and the tree goes that
   way — and right once it has gone. `chevron` is drawn pointing right, so the
   rotation is the showing state. One fact, read from aria-expanded, so the icon
   cannot disagree with what the button says it does. */
.pane-toggle__icon {
  display: inline-flex;
  transition: transform 120ms ease-out;
}

.pane-toggle[aria-expanded="true"] .pane-toggle__icon { transform: rotate(180deg); }

@media (prefers-reduced-motion: reduce) {
  .pane-toggle__icon { transition: none; }
}

@media (max-width: 48rem) {
  .layout { grid-template-columns: minmax(0, 1fr); }

  /* Below the breakpoint the sidebar is a drawer, and a drawer is never "put
     away" — it is already away, and the button opens it. Undoing the rule above
     here rather than fencing it behind a min-width keeps the breakpoint written
     down once: this block is later, so it wins.

     Without it, hiding the sidebar on a laptop and then narrowing the window
     left a drawer with `display: none` and a button that opened nothing. */
  html.sidebar-hidden .sidebar { display: block; }

  /* A drawer has no width to drag: it is `min(20rem, 85vw)` and the screen
     decides. */
  .sidebar-handle { display: none; }

  /* Block-level flex, not inline: `margin-left: auto` on an inline-level box
     does nothing, and the close sat on the left of a drawer that opens from the
     left — the one corner where it reads as part of the tree rather than as the
     way out of it. */
  .sidebar__close { display: flex; }

  /* FULL HEIGHT, top bar included. The top bar wraps to three lines at 390px —
     brand, switcher, name, sign out — so a drawer offset by a fixed header
     height would be wrong by however much it happened to wrap that day. Covering
     it is the standard drawer and it needs no number; what it costs is the
     button that opened it, which is why the drawer has a close of its own. */
  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(20rem, 85vw);
    min-height: 0;
    z-index: 40;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Not merely translated away. An off-screen drawer that is still visible to
       the layout is still TABBABLE, so a keyboard lands in a tree nobody can
       see — which is the same bug as a dimmed card that leaves find-on-page,
       one element larger. */
    visibility: hidden;
    transform: translateX(-100%);
  }

  body.is-drawer-open .sidebar {
    visibility: visible;
    transform: none;
  }

  .sidebar-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 30;
    background: color-mix(in srgb, var(--text) 45%, transparent);
  }

  /* After the rule above, so `hidden` still means hidden. */
  .sidebar-scrim[hidden] { display: none; }

  /* The page behind the scrim does not scroll under it. */
  body.is-drawer-open { overflow: hidden; }
}

@media (max-width: 48rem) and (prefers-reduced-motion: no-preference) {
  /* `visibility` is a DISCRETE property. Transitioning it plainly flips the
     value at the HALFWAY point, so the drawer stayed hidden for 90ms after being
     opened — and `focus()` does nothing to a hidden element, so focus landed
     nowhere and the keyboard was left behind by the drawer it was supposed to be
     following. Found by the suite, not by reading it.

     So: delayed to the end of the slide when closing, which is what keeps the
     drawer on screen while it leaves, and applied at once when opening. */
  .sidebar { transition: transform 180ms ease-out, visibility 0s linear 180ms; }

  body.is-drawer-open .sidebar {
    transition: transform 180ms ease-out, visibility 0s linear 0s;
  }
}

/* The note's kind, beside its name. Muted and small: it is there to be scanned
   past, not read — the row's own name is the thing being looked for, and the
   glyph only has to say which of four kinds it will open into. */
.tree__kind,
.result__kind {
  display: inline-flex;
  flex: none;
  color: var(--muted);
  opacity: 0.75;
}

/* The icon column, the same box on a folder and on a note. Every row now carries
   a chevron gutter before it, so the labels line up because the columns do —
   there is no number here measured against anything else. */
.tree__kind {
  width: 1.2rem;
  height: 1.35rem;
  display: grid;
  place-items: center;
  margin-right: 0.3rem;
}

/* An emoji stands in for the folder glyph, so it has to sit in the same box
   rather than push the name along. */
.tree__emoji { font-size: 0.95rem; line-height: 1; }
.result__kind { margin-right: 0.3rem; vertical-align: -0.15em; }

.tree__row.is-current .tree__kind { color: inherit; opacity: 0.9; }

.tree, .tree__children { list-style: none; margin: 0; padding: 0; }
.tree__item { position: relative; }

/* One level of nesting is the width of a chevron. It was 0.85rem, which was
   always a thin signal for "contained" and only survived because a note's own
   inset was quietly doing the work — badly, and for the wrong pair of rows. With
   the marks aligned, depth rests on this alone, so it is worth the width. */
.tree__children { position: relative; padding-left: 1.35rem; }

/* And containment is DRAWN rather than inferred: a rule down the left of each
   open folder's children, under the middle of the chevron that opened them. In
   a long tree "which folder is this in?" stops being a question you answer by
   comparing indents. */
.tree__children::before {
  content: "";
  position: absolute;
  left: 0.62rem;
  top: 0;
  bottom: 0;
  border-left: 1px solid var(--border);
}

.tree__row {
  display: flex;
  align-items: center;
  gap: 0.1rem;
  border-radius: 6px;
  min-height: 1.9rem;
  padding-right: 0.15rem;
}
.tree__row:hover { background: var(--bg); }

/* WHERE THE KEYBOARD IS. `:focus` rather than `:focus-visible`, because focus
   only ever arrives on a row from a key or from Esc handing it over — every row
   is tabindex="-1" except the one the cursor is on, and clicking a row clicks
   the link inside it. There is no mouse focus here to be quiet about.

   An outline rather than a background: the current note already fills its row
   with the accent, and a cursor that could not be seen on the row you most often
   start from would be a cursor you cannot trust. */
.tree__item:focus { outline: none; }

.tree__item:focus > .tree__row {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 6px;
}
.tree__row.is-current { background: var(--accent); color: var(--accent-text); }
.tree__row.is-current .tree__link, .tree__row.is-current .tree__action { color: inherit; }

/* A path node: visible only because something inside it is readable. It is a
   name, and nothing else — no link, no actions, no drop target. */
.tree__row.is-path { opacity: 0.55; }
.tree__label--path { padding: 0.25rem 0.3rem; font-style: italic; cursor: default; }

/* Sized to the row's text rather than to a fraction of it. The old triangle was
   a 0.7rem "▸" glyph, which is whatever size the browser's fallback font draws
   it — usually far smaller than it looks in the stylesheet, and never the same
   twice across platforms. A drawn chevron is the size it says it is. */
/* THE COLUMN, not the control. Every row has one; only a folder puts a button in
   it. They were the same class for an afternoon, and an existing test caught the
   difference at once: "nothing inside a row is tabbable on its own" reads
   `.tree__disclosure`, and an empty span wearing that name is not a control that
   forgot its tabindex — it is not a control. */
.tree__gutter {
  flex: none;
  display: grid;
  place-items: center;
  width: 1.35rem;
  height: 1.35rem;
}

.tree__disclosure {
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--muted);
  cursor: pointer;
}
button.tree__disclosure:hover { background: var(--bg); color: var(--accent); }

.tree__twisty {
  display: flex;
  transition: transform 0.12s ease;
  transform: rotate(90deg);
}
.tree__disclosure.is-collapsed .tree__twisty { transform: rotate(0deg); }

.tree__link {
  flex: 1;
  min-width: 0;
  /* A row, so the kind's glyph sits BESIDE the name. The label is a block — it
     has to be, for the ellipsis — so anything before it started a line of its
     own. A folder's emoji never showed this because it lives inside the label. */
  display: flex;
  align-items: center;
  padding: 0.25rem 0.3rem;
  color: var(--text);
  text-decoration: none;
  font-size: 0.9rem;
}
.tree__link:hover { text-decoration: none; }

/* THREE COLUMNS, THE SAME ON EVERY ROW — docs/TREE-INDENT-PLAN.md §9.
   
   A chevron's gutter, an icon, then the name. A folder puts a chevron in the
   first and its folder glyph in the second; a note leaves the first empty and
   puts its type icon in the second. Nothing is tuned against anything, because
   the boxes are the same boxes.
   
   This replaced a version where a note's icon TOOK the chevron's column, which
   aligned the rows and left the chevron sharing space with an icon. Reported
   while using it, with a picture of OmniFocus: the fold control wants a column
   of its own, and then a folder needs an icon like everything else. */


.tree__label {
  display: block;
  /* min-width: 0 so the ellipsis still works now that this is a flex item —
     a flex item's automatic minimum is its content, which would push the row
     wider instead of truncating. Trap one, in its usual disguise. */
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tree__row-actions {
  display: flex;
  align-items: center;
  opacity: 0;
  transition: opacity 0.1s ease;
}
.tree__row:hover .tree__row-actions,
.tree__row:focus-within .tree__row-actions { opacity: 1; }

.tree__action-form { margin: 0; display: inline-flex; }

.tree__action {
  display: inline-grid;
  place-items: center;
  width: 1.3rem;
  height: 1.3rem;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--muted);
  font-size: 0.85rem;
  cursor: pointer;
  text-decoration: none;
}
.tree__action:hover { background: color-mix(in srgb, var(--text) 14%, transparent); text-decoration: none; }
.tree__action--nudge { font-size: 0.55rem; }
.tree__action[disabled] { opacity: 0.25; cursor: default; }
.tree__action[disabled]:hover { background: none; }

/* --- drag indicators ------------------------------------------------------ */

.tree__item.is-dragging { opacity: 0.4; }

.tree__row.is-drop-before { box-shadow: inset 0 3px 0 0 var(--accent); }
.tree__row.is-drop-after  { box-shadow: inset 0 -3px 0 0 var(--accent); }
.tree__row.is-drop-inside {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 6px;
}

.tree__root-drop {
  margin-top: 0.6rem;
  padding: 0.7rem 0.5rem;
  border: 1px dashed var(--border);
  border-radius: 8px;
  color: var(--muted);
  font-size: 0.8rem;
  text-align: center;
}
.tree__root-drop.is-drop-target { border-color: var(--accent); color: var(--accent); }

.tree__empty { padding: 0.6rem 0.3rem; font-size: 0.85rem; }

/* --- notes ---------------------------------------------------------------- */

.note { padding: 1.5rem 1.1rem; }

/* "This note was updated — reload."
   Warn-coloured rather than danger: nothing has gone wrong and nothing has been
   lost. It is news, and news you can act on when you are ready. */
.freshness {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin: 0 0 1.1rem;
  padding: 0.6rem 0.85rem;
  border: 1px solid var(--border);
  border-left: 3px solid var(--warn);
  border-radius: 8px;
  background: var(--warn-soft);
  font-size: 0.9rem;
}
.freshness[hidden] { display: none; }   /* or flex would win and show it anyway */
.freshness .btn { margin-left: auto; }

/* CONTROLS FIRST, TITLE UNDER THEM.
   Reported: the toggle belongs inline with the other buttons, and the title
   belongs below the row they are in.

   Better than the title sharing that line, for a reason the measurements made
   obvious: with the title beside them the six action buttons had about 600px
   and wrapped to two rows below 1440px. Given the whole width they fit, and a
   long title no longer squeezes them.

   Done with `order` and a full-width basis rather than by restructuring twelve
   templates — and the DOM order is the right one anyway: the heading before the
   controls is what somebody navigating by heading wants to meet. */
.note__header {
  display: flex;
  align-items: center;
  /* Matching `.note__actions`' own gap, so the toggle reads as one more button
     in the row rather than as something parked beside it. */
  gap: 0.5rem;
  flex-wrap: wrap;
  /* Everything to the far end, and the toggle pushed back to the near one — see
     the margin below. `flex-end` rather than an auto margin on the first action
     because the run WRAPS: an auto margin only pushes the line it lands on, so a
     second row of buttons would have come back to the left and read as a
     different group. This way every row of them is anchored to the same edge. */
  justify-content: flex-end;
}

/* The one control that is not about this note — it is about the pane beside it —
   so it sits at the leading edge on its own while the note's own buttons gather
   at the far one. `margin-right: auto` eats the free space between them. */
.note__header > .pane-toggle { order: 1; margin-right: auto; }

/* `display: contents` dissolves the actions' own box, so its buttons become
   items of the HEADER's row rather than one indivisible block beside the
   toggle. Without it the whole group wrapped away the moment it did not fit,
   and the toggle was left alone on a line — the row of its own this change
   exists to remove. Now they wrap as what they are: one run of buttons.

   Safe on a plain <div>: `display: contents` is a problem on elements whose box
   carries a role, and this one carries none. The parent's gap replaces its own. */
.note__header > .note__actions { display: contents; }
.note__header > .note__actions > * { order: 2; }

.note__header > .note__title {
  order: 3;
  /* A line of its own: nothing can share it, so nothing is squeezed by a long
     one. */
  flex-basis: 100%;
  margin-top: 0.5rem;
}

.note__title { margin: 0; font-size: 1.7rem; letter-spacing: -0.02em; }
.note__meta { margin: 0.35rem 0 1.25rem; color: var(--muted); font-size: 0.85rem; }
/* WRAPS. The header itself wrapped, so the buttons dropped below the title and
   looked handled — but this row never broke, so six buttons at a narrow width
   simply ran off the side and took the page's width with them. Measured while
   fitting the drawer: the note page was 896px wide against an 800px viewport,
   and 576 against 390, on every type. docs/UI-IMPROVEMENTS.md §9 saw the shape
   of this and read it as a layout that changes between note types; it is also
   the reason the page scrolls sideways on a phone. */
.note__actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.note__actions form { margin: 0; }
.note__editor { display: grid; gap: 0.85rem; }

.input--title { font-size: 1.35rem; font-weight: 700; }
.input--body {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.92rem;
  line-height: 1.6;
  resize: vertical;
}

/* Quick Add: one box, two destinations. */
.quick-add {
  margin-top: 2.5rem;
  padding: 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
}
.quick-add__heading { margin: 0 0 0.6rem; font-size: 0.95rem; }
.quick-add__form { display: grid; gap: 0.6rem; }
/* Wraps, like every other row of controls here. It did not need to when it held
   two buttons and a quiet third; adding the table control pushed it past a 320px
   screen and the note scrolled sideways. `.uploader__bar` next to it has wrapped
   all along. */
.quick-add__actions { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }

.form__actions { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; }
.form__spacer { margin-left: auto; }

/* --- the workspace accent ------------------------------------------------- */

/*
 * Identification, not theming. --workspace-accent marks WHICH workspace you are
 * looking at; --accent remains the user's theme colour and keeps carrying the
 * contrast guarantees for buttons, focus rings and links. Letting a workspace
 * colour override the theme would put arbitrary user-chosen hex behind white
 * text.
 */
.switcher__dot {
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--workspace-accent, var(--accent));
  flex: none;
}

/* On the current row the background is already the theme accent, so a coloured
   dot on top of it would be unreadable. */
.switcher__item.is-current .switcher__dot { background: var(--accent-text); }

/* --------------------------------------------------------------- markdown */

/* Shown only on paper — docs/PRINTING-PLAN.md. print.css turns it back on, and
   it has to be hidden HERE because that sheet is loaded with media="print" and
   never reaches a screen at all. */
.print-only { display: none; }

.markdown { overflow-wrap: break-word; }
.markdown > :first-child { margin-top: 0; }

.markdown h1, .markdown h2, .markdown h3,
.markdown h4, .markdown h5, .markdown h6 {
  margin: 1.8em 0 0.6em;
  line-height: 1.3;
  letter-spacing: -0.015em;
}
.markdown h1 { font-size: 1.7rem; }
.markdown h2 { font-size: 1.35rem; padding-bottom: 0.25em; border-bottom: 1px solid var(--border); }
.markdown h3 { font-size: 1.15rem; }

.markdown p, .markdown ul, .markdown ol, .markdown blockquote { margin: 0 0 1em; }
.markdown li { margin: 0.25em 0; }

.markdown blockquote {
  padding: 0.2rem 0 0.2rem 1rem;
  border-left: 3px solid var(--accent);
  color: var(--muted);
}

.markdown code {
  padding: 0.15em 0.4em;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.88em;
}

.markdown pre {
  margin: 0 0 1em;
  padding: 0.9rem 1rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow-x: auto;
}
.markdown pre code { padding: 0; background: none; border: 0; }

/* A drawn Mermaid diagram — docs/MERMAID-PLAN.md Phase 1.
   The controller is on the <pre>, so the element that held the source is the
   element that holds the drawing, and it has to stop looking like a code block
   the moment it does. Only ever added by JavaScript after a successful render:
   a fence that has not drawn, or cannot, keeps every rule above. */
.markdown pre.diagram {
  padding: 0.5rem;
  background: var(--surface);
  border-color: var(--border-soft);
  /* A <pre> is preformatted text and an SVG is not; without this the drawing
     inherits a monospace baseline and the figure sits on it. */
  white-space: normal;
  text-align: center;
}
.diagram__drawing { display: block; }
/* Wide diagrams scroll rather than overflowing the note, and no diagram is ever
   taller than a screen — Mermaid draws at whatever size the graph needs. */
.diagram__drawing svg { max-width: 100%; height: auto; }

/* Said under the code block that stayed. Quiet, because a half-written diagram
   in a note is a normal thing to find and not an error the reader caused. */
/* The live preview — docs/MERMAID-PLAN.md §5.
   ABOVE the textarea, so it is in the same glance as the fence being typed
   rather than under twenty-four rows of text. Built by the controller and
   removed when the caret leaves a fence, so there is no empty box in the editor
   of a note that has no diagram in it. */
.diagram-preview {
  margin: 0 0 0.5rem;
  padding: 0.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  /* A diagram grows with its graph, and the thing being previewed must not push
     the text off the screen. */
  max-height: 45vh;
  overflow: auto;
  text-align: center;
}
.diagram-preview__drawing svg { max-width: 100%; height: auto; }

/* Below the Save and Cancel buttons, in the note's own editor. Nothing above it
   moves when it appears, which is the whole point of putting it there: the panel
   editors are compact sheets where a preview above the field is in the same
   glance, and a twenty-four row field with a form above it is not. */
.diagram-preview--below { margin: 0.75rem 0 0; }

/* The last drawing that worked, while the syntax is broken. Dimmed rather than
   removed: half-written syntax is the normal state of an editor, and flashing
   the panel empty on the way to every valid diagram would make it useless
   exactly where it is most useful. */
.diagram-preview__drawing.is-stale { opacity: 0.4; }

.diagram-preview__said {
  margin: 0.4rem 0 0;
  color: var(--muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.78rem;
  text-align: left;
  white-space: pre-wrap;
}

.diagram__error {
  margin: -0.6em 0 1em;
  padding: 0.4rem 0.6rem;
  color: var(--muted);
  background: var(--bg);
  border-left: 3px solid var(--warn);
  border-radius: 0 5px 5px 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8rem;
  white-space: pre-wrap;
}

.markdown table {
  width: 100%;
  margin: 0 0 1em;
  border-collapse: collapse;
  display: block;
  overflow-x: auto;
}
.markdown th, .markdown td {
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--border);
  text-align: left;
}
.markdown th { background: var(--bg); }

/* Column alignment — docs/TABLES-PLAN.md §1.1. Written as a class by
   MarkdownRenderer rather than as the inline style Redcarpet would have
   produced, because that style is not an allowed attribute and was silently
   stripped: every column came out left-aligned however the Markdown was
   written. A column that asks for nothing gets no class and keeps the default. */
/* "Add a table", and the two numbers it asks for — docs/TABLES-PLAN.md Phase 1.
   Inline in the editor's button bar rather than in an overlay: two questions do
   not earn a modal. */
.table-maker { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.table-maker__form { display: inline-flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.table-maker__field {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--muted);
  font-size: 0.82rem;
}
/* Wide enough for two digits and the spinner the browser puts beside them. */
.table-maker__number { width: 4.5rem; }

.markdown .md-left { text-align: left; }
.markdown .md-center { text-align: center; }
.markdown .md-right { text-align: right; }

/* Task lists. The checkbox replaces the bullet, so the marker is dropped.
   Nested lists keep a real indent — the pull-back is only for a list sitting
   at the top level, where the checkbox should line up with body text. Scoping
   that to a direct child matters: applied to every level it flattens the
   nesting, which looks exactly like nested lists not working at all. */
.markdown .task-list { list-style: none; padding-left: 1.4rem; }
.markdown > .task-list { padding-left: 0.1rem; }

.task-list__item { margin: 0.2em 0; }

/* A list nested inside an item should not also carry the 1em gap that
   separates top-level blocks. */
.markdown li > ul,
.markdown li > ol { margin-top: 0.25em; margin-bottom: 0; }

/* A ring in a darker tone of the theme's accent, with a checkmark inside once
   ticked. The centre is transparent rather than a named colour so it matches
   whatever it happens to be sitting on, in any theme.

   `color` drives both the ring and the checkmark through currentColor, so the
   two can never drift apart. The pair of declarations is a deliberate
   fallback: a browser that does not understand color-mix() drops the second at
   parse time and keeps the plain accent. (Written out rather than held in a
   custom property on purpose — a var() containing an unsupported value is
   invalid at computed-value time, which would blank the colour instead of
   falling back to the line above.)

   The tone is derived from --accent rather than listed in the palette, so a
   custom theme gets it for free without the user having to choose one. */
.task-list__checkbox {
  appearance: none;
  -webkit-appearance: none;
  display: inline-grid;
  place-content: center;
  flex: none;
  width: 1.3rem;
  height: 1.3rem;
  margin-right: 0.55rem;
  /* `middle` aligns the circle's centre with the middle of the text, measured
     at under 1px out. A negative em offset was ~9px low, which reads as the
     text floating above the circle rather than sitting in line with it. */
  vertical-align: middle;
  border-radius: 50%;
  background: transparent;
  color: var(--accent);
  color: var(--accent-strong);
  border: 2px solid currentColor;
  cursor: pointer;
  transition: filter 120ms ease;
}

/* The tick: two borders of an empty box, rotated into a checkmark. */
.task-list__checkbox::after {
  content: "";
  width: 0.45rem;
  height: 0.24rem;
  margin-top: -0.12rem;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  border-radius: 1px;
  transform: rotate(-45deg);
  opacity: 0;
  transition: opacity 90ms ease;
}
.task-list__checkbox:checked::after { opacity: 1; }

.task-list__checkbox:hover { filter: brightness(1.25); }
.task-list__checkbox:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.task-list__checkbox[disabled] { cursor: default; opacity: 0.65; }
.task-list__checkbox[disabled]:hover { filter: none; }

/* Done items recede without disappearing. */
.task-list__item.is-done { color: var(--muted); text-decoration: line-through; }
.task-list__item.is-done .task-list__checkbox { text-decoration: none; }

/* "3 of 7 done", above the note. The bar is the glance and the words are the
   answer, so the words come first and the bar takes what room is left. */
/* THE WAY BACK, under the frame — docs/UNDO-PLAN.md §6.
   In the flow rather than floating, which is what lets it stay until the next
   change instead of timing out. A bar hovering over the bottom row of a board
   would have to expire or it covers something; this one covers nothing, so it
   can wait as long as somebody needs to decide. */
.undo-bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0.75rem 0 0;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 0.85rem;
}

/* The sentence takes the room and the button keeps its own, so a long row name
   does not push Undo off the end of the line. */
.undo-bar__what {
  flex: 1;
  min-width: 0;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.task-progress {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0 0 1rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.task-progress__count { flex: 0 0 auto; font-variant-numeric: tabular-nums; }

/* min-width:0 because a flex item's floor is its content, and this one has
   none — without it the track refuses to shrink below its own default width
   and pushes the count off the end on a narrow screen. */
.task-progress__track {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 16rem;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  overflow: hidden;
}

.task-progress__fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  transition: width var(--ease);
}

.markdown hr { border: 0; border-top: 1px solid var(--border); margin: 2em 0; }
.markdown img { max-width: 100%; height: auto; border-radius: 8px; }

/* --- diffs, history and conflicts ----------------------------------------- */

/*
 * Added and removed lines are tinted from the theme rather than from fixed
 * greens and reds: colour-blind readers and dark themes both need the +/− marks
 * and the line numbers to carry the meaning, so the tint is a hint and never
 * the only signal.
 */
.diff {
  margin: 0 0 1.5rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  line-height: 1.55;
}

.diff__line {
  display: grid;
  grid-template-columns: 3.2rem 3.2rem 1.2rem 1fr;
  align-items: baseline;
  padding: 0.05rem 0.5rem 0.05rem 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.diff__number {
  text-align: right;
  padding-right: 0.6rem;
  color: var(--muted);
  opacity: 0.7;
  user-select: none;
}

.diff__mark { text-align: center; font-weight: 700; user-select: none; }

.diff__line--added   { background: var(--ok-soft); }
.diff__line--removed { background: var(--danger-soft); }
.diff__line--added .diff__mark   { color: var(--ok); }
.diff__line--removed .diff__mark { color: var(--danger); }

.diff__gap {
  padding: 0.3rem 0.7rem;
  background: var(--bg);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  color: var(--muted);
  font-size: 0.78rem;
}
.diff__gap:first-child { border-top: 0; }

.section__heading { margin: 1.75rem 0 0.7rem; font-size: 1.05rem; }

/* --- the history list --- */

.history { list-style: none; margin: 0; padding: 0; }

.history__entry {
  padding: 0.7rem 0;
  border-bottom: 1px solid var(--border);
}

.history__head {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  flex-wrap: wrap;
}

.history__who { font-weight: 600; }
.history__when { color: var(--muted); font-size: 0.85rem; }

.history__badge {
  padding: 0.05rem 0.45rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--muted);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.history__count {
  margin-left: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.82rem;
}
.history__added { color: var(--ok); }
.history__removed { color: var(--danger); margin-left: 0.4rem; }

.btn--small { padding: 0.3rem 0.6rem; font-size: 0.85rem; }

.history__more { margin: 0.4rem 0 0; }
.history__more summary { cursor: pointer; color: var(--muted); font-size: 0.85rem; }
.history__runs { margin: 0.4rem 0 0; padding-left: 1.1rem; font-size: 0.88rem; }
.history__runs li { margin: 0.15rem 0; }

.compare { display: flex; align-items: center; gap: 0.6rem; margin: 0 0 1rem; }
.compare__label { font-size: 0.85rem; font-weight: 600; }
.compare__select { max-width: 26rem; }

.card--inline { max-width: none; margin: 1.5rem 0; }

/* A version other than the current one is shown read-only; the tint says so
   without a banner nobody reads. */
.markdown--frozen {
  padding: 1rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
}

/* --- the conflict panel --- */

.conflict {
  margin: 0 0 1.5rem;
  padding: 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--danger);
  border-radius: 12px;
}
.conflict__heading { margin: 0 0 0.8rem; font-size: 1.05rem; }
.conflict__hint { margin: 0 0 0.8rem; color: var(--muted); font-size: 0.9rem; }
.conflict__actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }

/* The chosen resolution. A ring in the text colour rather than a fill, so it
   reads on every theme and does not compete with the primary Save button — the
   choice here is not the action, it is what the action will use. */
.conflict__actions .btn.is-chosen {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
  font-weight: 700;
}

/* --- uploading images ----------------------------------------------------- */

.uploader { position: relative; display: grid; gap: 0.5rem; }

/* The whole editor block is the drop target, so a file dropped anywhere near it
   lands rather than navigating the browser away to the image. */
.uploader.is-dropping::after {
  content: "Drop to add to this note";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border: 2px dashed var(--accent);
  border-radius: 10px;
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
  color: var(--accent);
  font-weight: 600;
  pointer-events: none;
}

.uploader__bar { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }

/* Off-screen rather than display:none — a hidden file input still has to be
   reachable by the button that clicks it. */
.uploader__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.uploads { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.3rem; }

.upload-chip {
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--muted);
  font-size: 0.82rem;
  justify-self: start;
}
.upload-chip.is-done { border-color: var(--ok); color: var(--ok); }
.upload-chip.is-failed { border-color: var(--danger); color: var(--danger); }

/* --- the file browser ----------------------------------------------------- */

/* A destination, not a caption. It was set smaller than the tree it sits above,
   which made the one link in the sidebar look like a label for the list beneath
   it — and put a rule above itself, grouping it with the buttons instead. */
.sidebar__link {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin: 0.15rem 0 0.6rem;
  padding: 0.4rem 0.45rem;
  border-radius: 6px;
  color: var(--text);
  font-size: 0.95rem;
  font-weight: 600;
  text-decoration: none;
}
.sidebar__link:hover { background: var(--bg); color: var(--accent); }
.sidebar__link .icon { color: var(--muted); }
.sidebar__link:hover .icon { color: var(--accent); }

/* The rule goes BELOW it, separating a place you can go from the things you
   have made. */
.sidebar__divider {
  height: 1px;
  margin: 0 0.2rem 0.6rem;
  background: var(--border);
}

.files { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.9rem; }

.files__item {
  display: flex;
  gap: 0.9rem;
  align-items: flex-start;
  padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--border);
}

.files__thumb {
  flex: none;
  width: 84px;
  height: 84px;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: var(--bg);
}
.files__thumb img { width: 100%; height: 100%; object-fit: cover; }

/* MAKING A NEW THING — docs/MAKING-THINGS-PLAN.md.
   A <details> menu beside the folder button, built like the workspace switcher
   because it is the same thing: a menu of links that must work without any
   JavaScript. */
.maker { position: relative; }

/* IN THE SIDEBAR ONLY, where two buttons share a row and should share it evenly.
   The flex child is the <details> now rather than the button inside it, so the
   rule that does that no longer reaches the button on its own — and "+ New" came
   out half the width of "+ Folder" until this existed.

   Not everywhere, though: a folder's page puts the same menu in a header of
   ordinary-width buttons, and `flex: 1` there stretched "+ New" across most of
   the page. Seen in a screenshot, not in a test. */
.sidebar__actions .maker { flex: 1; }

/* A summary is a list-item by default, marker and all — and `.btn` makes it an
   INLINE-flex, so it sizes to its text rather than to the details around it.
   Which is why "+ New" came out half the width of "+ Folder" even once the
   details itself was sharing the row. */
.maker__button {
  /* Fills whatever the details is given — which is half the sidebar's row, and
     its own width anywhere else. */
  width: 100%;
  justify-content: center;
  list-style: none;
  cursor: pointer;
}
.maker__button::-webkit-details-marker { display: none; }

.maker__caret { margin-left: 0.15rem; font-size: 0.75em; }

.maker__menu {
  position: absolute;
  z-index: 20;
  left: 0;
  top: calc(100% + 0.35rem);
  min-width: 15rem;
  margin: 0;
  padding: 0.3rem;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
}

.maker__choice {
  display: block;
  padding: 0.4rem 0.5rem;
  border-radius: var(--radius-sm);
  color: inherit;
  text-decoration: none;
}

.maker__choice:hover,
.maker__choice:focus-visible { background: var(--bg); }

.maker__name { display: block; font-weight: 600; font-size: 0.9rem; }

/* What each is FOR, which is the question somebody choosing actually has. */
.maker__hint { display: block; color: var(--muted); font-size: 0.8rem; }

.form__making { margin: 0 0 1rem; }

/* THE BIN — docs/UNDO-PLAN.md §11.
   One line a note, with everything about it on a second, quieter one, and the
   actions at the end. Sized by its content rather than by columns: a grid here
   meant every row was as tall as the longest thing that could ever be in it. */
.bin { list-style: none; margin: 0; padding: 0; }

.bin__item {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.7rem 0;
  border-bottom: 1px solid var(--border);
}

/* min-width so a long title shortens the flex item rather than pushing the
   buttons off the end — a flex item's floor is its content otherwise. */
.bin__detail { flex: 1; min-width: 0; }

/* ONE LINE EACH, CLIPPED. A row whose height depends on the length of somebody's
   title — or of the name of whoever deleted it — is a list that cannot be
   scanned: five notes came to five different heights, and one long address took
   three lines on its own. The full title is on the element's `title`, and the
   part an ellipsis eats is the part ordered last for exactly that reason. */
.bin__name,
.bin__meta {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bin__name { margin-bottom: 0.1rem; font-weight: 600; }
.bin__meta { color: var(--muted); font-size: 0.85rem; }

.bin__actions { flex: none; display: flex; align-items: center; gap: 1.25rem; }

/* DANGEROUS-LOOKING WITHOUT BEING LOUD.
   Putting a note back is free and is what this page is for, so it is the button.
   Destroying one is rare, restricted and the only irreversible thing left here —
   so it is red, and it is text: present for somebody looking for it, and not
   competing with the safe action beside it. The confirmation is the friction;
   the styling is only the warning. */
.bin__forever {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: 0.85rem;
  color: var(--danger);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.bin__forever:hover { text-decoration-thickness: 2px; }

/* On a narrow screen the actions drop under the note rather than squeezing the
   title into a column of nothing. */
@media (max-width: 40rem) {
  .bin__item { flex-wrap: wrap; }
  .bin__detail { flex-basis: 100%; }
}

.files__detail { min-width: 0; }
.files__name { margin: 0 0 0.2rem; font-weight: 600; overflow-wrap: anywhere; }
.files__meta { margin: 0; color: var(--muted); font-size: 0.85rem; }
.files__orphan {
  margin: 0.4rem 0 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  color: var(--muted);
  font-size: 0.85rem;
}
.files__delete { margin: 0; }

/* A rename is a real change that a line diff of the body cannot show, so it is
   stated in words rather than left to "+0 −0". */
.history__rename, .version__rename {
  margin: 0.35rem 0 0;
  color: var(--muted);
  font-size: 0.88rem;
}
.history__was { text-decoration: line-through; }

/* What a checklist change actually was, said in items rather than in lines.
   Same weight as a rename: a quiet sentence under the who-and-when, not a
   heading. */
.history__did, .version__did {
  margin: 0.35rem 0 0;
  color: var(--text);
  font-size: 0.9rem;
}
.version__did {
  padding-left: 1.1rem;
  margin-bottom: 0.9rem;
}
.version__did li { margin: 0.15rem 0; }

/* A history with nothing in it says so. */
.history__empty { margin: 0.75rem 0; }

/* A checklist at a version: its text projection, shown as text. Markdown would
   read the indentation as a code block. */
.version__list {
  margin: 0;
  padding: 0.9rem 1rem;
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.88rem;
  line-height: 1.5;
  white-space: pre;
}

/* The Quick Add picker sits after the two destinations, quiet, so the box still
   reads as "type and press Append" rather than as an upload form. */
.quick-add__browse { margin-left: auto; }

/* The Markdown to paste, shown as well as copyable.
   Stacked, not a row: a long filename makes a long snippet, and a wrapping row
   left the two buttons in a different place on every line of the list. */
.files__reuse {
  display: grid;
  justify-items: start;
  gap: 0.4rem;
  margin: 0.5rem 0 0;
  max-width: 100%;
}

.files__copy { display: flex; gap: 0.5rem; flex-wrap: wrap; }

.files__snippet {
  max-width: 100%;
  padding: 0.2rem 0.5rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8rem;
  overflow-wrap: anywhere;
  user-select: all;
}

.btn.is-copied { border-color: var(--accent); color: var(--accent); }

/* Shown only when the clipboard refuses. */
.files__fallback {
  flex-basis: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8rem;
}

/* The "… 47 more" row. A disclosure, not a node: no drag handle, no actions,
   and deliberately not a drop target — dropping onto "more" has no meaning, so
   a drop there lands on the folder and appends to the end. */
.tree__overflow { list-style: none; }

.tree__more {
  display: block;
  width: 100%;
  padding: 0.25rem 0.45rem;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.82rem;
  text-align: left;
  cursor: pointer;
}
.tree__more:hover { background: var(--surface); color: var(--accent); }

/* --- the folder page ------------------------------------------------------ */

.crumbs { margin: 0 0 0.6rem; font-size: 0.85rem; color: var(--muted); }
.crumbs__link { color: var(--muted); }
.crumbs__link:hover { color: var(--accent); }
.crumbs__sep { margin: 0 0.35rem; opacity: 0.6; }
.crumbs__path { color: var(--muted); opacity: 0.75; }

.contents { width: 100%; border-collapse: collapse; margin: 0 0 1rem; }
.contents th, .contents td {
  padding: 0.45rem 0.6rem;
  border-bottom: 1px solid var(--border);
  text-align: left;
  font-size: 0.9rem;
}
.contents th { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }

/* A checkbox and its label on one line, with the hint underneath. */
.field--check {
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: 0.5rem;
}
.field--check .field__hint { grid-column: 1 / -1; }
.check { width: 1.05rem; height: 1.05rem; accent-color: var(--accent); }

/* --- tabs ----------------------------------------------------------------- */

.tabs {
  display: flex;
  gap: 0.25rem;
  margin: 0 0 1.1rem;

  /* THE RULE UNDER THE ROW IS A BACKGROUND, not a border, and that is about the
     scrollbar rather than about the line.

     `overflow-y` computes from `visible` to `auto` the moment `overflow-x` is
     anything else, so asking for a horizontally scrolling row asks for a
     vertical scrollbar as well — and this row used to overflow itself by
     exactly one pixel. A tab carried `margin-bottom: -1px` to pull its 2px
     underline down over the container's 1px border, which put a pixel of every
     tab below the box that holds them. Measured at scrollHeight 41 against
     clientHeight 40: enough, on a machine with classic rather than overlay
     scrollbars, to draw a full-height bar down the right of the tab row.
     Reported, and the diagnosis in the report was exactly right — the container
     was a pixel too small for its contents.

     `overflow-y: hidden` is the obvious answer and is wrong: it clips that same
     pixel, so the active tab's underline renders 1px instead of 2 and the grey
     line shows through beneath it. Measured, and visible in a 4x screenshot.

     So there is nothing to overlap. The line is painted in the last pixel of
     this box instead, and a tab's own underline covers it by sitting where it
     already sat: no overflow in either direction, and no scrollbar.

     The row is one pixel taller than it was, which is the pixel the tabs used
     to hang below it by. That is the whole of the visual change and it is why
     the fix is here rather than in a media query. */
  background-image: linear-gradient(var(--border), var(--border));
  background-size: 100% 1px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  /* SCROLLS RATHER THAN MANGLES. The seventh tab — Applications — pushed the
     row past the width available and two labels broke across lines inside their
     own tabs, which reads as a rendering fault rather than as a full bar. A
     row of tabs is one line by nature; when it does not fit, it slides. */
  overflow-x: auto;
  scrollbar-width: thin;
}

.tabs__tab {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
  flex: none;
  padding: 0.5rem 0.8rem;
  color: var(--muted);
  text-decoration: none;
  font-weight: 600;
  font-size: 0.92rem;
  border-bottom: 2px solid transparent;
}
.tabs__tab:hover { color: var(--text); }
.tabs__tab.is-current { color: var(--accent); border-bottom-color: var(--accent); }

.icon { flex: none; vertical-align: -0.15em; }

/* --- a segmented control -------------------------------------------------- */

/* Views of one panel, not navigation. Enclosed and inset so it cannot be
   mistaken for a second row of tabs — see app/views/shared/_segmented. */
.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
}

.segmented__option {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.85rem;
  border-radius: 999px;
  color: var(--muted);
  text-decoration: none;
  font-weight: 600;
  font-size: 0.85rem;
  white-space: nowrap;
}
.segmented__option:hover { color: var(--text); }

.segmented__option.is-current {
  background: var(--surface);
  color: var(--accent);
  box-shadow: var(--shadow-sm);
}

.segmented__option.is-off { opacity: 0.45; cursor: not-allowed; }

.segmented__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  padding: 0 0.3rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--text) 12%, transparent);
  color: var(--text);
  font-size: 0.72rem;
  font-weight: 700;
}
.segmented__option.is-current .segmented__badge {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
}

.segmented-group { display: flex; flex-direction: column; align-items: flex-end; }
.segmented__reason { margin: 0.45rem 0 0; font-size: 0.8rem; text-align: right; }

/* The bar a segmented control sits in: what this panel is on the left, which
   view of it on the right. */
.panel__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 0 0 1.5rem;
}
.panel__bar .section__heading { margin: 0; }

/* --- the name filter ------------------------------------------------------ */

.filter { position: relative; margin: 0 0 0.9rem; max-width: 22rem; }

.filter__icon {
  position: absolute;
  left: 0.6rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--muted);
  pointer-events: none;
  display: flex;
}

.filter__input { padding-left: 2rem; }

/* --- the checklist -------------------------------------------------------- */

/*
 * A checklist page gives its list the whole area below the heading.
 *
 * The list is what the keys talk to, so the region you can click to start
 * talking to it should be the region that looks like the list's — which is
 * everything under the title and right of the sidebar, not merely the few
 * hundred pixels the rows happen to occupy. Clicking blank space below the last
 * item is unmistakably "I am working on this list"; having that do nothing, and
 * silently take the keyboard away, is the thing being fixed.
 *
 * Only the CLICK TARGET grows. The rows stay at a readable width inside it.
 */
.main--fill { max-width: none; }

.note--checklist {
  display: flex;
  flex-direction: column;
  /* The topbar, and this article's own padding. */
  min-height: calc(100dvh - 3.2rem - 3rem);
}

/* The reader wrapper sits between the article and the list, so it has to carry
   the stretch through — flex: 1 on the list alone had nothing to grow against. */
.note--checklist > [data-editor-target="reader"] {
  display: flex;
  flex-direction: column;
  flex: 1;
}

.note--checklist .checklist-root { flex: 1; }

/* A map IS its note, so it gets the whole page rather than a column of it. It is
   also routinely wider than a screen, and a narrow frame turns reading a map
   into side-scrolling a map. */
.note--map {
  display: flex;
  flex-direction: column;
  /* height, not min-height: a checklist grows and the PAGE scrolls, which is
     right for a list. A map scrolls inside its own frame, so the article has to
     be bounded or the frame simply grows to fit the drawing and never scrolls
     at all — which is how the whole page ended up as long as the map was tall. */
  height: calc(100dvh - 3.2rem - 3rem);
  overflow: hidden;
}

/* The reader wrapper sits between the article and the map, so it has to carry
   the stretch through — the same reason the checklist has this rule. Without it
   flex:1 on the frame has nothing to grow against, and nothing to shrink
   against either.

   A board needs it for exactly the same reason, and did not have it: the board
   was as tall as its two cards while the page below it sat empty, and the card
   panel's `max-height: 70%` was 70% of that. Reported as "the panel is
   extremely narrow in height", which it was — of a container nobody had told to
   grow. */
.note--map > [data-editor-target="reader"],
.note--board > [data-editor-target="reader"] {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  min-height: 0;
}

.note--map .map-view {
  display: flex;
  flex-direction: column;
  flex: 1;
  /* A flex item's min-width and min-height are auto, so without these it grows
     to fit its content instead of letting the frame inside it scroll — in BOTH
     directions, and a map is usually wider than it is tall. */
  min-width: 0;
  min-height: 0;
}

/* Fill what is left of the page rather than a fixed slice of the viewport: the
   header, the breadcrumb and the controls above it are all different heights.
   A floor rather than auto for the same reason as above — with it left at auto
   the frame never scrolls, it just makes the page longer, which is the opposite
   of what a scrolling frame is for — and a floor rather than zero because zero
   is a map that vanishes in a short window rather than a map that scrolls.
   Same fix as `.board-frame`, found the same way. */
.note--map .map-frame {
  flex: 1;
  min-width: 0;
  min-height: 14rem;
  max-height: none;
}

/* The list holds focus itself, so keys are heard here and nowhere else — which
   means it has to SAY when it is listening. A bright selection on a list that
   has lost focus is a lie: it looks live and answers nothing. */
.checklist-root {
  outline: none;
  border-radius: 10px;
  padding: 0.35rem;
  border: 1px solid transparent;
  transition: border-color 120ms ease;
}

/*
 * ONE indicator, and the same one however focus arrived.
 *
 * There were two: this border, plus a :focus-visible outline. Clicking in gives
 * mouse focus, which :focus-visible ignores — but the list focuses ITSELF on
 * page load, and a programmatic focus counts as keyboard-ish, so a refresh drew
 * both and the box suddenly gained a second, brighter line. The same state
 * should not look like two different states depending on how you got there.
 *
 * So the border is the focus indicator, and it is drawn on focus of any kind.
 * Strong enough to serve as one, soft enough not to shout around a box that is
 * now most of the page.
 */
.checklist-root.is-active {
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
  background: color-mix(in srgb, var(--accent) 3%, transparent);
}

/* Rows stay readable even though their container now spans the page. */
.checklist__level { list-style: none; margin: 0; padding: 0; max-width: 52rem; }
.checklist__level .checklist__level { max-width: none; }
/* Nesting gives the indentation, so the shape a screen reader hears is the shape
   a sighted reader sees. */
.checklist__level .checklist__level { padding-left: 1.55rem; }

.checklist__item { margin: 0; }

.checklist__row {
  display: flex;
  align-items: center;
  gap: 0.15rem;
  padding: 0.15rem 0.4rem;
  border-radius: 6px;
}

/* Accent-tinted rather than a flat grey, so it follows every theme and stays
   visible on a done row, which is already muted.
   Only while the list is listening, though — see below. */
.checklist-root.is-active .checklist__row.is-selected {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* Focus has gone elsewhere. The row stays marked, because it is still where the
   keys will resume, but it stops looking live: no accent, and an outline instead
   of a fill, which is the same distinction a file manager draws between an
   active and an inactive selection. */
.checklist__row.is-selected {
  background: none;
  box-shadow: inset 0 0 0 1px var(--border);
}

/* Same reasoning as the sidebar's: a chevron at the height of the text, not a
   glyph at a fraction of it. */
.checklist__twisty {
  flex: none;
  width: 1.35rem;
  height: 1.35rem;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--muted);
  cursor: pointer;
  padding: 0;
}
.checklist__twisty:hover { background: var(--bg); color: var(--accent); }

.checklist__chevron { display: flex; transform: rotate(90deg); transition: transform 120ms ease; }
.checklist__twisty.is-collapsed .checklist__chevron { transform: rotate(0deg); }

/* Every leaf gets one of these. Without it, leaves sit a twisty's width to the
   left of their parent-siblings and the list looks ragged for a reason nobody
   can point at. */
.checklist__twisty--spacer { cursor: default; }

.checklist__check { margin-right: 0.45rem; }

/* An inherited tick is drawn faintly while its ring stays full strength, so
   "I ticked this" and "this is covered by something above" are told apart. */
.checklist__check.is-inherited::after { opacity: 0.45; }

.checklist__content { flex: 1; min-width: 0; overflow-wrap: anywhere; }

/* Done rows recede without disappearing — muted is already defined per theme as
   readable-but-secondary, so no new colour enters the stylesheet. */
.checklist__row.is-done .checklist__content { color: var(--muted); text-decoration: line-through; }

.checklist__has-note { color: var(--muted); font-size: 0.8rem; }

.checklist__input {
  flex: 1;
  padding: 0.1rem 0.35rem;
  font: inherit;
}

.checklist__empty { padding: 0.6rem 0.4rem; }

/* --- the keys panel ------------------------------------------------------- */

/* Above the list and to the right: near enough to be about the list, outside the
   box the list draws around itself while it has focus. The summary is a quiet
   line rather than a button — it opens a reference, and a control styled like
   the ones that change your notes suggests it does something. */
.keys-panel { margin: 0 0 0.6rem; }

/* In the sidebar the panel is competing with the tree for a narrow column, so
   it is quieter and it scrolls — the same fix the map's and the board's needed
   when their tables turned out to be taller than the space they were opened
   in. docs/UI-IMPROVEMENTS.md §7. */
.keys-panel--sidebar {
  margin: 0.2rem 0 0.6rem;
  font-size: 0.85rem;
}

.keys-panel--sidebar[open] {
  max-height: 45vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.keys-panel > summary {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 0.35rem;
  list-style: none;
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--muted);
}
.keys-panel > summary::-webkit-details-marker { display: none; }
.keys-panel > summary:hover { color: var(--text); }
.keys-panel > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.keys-panel .keys { margin-top: 0.5rem; }

/* THE PANEL HAS TO FIT INSIDE A NOTE THAT IS BOUNDED TO THE VIEWPORT.
   
   A checklist's article grows and the PAGE scrolls, so an open Keys panel is
   simply longer and everything is reachable. A map and a board are bounded on
   purpose — `height` and `overflow: hidden`, so the frame inside them scrolls
   rather than the page — and a panel opened inside that box was clipped by it
   with no way to scroll: 468px of the board's keys and 362px of the map's were
   simply not reachable. Reported on a board; the map has had it since maps
   shipped.
   
   So inside those two the panel is a flex item that may SHRINK, and scrolls
   what does not fit. Capped, because a Keys panel that swallows the whole board
   to show itself has answered the question by removing the thing it is about. */
.note--map .keys-panel[open],
.note--board .keys-panel[open] {
  /* These two, and only these two. A first draft also set `flex: 0 1 auto` and
     `min-height: 0` — the usual pair for letting a flex item shrink — and
     taking each back out one at a time showed neither did anything: a
     max-height bounds the box directly, so the flex algorithm never gets asked.
     A rule carrying lines that do not matter teaches the next reader the wrong
     reason it works. */
  max-height: 55%;
  overflow-y: auto;
  /* Scrolled to its end, the page underneath should not then start moving. */
  overscroll-behavior: contain;
}

.keys {
  margin-top: 1.5rem;
  padding: 1rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
}
.keys__heading { margin: 0 0 0.3rem; font-size: 1rem; }
.keys__notes { margin: 0 0 0.9rem; padding-left: 1.1rem; font-size: 0.85rem; }
.keys__notes li + li { margin-top: 0.3rem; }
.keys__hint { margin: 0 0 0.9rem; font-size: 0.85rem; }

/* The short list the `?` panel shows. No group headings and no intro: six rows
   answering "what was that key again", with the rest a link away. */
.keys__list--common { margin: 0; }

.keys__all { margin: 0.7rem 0 0; padding-top: 0.6rem; border-top: 1px solid var(--border); }
.keys__all-link { font-size: 0.85rem; font-weight: 600; text-decoration: none; }
.keys__all-link:hover { text-decoration: underline; }
.keys__group { margin: 0.9rem 0 0.4rem; font-size: 0.85rem; color: var(--muted); }
.keys__list { display: grid; grid-template-columns: auto 1fr; gap: 0.3rem 1rem; margin: 0; }
.keys__list dt { white-space: nowrap; }
.keys__list dd { margin: 0; font-size: 0.9rem; }

kbd {
  display: inline-block;
  padding: 0.05rem 0.35rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 5px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8rem;
}

/* --- a note's settings page ----------------------------------------------- */

/* docs/NOTE-SETTINGS-PLAN.md. These rules began as `.checklist-settings`, a
   strip of selects squeezed above a list; they are the same fields on a page of
   their own now, which is what let each of them grow the sentence that says
   what it does. */

.settings-page { display: grid; gap: 1rem; }

.settings-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.6rem;
  margin: 0;
  font-size: 0.9rem;
}
.settings-facts__pair { display: grid; gap: 0.1rem; }
.settings-facts dt { font-size: 0.78rem; font-weight: 600; color: var(--muted); }
.settings-facts dd { margin: 0; display: flex; align-items: center; gap: 0.3rem; }

.settings-card {
  padding: 0.9rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.settings-card__heading { margin: 0 0 0.8rem; font-size: 1rem; }
.settings-card__empty { margin: 0; }
.settings-card__says { margin: 0 0 0.8rem; font-size: 0.9rem; max-width: 38rem; }

/* Last, and behind a rule. The workspace's own settings put deletion here and
   so does everybody else, for the same reason: one step further from the
   controls somebody uses every day is the right distance for the one that
   cannot be undone. */
.settings-card--danger { border-color: var(--danger); }
.settings-card--danger .settings-card__heading { color: var(--danger); }

/* One row per thing that can be made, rather than a button per row of its own:
   the COST of each conversion is said here in the open, and a cost needs a line
   to sit on. */
.settings-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.7rem; }

.settings-list__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding-top: 0.7rem;
  border-top: 1px solid var(--border);
}

.settings-list__what { display: grid; gap: 0.15rem; max-width: 34rem; }
.settings-list__name { font-weight: 600; font-size: 0.9rem; }
.settings-list__hint { font-size: 0.82rem; line-height: 1.4; }
.settings-list__do { margin: 0; }

/* Making a template: a field and a button on one line where there is room, and
   the sentence about what stays behind underneath both. */
.settings-make { display: grid; gap: 0.7rem; justify-items: start; max-width: 38rem; }
.settings-make__field { display: grid; gap: 0.2rem; width: 100%; }
.settings-make__label { font-size: 0.78rem; font-weight: 600; color: var(--muted); }
.settings-make__hint { font-size: 0.82rem; line-height: 1.4; }

/* The live placeholder report on the template form. Two lines rather than one,
   because "these will be filled in" and "these will not" are different news and
   the second one is the reason the report exists. */
/* A template made from a note, shown rather than offered. It reads as what it
   is — the words it holds — and not as a field somebody has been locked out of. */
.template-frozen {
  margin: 0 0 0.4rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.85rem;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-x: auto;
  max-height: 22rem;
}

.field__value { margin: 0; font-size: 0.95rem; }

/* The note's own link, shown so it can be selected when the clipboard refuses. */
.settings-link {
  padding: 0 0.2rem;
  border-radius: 4px;
  background: var(--bg);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.85rem;
}

/* Copying a link to a thing inside a note — what was said, and the text itself
   when the clipboard refuses. */
.note-link { display: grid; gap: 0.3rem; }
.note-link__said:empty { display: none; }
.note-link__said { margin: 0; font-size: 0.85rem; color: var(--muted); }
.note-link__fallback { max-width: 34rem; }

.placeholders { display: grid; gap: 0.2rem; }
.placeholders__warn { color: var(--warn); }
.placeholders code {
  padding: 0 0.2rem;
  border-radius: 4px;
  background: var(--bg);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.95em;
}

/* Something written in the braces that this application will not fill in. Not
   an error — the template is still made, and the words still go in — so it is
   the warning colour and not the danger one. */
.settings-make__warn {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--warn);
}
.settings-make code, .settings-make__warn code {
  padding: 0 0.2rem;
  border-radius: 4px;
  background: var(--bg);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.95em;
}
.settings-make__field > .input { width: auto; justify-self: start; min-width: 18rem; max-width: 100%; }

/* A column, not the strip's row. On a page the sentence under each control is
   what makes the page worth having over a menu, and a sentence in a flex row of
   fields either wraps into a mess or forces the row to the width of the longest
   one. `max-width` on the field rather than the card, so the hints stay
   readable on a wide screen without the card floating in the middle of it. */
.settings-fields { display: grid; gap: 1rem; }
.settings-fields__field { display: grid; gap: 0.2rem; max-width: 38rem; }

/* The control is as wide as it needs to be, not as wide as the sentence under
   it. A three-option select drawn across 38rem of nothing looks like a mistake.

   `width: auto` as well as `justify-self`, and that is the whole of why the
   first attempt did nothing: `.input` sets `width: 100%`, so aligning the grid
   item to the start still left it 100% of the column it was aligned inside. */
.settings-fields__field > .input {
  width: auto;
  justify-self: start;
  min-width: 11rem;
  max-width: 100%;
}
.settings-fields__field > .settings-fields__pair { justify-self: start; }
.settings-fields__label { font-size: 0.78rem; font-weight: 600; color: var(--muted); }
.settings-fields__hint { font-size: 0.82rem; line-height: 1.4; }
.input--small { padding: 0.25rem 0.4rem; font-size: 0.85rem; }

/* The item's Note, under the item, indented to the text rather than the ring. */
.checklist__note {
  margin: 0.1rem 0 0.4rem 3.1rem;
  padding: 0.4rem 0.7rem;
  border-left: 2px solid var(--border);
  font-size: 0.9rem;
}
.checklist__note > :last-child { margin-bottom: 0; }

/* The note button is on every row, because it is how a note gets WRITTEN with
   the mouse — offering it only once a note exists leaves no way to make the
   first one. Invisible until the row is selected or hovered, so a list of
   plain items is not a column of icons; solid once there is something to open. */
.checklist__has-note {
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 0.85rem;
  cursor: pointer;
  padding: 0 0.2rem;
  opacity: 0;
  transition: opacity 120ms ease;
}

.checklist__row:hover .checklist__has-note,
.checklist__row.is-selected .checklist__has-note { opacity: 0.45; }

.checklist__has-note.is-present { opacity: 1; }

.checklist__has-note:hover,
.checklist__has-note:focus-visible { opacity: 1; color: var(--accent); }

.checklist-note-editor {
  display: grid;
  gap: 0.5rem;
  margin: 0.3rem 0 0.6rem 3.1rem;
  padding: 0.6rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  position: relative;
}
.checklist-note-editor__label { font-size: 0.8rem; font-weight: 600; }
.checklist-note-editor__field { min-height: 5rem; }
.checklist-note-editor__actions { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }

/* A row living out its grace period: done, hidden by the Filter in a moment, and
   still available to untick until then. Dimmed rather than removed, so the list
   does not jump under the cursor the instant something is ticked. */
.checklist__row.is-fading { opacity: 0.55; }

.checklist__filtered { margin: 0.8rem 0 0; font-size: 0.82rem; }

.settings-fields__pair { display: flex; align-items: center; gap: 0.4rem; }
.settings-fields__custom { display: flex; align-items: center; gap: 0.3rem; }
.input--seconds { width: 5.5rem; }

/* --- the members page ----------------------------------------------------- */

.invite { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.invite__email { flex: 1; min-width: 14rem; }
.invite__hint { margin: 0.6rem 0 0; font-size: 0.85rem; }

.members td { vertical-align: middle; }
.members__control { margin: 0; }
.members__actions { text-align: right; }

/* A value somebody else's authority decides, shown as text rather than as a
   disabled control — a greyed-out select invites clicking and explains nothing. */
.members__fixed { color: var(--muted); }

.card--danger { border-color: color-mix(in srgb, var(--danger) 40%, var(--border)); }

.members__hint { font-size: 0.8rem; }

/* --- sharing one note ----------------------------------------------------- */

.shares {
  margin-top: 1.5rem;
  padding: 0.7rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
}
.shares__summary { cursor: pointer; font-weight: 600; font-size: 0.92rem; }
.shares__body { margin-top: 0.8rem; }
.shares__list { list-style: none; margin: 0.8rem 0 0; padding: 0; display: grid; gap: 0.4rem; }
.shares__list li { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; }
.shares__stop { margin: 0; }

/* --- daily notes ---------------------------------------------------------- */

.daily-on { display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap; margin: 0 0 1.2rem; }
.daily__nudge { display: inline-block; margin: 0 0.15rem 0 0; }

/* A rule that can never fire. Struck through rather than removed, because it is
   still there and still needs moving or deleting. */
.is-unreachable td { color: var(--muted); text-decoration: line-through; }
.is-unreachable td:last-child, .is-unreachable td:nth-child(3) { text-decoration: none; }

/* --- the daily-notes tab -------------------------------------------------- */

.daily__section { margin: 0 0 2.25rem; }
.daily__section .section__heading { margin-top: 0; }
.daily__row { display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap; margin: 0 0 0.6rem; }
.daily__example { font-size: 0.85rem; color: var(--muted); padding-bottom: 0.35rem; }

/* Save sits to the right, at the end of the thing it saves, with room before the
   next section starts — it used to butt straight up against the next heading. */
.daily__actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 0.9rem;
  margin-top: 0.5rem;
}
.daily__unsaved { color: var(--muted); font-size: 0.85rem; }

/* Not yet available, and unable to pretend otherwise: `inert` in the markup does
   the refusing, and this only has to look like it. */
.daily__gated { transition: opacity 120ms ease; }
.daily__gated.is-locked { opacity: 0.4; }

.daily__group { margin: 1.4rem 0 0.1rem; font-size: 0.9rem; }
.daily__group-note { margin: 0 0 0.5rem; font-size: 0.82rem; }

.daily__patterns { margin: 0.4rem 0 0; }
.daily__patterns summary { cursor: pointer; color: var(--muted); font-size: 0.85rem; }
.daily__patterns .keys__list { margin-top: 0.5rem; font-size: 0.85rem; }

/* Five controls that read as one sentence — "add THIS template, THERE, on THESE
   days" — so they sit on a line rather than stacking into five full-width rows. */
.daily__rule-form { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.daily__rule-form .input { width: auto; min-width: 9rem; }
.daily__rule-form .input--wide { min-width: 12rem; }

/* --- a switch ------------------------------------------------------------- */

.switch-field { display: flex; align-items: flex-start; gap: 0.8rem; }
.switch-field__text { display: flex; flex-direction: column; gap: 0.2rem; }
.switch-field__text .field__label { cursor: default; }

.switch { position: relative; display: inline-flex; flex: none; margin-top: 0.15rem; }

/* The real checkbox, kept where it is rather than hidden away: it stays the
   thing that is focused, clicked, announced and submitted. */
.switch__input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.switch__track {
  width: 2.7rem;
  height: 1.55rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--text) 22%, transparent);
  display: flex;
  align-items: center;
  padding: 0.15rem;
  transition: background 140ms ease;
  pointer-events: none;
}

.switch__knob {
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: transform 140ms ease;
}

.switch__input:checked + .switch__track { background: var(--accent); }
.switch__input:checked + .switch__track .switch__knob { transform: translateX(1.15rem); }

.switch__input:focus-visible + .switch__track {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --- the calendar --------------------------------------------------------- */

/* One month, stepped through with the arrows beside its name. Two months side by
   side left each day about a hundred pixels wide, and every template name came
   out as "Week…" — which is the one thing the calendar exists to tell you. */
.calendar__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.calendar__bar .section__heading { margin: 0; }
.calendar__steps { display: flex; align-items: center; gap: 0.5rem; }

/* Wide enough that stepping through the year does not shuffle the arrows about
   as the month names change length. */
.calendar__month {
  font-weight: 600;
  min-width: 9.5rem;
  text-align: center;
}

.calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }

.calendar__heading {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  text-align: center;
  padding-bottom: 0.25rem;
}
.calendar__heading-short { display: none; }
@media (max-width: 52rem) {
  .calendar__heading-long { display: none; }
  .calendar__heading-short { display: inline; }
}

.calendar__day {
  min-height: 5.5rem;
  padding: 0.35rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  font-size: 0.78rem;
  line-height: 1.3;
  display: flex;
  flex-direction: column;
  gap: 0.12rem;
}

.calendar__day.is-empty { background: none; color: var(--muted); }
.calendar__day.is-today { outline: 2px solid var(--accent); outline-offset: -2px; }

.calendar__number { font-weight: 700; font-size: 0.82rem; }

/* Wrapped, not truncated. A name cut off mid-word answers the question with
   "Week…", which is no answer at all. */
.calendar__base, .calendar__piece { overflow-wrap: anywhere; }
.calendar__base { font-weight: 600; }
.calendar__base--none { font-weight: 400; font-style: italic; opacity: 0.7; }
.calendar__piece { color: var(--muted); }

.calendar__key {
  display: flex;
  gap: 1.1rem;
  flex-wrap: wrap;
  font-size: 0.8rem;
  color: var(--muted);
  margin: 0 0 0.9rem;
}
.calendar__key strong { color: var(--text); }

/* Headings inside a panel that already has a title bar of its own. */
.daily__heading { margin: 0 0 0.5rem; font-size: 1rem; }
.daily__elsewhere { margin: 1.5rem 0 0; font-size: 0.88rem; }

/* --- search --------------------------------------------------------------- */

.search-form { display: flex; gap: 0.5rem; margin: 0 0 0.9rem; }
.search-form__input { flex: 1; min-width: 0; }

.search-help { margin: 0 0 1.5rem; }
.search-help summary { cursor: pointer; color: var(--muted); font-size: 0.85rem; }
.search-help .keys__list { margin-top: 0.6rem; font-size: 0.85rem; }

.search__count { font-size: 0.85rem; margin: 0 0 0.75rem; }
.search__none { margin: 1rem 0; }

.results { list-style: none; margin: 0; padding: 0; }

.result {
  padding: 0.85rem 0;
  border-top: 1px solid var(--border);
}
.result:last-child { border-bottom: 1px solid var(--border); }

.result__title { font-weight: 600; font-size: 1rem; }
.result__where { margin: 0.15rem 0 0.35rem; font-size: 0.8rem; }
.result__snippet { margin: 0; font-size: 0.9rem; overflow-wrap: anywhere; }

/* The only styled thing inside a snippet, and the only markup Search::Snippets
   is allowed to produce. */
.result__snippet mark {
  background: color-mix(in srgb, var(--accent) 25%, transparent);
  color: inherit;
  border-radius: 3px;
  padding: 0 0.1em;
}

/* --- a subject's page ------------------------------------------------------
   docs/ZETTELKASTEN-PLAN.md. Deliberately built on .result, because a mention
   and a search result are the same shape of thing: a note, where it is, and a
   piece of what it says. Two lists that looked different would be two lists to
   keep in step. */

.subject__title { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }

/* The namespace, said without being shouted: `customer` is context for the
   name, not half of it. */
.subject__namespace {
  font-size: 0.7em;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.1em 0.55em;
}

.subject__count { font-size: 0.85rem; margin: 0 0 0.75rem; }

/* --- the index of subjects -------------------------------------------------
   The facets are derived from use rather than maintained, so this row is a
   picture of what somebody has actually written — which is why it is chips and
   not a select: a select implies a list somebody chose. */

.facets { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0 0 0.75rem; }

.facet {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
  padding: 0.2em 0.7em;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.85rem;
  text-decoration: none;
  color: var(--text);
}

.facet:hover { border-color: var(--accent); }

.facet.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
}

.facet__count { font-size: 0.78em; opacity: 0.7; }

.subject__filters { font-size: 0.85rem; margin: 0 0 1rem; }

.subjects { list-style: none; margin: 0; padding: 0; }

.subjects__row { padding: 0.6rem 0; border-top: 1px solid var(--border); }
.subjects__row:last-child { border-bottom: 1px solid var(--border); }

.subjects__link { font-weight: 600; text-decoration: none; }
.subjects__link:hover { text-decoration: underline; }

.subjects__about { margin: 0.1rem 0 0; font-size: 0.8rem; }

/* Said, not shouted: whether anybody has written about a subject is the second
   most useful thing on the row and the first is its name. */
.subjects__written { font-style: italic; }

/* --- suggesting a subject after [[ ----------------------------------------
   Under the field rather than at the caret: putting it at the caret means
   mirroring the whole textarea in a hidden div to find out where the caret is,
   and every Markdown editor here can afford to be sure of "under the field". */

.uploader { position: relative; }

.completions {
  position: absolute;
  left: 0;
  right: 0;
  z-index: 20;
  max-height: 15rem;
  overflow-y: auto;
  margin: 0.2rem 0 0;
  padding: 0.2rem;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
}

.completions__item {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  padding: 0.35rem 0.5rem;
  border-radius: 6px;
  font-size: 0.9rem;
  cursor: pointer;
}

.completions__item.is-active { background: var(--accent-soft); }

.completions__namespace {
  font-size: 0.7em;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.completions__count { margin-left: auto; font-size: 0.78em; color: var(--muted); }

/* The writing at the top, and the mentions under it. The rule between them is
   the whole layout: above it is what one person decided to say about this
   subject, below it is what everybody happens to have said. */
.subject__writing { margin: 0 0 1.5rem; }

.subject__heading {
  font-size: 0.95rem;
  margin: 0 0 0.35rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border);
}

.subject__binned { margin: 0 0 1.5rem; }

/* Recoverable, rare, and the only control this page carries that the note's own
   settings would otherwise hold — a subject page is not in the tree, so there is
   nowhere else to reach it from. Quiet rather than red, because the bin is where
   it goes and `.bin__forever` is the thing that is actually irreversible: the
   two must not look alike. */
/* --- the unlinked half -----------------------------------------------------
   A GUESS, AND IT LOOKS LIKE ONE. The linked mentions above are somebody saying
   a passage is about this subject; these merely say the words. Presenting them
   alike would make the deliberate link worth less, so the whole section is
   quieter: behind a summary, dimmer, and with the fragments set back. */

.unlinked { margin: 1.5rem 0 0; border-top: 1px solid var(--border); padding-top: 0.75rem; }

.unlinked__summary {
  cursor: pointer;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--muted);
}

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

.results--guessed { opacity: 0.85; }
.results--guessed .result { border-top-color: var(--border-soft); }
.results--guessed .result__title { font-weight: 500; }

/* Quiet, and after the fragment rather than beside the title: the fragment is
   what somebody reads to decide, so the button belongs where the decision is
   made.

   ON THE BUTTON ITSELF, not on a descendant of it. `button_to` puts the class it
   is given on the BUTTON and wraps it in a bare form — so `.result__link button`
   matched nothing at all and these came out as default browser buttons, shouting
   next to the quiet things around them. A screenshot caught it; `.bin__forever`
   had the shape right all along. */
.result__link,
.subject__setting,
.subject__remove-button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: 0.8rem;
  color: var(--muted);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.result__link:hover,
.subject__setting:hover,
.subject__remove-button:hover { color: var(--text); }

/* The forms `button_to` wraps them in, which would otherwise take a line of
   their own between the fragment and the next result. */
.result__link-form { margin: 0.35rem 0 0; }
.subject__setting-form { margin: 1rem 0 0; }

.subject__remove { margin: 1.25rem 0 0; }


.subject__none { margin: 1rem 0; }

/* CLAMPED BY HEIGHT, not by cutting the text: a long block fades out rather
   than ending mid-word, and what is behind the fade is still the real markup —
   a table stays a table when it is the top of one. */
.result__fragment {
  position: relative;
  max-height: 9.5rem;
  overflow: hidden;
  font-size: 0.9rem;
}

/* THE FADE IS POSITIONED AT THE CLAMP, not at the bottom of the box, and that
   is the whole trick: CSS cannot ask whether an element overflowed, so a fade
   pinned to `bottom: 0` lands on top of a one-line fragment and greys out a
   sentence that was never cut. Screenshots of exactly that are why this is
   written down.

   At `top: 7rem` it is inside the box only when the box has reached its 9.5rem
   limit. A short fragment is shorter than 7rem, so the fade falls outside it and
   `overflow: hidden` removes it — no fade, because nothing was cut. */
.result__fragment::after {
  content: "";
  position: absolute;
  inset: 7rem 0 auto 0;
  height: 2.5rem;
  background: linear-gradient(to bottom, transparent, var(--bg));
  pointer-events: none;
}

.result__fragment > :first-child { margin-top: 0; }
.result__fragment > :last-child { margin-bottom: 0; }

/* A subject link inside a note. Underlined rather than coloured alone, because
   a note's body already spends colour on ordinary links and these are a
   different kind of going-somewhere: sideways, into this workspace, rather than
   outward. */
.subject-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--accent);
}

.subject-link:hover,
.subject-link:focus-visible {
  text-decoration-style: solid;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-radius: 3px;
}

.pager { display: flex; align-items: center; gap: 0.75rem; margin: 1.2rem 0 0; }
.pager__where { font-size: 0.85rem; }

.sidebar__search { margin: 0 0 0.5rem; }
.sidebar__search-input { width: 100%; }

/* --- files that are not pictures ------------------------------------------ */

.files__total { font-size: 0.85rem; margin: 0 0 1rem; }

/* Stands in for a thumbnail. An <img> pointed at a zip is a broken-image icon,
   which tells you less than three letters do. */
.files__thumb { text-decoration: none; }

.files__kind {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--muted);
  background: color-mix(in srgb, var(--text) 6%, transparent);
  border-radius: 6px;
}

.result__source { margin: 0.2rem 0 0; font-size: 0.78rem; font-style: italic; }

/* --- what has been read out of a file ------------------------------------- */

.files__reading { display: flex; gap: 0.5rem; flex-wrap: wrap; }

.files__state {
  display: inline-flex;
  align-items: center;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  font-size: 0.72rem;
  background: color-mix(in srgb, var(--text) 8%, transparent);
}
.files__state--done,
.files__state--partial { background: var(--ok-soft); }
.files__state--failed { background: var(--danger-soft); }

.ocr-actions {
  display: grid;
  gap: 1rem;
  margin-top: 1.5rem;
  padding-top: 1.2rem;
  border-top: 1px solid var(--border);
}
@media (min-width: 46rem) { .ocr-actions { grid-template-columns: 1fr 1fr; } }
.ocr-actions__item p { margin: 0 0 0.5rem; font-size: 0.85rem; }

/* --- plan and usage ------------------------------------------------------- */

.usage { margin: 1rem 0 0; }

.usage__row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
}
.usage__row:last-child { border-bottom: 0; }
.usage__row dt { color: var(--muted); }
.usage__row dd { margin: 0; font-weight: 600; }

.plan-form { display: flex; gap: 0.75rem; align-items: flex-end; flex-wrap: wrap; margin: 1rem 0 0; }

/* --- every note at once --------------------------------------------------- */

.note-list__bar {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 0 0 0.8rem;
}
.note-list__all { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; }
.note-list__count { font-size: 0.85rem; }

/* --- the banner ----------------------------------------------------------- */

.banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.7rem 0.9rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  margin: 0 0 1rem;
  font-size: 0.9rem;
}

.banner__text { margin: 0; }
.banner__actions { display: inline-flex; align-items: center; gap: 0.5rem; }

.banner--locked { border-color: color-mix(in srgb, var(--danger) 45%, var(--border));
                  background: var(--danger-soft); }
.banner--urgent { border-color: color-mix(in srgb, var(--warn) 35%, var(--border));
                  background: var(--warn-soft); }
.banner--warning { background: color-mix(in srgb, var(--accent) 8%, transparent); }

.banner__dismiss {
  border: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 1.2rem;
  line-height: 1;
  padding: 0 0.25rem;
}
.banner__dismiss:hover { color: var(--text); }

/* Where you are, in the sidebar. Distinct from :hover, which says where you are
   about to be — the two looking the same is why this was easy to miss. */
.sidebar__link.is-current {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent);
  font-weight: 600;
}
.sidebar__link.is-current .icon { color: var(--accent); }

/* --- the admin panel ------------------------------------------------------ */

/* Deliberately plainer than the app. This is an operator tool, and looking like
   the product is how somebody forgets which one they are holding. */
.admin { background: var(--bg); }

.admin__bar {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  flex-wrap: wrap;
  padding: 0.6rem 1.2rem;
  background: var(--surface);
  border-bottom: 2px solid var(--text);
}
.admin__brand a { font-weight: 700; text-decoration: none; color: var(--text); }
.admin__nav { display: flex; gap: 1rem; flex: 1; }
.admin__who { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--muted); }

.admin__link { color: var(--muted); text-decoration: none; font-size: 0.9rem; font-weight: 600; }
.admin__link:hover { color: var(--text); }
.admin__link.is-current { color: var(--accent); }

.admin__main { max-width: 78rem; margin: 0 auto; padding: 1.5rem 1.2rem 4rem; }
.admin__title { margin: 0 0 1.2rem; font-size: 1.5rem; }
.admin__heading { margin: 2rem 0 0.8rem; font-size: 1.05rem; }
.admin__note { font-size: 0.8rem; margin-top: 0.6rem; }

.admin__cards { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }

.admin__stat {
  padding: 0.9rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.admin__stat.is-warning { border-color: color-mix(in srgb, var(--warn) 45%, var(--border)); }
.admin__stat-value { font-size: 1.6rem; font-weight: 700; line-height: 1.1; }
.admin__stat-label { font-size: 0.85rem; color: var(--muted); }
.admin__stat-note { font-size: 0.75rem; color: var(--muted); }

.admin__filters { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin: 0 0 1rem; }
.admin__filters .input { width: auto; }

.admin__table { font-size: 0.88rem; }
.admin__table td, .admin__table th { vertical-align: top; }
.admin__agent { max-width: 22rem; overflow-wrap: anywhere; font-size: 0.78rem; }
.admin__diff { font-size: 0.78rem; }
.admin__diff s { opacity: 0.6; }

.admin__facts { max-width: 44rem; }
.admin__actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.admin__form { max-width: 52rem; }
.admin__grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); margin: 1rem 0; }

.admin__gate { max-width: 26rem; margin: 4rem auto; }
.admin__gate h1 { font-size: 1.3rem; margin: 0 0 0.8rem; }

/* --- charts, drawn by us -------------------------------------------------- */

.admin__chart { margin: 2rem 0; max-width: 46rem; }

.chart { width: 100%; height: auto; display: block; }
.chart__grid { stroke: var(--border); stroke-width: 1; }
.chart__label { fill: var(--muted); font-size: 10px; }

.chart__line { stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart__line--0 { stroke: var(--accent); }
.chart__line--1 { stroke: var(--muted); stroke-dasharray: 4 3; }

.chart__key { display: flex; align-items: center; gap: 0.4rem; font-size: 0.78rem; color: var(--muted); }
.chart__swatch { display: inline-block; width: 0.9rem; height: 0.2rem; border-radius: 2px; }
.chart__swatch--0 { background: var(--accent); }
.chart__swatch--1 { background: var(--muted); }
.chart__swatch--1 + span { margin-left: 0.5rem; }

/* --- admin cards ---------------------------------------------------------- */

/* Grouping, rather than one long column of headings and rows. A card says "this
   set of facts belongs together", which is most of what an operator screen has
   to communicate before anybody reads a number. */
.admin__card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
  margin: 0 0 1.2rem;
}

.admin__card-title { margin: 0 0 0.8rem; font-size: 1rem; }
.admin__card-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.7rem; }
.admin__card-head .admin__card-title { margin: 0; }

.admin__card--workspace { margin: 0; }

.admin__cards--wide {
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  margin: 0 0 1.2rem;
}

/* Label above value, so a long value cannot squash its own label — which is what
   a two-column definition list does the moment a plan name gets longer. */
.admin__pairs { display: grid; gap: 0.6rem 1.2rem; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); margin: 0; }
.admin__pairs > div { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.admin__pairs dt { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
.admin__pairs dd { margin: 0; font-size: 0.9rem; overflow-wrap: anywhere; }

.admin__badge {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--text) 10%, transparent);
  color: var(--muted);
}
.admin__badge--warning {
  background: var(--warn-soft);
  color: var(--text);
}

.admin__columns { display: grid; gap: 1.2rem; }
@media (min-width: 62rem) { .admin__columns { grid-template-columns: 1fr 1fr; } }

/* --- your account --------------------------------------------------------- */

.page-head { margin: 0 0 1.5rem; }
.page-head h1 { margin: 0 0 0.2rem; font-size: 1.5rem; }
.page-head p { margin: 0; }

.section-title { margin: 0 0 0.3rem; font-size: 1.1rem; }

/* A small label beside a name. Used by the theme cards and by the plan builder,
   where it marks an unlisted or custom plan. */
.pill {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.1rem 0.45rem;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-pill);
  vertical-align: middle;
}

.settings-panel {
  margin: 0 0 2rem;
  padding: 1.2rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.settings-panel > .muted { margin-top: 0; }
.settings-form { max-width: 32rem; }

/* --- API keys — docs/API-PLAN.md §4.2 -------------------------------------- */

/* The one-time reveal. Deliberately the loudest thing on the page — this is the
   only moment the secret exists anywhere we can show it — but a settings-panel
   rather than a card, so it is the same width as everything under it. A panel
   that is the only one of its width reads as a stray element rather than an
   urgent one. */
.apikey-issued {
  border-color: var(--danger);
  border-width: 2px;
}
.apikey-issued__value {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin: 0.8rem 0;
}
.apikey-issued__value code {
  flex: 1 1 20rem;
  padding: 0.6rem 0.7rem;
  font-size: 0.85rem;
  /* The whole key, wrapped rather than cut off. A truncated secret that looks
     copyable is worse than one that plainly does not fit. */
  overflow-wrap: anywhere;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.apikey-choice {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.25rem 0;
  cursor: pointer;
}
.apikey-choice input { flex: none; }

/* --- taking a copy — docs/EXPORT-PLAN.md §6.1 ------------------------------ */

/* One row per format, each a name and a sentence about what it is for. The same
   shape the + New menu uses, because "which of these do I want" is the same
   question there — uncollapsed, because this is a page somebody came to on
   purpose rather than a menu they opened in passing. */
.export-formats { list-style: none; margin: 0 0 1.5rem; padding: 0; }

.export-format {
  display: flex;
  align-items: baseline;
  gap: 0.8rem;
  border-bottom: 1px solid var(--border-soft);
}
.export-format:last-child { border-bottom: 0; }

.export-format__choice {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.8rem 0.6rem;
  margin: 0 -0.6rem;
  text-decoration: none;
  color: inherit;
  border-radius: var(--radius-sm);
}
.export-format__choice:hover,
.export-format__choice:focus-visible { background: var(--surface-2); }

.export-format__name { font-weight: 600; }
.export-format__hint { color: var(--muted); font-size: 0.9rem; }

/* What comes out, said beside the choice rather than discovered in a downloads
   folder. */
.export-format__packaging {
  flex: none;
  color: var(--muted);
  font-size: 0.82rem;
}

/* A line in the + New menu, between making something and bringing one in. */
.maker__divider {
  height: 1px;
  margin: 0.3rem 0.6rem;
  background: var(--border-soft);
}

/* --- an exported page — docs/EXPORT-PLAN.md §4 ------------------------------ */

/* HERE rather than in a stylesheet of its own. The export ships this file whole,
   so a second one would be a copy that drifts and the exported page would slowly
   stop looking like the page it came from. These classes are only ever used in
   the zip, which costs the application a few hundred bytes it never draws — and
   buys one stylesheet instead of two. */
.export { background: var(--surface); }

.export__page {
  max-width: 60rem;
  margin: 0 auto;
  padding: 2rem 1.5rem 4rem;
}
.export__title { margin: 0 0 1.5rem; }

.export-index { list-style: none; padding: 0; }
.export-index li { padding: 0.4rem 0; border-bottom: 1px solid var(--border-soft); }
.export-index__says { color: var(--muted); }

.export-one { margin: 0 0 3rem; padding: 0 0 2rem; border-bottom: 1px solid var(--border); }

/* A CHECKLIST'S NOTE, under its row where the application draws it. Native
   toggling: reachable with Tab, opens with Enter, and no script of ours. */
.export-note-inline > summary {
  cursor: pointer;
  color: var(--muted);
  font-size: 0.85rem;
  padding: 0.15rem 0;
}

/* A BOARD'S AND A MAP'S, below the drawing — a card is narrow on purpose and a
   map's box is reserved by MapLayout, so neither can hold a rendered note.
   `:target` is the whole mechanism: the marker is a link, the panel has that id,
   and this rule reveals it. Clicking also scrolls to it and focuses it, both of
   which are the browser's own. */
.export-notes { margin-top: 2.5rem; }
.export-notes__heading { font-size: 1.1rem; }

/* THE HEADING IS ALWAYS THERE and the body is not, so the section reads as a
   list of what there is when nothing is open — a table of contents rather than
   a heading with nothing under it, which is what hiding the whole panel gave. */
.export-note {
  padding: 0.5rem 0.8rem;
  margin: 0.3rem 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  scroll-margin-top: 1rem;
}
.export-note:target {
  padding: 1rem 1.2rem;
  margin: 0.8rem 0;
  background: var(--surface-2);
  border-color: var(--border);
}

.export-note__body { display: none; }
.export-note:target .export-note__body { display: block; }
.export-note:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
.export-note__heading { margin: 0; font-size: 1rem; font-weight: 500; }
.export-note:target .export-note__heading { margin: 0 0 0.6rem; font-weight: 600; }
.export-note__number {
  display: inline-block;
  min-width: 1.6rem;
  color: var(--muted);
}
.export-note__back { font-size: 0.85rem; }

/* AND IT DEGRADES TO THE FALLBACK. With no CSS at all — an old reader, a
   converter — every panel is simply visible and the file becomes "maps and
   boards always show them at the bottom", which is the right thing to happen
   when everything else has failed. */

/* The outline, ready to paste. Scrolls rather than wraps: a wrapped TaskPaper
   line reads as two rows of an outline that has only one. */
.export-outline {
  max-height: 22rem;
  overflow: auto;
  padding: 0.7rem 0.8rem;
  margin: 0.6rem 0 0.8rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 0.85rem;
  line-height: 1.5;
  white-space: pre;
  tab-size: 4;
}

/* The two buttons on the consent screen. Side by side, because they are one
   decision and stacking them makes Allow look like the only answer. */
.oauth-decision {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  margin-top: 1rem;
}

/* A revoked or expired key stays on the page — its name is worth reading after
   the fact — but it is plainly not one of the live ones. */
.apikey--dead { opacity: 0.55; }
.apikey--dead code { text-decoration: line-through; }

/* --- the theme picker ----------------------------------------------------- */

.themes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  grid-auto-rows: 1fr;
  gap: 0.6rem;
  margin-top: 1rem;
}
.themecard__form { margin: 0; display: block; height: 100%; }

.themecard {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  width: 100%;
  height: 100%;
  text-align: left;
  padding: 0.7rem 0.8rem;
  font: inherit;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color var(--ease), box-shadow var(--ease);
}
.themecard:hover { border-color: var(--accent); box-shadow: var(--shadow-sm); }
.themecard:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.themecard.is-current { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

.themecard__swatch {
  display: flex;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 1px solid var(--border-soft);
}
.themecard__chip { width: 1.1rem; height: 2.4rem; display: block; }

.themecard__body { flex: 1; min-width: 0; }
.themecard__name { display: block; font-weight: 600; }

/* Two lines are reserved whether or not the text needs them. Equal card heights
   are not enough on their own: the contents are vertically centred, so a
   one-line description sits its theme's NAME at a different height from its
   neighbour's, and the row reads as ragged. */
.themecard__desc {
  display: block;
  font-size: 0.8rem;
  line-height: 1.35;
  min-height: 2.7em;
  color: var(--muted);
}

.themecard__state {
  flex-shrink: 0;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--muted);
}
.themecard.is-current .themecard__state { color: var(--accent); }

/* --- building your own ---------------------------------------------------- */

.custom-theme { max-width: 40rem; margin-top: 1rem; }

.custom-theme__colours {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.8rem;
}
.custom-theme__colour { margin: 0; }
.custom-theme__pair { display: flex; align-items: center; gap: 0.4rem; }

/* A colour well alone cannot be pasted into; a hex field alone cannot be
   explored. Both, side by side. */
.custom-theme__well {
  width: 2.4rem;
  height: 2.1rem;
  padding: 0;
  flex-shrink: 0;
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.custom-theme__hex { flex: 1; min-width: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* A real element carrying the real custom properties, so what is previewed is
   what the page will do. Boxes painted to look like a theme would agree with
   themselves and could still be wrong. */
.custom-theme__preview {
  margin: 1.2rem 0;
  padding: 1rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.custom-theme__preview-text {
  margin: 0 0 0.6rem;
  padding: 0.7rem;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.custom-theme__preview-button {
  display: inline-block;
  padding: 0.35rem 0.7rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--accent-text);
  background: var(--accent);
  border-radius: var(--radius-sm);
}
.custom-theme__preview-muted { color: var(--muted); font-size: 0.85rem; margin-left: 0.4rem; }

@media (max-width: 40rem) {
  .themes { grid-template-columns: 1fr; }
}

/* ==========================================================================
   The marketing page.
   The only full-bleed layout in the application — every other page is a column
   with a maximum width. Fenced here rather than mixed in above so that the note
   editor is not loading rules for a page it will never render.
   ========================================================================== */

.marketing { padding: 0; max-width: none; }

.marketing__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.4rem;
  max-width: 68rem;
  margin: 0 auto;
}
.marketing__brand { font-weight: 700; font-size: 1.05rem; }
.marketing__nav { display: flex; align-items: center; gap: 0.6rem; }

.btn--large { padding: 0.6rem 1.1rem; font-size: 1rem; }

/* --- the hero ------------------------------------------------------------- */

/* Words, then one wide picture underneath.
   Side by side was tried first and the picture lost: a checklist is a short,
   wide thing, so in a half-width column it rendered a couple of inches tall and
   floated in whitespace. Full width gives it room to be read, which is the
   entire point of showing it rather than describing it. */
.hero {
  max-width: 68rem;
  margin: 0 auto;
  padding: 2.5rem 1.4rem 3rem;
}
@media (min-width: 56rem) {
  .hero { padding-top: 4rem; }
}

.hero__words { max-width: 42rem; }

.hero__title {
  margin: 0 0 0.8rem;
  font-size: clamp(1.9rem, 5vw, 3rem);
  line-height: 1.1;
  letter-spacing: -0.02em;
}
.hero__lede { margin: 0 0 1.6rem; font-size: 1.1rem; line-height: 1.55; color: var(--muted); }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0 0 1rem; }
.hero__note { margin: 0; font-size: 0.9rem; }

.hero__shot { margin-top: 2.5rem; }

/* --- pictures of the running application ---------------------------------- */

/* Generated by bin/screenshots, one per theme. See MarketingShot. */
.shot { display: block; }
.shot__image {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
}


/* --- a feature, shown before it is described ------------------------------ */

/* Words above, picture below, full width.
   Side by side was tried and the pictures lost: a screenshot of a whole page
   squeezed into a half-width column renders its text at four or five pixels,
   which is a picture of a product nobody can read. Showing rather than telling
   only works if the thing shown is legible.

   The words alternate their alignment instead, which gives the rhythm the
   two-column layout was there for without shrinking anything. */
.feature-row {
  max-width: 68rem;
  margin: 0 auto 4rem;
}
.feature-row:last-child { margin-bottom: 0; }

.feature-row__words { max-width: 40rem; margin-bottom: 1.4rem; }

/* Offset, not right-aligned. Moving the block gives the same rhythm; setting
   text-align on it would make a ragged left edge of body copy, which is harder
   to read for the sake of a pattern nobody is looking for. */
@media (min-width: 56rem) {
  .feature-row--flipped .feature-row__words { margin-left: auto; }
}

.feature-row__title {
  margin: 0 0 0.5rem;
  font-size: clamp(1.3rem, 2.6vw, 1.8rem);
  letter-spacing: -0.01em;
}
.feature-row__words p { margin: 0; color: var(--muted); line-height: 1.6; }

/* --- the sections --------------------------------------------------------- */

.marketing__section { padding: 3.5rem 1.4rem; }
.marketing__section--quiet { background: var(--surface-2); border-block: 1px solid var(--border-soft); }

.marketing__heading {
  max-width: 68rem;
  margin: 0 auto 1.6rem;
  font-size: clamp(1.4rem, 3vw, 1.9rem);
  letter-spacing: -0.01em;
}

.features {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  max-width: 68rem;
  margin: 0 auto;
}
.feature {
  padding: 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.feature__title { margin: 0 0 0.4rem; font-size: 1rem; }
.feature p { margin: 0; color: var(--muted); font-size: 0.92rem; line-height: 1.55; }

.assurances {
  display: grid;
  gap: 0.9rem;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  max-width: 68rem;
  margin: 0 auto;
}
.assurance { margin: 0; color: var(--muted); font-size: 0.92rem; line-height: 1.55; }
.assurance strong { color: var(--text); display: block; }

.marketing__closer { padding: 3.5rem 1.4rem; text-align: center; }
.marketing__closer .marketing__heading { margin-bottom: 0.4rem; }
.marketing__closer p { margin: 0 0 1.2rem; }

/* --- pricing -------------------------------------------------------------- */

.marketing__link { color: var(--muted); text-decoration: none; font-size: 0.92rem; }
.marketing__link:hover { color: var(--text); }

.pricing-section { padding: 3rem 1.4rem; }

/* A toggle with no JavaScript in it: two radios, :has() to read which is
   checked, and a sibling combinator to hide the other set of prices. It works
   before the scripts load and works entirely without them, which matters more
   here than anywhere else in the app — this is the page a stranger sees first,
   on whatever their phone happened to load.

   Both sets of prices are in the markup, so switching is instant and there is no
   round trip. Without :has() support the yearly prices would simply show
   alongside the monthly ones, which is untidy rather than broken. */
.pricing-toggle {
  display: flex;
  justify-content: center;
  gap: 0;
  margin: 0 auto 1.6rem;
  width: fit-content;
  padding: 0.2rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
}
.pricing-toggle__input { position: absolute; opacity: 0; pointer-events: none; }
.pricing-toggle__label {
  padding: 0.35rem 1rem;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--muted);
  border-radius: var(--radius-pill);
  cursor: pointer;
}
.pricing-toggle__input:checked + .pricing-toggle__label {
  color: var(--accent-text);
  background: var(--accent);
}
.pricing-toggle__input:focus-visible + .pricing-toggle__label {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.pricing-toggle:has(#billing-month:checked) ~ .pricing .tier__price--year,
.pricing-toggle:has(#billing-year:checked) ~ .pricing .tier__price--month { display: none; }

.pricing {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  max-width: 62rem;
  margin: 0 auto;
  align-items: start;
}

.tier {
  display: flex;
  flex-direction: column;
  padding: 1.4rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.tier--featured { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft), var(--shadow-sm); }

.tier__name { margin: 0 0 0.3rem; font-size: 1.1rem; }
.tier__tagline { margin: 0 0 1rem; font-size: 0.88rem; color: var(--muted); min-height: 2.6em; }

.tier__price { margin: 0 0 1rem; display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.35rem; }
.tier__amount { font-size: 1.9rem; font-weight: 700; letter-spacing: -0.02em; }
.tier__per { font-size: 0.88rem; color: var(--muted); }
.tier__saving {
  flex-basis: 100%;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ok);
}

.tier__features { list-style: none; margin: 0 0 1.2rem; padding: 0; flex: 1; }
.tier__feature {
  padding: 0.3rem 0;
  font-size: 0.9rem;
  color: var(--muted);
  border-bottom: 1px solid var(--border-soft);
}
.tier__feature:last-child { border-bottom: 0; }

.pricing__note {
  max-width: 62rem;
  margin: 1.4rem auto 0;
  font-size: 0.88rem;
  text-align: center;
}

/* --- the footer, and the pages it points at ------------------------------- */

.footer {
  margin-top: 3rem;
  padding: 1.6rem 1.4rem;
  border-top: 1px solid var(--border-soft);
  background: var(--surface-2);
}
.footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem 1.4rem;
  max-width: 68rem;
  margin: 0 auto;
}
.footer__brand { margin: 0; font-weight: 600; font-size: 0.9rem; }
.footer__copyright { font-weight: 400; color: var(--muted); margin-left: 0.4rem; }

.footer__links { display: flex; flex-wrap: wrap; gap: 0.2rem 1rem; }
.footer__link { color: var(--muted); text-decoration: none; font-size: 0.88rem; }
.footer__link:hover { color: var(--text); text-decoration: underline; }

/* The entry layout centres one card in the viewport, so a footer after it would
   sit a screen further down. Pulled back up rather than left floating. */
.entry { min-height: auto; padding-block: 4rem; }

/* --- long-form prose ------------------------------------------------------ */

.prose {
  max-width: 44rem;
  margin: 0 auto;
  padding: 2.5rem 1.4rem 3rem;
}
.prose h1 { margin: 0 0 1.4rem; font-size: clamp(1.7rem, 4vw, 2.2rem); letter-spacing: -0.02em; }

/* --- the documentation ---------------------------------------------------- */

/* Wider than a policy. `.prose` is 44rem, which is right for a column of legal
   prose and too narrow for a table of keys beside its meanings — the meanings
   are sentences, and at 44rem most of them wrap. */
.docs { max-width: 52rem; }

.docs__lede { font-size: 1.1rem; color: var(--muted); }

.docs__index, .docs__more-list { list-style: none; padding: 0; margin: 1.2rem 0 0; }

.docs__index li, .docs__more-list li {
  display: grid;
  gap: 0.1rem;
  padding: 0.7rem 0;
  border-top: 1px solid var(--border);
}

.docs__index-link, .docs__more-link { font-weight: 600; text-decoration: none; }
.docs__index-link:hover, .docs__more-link:hover { text-decoration: underline; }

.docs__facts { margin: 1.2rem 0 0; }
.docs__facts dt { font-weight: 600; margin-top: 1rem; }
.docs__facts dd { margin: 0.2rem 0 0; color: var(--muted); line-height: 1.65; }

/* The key tables, which are the panel's own markup on a page rather than in a
   box — so a shortcut cannot read one way here and another way in the app. */
.docs__keys { margin-top: 1rem; }

.docs__more {
  max-width: 52rem;
  margin: 0 auto;
  padding: 0 1.4rem 3rem;
  border-top: 1px solid var(--border);
}

.docs__more-title { font-size: 1rem; margin: 1.6rem 0 0; }

/* --- an example: what you type, and what it becomes ----------------------- */

/* Side by side where there is room, stacked where there is not. The source is
   the thing being explained, so it goes first in the markup and stays first
   when the two columns become one. */
.example {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
  margin: 1rem 0 1.6rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
}

.example__source {
  margin: 0;
  padding: 0.9rem 1rem;
  background: var(--bg);
  border-right: 1px solid var(--border);
  font-size: 0.85rem;
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.example__source code { background: none; padding: 0; font-size: inherit; }

/* The right-hand half of a before-and-after, which is source rather than a
   rendering — so it keeps the source panel's look and loses the divider that
   would otherwise sit on the wrong side of it. */
.example__source--after { border-right: 0; }

.example__result, .example__becomes { padding: 0.9rem 1rem; }

/* The rendered half carries `.markdown`, which sets its own margins for a note
   body sitting in a page of its own. Inside a small box the first and last of
   those push the content off centre. */
.example__result > :first-child { margin-top: 0; }
.example__result > :last-child { margin-bottom: 0; }

/* One column for source that is deliberately NOT rendered — see
   DocsHelper#markdown_source. */
/* Source above the result, for a result too wide to live in half a column. */
.example--stacked { grid-template-columns: 1fr; }
.example--stacked .example__source { border-right: 0; border-bottom: 1px solid var(--border); }
.example--stacked .example__result { overflow-x: auto; }

.example--source-only { grid-template-columns: 1fr; }
.example--source-only .example__source { border-right: 0; border-bottom: 1px solid var(--border); }

.example__caption {
  grid-column: 1 / -1;
  padding: 0.6rem 1rem;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
}

@media (max-width: 44rem) {
  .example { grid-template-columns: 1fr; }
  .example__source { border-right: 0; border-bottom: 1px solid var(--border); }
}
.prose h2 { margin: 2.2rem 0 0.6rem; font-size: 1.2rem; }
.prose h3 { margin: 1.6rem 0 0.4rem; font-size: 1rem; }
.prose p, .prose li { line-height: 1.65; }
.prose ul, .prose ol { padding-left: 1.2rem; }
.prose li { margin: 0.3rem 0; }
.prose a { color: var(--accent); }
.prose strong { color: var(--text); }
.prose em { color: var(--muted); }

.prose table {
  width: 100%;
  border-collapse: collapse;
  margin: 1rem 0;
  font-size: 0.92rem;
}
.prose th, .prose td {
  text-align: left;
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--border-soft);
}
.prose th { font-weight: 600; }
.prose tbody tr:last-child td { border-bottom: 0; }

.prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.88em;
  padding: 0.1rem 0.3rem;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
}

.prose__updated { margin-top: 2.5rem; font-size: 0.85rem; }

/* --- redeeming a code ----------------------------------------------------- */

.redeem-form { margin: 1.2rem 0; }
.redeem-form__row { display: flex; gap: 0.4rem; align-items: center; }
.redeem-form__input {
  flex: 0 1 12rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  text-transform: uppercase;
}

.grants { list-style: none; margin: 0 0 1.2rem; padding: 0; }
.grants__item {
  padding: 0.45rem 0.6rem;
  margin-bottom: 0.3rem;
  font-size: 0.9rem;
  background: var(--ok-soft);
  border-radius: var(--radius-sm);
}

/* --- the one control the browser draws for us ----------------------------- */

/* <input type="search"> gets a clear button from the browser, drawn in the
   USER AGENT's accent rather than the page's — a blue cross on a green page,
   which reads as a rendering fault rather than a control. It cannot be recoloured
   directly, so it is replaced: the native appearance is dropped and the shape is
   painted as a mask in currentColor, which is the theme's own ink.

   Found by looking at a screenshot. No assertion in the suite would have caught
   it, because every element involved was styled exactly as its rules said. */
input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  width: 0.8em;
  height: 0.8em;
  cursor: pointer;
  opacity: 0.45;
  background-color: currentColor;
  -webkit-mask: var(--icon-clear) center / contain no-repeat;
  mask: var(--icon-clear) center / contain no-repeat;
}
input[type="search"]::-webkit-search-cancel-button:hover { opacity: 0.85; }

/* --- no pointer to hover with ---------------------------------------------

   Reported from an iPad: drag-and-drop in the sidebar does not work. Two things
   are true, and only one of them is ours.

   The platform half: these are native HTML5 drag events, which a touch is not.
   A finger scrolls the sidebar; it does not start a drag. That is why the nudge
   arrows exist — test/browser/arrows.mjs — and they are sufficient on their own,
   because an item at the edge of a folder steps OUT rather than stopping.

   Ours: every one of those arrows was revealed by :hover, so on a device that
   cannot hover the whole fallback was invisible. The answer to "I can't drag"
   was sitting behind a gesture the device does not have.

   hover: none is the question worth asking — not a width, and not a user-agent
   string. A tablet with a trackpad hovers and gets the tidy version; a laptop
   with a touchscreen still hovers. It asks about the pointer, which is the thing
   that actually decides whether these can ever be seen. */
@media (hover: none) {
  /* The drag fallback. Without this an iPad cannot reorder the sidebar at all. */
  .tree__row-actions { opacity: 1; }

  /* "Add a note to this item" — invisible until hovered, so on a touch device
     there was no way to write the FIRST note on a checklist row. Kept faint, as
     it is on hover: showing it is the point, shouting is not. */
  .checklist__has-note { opacity: 0.45; }
}

/* ---------------------------------------------------------------- the map

   A checklist drawn as a tree. The geometry is computed on the server — see
   MapLayout and docs/MINDMAPS-RESEARCH.md §4 — so everything here is about
   colour and texture, never about where anything is. The one exception is
   min-height on a node, which is what lets a label the server underestimated
   grow into the sibling gap instead of being truncated. */

/* The map scrolls inside its own frame, not the page: it is routinely wider
   than a screen, and a page that scrolls sideways as a whole is a page whose
   header wanders off. */
.map-frame {
  overflow: auto;
  max-height: 75vh;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
}

.map {
  position: relative;
  /* Scaled from the corner, because the sizer around it is what positions the
     scroll area — the controller does the arithmetic that keeps the middle of
     the view where it was. */
  transform-origin: 0 0;
}

/* What the scrollbars measure. A transform does not change layout, so without
   this the frame would still believe the map is its unscaled size. */
.map-sizer { position: relative; }

.map-frame:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Focused, the map is one branch of a bigger one and the way back has to be on
   screen — otherwise "focus" is a place you can get into and not out of. */
.map-trail {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
}
.map-trail__arrow { color: var(--muted); }
.map-trail__here { color: var(--muted); }

.map-controls {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 0.5rem;
}

.map-controls__level {
  min-width: 3.2rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  font-size: 0.85rem;
}

.map-controls__hint { margin-left: 0.35rem; font-size: 0.8rem; }

.map__edges {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.map__edges path {
  fill: none;
  /* --edge-colour is set inline, per line, from the colour of the node it
     reaches — so a coloured branch is coloured all the way rather than being
     boxes that happen to match. No colour, and it is an ordinary edge. */
  stroke: var(--edge-colour, var(--border));
  stroke-width: 1.6;
}

/* A cross-link is a different KIND of line: this node relates to that one, where
   the tree cannot say so. Dashed and quieter, so it reads as a note about the
   map rather than as part of its shape. */
.map__edges .map__link {
  /* Coloured from the node it LEAVES, where an edge is coloured by the node it
     reaches. A branch edge belongs to the child hanging off it; a link belongs
     to the node somebody stood on to make it, and points away from there. */
  stroke: var(--link-colour, var(--muted));
  stroke-width: 1.4;
  stroke-dasharray: 5 4;
  opacity: 0.75;
}

/* The head says which way round the link is. Filled rather than stroked, or the
   dashes would break it up.

   A marker cannot see the path that used it, so its colour is set on the marker
   and one marker is emitted per colour in play. `context-stroke` would say this
   in a line, and is not old enough to rely on. */
.map__edges .map__link-arrow {
  fill: var(--muted);
  stroke: none;
}

.map__nodes { list-style: none; margin: 0; padding: 0; }

.map__node {
  position: absolute;
  /* The note marker and the folded mark sit in this box's corners. */
  isolation: isolate;
  /* The map re-flows after every change: adding one node can move every other
     one. The controller puts each node back where it was and then lets it
     travel, so the map settles rather than teleporting — this is the property
     it animates. */
  will-change: transform;
  box-sizing: content-box;
  padding: 8px 12px;
  line-height: 20px;
  font-size: 0.875rem;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  /* AFTER the shorthand, never before it. `border:` sets border-color too, so a
     border-color written above it is overwritten by the fallback and the chosen
     colour only ever appeared on the rules that happened to come later — which
     is precisely how this shipped once: coloured when selected, plain when not.

     --node-colour is set inline when somebody chose one, and inherited down a
     branch by the server. Absent, a node is bordered like anything else. */
  border-color: var(--node-colour, var(--border));
  border-radius: var(--radius-sm);
  overflow-wrap: anywhere;
  box-shadow: var(--shadow-sm);
}

/* The centre is the note itself, so it is the one thing on the map that is not
   a row. It reads as the title it is. */
/* An inset ring rather than a thicker border. A 2px border would make the root
   two pixels wider and taller than MapLayout reserved for it — which is exactly
   the error the prototype shipped with, and the reason BORDER is counted at
   all. A shadow is drawn inside the box and moves nothing. */
.map__node--root {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent), var(--shadow-sm);
  font-weight: 600;
  background: var(--surface-2);
}

/* Bottom corner, so it never argues with the note marker above it. */
.map__folded {
  position: absolute;
  right: 4px;
  bottom: 2px;
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1;
}

/* The note marker is a button, because a note reachable only from the keyboard
   is a note half the readers cannot open. Faint until there is something behind
   it — a map's value is partly in seeing at a glance which parts have depth. */
/* Pinned to the box's own corner rather than flowing after the label.
   Inline, it landed wherever the text happened to end — a different place on
   every node, and sometimes wrapped onto a line of its own. Reported.

   It sits in the node's padding and carries the node's background, so a long
   first line passes behind it rather than through it — and the text column the
   server measured is untouched, which is what keeps the geometry exact. */
.map__has-note {
  position: absolute;
  top: 4px;
  right: 3px;
  padding: 0 1px;
  border: 0;
  background: var(--surface);
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1.2;
  cursor: pointer;
  opacity: 0.25;
}
.map__has-note.is-present { opacity: 1; color: var(--accent); }
.map__has-note:hover, .map__has-note:focus-visible { opacity: 1; }

/* Linking is a mode, and a mode has to say it is one — the map looks otherwise
   exactly as it did a moment ago. */
.map-linking { margin: 0 0 0.5rem; font-size: 0.85rem; color: var(--muted); }

/* The two ends of a link as BUTTONS, for a device with no Enter and no Escape —
   without which linking was a mode a tablet could enter and not leave. Spacing
   rather than a display of their own: `.on-touch` is what makes them visible at
   all and it sets `display: inline`, so a flex row here would be a specificity
   argument with the rule that shows them. */
.map-linking__buttons { margin-inline-start: 0.4rem; }
.map-linking__buttons .btn + .btn { margin-inline-start: 0.3rem; }
.map-view.is-linking .map-linking { display: block; }
.map-view:not(.is-linking) .map-linking { display: none; }

/* Where the link started from, so both ends are visible while the second is
   being chosen. */
.map__node.is-link-source {
  border-color: var(--accent);
  border-style: dashed;
}

/* A node's note: along the bottom, over the map, never expanding it.
   Positioned against the VIEW rather than the frame, because the frame scrolls
   and an absolutely positioned child of a scroller scrolls away with it.
   docs/MINDMAPS-RESEARCH.md §5.2.

   The board's card panel, to the pixel, and deliberately: these are the same
   object one note type apart — a thing you selected, opened underneath what you
   are looking at. It used to be a 26rem box floated beside the node, which the
   controller had to flip and clamp to keep on screen and which was still a
   column of text six words wide to write a paragraph in. Reported. */
.map-note {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  z-index: 5;
  /* A ceiling and NO floor, which is where this parts company with the card
     panel it otherwise copies. The board's floor is there because its panel
     holds four fields and a move log, and a share of a short board is four
     lines and a scrollbar. This one holds a single textarea, which brings its own
     height: the writing face measures 304px unaided, and a floor of 22rem only
     ever showed up on the READING face, as 220px of white under a one-line
     note. Measured, not guessed. */
  max-height: 80%;
  overflow: auto;
  padding: 0.9rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: var(--shadow-md);
}
.map-note[hidden] { display: none; }

/* Whose note. Beside the node this went without saying; along the bottom it
   does not. */
.map-note__title {
  margin: 0 0 0.7rem;
  font-size: 1rem;
}

.map-note__label { display: block; margin-bottom: 0.35rem; font-size: 0.85rem; }

/* A note as it reads, rather than as it was typed. The PANEL is what scrolls
   now and what is bounded — a second scroller inside it gave a long note two
   bars side by side, one of which moved nothing. */
.map-note__rendered > :first-child { margin-top: 0; }
.map-note__rendered > :last-child { margin-bottom: 0; }
.map-note__rendered img { max-width: 100%; height: auto; }

.map-note__reader[hidden], .map-note__writer[hidden] { display: none; }
.map-note__field { width: 100%; }

.map-note__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.5rem;
  font-size: 0.8rem;
}

/* The containing block for the note panel — which is a sibling of the frame and
   not a child of it, because the frame scrolls. `.board-view` says the same
   thing for the same reason. */
.map-view { position: relative; }

/* A folded branch is drawn as one node, so the node has to say that there is
   more underneath it — otherwise a map with half of it put away looks like a
   map that never had the other half. */
.map__folded { font-weight: 700; }

/* A node being renamed. The same box, with a field in it rather than a label —
   so the geometry the server computed still holds while it is being typed in. */
.map__input {
  display: block;
  width: 100%;
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  line-height: inherit;
}
.map__input:focus { outline: none; }

/* Dragged, and where it would land.
   A drag re-parents; it never stores a position. But a drag where nothing moves
   is a drag nobody believes is happening, so the node follows the pointer and
   is lifted while it does — and the map says which node it would go under, or
   that it is about to cross the centre. Reported: the first version changed
   nothing on screen and started a text selection instead. */
.map__node.is-dragging {
  opacity: 0.85;
  z-index: 3;
  cursor: grabbing;
  /* Invisible to hit-testing while it is in the air. It now travels WITH the
     pointer, so without this it is the thing under the pointer and the map can
     never tell what you are dropping it on. */
  pointer-events: none;
  border-color: var(--accent);
  box-shadow: var(--shadow-md);
  /* It is being carried, not settling: the FLIP transition that makes a
     re-layout readable would make a drag lag behind the pointer. */
  transition: none;
}

.map__node.is-drop-target {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent);
}

/* Nothing else is selectable or clickable while a node is in the air. Without
   this the browser highlights every label the pointer crosses, which reads as
   "this control does not work". */
.map-view.is-dragging-node { user-select: none; }
.map-view.is-dragging-node .map__label { pointer-events: none; }

/* A node is dragged, not selected — say so before the drag starts. */
.map__node { cursor: grab; }
.map__node--root { cursor: default; }

/* Dragging the background moves the map under you. touch-action: none so a
   pointer drag is ours rather than the browser's scroll. */
.map-view.is-panning .map-frame { cursor: grabbing; }
.map-view.is-panning .map__node { pointer-events: none; }

/* Selection outranks colour: which node you are on has to be legible whatever
   the branch is painted. The ring OUTSIDE the box says selected; the border goes
   on saying which branch this is.

   One ring, not two. With an inset ring as well, a coloured node was drawn as
   accent / colour / accent — the colour sandwiched inside the selection rather
   than shown by it, which reads as a rendering fault rather than as a state.
   Reported. The inset ring stays where it started, on the root, where it is the
   only ring on the box. */
.map__node.is-selected {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* ------------------------------------------------------- the map, by finger

   AT THE END OF THE FILE, and that is load-bearing rather than tidy: a media
   query placed before the rules it overrides does nothing at all, which has
   already happened once in this stylesheet. The map's own rules are above; these
   have to come after them.

   `hover: none` is the question worth asking — not a width and not a user-agent
   string. It asks about the pointer, which is the thing that actually decides
   whether a keyboard shortcut is reachable. */

/* Hidden wherever there is a keyboard to do this with. */
.touch-bar { display: none; }

/* Two ways of saying the same instruction, and the pointer decides which is
   true. A sentence naming a key is no help on a device that has none, and the
   name of a button is no help to somebody who is about to press the key. */
.on-touch { display: none; }

/* One finger on the BACKGROUND scrolls the map; two anywhere zoom it. pan-x
   pan-y leaves the scrolling to the browser and takes pinch-zoom away from it,
   so a pinch reaches the map rather than zooming the whole page out from under
   it. */
.map-frame { touch-action: pan-x pan-y; }

/* A finger on a NODE is dragging that node, so the browser must not read it as
   a scroll — once it decides the gesture is a scroll it sends pointercancel,
   and the drag is over before it started. */
.map__node { touch-action: none; }

@media (hover: none) {
  .on-keys { display: none; }
  .on-touch { display: inline; }

  .touch-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.5rem;
  }

  /* A node's note marker is faint until there is something behind it, which on a
     touchscreen means invisible until you know it is there. Same fix, and the
     same reason, as the checklist row's. */
  .map__has-note { opacity: 0.5; }

  /* Nothing hovers, so nothing can be revealed by hovering: the drop target and
     the drag state are all the feedback there is. */
  .map__node { cursor: default; }
}

/* --- a checklist, drawn as a board ---------------------------------------- */

/* docs/KANBAN-PLAN.md Phase 1. Read-only, and here to be measured: three of the
   rules below are the traps research §4.3 named in advance, and one of them
   (sticky heads) is the thing this phase exists to find out about. */

.note--board {
  display: flex;
  flex-direction: column;
  /* height, not min-height, for the same reason .note--map has it: a board
     scrolls inside its own frame, and an unbounded article just grows to fit
     and never scrolls at all. */
  height: calc(100dvh - 3.2rem - 3rem);
  overflow: hidden;
}

/* The ONE thing on the page that scrolls sideways. Research §4.2: the page body
   must never scroll horizontally, so the overflow lives here and nowhere above
   it.

   scroll-snap on the frame, snap points on the columns: on a phone that turns a
   swipe into a column landing squarely rather than half a column and a sliver,
   which is most of what a one-column-at-a-time picker would have bought without
   being a second interface to build. */
.board-frame {
  flex: 1;
  /* Trap one, in its usual disguise. A flex item's min-height is auto, so
     without this the frame grows to fit its tallest column and the page gets
     longer instead of the frame scrolling.

     A FLOOR rather than zero, though. `min-height: 0` says the frame may shrink
     to nothing, and in a short window — everything above it is a fixed cost —
     it does exactly that: measured at 0px tall in a 420px viewport, which is a
     board you cannot see at all. Below the floor the page scrolls, which is
     what a page does when it does not fit. Found while re-pointing the Phase 1
     measurement suites at real notes. */
  min-width: 0;
  min-height: 14rem;
  overflow: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
}

.board-frame:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.board {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem;
  /* min-content, so the row is as wide as its columns rather than as wide as
     the frame — otherwise the last column's padding is dropped at the point the
     board starts scrolling. */
  width: max-content;
  min-height: 100%;
}

/* Fixed width, the same on every screen. Research §4.2: one column at a time
   with a picker is a second interface to design, test in three engines and keep
   in step with the first. */
.board-column {
  flex: 0 0 17rem;
  /* Trap one proper: a flex child refuses to shrink below its content, so one
     long unbroken word in a card title would push the column — and with it the
     whole board — wider. */
  min-width: 0;
  display: flex;
  flex-direction: column;
  scroll-snap-align: start;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
}

/* THE MEASUREMENT. Sticky inside a horizontally scrolling flex row is the
   fiddliest combination in CSS and research §4.3 refused to assume it works.
   test/browser/board.mjs compares the head's box to the frame's after scrolling
   and reports what each engine actually did. If one of them disagrees, this
   rule is what changes — and it changes here, in a read-only view, rather than
   in Phase 5 with a keyboard on top of it. */
.board-column__head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0;
  padding: 0.6rem 0.7rem;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text);
  background: var(--surface-2);
  border-bottom: 1px solid var(--border-soft);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

/* The name gives way and the count does not: a count is four characters and a
   name is as long as somebody made it. */
.board-column__label {
  display: flex;
  align-items: baseline;
  min-width: 0;
}

.board-column__name {
  min-width: 0;
  overflow-wrap: anywhere;
}

.board-column__numbers {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  font-weight: 500;
  color: var(--muted);
}

.board-column__count { font-variant-numeric: tabular-nums; }

/* The tick beside the name on the column that means finished. Quiet, because
   the column's own edge is already saying it in colour — this is the half a
   reader who cannot see the colour still gets. */
.board-column__finishes {
  flex: 0 0 auto;
  margin-left: 0.35rem;
  color: var(--ok);
  font-size: 0.85rem;
}

/* Over its limit — advisory, never enforced (research §5). The colour is the
   glance; the "4/3" beside it is the fact, and it is in the heading's text so a
   reader who cannot see the colour still has the number. */
.board-column__count.is-over {
  color: var(--danger);
  font-weight: 600;
}

.board-column__estimate {
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}

/* A column somebody painted, and the one that means finished. Both are a quiet
   edge rather than a fill: a board is read by its cards, and a coloured column
   that shouts drowns them.

   The transparent edge is on every column so a coloured one is not 3px taller
   than its neighbours — and it is a modifier class rather than a var() with a
   fallback, because a fallback to `transparent` is the one shape the stylesheet
   guard refuses. Same pattern as .board-card--coloured, one level up. */
.board-column { border-top: 3px solid transparent; }
.board-column--finishes { border-top-color: var(--ok); }
.board-column--coloured,
.board-column--coloured.board-column--finishes { border-top-color: var(--column-colour); }

.board-column__cards {
  list-style: none;
  margin: 0;
  padding: 0.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.board-column__empty {
  margin: 0;
  padding: 0 0.7rem 0.8rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.board-card {
  padding: 0.55rem 0.65rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-sm);
}

/* Research §3.7: colour stays on the card's EDGE. A left stripe rather than the
   border, so Phase 3's selection ring has the border to itself — the map
   learned that one the hard way, drawing a coloured node as accent / colour /
   accent and reading as a rendering fault rather than as a state. */
.board-card--coloured {
  border-left: 3px solid var(--card-colour, var(--border));
  padding-left: 0.5rem;
}

/* ONE quiet line, and only what is set is in it. */
.board-card__facts {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.5rem;
  margin: 0.35rem 0 0;
  font-size: 0.75rem;
  color: var(--muted);
}

.board-card__fact { white-space: nowrap; }

.board-card__who {
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 9rem;
}

.board-card__tasks,
.board-card__age,
.board-card__estimate { font-variant-numeric: tabular-nums; }

/* The status roles that already exist, so a due date needs no new colour and
   works on every palette — research §3.3. The word beside it is what carries
   the meaning; the colour is the glance. */
.board-card__due.is-today { color: var(--warn); }
.board-card__due.is-overdue { color: var(--danger); font-weight: 600; }

/* The content and the card's one control, side by side. The control does not
   shrink; the words take what is left. */
.board-card__line {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
}

.board-card__line .board-card__content { flex: 1; min-width: 0; }

/* The same mark the checklist's row and the map's node use, doing the same
   thing: open this one's note. Faint until there IS one, because "add a
   description" and "read the description" are worth telling apart at a glance
   without being two different marks to learn. */
.board-card__has-note {
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.35;
  cursor: pointer;
  opacity: 0.35;
}

.board-card__has-note.is-present { opacity: 0.85; }
.board-card__has-note:hover { opacity: 1; }

.board-card__has-note:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-radius: var(--radius-sm);
}

/* Nothing hovers on a touch screen, so a control that only appears on hover is
   a control that does not exist there. It is always drawn; hover only brightens
   it. */
span.board-card__has-note { cursor: default; }

/* A card is as tall as its text. Research §4.1: the board has no reserved
   height to undercount, which is the whole reason this is smaller than the map.
   `anywhere` rather than `break-word` so a pasted URL wraps too. */
.board-card__content {
  margin: 0;
  font-size: 0.9rem;
  line-height: 1.35;
  overflow-wrap: anywhere;
}



/* --- a board, driven from the keyboard ------------------------------------ */

/* docs/KANBAN-PLAN.md Phase 3. */

.board-view {
  display: flex;
  flex-direction: column;
  /* flex: 1 needs a flex parent that is itself growing — see the reader-wrapper
     rule above, without which this line did nothing at all. */
  flex: 1;
  min-width: 0;
  min-height: 0;
  position: relative;
}

/* There for a screen reader and for nothing else.
   Not `display: none` and not `hidden`, either of which removes it from the
   accessibility tree along with everything else — the point is to be READ and
   not seen. */
.visually-hidden,
.board-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* .board-live is what a move says out loud. Present in every state so a screen
   reader has something to watch — a live region that appears later, already
   full, announces nothing. */

/* Selection outranks colour, which is the map's lesson: which card you are on
   has to be legible whatever the card is painted. The ring is OUTSIDE the box,
   so a card's colour stripe goes on saying what it says. */
.board-card.is-selected,
.board-column__head.is-selected {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.board-column__head.is-selected { border-radius: var(--radius-sm) var(--radius-sm) 0 0; }

.board-card:focus-visible,
.board-column__head:focus-visible { outline: 2px solid var(--accent); }

/* Renaming happens in place, in the element the name was in, so nothing moves
   under the caret. */
.board-rename {
  width: 100%;
  font: inherit;
  padding: 0.05rem 0.25rem;
}

/* --- a card's panel -------------------------------------------------------- */

/* A POPOVER, not an expansion — the map's choice and for the map's reason: a
   card that grew to hold a paragraph would push every card below it down the
   column at the moment somebody is reading them. */
.card-panel {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  z-index: 3;
  /* A floor as well as a ceiling. 70% of a tall board is generous and 70% of a
     short one is four lines and a scrollbar — and a board with three columns and
     two cards in it is short. The min is what a person needs to write a
     description in; the max is what stops the panel covering the board it is
     about. */
  min-height: min(22rem, 100%);
  max-height: 80%;
  overflow: auto;
  padding: 0.9rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: var(--shadow-md);
}

.card-panel__title {
  margin: 0 0 0.7rem;
  font-size: 1rem;
}

.card-panel__fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: 0.6rem 0.9rem;
}

/* The description wants the whole width; the three short fields share what is
   left. minmax(0, …) rather than 1fr because 1fr grows to its content, which is
   research §4.3's trap two in a grid this small. */
.card-panel__field:first-child { grid-column: 1 / -1; }

.card-panel__field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
}

.card-panel__label { font-size: 0.8rem; color: var(--muted); }

.card-panel__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.8rem;
  flex-wrap: wrap;
}

/* Two faces: the card as it reads, and the card being written. The map's note
   editor one type along, and for the same reason — a panel that only ever
   showed the source meant coming back to a card showed Markdown rather than the
   note it makes. */
.card-panel__rendered { margin-bottom: 0.5rem; }

.card-panel__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 0.9rem;
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.card-panel__fact { white-space: nowrap; }

/* --- where a card has been ------------------------------------------------- */

/* READ-ONLY. Sentences, no buttons: research §0 and §6.1 — a record, not a
   second history, and there is no way to put one card back on its own. */
.card-log {
  margin-top: 1rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--border-soft);
}

.card-log__heading {
  margin: 0 0 0.5rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.card-log__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: 0.85rem;
}

.card-log__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem;
}

.card-log__when {
  flex: 0 0 auto;
  color: var(--muted);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}

.card-log__more { margin: 0.15rem 0; }

/* --- moving a card with a pointer, and with a finger ----------------------- */

/* docs/KANBAN-PLAN.md Phase 4. Research §9. */

/* The frame keeps its scrolling gestures — `pan-x pan-y` rather than `none`.
   The map puts `touch-action: none` on anything draggable, and that finding
   does NOT carry here: a board is dense, so a column of twenty cards is twenty
   cards' worth of pixels that a finger could no longer scroll. A touch drag is
   a long press instead, and until it lands the browser owns the gesture. */
.board-frame { touch-action: pan-x pan-y; }

/* A card is dragged, so say so before the drag starts. */
.board-card { cursor: grab; }

/* Held long enough to mean it. The lift is the only sign a long press has
   landed, and a long press with no feedback is one nobody holds long enough. */
.board-card.is-lifted {
  transform: scale(1.02);
  box-shadow: var(--shadow-md);
}

.board-card.is-dragging {
  cursor: grabbing;
  opacity: 0.9;
  box-shadow: var(--shadow-md);
  /* Above its neighbours while it travels, and out of the way of the question
     "what is under the pointer" — which the drop test answers from the POINTER
     rather than from elementFromPoint for exactly this reason. */
  position: relative;
  z-index: 2;
  pointer-events: none;
}

/* SCROLL SNAP HAS TO STAND DOWN WHILE A CARD IS IN THE AIR.
   
   Two of this design's own decisions collide here, and only a browser said so.
   §4.2 puts `scroll-snap-type: x proximity` on the frame so a swipe lands on a
   column edge. §9.6 wants edge autoscroll to nudge the frame a few pixels per
   frame while a card is dragged towards the edge. Snapping ate every one of
   those nudges — a 19px scroll landed inside the snap zone of the column it
   started at and was pulled straight back, so `scrollLeft` went 12 → 12 → 12
   forever while the loop ran perfectly.
   
   Snapping is for a finger flicking through columns. While a card is in the
   air the pointer is what is in charge, so the snapping stops. */
.board-view.is-dragging-card .board-frame { scroll-snap-type: none; }

/* Nothing under the pointer should look interactive while a card is in the
   air, and text selecting mid-drag reads as highlighting rather than moving. */
.board-view.is-dragging-card { user-select: none; }
.board-view.is-dragging-card .board-card__has-note { pointer-events: none; }

/* SOMEWHERE TO DROP THAT SAYS SO. The map shipped without this and it was the
   first thing reported — a drag with no target shown is a drag whose outcome
   you have to guess. */
.board-column.is-drop-target { background: var(--accent-soft); }

/* The line lands ABOVE the card it would push down, or at the end of the list.
   box-shadow rather than a border, because a border would move every card below
   it by two pixels at the moment somebody is aiming at one. */
.board-card.is-drop-before { box-shadow: 0 -3px 0 0 var(--accent); }

.board-column.is-drop-end .board-column__cards::after {
  content: "";
  display: block;
  height: 3px;
  border-radius: var(--radius-pill);
  background: var(--accent);
}

/* An EMPTY column is a drop target — research §9.5, the case that gets
   forgotten. Its "Nothing here" has to be a place, not a gap. */
.board-column.is-drop-target .board-column__empty { color: var(--accent); }

@media (hover: none) {
  /* Nothing hovers, and a grab cursor means nothing. The lift is the feedback. */
  .board-card { cursor: default; }
}

/* --- columns as things ----------------------------------------------------- */

/* docs/KANBAN-PLAN.md Phase 5. */

/* The one board-wide readout in this design — research §3.3. Shown only above
   zero, so it never becomes a badge people stop seeing. */
.board-header {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  margin: 0 0 0.4rem;
  min-height: 1.2rem;
  font-size: 0.85rem;
}

.board-header__overdue {
  color: var(--danger);
  font-weight: 600;
}

/* docs/KANBAN-PLAN.md Phase 8 — whose card is it. The picker IS the banner:
   what is on screen is said by the control that chose it, rather than beside
   it where the two can disagree. */
/* --- the box that narrows one note ----------------------------------------
   docs/FILTERING-PLAN.md. It sits in the header beside the archive toggle and
   the assignee picker, which is the row of "how am I looking at this" controls
   — and outside the frame, which is replaced on every redraw. */

.note-filter { display: inline-flex; align-items: center; gap: 0.5rem; }

.note-filter__input { width: 12rem; }

.note-filter__said { font-size: 0.8rem; white-space: nowrap; }

/* On a checklist the box sits above the frame rather than in a header row. */
.note-filter + .checklist-root { margin-top: 0.6rem; }

/* A row drawn because something UNDER it matched, not because it did — the
   sidebar's path node, one type along. Dimmed rather than stripped: a parent you
   cannot tick while filtering would be a filter that takes things away. */
.checklist__item--context > .checklist__row { opacity: 0.55; }

/* A card whose words were in its description, not on its face — §4.2. The
   marker it already carries, lit, so the answer to "why is this here" is the
   one thing on the card that opens the place the words are. */
.board-card__has-note.is-matched {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-radius: 4px;
}

.board-filter {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
  margin-left: auto;
}

.board-filter[hidden] { display: none; }

.board-filter__select { width: auto; }

/* DIMMED, NOT REMOVED. The card is still in the DOM, still counted by its
   column, still found by the browser's own search and still reachable by the
   arrows — this is a way of LOOKING at the board, and a board whose shape
   changes while you read it is not one you can trust.

   Opacity alone, and never `display`, `visibility` or `content-visibility`:
   each of those would take the card out of find-on-page, which is exactly the
   thing somebody filtering a board is most likely to reach for next. */
.board-card.is-dimmed {
  opacity: 0.32;
}

/* Except when it is the one you are standing on. Filtering must not be able to
   hide the selection: the keys still move through dimmed cards, so the card the
   keyboard is on has to be visible or the board loses its reader. */
.board-card.is-dimmed.is-selected {
  opacity: 1;
}

@media (prefers-reduced-motion: no-preference) {
  .board-card { transition: opacity 120ms ease-out; }
}

/* The column that means finished says so in WORDS as well as in colour —
   research §11, the same rule the WIP count and an overdue date follow. */
.board-column__finishes {
  flex: 0 0 auto;
  color: var(--ok);
  font-size: 0.8rem;
}

/* THE ARCHIVE IS A COLUMN THAT IS USUALLY NOT THERE — research §7.1.
   Drawn like one and stored as nothing: it is `archived_at IS NOT NULL` in the
   shape of a column, which is what keeps it out of BoardText and out of every
   count. Set apart so it does not read as a fifth place work can be. */
.board-column--archive {
  background: var(--bg);
  border-style: dashed;
}

.board-column--archive .board-column__head {
  background: var(--bg);
  color: var(--muted);
}

/* A checkbox and its words on one line, which the four card fields do not need
   and this one does. */
.card-panel__field--check {
  flex-direction: row;
  align-items: flex-start;
  gap: 0.5rem;
}

.card-panel__hint {
  display: block;
  font-size: 0.75rem;
  line-height: 1.35;
}

.card-panel__bulk {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.8rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--border-soft);
  font-size: 0.8rem;
}
