/* ===========================================================================
   Reset, typography, and the application frame.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   The faces. Self-hosted woff2, served from assets/fonts/, licences alongside
   them in assets/fonts/LICENSES.txt.

   These used to come from fonts.googleapis.com. They do not any more, and the
   reason is not page weight. This console is reached by analysts, it carries
   no identifying copy on purpose, and a webfont <link> announced every one of
   their browsers — IP, User-Agent, Referer — to a third party on every single
   page load. A console that hides what it is cannot ask a CDN to witness who
   opens it. Nothing on this page is allowed to be off-origin.

   TWO of the three are subset, and the third is NOT, for a licence reason that
   is easy to get wrong. Commit Mono and Space Grotesk are OFL-1.1 with no
   Reserved Font Name, so a latin subset is an ordinary permitted modification.
   Switzer is not OFL: it is the ITF Free Font License, which permits
   self-hosting outright (FFL v2.0 §01) and then forbids, in §02, "modifying or
   replacing glyphs, SUBSETTING, format conversion, or altering font names" —
   so switzer-variable.woff2 below is the file out of the ITF bundle, byte for
   byte, sha256 d1bf801f…91f8ee4. Subsetting it would have saved about 15 KB
   and broken the licence. If it is ever regenerated, copy the bundle file;
   do not run it through pyftsubset.
   ------------------------------------------------------------------------- */

/* Switzer (Fontshare / ITF Free) — the UI face. ONE variable file carrying the
   whole wght 100-900 axis, which is both lighter than the four statics the
   Fontshare CSS endpoint serves (43 KB against 75 KB unsubset) and means every
   weight the product asks for is a real master rather than an interpolation of
   two. The src is a URL and never local(): the four statics Fontshare's CSS
   endpoint serves have a broken name table — name IDs 1, 4 and 6 are each the
   literal string "false" — and this file, from the ITF Free bundle, names
   itself "Switzer Variable" rather than "Switzer". Neither would answer a
   local() lookup for the family this rule declares, so none is attempted; the
   family name below is ours, and only this file supplies it. */
@font-face {
  font-family: Switzer;
  src: url("../fonts/switzer-variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  /* No unicode-range. The split a CDN does here needs two subset files, and
     subsetting this face is the one thing its licence forbids. The upside of
     paying for the whole file is that latin-ext comes with it, so a Polish or
     Turkish filename stays in the interface face instead of falling back. */
}

/* Commit Mono (OFL-1.1) — the identifier face. Statics only; the project ships
   no variable build. 400 and 500 are the two weights anything monospaced on
   this screen actually computes to, which is exactly what the CDN used to
   serve for Plex Mono. Its advance is 600/1000 em, identical to Plex Mono's,
   so every sha256 column keeps the width it had. */
@font-face {
  font-family: "Commit Mono";
  src: url("../fonts/commitmono-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Commit Mono";
  src: url("../fonts/commitmono-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* Space Grotesk (OFL) — the display face, and a SINGLE 700 weight. It is
   declared at 700 and nowhere asked for at any other weight; see the
   --f-display note in tokens.css and the h1.t1 rule below. Subset to ASCII
   because the only strings it ever renders are the wordmark and the nine
   hard-coded page titles.

   font-display is BLOCK here, not swap, and it is the one face on this page
   that gets it. The other two carry running text and a swap-in is the right
   trade: text you can already read, reflowing once. This face carries exactly
   two things, and both are the largest type on their screen — the wordmark at
   up to 64.5px and the page title at 34px. Swapping those from the fallback
   grotesque to the real one is a visible jump at the top of the page, on the
   element the eye is already on. The file is 6.6 KB from the same origin as
   the document and is preloaded in index.html, so the block period is a
   same-connection fetch, not a round trip to a CDN.

   And block is safe against the failure this deployment has actually hit
   once — the container not being recreated, so the font 404s. A block period
   is a CEILING, not a wait: a load that FAILS ends it immediately and the
   fallback paints, which for a same-origin 404 is the next few milliseconds.
   The 3s only ever applies to a fetch still in flight. */
@font-face {
  font-family: "Space Grotesk";
  src: url("../fonts/spacegrotesk-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: block;
}

*,
*::before,
*::after { box-sizing: border-box; }

/* No synthesised faces, anywhere, ever.
   The display subset ships one weight, 700. Asking it for semibold — which
   .t1 does — makes the browser smear the 700 outlines sideways into a fake
   bolder, which at 34px is immediately visible as a broken font rather than
   as emphasis. The same guard stops the browser faking an italic or a bold
   for Commit Mono at a weight it does not carry. Weight and slant on this
   screen are always real masters or they are not applied. */
html { font-synthesis: none; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--abyss);
  color: var(--ice);
  font-family: var(--f-sans);
  font-size: var(--t-body);
  font-weight: var(--w-regular);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Numbers on this screen exist to be compared down a column, so they are
     tabular everywhere by default and never reflow as they tick. */
  font-variant-numeric: tabular-nums;
  text-rendering: optimizeLegibility;
  transition: background-color var(--d-base) var(--e-standard),
              color var(--d-base) var(--e-standard);
}

h1, h2, h3, h4, p, figure, blockquote { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }

button, input, select, textarea {
  font: inherit;
  color: inherit;
  font-variant-numeric: inherit;
}

button { background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }

/* Focus is always visible and always the same shape, so keyboard users learn
   it once. Never removed — only replaced with something better. */
:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
  border-radius: var(--r-control);
}
:focus:not(:focus-visible) { outline: none; }

::selection { background: var(--signal-soft); color: var(--ice); }

/* ---- scrollbars: present but quiet ------------------------------------ */
* { scrollbar-width: thin; scrollbar-color: var(--hair-strong) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--hair-strong);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: var(--r-chip);
}
*::-webkit-scrollbar-thumb:hover { background-color: var(--dim); background-clip: content-box; }

/* ===========================================================================
   Type roles
   ========================================================================= */

.display {
  font-size: var(--t-display);
  font-weight: var(--w-semibold);
  line-height: 1.05;
  letter-spacing: -0.022em;
}
.t1 { font-size: var(--t-t1); font-weight: var(--w-semibold); line-height: 1.15; letter-spacing: -0.018em; }

/* The display face, on the page title and nowhere else in the frame. The other
   half of its territory is the login wordmark, in auth.css.
   Not buttons, not table headers, not labels and not pills: a 700 grotesque
   at 12px is weight without size, which reads as emphasis nobody asked for.

   The rail's wordmark at 17px is a different case and worth saying plainly,
   because the reason changed when the face did. It stays in --f-sans, so the
   product name is set in Switzer there and in Space Grotesk on the sign-in
   screen. Under the serif that was forced — a 0.72 cap-height face with that
   much stroke contrast is illegible at 17px. This face would hold up fine at
   17px, so it is now a CHOICE and not a constraint, and an arguable one.
   Setting the rail in the display face would tie the two screens together;
   it would also put the display face on something the eye passes over on
   every page rather than on the two moments it was reserved for. Left as it
   is for now, deliberately, and open to being revisited.

   34px rather than --t-t1's 28px, and the number is measured rather than
   chosen. Space Grotesk 700 has a rendered x-height of 0.496 em against
   Switzer's 0.531, so at a matched px it reads slightly small; 34px brings
   its x-height to 16.9px where the 28px sans sits at 14.9px, which is the
   step up a page title wants over body copy without becoming a banner.

   It is also DOWN from the 36px the serif it replaces was set at, and that is
   the same correction read backwards. The serif was pushed up because its
   lowercase advance was 0.392 em; this face runs 0.616, so the identical
   string is half again as wide and the previous size overflowed the measure
   on a narrow column. Tracking tightens to -0.03em for the same reason: a
   geometric sans at display size has open counters and wide sidebearings,
   and -0.018em was leaving the word looking spaced rather than set.

   font-weight is the load-bearing line. .t1 sets --w-semibold and the subset
   carries only a 700, so this rule plus `font-synthesis: none` above is what
   keeps the browser from inventing a heavier face. Both are needed: the
   global guard stops the smear, this asks for the weight that exists. */
h1.t1 {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 34px;
  line-height: 1.1;
  letter-spacing: -0.03em;
}
/* Record headings — a filename, a rule id, a bot name, a chat title — are .t2
   and stay in the UI face. They are data, and the display face is not allowed
   to carry data; see the --f-display note in tokens.css. */
.t2 { font-size: var(--t-t2); font-weight: var(--w-semibold); line-height: 1.2; letter-spacing: -0.012em; }
.t3 { font-size: var(--t-t3); font-weight: var(--w-semibold); line-height: 1.25; letter-spacing: -0.008em; }
.headline { font-size: var(--t-headline); font-weight: var(--w-semibold); line-height: 1.3; letter-spacing: -0.004em; }
.body { font-size: var(--t-body); line-height: 1.5; }
.foot { font-size: var(--t-foot); line-height: 1.4; }
.cap { font-size: var(--t-cap); line-height: 1.35; }

.prose { max-width: var(--max-measure); }

.secondary { color: var(--mist); }
.tertiary { color: var(--dim); }

/* A machine-generated identifier.
   The ratio is derived, not picked. IBM Plex Sans and IBM Plex Mono share an
   x-height of 0.516 em, so 0.92em put a sha256 eight percent under the optical
   size of the sans beside it. Switzer is 0.531 and Commit Mono is 0.540, so
   holding that same relationship means 0.92 x 0.531 / 0.540 = 0.905. Matching
   the x-heights outright (0.983em) would have been the literal reading and
   would have grown every identifier on the screen by seven percent, which in
   a 6-column file table is a layout change, not a type change. */
.mono {
  font-family: var(--f-mono);
  font-size: 0.905em;
  letter-spacing: -0.01em;
}

.num { font-variant-numeric: tabular-nums; }

/* ===========================================================================
   The frame
   ---------------------------------------------------------------------------
     rail  |  vitals  (fixed, always visible)
           |  view    (scrolls)
   The vitals strip is pinned because "is the pipeline alive" is a question
   you have while doing something else. Giving it its own page would mean
   leaving whatever you were doing to answer it.
   ========================================================================= */

.app {
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr);
  height: 100vh;
  height: 100dvh;
  /* Nothing may escape the viewport. Without this the document itself becomes
     the scroller and takes the rail and the vitals strip with it. */
  overflow: hidden;
  transition: grid-template-columns var(--d-base) var(--e-standard);
}
.app[data-rail="collapsed"] { grid-template-columns: var(--rail-w-collapsed) minmax(0, 1fr); }

.main {
  /* The dock is positioned against this, so it spans the content area and
     never covers the rail. */
  position: relative;
  display: grid;
  grid-template-rows: var(--vitals-h) minmax(0, 1fr);
  min-width: 0;
  /* min-height: 0 is the load-bearing line.
     A grid item's default min-height is AUTO, which means "never smaller than
     my content". So .main grew to the full height of the view's content, blew
     past the 100dvh on .app, and the page scrolled as one document — the rail
     and the vitals strip scrolling away with it, which is exactly what they
     exist not to do. The 1fr row cannot constrain a child that refuses to
     shrink; this is what lets it. */
  min-height: 0;
}

.view {
  overflow-y: auto;
  overflow-x: hidden;
  /* The dock overlays the bottom of the view, so the view reserves that much
     extra padding — otherwise the last section can never be scrolled clear. */
  padding: var(--s6) var(--s6) calc(var(--s8) + var(--dock-space, 0px));
  scroll-behavior: smooth;
  /* Same trap, one level down. */
  min-height: 0;
}

@media (max-width: 900px) {
  .app, .app[data-rail="collapsed"] { grid-template-columns: var(--rail-w-collapsed) minmax(0, 1fr); }
  .view { padding: var(--s4) var(--s4) var(--s7); }
}

/* Page header: a title, a one-line explanation of what the screen is for,
   and the controls that belong to it. */
.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s5);
  margin-bottom: var(--s5);
  flex-wrap: wrap;
}
.page-head__why {
  color: var(--mist);
  font-size: var(--t-foot);
  margin-top: var(--s1);
  max-width: var(--max-measure);
}
.page-head__tools { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }

/* ---- generic layout helpers ------------------------------------------- */
.row { display: flex; align-items: center; gap: var(--s2); }
.row--between { justify-content: space-between; }
.row--wrap { flex-wrap: wrap; }
.col { display: flex; flex-direction: column; }
.grow { flex: 1 1 auto; min-width: 0; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stack > * + * { margin-top: var(--s4); }
.stack--tight > * + * { margin-top: var(--s2); }
.stack--loose > * + * { margin-top: var(--s6); }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
