:root {
  --bg: #0f1115;
  --bg-panel: #161a21;
  --bg-card: #1c212a;
  --bg-card-hover: #222835;
  --line: #262d38;
  --text: #e7ecf3;
  --text-dim: #94a1b3;
  --text-faint: #66707f;
  --accent: #6ee7a8;      /* the theme: a light green that holds up on this dark ground */
  --accent-ink: #06231a;  /* text and glyphs drawn on top of it */
  --ok: #3fb950;
  --warn: #d29922;
  --bad: #f85149;
  --shadow: 0 6px 24px rgba(0, 0, 0, .35);
  --status-active: #4c8dff;
  color-scheme: dark;
}

.status-planned { --status: #7b8798; }
.status-active  { --status: var(--status-active); }
.status-blocked { --status: #d29922; }
.status-done    { --status: #3fb950; }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.5 "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { margin: 0; font-weight: 600; }
button, input, select, textarea { font: inherit; color: inherit; }

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

.topbar {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 14px 20px;
  background: linear-gradient(180deg, #171c24, #12151b);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 20;
}

.brand { display: flex; align-items: center; min-width: 0; }
.brand-mark {
  width: 28px; height: 28px; flex: none;
  object-fit: contain;
}
.path {
  margin: 2px 0 0; font-size: 11px; color: var(--text-faint);
  max-width: 42ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.overall { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.ring { width: 44px; height: 44px; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: 5; stroke-linecap: round; }
.ring-track { stroke: #232a35; }
.ring-value { stroke: var(--ok); stroke-dasharray: 125.6; stroke-dashoffset: 125.6; transition: stroke-dashoffset .4s ease; }
.overall-text { display: flex; flex-direction: column; line-height: 1.25; }
.overall-text strong { font-size: 17px; }
.overall-text span { font-size: 11px; color: var(--text-dim); }

/* Held in the layout whether or not it is showing, so the bar does not twitch
   every time a keystroke is saved. */
.saving { font-size: 11px; color: var(--warn); white-space: nowrap; visibility: hidden; }
.saving.is-on { visibility: visible; }

.tabs { display: flex; gap: 4px; background: #10141a; padding: 4px; border: 1px solid var(--line); }
.tab {
  border: 0; background: transparent; color: var(--text-dim);
  padding: 6px 16px; cursor: pointer;
}
.tab:hover { color: var(--text); }
.tab.is-active { background: var(--accent); color: var(--accent-ink); }

/* ---------- the sidebar: writing a task, and narrowing the board ---------- */

/* These two used to be a bar across the top, which put them equally far from
   every column and made the composer a single line. As the board's own first
   column they are near the column a task is written into, and the box has room
   to be three lines deep — which is what a task written for an agent needs. */
.sidebar { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.sidebar > * {
  background: var(--bg-panel); border: 1px solid var(--line);
  padding: 12px;
}
.sidebar-head { font-size: 13px; color: var(--text-dim); margin-bottom: 10px; }
.quick-add { display: flex; flex-direction: column; gap: 10px; }
.quick-add button[type="submit"] { width: 100%; }
.filters { display: flex; flex-direction: column; gap: 8px; }
.filters .with-key, .filters .key-after { display: flex; }
.filters input, .filters select { width: 100%; min-width: 0; }

input, select, textarea {
  background: #0e1218; border: 1px solid var(--line);
  padding: 7px 10px; outline: none;
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
input::placeholder { color: var(--text-faint); }

button {
  background: var(--accent); border: 1px solid transparent; color: var(--accent-ink);
  padding: 7px 14px; cursor: pointer;
}
button:hover { filter: brightness(1.1); }
button.ghost { background: transparent; border-color: var(--line); color: var(--text-dim); }
button.ghost:hover { color: var(--text); border-color: #3a4453; filter: none; }
button.danger { background: transparent; border-color: #4a2a2a; color: var(--bad); }

/* ---------- views ---------- */

main { padding: 18px 20px 40px; }
.view { display: none; }
.view.is-active { display: block; }

.board-layout {
  display: grid; grid-template-columns: minmax(240px, 300px) minmax(0, 1fr); gap: 14px;
  align-items: start;
}
.board {
  display: grid; grid-template-columns: repeat(3, minmax(200px, 1fr)); gap: 14px;
  align-items: start;
}
/* 已完成 over 阻塞 in one track: between them they hold the cards nobody is
   working on today, and neither earns a column of its own beside the two that
   are the actual work.

   Neither of them scrolls inside itself. A column draws at most a page of
   cards (PAGE in board.js) and ends in a 加载更多 button, so it has a bounded
   height without a scroll area — and the page keeps the one scrollbar it
   ought to have, the outermost one. */
.column-stack { display: flex; flex-direction: column; gap: 14px; min-width: 0; }

@media (max-width: 1280px) { .board-layout { grid-template-columns: 220px minmax(0, 1fr); } }
@media (max-width: 1080px) {
  .board-layout { grid-template-columns: 1fr; }
  .board { grid-template-columns: repeat(2, minmax(200px, 1fr)); }
}
@media (max-width: 640px) {
  .board { grid-template-columns: 1fr; }
}

/* The window on a long column (board.js): the button is also the sentinel the
   observer watches, so it has to be a full-width target at the end of the
   list rather than something tucked into a corner. */
.load-more { width: 100%; font-size: 12px; padding: 8px; }

.column {
  background: var(--bg-panel); border: 1px solid var(--line);
  display: flex; flex-direction: column; min-height: 160px;
}
.column-head {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px; border-bottom: 1px solid var(--line);
}
.column-head .dot { width: 8px; height: 8px; background: var(--status); }
.column-head h2 { font-size: 13px; }
.column-head .count { margin-left: auto; color: var(--text-faint); font-size: 12px; }
.column-body { padding: 10px; display: flex; flex-direction: column; gap: 8px; min-height: 60px; }
.column.drag-over { border-color: var(--accent); }
.column.drag-over .column-body { background: rgba(110, 231, 168, .07); }

.card {
  background: var(--bg-card); border: 1px solid var(--line); border-left: 3px solid var(--status);
  padding: 9px 10px; cursor: grab; position: relative;
}
.card:hover { background: var(--bg-card-hover); }
.card.dragging { opacity: .4; }
.card.drop-before { box-shadow: 0 -2px 0 var(--accent); }
.card-title { font-size: 13px; word-break: break-word; }
/* Dimmed, not struck through. The rule was there to say "finished", and the
   colour already says it; the line on top of it makes the one thing you most
   want to read in that column — what you actually got done — harder to read. */
.card.is-done .card-title { color: var(--text-dim); }
.card-meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 7px; }

/* Every chip is the same box, whatever is in it and whatever else on the page
   happens to share one of its class names. The fixed height and the flat
   line-height are what make that true structurally: a chip cannot be grown by
   its content, by a longer name, or by a rule written for something else that
   was also called "milestone". */
.chip {
  display: inline-flex; align-items: center;
  height: 18px; padding: 0 6px; margin: 0;
  font-size: 10px; line-height: 1; white-space: nowrap;
  border: 1px solid var(--line); color: var(--text-dim); background: #0f131a;
}
.chip.tag { color: #9fd0ff; border-color: #26374d; }
.chip.milestone { color: #c7a6ff; border-color: #3a2f52; }

.card-actions { display: flex; gap: 4px; margin-top: 8px; opacity: 0; transition: opacity .12s; }
.card:hover .card-actions, .card:focus-within .card-actions { opacity: 1; }
.card-actions button { padding: 2px 8px; font-size: 11px; }

.drop-placeholder {
  height: 34px; border: 1px dashed var(--accent);
  background: rgba(110, 231, 168, .08);
}

.empty { color: var(--text-faint); font-size: 12px; text-align: center; padding: 14px 6px; }

/* ---------- plan ---------- */

.plan-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 16px; align-items: start; }
@media (max-width: 980px) { .plan-grid { grid-template-columns: 1fr; } }

.panel { background: var(--bg-panel); border: 1px solid var(--line); padding: 16px; }
.panel > h2 { font-size: 14px; margin-bottom: 12px; }
.panel-head { display: flex; align-items: center; margin-bottom: 12px; }
.panel-head h2 { font-size: 14px; }
.panel-head > button:last-child { margin-left: auto; }
.panel-sub { margin-top: 20px; border-top: 1px solid var(--line); padding-top: 14px; }
.panel-sub h3 { font-size: 12px; color: var(--text-dim); margin-bottom: 10px; }

/* Scoped to the panel it lives in, because "milestone" names two different
   things on this page: the card below, and the chip on a task that says which
   milestone the task is filed under. Unscoped, this rule reached the chip and
   gave it 12px of padding and a bottom margin — which is why one chip in the
   row was twice the height of its neighbours. */
#milestones .milestone {
  border: 1px solid var(--line); padding: 12px; margin-bottom: 10px;
  background: #141922;
}
#milestones .milestone.is-complete { border-color: #22402a; }
#milestones .milestone.is-overdue { border-color: #4a2a2a; }
.milestone-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.milestone-head h3 { font-size: 14px; }
.milestone-head .due { font-size: 11px; color: var(--text-dim); }
.milestone-head .due.overdue { color: var(--bad); }
.milestone-head .due.soon { color: var(--warn); }
.milestone-head .ops { margin-left: auto; display: flex; gap: 4px; opacity: 0; transition: opacity .12s; }
#milestones .milestone:hover .ops { opacity: 1; }
.milestone-head .ops button { padding: 2px 8px; font-size: 11px; }
#milestones .milestone .goal { color: var(--text-dim); font-size: 12px; margin: 6px 0 0; }

.bar { display: flex; height: 8px; overflow: hidden; background: #0e1218; margin: 10px 0 6px; }
.bar span { display: block; }
.bar .seg-done { background: var(--ok); }
.bar .seg-active { background: var(--status-active); }
.bar .seg-blocked { background: var(--warn); }
.bar .seg-planned { background: #39414f; }
.bar-legend { display: flex; gap: 10px; font-size: 11px; color: var(--text-faint); flex-wrap: wrap; }
.bar-legend b { color: var(--text-dim); font-weight: 600; }

.chart svg { width: 100%; height: 180px; display: block; }
.chart .axis { stroke: var(--line); }
.chart .area { fill: rgba(63, 185, 80, .14); }
.chart .line { fill: none; stroke: var(--ok); stroke-width: 2; }
.chart .bar-day { fill: #2c3644; }
.chart text { fill: var(--text-faint); font-size: 10px; }

.tagload { display: flex; flex-direction: column; gap: 7px; }
.tagload .row { display: grid; grid-template-columns: 9ch 1fr 5ch; gap: 8px; align-items: center; font-size: 12px; }
.tagload .row .name { color: #9fd0ff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tagload .row .track { background: #0e1218; height: 6px; overflow: hidden; }
.tagload .row .fill { background: var(--accent); height: 100%; }
.tagload .row .num { color: var(--text-faint); text-align: right; }

/* ---------- modal ---------- */

.modal { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; }
/* display:grid outranks the hidden attribute's UA rule, and a full-screen
   backdrop that is merely invisible still swallows every click on the page. */
.modal[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(6, 8, 12, .66); }
.modal-card {
  position: relative; width: min(560px, calc(100vw - 32px)); max-height: calc(100vh - 64px); overflow: auto;
  background: var(--bg-panel); border: 1px solid var(--line);
  padding: 20px; box-shadow: var(--shadow);
}
.modal-card h2 { font-size: 15px; margin-bottom: 14px; }

/* A dialog that takes the window rather than its own contents, so the one
   field worth enlarging can have everything left over. 100px of ground above
   and below keeps it reading as a dialog instead of as a page.

   The chain matters: every box between the card and the textarea has to be a
   flex column that passes the leftover height down, and each needs min-height
   0 — a flex item's default floor is its content, which would let a long
   title push the dialog past the window and put a scrollbar on it, which is
   the thing being removed. */
.modal-card.is-tall {
  height: max(340px, calc(100vh - 200px));
  max-height: calc(100vh - 32px);
  overflow: hidden;
  display: flex; flex-direction: column;
}
.modal-card.is-tall #modal-body,
.task-editor,
.field-grow,
.field-grow .composer {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
}
/* And the body is the one that scrolls when the dialog cannot hold it. The
   card itself is overflow:hidden while it is tall — that is what lets the
   editor's text box take the leftover height — so without this a dialog with
   more in it than fits (the project one, once it has members, links and keys)
   was simply cut off at the bottom edge, with the page behind it scrolling
   instead of the thing that had too much in it. */
.modal-card.is-tall #modal-body { overflow-y: auto; }
/* Only the box itself takes the slack; the label, the filing row and the hint
   under it are fixed rows. */
.field-grow textarea.title-field {
  flex: 1; min-height: 0; height: 100%; max-height: none; resize: none;
}
/* The footnote is one line of 11px text, and .empty's centred padding was
   spending fifty pixels on it — fifty pixels the box above it wants. */
.task-editor > .empty { margin: 0; padding: 8px 0 0; }
.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 12px; }
.field label { font-size: 11px; color: var(--text-dim); }
.field textarea { min-height: 84px; resize: vertical; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.confirm-text { margin: 0 0 4px; color: var(--text-dim); font-size: 13px; }
button.danger-solid { background: #8b2f2f; border-color: #a33c3c; color: #fff; }
.modal-actions { display: flex; gap: 8px; margin-top: 18px; }
.modal-actions .spacer { flex: 1; }

/* ---------- toast ---------- */

.toast {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
  background: #2a1c1c; border: 1px solid #4a2a2a; color: #ffb4b4;
  padding: 9px 16px; box-shadow: var(--shadow); z-index: 60;
}
.toast.ok { background: #16261a; border-color: #22402a; color: #a7e6b0; }

/* ---------- sign-in ---------- */

.login-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 24px;
}

.login-card {
  width: 100%;
  max-width: 420px;
  padding: 32px;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  text-align: center;
}

.login-card .brand-mark { display: block; margin: 0 auto 16px; }
.login-card h1 { font-size: 22px; }

.login-lead {
  margin: 10px 0 24px;
  color: var(--text-dim);
}

.login-error {
  margin: 0 0 20px;
  padding: 10px 12px;
  border: 1px solid var(--bad);
  color: var(--bad);
  text-align: left;
}

.login-button {
  display: block;
  padding: 12px 16px;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
  text-decoration: none;
}

.login-button:hover { filter: brightness(1.08); }

.login-dev { width: 100%; margin-top: 12px; }

.login-foot {
  margin: 24px 0 0;
  color: var(--text-faint);
  font-size: 12px;
}

.login-foot a { color: var(--text-dim); }

/* ---------- account button and popover ---------- */

.brand { position: relative; }

.brand-button {
  display: flex; align-items: center; gap: 12px; min-width: 0;
  background: transparent; border: 1px solid transparent; color: inherit;
  padding: 6px 10px 6px 6px; text-align: left; cursor: pointer;
}
.brand-button:hover { background: #1b212b; filter: none; }
.brand-button[aria-expanded="true"] { background: #1b212b; border-color: var(--line); }
.brand-text { display: flex; flex-direction: column; min-width: 0; }
.brand-name { font-size: 16px; font-weight: 600; letter-spacing: .3px; }

.popover {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 40;
  width: 268px; padding: 12px;
  background: var(--bg-panel); border: 1px solid var(--line);
  box-shadow: var(--shadow);
  display: flex; flex-direction: column; gap: 8px;
}
.popover[hidden] { display: none; }
.popover-head { display: flex; align-items: center; gap: 10px; }
.popover-who { display: flex; flex-direction: column; min-width: 0; }
.popover-who strong { font-size: 13px; }
.popover-who span { font-size: 11px; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; }
.popover-note { margin: 0; font-size: 11px; color: var(--text-faint); }
.popover-item {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; padding: 9px 12px;
}
.popover-item.danger { color: #ff9b9b; border-color: #4a2a2a; }
.popover-item.danger:hover { color: #ffbdbd; border-color: #6a3a3a; }
.popover form { margin: 0; }

/* The account's own switches. Grouped and headed, because "里程碑" on its own
   in a menu says nothing about what it would do. */
.popover-group { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 8px; }
.popover-group-head { display: block; font-size: 11px; color: var(--text-faint); padding: 0 2px 4px; }
.popover-check {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 2px; font-size: 13px; color: var(--text-dim); cursor: pointer;
}
.popover-check:hover { color: var(--text); }
.popover-check input { width: 15px; height: 15px; margin: 0; accent-color: var(--accent); cursor: pointer; }

/* ---------- keyboard: the selection ring, and the help sheet ---------- */

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card:focus, .milestone:focus { outline: none; }

.card.is-selected, .milestone.is-selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 0 0 5px rgba(110, 231, 168, .14);
}
.card.is-selected .card-actions { opacity: 1; }

kbd {
  display: inline-block; min-width: 20px; padding: 1px 6px;
  border: 1px solid var(--line); border-bottom-width: 2px;
  background: #0e1218; color: var(--text-dim);
  font: 500 11px/1.6 ui-monospace, "SFMono-Regular", Consolas, monospace; text-align: center;
}

.help-group { font-size: 11px; color: var(--text-faint); margin: 16px 0 6px; text-transform: none; }
.help-group:first-child { margin-top: 0; }
.help-row { display: flex; align-items: baseline; gap: 10px; padding: 3px 0; }
.help-keys { display: flex; gap: 4px; flex: none; min-width: 108px; flex-wrap: wrap; }
.help-label { font-size: 12px; color: var(--text-dim); }

.field-hint { font-size: 11px; color: var(--text-faint); }

/* ---------- touch and small screens ---------- */

button, .tab, .card, select { touch-action: manipulation; }


/* A phone has no hover, so controls that only appear on hover are controls
   that do not exist. Show them, and make them big enough to hit. Both queries
   are here on purpose: a touch device without a narrow viewport (a tablet)
   needs the first, and a narrow window on a laptop — which is how this gets
   checked — only matches the second. */
@media (hover: none), (max-width: 720px) {
  .card-actions, .milestone-head .ops { opacity: 1; }
  .card-actions { flex-wrap: wrap; gap: 6px; }
  .card-actions button, .milestone-head .ops button { min-height: 36px; padding: 7px 12px; font-size: 12px; }
  .card { cursor: pointer; padding: 11px 12px; }
  .tab { padding: 9px 18px; }
}

@media (max-width: 720px) {
  .topbar {
    flex-wrap: wrap; gap: 10px 12px; padding: 10px 12px;
    padding-top: max(10px, env(safe-area-inset-top));
  }
  .brand { flex: 1 1 100%; }
  .brand-button { width: 100%; }
  .popover { width: min(320px, calc(100vw - 24px)); }
  .overall { margin-left: 0; }
  .ring { width: 36px; height: 36px; }
  .tabs { margin-left: auto; }

  .sidebar > * { padding: 10px; }

  main { padding: 12px 12px 32px; padding-bottom: max(32px, env(safe-area-inset-bottom)); }
  .board { gap: 10px; }

  /* Full-width sheet from the bottom: the editors are forms, and a phone
     keyboard leaves no room for a centred dialog. */
  .modal { place-items: end center; }
  .modal-card {
    width: 100vw; max-height: 88vh;
    padding: 18px 16px calc(18px + env(safe-area-inset-bottom));
  }
  /* A phone has no 100px of ground to spare, and half the screen goes to the
     keyboard the moment the box is focused. */
  .modal-card.is-tall { height: 88vh; }
  .field-row { grid-template-columns: 1fr; gap: 0; }
  .modal-actions { flex-wrap: wrap; }
  .modal-actions button { flex: 1 1 auto; padding: 10px 14px; }
  .modal-actions .spacer { flex-basis: 100%; height: 0; }

  input, select, textarea { font-size: 16px; } /* iOS zooms anything smaller */
  .toast { left: 12px; right: 12px; bottom: 12px; transform: none; text-align: center; }
}

/* Columns are tall on a phone; give the four heads something to stick to so
   "which column am I in" survives a scroll. */
@media (max-width: 640px) {
  .column-head { position: sticky; top: 0; z-index: 5; background: var(--bg-panel); }
}

/* ---------- visible keyboard hints ---------- */

/* A shortcut nobody can see is a shortcut nobody uses. The key rides on the
   control it belongs to, and the bar at the bottom says what the current
   selection can do. Both disappear where there is no keyboard. */

/* On a filled button the hint is part of the label: no plate, no border, the
   button's own ink. A dark chip stamped onto a coloured button is the thing
   this replaces.

   Not the brand: that button is transparent, so a hint with no plate on it is
   just a lower-case letter sitting after the product name, which is how the
   "a" there stopped reading as a key at all. */
button:not(.ghost):not(.brand-button) kbd.hint {
  background: transparent; border: 0; padding: 0; margin-left: 7px;
  color: inherit; opacity: .8; font-size: 12.5px;
}

kbd.hint {
  margin-left: 7px; padding: 0 5px;
  font-size: 10px; line-height: 16px; min-width: 16px;
  border-bottom-width: 1px; color: var(--text-faint); background: #0b0e13;
}
.tab { display: inline-flex; align-items: center; }
.tab.is-active kbd.hint { background: transparent; border: 0; padding: 0; color: inherit; opacity: .75; }
.brand-name kbd.hint { opacity: 0; transition: opacity .12s; }
.brand-button:hover .brand-name kbd.hint, .brand-button:focus-visible .brand-name kbd.hint { opacity: 1; }

.with-key { position: relative; display: flex; flex: 1; min-width: 0; }
/* The control fills the row. A textarea has an intrinsic width (cols) and will
   sit at about 200px if nothing stretches it, which is what the editor's title
   box did — a third of the dialog, with the rest of the row empty. */
.with-key input, .with-key textarea { flex: 1; min-width: 0; padding-right: 30px; }
.with-key kbd.hint {
  position: absolute; right: 7px; top: 50%; transform: translateY(-50%);
  margin: 0; pointer-events: none;
}
.with-key:focus-within kbd.hint { opacity: 0; }

.hintbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 7px 16px;
  background: rgba(16, 20, 26, .97); border-top: 1px solid var(--line);
  font-size: 11.5px; color: var(--text-faint);
}
.hintbar .hint-item { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.hintbar .hint-item kbd { margin: 0; }
.hintbar .hint-item .keys { display: inline-flex; gap: 3px; }
.hintbar .hint-more { margin-left: auto; }

/* The bar is fixed, so the last card in a column would sit under it. */
body { padding-bottom: 36px; }

@media (hover: none), (max-width: 900px) {
  kbd.hint, .hintbar { display: none; }
  body { padding-bottom: 0; }
}

button.hint-more {
  background: transparent; border: 0; color: inherit; font: inherit;
  padding: 2px 4px; cursor: pointer;
}
button.hint-more:hover { color: var(--text-dim); background: #1b212b; filter: none; }

/* A leading key chip sits in front of the label it triggers. */
kbd.hint.lead { margin-left: 0; margin-right: 6px; }

/* The arrow only stands in for the chip where the chip is hidden. */
.card-actions .to-arrow { display: none; }

/* Optimistic rows: drawn before the server has confirmed them. */
.card.is-pending { opacity: .62; }

@media (hover: none), (max-width: 900px) {
  .card-actions .to-arrow { display: inline; }
}

/* ---------- a title is the whole description ---------- */

/* Titles run to several lines now, so the line breaks that were typed have to
   survive being drawn. */
.card-title { white-space: pre-wrap; }

.quick-add textarea {
  flex: 1; min-width: 0;
  min-height: 38px; max-height: 168px;
  resize: none; line-height: 1.5; overflow-y: auto;
}
/* The key badge pins to the top of a box that grows, not to its middle. And
   the wrapper must not stretch the box: a stretched textarea reports its
   stretched height as scrollHeight, so each autosize pass would ratchet it up
   until it hit the ceiling. */
.with-key.multiline { align-items: flex-start; }
.with-key.multiline kbd.hint { top: 11px; transform: none; }

.modal-card textarea.title-field { min-height: 72px; }

/* ---------- the composer: chips outside the box, pickers under it ---------- */

.composer { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.composer .with-key { position: relative; }

.composer-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.composer-chips[hidden] { display: none; }

.composer-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 4px 2px 9px;
  border: 1px solid var(--line); background: #0f131a;
  font-size: 11.5px; color: var(--text-dim);
}
.composer-chip.tag { color: #9fd0ff; border-color: #26374d; }
.composer-chip .chip-remove {
  background: transparent; border: 0; color: inherit; opacity: .65;
  padding: 0 4px; font-size: 13px; line-height: 1; cursor: pointer;
}
.composer-chip .chip-remove:hover { opacity: 1; background: rgba(255, 255, 255, .1); filter: none; }

/* ---------- the filing row: one select for the milestone, one for tags ---- */

.filing { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.filing-field { display: flex; align-items: center; gap: 5px; flex: 1 1 130px; min-width: 0; }
.filing-label { font-size: 11px; color: var(--text-faint); white-space: nowrap; }
.filing-field select { flex: 1; min-width: 0; padding: 5px 6px; font-size: 12px; }
.filing-field kbd.hint { flex: none; }
/* The "new one" box takes the whole row rather than squeezing in beside the
   selects: it is only ever up for the few seconds a name is being typed, and
   a name that does not fit is a name that gets abandoned. */
.filing-new { flex: 1 1 100%; padding: 5px 8px; font-size: 12px; }
.filing-new[hidden] { display: none; }

/* ---------- the tag manager ---------- */

/* The name is an input rather than a label with an edit button beside it:
   renaming is the whole reason this list exists, so it should not need a
   click to begin. */
.tag-manager { display: flex; flex-direction: column; gap: 10px; }
.tag-list { display: flex; flex-direction: column; max-height: 46vh; overflow-y: auto; }
.tag-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
.tag-row .tag-name { flex: 1; min-width: 0; padding: 5px 8px; font-size: 13px; }
.tag-row .tag-count { font-size: 11px; color: var(--text-faint); white-space: nowrap; }
.tag-row button { padding: 3px 9px; font-size: 11px; }
.tag-new { display: flex; gap: 8px; border-top: 1px solid var(--line); padding-top: 10px; }
.tag-new input { flex: 1; min-width: 0; }

/* In the editor the composer is a field in a form, so it sits closer. */
.modal-card .composer { gap: 8px; }

/* A select cannot hold a key badge, so it sits alongside — tight enough to
   read as part of the same control. */
.key-after { display: inline-flex; align-items: center; gap: 2px; min-width: 0; }
.key-after select { min-width: 0; }

/* ---------- pictures ---------- */

.composer-chip.image { padding: 0; overflow: hidden; position: relative; }
.composer-chip.image img { display: block; width: 64px; height: 44px; object-fit: cover; }
.composer-chip.image .chip-remove {
  position: absolute; top: 2px; right: 2px;
  background: rgba(8, 11, 16, .72); color: #fff; opacity: 1;
}
/* A picture still uploading: the real thumbnail, dimmed, with a ring turning
   over it. Same box as the finished chip, so nothing shifts when it lands. */
.composer-chip.image.is-pending img { opacity: .4; }
.composer-chip.image.is-pending {
  position: relative;
  border-color: var(--line);
}
.chip-spinner {
  position: absolute; top: 50%; left: 50%; width: 16px; height: 16px;
  /* The one radius left in the file. This is a circle, not a corner — a
     square that spins reads as a rendering fault, not as a house style. */
  margin: -8px 0 0 -8px; border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .25);
  border-top-color: var(--accent);
  animation: chip-spin .7s linear infinite;
}
@keyframes chip-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .chip-spinner { animation-duration: 2.4s; } }

.card-images { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
.card-images img {
  width: 72px; height: 52px; object-fit: cover;
  border: 1px solid var(--line); background: #0e1218;
}
/* The picture cursor (keys.js): a second selection ring, inside the card's
   own, saying which of the strip ← → and c are about to act on. It is drawn
   the same way as the card's ring so the two read as one nesting. */
.card-images img.is-current {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 0 0 4px rgba(110, 231, 168, .18);
}

/* The allowance, in the account menu. */
.popover-item.usage { flex-direction: column; align-items: stretch; gap: 6px; text-decoration: none; }
.usage-text { font-size: 12px; color: var(--text-dim); }
.usage-bar { display: block; height: 4px; background: #0e1218; overflow: hidden; }
.usage-fill { display: block; height: 100%; width: 0; background: var(--accent); transition: width .2s; }
.usage-fill.is-full { background: var(--bad); }

/* Clicking a thumbnail opens it as large as the window allows. */
.card-images img, .composer-chip.image img { cursor: zoom-in; }
.image-viewer img {
  display: block; max-width: 100%; max-height: calc(100vh - 220px);
  margin: 0 auto; background: #0e1218;
}
.ghost-link {
  color: var(--text-dim); font-size: 12px; text-decoration: none;
  padding: 7px 0; align-self: center;
}
.ghost-link:hover { color: var(--text); }
/* The viewer takes focus itself so ← → and c reach it, which means it draws a
   ring — and a ring round the whole dialog says nothing useful. */
.image-viewer:focus { outline: none; }
.viewer-count { color: var(--text-faint); font-size: 12px; align-self: center; }
.viewer-count[hidden] { display: none; }

.column-head .purge { margin-left: auto; padding: 2px 8px; font-size: 11px; }
.column-head .purge + .count { margin-left: 8px; }

/* ---------- projects ---------- */

/* A board is now several projects merged, and three things follow from that:
   a card has to say which one it is from, the filter has to take more than one
   answer, and the project page has to be somewhere the people on a project can
   be seen and changed. */

.chip.project { color: #ffd39f; border-color: #4d3a26; }
.chip.role { color: var(--text-dim); }

/* The project switch is the first control in the filter panel and is marked as
   the one that is not a filter: it decides where you are working, the three
   below narrow what you see there. A left edge in the accent colour is enough
   to say "different kind of thing" without a second heading. */
#filter-project { border-left: 2px solid var(--accent); }

/* The project page's left column holds two panels, one above the other: the
   projects, then the milestones inside them. */
.plan-column { display: flex; flex-direction: column; gap: 16px; min-width: 0; }

/* What the project IS on the left, what you can do with it on the right —
   the same shape as a member row, an invitation and a key inside the dialog
   this button opens. */
.project-row {
  display: flex; align-items: center; gap: 12px;
  border: 1px solid var(--line); background: #141922; padding: 12px; margin-bottom: 10px;
}
.project-row.is-mine { border-color: #2c3a44; }
.project-row:hover { background: #171d27; border-color: #3a4453; }
.project-row-main { flex: 1 1 auto; min-width: 0; }
.project-row-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.project-row-head h3 { font-size: 14px; }
.project-row .project-counts { color: var(--text-dim); font-size: 12px; margin: 5px 0 0; }
/* Full-strength text and a border you can see: this is the only door to
   renaming a project, inviting anybody, or making a key, and as a dim ghost
   button under the counts it read as a footnote about the row. */
.project-edit {
  flex: none; padding: 6px 16px; font-size: 12px;
  background: transparent; border: 1px solid #46525f; color: var(--text);
}
.project-edit:hover, .project-row:hover .project-edit {
  filter: none; border-color: var(--accent); color: var(--accent);
  background: rgba(110, 231, 168, .08);
}

/* One dialog for the whole of "who is on this and should they be": answering
   that across four screens is how the wrong person gets removed.

   Every section in it has the same shape — heading, count, action, one line of
   help, then the list — and the rules below are what enforce it. Sections are
   separated by a line rather than by more space, because the dialog scrolls
   and space alone stops reading as a boundary. */
.editor-section { padding: 16px 0; border-top: 1px solid var(--line); }
.editor-section:first-child { padding-top: 0; border-top: 0; }
.editor-section .field { margin-bottom: 0; }
.editor-section > input { width: 100%; }
.project-editor .section-head { font-size: 12px; color: var(--text-dim); margin: 0; }
.project-editor .section-head-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.project-editor .section-head-row > button { margin-left: auto; }
.section-count { font-size: 11px; color: var(--text-faint); }
/* Under the heading it explains, left-aligned and quiet. These used to be
   centred paragraphs at the BOTTOM of their section, which put the
   explanation of a control after everything the control had produced. */
.section-hint { margin: -2px 0 10px; font-size: 11px; line-height: 1.6; color: var(--text-faint); }
/* An empty list here is a state, not a page: the centred, generously padded
   version made a dialog with nothing in it yet look broken. */
.editor-section .empty { text-align: left; padding: 2px 0 0; }
/* Making a key takes a name, so the box and the button are one row of their
   own above the list rather than something wedged into the heading. */
.key-new { display: flex; gap: 6px; margin-bottom: 10px; }
.key-new .key-name { flex: 1 1 auto; min-width: 0; }

.member-list, .invite-list { display: flex; flex-direction: column; gap: 6px; }
.member {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  border: 1px solid var(--line); background: #141922; padding: 8px 10px;
}
.member-who { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.member-who strong { font-size: 13px; font-weight: 600; }
.member-email { font-size: 11px; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; }
.member button { padding: 2px 8px; font-size: 11px; }

.invite {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  border: 1px solid var(--line); background: #141922; padding: 8px 10px; font-size: 12px;
}
.invite.is-spent { opacity: .6; }
.invite-state { color: var(--text-dim); }
.invite-made { color: var(--text-faint); font-size: 11px; margin-left: auto; }
.invite button { padding: 2px 8px; font-size: 11px; }

/* The link exists in a readable form exactly once — only its hash is kept — so
   it gets a box and a button rather than a toast that scrolls away. */
.invite-fresh { border: 1px solid var(--accent); background: rgba(110, 231, 168, .07); padding: 10px; margin-bottom: 8px; }
.invite-fresh-note { margin: 0 0 8px; font-size: 12px; color: var(--text-dim); }
.invite-fresh-row { display: flex; gap: 6px; }
.invite-url { flex: 1 1 auto; min-width: 0; font-size: 12px; }

.popover-hint { display: block; font-size: 11px; color: var(--text-faint); padding: 2px 2px 0; }

.purge-group { display: flex; gap: 4px; margin-left: 8px; flex-wrap: wrap; }
.tag-project { width: 100%; margin-bottom: 10px; }

/* ---------- a card shows ten lines of its prompt ---------- */

/* A task here IS its prompt, and some of them run to forty lines. A column of
   those is one card per screen and the board stops being something you can
   look at, so a long one is folded at ten lines (CLAMP_LINES in board.js) with
   a button under it. Which cards are unfolded is remembered across redraws,
   because otherwise opening one and then moving a card would close it again.

   -webkit-line-clamp is the only way to fold at a LINE COUNT rather than a
   height, and every engine this ships to implements it. The fallback if one
   did not is an unfolded card, which is what this replaces — not a broken one. */
.card-title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 10;
  line-clamp: 10;
  overflow: hidden;
}
.card-title.is-open { display: block; overflow: visible; }
.card-unfold {
  margin-top: 4px; padding: 1px 7px; font-size: 11px;
  color: var(--text-dim); border-color: var(--line);
}

/* ---------- the four columns as tabs, where four do not fit ---------- */

/* Stacked, the four columns are four screenfuls of scrolling to reach 阻塞,
   and 已完成 is the one that grows without limit — so on a phone they become
   tabs and one is shown at a time. Same cards, same buttons, same order.
   The bar does not exist at all on a wide screen: there, all four are up. */
.board-tabs { display: none; }

@media (max-width: 640px) {
  /* The bar is FIXED to the bottom of the window, not sticky at the top of the
     board. A phone is held at the bottom, the columns are what somebody is
     switching between while reading, and a sticky bar still moves — it rides
     up as the page scrolls past whatever is above it. This one is where the
     thumb already is, always, and 新建 sits at its left because writing a task
     down is the reason the app is open on a phone at all. */
  .board-tabs {
    display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 4px; background: #0d1116; padding: 5px; border-top: 1px solid var(--line);
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
    /* Clear of the home indicator on a phone that has one. */
    padding-bottom: max(5px, env(safe-area-inset-bottom));
  }
  /* Room for it under the page, so the last card is reachable rather than
     sitting behind the bar. */
  main { padding-bottom: 76px; }
  /* Thumb-sized. 44px is the size a finger actually is; the bar was 10px of
     padding around 12px of text, which is a target you aim at rather than one
     you hit. */
  .board-tab {
    display: flex; align-items: center; justify-content: center; gap: 5px;
    padding: 12px 4px; min-height: 48px; font-size: 13px; min-width: 0;
  }
  /* The one entry that is not a column. Marked by its colour rather than by
     sitting somewhere else, so the row stays five equal thumb-sized targets. */
  .board-tab.is-new { color: var(--accent); font-weight: 600; }

  /* One line of header on a phone: the mark, how far along the board is, and
     where to go. The name of the app is on the tab, the address belongs to
     whoever is holding the phone, and the sentence under the percentage is
     four facts nobody reads while walking — the ring and the number are the
     whole of what that block is for. */
  /* One row, and it has to be said out loud: the wider phone layout above
     wraps the header so the account line has room, and here there is no
     account line to give room to. */
  .topbar { flex-wrap: nowrap; gap: 10px; padding: 8px 12px; }
  .brand { flex: 0 0 auto; }
  .brand-button { width: auto; }
  .brand-text { display: none; }
  .overall { margin-left: 0; }
  .tabs { margin-left: auto; flex: none; }
  .overall { gap: 8px; }
  .ring { width: 32px; height: 32px; }
  #overall-detail { display: none; }
  .overall-text strong { font-size: 15px; }
  .tabs { padding: 3px; }
  .tab { padding: 5px 10px; font-size: 13px; }
  .board-tab > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* The count is the reason the bar is drawn on every render: it is the same
     number the column head carries, and it moves with every card. */
  .board-tab .count {
    flex: none; min-width: 18px; padding: 0 5px;
    background: #0e1218; color: var(--text-faint); font-size: 11px;
  }
  .board-tab.is-active .count { background: rgba(6, 35, 26, .25); color: inherit; }
  /* The dot on the head is redundant once the bar says which column this is,
     but the head still carries the name — a screen that scrolls has to answer
     "which column am I in" without scrolling back up. */
  .board .column { display: none; }
  .board .column.is-current { display: block; }
  .column-stack { display: contents; }

  /* The bar switches PAGES, not highlights. Writing a task down is a screen
     with the box on it and no list underneath; reading a column is a screen of
     cards with no composer and no filters above them. Everything used to be on
     one long scroll, which meant the box was off the top of the screen by the
     time anybody wanted it and the cards were below three panels nobody was
     reading. */
  .board-layout[data-page='column'] .sidebar { display: none; }
  .board-layout[data-page='compose'] .board { display: none; }
  .board-layout[data-page='compose'] .board-main { display: contents; }
  /* On the composing page the box is the point, so it gets the room a phone
     has: enough to see a paragraph of what is being written. */
  .board-layout[data-page='compose'] #quick-title { min-height: 34vh; }
}

/* The project page's own switcher, in the milestone panel's head: which
   project's milestones to work through. It is not the board's filter — that
   one answers "what am I doing today", this one answers "what is in this
   project", which is the question somebody moving a milestone is asking. */
.panel-picker { margin-left: auto; max-width: 40%; font-size: 12px; padding: 4px 8px; }
.panel-head .panel-picker + button { margin-left: 8px; }

/* ---------- to an agent ---------- */

/* The sidebar's two doors: the whole of the open work out as one document, and
   the answer back in. They read as a pair — same width, key cap right-aligned
   — because they are two ends of one round trip and somebody who has just
   pressed the first is looking for the second. */
.handoff { display: flex; flex-direction: column; gap: 8px; }
.handoff button { display: flex; align-items: center; width: 100%; text-align: left; }
.handoff button kbd.hint { margin-left: auto; }

/* The paste step. A monospace box, because what goes in it is indented YAML
   and a proportional font hides the one thing that can be wrong with it. */
.revise { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.revise-input {
  flex: 1; min-height: 180px; resize: none;
  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
  font-size: 12px; line-height: 1.55; tab-size: 2; white-space: pre;
}
.revise > .field-hint { margin: 0 0 10px; }

/* The confirmation step. The count is above the list rather than below it:
   it is the sentence somebody checks before reading any of the detail. */
/* The counts and the master tick, on one row: what pressing the button would
   do, and the one control that changes all of it at once. */
.revise-summary {
  display: flex; align-items: center; gap: 10px;
  font-size: 12px; color: var(--text-dim); margin-bottom: 10px;
}
.revise-all {
  display: inline-flex; align-items: center; gap: 5px;
  margin-left: auto; cursor: pointer; user-select: none;
}
.revise-all input { margin: 0; }
.revise-unknown { margin: 10px 0 0; font-size: 11px; color: var(--warn); overflow-wrap: anywhere; }

.diff-list {
  flex: 1; min-height: 0; overflow: auto;
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 8px;
}
/* One card per task, and the stripe down its left is the whole legend: green
   is new work, blue is a change to work that exists, red is work about to
   stop existing. The word is there as well, because a colour alone is not a
   label for anybody reading this on a bad screen or without seeing colour. */
.diff {
  display: flex; gap: 10px; padding: 10px;
  background: var(--bg-card); border: 1px solid var(--line); border-left-width: 3px;
}
.diff-create { border-left-color: var(--ok); }
.diff-update { border-left-color: var(--status-active); }
.diff-delete { border-left-color: var(--bad); }
/* The box and the word for what this is are one label, so the hit target is
   the whole corner of the row rather than a small square, and so the kind is
   announced when the box takes focus. */
.diff-pick {
  flex: none; align-self: flex-start;
  display: flex; align-items: center; gap: 6px;
  cursor: pointer; user-select: none;
}
.diff-pick input { margin: 0; flex: none; }
.diff-kind {
  font-size: 10px; line-height: 18px; letter-spacing: .04em; color: var(--text-dim);
}
/* An unticked row stays where it is and stops shouting. Removing it would make
   the list jump under the pointer and leave no way to put it back. */
.diff.is-off { opacity: .42; }
.diff.is-off .diff-kind { color: var(--text-faint); }
.diff-create .diff-kind { color: var(--ok); }
.diff-update .diff-kind { color: var(--status-active); }
.diff-delete .diff-kind { color: var(--bad); }

.diff-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
/* A prompt runs to paragraphs and the newlines in it are meaningful, so they
   are kept — and the height is capped, because a diff of twelve tasks must
   stay something you can scan rather than twelve essays. */
.diff-title {
  font-size: 13px; white-space: pre-wrap; overflow-wrap: anywhere;
  max-height: 6.2em; overflow: auto;
}
/* What it says now, when it is about to say something else — and what it says
   now, when it is about to say nothing at all. */
.diff-title.is-old { color: var(--text-faint); text-decoration: line-through; }
.diff-title.is-gone { color: var(--text-dim); text-decoration: line-through; }
.diff-title.is-new { color: var(--text); }

.diff-meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.diff-status { color: var(--status); border-color: var(--status); }
.diff-arrow { font-size: 11px; color: var(--text-faint); }

/* The urls, not thumbnails of them: what is being checked here is whether the
   right links survived, and a picture would look right while pointing at
   something else. */
.diff-image { display: flex; gap: 6px; font-size: 11px; align-items: baseline; }
.diff-sign { flex: none; font-family: ui-monospace, Consolas, monospace; }
.diff-image.is-added .diff-sign { color: var(--ok); }
.diff-image.is-removed .diff-sign { color: var(--bad); }
.diff-url { color: var(--text-dim); overflow-wrap: anywhere; }
.diff-image.is-removed .diff-url { text-decoration: line-through; }

/* ---------- project keys ---------- */

/* One row per key, the same shape as a member and an invitation above it —
   they are three answers to one question ("what can reach this project"), so
   they read as one list with three sections rather than three designs. */
.key-list { display: flex; flex-direction: column; gap: 6px; }
.api-key {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  border: 1px solid var(--line); background: #141922; padding: 8px 10px;
}
/* A key that is switched off is still there and still counts against the cap,
   so it is faded rather than hidden — and the stripe says which state it is in
   without anybody having to read the buttons. */
.api-key.is-live { border-left: 3px solid var(--ok); }
.api-key.is-off { border-left: 3px solid var(--text-faint); opacity: .62; }
.api-key button { padding: 2px 8px; font-size: 11px; }
.key-who { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.key-who strong { font-size: 13px; font-weight: 600; }
/* Masked, and in the same monospace the .env file it is going into uses. What
   this is for is telling two keys apart; the whole of it travels by the copy
   button, not by being read off the screen. */
.key-token {
  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
  font-size: 11px; color: var(--text-dim);
}
.key-when { font-size: 11px; color: var(--text-faint); }
.key-name { font-size: 12px; padding: 6px 10px; }

/* ---------- the API reference ---------- */

/* Served at /api/doc and read by whoever holds a key — often before they have
   ever seen the app. It borrows the app's palette and nothing else: one column,
   generous measure, and code blocks that are the point of the page. */
.doc-page { background: var(--bg); }
.doc { max-width: 74ch; margin: 0 auto; padding: 40px 20px 80px; }
.doc h1 { font-size: 22px; margin-bottom: 8px; }
.doc h2 { font-size: 15px; margin: 32px 0 10px; color: var(--accent); }
.doc p { color: var(--text-dim); }
.doc .lead { font-size: 14px; color: var(--text); }
.doc .note { font-size: 12px; border-left: 2px solid var(--line); padding-left: 12px; }
.doc code { font-size: 12px; color: var(--text); }
.doc pre {
  background: var(--bg-panel); border: 1px solid var(--line);
  padding: 12px 14px; overflow-x: auto; font-size: 12px; line-height: 1.5;
}
.doc pre code { white-space: pre; }
.doc kbd { font-size: 11px; }
.doc a { color: var(--accent); }
.doc-foot { margin-top: 40px; font-size: 12px; }

/* The sidebar's third button is not about agents; the line says so without a
   second heading for one control. */
.handoff .handoff-aside { margin-top: 4px; border-top-color: var(--line); }
.handoff .handoff-aside::before {
  content: ""; position: absolute; left: 0; right: 0; top: -6px;
  border-top: 1px solid var(--line);
}
.handoff .handoff-aside { position: relative; }

/* ---------- published boards ---------- */

.public-panel { max-width: 900px; margin: 0 auto; }
.public-panel .panel-head { align-items: baseline; gap: 8px; }
.public-panel .panel-head .section-count { margin-left: 0; }
.public-list { display: flex; flex-direction: column; gap: 8px; }

/* A row is a button, because the whole row opens the board — a small "open"
   link on the right would be a smaller target for the only thing there is to
   do here. */
.public-row {
  display: flex; align-items: center; gap: 16px; width: 100%; text-align: left;
  background: var(--bg-card); border: 1px solid var(--line); color: inherit;
  padding: 12px 14px;
}
.public-row:hover { filter: none; background: var(--bg-card-hover); border-color: #3a4453; }
.public-row-main { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; gap: 3px; }
.public-name { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.public-meta { font-size: 11px; color: var(--text-faint); }
.public-row-side { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex: none; }
.public-progress { font-size: 11px; color: var(--text-dim); white-space: nowrap; }
/* The same bar in the directory and on the board it opens, so a percentage
   means the same thing in both places. */
.public-bar { display: block; width: 120px; height: 4px; background: #232a35; }
.public-bar-fill { display: block; height: 100%; background: var(--ok); }
.public-more { width: 100%; margin-top: 10px; }

.public-open { max-width: 1400px; margin: 0 auto; }
.public-open-head {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin-bottom: 14px; padding-bottom: 12px; border-bottom: 1px solid var(--line);
}
.public-open-who { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.public-open-who h2 { font-size: 17px; }
.public-open-progress { margin-left: auto; font-size: 20px; font-weight: 600; }
.public-milestones {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 8px; margin-bottom: 16px;
}
.public-milestone {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  background: var(--bg-panel); border: 1px solid var(--line); padding: 10px 12px; font-size: 12px;
}
.public-milestone strong { font-size: 13px; }
.public-milestone .public-bar { width: 100%; }
/* A read-only board keeps the columns and loses everything that would suggest
   a card can be moved. */
.public-board .card { cursor: default; }
.public-board .card:hover { background: var(--bg-card); }

/* On a narrow screen the board's four columns become tabs, and that rule is
   written against `.board .column` — which a published board is also made of.
   Without this it would be four hidden columns and an empty page, because a
   read-only board has no tab bar to make one of them current. Stacked is the
   right answer here anyway: this is a page somebody scrolls, not a board they
   work in. */
@media (max-width: 640px) {
  .public-board { display: block; }
  .public-board .column { display: block; margin-bottom: 12px; }
}

/* ---------- a project's mark ---------- */

/* One element at whatever size the place it is in has room for. The drawn
   fallback is a letter on a colour derived from the project's id, so every
   project has a mark and two projects in a list rarely share one. */
.project-logo {
  flex: none; border-radius: 6px; object-fit: cover; background: #0f131a;
  border: 1px solid var(--line);
}
.project-logo.is-drawn {
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 600; letter-spacing: .01em; text-transform: uppercase;
  color: hsl(from hsl(var(--mark) 70% 70%) h s l);
  background: hsl(from hsl(var(--mark) 60% 22%) h s l);
  border-color: hsl(from hsl(var(--mark) 40% 32%) h s l);
}
/* Browsers without relative colour syntax get the same shape in the theme's
   own greys rather than nothing. */
@supports not (color: hsl(from hsl(0deg 0% 0%) h s l)) {
  .project-logo.is-drawn { color: var(--text-dim); background: #1c212a; }
}

/* The picker: the mark as it stands, what to do to change it, and the state of
   the last attempt. The whole box is the drop target and the click target. */
.logo-zone {
  display: flex; align-items: center; gap: 14px;
  border: 1px dashed #3a4453; background: #10141a; padding: 12px 14px; cursor: pointer;
}
.logo-zone:hover, .logo-zone:focus-visible { border-color: var(--accent); outline: none; }
.logo-zone.is-over { border-color: var(--accent); background: rgba(110, 231, 168, .07); }
.logo-preview { display: inline-flex; flex: none; }
.logo-words { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
.logo-lead { font-size: 12px; color: var(--text-dim); }
.logo-state { font-size: 11px; color: var(--text-faint); }
.logo-zone button { flex: none; }

/* ---------- a shared link, read without an account ---------- */

.public-share { margin-left: 8px; }
.visitor-signin {
  margin-left: auto; padding: 7px 16px; font-size: 13px;
  color: var(--accent-ink); background: var(--accent); text-decoration: none;
}
.visitor-signin:hover { filter: brightness(1.1); }

/* Somebody reading a link has no board of their own on this page: the tabs,
   the progress ring of a board they cannot see, the account menu and the
   keyboard hints all describe things that are not there. */
.is-visitor .tabs,
.is-visitor .overall,
.is-visitor .saving,
.is-visitor .hintbar { display: none; }
.is-visitor .brand-button { pointer-events: none; }
.is-visitor .brand-name kbd.hint { display: none; }
.is-visitor main { padding-top: 24px; }

/* ---------- dialogs on a phone ---------- */

.modal-close { display: none; }

@media (max-width: 640px) {
  /* The whole screen, because a 560px card on a 375px phone was a card with
     its own scrollbar inside a page with another one — two scrollers, one
     under the other, and the inner one is the one nobody could reach.
     Full screen gives the dialog one scroller and the content the width. */
  .modal { place-items: stretch; }
  .modal-card,
  .modal-card.is-tall {
    width: 100%; max-width: 100%;
    height: 100dvh; max-height: 100dvh;
    border: 0; padding: 0; overflow: hidden;
    display: flex; flex-direction: column;
  }
  /* The title stays put while the body scrolls under it, with room on the left
     for the close button. */
  .modal-card h2 {
    flex: none; margin: 0; padding: 14px 16px 12px 52px;
    border-bottom: 1px solid var(--line); background: var(--bg-panel);
  }
  .modal-close {
    display: flex; align-items: center; justify-content: center;
    position: absolute; top: 6px; left: 6px; z-index: 2;
    width: 40px; height: 40px; padding: 0; font-size: 22px; line-height: 1;
    background: transparent; border: 0; color: var(--text-dim);
  }
  .modal-close:hover { color: var(--text); filter: none; }
  .modal-card.is-tall #modal-body,
  #modal-body {
    flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
    padding: 14px 16px calc(16px + env(safe-area-inset-bottom));
  }
  /* The pair of buttons somebody came to press stays on screen while the form
     above it scrolls: on a long dialog they were a scroll away from every
     field, which is the one thing a phone makes worse. */
  .modal-actions {
    position: sticky; bottom: 0; z-index: 1;
    margin-top: 14px; padding: 10px 0 calc(4px + env(safe-area-inset-bottom));
    background: var(--bg-panel); border-top: 1px solid var(--line);
  }
  .modal-actions button { flex: 1 1 auto; }
  .modal-actions .spacer { display: none; }
  /* Two fields side by side is one field each on a phone. */
  .field-row { grid-template-columns: 1fr; }
}

/* ---------- what a page shows while it is still arriving ---------- */

/* Not a spinner: the shape of the thing being fetched, in its place, so the
   layout does not jump when the real one lands. The shimmer is slow on
   purpose — fast is a progress bar, and this is not progress. */
.skeleton-line, .skeleton-chip, .skeleton-mark {
  display: block; border-radius: 3px;
  background: linear-gradient(90deg, #1b212b 0%, #232b37 40%, #1b212b 80%);
  background-size: 300% 100%;
  animation: skeleton-sweep 1.6s ease-in-out infinite;
}
@keyframes skeleton-sweep {
  from { background-position: 120% 0; }
  to { background-position: -40% 0; }
}
/* Somebody who has asked not to be moved gets the shapes without the sweep;
   they still say "this is what is coming". */
@media (prefers-reduced-motion: reduce) {
  .skeleton-line, .skeleton-chip, .skeleton-mark { animation: none; }
}

.skeleton-card { display: flex; flex-direction: column; gap: 7px; cursor: default; }
.skeleton-card:hover { background: var(--bg-card); }
.skeleton-chips { display: flex; gap: 6px; margin-top: 3px; }
.skeleton-chip { width: 44px; height: 16px; }
.skeleton-mark { width: 40px; height: 40px; border-radius: 6px; flex: none; }
.column.skeleton .column-head { opacity: .7; }
.public-row.skeleton { cursor: default; gap: 16px; align-items: center; }
.public-row.skeleton:hover { background: var(--bg-card); border-color: var(--line); }
.public-row.skeleton .public-row-main { gap: 6px; }
