/*
 * The whole of vitrine's styling, served at /assets/vitrine.css.
 *
 * It has to sit under app/assets/. /assets/{*path} is the only asset route, so a
 * top-level app/x.css embeds as x.css, and a request for /x.css falls through to
 * the catch-all and is answered with the app shell. A stylesheet that arrives as
 * text/html is one the browser refuses, and all the page does about it is look
 * unstyled.
 *
 * Every color, and every measurement two rules have to agree on, is a custom
 * property on :root, and no rule below spells a color of its own. That is what
 * makes the dark theme a second :root block rather than a second copy of every
 * rule, and it is what lets the commits that draw the diff and the lanes add
 * rules without picking a palette. Add a property when a surface needs a color
 * nothing here has.
 *
 * Class names are short and unprefixed: there is one page and nothing to collide
 * with. Where a name exists in the client already it is spelled the same way, so
 * .line.insert, .line.delete and .line.context are LineKind out of
 * diff/textdiff.ts verbatim and a renderer sets the class from the kind it holds.
 */

:root {
  /* form controls, scrollbars and the canvas follow the theme without a rule */
  color-scheme: light dark;

  /* Monospace throughout. Every column below is a column of git output, and in a
     proportional font an object id is a different width per id. */
  --mono: ui-monospace, 'DejaVu Sans Mono', 'Liberation Mono', monospace;
  --size: 13px;

  /* What the commit rows and the graph rail have to agree on: one SVG is drawn
     per row, the middle of its lane n half a lane past n * --lane-width and its
     dot of radius --lane-dot at half of --row-height. A row that took its height
     from its content instead would slide every rail off the commit it belongs
     to. The drawer reads all three off :root rather than spelling them again. */
  --row-height: 20px;
  --lane-width: 14px;
  --lane-dot: 3.5px;
  /* How wide the rail column is, in the list as a whole and not per row: a row
     sized to its own lanes would step the id column in and out down the page.
     Six lanes is what most histories use, and the count is a property of its own
     because that is the width the drawer starts from. Raising it is not how the
     drawer widens the column, though: a var() is substituted where it is
     declared, so a page that needs more sets --rail itself on the list. */
  --rail-lanes: 6;
  --rail: calc(var(--rail-lanes) * var(--lane-width));
  /* What is left of the row once the rail and the fixed columns have taken
     theirs is the subject, so the list is wide: below about 34em a subject
     ellipses after a handful of words, and above 56em the diff starts losing the
     80 columns a patch is written to. */
  --log-width: minmax(34em, 56em);

  /* Surfaces */
  --bg: #ffffff;
  --fg: #1b1b1b;
  --fg-dim: #666666;
  --bg-chrome: #f2f2f2;
  --bg-hover: #f0f0f0;
  --bg-select: #d8e4f0;
  --border: #cccccc;
  --accent: #2b5f9e;
  --danger: #b3202c;
  /* What lifts a panel off the page it covers. A border alone reads as a box drawn
     in the layout rather than as something in front of it, and the ref picker is
     the first thing here that is in front of anything. */
  --shadow: rgb(0 0 0 / 18%);

  /* The diff. The tints stay pale on purpose: the text keeps its own color on
     top of them, because a highlighted token has to win over the line it is on. */
  --diff-insert-bg: #e6ffec;
  --diff-insert-fg: #1a7f37;
  --diff-delete-bg: #ffebe9;
  --diff-delete-fg: #b3202c;
  --diff-hunk-bg: #eef2f7;
  --diff-hunk-fg: #4a5a70;

  /* Lane colors, indexed rather than named: the rail picks var(--lane-N) for
     N = lane % --lanes. The count lives here with the colors so the drawer cannot
     carry a copy of it that drifts when a color is added. */
  --lanes: 8;
  --lane-0: #3572b0;
  --lane-1: #2f9e44;
  --lane-2: #d9480f;
  --lane-3: #9c36b5;
  --lane-4: #c92a2a;
  --lane-5: #0c8599;
  --lane-6: #b08000;
  --lane-7: #d6336c;
}

/* The dark theme is the same document with other values. Only what a dark
   background changes is listed; the geometry above is shared. */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16181a;
    --fg: #dcdcdc;
    --fg-dim: #8b9199;
    --bg-chrome: #202325;
    --bg-hover: #23262a;
    --bg-select: #2b3f57;
    --border: #33383d;
    --accent: #79b8ff;
    --danger: #f2777a;
    /* darker and not lighter: a shadow on a dark background is the absence of the
       little light there is, and a pale one would read as a glow */
    --shadow: rgb(0 0 0 / 55%);

    --diff-insert-bg: #12261a;
    --diff-insert-fg: #56d364;
    --diff-delete-bg: #2c1618;
    --diff-delete-fg: #f2777a;
    --diff-hunk-bg: #1e2530;
    --diff-hunk-fg: #8ba3c7;

    /* the same eight hues, lifted until they read against the background */
    --lane-0: #74b0f0;
    --lane-1: #63d68a;
    --lane-2: #ff9f5a;
    --lane-3: #cf9bec;
    --lane-4: #ff8080;
    --lane-5: #4fd1e0;
    --lane-6: #e8c35a;
    --lane-7: #ff85b0;
  }
}

/* The page is the viewport and never scrolls: the panes do, so that the bar at
   the top stays put while a commit list a thousand rows long moves under it. */
html,
body {
  height: 100%;
  overflow: hidden;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--mono);
  font-size: var(--size);
  line-height: 1.45;
}

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

a:hover {
  text-decoration: underline;
}

/* The mount, and the only layout in the file: a bar across the top, the commit
   list left with the rail in it, the divider, the commit itself right. */
#app {
  height: 100%;
  display: grid;
  grid-template-columns: var(--log-width) auto 1fr;
  grid-template-rows: auto 1fr;
  grid-template-areas:
    'chrome chrome chrome'
    'log split detail';
}

/* Not every view is two panes. The picker and a group listing are one column,
   and the modifier belongs here so the file still holds one grid. */
#app.one-pane {
  grid-template-columns: 1fr;
  /* named as well as the areas, because the narrow layout below adds a third row
     and this rule outranks it whatever the width */
  grid-template-rows: auto 1fr;
  grid-template-areas:
    'chrome'
    'main';
}

/* The frame before the first render, drawn by the mount rather than by markup in
   the shell, which would be a second source of truth for a DOM the view layer
   owns. :has(*) and not :empty, so a newline inside the mount does not decide
   whether the frame appears. Both go when the first view lands. */
#app:not(:has(*))::before {
  grid-area: log;
  content: '';
  border-right: 1px solid var(--border);
}

#app:not(:has(*))::after {
  grid-area: detail;
  content: '';
}

/* Which repository, which revision, and what the view has to say about the read. */
.chrome {
  grid-area: chrome;
  display: flex;
  align-items: baseline;
  gap: 1.5em;
  padding: 4px 10px;
  background: var(--bg-chrome);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  overflow: hidden;
}

.chrome .repo {
  font-size: inherit;
  font-weight: bold;
  margin: 0;
}

/* The rev in the bar, which is a control and no longer a label: the button that
   says where you are, and the panel of every branch and tag behind it. */
.revs {
  min-width: 0;
}

.rev {
  font: inherit;
  color: var(--fg-dim);
  background: none;
  border: 1px solid transparent;
  border-radius: 3px;
  padding: 0 4px;
  max-width: 24em;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* not in the label, because the rev is the button's text and a marker in it would
   be a character somebody copies out along with the branch name */
.rev::after {
  content: ' \25be';
}

.rev:hover,
.revs.open .rev {
  color: var(--fg);
  background: var(--bg-hover);
  border-color: var(--border);
}

.rev:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Fixed and not absolute: the bar clips what runs past it and this is meant to run
   past it, and a fixed box is not clipped by an ancestor's overflow. Where it goes
   is measured on open, since only the button knows where it is. Above .path, which
   is the one other thing on the page that leaves the flow. */
.rev-panel {
  position: fixed;
  z-index: 2;
  display: flex;
  flex-direction: column;
  min-width: 22em;
  max-width: min(48em, calc(100vw - 2em));
  max-height: min(60vh, 32em);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 3px;
  box-shadow: 0 4px 12px var(--shadow);
}

.revs:not(.open) .rev-panel {
  display: none;
}

/* No focus ring: the filter has the focus for as long as the panel is open, so a
   ring around it says nothing, and what the eye wants is the row the arrows are on. */
.rev-filter {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  border-bottom: 1px solid var(--border);
  padding: 4px 8px;
  outline: none;
}

/* min-height: 0 for the same reason the panes have it: the flex item's automatic
   minimum is its content, so without it the list grows past the panel's max height
   instead of scrolling inside it. */
.rev-list {
  margin: 0;
  padding: 0 0 4px;
  list-style: none;
  overflow: auto;
  min-height: 0;
}

/* Stuck to the top of the list while its own rows scroll past, the way a path
   header is in a diff: 239 branches is more than a panel shows at once and a list
   you have scrolled into should still say what kind you are looking at. */
.rev-kind {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 4px 8px 2px;
  background: var(--bg);
  color: var(--fg-dim);
  font-weight: bold;
}

/* A marker column and the name, so the names line up whether or not a row is the
   one being shown. The dot is drawn here rather than put in the text for the reason
   the diff's +/- is: what a copy of the row yields should be the branch name. */
.rev-row {
  display: grid;
  grid-template-columns: 1.5ch 1fr;
  gap: 0 1ch;
  padding: 0 8px;
  cursor: pointer;
  white-space: nowrap;
}

.rev-row::before {
  content: '';
}

.rev-row.current::before {
  content: '\2022';
  color: var(--fg-dim);
}

/* the anchor takes the row's color: a panel of 239 links in the link color is a
   wall of blue, and the row is what you are clicking anyway */
.rev-row > * {
  min-width: 0;
  color: inherit;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rev-row > a:hover {
  text-decoration: none;
}

.rev-row.current {
  background: var(--bg-select);
}

.rev-row:hover {
  background: var(--bg-hover);
}

/* Where the arrow keys are, which is not where the focus is: the focus stays in the
   filter so typing keeps working, and this is what aria-activedescendant names. */
.rev-row.active {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* A name no URL can spell, which takes a backslash in a ref or bytes that are not
   UTF-8 in a tree entry, and a submodule, whose commits are another repository's.
   The row is the name and nothing to click, so it reads as one that is there and
   cannot be gone to. */
.rev-row[aria-disabled],
.entry[aria-disabled] {
  color: var(--fg-dim);
  cursor: default;
}

/* Both panes scroll on their own. min-height: 0 is what allows it: a grid item's
   automatic minimum is its content, so without it a long list makes the row
   taller than the viewport instead of overflowing inside it. */
.pane {
  overflow: auto;
  min-height: 0;
}

.log {
  grid-area: log;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The divider, which draws the line the commit list used to carry as a border.
   It is padding with the background clipped to the content box, so the column
   is a hit area several pixels wide around one pixel of ink: a one-pixel target
   is a pixel hunt, and every other splitter on the desktop is forgiving here.

   The whole page stops selecting text while it is dragged. That belongs on the
   root and not on the divider, because the pointer is captured and therefore
   almost never over the divider during a drag. */
.split {
  grid-area: split;
  box-sizing: border-box;
  width: 7px;
  padding: 0 3px;
  cursor: col-resize;
  background: var(--border);
  background-clip: content-box;
}

.split:hover,
.split:focus-visible,
.split.dragging {
  background-color: var(--accent);
  outline: none;
}

:root:has(.split.dragging) {
  user-select: none;
}

.detail {
  grid-area: detail;
}

.main {
  grid-area: main;
  padding: 8px 10px;
}

/* The repository listing, which is the one-pane view: a link per repository and
   per group, then what the daemon knows about it and the day it last moved.
   Three columns and not three fields run together, because a listing is read
   down the names and across only once the eye has found its row.

   The list owns the columns and a row is a subgrid of them. That is what makes
   them line up across rows at all: a row with a grid of its own would size its
   columns to its own content and every row would land somewhere else.

   Every column is sized to its own content and none of them stretches, so the
   three sit together and the leftover width is left over. The description is
   the one that may shrink below that, because it is the one field a reader can
   lose the tail of and still know which repository the row is. */
.repos {
  display: grid;
  grid-template-columns: max-content minmax(0, max-content) max-content;
  gap: 0 1.5em;
  margin: 0;
  padding: 0;
  list-style: none;
}

.repos li {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
}

/* on the cells and not the row: a grid item is what has a width to overflow */
.repos li > * {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A row of either pane's list, and the columns of a commit: the rail, the
   abbreviated id, the subject, who and when. A tree entry is a row too and has
   two columns of its own, which .entry below overrides these with.

   The columns are the same width in every row so the eye can read down them, which
   is also why who and when are not sized to their content. The rail keeps its
   column while it is still empty, because widening a laid-out row afterwards is
   how a whole list shifts sideways.

   The id column is git's own abbreviation length and the date column is what
   YYYY-MM-DD HH:MM needs, so a list that spells either differently has to say so
   here too. */
.row {
  display: grid;
  grid-template-columns: var(--rail) 8ch 1fr 12ch 16ch;
  gap: 0 1ch;
  align-items: center;
  height: var(--row-height);
  padding: 0 8px;
  cursor: pointer;
  white-space: nowrap;
}

.row:hover {
  background: var(--bg-hover);
}

.row.selected {
  background: var(--bg-select);
}

.row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The rail is sized here and filled by the lane layout, an SVG the height of the
   row. Block, so it sits on no text baseline and leaves no gap under itself. */
.row .rail {
  display: block;
  width: var(--rail);
  height: var(--row-height);
}

/* One line per rail crossing the row, one circle for the commit. The drawer says
   which lane color each of them takes and says it once, as the color of the
   element; whether that paints a stroke or a fill is this file's business. The
   width is what reads as a rail at this row height without a history full of
   merges coming out as a wall of ink. */
.rail line {
  stroke: currentColor;
  stroke-width: 1.5px;
}

.rail circle {
  fill: currentColor;
}

.row .oid,
.row .who,
.row .when {
  color: var(--fg-dim);
}

.row .subject,
.row .who {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* One entry of a directory: a marker column and the name, and nothing else. No
   mode and no size, both of which the file pane has room for and neither of which
   is worth a column that is 100644 all the way down. */
.row.entry {
  grid-template-columns: 2ch 1fr;
}

/* The kind as ls -F spells it, drawn here rather than put in the text for the
   reason the diff's +/- is: what a copy of the row yields should be the name. An
   ordinary file gets no marker and still gets the column, because content: ''
   generates a box — how .rev-row::before already works. */
.entry::before {
  content: '';
  color: var(--fg-dim);
}

.entry.tree::before {
  content: '/';
}

.entry.exec::before {
  content: '*';
}

.entry.symlink::before {
  content: '@';
}

.entry.gitlink::before {
  content: '&';
}

/* the anchor takes the row's color, the same as a ref row: every entry is a link,
   so the link color would paint a whole directory blue and the row is what is
   being clicked anyway */
.entry > * {
  min-width: 0;
  color: inherit;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The path header of a listing wraps where a diff's does not: a diff names one
   file at a time and the pane is as wide as it is, while a crumb trail is as deep
   as the tree and the end of it is the part that says where you are. */
.crumbs {
  white-space: normal;
}

/* The commit: who wrote it and when, its message as written, then the paths. A
   row like the bar above it, but wrapping: an object id and a name are as long
   as they are, and a pane narrow enough to break them is one to break them in.

   A file's header is the same row with other fields in it — a mode, a size and the
   whole object id — so it joins the rule rather than restating it. */
.commit-head,
.file-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 1.5em;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
}

.commit-head .oid,
.commit-head .who {
  color: var(--fg-dim);
}

/* Dim all the way across, where a commit's header dims only two of its fields:
   nothing in here is the file, it is what the pane knows about the bytes below it. */
.file-head {
  color: var(--fg-dim);
}

.message {
  margin: 0;
  padding: 8px 10px;
  /* pre-wrap and not pre: a message is written to a width the pane need not have,
     and a wrapped line beats a horizontal scrollbar over the whole commit */
  white-space: pre-wrap;
}

/* One header per changed path, stuck to the top of the pane while its own hunks
   scroll past, so a long diff never leaves you reading a file you cannot name. */
.path {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 2px 10px;
  background: var(--bg-chrome);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Wide enough for R100, so that the paths start in the same column whatever
   happened to them and the list reads down. Inline, because .path stays a block
   and its ellipsis is over the whole line rather than over one flex item. */
.path .status {
  display: inline-block;
  min-width: 4ch;
  color: var(--fg-dim);
}

.hunk {
  padding: 0 10px;
  background: var(--diff-hunk-bg);
  color: var(--diff-hunk-fg);
}

/* One element per line, which is the point of the whole pane: a highlighter later
   replaces what is inside .text with token spans and touches nothing else. Two
   gutters, before and after, six characters because a file with a million lines
   is not one anybody reads in a browser. */
.line {
  display: grid;
  grid-template-columns: 6ch 6ch 1fr;
  gap: 0 1ch;
  padding: 0 10px;
}

.line .num {
  color: var(--fg-dim);
  text-align: right;
  /* a copied hunk should paste as the file, not as the file with its line
     numbers wedged into every line */
  user-select: none;
}

.line .text {
  white-space: pre;
}

/* The +, - and space belong to the presentation and not to the line, so they are
   drawn here rather than put in the text node: the text stays the file's bytes,
   which is what a highlighter tokenizes and what a copy of the pane yields. */
.line .text::before {
  content: ' ';
}

/* A file is the same line with one gutter, because it has one side, and with no
   marker in front of the text, because nothing in it is a change: a column of spaces
   all the way down would be a character in every copied line and nothing else. Both
   are here and not in the drawer, so the element a highlighter walks is one element
   in both panes. */
.file .line {
  grid-template-columns: 6ch 1fr;
}

.file .line .text::before {
  content: none;
}

.line.insert {
  background: var(--diff-insert-bg);
}

.line.insert .text::before {
  content: '+';
  color: var(--diff-insert-fg);
}

.line.delete {
  background: var(--diff-delete-bg);
}

.line.delete .text::before {
  content: '-';
  color: var(--diff-delete-fg);
}

/* Anything the view has to say in its own voice: a binary file, a diff over the
   size cap, git's no-newline marker, a walk that ran out of budget. */
.note {
  padding: 0 10px;
  color: var(--fg-dim);
}

.error {
  padding: 8px 10px;
  color: var(--danger);
  white-space: pre-wrap;
}

/* Narrow enough that two columns would leave neither readable: stack them, the
   list over the commit, each still scrolling on its own. */
@media (max-width: 60em) {
  #app {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr 1fr;
    grid-template-areas:
      'chrome'
      'log'
      'detail';
  }

  .log {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}
