/* 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");

  /* Three steps and a pill. A region takes --radius, a control or a row takes
     --radius-sm, something sitting inline in a sentence takes --radius-xs, and
     anything meant to read as a tag is 999px. The smallest step was written as
     a bare 4px in eleven places before it was a step at all. */
  --radius: 10px;
  --radius-sm: 7px;
  --radius-xs: 4px;

  /* The spacing scale, and the whole of it. Four steps: a table cell, a block
     nested inside a region that is already inset, 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-tight is the one that is not simply "less". A board column is 280px
     wide and a card inside it sits inside the column's own inset, so a body
     inset there costs twice over and buys nothing; the step exists so that
     being tighter is a stated part of the scale rather than a number somebody
     picked. */
  --pad-cell: 9px 12px;
  --pad-tight: 10px;
  --pad-strip: 12px 16px;
  --pad-body: 16px;
  /* The gap between stacked regions, and the page gutter. */
  --gap: 16px;
  /* The width the page reads at. Stated once: .main reaches it by holding a
     gutter inside its own max-width and .main-wide by giving the gutter a
     track of its own, and the two only agree if they are working from the same
     number. They were 32px apart, which is what moved the header sideways on
     the way to the board and back. */
  --measure: 1120px;

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

/* The scrollbar's width is always spoken for, whether or not the page is long
   enough to need one. Without this the board, which fits the window, is a
   scrollbar wider than every page that scrolls, and the header jumps sideways
   each time you cross between them.

   The bar itself is a thumb and nothing else: no track, no arrows, and a
   colour that sits at the same strength as a border, because that is what it
   is -- an edge marking where you are, not a control asking to be used. Both
   properties inherit, so the page states them once and every scroller in it
   agrees. */
html {
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

/* The same bar for the Safari versions that do not read the two properties
   above. Where they are read this block is ignored, so the two cannot
   disagree. The transparent border is what insets the thumb: it makes the
   painted part narrower than the space the bar occupies, which is the whole of
   why a scrollbar this thin still has room to be round. */
*::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

*::-webkit-scrollbar-track,
*::-webkit-scrollbar-corner {
  background: transparent;
}

*::-webkit-scrollbar-thumb {
  border: 3px solid transparent;
  border-radius: 999px;
  background: var(--border-strong);
  background-clip: padding-box;
}

*::-webkit-scrollbar-thumb:hover {
  background: var(--text-faint);
  background-clip: padding-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: var(--radius-xs);
  background: var(--surface-3);
  overflow-wrap: anywhere;
}

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

.main {
  max-width: calc(var(--measure) + var(--gap) * 2);
  margin: 0 auto;
  padding: 24px var(--gap) 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(var(--gap), 1fr)
    [content-start] minmax(0, var(--measure)) [content-end]
    minmax(var(--gap), 1fr) [full-end];
  max-width: none;
  /* The top matches .main so the header does not step up or down between
     tabs. The bottom stays where it is: kanban.js measures the board against
     it. */
  padding: 24px 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);
}

/* The panel every dropdown in the app drops: the account and column menus,
   the card editor's block menu, and the canvas source panel. What makes it one
   panel is the surface, the radius and the shadow, so those are here; where it
   is anchored, how big it is and how hard its edge is are each panel's own
   business below. */
.nav-menu-list,
.ce-source,
.ce-menu {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
}

.nav-menu-list {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 20;
  min-width: 180px;
  padding: 4px;
  border: 1px solid var(--border);
}

/* 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, and it is the reason the form's button is named here too.

   Scoped to the rows themselves rather than to anything inside the panel: not
   every panel is a list of rows. The clone panel holds a field and a button,
   and dressing its Copy as a menu row made it full width and pushed it onto a
   line of its own. */
.nav-menu-list > a,
.nav-menu-list > button,
.nav-menu-list > form > 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);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

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

.nav-menu-list > a:active,
.nav-menu-list > button:active,
.nav-menu-list > form > 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,
.nav-menu-list > form > 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);
}

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

/* The lifted surface: a region that lies on the page. The card's property
   panel is one of these too -- it was carrying its own copy of all four
   declarations, which is two places for one recipe to drift in. */
.box,
.card-props {
  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. */
/* The muted line that closes a region: a box's status bar, and the card
   page's tail. Both are the region talking about itself. */
.box-foot,
.card-foot {
  display: flex;
  flex-wrap: wrap;
  gap: 4px var(--gap);
  padding: var(--pad-strip);
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 12px;
}

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

/* The sunken surface: a block set into the region around it rather than laid
   on top of it, which is the opposite of .box above. Five things are one of
   these -- a preformatted block in a box or a fold, and the code, file and
   canvas blocks of a card document -- and each was declaring the same three
   properties for itself. Each still brings its own padding, because what they
   hold differs; the surface is the part that must not. */
.box-body > .plain,
.ce-code > .ce-text,
.ce-file,
.ce-canvas,
.fold > .plain {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

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

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

button:disabled,
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);
}

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

/* 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 -- and
   so does a form that is nothing but short fields, which without this stretches
   the full width of the page and reads as a stack of banners. One width, said
   once, because it is one reason. */
.field-row-stacked > select,
.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. It closes on space rather than on a rule: the rule it used to
   end with was the baseline the underlined tabs stood on, and pills do not
   stand on anything. Everything that follows a header -- a box, a board column
   -- draws its own edge, so a line here only ever divided the page twice in
   the same place. */
.repo-header {
  margin-bottom: var(--gap);
}

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

/* The repo name is a link, but it is a heading first. Left at the accent it
   competed with the active tab under it and neither read as the page's
   identity; the app's one standing accent in this corner is the nav brand.
   Hover still underlines it, which is what says it is clickable. */
.repo-title a {
  color: inherit;
}

.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 because they are one
   bar, not a tab strip with a toolbar bolted under it. */
.repo-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 14px;
}

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

/* The section tabs. The active one carries aria-current, which is both the
   accessible answer and the styling hook.

   Pills, not underlines. These are the app's persistent page navigation, so
   they take the treatment the webmail gives its folder list rather than the
   one it gives the tabs inside a dialog: a dialog's tab strip has its row to
   itself, and this one shares a line with the ref switcher, the file finder
   and Clone. A pill and a button are the same shape, so the two halves of the
   bar read as one bar; an underline strip beside three bordered controls reads
   as two interfaces sharing a rule. The pill also carries the whole marker
   itself, which is what lets the header close on space instead of on a line.

   Nothing here changes the box between states, so activating a tab cannot
   shift the row and the strip still scrolls cleanly on a narrow screen. */
.tabs {
  display: flex;
  flex: 1 1 auto;
  gap: 4px;
  min-width: 0;
  overflow-x: auto;
}

/* The icon rides on the label's centre line, not its baseline, so a pill with
   a glyph is the same height as one without. */
.tabs a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border-radius: 999px;
  color: var(--text-muted);
  font-weight: 500;
  white-space: nowrap;
  transition: background var(--dur-fast), color var(--dur-fast);
}

/* A step under the 16px default: the glyph is beside 14px text and marking the
   tab, not competing with the word for it. */
.tabs .icon {
  width: 15px;
  height: 15px;
}

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

.tabs a[aria-current="page"] {
  background: var(--accent-soft);
  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;
}

/* Not a menu: a label, the url, and the one thing you came to do with it. The
   panel is wide enough to hold a clone url without shrinking it, and it is
   anchored to its right edge, so it grows leftward into the page rather than
   squeezing its own field. */
.clone-panel {
  min-width: 380px;
  padding: var(--pad-tight);
}

/* The heading belongs to the field under it, so it sits on the field's left
   edge. .menu-head insets itself to line up with the rows of a menu, which is
   a panel this one is not. */
.clone-panel .menu-head {
  margin: 0 0 6px;
}

/* One line, always. The url gives up width before the button does: a truncated
   url still copies whole, and a squeezed Copy is unusable. */
.clone-panel .field-row {
  flex-wrap: nowrap;
  gap: 6px;
}

.clone-panel input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--mono);
  font-size: 12px;
  /* Readonly, but not muted: this is the thing the panel exists to show, and
     the shared readonly rule greys it as though it were unavailable. */
  color: var(--text);
}

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

/* The magnifier inside a search field, in the two places the app has one. */
.finder-field > .icon,
.board-search > .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,
.tree-when {
  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: var(--radius-xs);
}

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

.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: var(--pad-strip);
  padding-bottom: 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 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);
}

/* Nothing to say, so nothing on the bar. This is the state a board is in
   almost all of the time: kanban.js empties the element when the stream is
   live, and the rule below is what stops the gap beside it outliving the
   word. */
.board-live:empty {
  display: none;
}

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

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

/* The board is a strip, not a page column: the columns start at the left of
   the canvas and run off the right of it, and when there are more than fit,
   this is what scrolls. Nothing here is centred and nothing is aligned to the
   page's measure -- a column is a fixed 280px, so the width the window has is
   the number of columns you can see, and giving that width back to a centring
   margin would only mean scrolling sooner.

   The padding at the bottom is the room the scrollbar appears in, so a board
   that overflows does not lose a strip of its last row to it. */
.board {
  display: flex;
  flex: 1 1 auto;
  /* A column is as tall as what it holds and no taller, up to the height of
     the board: a column with two cards is a short block, not a strip running
     to the floor, and only a full one scrolls. */
  align-items: flex-start;
  gap: 12px;
  min-height: 0;
  overflow-x: auto;
  padding-bottom: 8px;
}

/* The column is the shape Trello settled on: one dark surface holding a flat
   head, a list that scrolls inside it, and an "Add a card" line at the foot.
   Nothing inside it draws an edge of its own -- the head has no rule and no
   fill, the cards have no border -- so a column reads as one thing, and the
   only lines on the board are the columns themselves. */
.board-column {
  display: flex;
  flex: 0 0 280px;
  flex-direction: column;
  max-width: 280px;
  max-height: 100%;
  background: var(--surface);
}

/* 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 {
  gap: 6px;
  padding: 10px 8px 6px 14px;
  border-bottom: 0;
  cursor: grab;
  touch-action: none;
}

.board-column .box-title {
  font-size: 14px;
}

/* The count beside the name: quiet, tabular, and its own element, so the
   name is what is bold and the number is what changes. */
.board-count {
  flex: 0 0 auto;
  margin-right: 2px;
  color: var(--text-muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

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

/* The list is what scrolls, and its scrollbar is the column's right edge:
   the inset is on the left only, and on the right the gutter the bar lives in
   is the whole of the space between a card and the column. The gutter is
   always reserved, so a column that just tips over into scrolling does not
   have its cards jump narrower, and a short column lines its cards up with a
   tall one. */
.board-cards {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 8px;
  min-height: 24px;
  padding: 2px 0 2px 8px;
  overflow-y: auto;
  scrollbar-gutter: stable;
}

/* Trello's column bar: a thumb in a 12px strip, no arrows, no track. The
   page's own bar is the standard thin one, and a browser that reads the
   standard properties ignores the -webkit- pseudo elements while they are
   set -- which is why the page's arrows survive here and nowhere in Trello.
   So this one list steps out from under them, but only where the pseudo
   elements are read: Firefox does not match the selector test, keeps the
   inherited thin bar and its colour, and never sees an "auto" that would
   hand it a wide native one. */
@supports selector(::-webkit-scrollbar) {
  .board-cards {
    scrollbar-width: auto;
    scrollbar-color: auto;
  }
}

.board-cards::-webkit-scrollbar {
  width: 12px;
}

.board-cards::-webkit-scrollbar-thumb {
  border-width: 3px 4px 3px 3px;
}

.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: 8px 10px 8px 8px;
  background: var(--surface-2);
  /* No edge of its own: the fill is what sets a card off from the column, so
     the border .frame brings stays transparent and only holds the size. */
  border-color: transparent;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-sm);
  cursor: grab;
  /* A card is content in a column that scrolls, so it never gives. Without
     this every other child of .board-cards states it and the cards alone
     default to shrinking: once a column holds more than fits, flex takes the
     difference out of all of them at once. It is not visible on a short
     column, which is why it sat here unnoticed -- and it squashes rather than
     overflows because .frame puts "overflow: hidden" on the card, which drops
     a flex item's automatic minimum size from its content height to zero. The
     padding survives, the content box collapses under it, and the title is
     clipped through the middle. */
  flex: 0 0 auto;
}

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

/* Trello's hover is an outline; here it is the next shade up, which is what
   every other hover in the app is. */
.board-card:hover {
  background: var(--surface-3);
}

/* The "Add a card" line under the list. A button in shape and in the
   keyboard, but drawn as a row of the column: no fill and no edge until the
   pointer is on it, left aligned like the cards above it, and the same
   width as they are so the plus lines up with their text. */
.board-column-foot {
  flex: 0 0 auto;
  padding: 6px 8px 8px;
}

.board-add-card {
  display: flex;
  width: 100%;
  justify-content: flex-start;
  gap: 6px;
  padding: 6px 8px;
  min-height: 32px;
  background: transparent;
  border-color: transparent;
  color: var(--text-muted);
  font-weight: 500;
}

.board-add-card:hover:not(:disabled) {
  background: var(--surface-3);
  color: var(--text);
}

.board-add-card:active:not(:disabled) {
  transform: none;
}

/* 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 rename row lands in a body that has no inset of its own now, so it
   brings the list's. */
.board-column .box-body > .inline-edit {
  margin: 0 8px 8px;
}

/* The add-card row sits in the list, so it takes the list's left inset from
   its parent and only needs to stand clear of the scrollbar on the right. */
.board-cards > .inline-edit {
  margin-right: 2px;
}

/* ----------------------------------------------------------------- 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 var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--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 var(--dur) var(--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;
}

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

.attachment {
  padding: var(--pad-tight);
}

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

.thumb {
  max-width: 240px;
  height: auto;
  border-radius: var(--radius-xs);
}

/* ----------------------------------------------------------- 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. It sits on the card
   and not on the page around it: the page is one of the app's two widths like
   every other page, and the gutter is the card's own business. Inside it the
   title, the properties and the document all start on the same line, and .ce
   cancels it for itself so a grip has somewhere to go that is still inside the
   page.

   The gap is the rhythm .stack keeps, because it is the same thing: a column
   of regions. It ran at 14px against everything else's 16.

   No width of its own. The app has two and only two: the page measure and the
   board's full bleed. A card is a page like any other and takes the first. */
.card-page {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  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;
  /* 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. */
/* The properties sit beside each other while there is room for them, and fall
   into a column when there is not. Two short values stacked down the left of a
   panel this wide read as an empty table. */
.card-props {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1px 12px;
  padding: 6px;
  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: var(--radius-xs);
}

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

   It is the same line as a box's footer and says the same kind of thing, so it
   is styled with one: see .box-foot. The only difference is the inset -- this
   one closes a page rather than a box, so it has no box edge to stand off at
   the sides. */
.card-foot {
  align-items: center;
  padding: var(--pad-strip) 0 0;
}

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

/* Three places tell the reader something is wrong: a save that failed, a card
   that moved on underneath the page, and a diagram whose source does not
   parse. They were three copies of the same six declarations at two different
   paddings, so they are one shape here and differ only in which colour they
   are carrying.

   ".card-retry" is 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,
.card-conflict,
.ce-mermaid-error {
  padding: var(--pad-cell);
  border: 1px solid;
  border-radius: var(--radius-sm);
}

.card-retry,
.ce-mermaid-error {
  background: var(--danger-soft);
  border-color: var(--danger);
}

.card-retry p,
.card-conflict 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. The accent is
   the same one the access grid gives its boxes: a checkbox is a checkbox, and
   these were being drawn by the operating system in whatever grey it likes. */
.ce-check {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin: 4px 0 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.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;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
}

.ce code {
  padding: 1px 4px;
  background: var(--surface-3);
  border-radius: var(--radius-xs);
  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: var(--radius-xs);
}

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

.ce-file {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 8px 10px;
}

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

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

/* 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 {
  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: var(--radius-xs);
  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: var(--radius-xs);
}

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

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

.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;
  border: 1px solid var(--border-strong);
}

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

.ce-menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 5px 8px;
  border-radius: var(--radius-xs);
  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. The shape comes from the group above. */
.card-conflict {
  background: var(--warn-soft);
  border-color: var(--warn);
}

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

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