/* Baseball Card Studio.
 *
 * The Akamai 2025 brand, written out as CSS custom properties. The design
 * system's React library is not installed on this box, so the tokens and the
 * markup are mirrored here in plain CSS. Nothing in this file fetches anything:
 * the two fonts are served from /app/fonts, the logo from /app/brand.
 *
 * Rules that hold everywhere below:
 *   - colour comes from a token, never a literal hex
 *   - spacing is a multiple of 4 px, from --ak-space-*
 *   - radii are 12 px, borders are 1 px of --ak-border
 *   - numbers are JetBrains Mono with tabular figures, so they never jump
 */

/* ---------------------------------------------------------------- fonts -- */

@font-face {
  font-family: "Instrument Sans";
  src: url("/app/fonts/InstrumentSans.ttf") format("truetype-variations");
  font-weight: 400 700;
  font-stretch: 75% 100%;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("/app/fonts/JetBrainsMono.ttf") format("truetype-variations");
  font-weight: 100 800;
  font-display: swap;
}

/* --------------------------------------------------------------- tokens -- */

:root {
  /* Brand, raw. Navy, white and blue are the core. Orange is one accent at a
     time, and green and red only report state. */
  --ak-navy: #002f6c;
  --ak-blue: #00a4eb;
  --ak-orange: #ff8b00;
  --ak-green: #21c08b;
  --ak-red: #ff5c5c;

  /* Surfaces. Dark means navy, not black. */
  --ak-bg: #00142e;
  --ak-surface: #00214c;
  --ak-surface-2: #002f6c;
  --ak-surface-3: #013b85;

  --ak-text: #eaf2fb;
  --ak-text-muted: #8fa9c7;
  --ak-border: #0b3b77;
  --ak-border-strong: #14559f;

  --ak-primary: var(--ak-blue);
  --ak-accent: var(--ak-orange);
  --ak-link: var(--ak-blue);
  --ak-success: var(--ak-green);
  --ak-warning: var(--ak-orange);
  --ak-danger: var(--ak-red);

  /* Code on the Models page, coloured by highlight.js. Blue and white are the
     core, orange marks the literal values, and the rest is the page's own
     muted text. */
  --ak-code-text: var(--ak-text);
  --ak-code-muted: var(--ak-text-muted); /* comments, punctuation */
  --ak-code-keyword: var(--ak-blue); /* keywords, curl's flags */
  --ak-code-string: color-mix(in srgb, var(--ak-blue) 45%, var(--ak-text));
  --ak-code-literal: color-mix(in srgb, var(--ak-orange) 80%, var(--ak-text)); /* numbers, true, null */
  --ak-code-name: var(--ak-text); /* functions and classes, in semibold */

  /* Type */
  --ak-font-sans: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --ak-font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;

  --ak-text-display: 3.25rem;
  --ak-text-h1: 2rem;
  /* Panel titles. 24 px at the 16 px root, 27 px from 1400 px up. */
  --ak-text-h2: 1.5rem;
  --ak-text-h3: 1.0625rem;
  --ak-text-title: 0.9375rem;
  --ak-text-lead: 1rem;
  --ak-text-body: 0.875rem;
  --ak-text-small: 0.8125rem;
  --ak-text-caption: 0.6875rem;
  /* The millisecond counter on a step tile. Body size here, 24 px from
     1400 px wide and 28 px from 1600, where the tiles are wide enough for
     "4321 ms" at that size. */
  --ak-text-counter: var(--ak-text-body);

  --ak-weight-regular: 400;
  --ak-weight-medium: 500;
  --ak-weight-semibold: 600;
  --ak-weight-bold: 700;

  /* Space, on a 4 px base */
  --ak-space-1: 4px;
  --ak-space-2: 8px;
  --ak-space-3: 12px;
  --ak-space-4: 16px;
  --ak-space-5: 20px;
  --ak-space-6: 24px;
  --ak-space-8: 32px;
  --ak-space-10: 40px;
  --ak-space-12: 48px;
  --ak-space-16: 64px;

  --ak-radius-sm: 6px;
  --ak-radius-md: 12px;
  --ak-radius-lg: 20px;
  --ak-radius-full: 999px;

  --ak-shadow-sm: 0 1px 2px rgb(0 8 20 / 40%);
  --ak-shadow-md: 0 6px 20px rgb(0 8 20 / 45%);
  --ak-shadow-lg: 0 18px 48px rgb(0 8 20 / 55%);

  /* Motion. One system: every transition in this file and every
     element.animate() in the app use these, and web/app/motion.js is where
     JavaScript reads them. Nothing else writes a duration down. */
  --ak-dur-fast: 150ms; /* hover, focus, colour */
  --ak-dur-base: 200ms; /* numbers, bars, chips */
  --ak-dur-slow: 350ms; /* the lightbox, a card dealing in, a row folding */
  --ak-dur-flip: 500ms; /* the card flip */
  --ak-dur-pulse: 1200ms; /* the running dot's heartbeat. A loop, not a move */
  --ak-ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* everything that ends on screen */
  --ak-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* the flip, and the heartbeat */

  --ak-focus: 0 0 0 2px var(--ak-bg), 0 0 0 4px var(--ak-blue);

  color-scheme: dark;
}

/* The reader asked for less motion, so nothing moves. No transition starts
   and no animation runs, and a duration still reads 1 ms to anything that
   asks. JavaScript checks the same setting before it animates (motion.js). */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-name: none !important;
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-property: none !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
  }

  /* The opened part of a <details> is a pseudo-element that * does not
     reach. A rule of its own, so a browser that does not know the selector
     drops this one and keeps the rule above. */
  ::details-content {
    transition-property: none !important;
  }
}

/* ----------------------------------------------------------------- base -- */

* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--ak-bg);
  color: var(--ak-text);
  font-family: var(--ak-font-sans);
  font-size: var(--ak-text-body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3 {
  margin: 0;
  font-weight: var(--ak-weight-semibold);
  letter-spacing: -0.01em;
  line-height: 1.2;
}

p {
  margin: 0;
}

code {
  font-family: var(--ak-font-mono);
  font-size: 0.9em;
  color: var(--ak-text);
}

.num {
  font-family: var(--ak-font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

[hidden] {
  display: none !important;
}

:focus-visible {
  outline: none;
  box-shadow: var(--ak-focus);
  border-radius: var(--ak-radius-sm);
}

.logo {
  height: 26px;
  width: auto;
  display: block;
}

.logo-sm {
  height: 20px;
  opacity: 0.8;
}

.claim {
  margin: 0;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-caption);
  letter-spacing: 0.04em;
  word-spacing: 0.12em;
  text-transform: uppercase;
}

/* The wave. Behind the sign-in screen and the header, nowhere else. The
   sign-in card clears the crest: the brand guide asks for a clear zone around
   the motif, so it sits in the bottom third and nothing lands on it. */
.wave {
  position: absolute;
  inset: auto 0 0 0;
  width: 100%;
  height: 38%;
  pointer-events: none;
  z-index: 0;
}

.wave-a {
  fill: var(--ak-navy);
  opacity: 0.55;
}

.wave-b {
  fill: var(--ak-blue);
  opacity: 0.1;
}

/* --------------------------------------------------------------- button -- */

.btn {
  appearance: none;
  border: 1px solid transparent;
  border-radius: var(--ak-radius-md);
  padding: var(--ak-space-2) var(--ak-space-4);
  font-family: inherit;
  font-size: var(--ak-text-body);
  font-weight: var(--ak-weight-semibold);
  line-height: 1.2;
  white-space: nowrap;
  color: var(--ak-text);
  background: transparent;
  cursor: pointer;
  transition:
    background var(--ak-dur-fast) var(--ak-ease-out),
    border-color var(--ak-dur-fast) var(--ak-ease-out),
    transform var(--ak-dur-fast) var(--ak-ease-out);
}

.btn:active:not(:disabled) {
  transform: translateY(1px);
}

.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.btn-primary {
  background: var(--ak-blue);
  border-color: var(--ak-blue);
  color: #00203f;
}

.btn-primary:hover:not(:disabled) {
  background: #2cb9f4;
  border-color: #2cb9f4;
}

.btn-accent {
  background: var(--ak-orange);
  border-color: var(--ak-orange);
  color: #2b1600;
}

.btn-accent:hover:not(:disabled) {
  background: #ffa233;
  border-color: #ffa233;
}

.btn-secondary {
  background: var(--ak-surface-2);
  border-color: var(--ak-border-strong);
}

.btn-secondary:hover:not(:disabled) {
  background: var(--ak-surface-3);
}

.btn-ghost {
  color: var(--ak-text-muted);
  border-color: var(--ak-border);
}

.btn-ghost:hover:not(:disabled) {
  color: var(--ak-text);
  border-color: var(--ak-border-strong);
}

.btn-danger {
  color: var(--ak-danger);
  border-color: var(--ak-border);
}

.btn-danger:hover:not(:disabled) {
  border-color: var(--ak-danger);
}

.btn-sm {
  padding: var(--ak-space-1) var(--ak-space-3);
  font-size: var(--ak-text-small);
  border-radius: var(--ak-radius-sm);
}

.btn-lg {
  padding: var(--ak-space-3) var(--ak-space-5);
  font-size: var(--ak-text-lead);
  width: 100%;
}

.btn-xl {
  padding: var(--ak-space-4) var(--ak-space-8);
  font-size: 1.0625rem;
  width: 100%;
  letter-spacing: 0.01em;
}

/* --------------------------------------------------------------- fields -- */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-1);
  min-width: 0;
}

.field-label {
  display: flex;
  align-items: baseline;
  gap: var(--ak-space-2);
  color: var(--ak-text-muted);
  font-size: var(--ak-text-caption);
  font-weight: var(--ak-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.field-aux {
  text-transform: none;
  letter-spacing: 0;
  font-weight: var(--ak-weight-regular);
  opacity: 0.8;
}

.req {
  color: var(--ak-blue);
  text-transform: none;
  letter-spacing: 0;
  font-weight: var(--ak-weight-regular);
  font-size: var(--ak-text-caption);
}

input[type="text"],
input[type="password"],
input[type="number"],
input:not([type]),
select {
  appearance: none;
  width: 100%;
  min-width: 0;
  padding: var(--ak-space-2) var(--ak-space-3);
  background: var(--ak-bg);
  color: var(--ak-text);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-sm);
  font-family: inherit;
  font-size: var(--ak-text-body);
  line-height: 1.4;
  transition: border-color var(--ak-dur-fast) var(--ak-ease-out);
}

input.num {
  font-family: var(--ak-font-mono);
  font-variant-numeric: tabular-nums;
}

input:hover,
select:hover {
  border-color: var(--ak-border-strong);
}

input:focus,
select:focus {
  outline: none;
  border-color: var(--ak-blue);
  box-shadow: var(--ak-focus);
}

input::placeholder {
  color: var(--ak-text-muted);
  opacity: 0.6;
}

select {
  background-image: linear-gradient(45deg, transparent 50%, var(--ak-text-muted) 50%),
    linear-gradient(135deg, var(--ak-text-muted) 50%, transparent 50%);
  background-position: right 14px center, right 9px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: var(--ak-space-8);
}

.msg {
  margin-top: var(--ak-space-3);
  padding: var(--ak-space-2) var(--ak-space-3);
  border-radius: var(--ak-radius-sm);
  font-size: var(--ak-text-small);
}

.msg-error {
  background: rgb(255 92 92 / 12%);
  border: 1px solid rgb(255 92 92 / 35%);
  color: var(--ak-danger);
}

/* After a print Dropbox would not take, Save print file: a button that reads
   as a link, at the end of the reason. */
.msg-link {
  margin-left: var(--ak-space-1);
  padding: 0;
  border: 0;
  background: none;
  color: var(--ak-text);
  font: inherit;
  font-weight: var(--ak-weight-semibold);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.msg-link[aria-busy="true"] {
  opacity: 0.6;
  cursor: progress;
}

/* -------------------------------------------------------------- sign in -- */

/* The field, the button, and nothing to read. What the key is and where the
   browser keeps it is in docs/SECURITY.md. */
.signin {
  position: relative;
  min-height: 100dvh;
  isolation: isolate;
  display: grid;
  justify-items: center;
  align-content: center;
  gap: var(--ak-space-5);
  padding: var(--ak-space-6);
  overflow: hidden;
}

/* Where the page and its models run, in one line under the card. */
.signin-claim {
  position: relative;
  z-index: 1;
  text-align: center;
}

.signin-card {
  position: relative;
  z-index: 1;
  width: min(100%, 24rem);
  padding: var(--ak-space-8);
  background: var(--ak-surface);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-lg);
  box-shadow: var(--ak-shadow-lg);
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-4);
}

/* One field, so its label is a plain word at reading size rather than the
   small capitals the player form uses for nine of them. */
.signin-card .field {
  gap: var(--ak-space-2);
}

.signin-card .field-label {
  color: var(--ak-text);
  font-size: var(--ak-text-title);
  letter-spacing: 0;
  text-transform: none;
}

.signin-card input {
  padding: var(--ak-space-3);
  font-size: var(--ak-text-lead);
}

.signin-card .msg {
  margin-top: 0;
}

.signin-foot {
  margin-top: var(--ak-space-2);
  padding-top: var(--ak-space-4);
  border-top: 1px solid var(--ak-border);
}

/* --------------------------------------------------------------- shell --- */

/* At tablet landscape and wider the app is a shell: the header is pinned at
   the top, the footer at the foot, and the view between them scrolls on its
   own. Below 1101 px the page scrolls normally and the footer ends it. */
.app {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

@media (min-width: 1101px) {
  .app {
    height: 100dvh;
    min-height: 0;
  }

  .main,
  .view-page {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

/* A view holds its own screen-reader-only headings. Without a position of
   its own their box is placed against the page, and the one at the foot of
   the dashboard made the whole document scroll past the footer. */
.main,
.view-page {
  position: relative;
}

/* On every view: the copyright, small, in the middle, and nothing else. */
.foot {
  flex: none;
  padding: var(--ak-space-2) var(--ak-space-6);
  border-top: 1px solid var(--ak-border);
  background: var(--ak-bg);
  text-align: center;
}

.foot-line {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-caption);
  letter-spacing: 0.02em;
}

/* Three columns: the logo on the left, the views in the middle, the node
   pill and sign out on the right. The two sides share what is left equally,
   so the views sit in the middle of the page whatever the sides hold. The
   header has no padding of its own top and bottom: the sides carry it, and
   the views' links run the full height so the underline sits on its edge. */
.header {
  flex: none;
  position: sticky;
  top: 0;
  z-index: 20;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: stretch;
  gap: var(--ak-space-4);
  padding: 0 var(--ak-space-6);
  background: var(--ak-navy);
  border-bottom: 1px solid var(--ak-border);
  overflow: hidden;
}

.wave-header {
  height: 100%;
  opacity: 0.6;
}

.header-left,
.header-right {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--ak-space-3);
  min-width: 0;
  padding: var(--ak-space-3) 0;
}

.header-right {
  justify-content: flex-end;
}

.header-div {
  width: 1px;
  height: 20px;
  background: var(--ak-border-strong);
}

.header-title {
  font-size: var(--ak-text-h3);
  font-weight: var(--ak-weight-semibold);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The three views. Words from a tablet up, icons with their words under them
   on a phone. The view on show has a blue line along the header's foot. */
.nav {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: stretch;
  gap: var(--ak-space-1);
}

.nav-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--ak-space-2);
  padding: 0 var(--ak-space-3);
  color: var(--ak-text-muted);
  font-size: var(--ak-text-title);
  font-weight: var(--ak-weight-medium);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--ak-dur-fast) var(--ak-ease-out);
}

.nav-link:hover,
.nav-link[aria-current="page"] {
  color: var(--ak-text);
}

/* The line grows out from the middle when its view comes on show. */
.nav-link::after {
  content: "";
  position: absolute;
  left: var(--ak-space-3);
  right: var(--ak-space-3);
  bottom: 0;
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--ak-blue);
  transform: scaleX(0);
  transition: transform var(--ak-dur-base) var(--ak-ease-out);
}

.nav-link[aria-current="page"]::after {
  transform: scaleX(1);
}

/* The page's ring would wrap the header's whole height. This one sits inside. */
.nav-link:focus-visible {
  box-shadow: inset 0 0 0 2px var(--ak-blue);
  border-radius: var(--ak-radius-sm);
}

.nav-icon {
  display: none;
  width: 20px;
  height: 20px;
  stroke: currentColor;
  stroke-width: 1.5;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The node's state in the header, read from /healthz. A readout: the rows
   behind it are in the dashboard's observability row. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--ak-space-2);
  padding: var(--ak-space-1) var(--ak-space-3);
  border: 1px solid var(--ak-border-strong);
  border-radius: var(--ak-radius-full);
  font-size: var(--ak-text-small);
  line-height: 1.55;
  color: var(--ak-text-muted);
  white-space: nowrap;
}

.dot {
  width: 7px;
  height: 7px;
  border-radius: var(--ak-radius-full);
  background: var(--ak-text-muted);
  flex: none;
}

.dot-ok {
  background: var(--ak-success);
}

.dot-warn {
  background: var(--ak-warning);
}

.dot-bad {
  background: var(--ak-danger);
}

.dot-run {
  background: var(--ak-orange);
  animation: pulse var(--ak-dur-pulse) var(--ak-ease-in-out) infinite;
}

@keyframes pulse {
  50% {
    opacity: 0.35;
  }
}

/* The node has stopped answering. The only error the app raises on its own.
   Navy surface, orange edge: the accent marks it without repainting the bar. */
.banner {
  margin: 0;
  padding: var(--ak-space-3) var(--ak-space-6);
  background: var(--ak-surface);
  border-bottom: 1px solid var(--ak-border);
  border-left: 3px solid var(--ak-orange);
  color: var(--ak-warning);
  font-size: var(--ak-text-small);
}

/* The dashboard. One column of work, top to bottom in the order it is done:
   photo and player side by side, style, Generate, this tab's cards, and
   what the node is doing. At 1920 x 1080 all of it is on the screen at once
   until a card starts. */
.main {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-4);
  padding: var(--ak-space-4) var(--ak-space-6);
}

.main > * {
  flex: none;
  width: 100%;
  max-width: 120rem;
  margin-inline: auto;
}

/* 1 and 2. Side by side from a laptop up, the same height. While the camera
   runs its stage takes the row and the player steps aside (camera.js). */
.intake {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--ak-space-4);
}

@media (min-width: 1101px) {
  .intake {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
}

.intake.is-capturing {
  grid-template-columns: minmax(0, 1fr);
}

/* The stage is the camera and nothing else. The player and the jersey
   switch come back with the photo. */
.intake.is-capturing #panel-player,
.intake.is-capturing .jersey {
  display: none;
}

/* The player comes back the way everything that appears does. */
#panel-player {
  animation: appear var(--ak-dur-slow) var(--ak-ease-out);
}

@keyframes appear {
  from {
    opacity: 0;
  }
}

/* --------------------------------------------------------------- panels -- */

.panel {
  background: var(--ak-surface);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-md);
  padding: var(--ak-space-5);
}

.panel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ak-space-2) var(--ak-space-3);
  margin-bottom: var(--ak-space-4);
}

.panel-head h2 {
  display: flex;
  align-items: center;
  gap: var(--ak-space-2);
  font-size: var(--ak-text-h2);
}

.step-no {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: var(--ak-radius-full);
  background: var(--ak-blue);
  color: #00203f;
  font-family: var(--ak-font-mono);
  font-size: var(--ak-text-caption);
  font-weight: var(--ak-weight-bold);
}

.panel-sub {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  flex: 1;
  min-width: 10rem;
}

/* One fact about a panel, smaller than its hint and right after it. */
.panel-note {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-caption);
  letter-spacing: 0.02em;
}

.panel-sub:has(+ .panel-note) {
  flex: none;
}

.panel-action {
  margin-left: auto;
}

.subhead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ak-space-3);
  margin: var(--ak-space-6) 0 var(--ak-space-3);
  font-size: var(--ak-text-title);
}

.subhead-sub {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-regular);
}

/* Nothing here yet. One line of muted text, not a box to look at. */
.empty {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

/* ----------------------------------------------------------------- tabs -- */

.tabs {
  display: flex;
  gap: var(--ak-space-1);
  padding: var(--ak-space-1);
  margin-left: auto;
  align-self: center;
  background: var(--ak-bg);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-sm);
  width: fit-content;
}

.tab {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ak-text-muted);
  font-family: inherit;
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-semibold);
  padding: var(--ak-space-1) var(--ak-space-4);
  border-radius: var(--ak-radius-sm);
  cursor: pointer;
  transition: background var(--ak-dur-fast) var(--ak-ease-out);
}

.tab.is-on {
  background: var(--ak-surface-2);
  color: var(--ak-text);
}

.placeholder {
  padding: var(--ak-space-6);
  border: 1px dashed var(--ak-border);
  border-radius: var(--ak-radius-sm);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--ak-space-3);
}

.placeholder-lead {
  font-weight: var(--ak-weight-semibold);
}

.placeholder-sub {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

/* ------------------------------------------------------------ dropzone --- */

/* The photo, and the jersey switch beside it when the node offers one. */
.photo-body {
  display: flex;
  gap: var(--ak-space-5);
  align-items: flex-start;
}

.photo-main {
  flex: 1;
  min-width: 0;
}

.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ak-space-2);
  padding: var(--ak-space-5) var(--ak-space-4);
  border: 1px dashed var(--ak-border-strong);
  border-radius: var(--ak-radius-md);
  background: var(--ak-bg);
  text-align: center;
  cursor: pointer;
  transition:
    border-color var(--ak-dur-fast) var(--ak-ease-out),
    background var(--ak-dur-fast) var(--ak-ease-out);
}

.dropzone:hover,
.dropzone.is-over {
  border-color: var(--ak-blue);
  background: rgb(0 164 235 / 8%);
}

.dz-icon {
  width: 28px;
  height: 28px;
  stroke: var(--ak-blue);
  stroke-width: 1.5;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.dz-lead {
  font-weight: var(--ak-weight-semibold);
}

.dz-sub {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  max-width: 28rem;
}

.thumb-row {
  display: flex;
  align-items: center;
  gap: var(--ak-space-4);
  padding: var(--ak-space-3);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-md);
  background: var(--ak-bg);
}

.thumb {
  width: 84px;
  height: 84px;
  object-fit: cover;
  border-radius: var(--ak-radius-sm);
  border: 1px solid var(--ak-border);
  flex: none;
  background: var(--ak-surface-2);
}

.thumb-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--ak-space-1);
  min-width: 0;
}

.thumb-name {
  font-weight: var(--ak-weight-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 18rem;
}

.thumb-size {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.thumb-note {
  color: var(--ak-warning);
  font-size: var(--ak-text-small);
}

/* ---------------------------------------------------------------- booth -- */

/* The Booth tab: the guest number and how the node last read LiveX's folder
   on one line, and the day's pictures under it, newest first. The grid
   scrolls inside the panel, so a day of hundreds does not push the page. */
.booth-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--ak-space-2) var(--ak-space-4);
  margin-bottom: var(--ak-space-3);
}

.booth-find {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-1);
}

/* Six digits, and the room to read them from a step back. */
.booth-find input {
  width: 9ch;
  font-size: var(--ak-text-lead);
}

.booth-status {
  flex: 1;
  min-width: 12rem;
  padding-bottom: var(--ak-space-2);
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  font-variant-numeric: tabular-nums;
}

.booth-status.is-error {
  color: var(--ak-danger);
}

/* A little padding, so a tile's focus ring is not cut off by the scroll. */
.booth-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
  gap: var(--ak-space-2);
  max-height: 22.5rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--ak-space-1);
  margin: calc(-1 * var(--ak-space-1));
}

.booth-tile {
  appearance: none;
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 0;
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-sm);
  background: var(--ak-bg);
  color: inherit;
  font: inherit;
  text-align: left;
  overflow: hidden;
  cursor: pointer;
  /* A tap takes the picture. It must not zoom the page or pick a word. */
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
  transition: border-color var(--ak-dur-fast) var(--ak-ease-out);
}

.booth-tile:hover {
  border-color: var(--ak-border-strong);
}

/* Fetching the picture and finding the face, about a second. */
.booth-tile[aria-busy="true"] {
  border-color: var(--ak-blue);
  cursor: progress;
}

.booth-tile[aria-busy="true"] .booth-pic {
  opacity: 0.6;
}

.booth-pic {
  display: block;
  aspect-ratio: 1;
  background: var(--ak-surface-2);
  transition: opacity var(--ak-dur-fast) var(--ak-ease-out);
}

.booth-pic img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--ak-dur-base) var(--ak-ease-out);
}

.booth-pic img.is-loaded {
  opacity: 1;
}

.booth-label {
  display: block;
  padding: var(--ak-space-1) var(--ak-space-2);
  font-size: var(--ak-text-caption);
  font-weight: var(--ak-weight-semibold);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.booth-when {
  color: var(--ak-text-muted);
  font-weight: var(--ak-weight-regular);
}

.booth-none {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

/* The jersey option: a switch, and one line under it. */
.jersey {
  flex: none;
  width: 15rem;
  display: grid;
  gap: var(--ak-space-2);
  padding-top: var(--ak-space-3);
}

/* A real checkbox drawn as a switch, so the keyboard, the click on the label
   and the focus ring all stay the browser's own. The knob is its ::before. */
.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--ak-space-3);
  width: fit-content;
  font-weight: var(--ak-weight-semibold);
  cursor: pointer;
  user-select: none;
}

.switch input {
  appearance: none;
  position: relative;
  flex: none;
  width: 40px;
  height: 22px;
  margin: 0;
  border: 1px solid var(--ak-border-strong);
  border-radius: var(--ak-radius-full);
  background: var(--ak-surface-2);
  cursor: pointer;
  transition:
    background-color var(--ak-dur-fast) var(--ak-ease-out),
    border-color var(--ak-dur-fast) var(--ak-ease-out);
}

.switch input::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: var(--ak-radius-full);
  background: var(--ak-text-muted);
  transition:
    transform var(--ak-dur-fast) var(--ak-ease-out),
    background-color var(--ak-dur-fast) var(--ak-ease-out);
}

.switch input:checked {
  border-color: var(--ak-blue);
  background: var(--ak-blue);
}

.switch input:checked::before {
  transform: translateX(18px);
  background: var(--ak-text);
}

/* The global ring would square the track's ends. */
.switch input:focus-visible {
  border-radius: var(--ak-radius-full);
}

.jersey-note {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

/* --------------------------------------------------------------- camera -- */

/* The stage takes its aspect ratio from the stream, set by camera.js, so the
   guide SVG's viewBox is the video's own pixels and nothing has to be mapped
   between two coordinate spaces. The video is mirrored, the SVG is not: the
   boxes drawn on it are already mirrored by framing.mirrorBox(). */

.cam {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-3);
  /* The stage fades in as it takes the row, the way everything that
     appears does. It runs again each time the stage opens. */
  animation: appear var(--ak-dur-slow) var(--ak-ease-out);
}

/* The capture stage. On a phone it is the page's width. From a laptop up it
   is 640 px tall or more, as wide as the stream's shape makes it, in the
   middle of the whole photo and player row. */
.cam-stage {
  position: relative;
  aspect-ratio: 4 / 3;
  max-height: 70dvh;
  margin-inline: auto;
  width: 100%;
  overflow: hidden;
  background: var(--ak-bg);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-md);
}

/* The ring's colour as a 6 px frame inside the whole preview, so a person a
   few metres from the screen sees it go green, not only a line round a head.
   camera.js sets data-level on the stage and the ring together. */
.cam-stage::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 6px var(--cam-frame, transparent);
  pointer-events: none;
  transition: box-shadow var(--ak-dur-base) var(--ak-ease-out);
}

.cam-stage[data-level="red"] {
  --cam-frame: var(--ak-danger);
}

.cam-stage[data-level="amber"] {
  --cam-frame: var(--ak-orange);
}

.cam-stage[data-level="green"] {
  --cam-frame: var(--ak-green);
}

#cam-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scaleX(-1);
}

.cam-guide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.cam-ring {
  fill: none;
  stroke: var(--ak-border-strong);
  transition: stroke var(--ak-dur-base) var(--ak-ease-out);
}

.cam-ring[data-level="red"] {
  stroke: var(--ak-danger);
}

.cam-ring[data-level="amber"] {
  stroke: var(--ak-orange);
}

.cam-ring[data-level="green"] {
  stroke: var(--ak-green);
}

.cam-body,
.cam-bat {
  fill: none;
  stroke: var(--ak-text-muted);
  stroke-linecap: round;
  opacity: 0.55;
}

.cam-body {
  stroke-dasharray: 7 11;
  opacity: 0.4;
}

/* The HTML hidden attribute is not reliable inside SVG, and these two are
   toggled every frame. */
.cam-bat[hidden],
.cam-face[hidden] {
  display: none;
}

.cam-face {
  fill: none;
  stroke: var(--ak-blue);
  stroke-width: 2;
  opacity: 0.5;
}

.cam-count {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: end;
  justify-items: center;
  padding-bottom: 10%;
  margin: 0;
  font-family: var(--ak-font-mono);
  font-size: clamp(3.5rem, 14vw, 8rem);
  font-weight: var(--ak-weight-bold);
  color: var(--ak-text);
  text-shadow: 0 2px 24px rgb(0 8 20 / 80%);
}

.cam-hint {
  position: absolute;
  left: 50%;
  bottom: var(--ak-space-3);
  transform: translateX(-50%);
  max-width: 90%;
  padding: var(--ak-space-1) var(--ak-space-4);
  border-radius: var(--ak-radius-full);
  background: rgb(0 20 46 / 82%);
  border: 1px solid var(--ak-border);
  color: var(--ak-text);
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-semibold);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cam-hint[data-level="red"] {
  border-color: var(--ak-danger);
}

.cam-hint[data-level="amber"] {
  border-color: var(--ak-orange);
}

.cam-hint[data-level="green"] {
  border-color: var(--ak-green);
  color: var(--ak-green);
}

.cam-controls {
  display: flex;
  align-items: center;
  gap: var(--ak-space-4);
  flex-wrap: wrap;
}

.cam-pose {
  white-space: nowrap;
}

.cam-device {
  max-width: 14rem;
  font-size: var(--ak-text-small);
  margin-left: auto;
}

.cam-close {
  margin-left: auto;
}

.cam-device:not([hidden]) + .cam-close {
  margin-left: 0;
}

/* The stage, its controls and its note are one column as wide as the
   preview, in the middle of the row. */
@media (min-width: 1101px) {
  .cam {
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
  }

  .cam-stage {
    width: auto;
    max-width: 100%;
    height: max(640px, calc(100dvh - 22rem));
    max-height: none;
  }
}

.cam-note {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-caption);
  line-height: 1.5;
}

/* ----------------------------------------------------------------- form -- */

.grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--ak-space-4);
}

/* Name and number on one row at every width, the name with the room. Two
   classes, so the narrower screens' .grid columns do not win. */
.grid.grid-front {
  grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
}

/* Bio and swing metrics. A <details>, closed until asked for. Its content
   box grows from nothing to its own height and back (interpolate-size), and
   a browser without that opens and closes it at once. */
.more {
  margin-top: var(--ak-space-4);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-sm);
  background: var(--ak-bg);
  interpolate-size: allow-keywords;
}

.more::details-content {
  block-size: 0;
  overflow-y: clip;
  transition:
    block-size var(--ak-dur-slow) var(--ak-ease-out),
    content-visibility var(--ak-dur-slow) var(--ak-ease-out) allow-discrete;
}

.more[open]::details-content {
  block-size: auto;
}

.more-head {
  display: flex;
  align-items: center;
  gap: var(--ak-space-3);
  padding: var(--ak-space-3) var(--ak-space-4);
  border-radius: var(--ak-radius-sm);
  font-size: var(--ak-text-body);
  font-weight: var(--ak-weight-semibold);
  cursor: pointer;
  user-select: none;
  list-style: none;
  transition: background var(--ak-dur-fast) var(--ak-ease-out);
}

.more-head::-webkit-details-marker {
  display: none;
}

/* A chevron drawn with two borders, pointing right when closed and down when
   open. */
.more-head::before {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  margin-right: var(--ak-space-1);
  border-right: 1.5px solid var(--ak-text-muted);
  border-bottom: 1.5px solid var(--ak-text-muted);
  transform: rotate(-45deg);
  transition: transform var(--ak-dur-base) var(--ak-ease-out);
}

.more[open] > .more-head::before {
  transform: rotate(45deg);
}

.more-head:hover {
  background: var(--ak-surface);
}

.more-note {
  margin-left: auto;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-regular);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.more-body {
  padding: var(--ak-space-2) var(--ak-space-4) var(--ak-space-4);
}

.more-body .subhead {
  margin-top: var(--ak-space-5);
}

.metrics {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-2);
}

.metric {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--ak-space-1) var(--ak-space-3);
  padding: var(--ak-space-2) var(--ak-space-3);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-sm);
  background: var(--ak-bg);
}

.metric.is-off {
  opacity: 0.45;
}

.metric-top {
  display: flex;
  align-items: baseline;
  gap: var(--ak-space-2);
  min-width: 0;
}

.metric-name {
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-semibold);
  white-space: nowrap;
}

.metric-unit {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-caption);
}

.metric-value {
  grid-column: 2;
  grid-row: 1;
  min-width: 4.5rem;
  text-align: right;
  font-size: var(--ak-text-body);
  font-weight: var(--ak-weight-semibold);
  color: var(--ak-blue);
}

.metric-slider {
  grid-column: 1;
  grid-row: 2;
}

.metric-skip {
  grid-column: 2;
  grid-row: 2;
  justify-self: end;
}

.skip {
  display: inline-flex;
  align-items: center;
  gap: var(--ak-space-1);
  color: var(--ak-text-muted);
  font-size: var(--ak-text-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
  user-select: none;
}

.skip input {
  width: 13px;
  height: 13px;
  margin: 0;
  accent-color: var(--ak-blue);
}

input[type="range"] {
  appearance: none;
  width: 100%;
  height: 20px;
  background: transparent;
  cursor: pointer;
  margin: 0;
}

input[type="range"]::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: var(--ak-radius-full);
  background: var(--ak-surface-3);
}

input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -5px;
  border-radius: var(--ak-radius-full);
  background: var(--ak-blue);
  border: 0;
}

input[type="range"]::-moz-range-track {
  height: 4px;
  border-radius: var(--ak-radius-full);
  background: var(--ak-surface-3);
}

input[type="range"]::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border: 0;
  border-radius: var(--ak-radius-full);
  background: var(--ak-blue);
}

input[type="range"]:disabled::-webkit-slider-thumb {
  background: var(--ak-text-muted);
}

input[type="range"]:disabled::-moz-range-thumb {
  background: var(--ak-text-muted);
}

/* ---------------------------------------------------------------- tiles -- */

/* A tile is one button: the picture and the style's name, which opens the
   style's sample card. It lifts on hover. Three to a row at every size. */
.tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--ak-space-2);
}

.tile {
  position: relative;
  min-width: 0;
  transition: transform var(--ak-dur-fast) var(--ak-ease-out);
}

.tile:hover {
  transform: translateY(-2px);
}

.tile-open {
  appearance: none;
  display: block;
  width: 100%;
  padding: 0;
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-sm);
  background: var(--ak-bg);
  color: inherit;
  font: inherit;
  cursor: zoom-in;
  overflow: hidden;
  text-align: left;
  /* A tap opens the card. It must not zoom the page or pick a word. */
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
  transition: border-color var(--ak-dur-fast) var(--ak-ease-out);
}

.tile:hover .tile-open {
  border-color: var(--ak-border-strong);
}

.tile img {
  display: block;
  width: 100%;
  aspect-ratio: 300 / 420;
  object-fit: cover;
  background: var(--ak-surface-2);
}

.tile-name {
  display: block;
  height: 28px;
  padding: 0 var(--ak-space-2);
  font-size: var(--ak-text-caption);
  font-weight: var(--ak-weight-semibold);
  letter-spacing: 0.02em;
  line-height: 28px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Out in the lightbox. There is one of the card on screen, not two. */
.tile.is-lifted img {
  visibility: hidden;
}

/* 3. From a laptop up the tiles keep a fixed largest size, 200 px, in the
   middle of the panel under its title. From 1880 px, a 1080p screen, the
   title is a cell on the left and the three tiles run beside it. */
@media (min-width: 1101px) {
  .panel-style {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "head"
      "tiles";
    gap: var(--ak-space-4);
    align-items: center;
  }

  .panel-style .panel-head {
    grid-area: head;
    margin: 0;
  }

  .panel-style .tiles {
    grid-area: tiles;
    grid-template-columns: repeat(3, minmax(0, 12.5rem));
    justify-content: center;
    gap: var(--ak-space-3);
  }
}

@media (min-width: 1880px) {
  .panel-style {
    grid-template-columns: minmax(8rem, 1fr) auto;
    grid-template-areas: "head tiles";
    gap: 0 var(--ak-space-4);
    align-items: start;
  }

  .panel-style .panel-head {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--ak-space-1);
  }

  .panel-style .panel-sub {
    flex: none;
    min-width: 0;
  }

  .panel-style .tiles {
    grid-template-columns: repeat(3, minmax(0, 200px));
    gap: 6px;
  }

  /* The dashboard's rhythm tightens by 4 px, 12 between blocks and 16 inside
     a panel, so the 80 px taller tiles fit a 1080p screen with the rest. */
  .main {
    gap: var(--ak-space-3);
    padding-block: var(--ak-space-3);
  }

  .intake {
    gap: var(--ak-space-3);
  }

  .main .panel {
    padding: var(--ak-space-4);
  }
}

/* ------------------------------------------------------------- generate -- */

/* 4. One bar: the button, what the last press did, and Start over. */
.generate {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ak-space-2) var(--ak-space-5);
}

.generate .btn-xl {
  width: auto;
  min-width: 18rem;
}

.generate-note {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

#start-over {
  margin-left: auto;
}

/* ------------------------------------------------------- this session --- */

/* One row per job this tab posted. With none, the title line is the whole
   panel, and its note says there is nothing yet. */
.panel-run .panel-head {
  margin-bottom: 0;
}

.runs {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-3);
  margin-top: var(--ak-space-4);
}

.runs:empty {
  display: none;
}

.run {
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-sm);
  background: var(--ak-bg);
  padding: var(--ak-space-3);
  transition: border-color var(--ak-dur-base) var(--ak-ease-out);
}

/* A new row deals in the way a new card does. run.js takes the class off
   when it has. */
.run.is-new {
  animation: deal var(--ak-dur-slow) var(--ak-ease-out) backwards;
}

.run.is-done {
  border-color: rgb(33 192 139 / 35%);
}

.run.is-failed {
  border-color: rgb(255 92 92 / 40%);
}

.run.is-cancelled {
  border-color: var(--ak-border-strong);
}

/* The top line, and Cancel or Retry beside it. They sit outside the fold
   button, so a folded row still has them. */
.run-head {
  display: flex;
  align-items: center;
  gap: var(--ak-space-3);
}

.run-head .run-top {
  flex: 1;
  min-width: 0;
}

/* The card's front, small, once it is made. The slot is there from the
   start, so the line does not move when the picture arrives. A tap on it
   opens the card, as Open does. */
.run-thumb {
  flex: none;
  display: block;
  width: 36px;
  aspect-ratio: 1500 / 2100;
  overflow: hidden;
  border: 1px solid var(--ak-border);
  border-radius: 4px;
  background: var(--ak-surface-2);
  transition: border-color var(--ak-dur-fast) var(--ak-ease-out);
}

.run-thumb.is-ready {
  cursor: pointer;
}

.run-thumb.is-ready:hover {
  border-color: var(--ak-blue);
}

.run-thumb[data-status="failed"] {
  border-color: rgb(255 92 92 / 40%);
}

.run-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--ak-dur-base) var(--ak-ease-out);
}

.run-thumb img.is-loaded {
  opacity: 1;
}

/* Out in the lightbox. There is one of the card on screen, not two. */
.run.is-lifted .run-thumb img {
  visibility: hidden;
}

.run-act {
  flex: none;
}

/* Waiting on the node. It keeps the focus, so it is not disabled. */
.run-act[aria-busy="true"] {
  opacity: 0.6;
  cursor: progress;
}

/* The top line: dot, name, style, seconds. It is a button: a tap folds the
   row to this line or opens it again. */
.run-top {
  appearance: none;
  display: flex;
  align-items: center;
  gap: var(--ak-space-3);
  width: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* On a narrow row, beside Cancel or Retry, the style gives way first. The
   name keeps its width up to 60 percent of the line. */
.run-name {
  flex: none;
  max-width: 60%;
  font-weight: var(--ak-weight-semibold);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.run-style {
  min-width: 0;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.run-total {
  margin-left: auto;
  font-size: var(--ak-text-small);
  color: var(--ak-text-muted);
  white-space: nowrap;
}

/* A chevron, down while the row is open, right when it is folded. */
.run-caret {
  flex: none;
  width: 7px;
  height: 7px;
  margin: 0 var(--ak-space-1) 3px;
  border-right: 1.5px solid var(--ak-text-muted);
  border-bottom: 1.5px solid var(--ak-text-muted);
  transform: rotate(45deg);
  transition: transform var(--ak-dur-base) var(--ak-ease-out);
}

.run.is-folded .run-caret {
  transform: rotate(-45deg);
}

/* The fold. One grid row that goes from the content's height to nothing, so
   the row shrinks from where it was. Folded, the content is still there,
   clipped, and inert keeps it out of the reading order. */
.run-body {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--ak-dur-slow) var(--ak-ease-out);
}

.run.is-folded .run-body {
  grid-template-rows: 0fr;
}

/* No padding here: a padded box cannot shrink below its padding, and the
   folded row would keep it. The space above the tiles is their margin. */
.run-inner {
  min-height: 0;
  overflow: hidden;
}

.run-inner > .chips {
  margin-top: var(--ak-space-3);
}

/* One column per step: six, or seven when the jersey edit runs first. run.js
   sets --chips on each row. */
.chips {
  display: grid;
  grid-template-columns: repeat(var(--chips, 6), minmax(0, 1fr));
  gap: var(--ak-space-1);
}

.chip {
  position: relative;
  overflow: hidden;
  padding: var(--ak-space-2);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-sm);
  background: var(--ak-surface);
  min-width: 0;
  /* The counter sizes itself to this tile. See .chip-ms. */
  container-type: inline-size;
}

.chip-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: rgb(255 139 0 / 18%);
  transition: width var(--ak-dur-base) var(--ak-ease-out);
}

.chip.is-running {
  border-color: var(--ak-orange);
}

/* While a step runs the ticker aims its fill a little further every 80 ms.
   An ease-out restarted twelve times a second reads as a stutter, so this one
   runs at an even pace. The last move, to done, eases out like the rest. */
.chip.is-running .chip-fill {
  transition-timing-function: linear;
}

.chip.is-done {
  border-color: rgb(33 192 139 / 40%);
}

.chip.is-done .chip-fill {
  width: 100%;
  background: rgb(33 192 139 / 14%);
}

/* A skipped step is not news. A failed one is the reason the card is short,
   so it keeps its colour while the steps around it fade. */
.chip.is-skipped {
  border-color: var(--ak-border);
  opacity: 0.6;
}

.chip.is-failed {
  border-color: rgb(255 92 92 / 55%);
}

/* Stopped by a cancel: like a skipped step, and it says which. */
.chip.is-cancelled {
  border-color: var(--ak-border);
  opacity: 0.6;
}

.chip.is-failed .chip-name,
.chip.is-failed .chip-model {
  color: var(--ak-danger);
}

.chip-name,
.chip-ms,
.chip-model,
.chip-where {
  position: relative;
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chip-name {
  font-size: var(--ak-text-caption);
  font-weight: var(--ak-weight-semibold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ak-text-muted);
}

.chip.is-running .chip-name {
  color: var(--ak-orange);
}

.chip.is-done .chip-name {
  color: var(--ak-text);
}

/* "4321 ms" is 4.1 em wide. Seven tiles, with the jersey edit first, are too
   narrow for it at 24 px between 1400 and 1600 px wide, so the counter takes
   the smaller of its size and what the tile holds. Six tiles always hold it. */
.chip-ms {
  font-family: var(--ak-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: min(var(--ak-text-counter), 23cqi);
  font-weight: var(--ak-weight-semibold);
  line-height: 1.3;
}

.chip-model {
  font-size: var(--ak-text-caption);
  color: var(--ak-text-muted);
}

.chip-where {
  font-family: var(--ak-font-mono);
  font-size: var(--ak-text-caption);
  color: var(--ak-text-muted);
  opacity: 0.85;
}

.chip.is-done .chip-where {
  color: var(--ak-blue);
}

.run-warnings {
  margin-top: var(--ak-space-3);
  display: flex;
  flex-wrap: wrap;
  gap: var(--ak-space-2);
}

.run-warnings .warning {
  animation: rise var(--ak-dur-base) var(--ak-ease-out) backwards;
}

.warning {
  display: inline-flex;
  align-items: center;
  gap: var(--ak-space-1);
  padding: 2px var(--ak-space-2);
  border-radius: var(--ak-radius-full);
  background: rgb(255 139 0 / 14%);
  border: 1px solid rgb(255 139 0 / 35%);
  color: var(--ak-warning);
  font-size: var(--ak-text-caption);
}

.warning.is-bad {
  background: rgb(255 92 92 / 14%);
  border-color: rgb(255 92 92 / 35%);
  color: var(--ak-danger);
}

/* Why the card failed, or stopped. One line under the steps; the whole of
   it is on the line's title. */
.run-error {
  margin-top: var(--ak-space-3);
  color: var(--ak-danger);
  font-size: var(--ak-text-small);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.run.is-cancelled .run-error {
  color: var(--ak-text-muted);
}

/* -------------------------------------------------------------- results -- */

/* The results stage (results.js). Once a card of the session is made it
   takes the place of the form and the strip, and the observability row stays
   under it. */
.main.is-results > :is(#intake, #panel-style, #generate, #panel-run) {
  display: none;
}

/* A card is 59 percent of the screen's height, or less where three side by
   side would not fit: 637 px at 1920 x 1080, with the buttons and the
   observability row under it on the one screen, and about 590 px at 1366 x
   1024. Two to a row on a tablet held upright, one on a phone. Past that the
   row scrolls sideways. */
.results {
  --slot-gap: var(--ak-space-5);
  --slots: 1;
  container-type: inline-size;
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-2);
}

@media (min-width: 641px) {
  .results {
    --slots: 2;
  }
}

@media (min-width: 1101px) {
  .results {
    --slots: 3;
  }
}

/* Room round the cards for a focus ring: a box that scrolls sideways clips
   what reaches past it. A thin scrollbar under the row says there is more. */
.results-row {
  display: flex;
  justify-content: safe center;
  gap: var(--slot-gap);
  padding: var(--ak-space-1);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  scrollbar-color: var(--ak-border-strong) transparent;
}

.slot {
  flex: none;
  width: min(
    calc(59dvh * 1500 / 2100),
    calc((100cqi - 2 * var(--ak-space-1) - (var(--slots) - 1) * var(--slot-gap)) / var(--slots))
  );
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-2);
  scroll-snap-align: center;
}

.slot-box {
  position: relative;
  aspect-ratio: 1500 / 2100;
}

/* Until the card is made: a frame the card's shape, dashed, with the job's
   steps in it. The card comes in over it, and it goes once the card is up. */
.slot-steps {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-3);
  padding: var(--ak-space-4);
  border: 1px dashed var(--ak-border-strong);
  border-radius: var(--ak-radius-md);
  background: var(--ak-surface);
  overflow: hidden;
}

.slot.is-done .slot-steps {
  visibility: hidden;
  transition: visibility var(--ak-dur-slow);
}

.slot.is-failed .slot-steps {
  border-color: rgb(255 92 92 / 45%);
}

.slot-head {
  display: flex;
  align-items: center;
  gap: var(--ak-space-2);
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-semibold);
  text-transform: capitalize;
}

.slot-total {
  margin-left: auto;
  color: var(--ak-text-muted);
  font-weight: var(--ak-weight-regular);
}

/* The steps as a list, one under the other: the name and the counter on one
   line, the model and where it ran under them. The strip's tiles, laid out
   for a tall box. */
.slot .chips {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-1);
}

.slot .chip {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "name ms"
    "model where";
  align-items: baseline;
  column-gap: var(--ak-space-3);
}

.slot .chip-name {
  grid-area: name;
}

.slot .chip-ms {
  grid-area: ms;
}

.slot .chip-model {
  grid-area: model;
}

.slot .chip-where {
  grid-area: where;
}

.slot-reason {
  color: var(--ak-danger);
  font-size: var(--ak-text-small);
  line-height: 1.4;
}

.slot.is-cancelled .slot-reason {
  color: var(--ak-text-muted);
}

.slot-acts {
  display: flex;
  justify-content: center;
  gap: var(--ak-space-2);
  margin-top: auto;
}

.slot-acts .btn[aria-busy="true"] {
  opacity: 0.6;
  cursor: progress;
}

/* The card. A button: a tap, Space or Enter turns it over. */
.slot-card {
  appearance: none;
  position: absolute;
  inset: 0;
  display: block;
  padding: 0;
  border: 0;
  border-radius: var(--ak-radius-md);
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
  perspective: 2400px;
}

.slot-card:focus-visible {
  border-radius: var(--ak-radius-md);
}

/* One made after the stage came in deals into its place. results.js takes
   the class off when it has. */
.slot.is-new .slot-card {
  animation: deal var(--ak-dur-slow) var(--ak-ease-out) backwards;
}

.slot .card-inner {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform var(--ak-dur-flip) var(--ak-ease-in-out);
}

.slot.is-turned .card-inner {
  transform: rotateY(180deg);
}

.slot-face {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  backface-visibility: hidden;
  border-radius: var(--ak-radius-md);
  overflow: hidden;
  background: var(--ak-surface-2);
  box-shadow: var(--ak-shadow-md);
}

.slot-face img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.slot-back {
  transform: rotateY(180deg);
}

/* Under the card: its style, and a warning, if the card has one. One line
   is kept for them whether there is a warning or not, so a card that brings
   one moves nothing. */
.slot-label {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--ak-space-1) var(--ak-space-2);
  min-height: 24px;
  text-align: center;
}

.slot-style {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-semibold);
}

.slot-guest {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.slot-notes {
  display: contents;
}

/* Print, under each card. It holds its place while the card is being made,
   so the row does not move when the card comes. */
.slot-print.is-off {
  visibility: hidden;
}

.slot-print[aria-busy="true"] {
  opacity: 0.6;
  cursor: progress;
}

.slot-printed,
.lb-printed {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.slot-notes .warning {
  animation: rise var(--ak-dur-base) var(--ak-ease-out) backwards;
}

/* What to do with the cards: Share, Save all, Make another. */
.results-bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--ak-space-3);
}

.results-bar .btn[aria-busy="true"] {
  opacity: 0.6;
  cursor: progress;
}

.results .msg {
  margin: 0;
  text-align: center;
}

/* The stage comes in over the form, and goes again, as a view transition
   (motion.swap): the page as it was fades into the page as it is, the stage
   rises into its place, and the node's row moves to where the stage leaves
   it. The names are on only while it runs, so they change nothing else. */
.is-swapping #results {
  view-transition-name: results;
}

.is-swapping #obs {
  view-transition-name: obs;
}

::view-transition-group(*) {
  animation-duration: var(--ak-dur-slow);
  animation-timing-function: var(--ak-ease-out);
}

::view-transition-new(results) {
  animation: results-in var(--ak-dur-slow) var(--ak-ease-out) both;
}

::view-transition-old(results) {
  animation: results-out var(--ak-dur-base) var(--ak-ease-out) both;
}

@keyframes results-in {
  from {
    opacity: 0;
    transform: translateY(32px);
  }
}

@keyframes results-out {
  to {
    opacity: 0;
    transform: translateY(16px);
  }
}

/* -------------------------------------------------------------- gallery -- */

/* The Gallery view: a page as wide as the dashboard, the title and Refresh
   on one line. Two classes each, to win over .page and .page-head below. */
.page.page-wide {
  width: min(100%, 120rem);
}

.page-head.page-head-row {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--ak-space-4);
}

.page-head-text {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-2);
}

/* A tile is the front, the name, the style, and a dot when the card has
   warnings. The whole tile is one button: it opens the lightbox. */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--ak-space-4);
}

.gallery .empty {
  grid-column: 1 / -1;
}

.card {
  min-width: 0;
}

/* A new tile deals in: up from 16 px lower, from 96 percent, from nothing.
   gallery.js sets the delay, 40 ms apart through a batch, and takes the class
   off when the animation ends. */
.card.is-new {
  animation: deal var(--ak-dur-slow) var(--ak-ease-out) var(--deal-delay, 0ms) backwards;
}

@keyframes deal {
  from {
    opacity: 0;
    transform: translateY(16px) scale(0.96);
  }
}

.card-open {
  appearance: none;
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-2);
  width: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.card-open[aria-disabled="true"] {
  cursor: default;
}

.card-face {
  display: grid;
  width: 100%;
  aspect-ratio: 1500 / 2100;
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-sm);
  overflow: hidden;
  background: var(--ak-surface-2);
  transition:
    border-color var(--ak-dur-fast) var(--ak-ease-out),
    transform var(--ak-dur-fast) var(--ak-ease-out);
}

/* The image and the line that stands in for it share one cell. */
.card-face > * {
  grid-area: 1 / 1;
}

.card-open:not([aria-disabled="true"]):hover .card-face {
  border-color: var(--ak-border-strong);
  transform: translateY(-2px);
}

/* Out in the lightbox. There is one of the card on screen, not two. */
.card.is-lifted .card-face {
  visibility: hidden;
}

.card-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--ak-dur-base) var(--ak-ease-out);
}

.card-img.is-loaded {
  opacity: 1;
}

.card-pending {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--ak-space-1);
  padding: var(--ak-space-3);
  text-align: center;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

/* A card that failed or was cancelled: which, and why, in its picture's place. */
.card[data-status="failed"] .card-state {
  color: var(--ak-danger);
  font-weight: var(--ak-weight-semibold);
}

.card[data-status="cancelled"] .card-state {
  color: var(--ak-text);
  font-weight: var(--ak-weight-semibold);
}

.card-reason {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 6;
  overflow: hidden;
  font-size: var(--ak-text-caption);
  line-height: 1.4;
  text-wrap: balance;
}

.card-meta {
  display: flex;
  align-items: center;
  gap: var(--ak-space-2);
  min-width: 0;
}

.card-name {
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-semibold);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.card-style {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-caption);
  white-space: nowrap;
  margin-left: auto;
}

/* A card from a booth picture: whose it is, by the number on the picture. */
.card-guest {
  flex: none;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-caption);
  white-space: nowrap;
}

/* A shared card: a small link, in the link colour. A printed one: a small
   printer, in the colour that reports a thing done. */
.card-shared,
.card-printed {
  width: 14px;
  height: 14px;
  flex: none;
  fill: none;
  stroke: var(--ak-link);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.card-printed {
  stroke: var(--ak-success);
}

/* ------------------------------------------------------------- lightbox -- */

/* One card, big. It grows out of its tile to 80 percent of the screen's
   height (lightbox.js). The buttons and the warnings hang below it in a band
   of their own, so a card with warnings and a card without sit in the same
   place, and the arrow keys never move the card up or down. */
.lightbox {
  --lb-below: 136px;
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  padding: var(--ak-space-6);
  padding-bottom: calc(var(--ak-space-6) + var(--lb-below));
}

/* The page stays where it was while a card is open. */
html.has-lightbox,
html.has-lightbox body {
  overflow: hidden;
}

.lb-backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--ak-bg) 90%, transparent);
}

.lb-stage {
  position: relative;
}

/* 80 percent of the height, unless the width or the band below says less:
   on a phone the width decides. 1500 x 2100 is 1 to 1.4. */
.lb-card {
  appearance: none;
  display: block;
  /* A button takes the browser's font. The reason on a failed card is text. */
  font: inherit;
  color: inherit;
  height: min(
    80dvh,
    calc(100dvh - 2 * var(--ak-space-6) - var(--lb-below)),
    calc((100vw - 2 * var(--ak-space-6)) * 1.4)
  );
  aspect-ratio: 1500 / 2100;
  padding: 0;
  border: 0;
  border-radius: var(--ak-radius-md);
  background: transparent;
  cursor: pointer;
  perspective: 2400px;
}

.lb-card:focus-visible {
  border-radius: var(--ak-radius-md);
}

#lightbox .card-inner {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform var(--ak-dur-flip) var(--ak-ease-in-out);
}

#lightbox.is-flipped .card-inner {
  transform: rotateY(180deg);
}

.lb-face {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  backface-visibility: hidden;
  border-radius: var(--ak-radius-md);
  overflow: hidden;
  background: var(--ak-surface-2);
  box-shadow: var(--ak-shadow-lg);
}

.lb-face img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.lb-back {
  transform: rotateY(180deg);
}

/* A card that failed or was cancelled has no picture and no back. Its face
   says which and why, and only Delete and Close are under it. */
.lb-reason {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ak-space-3);
  max-width: 32ch;
  padding: var(--ak-space-6);
  text-align: center;
}

.lb-reason-title {
  font-size: var(--ak-text-h2);
  font-weight: var(--ak-weight-semibold);
  color: var(--ak-danger);
}

#lightbox[data-status="cancelled"] .lb-reason-title {
  color: var(--ak-text);
}

.lb-reason-text {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-body);
  line-height: 1.5;
  text-wrap: balance;
}

#lightbox[data-status]:not([data-status="done"]) [data-needs="made"] {
  display: none;
}

#lightbox[data-status]:not([data-status="done"]) .lb-card {
  cursor: default;
}

.lb-below {
  position: absolute;
  top: calc(100% + var(--ak-space-4));
  left: 50%;
  translate: -50% 0;
  width: max-content;
  max-width: calc(100vw - 2 * var(--ak-space-6));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ak-space-3);
}

.lb-actions,
.lb-warnings {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--ak-space-2);
}

/* A card has Save and Delete and its warnings. A style has its name, a line
   on how it looks and how it prints, and Use. */
#lightbox[data-kind="card"] [data-for="style"],
#lightbox[data-kind="style"] [data-for="card"] {
  display: none;
}

.lb-about {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ak-space-1);
  text-align: center;
}

.lb-title {
  font-size: var(--ak-text-h3);
  line-height: 1.25;
}

.lb-look {
  font-size: var(--ak-text-body);
}

.lb-prints {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.lb-actions .btn[aria-busy="true"] {
  opacity: 0.6;
  cursor: progress;
}

.lb-warnings .warning {
  animation: rise var(--ak-dur-base) var(--ak-ease-out) backwards;
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

.lb-below .msg {
  margin: 0;
}

/* ---------------------------------------------------------------- share -- */

/* The share dialog, over everything, the lightbox too (share.js). A panel in
   the middle: the one thing to know and a button, then the QR code, the short
   link and three buttons. The question before a second print (print.js) is
   the same panel with its question and two buttons. */
.share,
.ask {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: var(--ak-space-6);
}

/* The page stays where it was while the dialog is up. */
html:has(.share:not([hidden]), .ask:not([hidden])),
html:has(.share:not([hidden]), .ask:not([hidden])) body {
  overflow: hidden;
}

.share-backdrop,
.ask-backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--ak-bg) 88%, transparent);
}

.share-panel,
.ask-panel {
  position: relative;
  width: min(100%, 30rem);
  max-height: calc(100dvh - 2 * var(--ak-space-6));
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ak-space-3);
  padding: var(--ak-space-8) var(--ak-space-6);
  background: var(--ak-surface);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-md);
  box-shadow: var(--ak-shadow-lg);
  text-align: center;
}

.share-title,
.ask-title {
  margin: 0;
  font-size: var(--ak-text-h2);
  line-height: 1.2;
}

.share-who,
.ask-who {
  margin: 0;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-body);
}

.share-note,
.ask-note {
  margin: 0;
  font-size: var(--ak-text-lead);
}

.share-more {
  margin: 0;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.share-ask,
.share-actions,
.ask-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--ak-space-2);
}

.share-ask,
.ask-actions {
  margin-top: var(--ak-space-2);
}

.share-done {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ak-space-4);
  width: 100%;
}

.share-done[aria-busy="true"] {
  opacity: 0.6;
}

/* 22 rem: 352 px at the 16 px root and 396 px from 1400 px up, which a phone
   reads from a metre away off a tablet. The white round it is the code's own
   quiet zone, drawn in the SVG. */
.share-qr {
  display: block;
  width: min(22rem, 70vw, 48dvh);
  aspect-ratio: 1;
  border-radius: var(--ak-radius-sm);
}

.share-qr:not([src]) {
  visibility: hidden;
}

.share-link {
  font-family: var(--ak-font-mono);
  font-size: var(--ak-text-title);
  color: var(--ak-link);
  overflow-wrap: anywhere;
  text-decoration: none;
}

.share-link:hover {
  text-decoration: underline;
}

/* The page the code opens has Share on X and Share on LinkedIn. */
.share-social {
  margin: 0;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-body);
}

.share .btn[aria-busy="true"] {
  opacity: 0.6;
  cursor: progress;
}

.share-panel .msg {
  margin: 0;
}

/* -------------------------------------------------------- observability -- */

/* What the node is doing, under this tab's cards, always on show: services,
   GPUs and the session's numbers, three groups in one row. Each row is built
   once and its values move in place (node.js). */
.obs {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.9fr) minmax(0, 1.4fr);
  gap: var(--ak-space-4);
  align-items: stretch;
}

.obs-group {
  min-width: 0;
  padding: var(--ak-space-4) var(--ak-space-5);
  background: var(--ak-surface);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-md);
}

.obs-head {
  font-size: var(--ak-text-caption);
  font-weight: var(--ak-weight-semibold);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ak-blue);
  margin-bottom: var(--ak-space-2);
}

.node-rows {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-1);
}

.node-row {
  display: flex;
  align-items: center;
  gap: var(--ak-space-2);
  font-size: var(--ak-text-small);
  min-width: 0;
}

.node-label {
  width: 4.5rem;
  flex: none;
  color: var(--ak-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.node-value {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.node-value-dim {
  color: var(--ak-text-muted);
}

.node-right {
  margin-left: auto;
  flex: none;
  font-family: var(--ak-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--ak-text-caption);
  color: var(--ak-text-muted);
}

/* A service's dot says whether it is loaded. The pool's row has an empty
   one, so its words line up with the rows above it. */
.node-row .dot-none {
  visibility: hidden;
}

.node-services .node-row,
#node-services .node-row {
  gap: var(--ak-space-3);
}

.bar {
  flex: 1;
  min-width: 40px;
  height: 6px;
  border-radius: var(--ak-radius-full);
  background: var(--ak-bg);
  overflow: hidden;
}

.bar-fill {
  /* A span is inline, and height does not apply to an inline box. Without
     this the fill has no height and every bar reads as an empty track. */
  display: block;
  height: 100%;
  width: 0;
  background: var(--ak-blue);
  border-radius: var(--ak-radius-full);
  transition: width var(--ak-dur-base) var(--ak-ease-out);
}

.bar-fill.is-hot {
  background: var(--ak-orange);
}

.node-where {
  flex: none;
  margin-left: auto;
  font-family: var(--ak-font-mono);
  font-size: var(--ak-text-caption);
  color: var(--ak-text-muted);
  white-space: nowrap;
}

/* The where and the memory share the right of a service's row: memory in a
   column of its own, so the where lines up down the group. */
#node-services .node-where + .node-right {
  margin-left: 0;
  min-width: 4.5em;
  text-align: right;
}

/* A GPU's first line is its name and its temperature. The two bars under
   it hang from the same left edge as the name. */
#node-gpus [data-shape="gpu"] {
  margin-top: var(--ak-space-2);
}

#node-gpus [data-shape="gpu"]:first-child {
  margin-top: 0;
}

#node-gpus [data-shape="gpu"] .node-label {
  color: var(--ak-text);
  font-weight: var(--ak-weight-semibold);
}

#node-gpus .node-right {
  min-width: 7.5em;
  text-align: right;
}

/* Cards, the last one and the mean, from /stats, across the top. */
.node-nums {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--ak-space-2);
  margin-bottom: var(--ak-space-3);
}

.stat {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.stat-label {
  font-size: var(--ak-text-caption);
  color: var(--ak-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.stat-value {
  font-family: var(--ak-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--ak-text-h3);
  font-weight: var(--ak-weight-medium);
  white-space: nowrap;
}

/* One line per step: its p50 and p95 on the node, and a line of the cards
   this tab watched. Two columns once the group is wide enough. */
.node-stats {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--ak-space-1) var(--ak-space-6);
}

.node-stats .node-label {
  width: 3.9rem;
}

.spark {
  flex: none;
  overflow: visible;
}

.spark-line {
  fill: none;
  stroke: var(--ak-blue);
  stroke-width: 1.25;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.spark-dot {
  fill: var(--ak-orange);
}

.spark.is-empty .spark-line,
.spark.is-empty .spark-dot {
  opacity: 0;
}

/* ---------------------------------------------------------------- views -- */

/* For a screen reader only: the dashboard's heading, a link's "opens in a
   new tab", and the line that says an example was copied. */
.sr-only {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* A heading takes the focus when its view opens, so the next Tab goes into
   the view and a screen reader says where it is. It is not a control, and it
   draws no ring. */
#dashboard-title:focus-visible,
.page-title:focus-visible,
.detail-title:focus-visible {
  box-shadow: none;
}

/* Gallery, Models and How it works. A page each, in a column no wider than
   80 rem, the Gallery's wider. From 1101 px each one scrolls inside the
   shell, as the dashboard does, between a header and a footer that stay put. */
.view-page {
  flex: 1;
  min-width: 0;
  padding: var(--ak-space-8) var(--ak-space-6);
}

.page {
  width: min(100%, 80rem);
  margin: 0 auto;
}

.page-head {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-2);
  margin-bottom: var(--ak-space-6);
}

.page-title,
.detail-title {
  font-size: var(--ak-text-h1);
}

.page-lead {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-lead);
}

.page-head .claim {
  margin-top: var(--ak-space-1);
}

.page-msg {
  margin-bottom: var(--ak-space-4);
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--ak-space-2);
  border-radius: var(--ak-radius-full);
  background: color-mix(in srgb, var(--ak-blue) 14%, transparent);
  color: var(--ak-blue);
  font-size: var(--ak-text-caption);
  font-weight: var(--ak-weight-semibold);
  letter-spacing: 0.04em;
  line-height: 1.5;
  text-transform: uppercase;
  white-space: nowrap;
}

/* --------------------------------------------------------------- models -- */

/* A card per model, three to a row. A last row that is not full sits in the
   middle, so five cards are three and two with no empty slot. Six tracks, two
   to a card. Every row is as tall as the tallest card, so every card is the
   same size. */
.model-grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  gap: var(--ak-space-4);
}

.model-grid > .model-card {
  grid-column: span 2;
}

/* Two cards left for the last row start on the second track, one on the
   third. */
@media (min-width: 1101px) {
  .model-grid > .model-card:nth-child(3n + 1):nth-last-child(2) {
    grid-column: 2 / span 2;
  }

  .model-grid > .model-card:nth-child(3n + 1):last-child {
    grid-column: 3 / span 2;
  }
}

.model-card {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-1);
  min-width: 0;
  padding: var(--ak-space-5);
  background: var(--ak-surface);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-md);
  color: var(--ak-text);
  text-decoration: none;
  transition:
    border-color var(--ak-dur-fast) var(--ak-ease-out),
    background var(--ak-dur-fast) var(--ak-ease-out),
    transform var(--ak-dur-fast) var(--ak-ease-out);
}

.model-card:hover {
  border-color: var(--ak-blue);
  background: var(--ak-surface-2);
}

.model-card:active {
  transform: translateY(1px);
}

.model-card:focus-visible {
  border-radius: var(--ak-radius-md);
}

/* Comps is arithmetic. Its card sits on the page rather than on a surface. */
.model-card.is-arith {
  background: transparent;
}

.model-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ak-space-2);
  min-height: 24px;
  margin-bottom: var(--ak-space-2);
}

.model-where {
  display: inline-flex;
  align-items: center;
  gap: var(--ak-space-2);
  margin-left: auto;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  white-space: nowrap;
}

.model-name {
  font-size: 1.25rem;
  font-weight: var(--ak-weight-semibold);
  line-height: 1.25;
}

.model-provider,
.model-facts {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.model-role {
  flex: 1;
  margin: var(--ak-space-2) 0;
}

.model-id {
  margin-top: var(--ak-space-2);
  padding-top: var(--ak-space-3);
  border-top: 1px solid var(--ak-border);
  color: var(--ak-text-muted);
  font-size: var(--ak-text-caption);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* One model's page. */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--ak-space-2);
  margin-bottom: var(--ak-space-3);
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  text-decoration: none;
  transition: color var(--ak-dur-fast) var(--ak-ease-out);
}

.back-link:hover {
  color: var(--ak-text);
}

/* Tight enough that the longest model's page, Gemma's, is one screen at
   1920 x 1080. */
.detail-head {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-2);
  margin-bottom: var(--ak-space-5);
}

.detail-kicker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ak-space-2) var(--ak-space-3);
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.detail-role {
  font-size: var(--ak-text-lead);
}

/* Where the model is now, at the end of the top line, as on its card. */
.detail-status {
  display: inline-flex;
  align-items: center;
  gap: var(--ak-space-2);
  margin-left: auto;
}

/* One grid for every model: the same two columns whichever model it is, the
   facts on the left and the examples on the right, or the words where a model
   has no code. The two start on one line and end on one: the code box grows
   to the height of the facts and words beside it. Below 1101 px they stack. */
.detail-body {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--ak-space-8);
}

.detail-main,
.detail-text,
.detail-code {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-5);
  min-width: 0;
}

/* The facts as a two-column table: the name, then the value. The names' column
   is one width on every model's page, so the values line up from one page to
   the next. "Parameters" is the longest name. */
.facts {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  margin: 0;
  background: var(--ak-surface);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-md);
  overflow: hidden;
}

.fact {
  display: contents;
}

.fact-label,
.fact-value {
  margin: 0;
  padding: var(--ak-space-2) var(--ak-space-4);
  border-bottom: 1px solid var(--ak-border);
}

.fact:last-child .fact-label,
.fact:last-child .fact-value {
  border-bottom: 0;
}

.fact-label {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.fact-value {
  overflow-wrap: anywhere;
}

.detail-about {
  max-width: 68ch;
  line-height: 1.65;
}

.caps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ak-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.cap {
  padding: var(--ak-space-1) var(--ak-space-3);
  border: 1px solid var(--ak-border-strong);
  border-radius: var(--ak-radius-full);
  font-size: var(--ak-text-small);
}

.detail-section {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-2);
}

.detail-h2 {
  font-size: var(--ak-text-h3);
}

.routes {
  display: flex;
  flex-direction: column;
  gap: var(--ak-space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.detail-note {
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.detail-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ak-space-2);
}

.link-out {
  display: inline-flex;
  align-items: center;
  gap: var(--ak-space-2);
  padding: var(--ak-space-2) var(--ak-space-3);
  border: 1px solid var(--ak-border-strong);
  border-radius: var(--ak-radius-md);
  color: var(--ak-link);
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-semibold);
  text-decoration: none;
  transition: border-color var(--ak-dur-fast) var(--ak-ease-out);
}

.link-out:hover {
  border-color: var(--ak-blue);
}

.link-out:focus-visible {
  border-radius: var(--ak-radius-md);
}

/* The examples: a bar of tabs with Copy at its end, over one code box. */
.examples {
  display: flex;
  flex: 1;
  flex-direction: column;
  background: var(--ak-bg);
  border: 1px solid var(--ak-border);
  border-radius: var(--ak-radius-md);
  overflow: hidden;
}

.ex-bar {
  display: flex;
  align-items: center;
  gap: var(--ak-space-3);
  padding: 0 var(--ak-space-2);
  background: var(--ak-surface);
  border-bottom: 1px solid var(--ak-border);
}

/* The tabs scroll sideways when they do not fit, as on a phone. Copy stays. */
.ex-tabs {
  display: flex;
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}

.ex-tabs::-webkit-scrollbar {
  display: none;
}

.ex-tab {
  position: relative;
  flex: none;
  appearance: none;
  padding: var(--ak-space-3) var(--ak-space-2);
  border: 0;
  border-radius: var(--ak-radius-sm);
  background: transparent;
  color: var(--ak-text-muted);
  font-family: inherit;
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-semibold);
  line-height: 1.5;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--ak-dur-fast) var(--ak-ease-out);
}

.ex-tab:hover,
.ex-tab.is-on {
  color: var(--ak-text);
}

/* The tab on show is underlined in blue, as the view on show is in the
   header, and the line grows the same way. */
.ex-tab::after {
  content: "";
  position: absolute;
  left: var(--ak-space-2);
  right: var(--ak-space-2);
  bottom: 0;
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--ak-blue);
  transform: scaleX(0);
  transition: transform var(--ak-dur-base) var(--ak-ease-out);
}

.ex-tab.is-on::after {
  transform: scaleX(1);
}

/* Inside the tab, where the scrolling strip cannot cut it off. */
.ex-tab:focus-visible {
  box-shadow: inset 0 0 0 2px var(--ak-blue);
}

/* Wide enough for "Select and copy", so the bar never moves when it changes. */
.copy-btn {
  flex: none;
  min-width: 8.5rem;
}

.copy-btn.is-done {
  color: var(--ak-success);
  border-color: var(--ak-success);
}

/* Every example in one cell, so the box is as tall as the longest and a tab
   changes the code without moving anything. */
.ex-panels {
  display: grid;
  flex: 1;
}

.ex-panels > .example {
  grid-area: 1 / 1;
  transition: opacity var(--ak-dur-fast) var(--ak-ease-out);
}

.ex-panels > .example:not(.is-on) {
  visibility: hidden;
  opacity: 0;
}

.ex-note {
  padding: var(--ak-space-3) var(--ak-space-4);
  border-top: 1px solid var(--ak-border);
}

.code {
  margin: 0;
  padding: var(--ak-space-4);
  overflow-x: auto;
  color: var(--ak-code-text);
  font-family: var(--ak-font-mono);
  font-size: var(--ak-text-small);
  line-height: 1.6;
  tab-size: 2;
}

/* The page's ring would be cut off by the box. This one sits inside. */
.code:focus-visible {
  border-radius: 0;
  box-shadow: inset 0 0 0 2px var(--ak-blue);
}

.code code {
  font-size: inherit;
  color: inherit;
}

/* Before the colours load, and where they cannot: comment lines dimmed. */
.code-c {
  color: var(--ak-code-muted);
}

/* The colours, by the classes Prism gives its tokens. */
.code .token.comment {
  color: var(--ak-code-muted);
  font-style: italic;
}

.code .token.punctuation,
.code .token.operator {
  color: var(--ak-code-muted);
}

.code .token.keyword,
.code .token.parameter,
.code .token.variable {
  color: var(--ak-code-keyword);
}

.code .token.string {
  color: var(--ak-code-string);
}

.code .token.number,
.code .token.boolean,
.code .token.null {
  color: var(--ak-code-literal);
}

.code .token.function,
.code .token.builtin,
.code .token.class-name {
  color: var(--ak-code-name);
  font-weight: var(--ak-weight-semibold);
}

/* ------------------------------------------------------------------ how -- */

/* Three parts, one under the other: the pipeline, the node, and the notes.
   The title, its line and the claim share a row, and so does each part's
   title with the line that says what the part shows. */
.how-top {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ak-space-1) var(--ak-space-6);
  margin-bottom: var(--ak-space-4);
}

.how-top .claim {
  margin-top: 0;
}

.how-part + .how-part {
  margin-top: var(--ak-space-8);
}

.how-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ak-space-1) var(--ak-space-4);
}

.how-h2 {
  font-size: var(--ak-text-h2);
}

.how-status {
  min-height: 1.55em;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.flow,
.nodemap {
  margin-top: var(--ak-space-3);
}

.flow-svg,
.nm-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.flow text,
.nodemap text {
  font-family: var(--ak-font-sans);
}

/* A stage: a box, three lines, and its number under it. Idle is muted. Lit,
   while the card is in it, is blue. Done is settled, its number in full. */
.stage-box {
  fill: var(--ak-surface);
  stroke: var(--ak-border);
  stroke-width: 1;
  transition:
    fill var(--ak-dur-base) var(--ak-ease-out),
    stroke var(--ak-dur-base) var(--ak-ease-out);
}

.stage text {
  fill: var(--ak-text-muted);
  text-anchor: middle;
  transition: fill var(--ak-dur-base) var(--ak-ease-out);
}

.stage-name {
  font-size: var(--ak-text-title);
  font-weight: var(--ak-weight-semibold);
}

.stage-detail,
.stage-where {
  font-size: var(--ak-text-small);
}

.stage .stage-ms {
  font-family: var(--ak-font-mono);
  font-size: var(--ak-text-lead);
  fill: var(--ak-text);
}

/* Across the page each of the four in the middle has its number beside it. */
.stage .stage-ms.is-beside {
  text-anchor: start;
}

.stage[data-state="lit"] .stage-box {
  fill: color-mix(in srgb, var(--ak-blue) 24%, var(--ak-surface));
  stroke: var(--ak-blue);
  stroke-width: 2;
}

.stage[data-state="lit"] text,
.stage[data-state="done"] .stage-name,
.stage[data-state="failed"] .stage-name {
  fill: var(--ak-text);
}

/* Where a step runs turns blue while it runs. Your card has no where: its
   two lines are the two files. */
.stage[data-state="lit"]:not([data-stage="card"]) .stage-where {
  fill: var(--ak-blue);
}

.stage[data-state="done"] .stage-box {
  stroke: var(--ak-border-strong);
}

.stage[data-state="failed"] .stage-box {
  stroke: var(--ak-danger);
}

.stage[data-state="failed"] .stage-ms {
  fill: var(--ak-danger);
}

.stage[data-state="off"] .stage-box,
.stage[data-state="skipped"] .stage-box,
.stage[data-state="cancelled"] .stage-box {
  fill: none;
  stroke-dasharray: 4 4;
}

.stage[data-state="off"] .stage-ms,
.stage[data-state="skipped"] .stage-ms,
.stage[data-state="cancelled"] .stage-ms {
  fill: var(--ak-text-muted);
  font-family: var(--ak-font-sans);
  font-size: var(--ak-text-small);
}

/* Your card is card-shaped, dashed until the card reaches it. Then a picture
   of a card comes up in it: a sample card in the loop, the card itself when
   the drawing follows one. It fades in once it has loaded, and out when the
   loop starts over. */
.stage[data-stage="card"][data-state="idle"] .stage-box {
  fill: none;
  stroke-dasharray: 4 4;
}

.stage-img {
  opacity: 0;
  transform: scale(0.96);
  transform-box: fill-box;
  transform-origin: center;
  transition:
    opacity var(--ak-dur-slow) var(--ak-ease-out),
    transform var(--ak-dur-slow) var(--ak-ease-out);
}

.stage[data-state="lit"] .stage-img.is-ready {
  opacity: 1;
  transform: none;
}

/* Its three words go as the picture comes. */
.stage[data-stage="card"][data-state="lit"]:has(.stage-img.is-ready) text:not(.stage-ms) {
  opacity: 0;
}

.stage[data-stage="card"] text {
  transition:
    fill var(--ak-dur-base) var(--ak-ease-out),
    opacity var(--ak-dur-slow) var(--ak-ease-out);
}

.stage-frame {
  fill: none;
  stroke: none;
}

.stage[data-state="lit"] .stage-frame {
  stroke: var(--ak-blue);
  stroke-width: 2;
}

/* A line the card has not taken is grey. The ones it has are blue. */
.edge {
  fill: none;
  stroke: color-mix(in srgb, var(--ak-text-muted) 45%, transparent);
  stroke-width: 1.5;
  marker-end: url(#flow-arrow);
  transition: stroke var(--ak-dur-base) var(--ak-ease-out);
}

.edge.is-detour {
  stroke-dasharray: 5 5;
}

.edge.is-carried {
  stroke: var(--ak-blue);
  marker-end: url(#flow-arrow-on);
}

#flow-arrow path {
  fill: color-mix(in srgb, var(--ak-text-muted) 45%, transparent);
}

#nm-arrow path {
  fill: var(--ak-border-strong);
}

#flow-arrow-on path,
#nm-arrow-on path {
  fill: var(--ak-blue);
}

/* The card on its way. Drawn under the boxes, so it slips into the next one. */
.flow-dot {
  fill: var(--ak-blue);
  stroke: var(--ak-bg);
  stroke-width: 2;
  opacity: 0;
}

/* The node. A dashed line round what runs on it, the firewall across its top,
   and each service inside the card it runs on. */
.nm-region {
  fill: none;
  stroke: var(--ak-border-strong);
  stroke-dasharray: 6 6;
}

.nm-title {
  fill: var(--ak-text);
  font-size: var(--ak-text-title);
  font-weight: var(--ak-weight-semibold);
  paint-order: stroke;
  stroke: var(--ak-bg);
  stroke-width: 6px;
  stroke-linejoin: round;
}

.nm-fw {
  fill: none;
  stroke: var(--ak-text-muted);
  stroke-width: 2;
  stroke-dasharray: 2 4;
}

.nm-fw-text,
.nm-way-text {
  fill: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  paint-order: stroke;
  stroke: var(--ak-bg);
  stroke-width: 6px;
  stroke-linejoin: round;
}

.nm-way-text {
  fill: var(--ak-text);
}

.nm-wire {
  fill: none;
  stroke: var(--ak-border-strong);
  stroke-width: 1.5;
}

.nm-into {
  marker-end: url(#nm-arrow);
}

.nm-way {
  stroke: var(--ak-blue);
  marker-end: url(#nm-arrow-on);
}

.nm-gate {
  fill: var(--ak-bg);
  stroke: var(--ak-blue);
  stroke-width: 2;
}

.nm-box rect {
  fill: var(--ak-surface);
  stroke: var(--ak-border);
}

.nm-service rect {
  fill: var(--ak-surface-2);
  stroke: var(--ak-border-strong);
}

.nm-volume rect {
  fill: none;
  stroke: var(--ak-border-strong);
  stroke-dasharray: 3 3;
}

.nm-group > rect {
  fill: color-mix(in srgb, var(--ak-surface) 55%, transparent);
  stroke: var(--ak-border);
}

/* The bus drops into each service through its group's name. The name has a
   rim of its group's own colour, so the line stops short of the letters. */
.nm-group-name {
  fill: var(--ak-text);
  font-size: var(--ak-text-small);
  font-weight: var(--ak-weight-semibold);
  paint-order: stroke;
  stroke: color-mix(in srgb, var(--ak-surface) 55%, var(--ak-bg));
  stroke-width: 6px;
  stroke-linejoin: round;
}

.nm-name {
  fill: var(--ak-text);
  font-size: var(--ak-text-title);
  font-weight: var(--ak-weight-semibold);
}

.nm-line {
  fill: var(--ak-text-muted);
  font-size: var(--ak-text-small);
}

.nm-dot {
  fill: var(--ak-text-muted);
}

.nm-dot.dot-ok {
  fill: var(--ak-success);
}

.nm-dot.dot-warn {
  fill: var(--ak-warning);
}

.nm-dot.dot-bad {
  fill: var(--ak-danger);
}

/* Five notes. A column each from a laptop up, two on a tablet, one on a
   phone. Words, not boxes: a line over each keeps them apart. */
.how-notes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--ak-space-5) var(--ak-space-6);
}

.how-note {
  padding-top: var(--ak-space-3);
  border-top: 1px solid var(--ak-border);
}

.how-note-title {
  margin-bottom: var(--ak-space-1);
  font-size: var(--ak-text-title);
}

.how-note p {
  max-width: 68ch;
  color: var(--ak-text-muted);
  font-size: var(--ak-text-small);
  line-height: 1.45;
}

.how-note code {
  color: var(--ak-text);
}

/* --------------------------------------------------------- the stage -- */

/* One drawing at a time, in front of the page, for a talk (focus.js). The
   part's frame, its title, its line and its drawing, is lifted onto a stage
   the size of the window, 48 px in from every side, and the page behind it is
   dimmed as it is behind the lightbox. The frame is the same element, so the
   drawing inside it keeps running. Its drawing is laid out again to fill the
   width. */
.how-focus-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--ak-space-2);
  margin-left: auto;
  align-self: center;
}

.how-focus-btn svg {
  width: 14px;
  height: 14px;
  stroke: currentColor;
  stroke-width: 2;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* A double tap on a drawing opens it. No zoom, and no wait for one. */
.flow,
.nodemap {
  touch-action: manipulation;
}

.how-close {
  display: none;
}

/* Dimmed further than behind the lightbox: a card there is solid, and a
   drawing here is lines, which the page's own would run through. */
.how-shade {
  position: fixed;
  inset: 0;
  z-index: 44;
  background: color-mix(in srgb, var(--ak-bg) 96%, transparent);
}

/* The page stays where it was while a drawing is up. Only the root is held:
   body is 100 percent tall, and held too it would clip the page and take a
   phone's scroll back to the top. */
html.has-focus {
  overflow: hidden;
}

/* The title and its line over the drawing, in the middle of the window's
   height. A drawing taller than the window, as on a phone, starts at the top
   and scrolls on the stage. */
.how-frame.is-focus {
  position: fixed;
  inset: 0;
  z-index: 45;
  display: flex;
  flex-direction: column;
  justify-content: safe center;
  padding: var(--ak-space-12);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.how-frame.is-focus:focus {
  outline: none;
}

/* Room on the right for Close. */
.how-frame.is-focus .how-head {
  padding-right: 8rem;
}

.how-frame.is-focus .how-h2 {
  font-size: var(--ak-text-h1);
}

.how-frame.is-focus .how-status {
  font-size: var(--ak-text-h3);
}

.how-frame.is-focus .how-focus-btn {
  display: none;
}

/* Close, in the corner the drawing's right edge and the title's top make. */
.how-frame.is-focus .how-close {
  display: inline-flex;
  position: fixed;
  top: var(--ak-space-12);
  right: var(--ak-space-12);
}

.how-frame.is-focus .flow,
.how-frame.is-focus .nodemap {
  flex: none;
  margin-top: var(--ak-space-6);
}

/* On a stage the pipeline's names are 1 rem and its numbers 1.25, each under
   its box, before the drawing is scaled up: flow.js lays it out for them. */
.flow[data-fill] .stage-name {
  font-size: var(--ak-text-lead);
}

.flow[data-fill] .stage .stage-ms {
  font-size: 1.25rem;
}

/* The node map's names and title on a stage are 1 rem too (how.js). */
.nodemap[data-fill] .nm-name,
.nodemap[data-fill] .nm-title {
  font-size: var(--ak-text-lead);
}

/* A word in the number's place, "when asked" or "skipped", stays small. */
.flow[data-fill] .stage:is([data-state="off"], [data-state="skipped"], [data-state="cancelled"]) .stage-ms {
  font-size: var(--ak-text-small);
}

/* From a laptop up the view is one screen: the pipeline over the node and
   the notes. The page takes the width, the parts sit closer, and the notes
   are five columns. */
@media (min-width: 1101px) {
  #view-how {
    padding-top: var(--ak-space-3);
    padding-bottom: var(--ak-space-3);
  }

  .page.how-page {
    width: min(100%, 110rem);
  }

  .how-top .claim {
    margin-left: auto;
  }

  .how-part + .how-part {
    margin-top: var(--ak-space-4);
  }

  .how-notes {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}

/* ------------------------------------------------------------ responsive -- */

/* A 1080p projector, and the laptops that should look like it: 1440, 1470
   and 1512 px wide. From 1400 px the root is 18 px, so every rem size above
   grows by an eighth. Spacing stays in px. */
@media (min-width: 1400px) {
  :root {
    font-size: 18px;
    --ak-text-counter: 24px;
  }


  /* A running job, read from a few metres. The tiles' side padding stays
     8 px: at 1400 px a tile has about 104 px for text, and "4321 ms" at
     24 px is 101. */
  .run-top {
    gap: var(--ak-space-4);
  }

  .run-name,
  .run-total {
    font-size: var(--ak-text-h3);
  }

  .run-total {
    color: var(--ak-text);
  }

  .run-style {
    font-size: var(--ak-text-body);
  }

  .chip {
    padding: var(--ak-space-3) var(--ak-space-2);
  }

  .chip-name,
  .chip-model,
  .chip-where {
    font-size: var(--ak-text-small);
  }

}

/* From 1600 px a tile has about 125 px, and the counter goes to 28 px. */
@media (min-width: 1600px) {
  :root {
    --ak-text-counter: 28px;
  }
}

/* The session's step lines side by side, three rows of two, once the group
   is wide enough for "3674 ms" and "p95 11732" on a line: about 690 px of
   content at 1920. At 1512 two of them would cut the median short. */
@media (min-width: 1700px) {
  .node-stats {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Stacked, as on a tablet held upright, the style panel is the page's width.
   Once three 200 px tiles fit a row, every tile is 200 to 240 px, in the
   middle. */
#panel-style {
  container: style-panel / inline-size;
}

@container style-panel (min-width: 616px) {
  .tiles {
    grid-template-columns: repeat(3, minmax(200px, 240px));
    justify-content: center;
  }
}

/* A tablet held upright. The logo, the four views and Sign out fill the
   header, and the title would be cut short. It comes back on a phone, where
   the views have a row of their own. */
@media (min-width: 641px) and (max-width: 900px) {
  .header-div,
  .header-title {
    display: none;
  }
}

/* Below 1000 px the four views, the pill and Sign out do not fit one line.
   The pill goes, as it does on a phone: the observability row says the same
   and more. */
@media (min-width: 641px) and (max-width: 999px) {
  .header .pill {
    display: none;
  }
}

/* Tablet portrait and small laptops: the observability row stacks, and the
   model and note grids go to two across. */
@media (max-width: 1100px) {
  .obs {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Two cards to a row, and a last card on its own in the middle. */
  .model-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .model-grid > .model-card:nth-child(2n + 1):last-child {
    grid-column: 2 / span 2;
  }

  .detail-body {
    grid-template-columns: minmax(0, 1fr);
  }

  .how-note:last-child {
    grid-column: span 2;
  }
}

/* Phone: 390x844. Stacked, tighter padding, the node panel in one column. */
@media (max-width: 640px) {
  /* Two rows. Logo, title and sign out, then the three views as icons with
     their names under them, across the page. The pill's numbers are already
     on the node summary line at the bottom, so it goes. */
  .header {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "left right"
      "nav nav";
    column-gap: var(--ak-space-2);
    row-gap: 0;
    padding: 0 var(--ak-space-4);
  }

  .header-left {
    grid-area: left;
  }

  .header-right {
    grid-area: right;
  }

  .nav {
    grid-area: nav;
    gap: 0;
    margin: 0 calc(-1 * var(--ak-space-4));
    border-top: 1px solid var(--ak-border);
  }

  .nav-link {
    flex: 1 1 0;
    flex-direction: column;
    justify-content: center;
    gap: 2px;
    padding: var(--ak-space-2) 0;
    font-size: var(--ak-text-caption);
  }

  .nav-link::after {
    left: 30%;
    right: 30%;
  }

  .nav-icon {
    display: block;
  }

  .view-page {
    padding: var(--ak-space-6) var(--ak-space-4);
  }

  .model-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .model-grid > .model-card,
  .model-grid > .model-card:nth-child(2n + 1):last-child {
    grid-column: auto;
  }

  /* The status takes the next line when it does not fit beside the kind. */
  .detail-status {
    margin-left: 0;
  }

  /* Every example's tab in sight: they wrap, and Copy takes a line of its own. */
  .ex-bar {
    flex-wrap: wrap;
    padding-bottom: var(--ak-space-2);
  }

  .ex-tabs {
    flex: 1 1 100%;
    flex-wrap: wrap;
    overflow-x: visible;
  }

  .copy-btn {
    margin-left: auto;
  }

  .how-notes {
    grid-template-columns: minmax(0, 1fr);
  }

  .how-note:last-child {
    grid-column: auto;
  }

  /* The stage keeps 16 px from the edges. */
  .how-frame.is-focus {
    padding: var(--ak-space-4);
  }

  .how-frame.is-focus .how-head {
    padding-right: 6rem;
  }

  .how-frame.is-focus .how-close {
    top: var(--ak-space-4);
    right: var(--ak-space-4);
  }

  .header-title {
    font-size: var(--ak-text-title);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .header-left {
    min-width: 0;
  }

  .header .pill {
    display: none;
  }

  .main {
    padding: var(--ak-space-4);
    gap: var(--ak-space-4);
  }

  .foot {
    padding: var(--ak-space-2) var(--ak-space-4);
  }

  /* The jersey switch goes under the photo. */
  .photo-body {
    flex-direction: column;
    align-items: stretch;
    gap: var(--ak-space-3);
  }

  .jersey {
    width: auto;
    padding-top: 0;
  }

  /* Generate takes the width, and its note and Start over share the line
     under it. */
  .generate .btn-xl {
    width: 100%;
    min-width: 0;
  }

  .panel {
    padding: var(--ak-space-4);
  }

  .grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .chips {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .gallery {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .obs-group {
    padding: var(--ak-space-4);
  }

  .signin-card {
    padding: var(--ak-space-6);
  }
}
