/* Cockpit shell. One stylesheet, no bundler and no build step.

   TWO FAMILIES OF COLOUR, and the whole readability of this interface rests on not mixing them.

   A STATE hue is a verdict. There are five, they are the most saturated colours on screen, and
   they appear only where the application is asserting something: a card's dot and figure, a
   sidebar chip, a finding cell, a heading that counts findings. If a state hue is visible,
   something claimed it.

   An ACCENT hue is navigation. It says "this surface", "this leads somewhere", "this has focus" --
   never "this is fine" or "this is broken". Each surface has its own, they are deliberately muted
   against the state hues, and they appear on structure rather than on data: a filet, an underline,
   a focus ring, a control's edge.

   That split is what lets this file carry colour at all: the answer to a wall of grey is an accent
   that belongs to the page, never more state colour, which would mean more things claiming
   verdicts. */
:root {
  --bg: #101014;
  --panel: #191920;
  --panel-head: #22222b;
  --line: #32323d;
  /* --faint carries the table's dim cells, the card foot, the page meta and the refresh note,
     which is most of the text on a detail page: quiet is a ratio, not a fade. */
  --text: #f3f3f6;
  --dim: #a0a0ad;
  --faint: #7b7b88;

  --ok: #3dd68c;
  --warn: #ffb224;
  --fail: #ff5c61;
  --silent: #a78bfa;
  --unknown: #7d7d88;

  /* The navigation accent, redefined per surface below. Named here rather than repeated as a
     literal on the active nav item, the tile hover and the focus ring, which would let one of the
     three disagree with the others. */
  --accent: #7c9cff;
  /* The same hue at about a fifth of its strength over the panel, for a filet or a resting edge
     that has to be visible without competing with the text beside it.

     A literal and not color-mix(): an unsupported function makes a custom property
     guaranteed-invalid, so `border-left: 2px solid var(--accent-soft)` falls back to currentColor
     -- a filet the colour of the text on every browser that lacks it, and this project has no
     build step to catch that. */
  --accent-soft: #2d344e;
}

/* One hue per surface, keyed on the path, which is the same thing the sidebar reads to mark its
   active item. The hues sit away from all five state hues and stay lighter and less saturated than
   them, so a verdict is always the loudest colour on the page.

   Home keeps the default: it is the only page that shows every subject at once, so a hue of its
   own would compete with the state colours that are the whole point of the grid. */
body.surface-scheduled_pipelines { --accent: #7c9cff; --accent-soft: #2d344e; }
body.surface-team_merge_requests { --accent: #3fc0d0; --accent-soft: #203c44; }
body.surface-team_roster         { --accent: #d98fc4; --accent-soft: #423141; }
body.surface-collection_health   { --accent: #8fa2bd; --accent-soft: #31363f; }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /* Every size below is relative to this one in practice, and the tables are what suffer first:
     small dim text on a tight line down a hundred rows reads as monotone before any palette
     does. */
  font: 14px/1.5 ui-sans-serif, system-ui, sans-serif;
}

.shell { display: flex; min-height: 100vh; }

.sidebar {
  width: 208px;
  flex: none;
  background: #15151a;
  border-right: 1px solid var(--line);
  padding: 14px 0;
}

.brand {
  padding: 0 14px 12px;
  font-size: 14px;
  font-weight: 680;
  border-bottom: 1px solid var(--line);
}
.nav-label {
  padding: 13px 14px 5px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .7px;
  color: #6d6d79;
}
.nav-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px;
  font-size: 13px;
  color: var(--dim);
  text-decoration: none;
  /* Transparent rather than absent, so the active rule below swaps a colour instead of
     adding a border -- which would shift every label two pixels on navigation. */
  border-left: 2px solid transparent;
}
.nav-item:hover { background: #1c1c23; color: var(--text); }
.nav-item.active {
  background: var(--panel-head);
  color: var(--text);
  border-left-color: var(--accent);
  font-weight: 600;
}
.nav-count {
  margin-left: auto;
  font-size: 10.5px;
  font-weight: 680;
  border-radius: 20px;
  padding: 1px 7px;
}
/* An information section has no state, so it has no chip: the empty right edge is the first
   distinction and this quieter label the second. Deliberately not a colour -- any hue here would
   read as the state it does not have. */
.nav-section { color: #8f8f9b; }

.main { flex: 1; padding: 18px 22px; min-width: 0; }

/* Collection failures, at the top of every page and only when there are some. There is no healthy
   variant of this element on purpose: an indicator that is always present stops being read. Red
   because the figures below it may be frozen, whatever their own colours say. */
.banner {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
  padding: 8px 12px;
  border-radius: 8px;
  font-size: 12.5px;
  color: inherit;
  text-decoration: none;
}
.banner-alert {
  background: #2a1519;
  border: 1px solid #6b3238;
}
.banner-alert:hover { border-color: var(--fail); }
.banner-mark { color: var(--fail); font-size: 13px; line-height: 1; }

/* The page's title, over its surface's accent. The rule under the heading is the one
   place the accent is unmissable, and it is what tells two detail pages apart before a
   word of either is read -- which is the whole reason each surface has a hue. */
.page-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 4px 16px;
  margin-bottom: 18px;
  padding-bottom: 9px;
  border-bottom: 2px solid var(--accent-soft);
}
.page-head h1 { margin: 0; font-size: 19px; font-weight: 660; letter-spacing: -.2px; }
.page-meta { font-size: 11.5px; color: var(--faint); }

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
  gap: 12px;
}
.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  display: block;
}
.card:hover { border-color: #454553; }
.card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  background: var(--panel-head);
  border-bottom: 1px solid var(--line);
}
.card-title { font-size: 12.5px; font-weight: 640; }
.card-body { padding: 15px 12px; }
/* Source, state and freshness: three items on a card narrow enough that they are allowed to wrap
   rather than overflow the frame. */
.card-foot {
  padding: 7px 12px;
  border-top: 1px solid #2b2b34;
  font-size: 10.5px;
  color: var(--faint);
  display: flex;
  flex-wrap: wrap;
  gap: 2px 8px;
  justify-content: space-between;
}
.card-figure { text-align: center; }
.card-value { font-size: 31px; font-weight: 700; line-height: 1; }
.card-caption { font-size: 11px; color: var(--dim); margin-top: 7px; }

/* For a card body that breaks its figure down into named parts: one chip per
   part. Styled by the shell rather than by the module emitting it, so every
   breakdown reads the same whichever subject it belongs to. */
.card-breakdown {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 5px;
}
.card-chip {
  background: var(--panel-head);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 1px 8px;
  font-size: 11px;
}

/* Section tiles: a figure and a label, and nothing that could be read as a state. Not card-shaped
   on purpose -- no panel, no head, no foot, laid along the line instead of stacked -- so the two
   families are told apart by shape before any colour is read.

   The filet is the accent at rest, which is the second thing separating a tile from a card, a card
   having none.

   What keeps a state colour out is upstream, not here: routes.pages tiles() emits no state class.
   The filet is a plain border, so a .state-border-* landing on a tile would tint it. */
.tiles {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  margin-top: 20px;
  padding-top: 15px;
  border-top: 1px solid var(--line);
}
.tile {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 2px 0 2px 10px;
  border-left: 2px solid var(--accent-soft);
  color: inherit;
  text-decoration: none;
}
.tile:hover { border-left-color: var(--accent); }
.tile-figure { font-size: 22px; font-weight: 700; line-height: 1; }
.tile-label { font-size: 11px; color: var(--dim); }

/* The team roster: faces and names, and nothing that could be read as a state, a section having
   none.

   Its own grid rather than .cards: a roster card is a line of text beside a face, not a figure in
   a panel, and the two families must not resolve to the same object at a glance. Same track width
   as the cards, so the two grids line up down the page. */
.roster {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
  gap: 10px;
}
.roster-card {
  display: grid;
  /* The face in a fixed column, spanning both text rows; the text takes the rest. */
  grid-template-columns: 38px 1fr;
  align-items: center;
  gap: 1px 11px;
  padding: 10px 12px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  color: inherit;
  text-decoration: none;
  min-width: 0;
}
.roster-card:hover { border-color: #454553; }
.avatar-wrap { grid-row: 1 / 3; }
.avatar,
.avatar-fallback {
  width: 38px;
  height: 38px;
  border-radius: 50%;
}
.avatar { display: block; object-fit: cover; }
/* Shown when GitLab does not serve the picture, which the browser only discovers after this page
   was rendered -- hence the img's own onerror in the template, and a neutral disc rather than the
   browser's broken-image icon.

   The hue arrives from the template as `--avatar-hue`, computed from the username so the same
   colleague keeps the same disc between renders. Decoration and never a claim: every disc is one
   lightness and one saturation, and hue alone encodes no verdict anywhere in this file.

   The fallback hue keeps the disc grey, so a page that stopped emitting the property still
   renders. */
.avatar-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  background: hsl(var(--avatar-hue, 240) 24% 30%);
  border: 1px solid hsl(var(--avatar-hue, 240) 26% 42%);
  color: hsl(var(--avatar-hue, 240) 30% 84%);
  font-size: 12.5px;
  font-weight: 650;
  letter-spacing: .3px;
}
/* The name as GitLab renders it, which can be long: it wraps rather than overflowing the card,
   because a truncated colleague's name is what this page refuses everywhere else. */
.roster-name { font-size: 13px; font-weight: 620; overflow-wrap: anywhere; }
.roster-meta { font-size: 11px; color: var(--faint); }

/* Group headings, shared by the merge request pages and the schedules' two tables: one heading
   above the shared table.data shape, rather than a repeated column, because the grouping is what
   those pages are for and a repeated cell reads as data instead of as structure. One rule for both
   geometries, so a restyle of either cannot leave them different.

   No state colour in these rules, and that is what lets them be shared: the schedules heading's
   label carries a state class and a merge request heading must never, "seven to look at" being the
   verdict that page delivers where "twelve open" is a fact the reader interprets. The hue lives on
   the label span in the markup, so each page decides whether it puts one there. */
.mr-group + .mr-group,
.table-group + .table-group { margin-top: 24px; }
.mr-person,
.table-heading {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 3px 8px;
  margin: 0 0 8px;
  font-size: 13.5px;
  font-weight: 640;
}
/* The count qualifies the name rather than labelling it, so it is quieter -- but in a pill,
   because it is a figure among words and the shape is what makes it scannable. */
.mr-person-count,
.table-heading-count {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--dim);
  background: var(--panel-head);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 0 7px;
}
/* The table under a heading is already spaced by table.data's own margin-top, so the
   heading sits directly on the group. Only the worded empty state needs the gap, since it
   replaces a table and inherits none of its spacing. */
.table-group .empty { margin: 0; }
/* The marks a table row can wear, in one rule because they have to stay identical. Each says "a
   fact about this row, not a verdict on it", which is why none of them may ever take a state hue:

   .mr-draft -- "not ready for review" is a fact about the merge request, not a finding.
   .health-phase -- that line is the cause of the ones under it, and a state colour would rank it
     as one more of them.
   .status-pill -- the cell beside it already carries the verdict, and the two do not always agree,
     `success` on a schedule past its next run being still `overdue`. What the pill adds is shape,
     which is what makes a status scannable down a hundred rows without claiming anything. */
.mr-draft,
.health-phase,
.status-pill {
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 0 6px;
  margin-left: 4px;
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: .5px;
  color: var(--dim);
  white-space: nowrap;
}
/* The status pill has a cell of its own rather than hanging off a word, and carries the panel head
   behind it: at a hundred rows the fill is what separates the column from the text around it. */
.status-pill {
  display: inline-block;
  margin-left: 0;
  background: var(--panel-head);
}

/* The cockpit's own failures. Two halves of one question on one page, so each carries a heading
   and a one-line hint: a collection error sends the reader to the app, the network or the token,
   and a rejection sends them to the producer.

   Nothing here carries a state colour, the section having no state at all: a rejection is a
   producer's failure and the affected subject already expresses it through its own unknown. */
.health-half { margin: 0 0 5px; font-size: 13.5px; font-weight: 640; }
.health-half ~ .health-half { margin-top: 26px; }
/* What to do with the table under it, which is the only reason the two halves share a
   page. Quieter than the heading, because it qualifies it rather than labelling it. */
.health-hint { margin: 0 0 10px; font-size: 11.5px; color: var(--faint); }
/* .health-phase is declared with .mr-draft above, in the one pill rule. */
/* Not a producer name, so it does not wear the monospace face the real ones do: reading it as a
   project called "the payload named no producer" is the one confusion this cell can cause. */
.health-unattributed { font-family: ui-sans-serif, system-ui, sans-serif; color: var(--faint); }

/* The default outline is a thin dark ring that vanishes against these panels, and the cockpit has
   to be navigable without a mouse. One rule for every focusable element, because a control that
   answers a keyboard while showing nothing is the case this exists for. */
.card:focus-visible,
.nav-item:focus-visible,
.banner:focus-visible,
.tile:focus-visible,
.roster-card:focus-visible,
.row-action:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }

.state-ok      { color: var(--ok); }
.state-warn    { color: var(--warn); }
.state-fail    { color: var(--fail); }
.state-silent  { color: var(--silent); }
.state-unknown { color: var(--unknown); }

.state-bg-ok      { background: var(--ok); }
.state-bg-warn    { background: var(--warn); }
.state-bg-fail    { background: var(--fail); }
.state-bg-silent  { background: var(--silent); }
.state-bg-unknown { background: var(--unknown); }

.state-border-fail   { border-color: #6b3238; }
.state-border-warn   { border-color: #695122; }
.state-border-silent { border-color: #4a3a7c; }

.env-strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 10px;
  margin-bottom: 16px;
}
.env-cell {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 12px;
  text-align: center;
}
.env-count { font-size: 24px; font-weight: 700; line-height: 1; }
.env-name { font-size: 11px; color: var(--dim); margin-top: 5px; }
/* How old the count above is: quieter than the name, because it qualifies the figure rather than
   labelling it -- but present, since a count with no age reads as current however stale it is. */
.env-age { font-size: 10.5px; color: var(--faint); margin-top: 3px; }

table.data { width: 100%; border-collapse: collapse; font-size: 12.5px; }
table.data th {
  text-align: left;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .6px;
  color: var(--faint);
  font-weight: 620;
  padding: 0 7px 8px;
  border-bottom: 1px solid var(--line);
}
table.data td { padding: 8px 7px; border-bottom: 1px solid #26262f; }
/* A hundred rows is the length at which the eye loses the line it is on. Panel rather than an
   accent tint: hovering a row says nothing about the row, so it gets no hue. */
table.data tbody tr:hover { background: var(--panel); }
table.data td.mono { font-family: ui-monospace, monospace; font-size: 11.5px; color: #c2c2cd; }
table.data td.dim { color: var(--dim); font-size: 11.5px; }
table.data td.num { text-align: right; }

/* The refresh button on a table row: it re-reads one row on demand, which is what keeps a daily
   sweep honest rather than silently stale.

   The accent on its edge and its label, because a grey control on a grey panel behind a grey border
   is invisible on this page. A state hue here would claim a verdict about the row; there is never
   one on this button. Not a call to action either -- the fill arrives on hover only.

   The font is restated because a button takes the browser's and not the document's: those three
   declarations are what stop one column rendering in the browser's default face. */
.row-action {
  font: inherit;
  font-size: 11px;
  font-weight: 560;
  color: var(--accent);
  background: transparent;
  border: 1px solid var(--accent-soft);
  border-radius: 6px;
  padding: 3px 10px;
  cursor: pointer;
}
.row-action:hover:enabled { background: var(--accent-soft); border-color: var(--accent); }
/* Disabled for the length of one request, so a second click cannot be fired into the first one's
   answer -- and it has to look inert while it is, since a control that ignores a click without
   showing why reads as broken. */
.row-action:disabled {
  cursor: default;
  color: var(--faint);
  border-color: var(--line);
  background: transparent;
}
/* What the last click did, said on the row it was made on. Under the button rather than beside it,
   and allowed to break anywhere: a GitLab error body runs to a hundred characters in a cell that
   shares the table's width with seven others. */
[data-refresh-note] {
  display: block;
  margin: 4px 0 0 auto;
  max-width: 22ch;
  font-size: 10.5px;
  color: var(--faint);
  overflow-wrap: anywhere;
}

.empty { color: var(--dim); }

/* The page head's right-hand side, when a surface puts a control there beside its meta line.
   Baseline-aligned so the button sits on the text rather than above it, and allowed to wrap. */
.page-head-aside {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
}
