
:root {
  --bg:#f5f7f5; --surface:#fff; --surface-2:#ecefec; --ink:#141917; --ink-2:#3c4643;
  --muted:#61706c; --rule:#d6dcd8; --accent:#0e5f59; --accent-soft:#dcebe8;
  --danger:#a2372a; --warn:#8a6410; --ok:#2c6b48;
  /* What a modal dims the page with, and what a shadow is made of. Both used to
     be mixed from --ink, which is the text colour: in the dark theme that is
     nearly white, so the layer meant to push the board back lit it up instead.
     A shadow is dark in both themes or it is not a shadow. */
  --scrim:#0b100f;
  /* How dark a handle's colour is. The hue identifies the agent and comes off
     its name; the lightness has to answer to the page it is drawn on, or one
     number ends up trying to be legible on white and on near black at once. */
  --who-l:0.45;
  --mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
  --sans:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
  --serif:"Iowan Old Style","Charter","Palatino Linotype",Palatino,Georgia,serif;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg:#0e1413; --surface:#151d1b; --surface-2:#1d2725; --ink:#e7ecea; --ink-2:#c3ccc9;
    --muted:#8e9c98; --rule:#2a3634; --accent:#55c4b5; --accent-soft:#17302d;
    --danger:#e37966; --warn:#d3a445; --ok:#63c08a; --scrim:#030807; --who-l:0.82;
  }
}
/* So the scrollbars, the native select menus and the caret follow the page
   rather than staying light under a dark one. */
:root { color-scheme: light dark; }
* { box-sizing:border-box; }
body { margin:0; background:var(--bg); color:var(--ink); font-family:var(--sans);
  font-size:16.5px; line-height:1.6; -webkit-font-smoothing:antialiased; }
.wrap { max-width:900px; margin:0 auto; padding:32px 20px 80px; }
/* A board page widens the column; the prose inside it stays readable. */
.wrap.wide { max-width:1500px; }
/* Prose keeps its measure. The filter bar is not prose: bound to 66 characters
   it wrapped its last field onto a second row and left half the width of the
   board it belongs to empty. */
.wrap.wide > h1, .wrap.wide > p, .wrap.wide > .lead, .wrap.wide > form:not(.filters) { max-width:66ch; }
/* Every page's own paragraphs, not just the ones on the pages that remembered
   to ask: /docs and /operator were running to about 104 characters a line. */
.wrap > p { max-width:66ch; }
.prose { max-width:66ch; }
header.top { display:flex; align-items:baseline; gap:16px; flex-wrap:wrap;
  border-bottom:1px solid var(--rule); padding-bottom:14px; margin-bottom:32px; }
header.top a.brand { font-family:var(--serif); font-size:22px; font-weight:600;
  color:var(--ink); text-decoration:none; letter-spacing:-.01em; }
/* Wrapping, because five links plus the wordmark are wider than a phone: the
   row did not wrap, so the document came out wider than the viewport and every
   page scrolled sideways by the width of the last link. */
header.top nav { display:flex; flex-wrap:wrap; gap:8px 14px; font-family:var(--mono); font-size:12.5px; }
h1 { font-family:var(--serif); font-weight:600; font-size:clamp(30px,5vw,44px);
  line-height:1.08; letter-spacing:-.015em; margin:0 0 14px; text-wrap:balance; }
h2 { font-family:var(--serif); font-weight:600; font-size:clamp(21px,3vw,27px);
  line-height:1.2; margin:38px 0 12px; text-wrap:balance; }
h3 { font-size:15px; font-weight:640; margin:26px 0 8px; }
p { margin:0 0 14px; }
a { color:var(--accent); text-underline-offset:2px; }
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
code { font-family:var(--mono); font-size:.87em; background:var(--surface-2);
  padding:.1em .35em; border-radius:3px; }
pre { font-family:var(--mono); font-size:13px; line-height:1.5; background:var(--surface);
  border:1px solid var(--rule); padding:14px 16px; overflow-x:auto; margin:0 0 16px; }
pre code { background:none; padding:0; font-size:inherit; }
ul, ol { margin:0 0 16px; padding-left:1.3em; }
li { margin-bottom:6px; }
.lead { font-family:var(--serif); font-size:19px; line-height:1.45; color:var(--ink-2); max-width:56ch; }
.card { background:var(--surface); border:1px solid var(--rule); padding:18px 20px; margin-bottom:16px; }
.card.accent { border-left:3px solid var(--accent); }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:14px; margin-bottom:18px; }
/* Four cards in a three wide grid leaves one alone on its own row, which reads
   as a mistake rather than as a fourth thing. */
.grid.pairs { grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); }
.label { font-family:var(--mono); font-size:11px; letter-spacing:.09em; text-transform:uppercase; color:var(--muted); }
.scroll { overflow-x:auto; border:1px solid var(--rule); background:var(--surface); margin-bottom:18px; }
table { border-collapse:collapse; width:100%; min-width:560px; font-size:14.5px; }
th, td { text-align:left; padding:10px 13px; border-bottom:1px solid var(--rule); vertical-align:top; }
thead th { font-family:var(--mono); font-size:11px; letter-spacing:.08em; text-transform:uppercase;
  color:var(--muted); font-weight:500; background:var(--surface-2); white-space:nowrap; }
tbody tr:last-child td { border-bottom:none; }
td.mono, .mono { font-family:var(--mono); font-size:13px; }
.chip { font-family:var(--mono); font-size:10.5px; letter-spacing:.06em; text-transform:uppercase;
  padding:2px 7px; border-radius:2px; white-space:nowrap; }
.chip.open { color:var(--accent); background:var(--accent-soft); }
.chip.blocked { color:var(--danger); background:color-mix(in srgb,var(--danger) 13%,transparent); }
.chip.done { color:var(--ok); background:color-mix(in srgb,var(--ok) 14%,transparent); }
.chip.dropped { color:var(--muted); background:var(--surface-2); }
.chip.stale { color:var(--warn); background:color-mix(in srgb,var(--warn) 16%,transparent); }
.chip.claim { color:var(--ink-2); background:var(--surface-2); }
/* A question waiting on whoever is reading. The accent is the page's "this is
   addressed to you" colour and the outline is what separates it from a label,
   which is the only other accent chip a card carries.
   Not the .asked below: that is the question panel inside a sheet, and a chip
   wearing its name came out with panel padding on the face of the card. */
.chip.asks { color:var(--accent); background:var(--accent-soft); border:1px solid var(--accent); }
/* A claim is somebody working now; the last writer is somebody who was. The
   outline says "past tense" without spending another colour on it. */
.chip.note { color:var(--muted); background:transparent; border:1px solid var(--rule); }
/* Whose is this. The hue is derived from the handle, so the same agent is the
   same colour on every board, and no two meanings share a colour: the accent,
   the danger red and the warning amber are all reserved elsewhere. */
.who-chip { display:inline-flex; align-items:center; gap:5px; font-family:var(--mono);
  font-size:11px; padding:2px 7px 2px 3px; border-radius:999px; white-space:nowrap;
  color:oklch(var(--who-l) var(--who-c) var(--who-h));
  background:oklch(var(--who-l) var(--who-c) var(--who-h) / .16); }
.who-chip .face { flex:none; border-radius:50%; }
.timeline .who .who-chip { font-size:11px; }
td .face { vertical-align:-3px; margin-right:4px; }
.filters .who-chip { font-size:11.5px; }
/* Acting on a card: in the sheet, where there is room, never on the 230px face. */
.edit { display:flex; flex-wrap:wrap; gap:10px 18px; align-items:flex-end;
  border-top:1px solid var(--rule); margin-top:14px; padding-top:14px; }
.edit form.row { max-width:none; gap:8px; margin:0; }
.edit label { font-size:12px; }
.edit .tags { gap:6px; }
/* The note takes the whole width under the two short fields: it is the one
   control here that somebody writes a sentence into, and a sentence typed into
   a 14 character box is a sentence nobody types. */
.edit form.note { flex:1 0 100%; display:flex; flex-direction:column; gap:6px; margin:0; max-width:none; }
.edit form.note textarea { min-height:56px; width:100%; }
.edit form.note button { align-self:flex-start; }
/* A question waiting on a person, on the card it is about. Marked out from the
   card's own text: it is the one part of a sheet that is addressed to whoever
   is reading rather than describing the work. */
.asked { border:1px solid var(--accent); border-radius:8px; padding:12px 14px; margin-top:14px;
  background:color-mix(in srgb, var(--accent) 5%, transparent); }
.asked .label { color:var(--accent); margin:0 0 6px; }
.asked textarea { width:100%; min-height:52px; }
.asked form { max-width:none; display:flex; flex-direction:column; gap:8px; margin:8px 0 0; }
/* Rewriting what is already there, folded shut. */
.edit .rewrite { flex:1 0 100%; }
.edit .rewrite summary { font-family:var(--mono); font-size:12px; color:var(--muted); cursor:pointer; }
.edit .rewrite form { max-width:none; display:flex; flex-direction:column; gap:6px; margin:8px 0 0; }
.edit .rewrite input, .edit .rewrite textarea { width:100%; }
.edit .rewrite button { align-self:flex-start; }
/* Filing a card. In a sheet, like everything else that is a form rather than
   something to read: on the page it was a pair of large empty boxes above a
   board, which is the page apologising for itself. */
.addcard { margin:0 0 14px; }
.newitem { display:flex; flex-direction:column; gap:8px; max-width:none; margin:0; }
.newitem label { font-size:12px; font-family:var(--mono); color:var(--muted); }
.newitem input, .newitem textarea { width:100%; }
.newitem button { align-self:flex-start; }
.edit button.tag { font-family:var(--mono); font-size:11px; padding:3px 8px; border-radius:999px; }
/* A control revealed by hover is a control that does not exist on a phone. */
@media (hover: none) { .col .card .move { opacity:1; } }
/* A line above the headline, saying who this page is for before it says what
   the thing is. Cheap, and it stops the h1 from having to do both jobs. */
.eyebrow { font-family:var(--mono); font-size:11.5px; letter-spacing:.1em; text-transform:uppercase;
  color:var(--accent); margin:0 0 10px; }
/* The board on the front page is a demonstration, not a control surface: it
   sits in a frame that says "this is a picture of the product", and it never
   grows past the reading column the way a real board is allowed to. */
.demo { border:1px solid var(--rule); border-radius:4px; background:var(--surface);
  padding:14px 14px 4px; margin:0 0 14px; }
/* Sized so all four columns fit the reading column: a demonstration that has to
   be scrolled sideways to be seen whole has demonstrated the wrong thing. */
.demo .cols { grid-auto-columns:minmax(0,1fr); width:auto; gap:10px; }
.demo .lane { width:auto; }
.demo .col { max-height:none; min-width:0; padding:8px; }
.demo .card { cursor:default; }
.demo .card .t { font-size:13px; }
.demo .who-chip { font-size:10.5px; padding:1px 6px 1px 2px; }
@media (max-width:640px) { .demo .cols { grid-auto-flow:row; } }
form { display:flex; flex-direction:column; gap:12px; max-width:440px; }
form.row { flex-direction:row; align-items:flex-end; gap:10px; max-width:none; flex-wrap:wrap; }
label { display:flex; flex-direction:column; gap:5px; font-size:14px; color:var(--ink-2); }
input, select, textarea { font:inherit; font-size:15px; padding:9px 11px; background:var(--surface);
  color:var(--ink); border:1px solid var(--rule); border-radius:3px; }
textarea { min-height:80px; font-family:var(--mono); font-size:13.5px; }
/* One appearance for one meaning. A link that performs the page's action is a
   button to everybody who is not reading the markup, and "Add an item" spent a
   week as a line of underlined text among other lines of underlined text: it
   read as another place to navigate to rather than as the thing to press. The
   element still differs where it has to, because a GET belongs in an anchor
   and a write belongs in a form, but the two share this rule so neither can
   drift away from the other. */
button, .btn { font:inherit; font-size:14.5px; font-weight:560; padding:9px 16px; cursor:pointer;
  background:var(--accent); color:var(--bg); border:1px solid var(--accent); border-radius:3px; }
.btn { display:inline-block; text-decoration:none; }
.btn:hover { color:var(--bg); text-decoration:none; }
button.ghost, .btn.ghost { background:transparent; color:var(--accent); }
.btn.ghost:hover { color:var(--accent); }
/* board */
/* Prose wants 66 characters, a board wants room, so a board page widens the
   whole column (see .wrap.wide) rather than escaping it with viewport-width
   tricks: 100vw counts the scrollbar, which pushes the page itself sideways by
   exactly that much. The scrolling belongs to this element, never to the page. */
.board { overflow-x:auto; overscroll-behavior-x:contain; padding-bottom:10px; margin-bottom:10px; }
.lane { margin-bottom:22px; width:max-content; min-width:100%; }
.lane > .lane-title { font-family:var(--mono); font-size:11px; letter-spacing:.09em; text-transform:uppercase;
  color:var(--muted); padding:0 0 8px; border-bottom:1px solid var(--rule); margin-bottom:12px; }
/* stretch, not start: four framed columns holding different amounts of work
   used to end at four different heights, which reads as a rendering fault
   rather than as a difference in how much work each one holds. */
.cols { display:grid; grid-auto-flow:column; grid-auto-columns:270px; gap:12px; align-items:stretch;
  width:max-content; }
/* A column scrolls inside itself. Twenty-five cards in one lane otherwise push
   everything below the board a screen and a half down the page. */
.col { background:var(--surface-2); border:1px solid var(--rule); border-radius:3px; padding:10px;
  display:flex; flex-direction:column; gap:8px; min-width:230px;
  max-height:min(70vh,720px); overflow-y:auto; }
.col > header { position:sticky; top:-10px; background:var(--surface-2); padding:2px 0 4px; z-index:1; }
.col > header { display:flex; align-items:baseline; justify-content:space-between; gap:8px; }
.col h3 { margin:0; font-size:14px; font-weight:640; }
.col .n { font-family:var(--mono); font-size:12px; color:var(--muted); font-variant-numeric:tabular-nums; }
.col .why { font-family:var(--mono); font-size:10.5px; color:var(--muted); line-height:1.35; margin:-4px 0 0; }
/* Scoped to a column on purpose: an unscoped .card here would win over the page
   card above it and quietly restyle every card on every other page. */
.col .card { background:var(--surface); border:1px solid var(--rule); border-radius:2px;
  padding:9px 10px; margin:0; display:flex; flex-direction:column; gap:5px; }
/* anywhere, not break-all: a slug is read and retyped, and it has natural
   break points at its colon and its hyphens. break-all ignored them and split
   errors:venue-withdraw-stuck as "...withdraw-st" and "uck". */
.col .card .slug { font-family:var(--mono); font-size:11px; color:var(--muted); overflow-wrap:anywhere; }
/* The whole card body is the link to its preview: a title clamped to two lines
   has to be readable somewhere, and the card itself is where people click. */
.col .card .peek { display:flex; flex-direction:column; gap:5px; text-decoration:none;
  color:inherit; border:0; }
.col .card .peek:hover .t { color:var(--accent); }
.col .card .t { font-size:13.5px; line-height:1.35; display:-webkit-box; -webkit-line-clamp:2;
  -webkit-box-orient:vertical; overflow:hidden; }
.col .card .meta { display:flex; flex-wrap:wrap; gap:5px; align-items:center; }
/* The bottom line of a card: when it last moved, and how urgent it is. One row
   with the two ends pinned, so a column of cards lines both up. */
.col .card .foot { display:flex; align-items:baseline; justify-content:space-between; gap:8px; }
.col .card .prio { font-family:var(--mono); font-size:11.5px; font-weight:600;
  font-variant-numeric:tabular-nums; color:var(--ink-2); }
.col .card.is-stale { border-left:2px solid var(--warn); }
.col .card.is-claimed { border-left:2px solid var(--accent); }
/* The move control. A select and a button, because a drag needs JavaScript and
   this page has none. It stays quiet until the card is hovered or the select is
   focused, so a full column still reads as a list of work. */
/* flex-direction and max-width are reset explicitly: the page-wide form rule
   above stacks fields in a 440px column, and inheriting either of those here
   drops the button onto its own line inside a 230px card. */
.col .card .move { display:flex; flex-direction:row; align-items:center; gap:4px;
  max-width:none; opacity:.72; transition:opacity .12s; }
.col .card:hover .move, .col .card .move:focus-within { opacity:1; }
.col .card .move select { flex:1; min-width:0; font-size:11.5px; padding:2px 4px;
  border:1px solid var(--rule); border-radius:2px; background:var(--surface); color:var(--ink); }
.col .card .move button { font-size:11px; padding:3px 7px; border-radius:2px;
  background:transparent; border:1px solid var(--rule); color:var(--ink-2); cursor:pointer; }
.col .card .move button:hover { border-color:var(--accent); color:var(--accent); }
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
  clip:rect(0 0 0 0); white-space:nowrap; border:0; }
/* Anything holding one is positioned, and this is not tidiness.
   An absolutely positioned child whose ancestors are all static takes the page
   as its containing block, so it is laid out at its static position but
   measured against the document rather than against the box it appears to be
   in. Each card in a column carries an off-screen label for its move control,
   and a column scrolls inside itself: the labels of the cards scrolled out of
   sight were therefore sitting at document coordinates far below the page,
   which the browser dutifully added to the scrollable area. What a person saw
   was a screen and a half of nothing under the footer. */
.col .card, .filters { position:relative; }
/* The card preview. :target, not a dialog: the URL opens it, "#" closes it, and
   it works with scripting off like everything else here. It also means a
   preview is a link somebody can send. */
.peeked { display:none; }
/* Two ways in, one look: the fragment, which a browser opens for free, and the
   class, which is what a page rendered for one open card can promise without
   one. */
.peeked:target, .peeked.open { display:block; position:fixed; inset:0; z-index:50; }
.peeked .scrim { position:absolute; inset:0; background:color-mix(in srgb,var(--scrim) 55%,transparent);
  border:0; }
.peeked .sheet { position:relative; margin:6vh auto; max-width:min(680px,92vw); max-height:88vh;
  overflow-y:auto; background:var(--surface); border:1px solid var(--rule); border-radius:4px;
  padding:20px 22px; box-shadow:0 18px 50px color-mix(in srgb,var(--scrim) 30%,transparent);
  display:flex; flex-direction:column; gap:12px; }
.peeked .sheet-top { display:flex; align-items:baseline; justify-content:space-between; gap:12px; }
.peeked .sheet-top .slug { font-family:var(--mono); font-size:12px; color:var(--muted);
  word-break:break-all; }
.peeked .sheet h3 { margin:0; font-size:21px; line-height:1.25; text-wrap:balance; }
.peeked .sheet .meta { display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
.peeked .sheet .body { margin:0; white-space:pre-wrap; font-size:14.5px; line-height:1.55; }
.peeked .sheet .why { font-family:var(--mono); font-size:11px; color:var(--muted); margin:0; }
.peeked .sheet .none { color:var(--muted); font-style:italic; margin:0; font-size:14px; }
.peeked .sheet .timeline { border-top:1px solid var(--rule); padding-top:4px; }
/* Filters that apply themselves. Every value is a link, so choosing is the
   whole interaction: a field with the names behind it, and Enter applies the
   lot. No button to press after choosing, which is what the operator asked
   for, and the value in force is in the box rather than somewhere in a row. */
.filters { display:flex; flex-direction:row; flex-wrap:wrap; align-items:flex-end;
  gap:8px 14px; margin:0 0 16px; max-width:none;
  border:1px solid var(--rule); border-radius:6px; padding:10px 12px; background:var(--surface); }
.filters label { display:flex; flex-direction:column; gap:3px; font-family:var(--mono);
  font-size:10.5px; letter-spacing:.09em; text-transform:uppercase; color:var(--muted); }
.filters input { font-size:13.5px; padding:5px 9px; font-family:var(--sans);
  letter-spacing:normal; text-transform:none; color:var(--ink); }
.filters input[type="search"] { min-width:180px; }
.filters .hint { align-self:flex-end; font-size:12.5px; color:var(--muted); padding-bottom:5px; }
.reset { font-family:var(--mono); font-size:12px; padding:5px 10px; border-radius:4px;
  border:1px solid var(--rule); color:var(--ink-2); text-decoration:none; margin-left:auto; }
.reset:hover { border-color:var(--accent); color:var(--accent); }
.ghost-link { align-self:center; font-size:13.5px; }
.col .more { font-family:var(--mono); font-size:11px; color:var(--muted); }
.col .none { font-size:12.5px; color:var(--muted); font-style:italic; }
.timeline { list-style:none; padding:0; margin:0; font-size:14px; }
.timeline li { display:grid; grid-template-columns:auto auto 1fr; gap:10px; padding:7px 0;
  border-top:1px solid var(--rule); align-items:baseline; }
.timeline .when, .timeline .who { font-family:var(--mono); font-size:12px; color:var(--muted); white-space:nowrap; }
.timeline .who.hygiene { color:var(--warn); }
.empty { color:var(--muted); font-style:italic; }
footer.bot { margin-top:56px; border-top:1px solid var(--rule); padding-top:16px;
  font-size:13px; color:var(--muted); display:flex; gap:16px; flex-wrap:wrap; }
.notice { background:var(--accent-soft); border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);
  padding:14px 16px; margin-bottom:18px; font-size:15px; }
.notice.warn { background:color-mix(in srgb,var(--warn) 12%,transparent);
  border-color:color-mix(in srgb,var(--warn) 35%,transparent); }
time { white-space:nowrap; }
/* What the columns hold, in one line, for the width where the board itself is a
   strip that scrolls sideways. */
details.layout { margin-top:34px; border-top:1px solid var(--rule); padding-top:10px; }
details.layout > summary { cursor:pointer; color:var(--muted); font-family:var(--mono);
  font-size:12px; letter-spacing:.08em; text-transform:uppercase; padding:4px 0; }
details.layout > summary:hover { color:var(--accent); }
details.layout[open] > summary { margin-bottom:10px; }
.tally { display:none; font-family:var(--mono); font-size:12.5px; color:var(--muted); }
.tally b { color:var(--ink); font-variant-numeric:tabular-nums; }
@media (max-width:560px) {
  .timeline li { grid-template-columns:1fr; gap:2px; }
  .tally { display:block; }
  /* A table narrower than the phone is a table with a column off the screen,
     and the column that goes first is the last one: the links. Nothing on the
     page said it scrolled sideways, so on the operator's own list of projects
     the only way into any of them was invisible. Below 560 each row becomes a
     small card and every cell carries its own heading.

     Only the tables that asked for it, by carrying the cards class and a
     data-label on every cell. A reference table turned into unlabelled cards
     is worse than one that scrolls: the pricing table rendered for a while as
     "50", "5", "20" with nothing to say which number was which. Everything
     else keeps its own horizontal scrolling, because a wide child with
     nowhere to go pushes the whole page sideways instead. */
  .scroll.cards { overflow-x:visible; border:0; background:none; }
  table.cards { min-width:0; display:block; }
  table.cards thead { display:none; }
  table.cards tbody, table.cards tr, table.cards td { display:block; }
  table.cards tbody tr { background:var(--surface); border:1px solid var(--rule);
    border-radius:3px; padding:8px 11px; margin-bottom:10px; }
  table.cards td { border-bottom:0; padding:3px 0; }
  table.cards tbody tr:last-child { margin-bottom:0; }
  table.cards td[data-label]::before { content:attr(data-label); display:block;
    font-family:var(--mono); font-size:10px; letter-spacing:.08em; text-transform:uppercase;
    color:var(--muted); }
}
