/* Gigit's only stylesheet. Spec section 10: one hand-written file, no
   framework, no build step, no CDN. Everything a page can show is styled
   here, so a page only ever needs the helpers in html.ts. */

/* ------------------------------------------------------------- the palette
   Every colour in the app is one of these. Dark mode below redefines the
   same names and nothing else, so no rule is ever written twice. */

:root {
  color-scheme: light dark;

  --bg: #f6f8fa;
  --surface: #ffffff;
  --surface-2: #f6f8fa;
  --surface-3: #eaeef2;
  --border: #d0d7de;
  --border-strong: #afb8c1;

  --text: #1f2328;
  --text-muted: #59636e;

  --accent: #0969da;
  --accent-hover: #0860ca;
  --accent-fg: #ffffff;
  --accent-soft: #ddf4ff;

  --success: #1a7f37;
  --success-soft: #dafbe1;
  --danger: #cf222e;
  --danger-soft: #ffebe9;
  --warn: #9a6700;
  --warn-soft: #fff8c5;

  /* Diffs get their own pair so a tweak to the semantic colours above never
     changes how a patch reads. */
  --diff-add: #e6ffec;
  --diff-del: #ffebe9;
  --diff-hunk-bg: #ddf4ff;
  --diff-hunk-fg: #0550ae;

  /* The nav and the deploy log are dark in both themes: one is chrome, the
     other is a terminal. */
  --nav-bg: #24292f;
  --nav-fg: #ffffff;
  --nav-fg-muted: #d0d7de;
  --term-bg: #0d1117;
  --term-fg: #d5dbe3;

  --radius: 6px;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0d1117;
    --surface: #151b23;
    --surface-2: #1b2028;
    --surface-3: #262c36;
    --border: #3d444d;
    --border-strong: #4d545e;

    --text: #e6edf3;
    --text-muted: #9198a1;

    --accent: #4493f8;
    --accent-hover: #58a6ff;
    --accent-fg: #ffffff;
    --accent-soft: #121d2f;

    --success: #3fb950;
    --success-soft: #12261e;
    --danger: #f85149;
    --danger-soft: #291415;
    --warn: #d29922;
    --warn-soft: #272115;

    --diff-add: #12261e;
    --diff-del: #291415;
    --diff-hunk-bg: #121d2f;
    --diff-hunk-fg: #4493f8;

    --nav-bg: #151b23;
    --nav-fg: #e6edf3;
    --nav-fg-muted: #9198a1;
  }
}

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

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.5;
  /* The page column never scrolls sideways; wide things scroll in .scroll-x. */
  overflow-x: hidden;
}

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

a:hover {
  text-decoration: underline;
}

p {
  margin: 0 0 10px;
}

p:last-child {
  margin-bottom: 0;
}

ul {
  margin: 6px 0;
  padding-left: 20px;
}

code {
  font-family: var(--mono);
  font-size: 0.92em;
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--surface-3);
  overflow-wrap: anywhere;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

.main {
  max-width: 1120px;
  margin: 0 auto;
  padding: 20px 16px 64px;
}

/* The board page and nothing else. The content column keeps its 1120px and a
   ".full-bleed" child spans the window instead: a named grid, so there is no
   negative margin, no 100vw and no guessing at a scrollbar width. The side
   padding goes because the gutter track is now what provides it. */
.main-wide {
  display: grid;
  grid-template-columns:
    [full-start] minmax(16px, 1fr)
    [content-start] minmax(0, 1120px) [content-end]
    minmax(16px, 1fr) [full-end];
  max-width: none;
  padding: 20px 0 16px;
}

.main-wide > * {
  grid-column: content;
}

.main-wide > .full-bleed {
  grid-column: full;
}

/* The one vertical rhythm in the app: every page is a stack of boxes. */
.stack {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.main > .box {
  margin-bottom: 16px;
}

/* ------------------------------------------------------------------- nav */

.nav {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 16px;
  padding: 0 16px;
  min-height: 48px;
  background: var(--nav-bg);
  color: var(--nav-fg);
}

.nav a {
  color: var(--nav-fg-muted);
}

.nav-brand {
  font-size: 15px;
  font-weight: 600;
  color: var(--nav-fg) !important;
}

.nav a:hover {
  color: var(--nav-fg);
  text-decoration: none;
}

.nav-links {
  display: flex;
  gap: 16px;
}

.nav-spacer {
  flex: 1 1 auto;
}

.nav-user {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* The account menu: a details element dressed as a dropdown. nav.js only
   adds closing it on an outside click or Escape. */
.nav-menu {
  position: relative;
}

.nav-menu > summary {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border-radius: var(--radius);
  color: var(--nav-fg-muted);
  cursor: pointer;
  user-select: none;
  list-style: none;
}

.nav-menu > summary::-webkit-details-marker {
  display: none;
}

.nav-menu > summary:hover,
.nav-menu[open] > summary {
  background: rgba(127, 127, 127, 0.25);
  color: var(--nav-fg);
}

/* The caret sits one step under the 16px default so it does not shout next
   to 14px nav text. Two classes because ".icon" is defined further down the
   file and would otherwise win on size. */
.icon.nav-menu-caret {
  width: 14px;
  height: 14px;
}

.nav-menu[open] .nav-menu-caret {
  transform: rotate(180deg);
}

.nav-menu-list {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 20;
  min-width: 180px;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
}

/* Both rows are one control each; the logout one has to be a form post, so
   its button is stripped back to look like the link above it. */
.nav-menu-list a,
.nav-menu-list button {
  display: block;
  width: 100%;
  padding: 6px 10px;
  border: none;
  border-radius: 4px;
  background: none;
  color: var(--text) !important;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.nav-menu-list a:hover,
.nav-menu-list button:hover {
  background: var(--surface-3);
  text-decoration: none;
}

/* ------------------------------------------------------------------ boxes
   win() in html.ts renders one of these for every titled region. */

.box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.box-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: var(--radius) var(--radius) 0 0;
}

.box-title {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.box-actions {
  display: flex;
  gap: 4px;
}

.box-actions button {
  min-width: 26px;
  padding: 2px 6px;
  font-size: 12px;
}

.box-body {
  padding: 14px;
}

/* Where the status bar used to be: one muted line, and the region kanban.js
   and log.js write progress and errors into. */
.box-foot {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 16px;
  padding: 8px 14px;
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 12px;
}

.box-foot-item {
  margin: 0;
}

/* A plain bordered container. sunken() wraps anything that needs an edge of
   its own inside a box: a table, a diff, a blob. */
.frame {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.scroll-x {
  overflow-x: auto;
}

/* --------------------------------------------------------------- tables */

.grid {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.grid th {
  padding: 8px 12px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  color: var(--text-muted);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}

.grid td {
  padding: 7px 12px;
  border-top: 1px solid var(--border);
  vertical-align: top;
}

/* The first row sits against the frame edge or the header, never both. */
.grid tbody tr:first-child td {
  border-top: none;
}

.grid thead + tbody tr:first-child td {
  border-top: 1px solid var(--border);
}

/* Each action in a table cell is its own little post form; they belong on
   one line, not stacked. */
.grid td > form {
  display: inline-block;
}

.grid td > form + form {
  margin-left: 6px;
}

/* Headerless two column tables: commit metadata, run metadata, account. */
.kv td:first-child {
  width: 1%;
  white-space: nowrap;
  color: var(--text-muted);
}

/* -------------------------------------------------------------- controls */

button,
input,
select,
textarea {
  font: inherit;
  color: var(--text);
}

input[type="text"],
input[type="password"],
input[type="search"],
input[type="file"],
select,
textarea {
  padding: 5px 10px;
  min-height: 32px;
  max-width: 100%;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}

textarea {
  width: 100%;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
  resize: vertical;
}

input[readonly] {
  background: var(--surface-2);
  color: var(--text-muted);
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
  border-color: var(--accent);
}

label {
  color: var(--text-muted);
  font-weight: 500;
}

button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 5px 14px;
  min-height: 32px;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  font-weight: 500;
  cursor: pointer;
}

button:hover {
  background: var(--surface-3);
}

button:active {
  background: var(--border);
}

button:disabled {
  opacity: 0.55;
  cursor: default;
}

/* One primary per form at most: the thing the page is for. */
button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-fg);
}

button.primary:hover:not(:disabled) {
  background: var(--accent-hover);
}

button.danger {
  color: var(--danger);
  border-color: var(--border-strong);
}

button.danger:hover:not(:disabled) {
  background: var(--danger);
  border-color: var(--danger);
  color: #ffffff;
}

/* Two shapes only. ".field-row" is a row of controls side by side, which is
   now always buttons or links; every labelled control is a stacked row, with
   the label on its own line above its input. */
.field-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.field-row + .field-row,
.field-row-stacked + .field-row,
.field-row + .field-row-stacked,
.field-row-stacked + .field-row-stacked {
  margin-top: 12px;
}

/* Inside a stack the gap already spaces them; the adjacency rule above is for
   the plain form bodies that are not one. */
.stack > .field-row,
.stack > .field-row-stacked {
  margin-top: 0;
}

.field-row-stacked {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 5px;
}

.field-row-stacked > label {
  font-size: 13px;
}

/* A stacked control fills its box, which is right for a card title or a body,
   but a select holds one short line, so it gets a column width instead. */
.field-row-stacked > select {
  max-width: 420px;
}

/* Same reason, for a form that is nothing but short fields: without this the
   inputs stretch the full width of the page and read as banners. */
.narrow-form {
  max-width: 420px;
}

/* ------------------------------------------------------------------ icons
   Lucide 1.38.0 (https://lucide.dev), ISC. The geometry is one <symbol>
   sprite printed by the page shell (src/icons.ts); every icon anywhere is a
   <use> pointing at it, so a glyph exists once per document however often it
   is drawn, and kanban.js builds one carrying no path data at all.

   A <use> renders into a shadow tree, so no rule may reach the shapes:
   ".icon path {...}" matches nothing and fails silently. Fill and stroke are
   inherited properties, so setting them here on the referencing <svg> is
   what crosses the boundary. That is also why no <symbol> carries a
   presentation attribute of its own: one there could not be overridden. */

.icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.icon {
  display: inline-block;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  /* Sits the 16px box on the baseline of the 14px text beside it. */
  vertical-align: -0.125em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* A control whose whole label is its icon: the name lives in aria-label and
   title instead. Square, so a row of them reads as a row of them. This block
   must stay after ".box-actions button" and "button.danger", which are the
   same specificity and set the padding and rest colour it is overriding. */
button.icon-button {
  width: 28px;
  min-width: 28px;
  min-height: 28px;
  padding: 0;
  color: var(--text-muted);
}

button.icon-button:hover:not(:disabled) {
  color: var(--text);
}

button.icon-button.danger:hover:not(:disabled) {
  color: var(--accent-fg);
}

/* -------------------------------------------------------- repo header */

.repo-header {
  margin-bottom: 16px;
}

/* Inside a stack the gap already separates it. */
.stack > .repo-header {
  margin-bottom: 0;
}

.repo-title {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.repo-desc {
  margin: 4px 0 0;
  color: var(--text-muted);
}

.repo-clone {
  margin-top: 12px;
  align-items: center;
}

.repo-clone input {
  flex: 1 1 320px;
  min-width: 0;
  font-family: var(--mono);
  font-size: 12px;
}

/* The section tabs. The active one carries aria-current, which is both the
   accessible answer and the styling hook. The underline is an inset shadow
   rather than a border so it cannot shift the row when a tab activates, and
   so the row still scrolls cleanly on a narrow screen. */
.tabs {
  display: flex;
  gap: 2px;
  margin-top: 14px;
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
}

.tabs a {
  padding: 8px 14px;
  border-radius: var(--radius) var(--radius) 0 0;
  color: var(--text);
  white-space: nowrap;
}

.tabs a:hover {
  background: var(--surface-3);
  text-decoration: none;
}

.tabs a[aria-current="page"] {
  box-shadow: inset 0 -2px var(--accent);
  font-weight: 600;
}

/* ------------------------------------------------------------ breadcrumb */

.breadcrumb {
  margin-bottom: 12px;
  font-family: var(--mono);
  font-size: 13px;
  overflow-wrap: anywhere;
}

.breadcrumb .sep {
  color: var(--text-muted);
  padding: 0 2px;
}

/* ----------------------------------------------------------- file listing */

.tree-name {
  overflow-wrap: anywhere;
}

/* Directories read before files: heavier, and the row they lead to is the
   common case, so give them the whole cell width. */
.tree-dir a {
  font-weight: 600;
}

/* SHAs, refs and paths are all fixed width wherever they appear. */
.mono,
.sha {
  font-family: var(--mono);
  font-size: 12.5px;
}

/* ------------------------------------------------------------- activity */

.event {
  padding: 12px 14px;
}

.event-head {
  overflow-wrap: anywhere;
}

.event ul {
  margin: 6px 0 0;
  color: var(--text-muted);
}

/* --------------------------------------------------------------- pills */

.pill {
  display: inline-block;
  padding: 1px 9px;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
}

.pill-queued {
  background: var(--surface-3);
  border-color: var(--border);
  color: var(--text-muted);
}

.pill-running {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}

.pill-success {
  background: var(--success-soft);
  border-color: var(--success);
  color: var(--success);
}

.pill-failed {
  background: var(--danger-soft);
  border-color: var(--danger);
  color: var(--danger);
}

.pill-error,
.pill-unknown {
  background: var(--warn-soft);
  border-color: var(--warn);
  color: var(--warn);
}

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

.plain {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-family: var(--mono);
  font-size: 13px;
}

/* preText inside a box body wants an edge of its own, like a code block. */
.box-body > .plain {
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.diff,
.blob {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.45;
}

.diff td,
.blob td {
  padding: 0 10px;
  vertical-align: top;
  white-space: pre;
}

/* The gutter is scenery, not content: it must never land in a copied
   selection of the file. */
.diff .lineno,
.blob .lineno {
  width: 1%;
  padding: 0 8px;
  text-align: right;
  background: var(--surface-2);
  border-right: 1px solid var(--border);
  color: var(--text-muted);
  user-select: none;
}

.diff .add {
  background: var(--diff-add);
}

.diff .del {
  background: var(--diff-del);
}

.diff .hunk {
  background: var(--diff-hunk-bg);
  color: var(--diff-hunk-fg);
}

.diff-file {
  margin: 16px 0 6px;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.diff-file:first-child {
  margin-top: 0;
}

/* A terminal, so it stays dark in both themes. log.js appends to it and
   keeps it pinned to the bottom. */
.log-output {
  height: 60vh;
  margin: 0;
  padding: 10px 12px;
  overflow: auto;
  background: var(--term-bg);
  color: var(--term-fg);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------- board */

/* The board is a region, not a box in a stack: it owns the width of the
   window and the rest of its height, so every column ends on the same line
   and scrolls inside itself rather than each one growing the page.
   "--board-top" is where the region starts. kanban.js measures it, because
   the repo header above it wraps at narrow widths; this value is what a
   reader with no javascript gets, and it is deliberately generous. dvh, not
   vh, so the last card is not under the browser chrome on a phone.

   The floor is not decoration. At 320x256 CSS px, which is 400% zoom on a
   1280x1024 screen and the reflow viewport WCAG 2.1 asks a page to survive,
   everything above the region wraps until it is taller than the window: the
   subtraction goes negative, a negative height clamps to zero, and the board
   is a zero-height scroll container that no amount of page scrolling can
   bring back. Past that point the region gives up owning the viewport and
   becomes an ordinary block the page scrolls to. kanban.js clamps the
   measured value to the same 320. */
.board-region {
  --board-top: 260px;
  display: flex;
  flex-direction: column;
  height: max(320px, calc(100dvh - var(--board-top)));
  padding: 0 16px;
}

/* One row: the filter on the left, what the page is doing on the right, one
   control height for all of it. */
.board-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-bottom: 10px;
}

.board-bar button,
.board-bar select,
.board-bar input[type="search"] {
  min-height: 28px;
  padding: 3px 10px;
}

/* The label wraps the field so the magnifier sits inside the frame and is
   part of the click target. */
.board-search {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.board-search > .icon {
  position: absolute;
  left: 9px;
  color: var(--text-muted);
  pointer-events: none;
}

.board-search input[type="search"] {
  width: 220px;
  padding-left: 30px;
}

/* The status line is the spacer as well, so a message appears in the gap
   instead of moving the controls on either side of it. */
.board-status {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  overflow: hidden;
  color: var(--text-muted);
  font-size: 12px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.board-live {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  color: var(--text-muted);
  font-size: 12px;
}

.board-live-dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--text-muted);
}

/* The dot is the state, so the state is what colours it: attached, trying, or
   not connected at all. kanban.js writes both the attribute and the word. */
.board-live[data-state="live"] .board-live-dot {
  background: var(--success);
}

.board-live[data-state="reconnecting"] .board-live-dot {
  background: var(--warn);
}

.board-live[data-state="offline"] .board-live-dot {
  background: var(--border-strong);
}

.board {
  display: flex;
  flex: 1 1 auto;
  align-items: stretch;
  gap: 12px;
  min-height: 0;
  overflow-x: auto;
  padding-bottom: 8px;
}

.board-column {
  display: flex;
  flex: 0 0 280px;
  flex-direction: column;
  max-width: 280px;
  max-height: 100%;
}

/* The head is where a column is picked up, and on a touch screen it is the
   only place: panning is off here and nowhere else on the column, so a
   finger anywhere below still scrolls the board sideways. */
.board-column > .box-head {
  cursor: grab;
  touch-action: none;
}

.board-column .box-body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
  padding: 10px;
}

.board-cards {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 8px;
  min-height: 48px;
  overflow-y: auto;
}

.board-card {
  /* Grip in the first column, title and meta stacked in the second. */
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: 6px;
  padding: 10px;
  background: var(--surface-2);
  cursor: grab;
}

/* The filter hides cards rather than removing them, and an author "display"
   beats the "[hidden]" rule in the browser's own stylesheet. */
.board-card[hidden] {
  display: none;
}

/* The whole card is what drags under a mouse, so this only makes visible the
   affordance "cursor: grab" already implied. Under a finger it is the drag
   itself: panning is off here and on nowhere else on the card, so a swipe
   anywhere but the grip still scrolls the column. */
.board-card-grip {
  grid-column: 1;
  grid-row: 1;
  align-self: start;
  margin-top: 2px;
  color: var(--border);
  touch-action: none;
}

.board-card:hover .board-card-grip {
  color: var(--text-muted);
}

.board-card:hover {
  border-color: var(--border-strong);
}

/* An empty column says "No cards yet" right up until something is about to
   land in it. */
.board-cards > .drag-placeholder + .empty {
  display: none;
}

/* The card title fills its card so the whole block reads as one target. */
.board-card-title {
  display: block;
  grid-column: 2;
  color: var(--text);
  font-weight: 500;
  overflow-wrap: anywhere;
}

.board-card-meta {
  display: flex;
  grid-column: 2;
  justify-content: space-between;
  gap: 8px;
  margin-top: 6px;
  color: var(--text-muted);
  font-size: 12px;
}

/* Each half is an icon plus its word, and the assignee half is what has to
   give when the name is long. */
.board-card-meta > span {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

.drop-line {
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
  pointer-events: none;
}

/* Only ever the inline row the bar's Add column opens, parked at the tail of
   the column row; it removes itself when the row closes. */
.board-add {
  flex: 0 0 240px;
  align-self: flex-start;
}

/* The inline add-card and rename-column rows kanban.js builds. */
.inline-edit input[type="text"] {
  flex: 1 1 100%;
  min-width: 0;
}

/* Inside a column both of those land in a flex column that is already full
   height, and the row is content: the card list is what gives. */
.board-cards > .inline-edit,
.board-column .box-body > .inline-edit {
  flex: 0 0 auto;
}

/* The column menu borrows the nav's disclosure panel by class name. Only the
   summary and the danger entry differ, because that panel was written for
   the dark nav strip where every row is the same colour. */
.col-menu {
  position: relative;
}

.col-menu > summary {
  display: flex;
  align-items: center;
  padding: 2px 6px;
  border-radius: var(--radius);
  color: var(--text-muted);
  cursor: pointer;
  user-select: none;
  list-style: none;
}

.col-menu > summary::-webkit-details-marker {
  display: none;
}

.col-menu > summary:hover,
.col-menu[open] > summary {
  background: var(--surface-3);
  color: var(--text);
}

.col-menu .nav-menu-list button {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ".nav-menu-list button" wins its colour with !important, so Delete has to
   take it back the same way, hover included. */
.col-menu .nav-menu-list button.danger {
  color: var(--danger) !important;
}

.col-menu .nav-menu-list button.danger:hover:not(:disabled) {
  color: var(--accent-fg) !important;
}

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

/* src/static/drag.js, and the only motion in this file. The node being
   dragged leaves the flow for a fixed layer above everything else, a
   placeholder of the same size holds its slot, and the siblings shuffling
   around it are moved with a transform and nothing else. drag.js reads these
   durations back out of the computed style rather than keeping its own copy,
   so the reduced-motion block at the end of the section turns the animation
   off in the module as well as in the browser. */

.drag-layer {
  position: fixed;
  z-index: 40;
  inset: 0;
  pointer-events: none;
}

/* The node in the air. Its size and its origin are set inline because they
   are measurements; the tilt is what makes it read as picked up rather than
   as a copy that happens to be moving. */
.drag-lift {
  position: fixed;
  margin: 0;
  transform: translate(var(--drag-x, 0px), var(--drag-y, 0px)) rotate(3deg) scale(1.03);
  box-shadow: 0 18px 38px rgba(0, 0, 0, 0.3);
  cursor: grabbing;
  pointer-events: none;
}

/* The flight into the slot at the end of a drag, or back to where the drag
   began when it is abandoned. The tilt comes out as it lands. */
.drag-land {
  transform: translate(var(--drag-x, 0px), var(--drag-y, 0px)) rotate(0deg) scale(1);
  box-shadow: none;
  transition:
    transform 160ms ease-out,
    box-shadow 160ms ease-out;
}

/* The hole the lifted node left, which is also the drop target: the box it
   had, so nothing else moved when it went. */
.drag-placeholder {
  flex: 0 0 auto;
  background: var(--surface-2);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
}

/* Play, the last quarter of a FLIP: drag.js has put a sibling back where it
   used to be with a transform and is now taking that transform away. */
.drag-slide {
  transition: transform 160ms ease-out;
}

/* A drag is a drag and not a selection, wherever the pointer ends up. */
body.drag-live {
  cursor: grabbing;
  user-select: none;
}

/* A canvas frame swallows every pointer event that happens over it, and a
   drag that crosses one must not lose the pointer. drag.js captures the
   pointer, which is already enough; this is the belt to that pair of braces. */
body.drag-live .ce-canvas-frame {
  pointer-events: none;
}

/* The drag still works, it just stops moving. Nothing else in this file
   animates, so this block is the whole of it. */
@media (prefers-reduced-motion: reduce) {
  .drag-land,
  .drag-slide {
    transition: none;
  }
}

/* ----------------------------------------------------------- attachments */

.attachment {
  padding: 10px;
}

.attachment-name {
  display: flex;
  align-items: center;
  gap: 6px;
  overflow-wrap: anywhere;
}

.thumb {
  max-width: 240px;
  height: auto;
  border-radius: 4px;
}

/* ----------------------------------------------------------- card page */

/* The card reads as a document and not as a form (plan section 5): a heading,
   one row of properties, then the body flowing straight down with nothing
   drawn between them. The left padding is the gutter the block grips live in,
   so the whole card shares one text column and a grip never leaves the page. */

.card-page {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 860px;
  padding-left: 26px;
}

/* The conflict and retry bars insert themselves at the top of the page and
   the gap above already spaces them. */
.card-page > .field-row {
  margin-top: 0;
}

.card-head {
  display: flex;
  align-items: flex-start;
  gap: 8px;
}

/* The page's subject, sized like one, and editable in place. The min-height
   keeps a title the reader has just emptied clickable. */
.card-title {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 1.3em;
  margin: 0;
  font-size: 24px;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

/* Inside a document the caret is the indicator. An outline around the whole
   title, or the whole body, on every click is noise. */
.card-title:focus,
.ce:focus {
  outline: none;
}

.card-menu {
  flex: 0 0 auto;
}

/* Properties, not form fields: named values on one wrapping line, set smaller
   than the body so the heading is what the page leads with. */
.card-props {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 16px;
  font-size: 13px;
}

.card-prop {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
}

.card-prop-name {
  color: var(--text-muted);
}

/* A select that reads as a value until it is pointed at. */
.card-prop select {
  padding: 2px 6px;
  min-height: 26px;
  max-width: 220px;
  background: none;
  border-color: transparent;
}

.card-prop select:hover {
  background: var(--surface);
  border-color: var(--border-strong);
}

/* The spacer, and this page's status target: a message lands in the gap at
   the end of the row rather than moving a property. */
.card-status {
  flex: 1 1 120px;
  margin: 0;
  overflow: hidden;
  color: var(--text-muted);
  text-align: right;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Files this card owns that no block points at any more. The server renders
   it only when it counted one, and kanban.js removes it again when the last
   one is deleted: it is what is left of the attachments box, and it is a
   repair surface rather than a feature. */
.card-unused {
  font-size: 13px;
}

/* ".fold" is defined further down the file and carries a margin of its own,
   which the gap on .card-page has already provided. */
.card-page > .card-unused {
  margin-top: 0;
}

.card-unused > p {
  margin: 8px 0 0;
}

.card-unused > .stack {
  margin-top: 10px;
}

/* The only Save button on the page, and only after a save has actually
   failed. Danger rather than warn: unlike a conflict, this is the page
   holding changes that are not anywhere else yet. */
.card-retry {
  padding: 8px 12px;
  background: var(--danger-soft);
  border: 1px solid var(--danger);
  border-radius: var(--radius);
}

.card-retry p {
  flex: 1 1 240px;
  margin: 0;
}

.card-retry[hidden] {
  display: none;
}

/* --------------------------------------------------------- card editor */

/* The block document on the card page. It draws no frame of its own: the
   document is the page (plan section 5), not a field on it, so what is left
   is the page's sans stack at a reading size and line height. The min-height
   is the click target below the last block. The negative margin and the
   matching padding take the region out over the gutter .card-page left open,
   so the text still starts where the heading does and a grip in the gutter is
   inside this element rather than beside it. Spec section 10c. */

.ce-wrap {
  position: relative;
}

.ce {
  margin-left: -26px;
  padding: 4px 0 40px 26px;
  min-height: 240px;
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.7;
}

/* The newline decision, applied uniformly: a soft break is a newline in the
   document, in the projection and here. There is no <br> anywhere. */
.ce-text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  min-height: 1.6em;
}

/* Positioned, because the gutter grip is an absolutely placed child of
   whichever block the pointer is over. */
.ce-block {
  position: relative;
  margin: 2px 0;
}

.ce-block + .ce-block {
  margin-top: 6px;
}

.ce-h1 > .ce-text,
.ce-h2 > .ce-text,
.ce-h3 > .ce-text {
  font-weight: 600;
  line-height: 1.3;
}

.ce-h1 > .ce-text {
  font-size: 24px;
}

.ce-h2 > .ce-text {
  font-size: 19px;
}

.ce-h3 > .ce-text {
  font-size: 16px;
}

.ce-h1,
.ce-h2,
.ce-h3 {
  margin-top: 16px;
}

.ce-li {
  display: flex;
  align-items: flex-start;
  gap: 8px;
}

.ce-li > .ce-text {
  flex: 1 1 auto;
  min-width: 0;
}

.ce-bullet,
.ce-marker {
  flex: 0 0 auto;
  min-width: 18px;
  text-align: right;
  color: var(--text-muted);
  user-select: none;
}

/* The checkbox sits on the first line of the item, not on the box's own
   baseline, which is what makes a wrapped to-do read straight. */
.ce-check {
  flex: 0 0 auto;
  margin: 5px 0 0;
}

.ce-todo[data-done="1"] > .ce-text {
  color: var(--text-muted);
  text-decoration: line-through;
}

.ce-block[data-indent="1"] {
  margin-left: 24px;
}

.ce-block[data-indent="2"] {
  margin-left: 48px;
}

.ce-block[data-indent="3"] {
  margin-left: 72px;
}

.ce-quote {
  padding-left: 12px;
  border-left: 3px solid var(--border-strong);
  color: var(--text-muted);
}

/* Code reads like the .plain blocks elsewhere on the site. */
.ce-code > .ce-text {
  padding: 8px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
}

.ce code {
  padding: 1px 4px;
  background: var(--surface-3);
  border-radius: 4px;
  font-family: var(--mono);
  font-size: 13px;
}

.ce-divider {
  padding: 6px 0;
}

.ce-divider hr {
  margin: 0;
  border: 0;
  border-top: 1px solid var(--border-strong);
}

/* .thumb's 240px cap is a list treatment; an image in the body is content. */
.ce-img {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: 4px;
}

.ce-caption {
  padding-top: 4px;
  color: var(--text-muted);
  font-size: 13px;
}

.ce-file {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 8px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.ce-missing {
  padding: 8px 10px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
}

/* The canvas block: an author's own html document in a frame that cannot
   reach this page. The padding is not decoration, it is the only part of the
   block a pointer can land on, because a frame swallows everything inside
   itself; the chrome below lives in it. */
.ce-canvas {
  padding: 8px 8px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* The height is the frame's own height attribute, written by the resize
   handle and printed by both renderers. Nothing here may set one: an author
   rule would beat the attribute and the handle would stop working. */
.ce-canvas-frame {
  display: block;
  width: 100%;
  background: var(--surface);
  border: 0;
  border-radius: 4px;
}

/* The way into the block menu, in the top right of the padding. One of these
   exists; editor.js moves it into whichever canvas the pointer is over, the
   same way it moves the gutter grip. */
.ce-canvas-bar {
  position: absolute;
  top: 4px;
  right: 4px;
}

.ce-canvas-bar button {
  background: var(--surface);
}

/* The bottom edge, which drag.js turns into the height. */
.ce-canvas-grip {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 14px;
  cursor: ns-resize;
  touch-action: none;
}

.ce-canvas-grip::after {
  content: "";
  display: block;
  width: 40px;
  height: 3px;
  margin: 5px auto;
  border-radius: 2px;
  background: var(--border-strong);
}

/* An upload still in flight. It carries data-pending, so it is also the one
   kind of block that can never be serialised into a save. */
.ce-pending {
  opacity: 0.55;
}

.ce-block[data-empty="1"] > .ce-text::before {
  content: attr(data-placeholder);
  color: var(--text-muted);
  pointer-events: none;
}

.ce-hint {
  margin: 6px 0 0;
  font-size: 12px;
}

.ce-file-input {
  display: none;
}

/* The drag handle, in the gutter at the left of the document. One of these
   exists; editor.js moves it into the block under the pointer, so it is only
   ever visible where a drag would start, and it sits inside .ce's own left
   padding so reaching for it is not leaving the region. */
.ce-grip {
  position: absolute;
  top: 2px;
  left: -26px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 4px;
  color: var(--border-strong);
  cursor: grab;
  user-select: none;
  touch-action: none;
}

.ce-grip:hover {
  background: var(--surface-3);
  color: var(--text);
}

/* The image or file block whose menu is open. */
.ce-picked {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Download, Replace and Delete for the file behind a block, on the same
   dropdown panel the column menu reuses. editor.js measures it against
   .ce-wrap and writes top and right inline. */
.ce-block-menu {
  min-width: 150px;
}

.ce-block-menu[hidden] {
  display: none;
}

.ce-block-menu a,
.ce-block-menu button {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ".nav-menu-list button" wins its colour with !important, so Delete has to
   take it back the same way, hover included. */
.ce-block-menu button.danger {
  color: var(--danger) !important;
}

.ce-block-menu button.danger:hover:not(:disabled) {
  color: var(--accent-fg) !important;
}

/* Source and Preview for a canvas. The panel covers the block it belongs to,
   so the two read as one block showing two faces; editor.js measures it
   against .ce-wrap and writes the box the way the menus write theirs. */
.ce-source {
  position: absolute;
  z-index: 6;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 220px;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
}

.ce-source[hidden] {
  display: none;
}

/* No highlighting of any kind (plan section 6). The panel decides how tall it
   is, so the corner grab a textarea normally gets would only fight it. */
.ce-source-text {
  flex: 1 1 auto;
  min-height: 0;
  resize: none;
}

/* The slash menu, absolutely placed inside .ce-wrap and never inside the
   editable region. Not a modal: it closes on Escape and on blur. */
.ce-menu {
  position: absolute;
  z-index: 5;
  min-width: 220px;
  max-height: 280px;
  overflow-y: auto;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
}

.ce-menu[hidden] {
  display: none;
}

.ce-menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 5px 8px;
  border-radius: 4px;
  cursor: pointer;
}

.ce-menu-item.is-active {
  background: var(--accent-soft);
}

.ce-menu-hint {
  color: var(--text-muted);
  font-family: var(--mono);
  font-size: 12px;
}

.ce-link-bar {
  margin-top: 6px;
}

.ce-link-bar[hidden] {
  display: none;
}

.ce-link-bar input[type="text"] {
  flex: 1 1 220px;
}

/* The card moved on under this page and the editor has stopped saving. Warn
   rather than danger: nothing is broken and nothing is lost yet, there is
   just a newer version to take. */
.card-conflict {
  padding: 8px 12px;
  background: var(--warn-soft);
  border: 1px solid var(--warn);
  border-radius: var(--radius);
}

.card-conflict p {
  flex: 1 1 240px;
  margin: 0;
}

/* ----------------------------------------------------------- login page */

.login-box {
  max-width: 360px;
  margin: 48px auto 0;
}

.field-row.login-actions {
  margin-top: 18px;
}

.form-error {
  margin-top: 12px;
  color: var(--danger);
}

.login-actions button {
  width: 100%;
}

/* The token field and its reveal button are one control. */
.input-reveal {
  display: flex;
  align-items: stretch;
  gap: 6px;
}

.input-reveal input {
  flex: 1 1 auto;
  min-width: 0;
}

.reveal {
  flex: 0 0 auto;
  padding: 0 10px;
  color: var(--text-muted);
}

.reveal .icon-eye-off,
.reveal[aria-pressed="true"] .icon-eye {
  display: none;
}

.reveal[aria-pressed="true"] .icon-eye-off {
  display: inline-block;
}

/* ------------------------------------------------- credentials and copying */

/* The copy button lives in a box header, so it inherits ".box-actions button"
   for its size. Only its two transient states are its own. */
.copy-button[data-state="done"] {
  color: var(--success);
  border-color: var(--success);
}

.copy-button[data-state="failed"] {
  color: var(--danger);
  border-color: var(--danger);
}

/* A copy button standing on its own in the page stack, with the text it copies
   never painted at all. It is the whole feature, so it is a pill rather than
   another rectangle: nothing else on the page is this shape, which is what
   stops one small control reading as a broken box. */
.pill-row {
  display: flex;
  justify-content: flex-end;
}

/* Laid out explicitly rather than by inheritance from "button". This control
   is one icon beside one word and nothing else, so the row, the no-wrap and
   the centring are stated here where they can be read, instead of being
   assembled out of three rules further up the file. */
button.copy-button.pill {
  display: inline-flex;
  flex-flow: row nowrap;
  align-items: center;
  justify-content: center;
  /* The icon is a 16px box with the glyph drawn inside it, so the optical gap
     is always a little tighter than the number. 10px reads as the 8px it
     needs to be. */
  gap: 10px;
  padding: 5px 18px;
  min-height: 30px;
  border-radius: 999px;
  color: var(--text-muted);
  font-size: 13px;
  /* The label is a flex item, so its box is centred against the icon's; with
     the inherited leading still on it, the glyphs sit low inside that box and
     the two read as misaligned. Collapsing the line box to the text height
     makes centring the boxes the same thing as centring what you can see. */
  line-height: 1;
}

button.copy-button.pill > .icon {
  flex: 0 0 auto;
  margin: 0;
  /* Only meaningful when an icon is not a flex item, and here it always is. */
  vertical-align: 0;
}

button.copy-button.pill > .copy-label {
  display: block;
  white-space: nowrap;
}

button.pill:hover:not(:disabled) {
  color: var(--text);
  border-color: var(--accent);
}

/* The done and failed colours above set only the text and the border, which on
   a pill leaves the fill flat. Tint it too, so a click that landed is obvious
   without reading the label. */
button.pill[data-state="done"] {
  background: var(--success-soft);
}

button.pill[data-state="failed"] {
  background: var(--danger-soft);
}

/* A link that stands in a row of buttons: the admin's per-user "Setup" is a
   GET, so it must be an anchor, but it has to line up with the two forms
   beside it. Everything here is copied off "button" on purpose; the two must
   not drift apart. */
.button-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 5px 14px;
  min-height: 32px;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  color: var(--text);
  font-weight: 500;
  text-decoration: none;
}

.button-link:hover {
  background: var(--surface-3);
  color: var(--text);
  text-decoration: none;
}

/* A long block that is worth keeping on the page but not worth reading every
   visit. The summary is the only affordance; the block itself is a .plain. */
.fold {
  margin-top: 12px;
}

.fold > summary {
  cursor: pointer;
  color: var(--text-muted);
  font-weight: 500;
  list-style-position: inside;
}

.fold > summary:hover {
  color: var(--accent);
}

.fold > .plain {
  margin-top: 10px;
  max-height: 60vh;
  overflow-y: auto;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* ---------------------------------------------------------- access grid */

.access-grid th:not(:first-child),
.access-grid td:not(:first-child) {
  text-align: center;
}

.access-grid input[type="checkbox"] {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* The boxes carry no visible label, so the label is only there to widen the
   hit area around its checkbox. */
.access-grid td label {
  display: inline-block;
  padding: 6px;
  cursor: pointer;
}

/* ----------------------------------------------------------------- misc */

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

.right {
  display: block;
  text-align: right;
}

/* Never a blank box: an empty region says what would fill it. */
.empty {
  margin: 0;
  padding: 20px 4px;
  color: var(--text-muted);
  text-align: center;
}

.pager {
  margin-top: 14px;
  justify-content: center;
  color: var(--text-muted);
}

/* ---------------------------------------------------------------- phone */

@media (max-width: 640px) {
  .main {
    padding: 14px 10px 48px;
  }

  /* The gutter track still provides the side padding, and the bottom one
     stays at 16px because kanban.js measures the board against it. */
  .main-wide {
    padding: 14px 0 16px;
  }

  .nav {
    padding: 6px 10px;
    gap: 10px;
  }

  .box-body {
    padding: 12px;
  }

  .repo-title {
    font-size: 18px;
  }

  /* A narrower gutter, and the document and the grips that live in it move
     with it. */
  .card-page {
    padding-left: 22px;
  }

  .ce {
    margin-left: -22px;
    padding-left: 22px;
  }

  .ce-grip {
    left: -22px;
  }

  .card-title {
    font-size: 20px;
  }

  .board-region {
    padding: 0 10px;
  }

  /* The bar wraps to two rows on a phone rather than squeezing the field. */
  .board-bar {
    flex-wrap: wrap;
  }

  .board-column,
  .board-add {
    flex-basis: 84vw;
    max-width: 84vw;
  }
}
