:root {
  /* Named because two rules have to agree on it: the sidebar's grid
     track and the checkout box above it, which lines up with the column
     or reads as a fourth edge on a page with three. */
  --side-w: 320px;

  /* The settings rail's track. In ch, unlike --side-w, because what has
     to fit is a list of words in the page's own font rather than a diff
     and a set of boxes: 22 is the longest entry plus its indent. */
  --rail-w: 22ch;

  /* The sticky bar's height: its own box, and the offset the things
     below it park at. Spelled out rather than left to the content,
     because a height the browser works out is not a value another rule
     can name. Taller than the tallest thing in the bar, which centers in
     what is left -- a claim about this number that nothing checks, so
     anything added to the bar is measured against it by hand.

     Pixels, not em: em resolves against each element's own font-size, so
     the bar and the box below it agree only while nothing between them
     changes it, and its base is the browser's monospace default rather
     than the document's. One literal number has neither indirection. */
  --head-h: 48px;

  /* The gutter between the page and the window, on every page. Each
     top-level box pads itself by this rather than the body padding
     once, so the sticky bar's background reaches the window's edges
     instead of leaving two strips the diff scrolls through.

     Pixels for the same reason --head-h is: 0.5em was a different
     number on a box in monospace than on one that had not set it yet,
     so the left edge moved from one page to the next. */
  --gutter: 8px;

  /* Every color on the site is named here and used nowhere else as a
     literal, so the dark block at the bottom of this file is a second
     set of these values rather than a second copy of the rules. A rule
     that writes a hex code of its own is a rule dark mode does not
     reach, and nothing checks for one.

     The names say what the color is for, not what it looks like:
     --fg-muted is the gray a note or an age is in, and in neither theme
     is it a gray. Both are Catppuccin -- Latte here, Frappe below -- so
     the page is the same design at two weights, and the same one as the
     terminal it is read beside.

     The two are the same roles filled from each palette: base is the
     page, mantle the tinted row, crust the hover, surface0 the rules
     between rows, surface2 a control's edge, text the words, subtext0
     the notes and ages, overlay1 the faintest thing still read. The
     accents are blue for a link, green for good, red for bad. */
  --bg: #eff1f5;
  --bg-sub: #e6e9ef;
  --bg-hover: #dce0e8;
  --border: #ccd0da;
  --border-strong: #acb0be;
  --fg: #4c4f69;
  --fg-muted: #6c6f85;
  --fg-faint: #8c8fa1;
  --link: #1e66f5;
  --good: #40a02b;
  --bad: #d20f39;
  /* Yellow mixed into the page, flat rather than an alpha over it: the
     bar is fixed over the text, and text showing through the one thing
     on the page saying the page is stale is worse than one more hex. */
  --warn-bg: #ecddca;

  --diff-add-bg: rgba(64, 160, 43, 0.14);
  --diff-del-bg: rgba(210, 15, 57, 0.12);
  --diff-hunk-bg: rgba(30, 102, 245, 0.12);

  --tok-k: #8839ef;
  --tok-n: #1e66f5;
  --tok-s: #40a02b;
  --tok-m: #fe640b;
  --tok-c: #8c8fa1;

  /* What the browser draws itself -- form controls, scrollbars, the
     focus ring -- follows this rather than staying light under a dark
     page. */
  color-scheme: light dark;
}

/* The gutter is the pages', so the body has none of its own. */
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
}

/* The UA's link blue is a fixed color, so it survives the dark palette
   and is the one thing on the page still drawn for a white background.
   Rules that want a link in the row's color say color: inherit and win
   on specificity. */
a:link, a:visited {
  color: var(--link);
}

/* A form's UA margin would stack on the gap between sections. */
form {
  margin: 0;
}

/* A box in the page's font, not the platform's pill in the system
   font -- the one thing on a page that lines up with nothing. */
button, input[type="submit"] {
  font: inherit;
  color: inherit;
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: 0;
  padding: 4px 10px;
  cursor: pointer;
}
button:hover, input[type="submit"]:hover {
  background: var(--bg-hover);
}

/* Every page's text, spaced by padding since there are no blank lines
   to do it with. The sides are the shared gutter, so the left edge is
   the same on every page. The top is the header's above it: every page
   opens with one, and its margin is the space below the breadcrumb. */
.page-text {
  margin: 0;
  padding: 0 var(--gutter) 1em;
  font-family: monospace;
}
.page-text > * + * {
  margin-top: 1.5em;
}
.page-text section > b {
  display: block;
  margin-bottom: 0.4em;
}

/* Prose wraps to the window, up to the 72 columns it was written for. */
.page-text p {
  max-width: 72ch;
  margin: 0 0 0.4em;
}
.page-text p:last-child {
  margin-bottom: 0;
}

/* A command to copy, in the box the checkout command uses, so a reader
   can tell what to paste from what to read. As wide as the command: a
   border out to the window's edge is a rule through empty space. */
.cmd {
  width: fit-content;
  max-width: 72ch;
  margin: 0.4em 0;
  padding: 4px 8px;
  background: var(--bg-sub);
  border: 1px solid var(--border);
  overflow-x: auto;
}

/* Beside a line of text rather than standing on its own. */
.rows button, .rows input[type="submit"] {
  padding: 1px 8px;
}

/* A table in markup and a grid in layout. The columns line up down the
   page, which a per-row grid would not do, and table layout gives a
   drag its granularity: a selection leaving a cell snaps to whole cells
   and then to whole rows. display: contents hands every cell to one
   grid spanning the list, so it lines up and selects like text.

   A grid must be told its column count, hence cols-N. The last column
   takes the slack and wraps, so a long title ends at the window.

   The gap is the grid's rather than padding on a cell, so the space
   between two columns belongs to neither and a drag starting there
   begins in neither. */
.rows {
  display: grid;
  width: fit-content;
  column-gap: 1.5em;
  row-gap: 4px;
  align-items: baseline;
}
.rows tbody, .rows tr {
  display: contents;
}
.rows td {
  padding: 0;
}
.rows.cols-2 {
  grid-template-columns: max-content minmax(0, 1fr);
}
.rows.cols-3 {
  grid-template-columns: repeat(2, max-content) minmax(0, 1fr);
}
.rows.cols-4 {
  grid-template-columns: repeat(3, max-content) minmax(0, 1fr);
}
.rows.cols-5 {
  grid-template-columns: repeat(4, max-content) minmax(0, 1fr);
}
.row-time, .row-id {
  white-space: nowrap;
}
.row-title, .row-name {
  overflow-wrap: anywhere;
}

/* An id is a word, not a sentence: any click inside it takes the whole
   id, so a reader need not land on both of its edges. */
.row-id a {
  -webkit-user-select: all;
  user-select: all;
}

/* A repo's section in four columns -- the change, its title, the refs
   pointing at it, and when it landed. The title takes the slack, so the
   two columns on the right sit against the page's edge however long a
   title is.

   The time is last, so the one column a reader scans for "when" is in
   one place: with the refs after it the times stepped left and right
   down the page as rows carried one or did not.

   Each section is a table of its own, so a track sized to its own
   contents would put every section's times at a different place across a
   page of them. The width is the page's and the id and time tracks have
   a floor, so the columns line up from one section to the next: 8ch is a
   change id or a short sha, and 9ch the longest "115m ago". Either grows
   past it if something longer turns up.

   One bordered box with a rule between rows, the way the change page
   draws its checks, its deploys, and its files: a section is a list of
   the same kind, and a page of loose lines beside a page of boxes read
   as two designs. The space between columns is the cells' padding
   rather than a grid gap, since a gap would break the rule between two
   rows.

   The cells stretch rather than sit on their baselines: the rule between
   two rows is drawn on the tops of four cells, and a cell only as tall as
   its own text puts its share of that rule at its own height. */
.rows.repo-cols {
  width: 100%;
  column-gap: 0;
  row-gap: 0;
  align-items: stretch;
  border: 1px solid var(--border);
  grid-template-columns:
    minmax(8ch, max-content) minmax(0, 1fr)
    max-content minmax(9ch, max-content);
}
.rows.repo-cols td {
  padding: 6px 8px;
}
.rows.repo-cols tr + tr td {
  border-top: 1px solid var(--border);
}
/* Against the box's edge, so the ages read as one column rather than
   one ragged on the right because "2h" is shorter than "29h". */
.rows.repo-cols .row-time {
  text-align: right;
}

/* The whole row is the target, the way a check, a deploy, or a file in
   the diff is: a reader aiming at a line should not have to hit the
   eight characters at its left edge. script.js follows the id's link
   for a click anywhere else on the row.

   A commit no change here produced has nowhere to go, so it gets
   neither the tint nor the cursor: a row that lights up under the
   pointer and does nothing when clicked is worse than one that never
   offered. The tint is on the cells rather than the row, since a row
   in this grid is display: contents and paints nothing.

   With the row clickable the id no longer has to carry the click, so
   it drops the blue and the underline for the gray the ages and the
   notes are in. What is left in the row's own color is the subject,
   which is what a reader is scanning for. */
.rows.repo-cols .row-link:hover td {
  background: var(--bg-sub);
  cursor: pointer;
}
.rows.repo-cols .row-id, .rows.repo-cols .row-time {
  color: var(--fg-muted);
}
.rows.repo-cols .row-id a {
  color: inherit;
  text-decoration: none;
}
.rows.repo-cols .row-id a:hover {
  text-decoration: underline;
}

/* What points at a commit, in one column down the page, so a reader
   scanning it sees where the branch and each service are. Right-aligned,
   since the column is as wide as the busiest row in the section and the
   time is against the edge on its other side.

   The pills sit on the row's first line rather than stretching: the cell
   is as tall as the tallest of the four, and a pill grown to fill it is a
   box around empty space. */
.row-refs {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: flex-end;
  gap: 4px;
  white-space: nowrap;
  text-align: right;
}

/* Four columns need a window wide enough for four. Below that the title
   track is the only one that can give -- the refs are nowrap and the id
   and the time have floors -- so it gave until a title was one letter
   per line and the page was mostly rules.

   So a narrow window gets a row per row instead of a grid per section:
   the id and the age on a line, then the title, then the refs under it,
   each row a grid of its own. The columns no longer line up from one
   section to the next, which was the point of the shared grid, but on a
   phone there is one column of anything and nothing to line up with.

   The rule between two rows moves to the row, since a row is a box here
   rather than four cells in a grid, and a border on each of the four
   would draw three rules through the middle of one row. */
@media (max-width: 48em) {
  .rows.repo-cols, .rows.repo-cols tbody {
    display: block;
  }
  .rows.repo-cols tr {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    padding: 6px 8px;
  }
  .rows.repo-cols tr + tr {
    border-top: 1px solid var(--border);
  }
  /* A row is a box of its own here, so it holds the tint itself rather
     than handing it to four cells. */
  .rows.repo-cols .row-link:hover {
    background: var(--bg-sub);
    cursor: pointer;
  }
  .rows.repo-cols .row-link:hover td {
    background: none;
  }
  .rows.repo-cols tr + tr td {
    border-top: 0;
  }
  .rows.repo-cols td {
    padding: 0;
  }
  /* Source order is id, title, refs, time; the age belongs beside the id
     it dates, and a grid item is placed in order. */
  .rows.repo-cols .row-id {
    order: 1;
  }
  .rows.repo-cols .row-time {
    order: 2;
  }
  .rows.repo-cols .row-title {
    order: 3;
  }
  .rows.repo-cols .row-refs {
    order: 4;
  }
  .rows.repo-cols .row-title, .rows.repo-cols .row-refs {
    grid-column: 1 / -1;
  }
  /* Under the title now, not against the page's right edge. */
  .rows.repo-cols .row-refs {
    justify-content: flex-start;
    text-align: left;
  }
  /* Between the lines of one row, not around them: the row's own
     padding is the space at its edges. A cell with nothing in it --
     a change with no title yet, a commit nothing points at -- is not
     a line, so it gets no gap above it. */
  .rows.repo-cols .row-title:not(:empty),
  .rows.repo-cols .row-refs:not(:empty) {
    margin-top: 4px;
  }
}

/* A ref is drawn the way `git log` decorates a commit: a name on the one
   commit it points at, and where everything else sits relative to it is
   the order the rows are already in. One shape for both kinds, so they
   read as one family.

   The branch is in the gray the notes and the ages are in, and the
   service is in the green that means live everywhere else on the site,
   so the column has one color and it means the same thing there as it
   does beside a check. This page has four -- green for good, red for
   bad, gray for a fact, blue for a link -- and a fifth for the branch
   would be a color a reader has to be taught.

   The two used to be a left edge on the row instead, both of them, which
   put two bars of near enough the same weight in one column and asked a
   reader to tell them apart by hue. A name in a box does not need the
   hue to say which it is. */
/* A ref that links carries no underline: the box is already the thing
   a pointer aims at, and a rule under a name inside one draws a second
   edge through it. It answers to the pointer the way a button does
   instead, with the same tint. */
.ref {
  padding: 0 0.5em;
  border: 1px solid currentcolor;
  border-radius: 1em;
  font-size: 0.85em;
  line-height: 1.5;
}
a.ref {
  text-decoration: none;
}
a.ref:hover {
  background: var(--bg-hover);
}
.ref-branch {
  color: var(--fg-muted);
}
.ref-deploy {
  color: var(--good);
}

/* /settings: a contents rail and the groups beside it.

   One column by default, so a phone gets the page and not the index of
   it; two once there is room. The rail is written first, being the index,
   and order puts it back under the reading in one column -- a screen that
   opens on a table of contents is one where the reader scrolls past it
   every time.

   The gutter is the grid's here, so the main column drops the padding
   .page-text gives every other page: the two would stack, and the left
   edge of this page would not be the left edge of the one before it. */
.settings {
  display: grid;
  margin: 0 0 1em;
  padding: 0 var(--gutter);
  font-family: monospace;
  row-gap: 1.5em;
}
.settings > .page-text {
  padding: 0;
}
.settings-rail {
  order: 2;
}
/* min-width: 0 because a grid item otherwise refuses to shrink below its
   content: a token, a DSN, or a regexp is as wide as somebody made it,
   and a column grown to fit one would push the rail off screen. What is
   too wide scrolls in its own .cmd box. */
.settings-main {
  order: 1;
  min-width: 0;
}

/* The rail is anchors into this page, so every target needs the sticky
   bar's height cleared: without it the browser scrolls the heading to
   the top of the window, which is under the bar. */
.set-group, .settings-main section {
  scroll-margin-top: calc(var(--head-h) + 8px);
}

/* A group is two or three sections under one word. The same size and
   weight as everything else on the page -- there is one type size on this
   site, and a heading that broke it would be the only one -- so what says
   it is a group is the rule under it and the room above it. The sections
   inside already open with a bold word, and a group told apart from them
   by weight alone read as one more section. */
.set-group {
  font: inherit;
  font-weight: bold;
  margin: 0 0 0.4em;
  padding-bottom: 0.2em;
  border-bottom: 1px solid var(--border-strong);
}
/* More room above a group than between the sections inside it, so the
   boundary a reader is looking for is the wider gap. Not the first,
   which would leave a hole under the breadcrumb. */
.settings-main > .set-group {
  margin-top: 2.5em;
}
.settings-main > .set-group:first-child {
  margin-top: 0;
}

/* A section's name and its count on one line, the count against the
   right edge of the box below it: what a scan of this page is after is
   the number, and a column of them reads as a column only if they line
   up with something. The gray the ages and the notes are in, since it is
   a fact about the list and not a heading. */
.set-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 2em;
  margin-bottom: 0.4em;
}
.set-head > b {
  margin: 0;
}
.set-count {
  color: var(--fg-muted);
  white-space: nowrap;
}

/* The rail. A list of words, so it drops the bullets and the indent a
   browser gives one; the nesting is the left edge instead. Sticky beside
   the reading, which is longer than a screen and the reason the rail
   exists.

   The links are gray until hovered. A column of eleven blue underlines
   beside the page is a second page, where what is wanted is a map of
   this one. */
.settings-rail ul {
  list-style: none;
  margin: 0.4em 0 0;
  padding: 0;
}
.settings-rail ul ul {
  margin: 0;
  padding-left: 2ch;
}
.settings-rail li {
  line-height: 1.6;
}
.settings-rail a {
  color: var(--fg-muted);
  text-decoration: none;
}
.settings-rail a:hover {
  color: var(--link);
  text-decoration: underline;
}

/* Every explanation on the page, and every form that adds something.
   The whole line is the target, not the triangle a browser draws at the
   start of it, so the marker stays and the row gets the tint a button
   does: the prose is worth reading once and worth not reading again on
   the way to a control.

   Gray while closed, since a stack of them under a table is a list of
   things a reader may ignore. Open, the words are the page's own color:
   somebody is reading them. */
.settings-main details {
  margin-top: 0.4em;
}
.settings-main summary {
  cursor: pointer;
  width: fit-content;
  padding: 2px 4px;
  margin-left: -4px;
  color: var(--fg-muted);
}
.settings-main summary:hover {
  background: var(--bg-sub);
}
.settings-main details[open] > summary {
  color: var(--fg);
}
/* The contents indented under the line that opened them, so an open
   disclosure is a block a reader can see the extent of rather than text
   that resumes the page. */
.settings-main details[open] > :not(summary) {
  margin-top: 0.4em;
  padding-left: 2ch;
}
/* The one destructive thing on the page. Red on the summary only: the
   words inside are the explanation of what goes, and a paragraph in the
   color of bad news is harder to read than the news. */
.settings-main details.danger > summary {
  color: var(--bad);
}

/* Space between a section's controls and the table above them, and
   between the fields of a form that has more than one line of them. */
.settings-main section > * + :not(details) {
  margin-top: 0.4em;
}
.settings-main form > div + div {
  margin-top: 0.4em;
}
.settings-main form > div + input {
  margin-top: 0.4em;
  display: block;
}

/* Every list on /settings in one grid: an id, a name that takes the
   slack, a state, and what can be done about the row. The width is the
   page's and three of the four tracks are a fixed number of characters,
   so the id, the state, and the action are each in one column from the
   top of the page to the bottom.

   That alignment is the whole reason this class exists. The lists were
   .cols-N tables sized to their own contents -- fit-content, a track per
   column count -- so six sections meant six column rhythms down one page
   and nothing lined up with the section above it.

   Fixed rather than the minmax(Nch, max-content) the home page's
   .repo-cols uses, which is the difference between them and the reason
   for it: a grid sizes its tracks from its own rows, so max-content there
   still lets a busy section set a width no other section shares. On the
   home page every section holds the same kind of row and they come out
   near enough alike. Here they do not -- "remove service" is a third
   wider than "revoke", and one long button pulled a whole table's state
   column left of every other one. So the numbers are said once: 10ch is
   an age or a slug, 15ch the longest state -- "(not deployed)" -- and
   19ch the widest button, "remove service" plus what a button and a cell
   pad themselves by.
   Anything longer than its track overflows the cell rather than widening
   it, which is the trade: a control that does not fit is a number to
   change here, where the other three tables can be looked at at the same
   time. A button's label is also the "do" its form posts, so the label is
   not the thing to shorten.

   The box is the shape every other list on the site is drawn in: one
   border, a rule between rows, the cells stretching rather than sitting
   on their baselines, since the rule is drawn on the tops of four cells
   and a cell only as tall as its own text puts its share of it at its own
   height.

   A row with nothing to say in a track leaves the cell empty rather than
   spanning past it, so the columns hold. The exception is a row whose id
   is its name -- an account -- which spans the first two: a person's name
   belongs at the box's left edge, and the two columns a reader scans are
   untouched either way. */
.rows.set-cols {
  width: 100%;
  column-gap: 0;
  row-gap: 0;
  align-items: stretch;
  border: 1px solid var(--border);
  grid-template-columns: 10ch minmax(0, 1fr) 15ch 19ch;
}
.rows.set-cols td {
  padding: 6px 8px;
}
.rows.set-cols tr + tr td {
  border-top: 1px solid var(--border);
}
.rows.set-cols .row-wide {
  grid-column: 1 / 3;
}
/* A fact about the row, in the gray the ids and the ages are in, and
   against its track's right edge for the reason .repo-cols right-aligns
   its times: the column is as wide as the longest thing any section puts
   in it, and left-aligned it read as one column ragged on the right
   because "master" is longer than "main" and "(not deployed)" longer than
   a short sha. */
.rows.set-cols .row-state {
  color: var(--fg-muted);
  white-space: nowrap;
  text-align: right;
}
/* Against the box's edge, so the buttons read as one column rather than
   one ragged on the left because "deactivate" is longer than "revoke". */
.rows.set-cols .row-act {
  text-align: right;
  white-space: nowrap;
}

/* Four tracks need a window wide enough for four, and below that the
   name is the only one that can give -- the other three are nowrap or
   have floors. So a narrow window gets a row per row: the id and the
   state on a line, the name under them, the action under that. The
   columns no longer line up from one section to the next, which was the
   point of the shared grid, but on a phone there is one column of
   anything and nothing to line it up with. */
@media (max-width: 48em) {
  .rows.set-cols, .rows.set-cols tbody {
    display: block;
  }
  .rows.set-cols tr {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: 4px 1.5em;
    padding: 6px 8px;
  }
  .rows.set-cols tr + tr {
    border-top: 1px solid var(--border);
  }
  .rows.set-cols tr + tr td {
    border-top: 0;
  }
  .rows.set-cols td {
    padding: 0;
  }
  /* An empty cell is not a line, so it gets no row of its own. */
  .rows.set-cols td:empty {
    display: none;
  }
  .rows.set-cols .row-name, .rows.set-cols .row-act {
    grid-column: 1 / -1;
  }
  .rows.set-cols .row-act {
    text-align: left;
  }
}

/* The notes and the more... link sit below the table rather than in it,
   so they need its cells' left padding said again: without it the
   section has two left edges, and the one a reader follows down the page
   is the column of ids. */
.repo-notes, .repo-more {
  padding-left: 8px;
}

/* The same thing on /settings: a fact about a row too long for the grid
   every list there shares. A seed error is a sentence with a URL in it,
   so it wraps at the page's width instead of overflowing a 19ch track. */
.set-notes {
  padding-left: 8px;
  color: var(--fg-muted);
}

.repo-notes {
  color: var(--fg-muted);
}

/* Each repo is a page of its own: its changes, its history, its notes,
   and a link to more of it. More room between two of them than between
   the parts of one, so a reader sees where a repo ends. */
.repo-sec + .repo-sec {
  margin-top: 3em;
}

/* A way further down the same history, not one of its rows: gray, so
   the column of change ids is the only blue in the section. The same
   gray the notes above it are in, and the sidebar's facts elsewhere, so
   the page has one gray rather than two a reader has to tell apart. */
.repo-more {
  margin-top: 0.4em;
}
.repo-more a {
  color: var(--fg-muted);
}

/* The one page with something beside its text. One column by default,
   so a phone gets the text first; two once there is room.

   The sidebar is written first, so the facts a reader glances at do not
   wait on a long diff. order puts the text back on top in one column
   and the grid areas put the sidebar beside it in two, so neither
   layout reads in source order.

   font-family is the bare generic, as pre's UA rule is: a browser
   applies its monospace default size (13px, not 16px) only when the
   family is exactly monospace, and naming a face would leave this a
   size larger than the text beside it. */
.change {
  display: grid;
  margin: 0 0 1em;
  padding: 0 var(--gutter);
  font-family: monospace;
  /* In one column the sidebar is a second row, and the comments above
     it end in text: without this the author's label sits against the
     last comment. Inert in two columns, which are one row. */
  row-gap: 1.5em;
}
.change-side {
  order: 2;
}

/* min-width: 0 because a grid item otherwise refuses to shrink below
   its content, whatever the track says: the diff is as wide as the
   longest line anybody committed, and a column grown to fit one runs
   under the sidebar and covers the checks. Anything too wide for the
   track scrolls in its own box. */
.change-main {
  order: 1;
  min-width: 0;
}

.change-text {
  margin: 0;
}
.change-text > b {
  display: block;
  margin-bottom: 0.4em;
}
.change-main > * + * {
  margin-top: 1em;
}

/* A description, a commit message, a comment: text somebody wrote
   hard-wrapped, which keeps its own whitespace and wraps when a line is
   too long for the column. A pre, not a div told to preserve
   whitespace: a parser drops a pre's leading newline, and hml writes a
   multi-line element's text on the line below the tag, so the div read
   a blank line lower than everything beside it. */
.change-body {
  margin: 0;
  font: inherit;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Who and when on a line above what they said. */
.comment + .comment {
  margin-top: 1em;
}
.comment-head {
  margin-bottom: 0.4em;
}
.comment-head time, .comment-head .edited, .comment-head .relation {
  color: var(--fg-muted);
}

/* The breadcrumb, and whatever the page offers beside it: the checkout
   command on a change, retry on a run. What is below is longer than a
   screen. Opaque and above the text, which tints its own diff rows.

   Outside the change page's grid rather than a cell in it: a grid item
   sticks only within its own grid area, and would unstick as soon as
   row one scrolled past. */
.page-head {
  font-family: monospace;
  margin: 0 0 0.8em;
  padding: 0 var(--gutter);
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  gap: 2em;
  align-items: center;
  justify-content: space-between;
  background: var(--bg);
  box-sizing: border-box;
  height: var(--head-h);
}
.checkout {
  display: flex;
  gap: 8px;
  align-items: center;
  min-width: 0;
}
/* One line, always: a wrapped shell command reads as two. The box takes
   what the button leaves, so a longer change id gets a wider box. */
.checkout code {
  flex: 1;
  min-width: 0;
  background: var(--bg-sub);
  border: 1px solid var(--border);
  padding: 4px 8px;
  white-space: nowrap;
  overflow-x: auto;
}
.checkout button {
  flex: none;
}

/* Who is signed in and the way out, in the header's second slot. It was
   a section of /settings, which spent a heading and three lines on two
   facts and put the way out of the page in the middle of it. The name is
   gray, being a fact: what is worth a click here is the link. */
.page-you {
  display: flex;
  gap: 1.5em;
  align-items: baseline;
  min-width: 0;
}
.page-you span {
  color: var(--fg-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (min-width: 800px) {
  .change {
    grid-template-columns: minmax(0, 1fr) var(--side-w);
    column-gap: 2em;
    align-items: start;
  }
  .change-main {
    grid-area: 1 / 1;
  }
  .change-side {
    grid-area: 1 / 2;
  }

  /* The checkout takes the sidebar's column exactly, so its edges
     are the edges of the boxes below it and the top of the page
     reads as two columns rather than a bar with something in the
     corner. Only here: in one column there is nothing to line up
     with. */
  .checkout {
    flex: none;
    width: var(--side-w);
  }

  /* The rail on the left, unlike the change page's sidebar: it is an
     index into the reading beside it, and an index is read before the
     thing it indexes. It sticks at the bar's height, the page being
     several screens long and the rail being the way around it. */
  .settings {
    grid-template-columns: var(--rail-w) minmax(0, 1fr);
    column-gap: 3em;
    align-items: start;
  }
  .settings-rail {
    grid-area: 1 / 1;
    position: sticky;
    top: calc(var(--head-h) + 8px);
  }
  .settings-main {
    grid-area: 1 / 2;
  }
}

/* The UA's 1em on a pre would double the header's gap, and live output
   arrives in a second pre, whose margin would read as a hole in the
   middle of the text. */
pre.output {
  margin: 0;
  padding: 0 var(--gutter);
}

/* The word under the output, which is its own element rather than part
   of the pre, so it needs the gutter said again. Padding, not margin:
   it is inline while hidden, and display would fight the attribute. */
#live-eof {
  padding-left: var(--gutter);
}

/* A label above its value. Bold and lowercase, as every section header
   here is. An empty section is plain text -- "(no checks)" -- since a
   box around it announces the ordinary state of a change under
   review. */
.side-section {
  margin-bottom: 1.5em;
}
.side-section label {
  display: block;
  font-weight: bold;
  margin-bottom: 0.4em;
}

/* A block, so the description starts on its own line, the gap being a
   label's rather than the blank line that read as two sections.

   On a deploy page the title is the way to the change, the reader
   being on neither. Unstyled, as the hashes in the sidebar are: a
   stack of them in blue and underlined reads as a menu, where what is
   wanted is a heading that happens to be a link. */
.change-title {
  display: block;
  margin-bottom: 0.4em;
}
.change-title a {
  color: inherit;
  text-decoration: none;
}
.change-title a:hover {
  text-decoration: underline;
}

/* Only a deploy page stacks these: a change has one set of its own
   words, and its comments are separated from them by the diff. More
   room between two changes than between a title and its description,
   so the boundary a reader is looking for is the wider gap and not the
   weight of a line. Enough to beat the blank line inside a description
   and the 1em above the diff, and no more: a deploy carries two or
   three of these, and every extra half-em pushes the diff down. */
.change-text + .change-text {
  margin-top: 2em;
}

/* An outcome carries a color, as a check's state does. In progress is
   the ordinary case and reads as text. */
.status-merged {
  color: var(--good);
}
.status-closed {
  color: var(--bad);
}

/* Why a form was refused, in the red the other bad news is in. */
.error {
  color: var(--bad);
}

/* The page has stopped keeping up: a region's fetch failed, the page
   render could not fill one, or the bell went quiet and no region will
   hear that it should ask again. One sentence for all of it, which is
   why nothing here marks the regions themselves -- a wake asks for them
   together, so the same trouble marks most of them and the page filled
   up with one fact repeated.

   Centered at the top, over the sticky header, where a reader about to
   trust an old number meets it first. A pill rather than a full-width
   bar: it is one line, and a rule across the page reads as chrome the
   page always had. */
.live-down {
  position: fixed;
  top: 8px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 3;
  padding: 4px 10px;
  font-family: monospace;
  color: var(--bad);
  background: var(--warn-bg);
  border: 1px solid var(--border);
  border-radius: 4px;
}

/* A change that no longer applies to its base branch: red, because it
   is the same kind of news as a closed status, and first in the column
   because nothing below it can happen until it is fixed. */
.conflict {
  color: var(--bad);
  margin-bottom: 0.4em;
}
.paths {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border);
}
.paths li + li {
  border-top: 1px solid var(--border);
}
.paths li {
  padding: 6px 8px;
  overflow-wrap: anywhere;
}

/* Beside the status but not part of the verdict, so it drops the status
   color. Unstyled link, like every hash in this column: it is a list of
   facts, and blue underlines down it read as decoration. */
.merge-sha {
  color: var(--fg-muted);
}
.merge-sha a {
  color: inherit;
}
.merge-sha a:hover {
  text-decoration: underline;
}

/* The sidebar's lists -- checks, commits, and where a change is
   running -- are one design: the same bordered box, a rule between
   rows, the age or the state at the right edge, so the column reads as
   one column. That state carries the color, being the word a reader
   came for.

   A service the change has not reached is a row without a link, so its
   line is a span laid out like the anchor beside it rather than a
   second design. */
.checks, .deploys {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border);
}
.check + .check, .deploy + .deploy {
  border-top: 1px solid var(--border);
}
.check a, .deploy a, .deploy-line {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.3em 8px;
  padding: 6px 8px;
  color: inherit;
  text-decoration: none;
}
.check a:hover, .deploy a:hover {
  background: var(--bg-sub);
}
.check-name, .deploy-name {
  overflow-wrap: anywhere;
}
.check-age, .check-elapsed, .deploy-state {
  color: var(--fg-muted);
  text-align: right;
  white-space: nowrap;
}
.check-ok .check-state, .deploy-live .deploy-state {
  color: var(--good);
}
.check-fail .check-state, .deploy-no .deploy-state {
  color: var(--bad);
}
.check-pending .check-state {
  color: var(--fg-muted);
}

.hashes {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border);
}
.hashes li + li {
  border-top: 1px solid var(--border);
}
/* A commit no change here produced has nowhere to go, so its line is a
   div laid out like the anchor beside it rather than a row that lights
   up under the pointer and does nothing when clicked. */
.hashes a, .hash-line {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  padding: 6px 8px;
  color: inherit;
  text-decoration: none;
}
.hashes a:hover {
  background: var(--bg-sub);
}
.hash-age {
  color: var(--fg-muted);
  text-align: right;
  white-space: nowrap;
}

/* One box per changed file, in the same bordered box -- except that the
   lines across it belong to the headers rather than to the list: every
   header draws its own top edge, so the first is the list's top edge
   and the rest are the rules between files. The header is the thing
   that sticks, and an edge owned by the list or by the file above it
   scrolls away from the box that needs it.

   A plain block, not a grid with a gap: a grid's implicit column sizes
   to max-content and nothing clamps it to the container, so one long
   line in one patch widened every box on the page.

   The header is the whole target, not the disclosure triangle, which
   list-style and the WebKit marker drop. Fixed columns for the stats,
   so the filenames start in one place; the name takes the rest and
   wraps rather than pushing the box wide; the button's column holds its
   width whether or not the button shows, so a filename does not shift
   on hover. */
.diff-files {
  border: 1px solid var(--border);
  border-top: 0;
}
.diff-file > summary {
  list-style: none;
  cursor: pointer;
  display: grid;
  grid-template-columns: 5ch 5ch minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
  border-top: 1px solid var(--border);
  /* Two, where every other list uses six: the room above and below
     the text is inside the copy button instead. Two plus two plus
     the button's ten is six plus six, so a file row and a commit
     row are still the same height. */
  padding: 2px 8px;
}

/* The path, for the terminal open beside the diff. Transparent until
   the row is hovered, so a list of files is not a column of buttons.
   Opacity rather than hiding it: a hidden element takes no focus, and
   Tab reaching the button is the only way to copy a path mouseless. */
.copy-path {
  opacity: 0;
  padding: 3px 10px;
}
.diff-file > summary:hover .copy-path,
.copy-path:focus-visible {
  opacity: 1;
}
.diff-file > summary::-webkit-details-marker {
  display: none;
}
.diff-file > summary:hover {
  background: var(--bg-sub);
}

/* An open header keeps the tint and rules itself off from the patch,
   becoming its lid rather than a line in a list, and takes space below
   so the next filename reads as the next thing. Not the last file's,
   which would leave a gap above the list's own edge.

   It sticks while the patch scrolls past, a diff longer than the screen
   being read with nothing on it saying which file this is. Only when
   open: a list of closed headers stacking at the top would push the
   reading off screen.

   It parks at the bar's height exactly, so the bar's bottom edge meets
   this box's top border, the lid an open file is drawn with. An overlap
   would spend that border to absorb a pixel of rounding; flush keeps it,
   and pays at a zoom where two independently positioned edges round
   apart and a row of scrolling diff shows through the seam.

   Below the bar, not above it: a header sticks only within its own
   file's box, so a file scrolled past pushes its header up through the
   bar, and one that paints over the bar covers the breadcrumb on the way
   out. */
.diff-file[open] > summary {
  background: var(--bg-sub);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: var(--head-h);
  z-index: 1;
}
.diff-file[open]:not(:last-child) {
  margin-bottom: 8px;
}

/* The ring inside the header rather than straddling its border, where
   it reads as a line on one file instead of a rectangle over the page.
   Only a keyboard puts it there -- a click on a summary is refused
   focus in script.js -- and it stays: Tab and Enter are the only way to
   read a diff mouseless. */
.diff-file > summary:focus-visible {
  outline-offset: -2px;
}

/* The padding the header gave the button, back on the text, so a
   filename long enough to wrap is not pressed against the box's edge.
   Nothing changes on a row that fits: the button is taller either
   way. */
.diff-name {
  overflow-wrap: anywhere;
  padding: 3px 0;
}

/* A wrapped diff line is two rows of one tint, and moves the column a
   reader is comparing. */
.diff-patch {
  margin: 0;
  overflow-x: auto;
}

/* Gray for a zero, since the color says "lines changed" and a green +0
   contradicts its own number. Foreground only: the list is dense and a
   tint would compete with the filename. Right-aligned, so the digits
   line up down the list. */
.diff-stat {
  text-align: right;
}
.adds {
  color: var(--good);
}
.dels {
  color: var(--bad);
}
.zero {
  color: var(--fg-faint);
}

/* Per-line tints: gi inserted, gd deleted, gu a hunk header, gc
   context. Each line is a span from highlight.Diff, made a block here
   so the tint reaches the full width of the pre; the marker char
   inherits the row's color and the tokens below override it. Scoped
   under .diff, which wraps each patch. */
.diff .gi,
.diff .gd,
.diff .gu,
.diff .gc {
  display: block;
}
.diff .gi {
  background-color: var(--diff-add-bg);
  color: var(--good);
}
.diff .gd {
  background-color: var(--diff-del-bg);
  color: var(--bad);
}
.diff .gu {
  background-color: var(--diff-hunk-bg);
  color: var(--link);
}
.diff .gc {
  color: var(--fg);
}

/* Five tokens, which is what github.com/croaky/highlight emits: k
   keywords, n names, s strings, m numbers, c comments. Subtle enough to
   stay legible on the row tints. Operators and punctuation carry no
   class and take the row's color. */
.diff .k {
  color: var(--tok-k);
}
.diff .n {
  color: var(--tok-n);
}
.diff .s {
  color: var(--tok-s);
}
.diff .m {
  color: var(--tok-m);
}
.diff .c {
  color: var(--tok-c);
  font-style: italic;
}

/* Dark mode: the same roles, filled from Frappe instead of Latte. Only
   the tokens at the top of this file are restated, since every rule
   above reads one of them -- a page that looks wrong here is a rule
   that wrote a color of its own.

   Frappe is one step darker than base for its tinted row where Latte is
   one step lighter, so --bg-sub is surface0 here and mantle there. The
   rest line up.

   The diff tints are their accents at low alpha rather than flat fills:
   neither palette has near-base backgrounds that read as "added" and
   "removed", and a tint mixed with whatever is under it is right on the
   box and on the sticky header both. */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #303446;
    --bg-sub: #414559;
    --bg-hover: #51576d;
    --border: #51576d;
    --border-strong: #626880;
    --fg: #c6d0f5;
    --fg-muted: #a5adce;
    --fg-faint: #838ba7;
    --link: #8caaee;
    --good: #a6d189;
    --bad: #e78284;
    --warn-bg: #514f53;

    --diff-add-bg: rgba(166, 209, 137, 0.14);
    --diff-del-bg: rgba(231, 130, 132, 0.14);
    --diff-hunk-bg: rgba(140, 170, 238, 0.14);

    --tok-k: #ca9ee6;
    --tok-n: #8caaee;
    --tok-s: #a6d189;
    --tok-m: #ef9f76;
    --tok-c: #838ba7;
  }
}
