/* 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, the radii and the motion are the webmail's (../email), carried
   over so the two apps on this box read as one product. Dark only -- there is
   no light palette and no theme switch. */

/* ------------------------------------------------------------- the palette
   Every colour in the app is one of these. */

:root {
  color-scheme: dark;

  --bg: #0f1216;
  --surface: #161a20;
  --surface-2: #1c2128;
  --surface-3: #252b34;
  --border: #262c35;
  --border-strong: #333b46;

  /* The board's work surface. A shade off the page so a column reads as a
     card lying on the board rather than as one more block of the page. */
  --canvas: #12161c;

  --text: #e7eaf0;
  --text-muted: #9aa3b2;
  --text-faint: #7b8494;

  --accent: #5b9dff;
  --accent-hover: #79aeff;
  /* Ink for text sitting on the accent. The accent is bright, so this is the
     dark half of the pair, not white. */
  --accent-fg: #08111f;
  --accent-soft: #17233a;

  --success: #4ade80;
  --success-soft: #14291e;
  --danger: #ff6b6b;
  --danger-soft: #2b1719;
  --warn: #fbbf24;
  --warn-soft: #2b2213;

  /* Diffs get their own pair so a tweak to the semantic colours above never
     changes how a patch reads. */
  --diff-add: #14291e;
  --diff-del: #2b1719;
  --diff-hunk-bg: #17233a;
  --diff-hunk-fg: #5b9dff;

  /* The nav is the webmail's topbar: a surface strip over the page, not the
     black bar it used to be. The deploy log stays a terminal. */
  --nav-bg: #161a20;
  --nav-fg: #e7eaf0;
  --nav-fg-muted: #9aa3b2;
  --term-bg: #0b0e12;
  --term-fg: #d5dbe3;

  /* The chevron every select draws for itself. It is a colour like any other
     here, so it is a token. */
  --select-caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='%239aa3b2' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6.5 8 10.5 12 6.5'/%3E%3C/svg%3E");

  --radius: 10px;
  --radius-sm: 7px;

  /* The spacing scale, and the whole of it. Three steps: a table cell, a strip
     that heads or foots a region, and a region's own body. Everything that
     pads anything in this file uses one of them, so two regions side by side
     cannot disagree about what a comfortable inset is. */
  --pad-cell: 9px 12px;
  --pad-strip: 12px 16px;
  --pad-body: 16px;
  /* The gap between stacked regions, and the page gutter. */
  --gap: 16px;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 8px 28px rgba(0, 0, 0, 0.55);

  /* Motion. One place to slow everything down, and one place for the
     reduced-motion block below to switch it all off. */
  --dur-fast: 120ms;
  --dur: 190ms;
  --dur-slow: 280ms;
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
  --ease-out: cubic-bezier(0, 0, 0.2, 1);

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

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 0ms;
    --dur: 0ms;
    --dur-slow: 0ms;
  }

  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------------------------------------------------------- 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;
  -webkit-font-smoothing: antialiased;
  /* 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: 1px;
  border-radius: 4px;
}

.main {
  max-width: 1120px;
  margin: 0 auto;
  padding: 24px var(--gap) 64px;
}

/* The card page. A document reads at a narrower measure than a file listing,
   and the section tabs above it belong to the same column, so the width is set
   on the page rather than on the card: the header, the tabs and the document
   all end on one line. */
.main-doc {
  max-width: 912px;
}

/* 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);
  border-bottom: 1px solid var(--border);
  color: var(--nav-fg);
}

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

.nav-brand {
  font-size: 15px;
  font-weight: 650;
  color: var(--accent) !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, and the panel
   every other menu in the app reuses by class name. nav.js adds what a bare
   details cannot do, which is close on an outside click or on Escape. */
.nav-menu {
  position: relative;
}

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

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

.nav-menu > summary:hover,
.nav-menu[open] > summary {
  background: var(--surface-3);
  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: var(--shadow-md);
}

/* One row shape for every menu in the app, which is what makes the account
   menu, a column menu and the card menu read as the same control: a flex row,
   the icon a fixed box on the left, the label the part that grows, and both
   sitting on the row's centre line rather than on a text baseline. The logout
   row 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: flex;
  align-items: center;
  /* Stated, not left to the default: the base button rule centres its content,
     and specificity is settled per property, so a rule that never mentions
     justify-content does not take it back. */
  justify-content: flex-start;
  gap: 8px;
  width: 100%;
  min-height: 30px;
  padding: 5px 10px;
  border: none;
  border-radius: var(--radius-sm);
  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;
}

.nav-menu-list a:active,
.nav-menu-list button:active {
  background: var(--border-strong);
}

/* Inset, because the row is flush with the edge of the panel and a ring drawn
   outside it would be clipped by the panel's own radius. */
.nav-menu-list a:focus-visible,
.nav-menu-list button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* An icon button that drops the panel above. It started as the column menu and
   is now every menu in the app that is not the account one: a column, a card,
   the ref switcher, the clone url. It lives here, beside the panel it opens,
   rather than in the board section, so that the two places which restyle its
   summary can simply come later in the file instead of out-specifying it. Only
   the summary and the danger entry differ from the nav's, 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;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: var(--radius-sm);
  transition: background var(--dur-fast), color var(--dur-fast);
  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 > summary:active {
  background: var(--border-strong);
}

/* ".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(--danger) !important;
}

/* ------------------------------------------------------------------ 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-shadow: var(--shadow-sm);
}

/* A rule and nothing else. The head used to carry a fill of its own, which
   put three greys inside 60px on any box holding a table: the head, the frame
   around the table, and the table's own sticky header. The table header is the
   one that has to be a fill, because it stays put while rows scroll under it,
   so the other two give way. */
.box-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: var(--pad-strip);
  border-bottom: 1px solid var(--border);
}

.box-title {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 15px;
  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: var(--pad-body);
}

/* A body that is nothing but a table takes no inset of its own: the cells
   already carry it, and the box's border is the only edge the table needs.
   No caller has to ask for this -- a box holding a table is a box holding a
   table, and it should not be possible to build one that forgot to say so. */
.box-body:has(> .table-wrap:only-child) {
  padding: 0;
}

/* When that table is also the last thing in the box it takes the box's rounded
   bottom, so its final row does not square off the corner. The wrapper already
   clips, because it is the thing that scrolls. */
.box-body:last-child > .table-wrap:only-child {
  border-radius: 0 0 var(--radius) var(--radius);
}

/* A table sharing its body with something else -- a breadcrumb above it, a
   note below -- floats in the body's inset instead of filling it, so there it
   draws the edge the box is no longer giving it. */
.box-body > .table-wrap:not(:only-child) {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* 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: var(--pad-strip);
  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 */

/* Every table in the app is a ".grid" inside a ".table-wrap": the wrapper is
   what scrolls sideways, so a wide table never widens the page and never needs
   a container of its own. table() in html.ts emits the pair. */
.table-wrap {
  overflow-x: auto;
}

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

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

/* Rows are divided by the line under each one, so a table ends on a rule when
   something follows it and on nothing when it is the last thing in its box.
   The old rule ran along the top of each row, which meant every table opened
   with a line the header had already drawn. */
.grid td {
  padding: var(--pad-cell);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

.grid tbody tr:last-child td {
  border-bottom: none;
}

/* Middle is right for a row of single lines with controls at the end of it. A
   row carrying a block is not that, and reads from the top. */
.grid tr:has(pre) td {
  vertical-align: top;
}

/* The column of actions. Fixed and right aligned, so the buttons of every row
   line up with each other rather than with the width of the text beside them,
   and the reading columns keep the space. */
.grid th.act,
.grid td.act {
  width: 1%;
  text-align: right;
  white-space: nowrap;
}

/* A table saying it has nothing to show: one row, centred, muted. */
.grid td.empty {
  padding: 22px 12px;
  color: var(--text-muted);
  text-align: center;
}

/* Each action in a table cell is its own little post form; they belong on one
   line, not stacked. The cell keeps display:table-cell -- a flex container here
   would be pulled out of the row and stop sizing with the columns beside it --
   so the spacing is a margin between siblings rather than a gap. */
.grid td > form {
  display: inline-flex;
  vertical-align: middle;
}

.grid td.act > * {
  vertical-align: middle;
}

.grid td.act > * + * {
  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: 6px 10px;
  min-height: 34px;
  max-width: 100%;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  transition: border-color var(--dur-fast), background var(--dur-fast);
}

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: none;
  border-color: var(--accent);
}

/* The combo box, drawn rather than left to the platform. The native control
   paints a different arrow on every operating system and sizes its own box to
   suit, which made it the one control on a form that never matched the inputs
   beside it. Appearance goes, the chevron becomes the token at the top of this
   file, and the right padding is what keeps a long option off it. */
select {
  appearance: none;
  padding-right: 30px;
  background-image: var(--select-caret);
  background-repeat: no-repeat;
  background-position: right 9px center;
  background-size: 16px 16px;
  cursor: pointer;
}

select:hover:not(:disabled) {
  border-color: var(--border-strong);
  background-color: var(--surface-2);
}

/* The option list belongs to the platform, so the closed control is the only
   surface left to say the press landed. */
select:active:not(:disabled) {
  background-color: var(--surface-3);
}

input:disabled,
select:disabled,
textarea:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

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

button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 6px 14px;
  min-height: 34px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-weight: 500;
  line-height: 1.2;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-fast), border-color var(--dur-fast),
    color var(--dur-fast), opacity var(--dur-fast), transform var(--dur-fast);
}

button:hover:not(:disabled) {
  background: var(--surface-2);
}

/* The press is a nudge rather than a colour change: on a dark surface the
   next shade up is barely a step, and the scale reads at any size. */
button:active:not(:disabled) {
  background: var(--surface-3);
  transform: scale(0.97);
}

button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
}

/* 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);
  filter: brightness(1.08);
}

button.primary:active:not(:disabled) {
  background: var(--accent);
  filter: brightness(0.94);
}

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

button.danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  border-color: var(--danger);
  color: var(--danger);
}

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

/* A control inside a table row. The row is the unit being read and its height
   should be set by its text, not by the buttons parked at the end of it: a
   full sized button in a 13px cell makes every row half again as tall as it
   needs to be. Stated here, so no caller has to remember a modifier. */
.grid td button,
.grid td .button-link {
  gap: 5px;
  min-height: 26px;
  padding: 3px 10px;
  font-size: 12px;
}

.grid td button.icon-button {
  width: 26px;
  min-width: 26px;
  padding: 0;
}

/* The lightest action there is: a button that reads as a link, for the row
   actions that only ever open something. */
.link-btn {
  min-height: 0;
  padding: 0;
  border: none;
  background: none;
  color: var(--accent);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}

.link-btn:hover:not(:disabled) {
  background: none;
  color: var(--accent-hover);
}

.link-btn:active:not(:disabled) {
  background: none;
}

/* 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(--danger);
}

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

/* The block every page opens with: a title, sometimes a description, and the
   section tabs. The tabs are part of it rather than a strip floating under it,
   so the rule they sit on is the line that closes the header. */
.repo-header {
  margin-bottom: var(--gap);
}

/* A header whose tabs supply the closing rule does not also need the gap: the
   rule is the separation. */
.repo-header > .tabs {
  margin-top: 14px;
  border-bottom: 1px solid var(--border);
}

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

.repo-title {
  margin: 0;
  font-size: 20px;
  font-weight: 650;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}

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

/* One row under the repo name: the section tabs on the left, and on the right
   the controls that act on the repo. They share a line and a rule because they
   are one bar, not a tab strip with a toolbar bolted under it. The rule lives
   here rather than on .tabs so it runs the full width behind both halves. */
.repo-bar {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  margin-top: 14px;
  border-bottom: 1px solid var(--border);
}

/* The tabs give first: on a narrow window they scroll sideways and the
   controls keep their size, because a squeezed Clone button is unusable while
   a scrolled tab is not. */
.repo-tools {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  gap: 8px;
  padding-bottom: 6px;
}

/* The section tabs. The active one carries aria-current, which is both the
   accessible answer and the styling hook. Every tab reserves the underline as
   a transparent border, so activating one cannot shift the row, and the row
   still scrolls cleanly on a narrow screen.

   A tab is muted until it is the one you are on. Before, all three were full
   strength and only a 2px line said which page you were looking at; the
   colour is the louder signal and it should be carrying that. */
.tabs {
  display: flex;
  flex: 1 1 auto;
  gap: 4px;
  min-width: 0;
  overflow-x: auto;
}

.tabs a {
  padding: 10px 13px;
  border-bottom: 2px solid transparent;
  color: var(--text-muted);
  font-weight: 500;
  white-space: nowrap;
  transition: color var(--dur-fast), border-color var(--dur-fast);
}

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

.tabs a[aria-current="page"] {
  border-bottom-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

/* The two disclosures in the repo bar. Both reuse the column menu's summary
   and the account menu's panel, so a menu is a menu everywhere in the app; what
   they add is a label beside the icon, which a 26px square summary has no room
   for. */
.ref-menu > summary,
.clone-menu > summary {
  gap: 6px;
  width: auto;
  height: 28px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text);
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
}

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

/* The branch name is the one part of the control that can be long, so it is
   the only part allowed to be cut. */
.ref-current {
  display: inline-block;
  max-width: 160px;
  overflow: hidden;
  font-family: var(--mono);
  text-overflow: ellipsis;
  white-space: nowrap;
}

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

/* Branches and tags are one panel with two headings rather than two panels: a
   ref is a ref, and which kind it is matters less than which one you want. */
.menu-head {
  margin: 6px 10px 2px;
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.menu-head:first-child {
  margin-top: 2px;
}

.ref-list {
  left: 0;
  right: auto;
  max-height: 380px;
  min-width: 260px;
  overflow-y: auto;
}

/* The tick holds its column on every row, so the names line up whether or not
   the row is the one being shown. */
.ref-list .ref-tick {
  visibility: hidden;
}

.ref-list a[aria-current="true"] .ref-tick {
  visibility: visible;
}

.ref-name {
  flex: 1 1 auto;
  overflow: hidden;
  font-family: var(--mono);
  font-size: 13px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* When the tip last moved. It closes every row, so a branch nobody has pushed
   to in months is visible from the control you switch with, which is what the
   branches table used to be for. */
.ref-when {
  flex: 0 0 auto;
  color: var(--text-muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.ref-note {
  flex: 0 0 auto;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-muted);
  font-size: 11px;
}

.clone-panel {
  min-width: 340px;
  padding: 4px 10px 10px;
}

.clone-panel .field-row {
  gap: 6px;
  margin-top: 0;
}

.clone-panel input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 30px;
  font-family: var(--mono);
  font-size: 12px;
}

/* ------------------------------------------------------------- go to file */

/* The field and the panel it drops. finder.js clears the hidden attribute on
   the wrapper, so with no script none of this is on the page at all. */
.finder {
  position: relative;
}

.finder-field {
  display: flex;
  align-items: center;
  position: relative;
}

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

.finder-field input[type="search"] {
  width: 190px;
  min-height: 28px;
  padding: 3px 10px 3px 30px;
  font-size: 13px;
}

/* Wider than the field it hangs off, because a path is longer than a query and
   truncating every row would defeat the control. */
.finder-panel {
  max-height: 360px;
  min-width: 380px;
  overflow-y: auto;
}

.finder-panel a {
  font-family: var(--mono);
  font-size: 12px;
  overflow-wrap: anywhere;
}

/* The keyboard's place in the list. It is an attribute rather than the focus,
   so it needs its own fill: the caret stays in the field while the arrows walk
   the results. */
.finder-panel a[aria-selected="true"] {
  background: var(--accent-soft);
}

.finder-note {
  margin: 0;
  padding: 6px 10px;
  color: var(--text-muted);
  font-size: 12px;
}

/* ------------------------------------------------------------ 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 */

/* The icon and the name are one cell, and the icon holds its own column, so a
   long name wraps under itself rather than under the glyph. */
.tree-name {
  display: flex;
  align-items: baseline;
  gap: 8px;
  overflow-wrap: anywhere;
}

.tree-name > .icon {
  color: var(--text-muted);
}

/* What the file is, rather than what git calls it. Set back from the name it
   describes, because the name is what the reader is scanning for. */
.tree-kind {
  color: var(--text-muted);
  white-space: nowrap;
}

/* 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;
}

/* The commit leads the line instead of trailing the item on a row of its own,
   which is one row less per event and puts the link where the eye starts. The
   chip is what keeps it from reading as the first word of the sentence. */
.event-sha {
  display: inline-block;
  margin-right: 6px;
  padding: 0 6px;
  background: var(--surface-3);
  border-radius: 4px;
}

.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. */
/* The region runs the full width of the window while the header above it stops
   at the content column, and without something drawn under it that difference
   reads as a mistake rather than as a decision. The tint and the rule at the
   top are that something: above the line is the page, below it is the board,
   and a white column now lies on a surface instead of floating on the page. */
.board-region {
  --board-top: 260px;
  display: flex;
  flex-direction: column;
  height: max(320px, calc(100dvh - var(--board-top)));
  padding: 12px 16px 0;
  background: var(--canvas);
  border-top: 1px solid var(--border);
}

/* 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;
}

/* Shorter than the form default, so the chevron moves in with the box. */
.board-bar select {
  padding-right: 26px;
  background-position: right 7px center;
}

/* 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;
  /* The one box head in the app that keeps a fill of its own. Everywhere else
     the head is a label and a rule is enough to set it off; here it is the
     handle the column is picked up by, and the fill is what marks out the
     strip you can grab. */
  background: var(--surface-2);
  border-radius: var(--radius) var(--radius) 0 0;
}

.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;
}

/* ----------------------------------------------------------------- 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.6);
  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,
   a panel of properties, then the body flowing straight down. The left padding
   is the gutter the block grips live in and it sits on the whole stack, not on
   the card alone, so the tabs above start on the same line the title does and
   a grip still never leaves the page. */

.card-stack {
  padding-left: 26px;
}

.card-page {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* 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;
  /* Pulled back by its own padding so the heading's first letter sits on the
     same line as the first letter of the document below it. */
  margin: 0 0 0 -6px;
  padding: 2px 6px;
  border-radius: var(--radius);
  font-size: 24px;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

/* Nothing on the page says the heading is typeable, so the pointer has to: the
   title takes a fill under the cursor the way a menu row does, which is the
   only affordance a contenteditable heading can carry without turning back
   into the labelled input this page replaced. */
.card-title:hover {
  background: var(--surface-3);
}

/* Inside a document the caret is the indicator, so neither the title nor the
   body takes the page's focus ring. The title still marks itself out while it
   is being typed in, because it is the one block that is not part of the
   document below it. */
.card-title:focus,
.ce:focus {
  outline: none;
}

.card-title:focus {
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border-strong);
}

/* A card whose title has just been emptied is still a click target and still
   says what it is waiting for. */
.card-title:empty::before {
  content: "Untitled";
  color: var(--text-muted);
}

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

/* Properties, not form fields. The document below draws no frame at all, so
   without one here the two would run together into one flat column; the panel
   is the whole reason the page has a top and a body rather than a single
   undifferentiated surface. One layer only: it lifts off the page, and nothing
   inside it is boxed again. */
.card-props {
  display: grid;
  gap: 1px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  font-size: 13px;
}

/* A name and its value, the names on one line so the values read as a column
   of answers rather than as a run of labelled fields. */
.card-prop {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr);
  align-items: center;
  gap: 8px;
  min-height: 30px;
  margin: 0;
  padding: 0 6px;
  border-radius: 4px;
}

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

/* The row is the hit target for the control it holds, so the row is what
   lights up under the pointer. */
.card-prop:hover {
  background: var(--surface-2);
}

/* The house style for a property: the value, and nothing drawn around it. The
   frame and the chevron arrive together, and only once the pointer or the
   keyboard has said the value is about to be changed. The room the chevron
   needs is held open the whole time, so arriving does not move the text. */
.card-prop select {
  width: 100%;
  min-height: 26px;
  padding: 2px 24px 2px 6px;
  background-color: transparent;
  background-image: none;
  background-position: right 5px center;
  border-color: transparent;
}

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

/* 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;
}

/* The page's tail: when the card was made, when it last changed, and the line
   every status message on this page lands in. All three are the page talking
   about itself rather than the card's content, which is why they sit under the
   document instead of in the property panel above it. */
.card-foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 16px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 12px;
}

.card-foot p {
  margin: 0;
}

/* The spacer as well as the status target, so a message grows into the gap at
   the end of the line instead of moving what is beside it. */
.card-status {
  flex: 1 1 120px;
  overflow: hidden;
  text-align: right;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.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-file-input {
  display: none;
}

/* ------------------------------------------------------------- diagrams */

/* A mermaid diagram is a code block that mermaid-block.js has drawn. The class
   below is added only once a diagram has actually painted, so a card with no
   javascript, and a diagram whose source does not parse, both keep their source
   on screen: the block degrades to exactly the code block it is. */
.ce-mermaid {
  padding: 4px 0;
  overflow-x: auto;
}

.ce-mermaid svg {
  max-width: 100%;
  height: auto;
}

/* The picture replaces the source rather than sitting under it. Clicking the
   picture takes the class off again and puts the caret back in the text. */
.ce-mermaid-on > .ce-text {
  display: none;
}

.ce-mermaid-on {
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
}

/* A source that does not parse says so where the picture would have been, and
   keeps the source visible above it so the error has something to point at. */
.ce-mermaid-error {
  padding: 8px 10px;
  background: var(--danger-soft);
  border: 1px solid var(--danger);
  border-radius: var(--radius);
  color: var(--danger);
  font-family: var(--mono);
  font-size: 12px;
  white-space: pre-wrap;
}

/* 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(--danger) !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: var(--shadow-md);
}

.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: var(--shadow-md);
}

.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;
  gap: 7px;
  padding: 6px 14px;
  min-height: 34px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-weight: 500;
  line-height: 1.2;
  white-space: nowrap;
  text-decoration: none;
  transition: background var(--dur-fast), border-color var(--dur-fast);
}

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

.button-link:active {
  background: var(--surface-3);
  transform: scale(0.97);
}

/* 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-stack {
    padding-left: 22px;
  }

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

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

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

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

  /* 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;
  }
}
