/*
 * library.css. GENERATED by shared/assets/build-library-css.sh. Do not edit.
 * Source of truth is shared/assets/css/components/<name>.css, one file each.
 * Order is base before extensions, see the note in the build script.
 */

/* ---- action-card.css ---- */
/*
 * action-card.css. The mascot over a list of next steps, on the suite navy. Entry [292], from
 * sitemapify-v3.html's .actioncard and .act. The mockup's radial gradient was three literal hexes;
 * this is the navy (--brand) with its hover step for the rows, so it re-skins with the suite.
 *
 * Words on the navy are --brand-ink (white, 14.28:1). The primary row is the action fill with
 * --action-ink, the pair tools/check-themes.php measures for every theme. Every row is at least
 * --touch-min tall.
 */
.c-action-card {
  background: var(--brand);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  color: var(--brand-ink);
  overflow: hidden;
}

.c-action-card__top {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-5) var(--space-4);
}

.c-action-card__says { min-width: 0; }
.c-action-card__says b { display: block; font-size: var(--text-md); font-weight: var(--weight-bold); overflow-wrap: anywhere; }
.c-action-card__says span { display: block; margin-top: var(--space-1); font-size: var(--text-xs); opacity: 0.72; }

.c-action-card__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding: 0 var(--space-4) var(--space-5);
  list-style: none;
}

.c-action-card__form { margin: 0; }

.c-action-card__act {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-3) var(--space-4);
  background: var(--brand-hover);
  border: var(--border-width) solid var(--brand-hover);
  border-radius: var(--radius-sm);
  color: var(--brand-ink);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease);
}

.c-action-card__act:hover { border-color: var(--brand-border); }
.c-action-card__act:focus-visible { outline-color: var(--brand-accent); }

.c-action-card__ic {
  display: grid;
  place-items: center;
  flex: none;
  width: var(--icon-face);
  height: var(--icon-face);
  border-radius: var(--radius-sm);
  background: var(--brand);
}

.c-action-card__ic svg,
.c-action-card__ar svg { width: var(--space-6); height: var(--space-6); fill: none; stroke: currentColor; }

.c-action-card__tx { flex: 1 1 auto; min-width: 0; }
.c-action-card__tx b { display: block; font-size: var(--text-sm); font-weight: var(--weight-semi); }
.c-action-card__tx small { display: block; font-size: var(--text-xs); opacity: 0.72; }
.c-action-card__ar { flex: none; opacity: 0.6; }

.c-action-card__act--primary {
  background: var(--action);
  border-color: var(--action);
  color: var(--action-ink);
}

.c-action-card__act--primary:hover { border-color: var(--action-ink); }
.c-action-card__act--primary .c-action-card__ic { background: var(--action-hover); }

/* ---- agent-strip.css ---- */
/*
 * agent-strip.css. Where the mascot stands on a screen that has no process. Entry [182].
 *
 * ## Two states and the quiet one is not a broken banner
 *
 * A screen with something true to say gets the strip: a tinted band above the content with the
 * figure, a label and one sentence. A screen with nothing true to say gets the SAME component
 * with no chrome at all, the figure alone. That is deliberate. An eyebrow over an empty line
 * reads as a component that failed to load, and a band with nothing in it reads as a bug; a
 * small robot floating above the first panel reads as an agent that is present and has nothing
 * to report, which is exactly what it is.
 *
 * ## The colour is the mockup's, translated to tokens rather than copied
 *
 * The prototype paints `linear-gradient(120deg, var(--blue-50), #FFFFFF 70%)` over
 * `1px solid var(--blue-100)`. House rule 9: no value comes out of that file. --brand-subtle is
 * the suite's tint of the navy and --brand-border its line, so the gradient is those two, and it
 * fades to --surface rather than to white so it is right on every theme. The eyebrow is --brand
 * on --brand-subtle, 11.94:1 by the table in tokens.css, and NOT --brand-ink, which is white and
 * is the colour of text ON the navy. Nothing here is per app except the mascot's own antenna.
 *
 * ## No status colour, ever
 *
 * The strip never turns amber, red or green. A tint that changed with the state of somebody's
 * site would be this band answering a question the sentence inside it is already answering, in a
 * register nobody can read precisely: design.md 3a, and the same argument mascot.css makes for
 * keeping the palette identical across all five motions.
 */

.c-agent-strip {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-5);
  min-width: 0;
  background: linear-gradient(120deg, var(--action-subtle), var(--surface) 70%);
  border: 1px solid var(--action-border);
  border-radius: var(--radius-lg);
}

/* Basis 0, not auto: under the wrap below a text whose basis is its own width drops under the
   mascot at 360; at 0 it takes the row's remainder and the file's shape, the figure beside the
   words, holds at every width. Entry [241]. */
.c-agent-strip__text {
  flex: 1 1 0;
  min-width: 0;
}

.c-agent-strip__eyebrow {
  margin: var(--space-0);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--action-text);
}

.c-agent-strip__says {
  margin: var(--space-1) var(--space-0) var(--space-0);
  overflow-wrap: anywhere; /* a domain is one word; at 360 it broke the page, not itself. Entry [272]. */
  font-size: var(--text-md);
  font-weight: var(--weight-semi); /* the file's 600, entry [242] */
  color: var(--ink);
  text-wrap: pretty;
}

/* The second line, entry [237]: the method under the finding, muted, one size down. */
.c-agent-strip__sub {
  margin: var(--space-1) var(--space-0) var(--space-0);
  overflow-wrap: anywhere;
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-2);
  text-wrap: pretty;
}

.c-agent-strip__act {
  flex: none;
}

/*
 * QUIET. Chrome off, figure only, and the margin stays so the content below sits where it does
 * on every other screen. A screen that has nothing to say must not also move its own layout.
 */
.c-agent-strip--quiet {
  padding: var(--space-0);
  background: none;
  border: 0;
}

/*
 * The strip stacks on a phone and the button goes full width under the sentence, because a
 * 44px target beside a 4.5rem figure on a 360px screen leaves the sentence about nine characters
 * wide. Breakpoint from the token scale, CLAUDE.md house rule 10; it was 767px, which is not on
 * the scale since [239] moved --bp-md, and is 640 since [241].
 */
@media (max-width: 640px) {
  .c-agent-strip {
    flex-wrap: wrap;
  }

  /* The first run figure (lg, 11rem) stacks above the words under 640 rather than beside them:
     beside, it left the sentence a 102px column and a domain scrolled the page. Entry [272],
     seen by twelve of thirteen wave 3 agents on onboarding step 1 at 360. */
  .c-agent-strip > .c-mascot--lg {
    flex: 0 0 100%;
    justify-content: center;
  }

  .c-agent-strip__act,
  .c-agent-strip__act > * {
    width: 100%;
  }
}

/* ---- area-chart.css ---- */
/*
 * area-chart.css. Several series over one run of days, each a tinted area under a line. Entry
 * [292]. The plot is a 700 by 170 viewBox stretched to the column at --plot-h tall, so the strokes
 * are non-scaling and keep their width at any size.
 *
 * The two tones are the owner's "clicks blue and impressions orange", named in tokens:
 *   blue    --brand-border, the suite's one blue; Sitemapify's --action is its orange since [243]
 *   orange  --brand-accent, the logo's orange
 * A lone day between two gaps is drawn as a dot: a zero length path with a round cap.
 */
.c-area {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  margin: 0;
}

.c-area__plot {
  display: block;
  width: 100%;
  height: var(--plot-h);
  overflow: visible;
}

.c-area__grid {
  stroke: var(--line);
  stroke-width: 1;
  stroke-dasharray: 3 4;
  vector-effect: non-scaling-stroke;
}

.c-area__series { --c-area-ink: var(--brand-border); }
.c-area__series--blue { --c-area-ink: var(--brand-border); }
.c-area__series--orange { --c-area-ink: var(--brand-accent); }

.c-area__fill { fill: var(--c-area-ink); fill-opacity: 0.08; stroke: none; }

.c-area__line {
  fill: none;
  stroke: var(--c-area-ink);
  stroke-width: 2.2;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

.c-area__axis {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-3);
}

.c-area__legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-xs);
  color: var(--ink-2);
}

.c-area__legend li { display: inline-flex; align-items: center; gap: var(--space-3); min-width: 0; }
.c-area__legend b { font-family: var(--font-mono); font-weight: var(--weight-semi); color: var(--ink); }
.c-area__legend .c-area__scale { color: var(--ink-3); margin-left: auto; }

.c-area__key {
  flex: none;
  width: var(--space-4);
  height: var(--space-4);
  border-radius: var(--radius-xs);
  background: var(--brand-border);
}

.c-area__key--orange { background: var(--brand-accent); }

@media (max-width: 640px) {
  .c-area__plot { height: var(--plot-h-sm); }
  .c-area__legend .c-area__scale { margin-left: 0; }
}

/* ---- auth-panel.css ---- */
/*
 * auth-panel.css. The brand panel on the signed out pages: the coloured half of the split,
 * its wordmark, its 40px headline and the artwork bleeding off the bottom edge.
 *
 * Split out of login.css on the day it was written, because the two together are 234 lines
 * and house rule 5 caps a component stylesheet at 200. The seam is a real one rather than an
 * arbitrary cut: this file is the panel, login.css is the form column and the frame that
 * holds both. shared/views/partials/auth-panel.php is the markup, and it is one partial for
 * all six screens.
 *
 * Measured from signin-one.html into docs/component-specs.md under "The brand panel".
 *
 * The ground is --brand rather than the template's slate-100 (app.css 11069), ruled
 * 2026-08-24: apps get their own auth pages and the panel has to theme with them. That is
 * the one deliberate divergence here, and it is why the artwork is drawn in currentColor
 * rather than shipped as a fixed palette file.
 */

/*
 * position and z-index together open a stacking context, which is what keeps the art's
 * z-index: -1 above this element's own background instead of behind the page. The template
 * does the same thing with `relative z-[1]` on signin-one.html:33.
 */
/*
 * The ground is flat --brand, and the color-mix that used to be here is gone. Entry [053].
 *
 * The mix existed for one reason: --brand was lightness 0.47 and a saturated magenta at 720
 * by 812 advanced instead of receding, with the artwork on it having nothing to separate
 * against. --brand is now lightness 0.28. That condition no longer exists, so neither does
 * the mechanism.
 *
 * It was also actively harmful at this darkness, which is the part worth recording. --ink is
 * a blue black at hue 222, so mixing toward it drags the hue and flattens the saturation:
 *
 *   flat --brand   hue 316, saturation 78 percent   the logo
 *   mixed 88%      hue 313, saturation 68 percent
 *   mixed 66%      hue 310, saturation 54 percent   a dark plum, not the brand
 *
 * At 66 percent, the value that shipped against the lighter brand, the panel had stopped
 * being the logo colour at all. Four candidates were rendered and looked at before this was
 * cut, the same way it was added.
 *
 * White on the flat brand is 9.60:1.
 */
.c-login__brand {
  position: relative;
  z-index: 1;
  overflow: hidden;
  background: var(--brand);
  color: var(--brand-ink);
}

.c-login__brand-inner {
  position: relative;
  max-width: var(--auth-panel-max);
  padding: var(--auth-inset) var(--space-8) var(--space-8) var(--auth-inset);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* mb-10 under the wordmark, signin-one.html:36. */
.c-login__brand .c-login__mark {
  color: var(--brand-ink);
  margin-bottom: var(--space-9);
}

/* .left-column h4, app.css 11085: 40px on a 48px line box. */
.c-login__headline {
  font-size: var(--text-display);
  line-height: var(--leading-display);
  font-weight: var(--weight-normal);
  color: var(--brand-ink);
}

/* The template sets the second half of the same line one weight up, signin-one.html:41. */
.c-login__headline-em { font-weight: var(--weight-bold); }

.c-login__blurb {
  margin-top: var(--space-5);
  color: var(--brand-ink);
  opacity: 0.82;
  font-size: var(--text-md);
  line-height: var(--leading-loose);
  max-width: 42ch;
}

.c-login__art-slot {
  position: absolute;
  inset-inline: 0;
  bottom: var(--auth-art-drop);
  height: 100%;
  z-index: -1;
  pointer-events: none;
}

/*
 * The template uses object-contain on an <img>. Ours is an inline <svg>, which is not a
 * replaced element, so object-fit would do nothing. It does not need to: a viewBox at
 * 100% by 100% already scales with the default preserveAspectRatio="xMidYMid meet", which
 * is the same geometry. The viewBox matches ils1.svg's so the slot behaves identically.
 */
.c-login__art {
  display: block;
  width: 100%;
  height: 100%;
  color: var(--brand-ink);
  opacity: 0.92;
}

.c-login__art-accent { color: var(--brand-accent); }

/*
 * The knockout, entry [131]. One shape in the whole set is SOLID rather than a tint: the badge
 * riding on art_route()'s curve, in three of the twelve drawings. Its glyph is cut out of that
 * disc, so it takes the ground the disc sits on and not the ink.
 *
 * `--brand` and not white, because the ground here IS --brand: .c-login__brand paints it flat,
 * and a white glyph would be right on this panel by coincidence and wrong the moment anything
 * reuses the motif on another surface.
 */
.c-login__art-knockout { stroke: var(--brand); }


@media (min-width: 1440px) {
  .c-login__art-slot { bottom: var(--auth-art-drop-xl); }
}

/* The template drops this column below 1024px rather than stacking it. login.css says why. */
@media (max-width: 1180px) {
  .c-login__brand { display: none; }
}

/* ---- banner.css ---- */
/*
 * banner.css. The notice pattern, entry [291], from the approved file
 * docs/appslist/sitemapify/sitemapify-ui-fixes.html (`.notice`): a grid of a 38px mark, the
 * body and the action; the mark a rounded square tinted by tone with the icon inside; the title
 * bold on its own line and one muted line of detail under it; the action a small secondary
 * button on the right, top aligned; a 4px left edge in the tone colour. Every banner in the
 * suite and every flash message after a redirect.
 *
 * The lamp dot and the inline variant word were the strip this replaced, and they are gone.
 *
 * Tone is three local custom properties (edge, tint, ink), so a variant is three lines. The
 * inks are the semantic *-ink values because the file's amber and red are exactly those two;
 * info is the suite navy on its subtle, because --info is a cyan an icon cannot be read in and
 * --action is each app's accent, not a tone.
 *
 * Measurements the scale does not carry are written from tokens: the 38px mark is
 * --space-8 + --space-2 + --space-1, the 4px edge is four --border-width, the 18px side pad is
 * --alert-pad-y, the file's 14px gap is --space-4 + --space-1.
 */

.c-banner {
  --c-banner-edge: var(--warn-ink);
  --c-banner-tint: var(--warning-subtle);
  --c-banner-ink: var(--warn-ink);
  display: grid;
  grid-template-columns: calc(var(--space-8) + var(--space-2) + var(--space-1)) minmax(0, 1fr) auto;
  align-items: start;
  gap: calc(var(--space-4) + var(--space-1));
  margin-bottom: var(--space-6);
  padding: var(--space-5) var(--alert-pad-y);
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-left: calc(var(--border-width) * 4) solid var(--c-banner-edge);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink);
}

.c-banner__mark {
  display: grid;
  place-items: center;
  width: calc(var(--space-8) + var(--space-2) + var(--space-1));
  height: calc(var(--space-8) + var(--space-2) + var(--space-1));
  border-radius: var(--radius-sm);
  background: var(--c-banner-tint);
  color: var(--c-banner-ink);
}

.c-banner__mark svg {
  width: var(--space-6);
  height: var(--space-6);
  fill: currentColor;
}

/* min-width 0 and anywhere-wrapping so a 2,048 character address wraps inside the strip
   instead of pushing the page sideways. */
.c-banner__body {
  min-width: 0;
  overflow-wrap: anywhere;
}

.c-banner__title {
  margin: 0;
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  color: var(--ink);
}

.c-banner__text {
  margin: var(--space-1) 0 0;
  color: var(--ink-3);
}

.c-banner__action {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding-top: var(--space-2);
}

/* The chip is --control-h-sm, the file's small button. The hit area is the 44px floor from
   house rule 10, laid over it by its own pseudo element. */
.c-banner__dismiss {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--control-h-sm);
  margin-top: var(--space-2);
  padding: var(--space-3) var(--space-4);
  background: none;
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  color: var(--ink-2);
  cursor: pointer;
}

.c-banner__dismiss::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100%;
  min-width: var(--touch-min);
  height: var(--touch-min);
  transform: translate(-50%, -50%);
}

.c-banner__dismiss:hover {
  color: var(--ink);
  border-color: var(--ink-3);
}

/* A dismissable banner with an action has four columns, not three. */
.c-banner:has(.c-banner__action):has(.c-banner__dismiss) {
  grid-template-columns: calc(var(--space-8) + var(--space-2) + var(--space-1)) minmax(0, 1fr) auto auto;
}

.c-banner--info {
  --c-banner-edge: var(--brand-border);
  --c-banner-tint: var(--brand-subtle);
  --c-banner-ink: var(--brand);
}

.c-banner--success {
  --c-banner-edge: var(--success-ink);
  --c-banner-tint: var(--success-subtle);
  --c-banner-ink: var(--success-ink);
}

.c-banner--warning {
  --c-banner-edge: var(--warn-ink);
  --c-banner-tint: var(--warning-subtle);
  --c-banner-ink: var(--warn-ink);
}

.c-banner--danger {
  --c-banner-edge: var(--danger-ink);
  --c-banner-tint: var(--danger-subtle);
  --c-banner-ink: var(--danger-ink);
}

/* Under the small breakpoint the action and the dismiss drop beneath the words, in the body's
   column, rather than squeezing the title to two words wide. */
@media (max-width: 640px) {
  .c-banner,
  .c-banner:has(.c-banner__action):has(.c-banner__dismiss) {
    grid-template-columns: calc(var(--space-8) + var(--space-2) + var(--space-1)) minmax(0, 1fr);
  }

  .c-banner__action,
  .c-banner__dismiss {
    grid-column: 2;
    justify-self: start;
    padding-top: 0;
  }
}

/* ---- banner.brand.css ---- */
/*
 * banner.brand.css. The value line's variant, entry [292]: the one callout in the app's own
 * action colour, "in brand orange", from sitemapify-v3.html's .valuecall. Edge and mark are the
 * action fill (Sitemapify's orange); the ground is its tint; the words stay ink, because orange
 * as text fails on white (tokens.css, --action-text). The mark's glyph is --action-ink, the navy
 * on the orange, which is the pair check-themes measures.
 */
.c-banner--brand {
  --c-banner-edge: var(--action);
  --c-banner-tint: var(--action);
  --c-banner-ink: var(--action-ink);
  margin-bottom: 0;
  align-items: center;
  background: var(--action-subtle);
  border-color: var(--action-border);
  border-left-color: var(--action);
}

.c-banner--brand .c-banner__title {
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
}

.c-banner--brand .c-banner__text { color: var(--ink-2); }
.c-banner--brand .c-banner__action { padding-top: 0; }

/* ---- bar-row.css ---- */
/*
 * bar-row.css. Lamp slot, flexible label, right aligned value or meter.
 * design.md section 5, 5 screens, plus every labelled meter row.
 *
 * DERIVED, not read from the template: the template has no bar row. It takes the
 * vertical rhythm of .table-td (app.css 10798), --cell-pad-y above and below a
 * 0.875rem line, which computes to the same 60px a table row does. That is the point:
 * a panel with bar rows down one side and a table down the other lines up.
 *
 * No margin on the root, ever. This drops into a panel body and into a table cell
 * unchanged, and a margin here would push the table cell open. Whatever stacks these
 * rows owns the gap between them.
 *
 * Inside a table cell the cell already spends --cell-pad-y, so the row gives its own
 * back rather than paying twice. The usage screen is the case: it puts a bar row in
 * the Used column.
 */

/* 12px of padding on a 20px line box is the approved file's 44px row, entry [243]; it was the
   table cell's 20px, a 60px row, from [087] to then. */
.c-bar-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0 var(--space-4);
  padding: var(--space-4) 0;
  min-width: 0;
}

.c-table__td .c-bar-row { padding-block: 0; }

.c-bar-row__lamp {
  display: inline-flex;
  flex: none;
}

/* The label WRAPS, entry [243], the file's own; it truncated with an ellipsis before, which at
   360 cut "store-products-sitemap.xml" to its first eleven letters. A metered row keeps nowrap
   below, for the reason given there. */
.c-bar-row__label {
  flex: 1 1 8rem;
  min-width: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.c-bar-row__meter {
  display: block;
  flex: 1;
  min-width: 0;
}

/*
 * The value WRAPS, entry [251]. It was `flex: none; white-space: nowrap`, which is right for a
 * short readout ("1,088", "24 of 1,012") and wrong for the one caller that puts a sentence here:
 * a scan step's detail ("100 checked, 0 open, nothing entered or left, 37 changed with nothing
 * held back"). A value that cannot shrink and cannot wrap took the whole row and squeezed the
 * label, which is flex: 1, down to zero, so at 360 every step label became a column of single
 * letters and at 1280 the value itself was clipped. The first scan screen is the first thing an
 * install sees, so this was the worst place for it. Now the value may shrink (flex-shrink 1) and
 * wrap (overflow-wrap), and max-width keeps it off the LABEL's minimum, which is the other half
 * of the ruling: whatever the value's length, the label keeps at least 40 per cent to read in.
 * A number is short and never reaches the cap, so tabular readouts are unchanged.
 */
.c-bar-row__value {
  flex: 1 1 auto;
  min-width: 9rem;
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--ink-2);
  overflow-wrap: anywhere;
}

/*
 * With a meter present the label stops stretching, because the bar is the thing that
 * should take the free space. The label still truncates rather than wrapping, so a
 * long plan name cannot make the row two lines tall.
 */
.c-bar-row--metered .c-bar-row__label {
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * mono and detail, entry [237]. A file name label in the mono face, and one toned word after
 * the value. The value keeps margin-left: auto, so the detail hangs off its right; on a
 * narrow screen the row wraps and the detail drops under the value rather than shrinking it.
 */
.c-bar-row__label--mono {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

.c-bar-row__detail {
  flex: none;
  min-width: 6.5rem;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  line-height: var(--leading-base);
  text-align: right;
  white-space: nowrap;
}

.c-bar-row__detail--success { color: var(--success-ink); }
.c-bar-row__detail--warning { color: var(--warn-ink); }
.c-bar-row__detail--danger  { color: var(--danger-ink); }
.c-bar-row__detail--mute    { color: var(--ink-3); }

/* ---- button.css ---- */
/*
 * button.css. The permission gated control. design.md section 5.
 *
 * Geometry is the template's .btn, app.css 8731: 44px tall, 10px over 1rem of
 * padding, 0.875rem semibold, 0.25rem radius, 150ms. The template's capitalize is
 * deliberately not copied, entry [047]. Not the prototype's
 * 29px uppercase chip. docs/component-specs.md carries the line for every value.
 *
 * The same rules style both elements this component renders, a <button> and an <a>,
 * so a drill down link and a write action look identical and neither one is a
 * clickable div. The keyboard focus ring comes from base.css and is not overridden.
 */

.c-button {
  /*
   * The hover ring colour, one property so the hover rule is written once and each
   * variant only names its own colour.
   */
  --c-button-ring: var(--brand-border);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-height: var(--control-h);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  line-height: var(--leading-control);
  /*
   * The border is subtracted from the padding so the box lands on --control-h exactly.
   * The template's .btn has no border and computes to 44px from padding and line box
   * alone; ours needs one for the default variant on a white surface, and without this
   * the button would be 46px and every control column would sit two pixels out.
   */
  padding: calc(var(--control-pad-y) - var(--border-width)) var(--control-pad-x);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  color: var(--ink-2);
  white-space: nowrap;
  cursor: pointer;
  text-decoration: none;
  transition:
    color var(--duration-fast) var(--ease),
    background-color var(--duration-fast) var(--ease),
    border-color var(--duration-fast) var(--ease),
    box-shadow var(--duration-fast) var(--ease);
}

/*
 * Hover is a ring, not a background change. That is what the template does
 * (.btn-primary:hover, app.css 8858: a 2px ring of the button's own colour, offset
 * 1px), and it means a filled button and an outlined one answer the same way.
 */
.c-button:not([disabled]):hover {
  color: var(--ink);
  text-decoration: none;
  box-shadow:
    0 0 0 var(--border-width) var(--surface),
    0 0 0 calc(var(--border-width) + var(--focus-width)) var(--c-button-ring);
}

/*
 * Pressed. DERIVED: the template resets .btn:active to box-shadow none and gives it
 * nothing else, so the ring is dropped and the surface steps one shade down, which
 * is the same move every other pressed surface in this library makes.
 */
.c-button:not([disabled]):active {
  box-shadow: none;
  background: var(--surface-3);
}

/*
 * 20px, which is the template's `text-xl` on a labelled button (docs/ui-patterns.md 1.2),
 * against 0.875rem label text. It was 16px, which read as an afterthought beside the label
 * rather than as part of it. Entry [048].
 */
.c-button__icon {
  width: var(--space-6);
  height: var(--space-6);
  flex: none;
  fill: currentColor;
}

/*
 * Blocked. Either the caller disabled it or the role does not hold the permission,
 * and both look the same because to the person they are the same: it will not act.
 * The title attribute names the permission, which is the whole tooltip need in this
 * library. Hover must not answer, which is why every state rule above is scoped
 * :not([disabled]) rather than reset here once per variant.
 */
.c-button[disabled] {
  opacity: 0.34;
  cursor: not-allowed;
}

/* ---- button.action.css ---- */
/*
 * button.action.css. The `action` variant, entry [237].
 *
 * The approved Sitemapify file paints every control a person clicks `--action`, the blue the
 * owner ruled a suite structural token, and keeps the navy for the reality band, the dark
 * Upgrade button and the solid pills. `primary` stays --brand for every other mount ("other
 * apps are not migrated in this pass"), so this is a variant a screen asks for by prop and not
 * a change to what primary means. When a mount moves, it moves by calling this.
 *
 * Its own file rather than button.variants.css, which sits at 195 lines against the 200 cap.
 * `.action` after the dot so the build emits it after button.css, the rule table.narrow.css
 * records.
 */
.c-button--action {
  --c-button-ring: var(--action);

  background: var(--action);
  border-color: var(--action);
  /* --action-ink, entry [243]: white on the suite blue, the navy on Sitemapify's orange, where
     white measured 2.30:1. tools/check-themes.php holds every theme's pair above 4.5. */
  color: var(--action-ink);
}

.c-button--action:not([disabled]):hover {
  color: var(--action-ink);
}

.c-button--action:not([disabled]):active {
  background: var(--action-hover);
  border-color: var(--action-hover);
}

/* ---- button.variants.css ---- */
/*
 * button.variants.css. Everything a CALLER asks for by prop.
 *
 * Split out of button.css 2026-08-27, entry [063], at 214 lines against the 200 line cap in
 * house rule 5. The seam is the same one table.cell.css draws and the same one that named
 * table.narrow.css: button.css is the control, its geometry, its states and the shape of its
 * glyph, and none of it is optional. Every rule here is reached by a prop value on
 * shared/views/components/button.php: variant, size, block, icon_only.
 *
 * `.variants` and not `-variants`, for the reason table.narrow.css records: the build script
 * globs *.css, a hyphen sorts before a dot, and an override emitted ahead of its base is a
 * cascade bug waiting for its first equal specificity collision.
 */

/*
 * .btn.btn-sm, app.css 8776. 32px. The border comes out of the vertical padding, as
 * it does on the tag, so the outer box is the template's 32px and not 34.
 */
.c-button--sm {
  position: relative;
  min-height: var(--control-h-sm);
  padding: calc(var(--space-3) - var(--border-width)) var(--space-4);
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
}

/*
 * The 44px hit area, laid over the chip so it costs the row no height. Entry [064], and the
 * mechanism is pagination.css's, which has done this over a 34px chip since it was written.
 *
 * This is the button half of the ruling in docs/architect.md: a button keeps the template's
 * paint (--control-h-sm is .btn-sm at app.css 8776) and gains an invisible target, because a
 * click on the overlay still reaches the control. An input cannot do this, so --input-h grew
 * to 44px instead. The two halves of the conflict resolve in opposite directions for a
 * mechanical reason rather than a stylistic one.
 *
 * icon-only is excluded because its box is already --touch-min square: an overlay on top of
 * that would extend PAST the neighbouring button in a row action cell, where three sit 8px
 * apart, and a target that fires the wrong control is worse than a small one.
 *
 * Vertical only. A small button is always wider than 44px once it carries a word, and the
 * ones that are not carry an icon and are already square.
 *
 * ## WHERE THIS DOES NOT WORK, measured rather than assumed
 *
 * The overlay paints nothing and carries no z-index, so anything painted after it covers it.
 * That is fine in a toolbar or a panel header, where the space above and below the button is
 * empty, and it is NOT fine in two places: a table row, where the next row wins, and a tight
 * form stack, where the rule above the submit wins. tools/check-touch.sh found both, on the
 * impersonate and oauth rows and on the settings save.
 *
 * Giving it a z-index would make it win by covering its neighbours, which is the same
 * overlapping target problem in a different costume. So a small button in a row is icon only
 * and genuinely 44px square, and a form's submit is not small at all. The overlay covers the
 * case it can actually cover and the call sites carry the rest.
 */
.c-button--sm:not(.c-button--icon-only)::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: var(--touch-min);
  transform: translateY(-50%);
}

/* primary. One per panel at most: the action the screen exists for. */
.c-button--primary {
  --c-button-ring: var(--brand);

  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-ink);
}

.c-button--primary:not([disabled]):hover {
  color: var(--brand-ink);
}

.c-button--primary:not([disabled]):active {
  background: var(--brand-hover);
  border-color: var(--brand-hover);
}

/* danger. Outlined rather than filled, so it does not compete with primary. */
.c-button--danger {
  --c-button-ring: var(--danger-dim);

  border-color: var(--danger-dim);
  color: var(--danger);
  background: var(--surface);
}

.c-button--danger:not([disabled]):hover {
  color: var(--danger);
}

.c-button--danger:not([disabled]):active {
  background: var(--danger-subtle);
}

/*
 * outline. The template's .btn-outline-dark, app.css 9050: NO fill, a 1px near black
 * border, dark text. Added entry [063] for the one thing the template uses it for, the
 * full width action on a pricing card (pricing.html 2218, and the same class on all four
 * of its cards).
 *
 * It exists because neither variant we had is what that button is. --primary is a brand
 * fill and made the plan card's Edit the loudest thing on the screen. --default is a light
 * grey fill with a slate-200 border, which on a white card body is QUIETER than the
 * template's and close enough to the 34 per cent opacity of [disabled] to read as one.
 * The template's answer is a strong edge around nothing, which is unmistakably a control
 * and still takes no attention from the price above it.
 *
 * Rendered from pricing.html at 1440 to get the numbers rather than reading its markup:
 * card 259x362, button 211x50, 13.8 per cent of the card, background rgba(0,0,0,0), border
 * 1px rgb(17,17,18), colour rgb(15,23,42), weight 600, no icon.
 */
.c-button--outline {
  --c-button-ring: var(--ink);
  background: transparent;
  border-color: var(--line);
  color: var(--action-text);
}

.c-button--outline:not([disabled]):hover { background: var(--surface-2); }
.c-button--outline:not([disabled]):active { background: var(--surface-3); }

/* Full width, for the primary action on an auth card. The template's block-btn. */
.c-button--block {
  display: flex;
  width: 100%;
}

/*
 * Icon only, entry [057]. A table row's controls.
 *
 * The label is still in the DOM, clipped by l-sr, so the accessible name is unchanged; the
 * words reach a sighted person through data-tip on hover and on focus. docs/ui-patterns.md
 * 1.4 for why a row is icon shaped and a toolbar is not.
 *
 * It stays --touch-min square rather than shrinking to the template's 24px action-btn. The
 * template's is below the floor in house rule 10, and the same trade was made for the header's
 * icon button in entry [045]: keep the picture, keep the target legal.
 */
.c-button--icon-only {
  min-width: var(--touch-min);
  padding-inline: var(--space-0);
  gap: var(--space-0);
}

/*
 * A SMALL icon button is still a touch target, entry [063].
 *
 * This rule used to send it back down to --control-h-sm, 32px, which undid the --touch-min
 * on the rule above and put the panel's most repeated control below the floor house rule 10
 * sets. Measured at 375 before the change: 32x36, on every row action on 24 screens, 301 of
 * them on the alerts screen alone.
 *
 * The overlay trick that tokens.css uses for the header button, a small painted face inside a
 * bigger invisible box, is NOT available here: these three sit side by side with a --space-2
 * gap, so 44px hit areas around 32px faces would OVERLAP each other by 8px and a tap near an
 * edge would fire the wrong control. A target that lies about which button it is is worse
 * than a small one. So the box itself grows and the row is honestly 44px.
 *
 * The padding stays --space-0 from the rule above, so the glyph is unchanged and only the
 * box around it moves.
 */
.c-button--icon-only.c-button--sm {
  min-width: var(--touch-min);
  min-height: var(--touch-min);
}

/*
 * Pressed. A chip that is currently filtering, reached by `pressed => true` on the partial.
 *
 * It is button.css's :active paint made durable rather than a fifth variant with colours of
 * its own: a control being pushed and a control that is held down are the same picture, which
 * is why the browser draws them alike and why this needs no new decision. The surface steps
 * one shade down and the edge darkens to --ink-3, so a pressed chip reads as recessed against
 * its unpressed neighbours at a glance rather than only by its text.
 *
 * Deliberately NOT a brand fill. Six pressed chips in one filter row filled with --brand are
 * six primary buttons, and the rule above says a panel gets at most one.
 *
 * Not scoped :not([disabled]), unlike every state rule in button.css. Those are responses to
 * a pointer and a dead control must not answer one. This is not a response, it is the
 * control's own state, and a disabled toggle still has one: the opacity [disabled] carries
 * already says the state cannot be changed from here.
 */
.c-button--pressed {
  background: var(--surface-3);
  border-color: var(--ink-3);
  color: var(--ink);
}

/* ---- chart.css ---- */
/*
 * chart.css. Line and bar, drawn as SVG.
 *
 * Every colour is a token, so a chart re-skins with the rest of the suite. The
 * template's own chart config carried 279 literal hex values across 3533 lines, which
 * is exactly the thing Phase 0 refused to port.
 *
 * No template equivalent for the frame, so the two pieces of text around the plot are
 * derived from the nearest thing each does. The caption names the figure, so it takes
 * .card-text (app.css 7899): 0.875rem, weight 400, a 1.25rem line box, slate-600. The
 * axis ticks label a series of columns, so they take .table-th (app.css 10772):
 * 0.75rem, weight 600, uppercase, a 1rem line box, slate-600. Phase 2 had both at
 * --text-xs in --ink-3, which read as a footnote rather than a heading.
 *
 * The plot has no fixed height in pixels: the viewBox scales it and the caption and
 * axis sit outside it, so the component fills whatever column it is handed.
 */

.c-chart {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  margin: 0;
}

.c-chart__caption {
  font-size: var(--text-sm);
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

/*
 * The one measurement in this file with no token behind it. --c-chart-height is a
 * plot height, and tokens.css carries no height scale, so the fallback stays as it
 * was until there is one. See the note in the build report.
 */
.c-chart__plot {
  width: 100%;
  height: var(--c-chart-height, var(--plot-h));
  display: block;
  overflow: visible;
}

/* Tone. One custom property drives every mark, so a variant is one line. */
.c-chart            { --c-chart-ink: var(--brand); --c-chart-fill: var(--brand-subtle); }
.c-chart--success   { --c-chart-ink: var(--success); --c-chart-fill: var(--success-subtle); }
.c-chart--warning   { --c-chart-ink: var(--warning); --c-chart-fill: var(--warning-subtle); }
.c-chart--danger    { --c-chart-ink: var(--danger); --c-chart-fill: var(--danger-subtle); }
.c-chart--info      { --c-chart-ink: var(--info-dim); --c-chart-fill: var(--info-subtle); }
.c-chart--ai        { --c-chart-ink: var(--ai); --c-chart-fill: var(--ai-subtle); }

.c-chart__line {
  fill: none;
  stroke: var(--c-chart-ink);
  stroke-width: 0.8;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

.c-chart__area { fill: var(--c-chart-fill); stroke: none; }

.c-chart__dot { fill: var(--c-chart-ink); }

.c-chart__bar { fill: var(--c-chart-ink); }

.c-chart__axis {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  text-transform: uppercase;
  color: var(--ink-2);
}

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

.c-chart__none {
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

/*
 * On a narrow screen the per point captions stop being readable long before the plot
 * does, so the axis is dropped and the accessible name carries the detail instead.
 */
@media (max-width: 640px) {
  .c-chart__axis { display: none; }
  .c-chart__plot { height: var(--c-chart-height, var(--plot-h-sm)); }
}

/* ---- code.css ---- */
/*
 * code. A stored value shown exactly as it is stored.
 *
 * Component 25, added 2026-08-27, entry [064], for the audit entry panel. It is a component
 * rather than two rules in a screen file because design.md section 2 says a screen never
 * writes its own CSS, and because the shape (a labelled block of unedited machine text) is
 * the one every future raw value wants: a webhook payload, a job's stored arguments, an
 * error's stack.
 *
 * ## Why a <pre> and why it wraps
 *
 * The value is not re-indented, so its own newlines are the only structure it has and a <pre>
 * is what preserves them. But a 115 character JSON blob on one line inside a scroll container
 * is the same unreadable prefix the audit column already was, so this wraps instead:
 * pre-wrap keeps the stored newlines AND breaks the long line. anywhere rather than break-word
 * because the content is machine text with no spaces to break at, which is the opposite of
 * identity.css's case and gets the opposite answer.
 *
 * Monospace and tabular figures for the reason state-of-play section 3 gives: this is a value
 * a person compares character by character against another one.
 */

.c-code {
  min-width: 0;
}

.c-code__label {
  margin: 0 0 var(--space-3);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-2);
}

.c-code__body {
  margin: 0;
  padding: var(--space-5);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.c-code__empty {
  margin: 0;
  padding: var(--space-5);
  border: var(--border-width) dashed var(--line);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

/* ---- dash.css ---- */
/*
 * dash.css. The main column and the right rail. Entry [292] made the rail sticky with its own
 * scroll; entry [295], the owner's ruling, dropped that: "It reads badly, particularly on the AI
 * crawlers panel. Let both columns scroll normally and balance them by what is in each instead."
 * So both columns are plain flex columns in the page's own scroll, and overview.php decides what
 * sits in which so their heights come out close (measured there).
 *
 * The rail's width is the nav's plus --space-11, 294px against the mockup's 312. Under --bp-md
 * (900) the two stack, the rail under the main column.
 */
.c-dash {
  display: grid;
  grid-template-columns: minmax(0, 1fr) calc(var(--nav-width) + var(--space-11));
  align-items: start;
  gap: var(--space-6);
  min-width: 0;
}

.c-dash__main,
.c-dash__rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
}

.c-dash__main > *,
.c-dash__rail > * { flex: none; }

.c-dash__main > .c-engines { margin-bottom: 0; }

/*
 * A full panel moved into the rail (the changes feed while Google's charts are drawn) takes the
 * fact lists' head: base size title beside its icon on the first line, the badge and the tools on
 * the next. At 294px the panel's 18px title would otherwise wrap under its own icon.
 */
.c-dash__rail .c-panel__head { gap: var(--space-4); padding: var(--space-4) var(--space-5); }
.c-dash__rail .c-panel__title { flex: 1 1 10rem; font-size: var(--text-base); }

/* The value line's wrapper: it carries data-value (paid or free) for a harness, and nothing else. */
.c-value-line { min-width: 0; }

@media (max-width: 900px) {
  .c-dash { grid-template-columns: minmax(0, 1fr); }
}

/* ---- dialog.css ---- */
/*
 * dialog.css. The confirmation dialog, driven by data-confirm on a form.
 *
 * Not a component: it has no partial, because it renders nothing until a person
 * submits something dangerous. Same shape as login.css, a stylesheet with no partial,
 * and it keeps the c-dialog prefix so the class discipline holds.
 *
 * Geometry follows the card: --card-pad, --radius-lg, --shadow-lg, per
 * docs/component-specs.md.
 */

.c-dialog {
  border: 0;
  border-radius: var(--radius-lg);
  padding: 0;
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
  max-width: 28rem;
  width: calc(100% - var(--space-8));
}

.c-dialog::backdrop {
  background: rgb(15 23 42 / 0.5);
  /* The approved file's 3px blur behind a dialog, entry [243]. */
  backdrop-filter: blur(3px);
}

.c-dialog__body {
  padding: var(--card-pad);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  margin: 0;
}

.c-dialog__text {
  font-size: var(--text-md);
  line-height: var(--leading-base);
  color: var(--ink);
  margin: 0;
}

/*
 * The optional note, entry [057]. It was markup with no rule at all until entry [061]:
 * the label and the input rendered at browser defaults inside a styled dialog, which
 * nobody noticed because the only screen that asks for a note is behind a confirm.
 *
 * The input is not restyled here. confirm.js gives it .c-field__input as well, so it is
 * the same control as every other text input in the suite and this file owns only the
 * stack around it. .c-dialog__note-input stays on the element because confirm.js selects
 * on it, and a class that is a JS hook is named in tools/check-classes.php for that.
 */
.c-dialog__note {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.c-dialog__note-label {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-control);
  color: var(--ink-2);
}

.c-dialog__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-4);
  flex-wrap: wrap;
}

@media (max-width: 640px) {
  .c-dialog__actions > * { flex: 1 1 100%; }
}

/*
 * The spoken dialog, entry [237]: the box shared/views/components/dialog.php renders, as
 * against the one confirm.js builds. Same shell above; these are the parts the approved
 * Sitemapify file adds, an icon disc beside the title, a bordered list of gains with a mark
 * each, and a foot whose two buttons share the width.
 */
.c-dialog--spoken { max-width: 30rem; }

.c-dialog__top {
  display: flex;
  align-items: flex-start;
  gap: var(--space-5);
}

.c-dialog__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--icon-face);
  height: var(--icon-face);
  border-radius: var(--radius-full);
  background: var(--action-subtle);
  color: var(--action-text);
}

.c-dialog__icon svg {
  width: var(--space-6);
  height: var(--space-6);
  fill: currentColor;
}

/* Entry [291]: a picture in the icon disc's place, the review modal's mascot. It keeps its own size. */
.c-dialog__figure { display: inline-flex; flex: none; }

.c-dialog__words { min-width: 0; }

.c-dialog__title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  color: var(--ink);
}

.c-dialog--spoken .c-dialog__text {
  font-size: var(--text-sm);
  color: var(--ink-2);
}

/* Entry [287]: a caller's own markup, a form most often. It follows the words at the same gap. */
.c-dialog__slot {
  margin-top: var(--space-5);
}

.c-dialog__gains {
  margin: 0;
  padding: 0;
  list-style: none;
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.c-dialog__gain {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--border-width) solid var(--line-soft);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink);
}

.c-dialog__gain:last-child { border-bottom: 0; }

.c-dialog__gain-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--space-6);
  height: var(--space-6);
  border-radius: var(--radius-full);
  background: var(--success-subtle);
  color: var(--success-ink);
}

.c-dialog__gain--warning .c-dialog__gain-mark {
  background: var(--warning-subtle);
  color: var(--warn-ink);
}

.c-dialog__gain-text { min-width: 0; }

.c-dialog__gain-mark svg {
  width: var(--space-4);
  height: var(--space-4);
  fill: currentColor;
}

.c-dialog__actions--spread > * { flex: 1 1 0; }

/* ---- engine-card.css ---- */
/* engine-card. Entry [291]: one search engine's card, two side by side on Overview. Tokens only.
   Entry [292]: the cards sit in the dashboard's main column beside the rail now, so they go side
   by side from --bp-lg rather than --bp-md, and the gaps match the column's own. */
.c-engines {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  margin-bottom: var(--space-6);
}

@media (min-width: 1180px) {
  .c-engines { grid-template-columns: 1fr 1fr; }
}

.c-engine {
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.c-engine__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-6);
  border-bottom: var(--border-width) solid var(--line-soft);
}

.c-engine__logo {
  display: grid;
  place-items: center;
  flex: none;
  width: var(--control-h-sm);
  height: var(--control-h-sm);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: var(--border-width) solid var(--line);
}

.c-engine__title { flex: 1; min-width: 0; }
.c-engine__title b { display: block; font-size: var(--text-md); font-weight: var(--weight-bold); color: var(--ink); }
.c-engine__title small { display: block; font-size: var(--text-xs); color: var(--ink-3); }

.c-engine__nums {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--border-width);
  margin: 0;
  background: var(--line-soft);
}

.c-engine__num { background: var(--surface); padding: var(--space-5); min-width: 0; }
.c-engine__num dt {
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-3);
}

.c-engine__num dd { margin: 0; }
.c-engine__v { font-size: var(--text-xl); font-weight: var(--weight-heavy); line-height: var(--leading-tight); color: var(--ink); overflow-wrap: anywhere; }
.c-engine__v--good { color: var(--success-ink); }
.c-engine__v--bad { color: var(--danger-ink); }
.c-engine__d { font-size: var(--text-xs); color: var(--ink-3); }

.c-engine__rows { list-style: none; margin: 0; padding: var(--space-2) 0; }
.c-engine__rows li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-6);
  border-bottom: var(--border-width) solid var(--line-soft);
  font-size: var(--text-sm);
  color: var(--ink-2);
}

.c-engine__rows li span { min-width: 0; overflow-wrap: anywhere; }
.c-engine__rows li b { font-family: var(--font-mono); font-weight: var(--weight-semi); color: var(--ink); }

.c-engine__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-7) var(--space-6);
  text-align: center;
}

.c-engine__q { font-size: var(--text-md); font-weight: var(--weight-semi); color: var(--ink); }
.c-engine__why { margin: 0 0 var(--space-3); max-width: 34ch; font-size: var(--text-sm); color: var(--ink-3); }

.c-engine__foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-6);
  background: var(--surface-2);
  border-top: var(--border-width) solid var(--line-soft);
  font-size: var(--text-xs);
  color: var(--ink-3);
}

@media (max-width: 640px) {
  .c-engine__nums { grid-template-columns: 1fr; }
}

/* ---- fact-list.css ---- */
/*
 * fact-list.css. A compact card of name and value rows for the dashboard rail. Entry [292], from
 * sitemapify-v3.html's .sidestat (head 13px 17px, rows 10px 17px, a hairline between). Tokens only.
 * Each row is at least --touch-min tall, though nothing in it is a control, so the rail's rhythm
 * matches the action card's rows above it.
 */
.c-facts {
  flex: none;
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  min-width: 0;
}

.c-facts__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: 0;
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--border-width) solid var(--line-soft);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  color: var(--ink);
}

.c-facts__icon { flex: none; width: var(--space-6); height: var(--space-6); fill: none; stroke: currentColor; color: var(--ink-2); }

.c-facts__rows { margin: 0; }

.c-facts__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--touch-min);
  padding: var(--space-3) var(--space-5);
  border-bottom: var(--border-width) solid var(--line-soft);
  font-size: var(--text-sm);
}

.c-facts__row:last-child { border-bottom: 0; }

.c-facts__name { flex: 1 1 auto; min-width: 0; color: var(--ink-2); overflow-wrap: anywhere; }
.c-facts__name--mono { font-family: var(--font-mono); font-size: var(--text-xs); }

.c-facts__value {
  flex: none;
  margin: 0;
  font-family: var(--font-mono);
  font-weight: var(--weight-semi);
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--ink);
}

.c-facts__value--success { color: var(--success-ink); }
.c-facts__value--warning { color: var(--warn-ink); }
.c-facts__value--danger { color: var(--danger-ink); }
.c-facts__value--mute { color: var(--ink-3); }

.c-facts__empty,
.c-facts__note {
  margin: 0;
  padding: var(--space-4) var(--space-5);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

.c-facts__note { border-top: var(--border-width) solid var(--line-soft); background: var(--surface-2); }

/* ---- field.css ---- */
/*
 * field. Label, control, and the quiet text under it.
 *
 * Geometry is the template's .form-label (app.css 9503) and .form-control
 * (app.css 9534): a 0.875rem medium weight label with 0.5rem under it, over a 38px
 * control padded 0.5rem by 0.75rem. Focus is the template's 1px ring, app.css 568.
 *
 * The control is monospace because most of what this panel edits is an identifier, a
 * key or a figure, and people compare those character by character. That is a
 * design.md decision about the family only. Every measurement here is the template's.
 */

.c-field {
  display: block;
}

.c-field__label {
  display: block;
  width: 100%;
  margin-bottom: var(--space-3);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-control);
  color: var(--ink-2);
  cursor: pointer;
}

/* Says required in words, so the signal does not depend on a colour or a symbol. */
.c-field__required {
  margin-left: var(--space-3);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--ink-3);
}

.c-field__input {
  display: block;
  width: 100%;
  min-height: var(--input-h);
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--input-pad-y) var(--input-pad-x);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink);
  transition:
    border-color var(--duration-fast) var(--ease),
    box-shadow var(--duration-fast) var(--ease);
}

.c-field__input::placeholder {
  color: var(--ink-3);
  font-weight: var(--weight-normal);
}

.c-field__input:hover:not([disabled]):not([readonly]) {
  border-color: var(--muted-dim);
}

/*
 * Focus is a 1px ring in the brand colour on top of the border, which is what
 * .form-control:focus does at app.css 568. The keyboard outline from base.css sits
 * outside it and is not touched.
 */
.c-field__input:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 var(--border-width) var(--brand);
  outline: none;
}

.c-field__input[readonly] {
  background: var(--surface-2);
  color: var(--ink-2);
}

.c-field__input[disabled] {
  background: var(--surface-2);
  color: var(--ink-3);
  border-color: var(--line-soft);
  cursor: not-allowed;
}

.c-field__input--area {
  min-height: var(--space-11);
  resize: vertical;
}

.c-field__hint {
  margin-top: var(--space-3);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

/*
 * Error. The border turns red and the message appears with it. Neither one alone is
 * the state, which is the section 7 rule about colour never being the only signal.
 */
.c-field__error {
  margin-top: var(--space-3);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--danger);
}

.c-field--error .c-field__input {
  border-color: var(--danger);
  background: var(--danger-subtle);
}

.c-field--error .c-field__input:focus {
  border-color: var(--danger);
  box-shadow: 0 0 0 var(--border-width) var(--danger);
}

/* ---- field.switch.css ---- */
/*
 * field.switch.css. The `switch` type, split from field.variants.css in entry [240] when the
 * approved file's checkbox row took that file past the 200 line cap. Everything here is reached
 * by `type => 'switch'` on shared/views/components/field.php. `.switch` after the dot so the
 * build emits it after field.css and field.variants.css, whose grid it extends.
 */

/*
 * switch, entry [237]. The checkbox input with its native picture removed and a track and
 * thumb drawn in its place; role="switch" is on the element, so the state is announced as
 * on or off rather than checked. The row puts the words first and the control at the far
 * right, which is where the approved Sitemapify file draws every toggle, and the row is still
 * the 44px target. --success for on, because on is a state and not a brand.
 */
.c-field--switch .c-field__check {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--space-5);
}

.c-field--switch .c-field__check-text,
.c-field--switch .c-field__hint--row { grid-column: 1; }

.c-field__box--switch { grid-column: 2; grid-row: 1 / span 2; }

.c-field__box--switch {
  appearance: none;
  position: relative;
  width: var(--space-9);
  height: var(--space-6);
  margin: 0;
  border-radius: var(--radius-pill);
  background: var(--line);
  transition: background var(--duration-fast) var(--ease);
}

.c-field__box--switch::after {
  content: "";
  position: absolute;
  top: var(--space-1);
  left: var(--space-1);
  width: var(--space-5);
  height: var(--space-5);
  border-radius: var(--radius-full);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: transform var(--duration-fast) var(--ease);
}

.c-field__box--switch:checked { background: var(--success); }

.c-field__box--switch:checked::after {
  transform: translateX(calc(var(--space-9) - var(--space-5) - var(--space-1) * 2));
}

/* ---- field.variants.css ---- */
/*
 * field.variants.css. Everything a `type` or a flag prop selects.
 *
 * Split out of field.css 2026-08-27, entry [067], at 218 lines against the 200 line cap in
 * house rule 5. The seam is button.variants.css's, and the same one again: field.css is the
 * text control, its label, its states and its error, and none of that is optional. Every rule
 * here is reached by a prop on shared/views/components/field.php, `type` => checkbox or file,
 * or `disabled` => true.
 *
 * `.variants` and not `-variants`, for the reason table.narrow.css records: the build script
 * globs *.css, a hyphen sorts before a dot, and an override emitted ahead of its base is a
 * cascade bug waiting for its first equal specificity collision.
 */

/*
 * checkbox. The label wraps the control instead of sitting above it, so the whole
 * row is the hit target, and the row is --touch-min tall for house rule 10. The box
 * itself is 1rem square, which is what sits with 0.875rem label text: the template
 * ships no styled checkbox, so this proportion is DERIVED from the label line box.
 */
/*
 * The row, entry [240], the approved Sitemapify file's `.setrow`: a drawn 20px box top left, the
 * label in the semi weight with its hint under it, a hairline between rows. The owner's ruling:
 * alerts and HTML sitemap are built from this row. The label is the target, at least 44px.
 */
.c-field__check {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  column-gap: var(--space-4);
  row-gap: var(--space-1);
  cursor: pointer;
  padding: var(--space-5) var(--space-0);
  min-height: var(--touch-min);
  border-bottom: var(--border-width) solid var(--line-soft);
}

/* The hint in the row: column two, under the label, in the hint's own type. */
.c-field__hint--row {
  grid-column: 2;
  margin: 0;
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

.c-field--checkbox:last-child .c-field__check,
.c-field--switch:last-child .c-field__check { border-bottom: 0; }

.c-field__box {
  appearance: none;
  position: relative;
  margin: 0;
  cursor: pointer;
  flex: 0 0 auto;
}

/* The drawn box. Scoped off the switch, which draws a track over the same input. */
.c-field__box:not(.c-field__box--switch) {
  width: var(--space-6);
  height: var(--space-6);
  border: 2px solid var(--line);
  border-radius: var(--radius-xs);
  background: var(--surface);
}

.c-field__box:not(.c-field__box--switch):checked {
  background: var(--action);
  border-color: var(--action);
}

/* The tick: a rotated border corner in the box's ink, the file's own drawing. */
.c-field__box:not(.c-field__box--switch):checked::after {
  content: "";
  position: absolute;
  left: 6px;
  top: 2px;
  width: 5px;
  height: 9px;
  border: solid var(--action-ink);
  border-width: 0 2px 2px 0;
  transform: rotate(42deg);
}

.c-field__check-text {
  grid-column: 2;
  min-width: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  line-height: var(--leading-base);
  color: var(--ink);
}

.c-field__box:not(.c-field__box--switch) { grid-column: 1; grid-row: 1 / span 2; margin-top: 1px; }

/*
 * The file variant, entry [041]. The preview is the control's current value, so it sits
 * where a text input's value would and takes the same vertical rhythm.
 *
 * The chequerboard is two crossed gradients rather than an image: a brand logo is
 * usually transparent, and shown on a white card a white logo is invisible. Nothing
 * here is a colour from the prototype: both stops are existing surface tokens.
 */
.c-field__preview {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--space-11);
  padding: var(--space-4);
  margin-bottom: var(--space-4);
  border: var(--border-width) dashed var(--line);
  border-radius: var(--radius-md);
  background-color: var(--surface-2);
  background-image:
    linear-gradient(45deg, var(--line) 25%, transparent 25%, transparent 75%, var(--line) 75%),
    linear-gradient(45deg, var(--line) 25%, transparent 25%, transparent 75%, var(--line) 75%);
  background-size: var(--space-6) var(--space-6);
  background-position: 0 0, var(--space-3) var(--space-3);
}

.c-field__preview-img { max-width: 100%; max-height: var(--space-11); height: auto; }

.c-field__preview-empty {
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

/*
 * Disabled, entry [067]. The input already dims itself through [disabled] above; what this
 * adds is the rest of the row, so a disabled checkbox does not sit at full strength beside a
 * dimmed text box.
 *
 * The label and hint dim and the CONTROL keeps its own [disabled] rule, rather than one
 * opacity over the whole block: button.css uses opacity 0.34 on a control that is a single
 * painted box, and a field is a label, a control and a hint at three different weights
 * already. Fading all three together loses that hierarchy at exactly the moment a person is
 * trying to work out why they cannot type.
 */
.c-field--disabled .c-field__label,
.c-field--disabled .c-field__check-text,
.c-field--disabled .c-field__hint {
  color: var(--ink-3);
}

.c-field--disabled .c-field__check {
  cursor: not-allowed;
}

.c-field__box[disabled] {
  cursor: not-allowed;
  accent-color: var(--muted);
}

/* A file input is a browser control we do not restyle, only size and space. */
.c-field__input--file {
  padding: var(--space-3);
  line-height: var(--leading-base);
}

/* ---- flow.css ---- */
/*
 * flow.css. Boxes and arrows, one screen, deliberately small.
 *
 * No template equivalent. A node is derived: .card's radius (0.375rem, app.css 11252)
 * around .badge's type (app.css 8041), which is 0.75rem at weight 600 in a 1rem line
 * box with 0.25rem by 0.5rem of padding. That makes a node the same object as a tag
 * with a longer label, which is what it is.
 *
 * Inter rather than the mono face, because .badge is Inter and a node label is a
 * sentence about what moves, not an identifier to compare character by character.
 */

.c-flow {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-4);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

.c-flow__step {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-width: 0;
}

.c-flow__node {
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface-2);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
}

/* The node everything converges on. The border carries it as well as the text
   colour, so it is still distinguishable without colour. */
.c-flow__node--brand {
  border-color: var(--brand-border);
  background: var(--brand-subtle);
  color: var(--brand);
}

.c-flow__arrow {
  color: var(--ink-3);
  font-size: var(--text-md);
  line-height: var(--leading-tight);
}

/* House rule 10. Below the small breakpoint this component's arrangement stops working. */
@media (max-width: 640px) {
  .c-flow { flex-direction: column; align-items: stretch; }
  .c-flow__arrow { transform: rotate(90deg); align-self: center; }
}

/* ---- help.css ---- */
/*
 * help.css. Component 24, added 2026-08-26, entry [056].
 *
 * A question mark beside a heading and the paragraph it opens. help.php carries the argument
 * for why it exists, why it is not menu and not tooltip, and why it is click only.
 *
 * ## Why there is no positioning script
 *
 * The panel is `position: absolute` inside the trigger's own `position: relative` box, and that
 * is enough because NOTHING ABOVE IT CLIPS. `.l-shell__main` has no overflow and `.l-bar` is
 * sticky with overflow visible. The sidebar is the one that clips, `overflow-y: auto`, which is
 * why the rail's tooltip needed a body level surface and measuring script and this does not.
 *
 * It opens DOWNWARD rather than leftward for a z-index reason and not a taste one: `.l-bar` is
 * `--z-sticky` (100) and the sidebar is `--z-nav` (200), so a panel reaching left over the
 * sidebar would render underneath it. Downward it covers page content at `--z-base`.
 */

.c-help {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
}

/*
 * The marker is removed twice on purpose: `list-style` covers the standards path and the
 * pseudo element covers WebKit, which still ships its own. Missing either one leaves a
 * triangle beside the glyph.
 */
.c-help__trigger {
  list-style: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  border-radius: var(--radius-full);
  color: var(--ink-3);
  cursor: pointer;
  transition: color var(--duration-base) var(--ease);
}

.c-help__trigger::-webkit-details-marker { display: none; }
.c-help__trigger::marker { content: ''; }

.c-help__trigger:hover,
.c-help__trigger:focus-visible { color: var(--brand); }

.c-help[open] > .c-help__trigger { color: var(--brand); }

.c-help__glyph { width: var(--space-6); height: var(--space-6); fill: none; }

/*
 * The panel. It overlays the page rather than pushing it down, which is the whole point: an
 * intro that reflows the screen every time somebody opens it is worse than the paragraph it
 * replaced.
 *
 * The width is a reading measure rather than a box measure. 34rem holds roughly 60 words at
 * this size, which is the length these paragraphs actually are.
 */
.c-help__panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  width: max-content;
  max-width: 34rem;
  padding: var(--space-6);
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  z-index: var(--z-overlay);
}

.c-help--start .c-help__panel { inset-inline-start: 0; }
.c-help--end .c-help__panel { inset-inline-end: 0; }

.c-help__text {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

/*
 * On a narrow screen the panel stops hanging off the trigger and spans the bar instead. A
 * 34rem panel anchored to a control near the right edge of a 360px screen would open off the
 * side, and there is no measuring script here to catch that. House rule 10.
 */
@media (max-width: 640px) {
  .c-help { position: static; }

  .c-help__panel {
    inset-inline: var(--space-5);
    width: auto;
    max-width: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .c-help__trigger { transition: none; }
}

/* ---- icon-button.css ---- */
/*
 * icon-button.css. The header's circular icon control, and the count badge on it.
 *
 * `nav-tools`, index.html 1825, is a row of these. Measurements in
 * docs/component-specs.md under app-header: a 32px circle on slate-100 (app.css 15861,
 * 15876, 15921), radius 9999px (app.css 5635), a 1.25rem glyph (app.css 6889).
 *
 * ## Why this is a class and not a partial
 *
 * Two different components render the same control and neither can delegate its own
 * button element. The top bar's toggles carry `data-action` and their own aria-expanded
 * against the sidebar; menu's trigger carries aria-haspopup and aria-controls against
 * its list. A shared partial would have to accept both sets and would end up a worse
 * <button> than either. So the markup stays where its behaviour is and the picture
 * lives here, once. House rule 7 is about not duplicating the picture, and this is how
 * it is kept without a component that renders nothing of its own.
 *
 * ## The one place the template is not followed
 *
 * The template's button IS the 32px circle, so its hit area is 32px, and house rule 10
 * says 44px. Ours is a --touch-min box with the 32px circle painted inside it. The
 * picture is the template's to the pixel and the target is legal. This is the same
 * trade secret.css made at 44px in a 38px column, and for the same reason.
 *
 * Below 1024px the template drops the circle entirely and leaves the bare glyph. That
 * is not copied: a control with no shape on the width where it is hit by a thumb is
 * the wrong way round. The circle stays at every width.
 */

.c-iconbtn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  flex: 0 0 auto;
  border-radius: var(--radius-full);
  background: none;
  color: var(--ink);
}

/* The painted circle. app.css 15861 for the size, 15921 for the fill. */
.c-iconbtn__face {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-face);
  height: var(--icon-face);
  border-radius: var(--radius-full);
  background: var(--surface-3);
  transition: background var(--duration-base) var(--ease), color var(--duration-base) var(--ease);
}

.c-iconbtn:hover .c-iconbtn__face,
.c-iconbtn:focus-visible .c-iconbtn__face {
  background: var(--surface-2);
  color: var(--brand);
}

.c-iconbtn[aria-expanded="true"] .c-iconbtn__face {
  background: var(--brand);
  color: var(--brand-ink);
}

/*
 * The quiet variant, entry [048]. A row action or a card header kebab.
 *
 * The template's header control is the 32px filled circle above. Its row and card-header
 * kebab is a different thing entirely: `<button class="text-xl text-center block w-full">`
 * with dots-vertical inside and no shape at all (advance-table.html, and the twelve card
 * headers in the dashboards). A column of grey discs, one per row, competes with the data
 * it sits beside, which is presumably why the template does not do it either.
 *
 * So: no face fill, no hover fill, the glyph itself carries the state. The hit box stays
 * --touch-min, because the template's bare glyph is not a legal target and house rule 10 is
 * not negotiable for a control that a thumb has to find in a table row.
 */
.c-iconbtn--quiet .c-iconbtn__face {
  background: none;
  width: auto;
  height: auto;
  color: var(--ink-3);
}

.c-iconbtn--quiet:hover .c-iconbtn__face,
.c-iconbtn--quiet:focus-visible .c-iconbtn__face { background: none; color: var(--brand); }

.c-iconbtn--quiet[aria-expanded="true"] .c-iconbtn__face { background: none; color: var(--brand); }

/*
 * The count. app.css 4476 and 3853: a 16px circle hung off the top right corner of the
 * face by -0.25rem.
 *
 * Two departures from the template, both deliberate.
 *
 * The fill is --danger and not Tailwind's red-500 (app.css 6279). The theming rule says
 * semantic colour is identical across the suite, and the template itself uses
 * bg-danger-500 for the unread dot eight lines further down: red-500 there is the
 * inconsistency, not the rule.
 *
 * It grows sideways past one digit rather than clipping. The template's own screenshot
 * shows "10" in a 16px circle. min-width holds the circle round at one digit and the
 * padding lets two or three push it into a pill.
 */
.c-iconbtn__badge {
  position: absolute;
  top: 0;
  right: calc(var(--space-2) * -1);
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: var(--badge-size);
  height: var(--badge-size);
  padding: var(--space-0) var(--space-1);
  border-radius: var(--radius-pill);
  background: var(--danger);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semi);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--surface);
}

@media (prefers-reduced-motion: reduce) {
  .c-iconbtn__face { transition: none; }
}

/* ---- identity.css ---- */
/*
 * identity.css. Component 23, added 2026-08-24, entry [048].
 *
 * A mark, a name, and the detail that would otherwise have taken a column of its own.
 * docs/ui-patterns.md section 2.3 for why the template stacks rather than columnises, and
 * identity.php for why this is a component and not a prop on table.
 *
 * ## This file owns .c-avatar, and menu.php borrows it
 *
 * The circle was written twice before today: once here in spirit and once as
 * .c-menu__avatar in menu-trigger.css, for the profile trigger in entry [045]. Two copies of
 * one shape drift, so there is now one rule; entry [165] went further and had menu.php draw its
 * mark through partials/avatar.php, so the trigger gained the image branch and `c-menu__avatar`
 * went away entirely, having painted nothing since [048].
 * menu-trigger.css keeps only what is genuinely the menu's business, which turned out to be
 * nothing at all.
 *
 * The fill changed when they merged, and it is worth saying out loud: the profile mark used
 * to be solid --brand with --brand-ink on it. It is now the template's own no photograph
 * treatment from project.html:2257, the brand hue at low saturation with the page's dark ink
 * on top. That is both the template's answer and the legible one: --brand-subtle against
 * --ink measures 14.9:1, where --brand-subtle against --brand is 3.73:1 and fails AA for
 * 12px text.
 */

.c-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-face);
  height: var(--icon-face);
  flex: 0 0 auto;
  border-radius: var(--radius-full);
  background: var(--brand-subtle);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  line-height: 1;
  /* project.html:2257 pulls the pair together with -tracking-[1px] so two letters read as
     one mark rather than as two characters. Ours is the nearest thing on the scale. */
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--ink);
}

.c-avatar--sm { width: var(--avatar-sm); height: var(--avatar-sm); }
.c-avatar--md { width: var(--icon-face); height: var(--icon-face); }
.c-avatar--lg {
  width: var(--avatar-lg);
  height: var(--avatar-lg);
  font-size: var(--text-sm);
}

/* A row whose identity is a thing rather than a customer. A coloured mark on a job or a
   webhook claims more than it should. */
.c-avatar--mute { background: var(--surface-3); color: var(--ink-2); }

/* ---- the block ---------------------------------------------------------------------- */

.c-identity {
  display: inline-flex;
  align-items: center;
  /* advance-table.html: ltr:mr-3 between the mark and the name. */
  gap: var(--space-4);
  min-width: 0;
  text-align: left;
}

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

/*
 * The name never truncates. That is the whole point of the component: the customers screen
 * had an ellipsis on the one string a person is looking for. It wraps instead, and the mark
 * beside it is flex: 0 0 auto so it never gives up width to make room.
 */
.c-identity__name {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-base);
  color: var(--ink);
  /*
   * break-word and NOT anywhere. `anywhere` was the first attempt and it was worse than the
   * truncation it replaced: in the job queue's 9 column table the customer column negotiated
   * down to about 80px and `cedar-botanics.de` came out as four lines reading "ceda", "r-bota",
   * "nics.", "de". A word only breaks here when it genuinely cannot fit, and the min-width
   * below is what stops the column getting that narrow in the first place.
   */
  overflow-wrap: break-word;
}

a.c-identity__name { position: relative; text-decoration: none; }
a.c-identity__name:hover { color: var(--brand); text-decoration: underline; }
/*
 * THE 44px HIT AREA, and it is the ONE edit this component was opened for. Entry [214], with the
 * owner's word, 2026-09-10.
 *
 * The name sets at `--text-sm` on `--leading-base`, a 20px line box, and when it is a link that is
 * the whole target. House rule 10 puts the floor at 44px and this has been under it since entry
 * [048]. Not a table only control either: every app card on /apps and every finding on a public
 * report carries it, so it is a 20px tap target on the two screens a phone reaches first.
 *
 * NOTHING VISIBLE MOVES. The overlay is `pagination.css`'s pattern, unchanged and for the same
 * reason: that file lays a `--touch-min` pseudo element over a 34px chip so the hit area is legal
 * without the row growing to twice its height, and `tools/touch-probe.js` asks what is AT THE
 * POINT rather than measuring the painted box, precisely so this answer counts. Growing the link
 * itself would add 24px to every row of a nine column table on eight admin screens.
 *
 * `height: 100%` with a `min-height` floor, and not a flat 44px: this name WRAPS by design, per
 * the note above, and a fixed height would leave a three line name with a hit area over its
 * first line and a half.
 *
 * The width is the link's own and is deliberately NOT widened. A name is as wide as it is, the
 * cell beside it may hold another control, and 44px of invisible overlay reaching sideways out of
 * a text link is how a hit area starts stealing its neighbour's clicks: pagination.css carries the
 * measurement, two overlays 3px apart and elementFromPoint at the edge of "Page 1" answering
 * "Page 2".
 */
a.c-identity__name::after {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 100%;
  height: 100%;
  min-height: var(--touch-min);
  transform: translateY(-50%);
}

/* An identifier a person compares character by character. state-of-play.md section 3. */
.c-identity__name--mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* The line that used to be its own column. Weaker, and it may truncate: it is context. */
.c-identity__detail {
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 32ch;
}

.c-identity__note {
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--ink-3);
}

/*
 * In a table cell the block fills the cell rather than sitting inline, so a long name wraps
 * against the column and not against the page.
 *
 * The min-width is the load bearing part. Table layout hands a column its content's minimum,
 * and a wrapping name has a tiny minimum, so a dense table will squeeze this column to
 * nothing and shatter the name. 11rem is two thirds of a 16 character domain at 0.875rem: wide
 * enough that break-word almost never fires, narrow enough that a nine column table still
 * fits. It is a floor and not a width: a roomy table still gives the column more.
 */
.c-table__td > .c-identity { display: flex; width: 100%; min-width: 11rem; }

/*
 * Below the small breakpoint the table collapses to stacked rows (table.css), where a cell
 * is a label and a value side by side. The detail's 32ch cap is a desktop measure and would
 * push the row wide there, so it comes off and the line wraps instead.
 */
@media (max-width: 640px) {
  .c-identity__detail { white-space: normal; max-width: none; overflow: visible; }
}

/* ---- a real mark instead of letters, entry [090] --------------------------------------
 *
 * The thirteen apps have an uploaded icon now and the registry was drawing "SX" for
 * Sitemapify. The image takes the circle the letters were centred in rather than a slot
 * beside it, so a row is the same height and the same rhythm whether an app has an icon or
 * not: a list where some rows are 28px and some are 40px reads as a broken table.
 *
 * `cover` and not `contain`, because the uploaded icon is square by the intake's own rule
 * (1000 x 1000, entry [089]) and the circle is square, so there is nothing to letterbox. A
 * non square file that somehow reached here crops rather than sitting on a coloured bar.
 *
 * The background stays. A PNG with transparency shows --brand-subtle through it, which is the
 * same treatment the letters get and stops a transparent icon reading as a hole in the row.
 */

.c-avatar--image { overflow: hidden; }

.c-avatar__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ---- kv.css ---- */
/*
 * kv.css. A two column definition list: a name and its value.
 *
 * DERIVED, not read from the template: the template has no name and value block. The
 * term takes .form-label (app.css 9503: 0.875rem, weight 500, slate-600; its capitalize
 * is deliberately not copied, entry [047])
 * and the value takes .table-td (app.css 10798: 0.875rem on a 1.25rem line), so a kv
 * block reads as the same material as the form and the table beside it.
 *
 * The label column sizes to its own content with a floor, so a long label widens the
 * column rather than wrapping into three lines, and nothing outside this root moves.
 * The columns are --cell-pad-x apart, which is the gutter a table uses.
 */

.c-kv {
  display: grid;
  grid-template-columns: minmax(var(--space-11), max-content) 1fr;
  column-gap: var(--cell-pad-x);
  row-gap: var(--space-3);
  margin: 0;
}

.c-kv__label {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-base);
  letter-spacing: normal;
  color: var(--ink-2);
}

.c-kv__value {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
  color: var(--ink);
  word-break: break-word;
}

/* Stacked. Label over value, for a narrow container such as a side panel. */
.c-kv--stacked {
  grid-template-columns: 1fr;
  row-gap: var(--space-1);
}

.c-kv--stacked .c-kv__value {
  margin-bottom: var(--space-3);
}

.c-kv--stacked .c-kv__value:last-child {
  margin-bottom: var(--space-0);
}

/* Below the small breakpoint (--bp-sm) two columns stop fitting, so rows stack too. */
@media (max-width: 640px) {
  .c-kv--rows {
    grid-template-columns: 1fr;
    row-gap: var(--space-1);
  }

  .c-kv--rows .c-kv__value {
    margin-bottom: var(--space-3);
  }
}

/* ---- lamp.css ---- */
/*
 * lamp.css. The status dot with its word beside it. design.md section 5.
 *
 * The template ships no dot indicator, so this one is derived rather than read: it
 * takes the .badge line box (app.css 8041), because a lamp and a tag stand side by
 * side in a table cell and in a rack channel and have to sit on one baseline. So the
 * label is 0.75rem at weight 600 in a 1rem line box, and the dot is 0.5rem, half of
 * that line box.
 *
 * Both halves are always present: the accessibility floor says colour is never the
 * only signal, so the dot carries the glance and the label carries the meaning.
 *
 * The crit variant pulses at --duration-slow (1800ms). That token was added for this,
 * because --duration-fast and --duration-base are interaction timings and a pulse at
 * either would flash six to eleven times a second, well past the three per second
 * seizure threshold in WCAG 2.3.1.
 */

.c-lamp {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  vertical-align: middle;
}

.c-lamp__dot {
  width: var(--space-3);
  height: var(--space-3);
  flex: none;
  border-radius: var(--radius-full);
  background: var(--ink-3);
  box-shadow: inset 0 0 0 var(--border-width) var(--ground);
}

/* 0.75rem on --leading-tight is a 1rem line box, which is .badge exactly. */
.c-lamp__label {
  font-family: var(--font-cond);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  color: var(--ink-2);
  white-space: nowrap;
}

.c-lamp--success .c-lamp__dot { background: var(--success); }
.c-lamp--success .c-lamp__label { color: var(--success); }

.c-lamp--warning .c-lamp__dot { background: var(--warning); }
.c-lamp--warning .c-lamp__label { color: var(--warning); }

.c-lamp--danger .c-lamp__dot {
  background: var(--danger);
  animation: c-lamp-pulse var(--duration-slow) var(--ease) infinite;
}

/*
 * base.css already reduces every animation to 0.01ms under prefers-reduced-motion,
 * which stops the pulse. This rule additionally pins the dot at full opacity, so a
 * reader who has asked for no motion sees a solid critical dot rather than one frozen
 * at whatever opacity the animation happened to reach.
 */
@media (prefers-reduced-motion: reduce) {
  .c-lamp--danger .c-lamp__dot { animation: none; opacity: 1; }
}

@keyframes c-lamp-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}
.c-lamp--danger .c-lamp__label { color: var(--danger); }

/*
 * pulse, entry [176]. Opt in, for a lamp whose state has not settled: the step a long job is
 * on right now. It reuses the critical pulse rather than introducing a second rhythm, so two
 * unsettled lamps on one screen breathe together, and it is off by default because a list of
 * six lamps that all pulse says nothing about which one is moving.
 */
.c-lamp--pulse .c-lamp__dot {
  animation: c-lamp-pulse var(--duration-slow) var(--ease) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .c-lamp--pulse .c-lamp__dot { animation: none; opacity: 1; }
}

.c-lamp--ai .c-lamp__dot { background: var(--ai); }
.c-lamp--ai .c-lamp__label { color: var(--ai); }

/* off is the default: a dark dot on the neutral ramp, and quiet text. */
.c-lamp--mute .c-lamp__dot { background: var(--muted); }
.c-lamp--mute .c-lamp__label { color: var(--ink-3); }

/* ---- link-card.css ---- */
/*
 * link-card.css. One row that is one link: a mark, a title, a line under it, a trailing glyph.
 * Entry [290], the Sitemapify onboarding's sign in rows. link-card.php says why it is not a
 * button and not an identity.
 *
 * Every value is a token. The row's height comes from its padding and the 40px mark, which
 * together clear --touch-min at every width without a floor of their own; the floor is still
 * written, so a caller that drops the mark keeps a legal target.
 */

.c-link-card {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  min-height: var(--touch-min);
  padding: var(--space-5) var(--space-6);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  color: var(--ink);
  text-decoration: none;
  transition: border-color var(--duration-fast) var(--ease), box-shadow var(--duration-fast) var(--ease);
}

a.c-link-card:hover {
  border-color: var(--brand-border);
  box-shadow: var(--shadow-md);
}

.c-link-card__lines {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.c-link-card__title {
  font-size: var(--text-md);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  overflow-wrap: break-word;
}

/* It wraps and never truncates: it is the line that says what signing in gives and cannot do. */
.c-link-card__detail {
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-3);
  overflow-wrap: anywhere;
}

.c-link-card__glyph {
  flex: 0 0 auto;
  width: var(--space-6);
  height: var(--space-6);
  color: var(--ink-3);
}

a.c-link-card:hover .c-link-card__glyph { color: var(--brand); }

/* Done: the action worked. The border and the check carry it, and the words say it. */
.c-link-card--done {
  border-color: var(--success);
  background: var(--success-subtle);
  box-shadow: none;
}

.c-link-card--done .c-link-card__glyph { color: var(--success-ink); }

@media (max-width: 640px) {
  .c-link-card { padding: var(--space-4) var(--space-5); gap: var(--space-4); }
}

@media (prefers-reduced-motion: reduce) {
  .c-link-card { transition: none; }
}

/* ---- login.css ---- */
/*
 * login.css. The signed out page frame: sign in, forgot password, reset password, accept
 * invite, lock, and the error page. Six screens, one frame, one brand panel partial.
 *
 * Rebuilt 2026-08-24, entry [046], against signin-one.html measured into
 * docs/component-specs.md under "The auth page frame". The first build took that page's
 * skeleton and none of its geometry. Two things it got wrong, both fixed here:
 *
 * 1. THE FORM SIDE IS NOT A CARD. `.auth-box` (app.css 11105) is a 524px block with 42/44px
 *    of padding and no background, no border, no radius and no shadow. The white is
 *    `.inner-content`, which is the whole right column, edge to edge, full height. There is
 *    no `box-shadow` on any auth surface anywhere in app.css, including the one family that
 *    does use a real card (`.auth-box-3`, 11189). The weight comes from the full bleed split,
 *    not from elevation. So the border, the radius and --shadow-sm are gone, and the width
 *    goes from 26rem to the template's 524px.
 *
 * 2. THE BRAND PANEL CARRIES ART. signin-one.html:44 anchors an illustration to the bottom of
 *    the panel, bleeding 130px past the edge, under the text at a negative z-index. Ours is
 *    partials/auth-art.php, drawn in currentColor for the reasons that file gives.
 *
 * The panel's ground is --brand rather than the template's slate-100, ruled 2026-08-24: apps
 * get their own auth pages and the panel has to theme with them. That is variant two's colour
 * logic on variant one's content, and it is the one deliberate divergence in this file.
 *
 * The brand panel's own rules live in auth-panel.css, split off because the two together are
 * 234 lines and house rule 5 caps a component stylesheet at 200. This file is the frame and
 * the form column; that one is the coloured half.
 *
 * A page frame rather than a component, so it has no partial in views/components/. Every
 * selector is prefixed c-login.
 */

.c-login {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 1fr 1fr;
  overflow: hidden;
}

/* ---- the form column ---------------------------------------------------------------- */

/* .inner-content: the paint is the whole column, full height, no inset. */
.c-login__panel {
  background: var(--surface);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  overflow-y: auto;
}

/* .auth-box, app.css 11105. No background, no border, no radius, no shadow. */
.c-login__card {
  flex: 1;
  width: 100%;
  max-width: var(--auth-width);
  margin-inline: auto;
  padding: var(--auth-pad);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-5);
}

.c-login__mark {
  font-size: var(--text-xl); font-weight: var(--weight-bold);
  line-height: var(--leading-tight); letter-spacing: var(--tracking-wide);
  color: var(--brand);
}

/* An uploaded mark replaces the wordmark, capped rather than sized so a wide logo and a
   square one sit in the same slot uncropped. */
.c-login__mark--image { display: block; }
.c-login__mark-img { max-width: 100%; max-height: var(--space-9); height: auto; }

/*
 * The mark above the form, at every width. Entry [054].
 *
 * This class used to be --mobile and was display:none until the brand panel was hidden,
 * which is exactly what the template does (mobile-logo ... lg:hidden, on all twelve of its
 * auth pages). It is now permanent, and auth-panel.php records that no template variant
 * does this: it is the template's mobile arrangement promoted to every width, and it is
 * ours rather than a measurement.
 *
 * Centred, and capped so a wide logo does not set the card's width. The bottom margin is
 * the template's own mb-6 on that block.
 */
.c-login__mark--lead {
  display: block;
  margin: 0 auto var(--space-6);
  text-align: center;
}

.c-login__mark--lead .c-login__mark-img,
.c-login__mark--lead.c-login__mark--image .c-login__mark-img {
  margin-inline: auto;
  max-height: var(--space-10);
}

.c-login__head { display: flex; flex-direction: column; gap: var(--space-4); }

/* .auth-box h4, app.css 11121: 1.5rem on a 2rem line box, 0.75rem under it. */
.c-login__title {
  font-size: var(--text-2xl);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
  color: var(--ink);
}

.c-login__sub,
.c-login__lead {
  font-size: var(--text-md);
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

/* space-y-4, signin-one.html:64. */
.c-login__form { display: flex; flex-direction: column; gap: var(--space-5); }

.c-login__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
}

.c-login__link {
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-2);
  min-height: var(--touch-min);
  display: inline-flex;
  align-items: center;
}

.c-login__link:hover { color: var(--brand); }

/* mt-12 on the closing line, signin-one.html:126. The template puts no rule above it. */
.c-login__aside,
.c-login__exit { margin-top: var(--space-10); }

.c-login__aside {
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-3);
  text-align: center;
}

/*
 * .auth-footer, app.css 11174: pinned to the bottom of the column with 2.5rem under it.
 *
 * Sans, not mono, since entry [054]. It was monospace because it held `aisfapp_hub`, and a
 * database name is an identifier a person compares character by character, which is what
 * state-of-play.md section 3 says the mono face is for. It holds a copyright line now. That
 * is prose, and prose in a monospace face reads as a machine talking.
 */
.c-login__footer {
  padding: 0 var(--auth-pad) var(--space-9);
  text-align: center;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

/* .auth-box at md, app.css 11111. The template's 768 is not on our scale; --bp-md is. */
@media (min-width: 900px) {
  .c-login__card { padding: var(--auth-pad-y) var(--auth-pad-x); }
  .c-login__footer { padding-inline: var(--auth-pad-x); }
}

/*
 * The template drops the brand column below 1024px rather than stacking it, and so do we at
 * the nearest point on our scale. A coloured band above a form on a phone is decoration that
 * pushes the form off the screen, and the column carries its own wordmark for this case.
 */
@media (max-width: 1180px) {
  .c-login { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 640px) {
  .c-login__card { padding: var(--space-7) var(--space-5); }
  .c-login__footer { padding-inline: var(--space-5); }
}

/* ---- mascot.css ---- */
/*
 * mascot.css. The agent's face: its shape, its palette and its sizes. Entry [176].
 *
 * ## Every colour is a token and the state does not change any of them
 *
 * House rule 9: the mockup draws this robot in five hand picked gradients and none of them
 * crossed over. The shell is the surface the panel is on, the outline and the eyes are
 * --brand-border, the visor is --brand, and the ONE per app value is --brand-accent on the
 * antenna light, which is the same single identity dot shared/assets/css/components/
 * auth-panel.css gives the signed out illustration.
 *
 * A state changes the MOTION and never the palette. Sitemapify's accent sits 15 degrees from
 * --warning, so a mascot that turned amber while a scan ran would be a brand colour a person
 * reads as a status. design.md 3a. The words beside it carry the state; this carries the life.
 *
 * ## The size is the component's own geometry, not a spacing step
 *
 * --c-mascot-size is set here rather than in tokens.css for the same reason --c-meter-fill is:
 * it is one component's internal dimension and no other component has an opinion about it.
 * The three steps are the three places a mascot appears: beside a heading, inside a panel,
 * and as the picture on a screen that has nothing else on it yet.
 *
 * ## The motion is in mascot.motion.css
 *
 * House rule 5's 200 line cap split it, and the seam is the one the component already has:
 * this file is what the mascot IS and that file is what it DOES. Every keyframe, every state
 * modifier that animates, and the whole prefers-reduced-motion block are there.
 */

.c-mascot {
  --c-mascot-size: 7rem;
  display: flex;
  align-items: center;
  gap: var(--space-5);
  min-width: 0;
}

.c-mascot--sm { --c-mascot-size: 4.5rem; }
.c-mascot--lg { --c-mascot-size: 11rem; }

.c-mascot__stage {
  position: relative;
  display: grid;
  place-items: center;
  flex: none;
  width: var(--c-mascot-size);
  height: var(--c-mascot-size);
}

.c-mascot__art {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* The speech line. Not a tooltip and not a caption: it is the sentence the screen is making. */
.c-mascot__says {
  margin: var(--space-0);
  min-width: 0;
  padding: var(--space-4) var(--space-5);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

/*
 * lg is the picture on a screen that has nothing else on it yet, so it stacks and centres.
 * The line under it is capped at a measure rather than at the panel width: a single sentence
 * running the full width of a 1600px stage is one line of 200 characters, which nobody reads.
 */
.c-mascot--lg {
  flex-direction: column;
  gap: var(--space-4);
  text-align: center;
}

.c-mascot--lg .c-mascot__says { max-width: 52ch; }

/* ---- the palette, all of it ---- */

.c-mascot__shade { fill: var(--ink); opacity: 0.08; }
.c-mascot__glow { fill: var(--brand-subtle); opacity: 0.5; }
.c-mascot__stem { fill: none; stroke: var(--brand-border); stroke-width: 4; stroke-linecap: round; }
.c-mascot__spark { fill: var(--brand-accent, var(--brand-border)); }
.c-mascot__shell { fill: var(--surface); stroke: var(--brand-border); stroke-width: 2; }
.c-mascot__chest { fill: var(--brand-subtle); }
.c-mascot__visor { fill: var(--brand); }
.c-mascot__eye { fill: var(--brand-border); }
.c-mascot__smile { fill: none; stroke: var(--brand-border); stroke-width: 3.4; stroke-linecap: round; }
.c-mascot__frown { fill: none; stroke: var(--brand-border); stroke-width: 3.4; stroke-linecap: round; }
.c-mascot__gleam { fill: var(--surface); opacity: 0.7; }

/* One mouth at a time. The failed state is the only one that is not smiling. */
.c-mascot__frown { display: none; }
.c-mascot--failed .c-mascot__smile { display: none; }
.c-mascot--failed .c-mascot__frown { display: block; }
.c-mascot__sweep { fill: var(--brand-border); opacity: 0; }

/* ---- mascot.motion.css ---- */
/*
 * mascot.motion.css. Everything the agent's face does. Entry [176].
 *
 * Split from mascot.css under house rule 5's 200 line cap, on the seam the component already
 * has: that file is what the mascot IS, this is what it DOES.
 *
 * ## The one rule that governs all of it
 *
 * Nothing here runs faster than 900ms per cycle. --duration-slow's comment in tokens.css
 * carries the arithmetic: under about 350ms a cycle crosses three flashes a second, which is
 * the seizure threshold in WCAG 2.3.1.
 *
 * ## And the block at the bottom is not optional
 *
 * It turns every animation OFF under prefers-reduced-motion rather than leaving base.css to
 * collapse them to 0.01ms, because a collapsed animation FREEZES AT ITS LAST KEYFRAME: the
 * visor sweep would park below the chin and the blink would park shut. A mascot that cannot be
 * stilled is an accessibility fault, and one stilled mid blink is the same fault wearing a hat.
 */

/*
 * transform-box: fill-box is what makes transform-origin: center mean the middle of the eye
 * rather than the middle of the whole drawing. Without it a blink slides the eye off the face.
 */
.c-mascot__eye,
.c-mascot__spark,
.c-mascot__art {
  transform-box: fill-box;
  transform-origin: center;
}

.c-mascot__eye { animation: c-mascot-blink 5200ms var(--ease) infinite; }
.c-mascot__spark { animation: c-mascot-spark 2400ms var(--ease) infinite; }

.c-mascot--idle .c-mascot__art,
.c-mascot--starting .c-mascot__art,
.c-mascot--done .c-mascot__art { animation: c-mascot-float 4500ms var(--ease) infinite; }

/* Working is the only state with a sweep, and its light pulses at the fastest rate here. */
.c-mascot--working .c-mascot__sweep { animation: c-mascot-sweep 2600ms linear infinite; }
.c-mascot--working .c-mascot__spark { animation-duration: 900ms; }

/*
 * The arms, and they move only while it is working. Nine degrees off the shoulder, which is
 * about three pixels at the size this is ever drawn: enough that the figure is not a photograph
 * of a robot, not enough to become the thing on the screen a person is watching.
 */
.c-mascot__arm { transform-box: fill-box; transform-origin: top center; }
.c-mascot--working .c-mascot__arm--left { animation: c-mascot-arm-left 2600ms var(--ease) infinite; }
.c-mascot--working .c-mascot__arm--right { animation: c-mascot-arm-right 2600ms var(--ease) infinite; }

/* Failed is still and its light is out. Stillness is the signal, and the words say why. */
.c-mascot--failed .c-mascot__spark { animation: none; opacity: 0.35; }

/*
 * The orbit lives INSIDE the stage rather than outside it. The first version hung the ring off
 * the figure with a negative inset, which put it through the panel's left edge on the queued
 * screen: a ring cut in half by a card border is the "half styled" reading the whole entry is
 * against. So the stage grows by 45 per cent when there is a ring, the ring fills it, and the
 * art shrinks to 69 per cent of that, which is 1.0 of --c-mascot-size again. The figure is the
 * same size it is everywhere else and nothing crosses a border.
 */
.c-mascot--orbiting .c-mascot__stage {
  width: calc(var(--c-mascot-size) * 1.45);
  height: calc(var(--c-mascot-size) * 1.45);
}

.c-mascot--orbiting .c-mascot__art {
  width: 69%;
  height: 69%;
}

.c-mascot__orbit {
  position: absolute;
  inset: 0;
  border: var(--border-width) dashed var(--brand-border);
  border-radius: var(--radius-full);
  opacity: 0.7;
  animation: c-mascot-turn 34000ms linear infinite;
}

@keyframes c-mascot-blink {
  0%, 94%, 100% { transform: scaleY(1); }
  97%           { transform: scaleY(0.12); }
}

@keyframes c-mascot-spark {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}

@keyframes c-mascot-float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-5%); }
}

@keyframes c-mascot-sweep {
  0%        { transform: translateY(0); opacity: 0; }
  15%, 70%  { opacity: 0.28; }
  100%      { transform: translateY(64px); opacity: 0; }
}

@keyframes c-mascot-arm-left {
  0%, 100% { transform: rotate(0deg); }
  50%      { transform: rotate(-9deg); }
}

@keyframes c-mascot-arm-right {
  0%, 100% { transform: rotate(0deg); }
  50%      { transform: rotate(9deg); }
}

@keyframes c-mascot-turn {
  to { transform: rotate(360deg); }
}

/*
 * STILLED, not collapsed. base.css shortens every animation to 0.01ms, which for a two
 * keyframe loop means it lands on the LAST frame and stays there: a shut eye and a sweep
 * parked below the chin. Each one is turned off outright and pinned at the value it should
 * rest at, so a reader who asked for no motion gets the drawing rather than its final frame.
 */
@media (prefers-reduced-motion: reduce) {
  /*
   * EVERY SELECTOR HERE IS SCOPED TO .c-mascot, AND THAT IS THE WHOLE POINT OF THE BLOCK.
   *
   * The first version listed the bare element classes, `.c-mascot__sweep` at specificity 0,1,0.
   * The animations it was meant to stop are state scoped, `.c-mascot--working .c-mascot__sweep`
   * at 0,2,0, so the working agent went on sweeping its visor and waving its arms for a reader
   * who had asked for no motion. Two of six survived, the two that only exist in one state.
   *
   * FOUND BY THE HARNESS AND NOT BY READING IT: tools/verify/sitemapify-scan-dom.php drives
   * this page a second time with prefers-reduced-motion emulated and asks each animation for
   * its computed name. Nothing about the source looked wrong.
   *
   * Matching 0,2,0 and sitting later in the file wins the tie, and it wins it for any state
   * modifier added later without that modifier having to be listed here.
   */
  .c-mascot .c-mascot__eye,
  .c-mascot .c-mascot__spark,
  .c-mascot .c-mascot__art,
  .c-mascot .c-mascot__orbit,
  .c-mascot .c-mascot__arm,
  .c-mascot .c-mascot__sweep {
    animation: none;
    transform: none;
  }

  /*
   * And pinned at the value each should REST at, because base.css collapses an animation to
   * 0.01ms rather than removing it, which lands it on its last keyframe: a shut eye and a
   * sweep parked below the chin.
   */
  .c-mascot .c-mascot__eye,
  .c-mascot .c-mascot__spark { opacity: 1; }
  .c-mascot .c-mascot__sweep { opacity: 0; }
  .c-mascot--failed .c-mascot__spark { opacity: 0.35; }
}

/* ---- menu.css ---- */
/*
 * menu.css. The panel a menu opens, and the plain items in it. design.md component 22.
 *
 * Rewritten in entry [045] against the template's header rather than derived. The panel
 * is `nav-tools`, index.html 1825, measured into docs/component-specs.md under
 * app-header.
 *
 * The three triggers are in menu-trigger.css and the alert feed's head row and rich row
 * are in menu-feed.css. One component, three files, because one file is past the 200
 * line cap in house rule 5.
 *
 * Positioning is still two properties. The trigger's parent is the containing block
 * and the panel sits under it, aligned to whichever edge `align` chose. Nothing
 * measures anything at runtime, which is why this is CSS and not Popper.
 */

.c-menu {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
}

/*
 * The panel, index.html 1975. White, 1px border, radius 0.375rem (app.css 5629),
 * clipped, 23px below the trigger (app.css 3937).
 *
 * The template's shadow here is Tailwind's `shadow` (app.css 7398), which is not a
 * value tokens.css carries: our three shadows are the template's own surface shadows,
 * app.css 7418 and 7408. --shadow-md is the nearest and is used rather than adding a
 * fourth token for one panel. It is heavier than the template's by design: this panel
 * hangs over a page rather than sitting on it.
 *
 * 23px is measured from the trigger's own box in the template, whose header is 24px
 * taller than ours. Against a --touch-min trigger the same gap reads as a detached
 * panel, so ours hangs at --space-2 and the number is not carried across. That is the
 * one measurement on this component that is deliberately not the template's.
 */
.c-menu__list {
  position: absolute;
  top: calc(100% + var(--space-2));
  padding: var(--space-0);
  overflow: hidden;
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  /* Above the sticky bar it hangs from, below a modal, which owns the screen. */
  z-index: var(--z-overlay);
}

/*
 * Two widths, both the template's: w-44 on the profile and language menus (app.css
 * 4803), w-[335px] on the bell and mail panels (app.css 4806).
 *
 * The small one is a floor rather than a fixed width. The template's items are single
 * words, "Chat" and "Email"; ours are sentences, "Stop previewing", and at a fixed
 * 176px they wrap. So the number is where it starts and content decides the rest.
 */
.c-menu__list--sm {
  min-width: 11rem;
  width: max-content;
  max-width: min(20rem, calc(100vw - var(--space-9)));
}

/* 20.9375rem is 335px. In rem so it scales with a raised root size. */
.c-menu__list--lg {
  width: 20.9375rem;
  max-width: calc(100vw - var(--space-9));
}

.c-menu__list[hidden] { display: none; }

.c-menu--end .c-menu__list { right: 0; }
.c-menu--start .c-menu__list { left: 0; }

.c-menu__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-3) var(--space-5);
  text-align: left;
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-2);
  transition: background var(--duration-base) var(--ease), color var(--duration-base) var(--ease);
}

.c-menu__item:hover,
.c-menu__item:focus-visible {
  background: var(--surface-3);
  color: var(--ink);
  text-decoration: none;
}

/* The current choice. A tick as well as weight, because weight alone is not a signal. */
.c-menu__item--on { font-weight: var(--weight-semi); color: var(--ink); }

.c-menu__tick { margin-left: auto; flex: 0 0 auto; color: var(--brand); }

.c-menu__form { display: block; }

.c-menu__heading {
  padding: var(--space-3) var(--space-5) var(--space-2);
  font-family: var(--font-cond);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-3);
}

/*
 * A destructive item, and one the role cannot use. Entry [048], when the plan card's
 * controls moved into a menu and archive arrived with them: a destructive action that looks
 * identical to "move up" is the shape of an accident.
 */
.c-menu__item--danger { color: var(--danger); }
.c-menu__item--danger:hover:not(:disabled) { background: var(--danger-subtle); color: var(--danger); }

.c-menu__item--off,
.c-menu__item--off:hover { color: var(--ink-3); cursor: not-allowed; background: none; }

.c-menu__rule {
  margin: var(--space-0);
  border: 0;
  border-top: var(--border-width) solid var(--line-soft);
}

/* ---- menu-feed.css ---- */
/*
 * menu-feed.css. The alert panel's head row and its rows. design.md component 22.
 *
 * This is the template's notification dropdown, index.html 1975, with its geometry kept
 * and its content swapped. Measurements are in docs/component-specs.md under app-header.
 * Split from menu.css for the 200 line cap in house rule 5, not because it is a second
 * component: there is one menu and one panel, and the bell fills it differently.
 *
 * ## What transfers and what does not
 *
 * Transfers: the head row at 1rem all round with the title on the left and an underlined
 * link on the right; the row at 0.5rem by 1rem; the 32px slot with a 0.75rem gap; the
 * name line, the message under it and the timestamp under that; the 76px row this
 * computes to; the 1px slate-100 divider.
 *
 * Does not: the template's items are person-authored and carry a photograph and an
 * emoji. Ours are machine generated suite health with no author, and house rule 2 bans
 * emoji. So the face becomes severity and the name becomes the alert kind.
 *
 * ## The divider
 *
 * The template's BELL panel has no dividers: its item container is a bare
 * `<div role="none">` at index.html 1994. Its MAIL panel, which is the same object with
 * the same rows at index.html 1893, has `divide-y divide-slate-100`. The divider is
 * taken from the mail panel because our rows are three lines each and without a rule
 * between them a two-row panel reads as one six-line paragraph. app.css 5530 for the
 * 1px, 5562 for slate-100, which is --line-soft.
 */

/* The head row, index.html 1977. 1rem all round, computing to a 52px strip. */
.c-menu__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-5);
  border-bottom: var(--border-width) solid var(--line-soft);
}

/* 0.875rem weight 500 slate-700. app.css 6901, 6979, 7118. */
.c-menu__head-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-base);
  color: var(--ink);
}

/*
 * 0.75rem weight 400 slate-500, underlined. app.css 6897, 6991, 7174, 7344.
 *
 * The underline is the template's and is kept: it is the only link in the panel that is
 * not a whole row, so without it there is nothing telling it apart from the title beside
 * it. It takes --touch-min because it is a control in a corner, which the template's
 * 16px text does not.
 */
.c-menu__head-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  padding: var(--space-0) var(--space-2);
  margin: calc(var(--space-4) * -1) calc(var(--space-2) * -1);
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
  text-decoration: underline;
  color: var(--ink-2);
  white-space: nowrap;
}

.c-menu__head-link:hover,
.c-menu__head-link:focus-visible { color: var(--brand); }

/*
 * The row, index.html 1998. 0.5rem by 1rem around a 32px slot and a three line body,
 * which computes to 76px.
 */
.c-menu__row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-5);
  color: var(--ink-2);
  transition: background var(--duration-base) var(--ease);
}

.c-menu__row + .c-menu__row {
  border-top: var(--border-width) solid var(--line-soft);
}

.c-menu__row:hover,
.c-menu__row:focus-visible { background: var(--surface-3); text-decoration: none; }

/*
 * The slot the template fills with a face. aria-hidden, and it carries no meaning of its
 * own: the lamp beside it says the severity in a word, and a disc that was the only
 * signal would be the colour-only failure lamp exists to prevent.
 *
 * The tint is the severity colour at 14 percent over the card surface, which is the same
 * arithmetic banner.css uses, so an alert row and an alert banner are the same colour.
 * The flat --*-subtle value is declared first for a browser with no color-mix.
 */
.c-menu__row-mark {
  --c-mark: var(--muted);
  --c-mark-tint: var(--muted-subtle);
  width: var(--icon-face);
  height: var(--icon-face);
  flex: 0 0 auto;
  border-radius: var(--radius-full);
  background: var(--c-mark-tint);
  background: color-mix(in srgb, var(--c-mark) 14%, var(--surface));
  border: var(--border-width) solid var(--c-mark);
}

.c-menu__row-mark--crit { --c-mark: var(--danger); --c-mark-tint: var(--danger-subtle); }
.c-menu__row-mark--warn { --c-mark: var(--warning); --c-mark-tint: var(--warning-subtle); }
.c-menu__row-mark--live { --c-mark: var(--success); --c-mark-tint: var(--success-subtle); }
.c-menu__row-mark--ai   { --c-mark: var(--ai); --c-mark-tint: var(--ai-subtle); }

.c-menu__row-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
  flex: 1 1 auto;
}

/*
 * The message. 0.75rem on a 1rem line box, app.css 6897 and 7048, clamped to two lines
 * so one long sentence cannot make its row four times the height of the one above it.
 * The template's messages are written to fit; ours are built by a view from whatever a
 * provider name and a date happen to be.
 */
.c-menu__row-text {
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--ink-2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* 0.75rem slate-400. app.css 6897, 7178. */
.c-menu__row-when {
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--ink-3);
}

/* When there is nothing to show. The template has no empty state; a panel that opens
   on nothing and says nothing reads as broken rather than as quiet. */
.c-menu__empty {
  padding: var(--space-5);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

@media (prefers-reduced-motion: reduce) {
  .c-menu__row { transition: none; }
}

/* ---- menu-trigger.css ---- */
/*
 * menu-trigger.css. The three controls that open a menu. design.md component 22.
 *
 * Split out of menu.css in entry [045], when the header gained two more triggers and
 * the one file went past the 200 line cap in house rule 5. menu.css is the panel and
 * what is in it; this is what you press to get there. They are one component and the
 * split is a file size rule, not a seam.
 *
 * The pill has no template counterpart and keeps its derived control metrics. The other
 * two are measured: the circular icon button is in icon-button.css, and the profile
 * block is index.html 2039, read out into docs/component-specs.md under app-header.
 */

/* The default pill. No template counterpart, so it carries the control metrics. */
.c-menu__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--touch-min);
  max-width: 100%;
  padding: var(--control-pad-y) var(--control-pad-x);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-base);
  color: var(--ink-2);
  transition: background var(--duration-base) var(--ease), color var(--duration-base) var(--ease);
}

.c-menu__trigger:hover,
.c-menu__trigger[aria-expanded="true"] { background: var(--surface-2); color: var(--ink); }

.c-menu__icon { flex: 0 0 auto; }

.c-menu__stack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
}

.c-menu__name {
  font-weight: var(--weight-semi);
  color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}

/* The role, in the same uppercase treatment the sidebar foot gives it. */
.c-menu__note {
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-3);
}

/*
 * The profile block, index.html 2039. A 32px avatar, 10px, the name at 0.875rem weight
 * 400 slate-600, 10px, a 16px chevron. The 10px gaps are the template's own and are not
 * on the spacing scale, so they are written as --space-3 plus --space-1, which is 10px
 * exactly rather than a literal nobody can trace.
 *
 * The template's row is 44px of controls inside a 32px block because its avatar has no
 * hit area of its own. Ours takes --touch-min like every other control.
 */
.c-menu__profile {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--space-3) + var(--space-1));
  min-height: var(--touch-min);
  max-width: 100%;
  padding: var(--space-0) var(--space-2);
  border-radius: var(--radius-md);
  background: none;
  color: var(--ink-2);
  transition: background var(--duration-base) var(--ease), color var(--duration-base) var(--ease);
}

.c-menu__profile:hover,
.c-menu__profile[aria-expanded="true"] { background: var(--surface-2); color: var(--ink); }

/*
 * The mark alone, entry [165]. Sitemapify's header carried the customer's full email address
 * across the top bar, which is their identity printed at anybody standing behind them, and it
 * pushed everything else along. The circle says which site this is and the menu says who is
 * signed in, which is the split the rest of this suite already uses.
 *
 * The chevron goes with the name. A circle with an arrow beside it is two marks where one will
 * do, and the trigger already carries aria-haspopup.
 */
.c-menu__profile--bare {
  gap: var(--space-0);
  padding: var(--space-0);
  /* The mark is 32px and the rule is 44. Without this the trigger shrinks to the circle and
     becomes the one control in the suite that fails house rule 10's touch target. */
  min-width: var(--touch-min);
  justify-content: center;
}

.c-menu__profile--bare .c-menu__chevron { display: none; }

/*
 * The profile mark is .c-avatar, which lives in identity.css. It used to be duplicated here
 * as a second copy of the same circle; entry [048] made the two share rather than diverge,
 * because component 23 needed the identical shape for table rows and card headers.
 *
 * Nothing menu specific was left over once the shared shape came out, so there is no rule
 * here at all, only this note so the next reader does not go looking for one. menu.php emits
 * the mark through partials/avatar.php since entry [165], so the trigger can carry a real logo
 * and not only letters; `c-menu__avatar` went with that change, having painted nothing.
 *
 * One visible consequence, recorded so it is not read as a regression: the profile mark went
 * from solid --brand to the template's own no photograph treatment, --brand-subtle with --ink
 * on it (project.html:2257). That is the accessible direction as well as the template's:
 * 14.9:1 against 3.73:1.
 */

.c-menu__who {
  font-size: var(--text-sm);
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}

.c-menu__chevron {
  flex: 0 0 auto;
  transition: transform var(--duration-base) var(--ease);
}

.c-menu__trigger[aria-expanded="true"] .c-menu__chevron,
.c-menu__profile[aria-expanded="true"] .c-menu__chevron { transform: rotate(180deg); }

/*
 * On a phone the profile trigger keeps its target and drops its text, so the header
 * does not wrap to two rows for a name nobody needs to read there. The template hides
 * the name below 1024px and the whole block below 768px; ours keeps the block at every
 * width, because sign out lives in it and a phone is where somebody signs out.
 */
@media (max-width: 640px) {
  .c-menu__stack,
  .c-menu__who { display: none; }
  .c-menu__chevron { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .c-menu__chevron { transition: none; }
  .c-menu__trigger,
  .c-menu__profile { transition: none; }
}

/* ---- meter.css ---- */
/*
 * meter.css. A track and a fill, or the same component as a sparkline in bars mode.
 *
 * Geometry is the template's progressbar.html: track h-2 (0.5rem), slate-200,
 * rounded-xl, overflow hidden, with the fill a semantic colour at full height. The
 * sizes offered there are h-1 to h-4, which is what sm, md and lg step through.
 *
 * The track radius is --radius-pill rather than a 0.75rem literal. rounded-xl on a
 * track this short already clamps to a half height curve, so the pill renders the
 * template's shape exactly at every size we ship.
 *
 * The track colour is --line because that token holds slate-200, which is what the
 * template's track is. It is a surface here, not a border.
 *
 * The fill percentage is `--c-meter-fill`, built in meter.fill.css from the two data
 * attributes the partial writes (its tens and units). Entry [293] took out the style
 * attribute that carried it from 2026-08-21, and with it the policy's one exception.
 */

.c-meter {
  --c-meter-ink: var(--brand);
  --c-meter-ink-dim: var(--brand-border);
  display: block;
  min-width: 0;
}

/* .form-label, app.css 9503: 0.875rem, weight 500, slate-600, 0.5rem below. */
.c-meter__label {
  display: block;
  margin-bottom: var(--space-3);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-base);
  letter-spacing: normal;
  color: var(--ink-2);
}

.c-meter__track {
  display: block;
  width: 100%;
  min-width: var(--space-11);
  height: var(--space-3);
  background: var(--line);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.c-meter--sm .c-meter__track { height: var(--space-2); }
.c-meter--lg .c-meter__track { height: var(--space-4); }

.c-meter__fill {
  display: block;
  height: 100%;
  width: var(--c-meter-fill, 0%);
  background: var(--c-meter-ink);
  transition: width var(--duration-base) var(--ease);
}

/* Bars mode. Twelve tracks stood on end, not a chart library. */
.c-meter__bars {
  display: flex;
  align-items: flex-end;
  gap: var(--space-1);
  height: var(--space-11);
}

.c-meter__bar {
  display: block;
  flex: 1;
  min-width: 0;
  min-height: var(--space-1);
  height: var(--c-meter-fill, 0%);
  border-radius: var(--radius-xs);
  background: var(--muted);
}

.c-meter__bar--fill { background: var(--c-meter-ink-dim); }
.c-meter__bar--peak { background: var(--c-meter-ink); }
.c-meter__bar--off { background: var(--muted); }

.c-meter--action { --c-meter-ink: var(--action); --c-meter-ink-dim: var(--action-hover); }
.c-meter--success { --c-meter-ink: var(--success); --c-meter-ink-dim: var(--success-dim); }
.c-meter--warning { --c-meter-ink: var(--warning); --c-meter-ink-dim: var(--warning-dim); }
.c-meter--danger { --c-meter-ink: var(--danger); --c-meter-ink-dim: var(--danger-dim); }
.c-meter--info { --c-meter-ink: var(--info); --c-meter-ink-dim: var(--info-dim); }
.c-meter--ai { --c-meter-ink: var(--ai); --c-meter-ink-dim: var(--ai-dim); }
.c-meter--mute { --c-meter-ink: var(--ink-3); --c-meter-ink-dim: var(--muted); }

/*
 * Ring mode. The same percentage, bent into a circle. Entry [176].
 *
 * One conic-gradient reading --c-meter-fill, which is the property this component already
 * carries, so a ring costs no second inline style and no second code path: PHP writes the
 * number once and both shapes read it. The fill element becomes the hole rather than being
 * dropped, because the poll addresses the track by its role and the two elements are what
 * that assumption is written against.
 *
 * NO TRANSITION, deliberately, and it is the one place this differs from the bar. A conic
 * gradient interpolates only if --c-meter-fill is registered with @property, and registering
 * it would make the property's COMPUTED value animate: every probe that sets it and reads it
 * back in the same tick would then measure a frame of an animation rather than the value it
 * just set, which is exactly the fault sitemapify-scan-dom.js already had once. The ring
 * steps when the count steps, which is what the count does.
 *
 * --c-meter-ring is the component's own geometry, the same kind of value --c-meter-fill is,
 * and it is not a spacing step because no other component has an opinion about it.
 */
.c-meter--ring {
  --c-meter-ring: 6.5rem;
  display: grid;
  place-items: center;
}

.c-meter--ring .c-meter__track {
  position: relative;
  width: var(--c-meter-ring);
  height: var(--c-meter-ring);
  min-width: 0;
  border-radius: var(--radius-full);
  /*
   * The REST arc is --muted-dim and not --line, which is the one place a ring differs from a
   * bar. A 0.5rem track reads against --line because the fill sits directly along it over a
   * short run; a 6.5rem ring puts a large area of near white on a white card, and the arc
   * that has not filled yet simply disappears, so the ring reads as complete at every value.
   * Measured on the running fixture at 63 per cent, where the missing third was invisible.
   */
  background: conic-gradient(
    var(--c-meter-ink) 0 var(--c-meter-fill, 0%),
    var(--muted-dim) var(--c-meter-fill, 0%) 100%
  );
}

.c-meter--ring .c-meter__fill {
  position: absolute;
  inset: var(--space-4);
  width: auto;
  height: auto;
  border-radius: var(--radius-full);
  background: var(--surface);
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .c-meter__fill { transition: none; }
}

/* ---- meter.fill.css ---- */
/*
 * meter.fill.css. The meter's fill percentage from its two data attributes. Entry [293].
 *
 * A fill is the one number a stylesheet cannot know. The partial writes it as tens and units
 * (data-meter-t="4" data-meter-u="2" for 42) and these rules add the two back into
 * `--c-meter-fill`, the property meter.css, the ring and the bars already read. Twenty two
 * selectors in place of a style attribute: the owner's ruling was that a documented exception
 * to the CSP is still the thing that makes the next one easy to argue for, so Core\Response now
 * sends style-src-attr 'none'.
 *
 * `.c-meter__bar` repeats the sum because each bar of a sparkline has its own number. A zero
 * is stated rather than left to the fallback, so a bar never inherits its parent's digits.
 * Script that moves a live meter sets `--c-meter-fill` itself with style.setProperty, which is
 * CSSOM rather than an attribute, and an inline declaration outranks every rule here.
 */

.c-meter,
.c-meter__bar { --c-meter-fill: calc(var(--c-meter-t, 0%) + var(--c-meter-u, 0%)); }
[data-meter-t="0"] { --c-meter-t: 0%; }
[data-meter-t="1"] { --c-meter-t: 10%; }
[data-meter-t="2"] { --c-meter-t: 20%; }
[data-meter-t="3"] { --c-meter-t: 30%; }
[data-meter-t="4"] { --c-meter-t: 40%; }
[data-meter-t="5"] { --c-meter-t: 50%; }
[data-meter-t="6"] { --c-meter-t: 60%; }
[data-meter-t="7"] { --c-meter-t: 70%; }
[data-meter-t="8"] { --c-meter-t: 80%; }
[data-meter-t="9"] { --c-meter-t: 90%; }
[data-meter-t="10"] { --c-meter-t: 100%; }

[data-meter-u="0"] { --c-meter-u: 0%; }
[data-meter-u="1"] { --c-meter-u: 1%; }
[data-meter-u="2"] { --c-meter-u: 2%; }
[data-meter-u="3"] { --c-meter-u: 3%; }
[data-meter-u="4"] { --c-meter-u: 4%; }
[data-meter-u="5"] { --c-meter-u: 5%; }
[data-meter-u="6"] { --c-meter-u: 6%; }
[data-meter-u="7"] { --c-meter-u: 7%; }
[data-meter-u="8"] { --c-meter-u: 8%; }
[data-meter-u="9"] { --c-meter-u: 9%; }

/* ---- meter.split.css ---- */
/*
 * meter.split.css. The `split` shape, entry [237]: the approved Sitemapify file's coverage
 * bar. A head line and a mono aside above the track, a hatched remainder where the fill
 * stops, and a legend under it. Every rule here is reached by `shape => 'split'` and the
 * props head, aside, aside_tone, rest and legend on shared/views/components/meter.php.
 *
 * Its own file because meter.css sits at 151 lines and this is fifty more. `.split` after the
 * dot so the build emits it after meter.css.
 *
 * The hatch is a repeating-linear-gradient of the warning tint and a second tint a fifth of the
 * way to the warning itself, 7px and 7px, the approved file's own stripe (entry [243]; until
 * then a 1px rule of the dim on a 7px tint), which is how the
 * approved file draws "listed with Google, never opened by us": not empty, not filled,
 * something that exists and was not looked at. The angle and the 7px stripe are the file's
 * own and have no token; they are a picture, not a measurement, so they are stated here once.
 */
.c-meter--split .c-meter__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}

.c-meter--split .c-meter__head-text {
  font-family: var(--font-sans);
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  color: var(--ink);
}

.c-meter__aside {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.c-meter__aside--success { color: var(--success-ink); }
.c-meter__aside--warning { color: var(--warn-ink); }
.c-meter__aside--danger  { color: var(--danger-ink); }
.c-meter__aside--mute    { color: var(--ink-3); }

/* The split track is the lg height whatever size was asked, so the hatch has room to read. */
.c-meter--split .c-meter__track {
  height: var(--space-4);
}

.c-meter__track--rest-warning {
  background: repeating-linear-gradient(
    115deg,
    var(--warning-subtle) 0,
    var(--warning-subtle) 7px,
    color-mix(in srgb, var(--warning) 20%, var(--warning-subtle)) 7px,
    color-mix(in srgb, var(--warning) 20%, var(--warning-subtle)) 14px
  );
}

.c-meter__track--rest-mute {
  background: repeating-linear-gradient(
    115deg,
    var(--muted-subtle) 0,
    var(--muted-subtle) 7px,
    color-mix(in srgb, var(--muted) 20%, var(--muted-subtle)) 7px,
    color-mix(in srgb, var(--muted) 20%, var(--muted-subtle)) 14px
  );
}

.c-meter__legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4) var(--space-6);
  margin-top: var(--space-4);
}

.c-meter__legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

.c-meter__swatch {
  width: var(--space-4);
  height: var(--space-4);
  flex: none;
  border-radius: var(--radius-xs);
}

.c-meter__swatch--fill { background: var(--c-meter-ink); }

.c-meter__swatch--rest {
  background: repeating-linear-gradient(
    115deg,
    var(--warning-subtle) 0,
    var(--warning-subtle) 4px,
    var(--warning-dim) 4px,
    var(--warning-dim) 5px
  );
}

.c-meter__track--rest-mute ~ .c-meter__legend .c-meter__swatch--rest {
  background: repeating-linear-gradient(
    115deg,
    var(--muted-subtle) 0,
    var(--muted-subtle) 4px,
    var(--muted-dim) 4px,
    var(--muted-dim) 5px
  );
}

/* ---- nav.css ---- */
/*
 * nav.css. The sidebar, rendered into .l-shell__nav. The primitive owns the column
 * width (248px, app.css 12911), the surface, the border, the sticky behaviour and the
 * scroll, so none of that is repeated here. The collapsed rail is nav-rail.css.
 *
 * Geometry is .navItem, app.css 12984: 0.875rem type, weight 500, a 1.25rem line box,
 * a 200ms transition. Phase 2 built the row at --text-md, the prototype's size rather
 * than the template's. Padding takes the row to --touch-min: 0.75rem + 1.25rem +
 * 0.75rem is 44px exactly. The foot has no template counterpart and follows the rhythm.
 */

.c-nav {
  display: flex;
  flex-direction: column;
  min-height: 100%;
}

/* The brand block, the mark and the drawn wordmark are in nav.brand.css, split at the cap in
   entry [243]. */

/* Inset 8px each side, entry [243]: the approved file's rows are rounded pills standing in from the
   sidebar's edge (its 10px), and the current row's bar stands in the inset. Every width. */
.c-nav__groups { padding: var(--space-4) var(--space-3) var(--space-8); flex: 1 1 auto; }
.c-nav__group + .c-nav__group { margin-top: var(--space-5); }
.c-nav__group-title {
  padding: var(--space-3) var(--space-6);
  font-family: var(--font-cond);
  font-size: var(--text-xs);
  /* 700 and 0.13em are the approved file's, entry [242]; the size stayed 12 by [240 ruling 2]. */
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-3); /* the token clears 4.5 since [244]; it measured 2.56 on white before */
}

/* .navItem, app.css 12984. 0.75rem of padding around a 1.25rem line box is 44px. */
.c-nav__item {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-4) var(--space-6);
  text-align: left;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-base);
  color: var(--ink-2);
  position: relative;
  border-radius: var(--radius-sm);
  transition: background var(--duration-base) var(--ease), color var(--duration-base) var(--ease);
}

/* The current row, entry [239]: the approved file's blue-50 ground, blue words, blue bar. The
   action colour, because a nav row is the thing a person clicks. */
.c-nav__item:hover { background: var(--action-subtle); color: var(--ink); text-decoration: none; }

.c-nav__item--here {
  background: var(--action-subtle);
  color: var(--action-text);
  font-weight: var(--weight-semi);
}

/* The bar, in the groups' inset to the left of the rounded row, as the file draws it: 4px wide
   (the file's 3 snapped), 8px in from the row's top and bottom. Entry [243]; it was a border-left
   on the row itself. */
.c-nav__item--here::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-3));
  top: var(--space-3);
  bottom: var(--space-3);
  width: var(--space-2);
  border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
  background: var(--action);
}

.c-nav__item--here .c-nav__num,
.c-nav__item--here .c-nav__icon { color: var(--action-text); }

.c-nav__item--locked { color: var(--ink-3); cursor: not-allowed; }
.c-nav__item--locked:hover { background: none; color: var(--ink-3); }

.c-nav__num {
  width: var(--space-6); flex: 0 0 auto;
  font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-3);
}

/* The glyph takes the number chip's slot at the same width, so rows with and without
   one still line up down the column. The label carries the meaning; this is second. */
.c-nav__icon {
  width: var(--space-6); height: var(--space-6); flex: 0 0 auto; display: inline-flex;
  color: var(--ink-3);
}
.c-nav__icon svg { width: 100%; height: 100%; }
.c-nav__label {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* A word, not a colour and not an icon. Colour is never the only signal. */
.c-nav__lock {
  flex: 0 0 auto;
  font-family: var(--font-cond);
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-3);
  border: var(--border-width) solid var(--muted-dim);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
}

/* The count bubble takes .badge.pill, app.css 8041 and 8055. */
/* [291]: a connected search engine's row. A mark, not a count, so no text is drawn; the label is
   in an l-sr span and the title. */
.c-nav__dot {
  flex: 0 0 auto;
  width: var(--space-3);
  height: var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--success-dim);
}

.c-nav__badge {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  background: var(--danger);
  color: var(--ground);
  border-radius: var(--radius-pill);
  /* space-2, not space-3, entry [251] A3: at space-3 a two digit badge left the longest label,
     "Sitemap health", one pixel short of its width and it ellipsised to "Sitemap hea...". */
  padding: var(--space-1) var(--space-2);
}

/* No drawer since entry [240]: the sidebar stacks above the page under --bp-md (layout.css), so
   the close control inside it is never drawn. The groups keep their foot padding there. */
@media (max-width: 900px) {
  /* In flow above the page, entry [241]: no viewport height to fill, so no white above the foot. */
  .c-nav { min-height: auto; }
  .c-nav__groups { padding-bottom: var(--space-5); }
}

/* Identity and sign out, gap G9. margin-top:auto pins it to the foot. */
.c-nav__foot {
  margin-top: auto;
  border-top: var(--border-width) solid var(--line);
  padding: var(--space-5) var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.c-nav__who { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.c-nav__who-name {
  font-size: var(--text-sm); font-weight: var(--weight-semi);
  line-height: var(--leading-base); color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.c-nav__who-role {
  font-size: var(--text-xs); line-height: var(--leading-tight);
  color: var(--ink-3);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}

/* ---- nav.aside.css ---- */
/*
 * nav.aside.css. The optional block at the foot of the sidebar, above the user.
 *
 * Its own file because nav.css is at the 200 line cap in CLAUDE.md house rule 5, and the seam
 * is the one button.variants.css already draws: nav.css is the menu every mount gets, and this
 * is what a mount asks for by prop. Sitemapify passes a plan name and an upgrade link; nothing
 * else passes one, and an empty prop renders no element at all.
 *
 * Emitted AFTER nav.css by build-library-css.sh, which ranks a family's base file first.
 * Entry [158].
 */

/*
 * The optional block above the user, entry [158]. Sitemapify puts its plan and an upgrade link
 * there; nothing else passes one, and an empty prop renders no element at all.
 *
 * It sits INSIDE __foot and above __who, so the rail state, the drawer and the sticky footer
 * all keep working without a second rule: __foot already owns the border, the padding and the
 * stacking, and this is one more child of it.
 */
/* The plan box, entry [239]: the approved Sitemapify file's `.planbox`, a bordered card at the
   sidebar's foot rather than a ruled strip. Label, plan name, one line of figures, a meter and
   one button; the words and the meter's tone come from the caller, the frame from here. */
.c-nav__aside {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-4);
  min-width: 0;
}

.c-nav__aside .c-meter { margin-top: var(--space-2); }
.c-nav__aside .c-button { margin-top: var(--space-3); }

.c-nav__aside-label {
  font-size: var(--text-xs);
  color: var(--ink-3); /* the file's faint ink, [241 ruling 4]; the token clears 4.5 since [244] */
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

.c-nav__aside-value {
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The rail hides it whole: a plan name in a 64px column is a truncated word, not information. */
.l-shell--rail .c-nav__aside { display: none; }

/* The line under the plan name: what the plan gives and how much is gone. Entry [159]. */
.c-nav__aside-note {
  font-size: var(--text-xs);
  color: var(--ink-3);
}

/*
 * STICKY AT THE FOOT, entry [292]: with an aside, the foot stays at the bottom of the sidebar and
 * the rows scroll above it. Only when a mount passes an aside, so the hub's foot is unchanged.
 * Under --bp-md the sidebar stacks in flow above the page (layout.css), and the foot flows with it.
 */
.c-nav__foot--aside {
  position: sticky;
  bottom: 0;
  background: var(--surface);
}

@media (max-width: 900px) {
  .c-nav__foot--aside { position: static; }
}

/* The plan card's states, entry [292]: amber while a cancelled plan runs out, red once refunded. */
.c-nav__aside--warning {
  border-color: var(--warning);
  background: var(--warning-subtle);
}

.c-nav__aside--danger {
  border-color: var(--danger);
  background: var(--danger-subtle);
}

.c-nav__aside-head { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }

/* The countdown: every state carries one when its date is stored, and none when it is not. */
.c-nav__aside-countdown {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding-top: var(--space-3);
  border-top: var(--border-width) solid var(--line-soft);
  font-size: var(--text-xs);
  color: var(--ink-3);
}

.c-nav__aside-countdown b { color: var(--ink); font-weight: var(--weight-semi); }
.c-nav__aside-countdown--warning b { color: var(--warn-ink); }
.c-nav__aside-countdown--danger b { color: var(--danger-ink); }
.c-nav__aside-countdown svg { flex: none; }

/* ---- nav.badge.css ---- */
/*
 * nav.badge.css. The count bubble's tones, entry [292]: "Issue counts are red for high, amber for
 * medium, grey otherwise." An item's `tone` picks one; an item with none keeps the solid red badge
 * nav.css has always drawn, so every mount that passes no tone is unchanged.
 *
 * The tag's pairing (tag.css): each text colour is the ink that clears 4.5:1 on its own tint. The
 * number is never the only signal: the row's label says what it counts.
 */
.c-nav__badge--danger {
  background: var(--danger-subtle);
  color: var(--danger-ink);
}

.c-nav__badge--warning {
  background: var(--warning-subtle);
  color: var(--warn-ink);
}

.c-nav__badge--mute {
  background: var(--muted-subtle);
  color: var(--ink-2);
}

/* A toned row gives its count room: the file's Issues labels ("Canonical conflicts") ellipsised
   at the 230px rail with the plain row's gaps. The right padding and the label's gap to the count
   close up; the icon's gap to the label stays, so the labels still line up down the column. Only
   toned rows, so every other row is unchanged. */
.c-nav__item:has(.c-nav__badge--danger, .c-nav__badge--warning, .c-nav__badge--mute) {
  padding-right: var(--space-3);
}

.c-nav__item:has(.c-nav__badge--danger, .c-nav__badge--warning, .c-nav__badge--mute) .c-nav__label {
  margin-right: calc(-1 * var(--space-2));
}

/* ---- nav.brand.css ---- */
/*
 * nav.brand.css. The sidebar's head: the brand block at --bar-height, the mark (an uploaded logo,
 * the rail's initials, or since entry [243] the approved Sitemapify file's DRAWN wordmark), the
 * name and the note under it. Split from nav.css at house rule 5's cap in [243]; `.brand` after
 * the dot so the build emits it after nav.css. Every rule here is reached by `logo`, `icon`,
 * `brand` or `wordmark` on shared/views/components/nav.php.
 */

/* Brand block. It stands at --bar-height so the head of the sidebar lines up with
   the top bar beside it. */
.c-nav__brand {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-height: var(--bar-height);
  padding: var(--space-4) var(--space-6);
  border-bottom: var(--border-width) solid var(--line);
  flex: 0 0 auto;
}

.c-nav__mark {
  width: var(--space-8); height: var(--space-8); flex: 0 0 auto;
  border: var(--border-width) solid var(--brand);
  border-radius: var(--radius-md);
  background: var(--brand-subtle);
}

.c-nav__brand-text { display: flex; flex-direction: column; min-width: 0; }

/* The drawn wordmark, entry [243]: the approved Sitemapify file's brand block. A 32px mark with
   no box of its own, the name 18px/800 in the ink with its last letters in the action colour
   (the one place that colour is type, as the brand mark, design.md's collision rule), and an
   8px caps tagline in the faintest ink. */
.c-nav__mark--drawn {
  border: 0;
  background: none;
  border-radius: 0;
  display: inline-flex;
}

.c-nav__mark--drawn svg { width: 100%; height: 100%; }

.c-nav__word {
  font-family: var(--font-cond);
  font-size: var(--text-lg);
  font-weight: var(--weight-heavy);
  letter-spacing: -0.02em;
  line-height: 1.05;
  color: var(--brand);
  white-space: nowrap;
}

.c-nav__word-accent { font-style: normal; color: var(--action); }

.c-nav__tagline {
  margin-top: var(--space-1);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semi);
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--ink-3); /* 5.17:1 on white since the token moved in [244] */
  white-space: nowrap;
}
.c-nav__brand-name {
  font-family: var(--font-cond);
  font-weight: var(--weight-bold);
  font-size: var(--text-md);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink);
}

.c-nav__brand-note {
  font-family: var(--font-mono); font-size: var(--text-xs);
  line-height: var(--leading-tight); letter-spacing: var(--tracking-wide);
  color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- nav.lock.css ---- */
/*
 * nav.lock.css. A sidebar row that is locked and can still be opened.
 *
 * Entry [175]. An extension file rather than more of nav.css, which is at its 200 line
 * cap, and the same split nav.aside.css already makes.
 *
 * ## Why this is not .c-nav__item--locked with the cursor put back
 *
 * They are two different facts and they read differently on purpose. A ROLE gate row is
 * `--locked`: dimmed, `cursor: not-allowed`, no hover, because nothing happens when you
 * press it. A PLAN gate row is `--lockable`: it answers a pointer like every other link,
 * because something does happen. It opens the wall that says what the screen is and what
 * it costs, which is the one page a customer on the free plan most needs to reach.
 *
 * The row is still quieter than an open one. The label keeps --ink-2 from the base rule
 * and the chip carries the word, so the difference is legible without colour being the
 * only signal, which is design.md section 7.
 */

.c-nav__item--lockable .c-nav__label { color: var(--ink-3); }

/* The chip is the signal, so it brightens with the row rather than staying flat. */
.c-nav__item--lockable:hover .c-nav__lock,
.c-nav__item--lockable:focus-visible .c-nav__lock {
  color: var(--ink-2);
  border-color: var(--ink-3);
}

.c-nav__item--lockable.c-nav__item--here .c-nav__label { color: var(--ink); }

/*
 * THE RAIL HAS NO ROOM FOR THE CHIP. At 64px the label has already clipped to nothing
 * and the chip would sit against the glyph and overflow the column. It goes, and the
 * tooltip carries the word instead: nav.php appends ", locked" to data-tip for exactly
 * these rows. The chip is removed rather than hidden because, unlike a label, it is not
 * content a screen reader needs twice: the wall the row opens says the same thing in a
 * sentence.
 */
@media (min-width: 1181px) {
  .c-nav--rail .c-nav__lock { display: none; }
}

/* ---- nav-mark.css ---- */
/*
 * nav-mark.css. The uploaded brand mark in the sidebar, and what it becomes on the rail.
 *
 * Split out of nav.css 2026-08-26, entry [059], because nav.css was 215 lines against the
 * 200 line cap in house rule 5. It had been over since entry [054] and nothing was checking,
 * which is what tools/check-file-size.sh now exists to stop.
 *
 * The seam is the subject rather than the line count: everything here is the uploaded mark
 * from entry [054], including the two rail rules, which belong with the mark and not with the
 * rail. nav.css is the sidebar's structure, groups, items and foot.
 *
 * Emitted after nav.css by shared/assets/build-library-css.sh, which orders a base ahead of
 * its extensions.
 */

/* The uploaded mark keeps the default square's box, so the brand block does not change
   height when a logo is set, and contain lets a wide logo and a square icon both sit in
   it without either being cropped. */
/*
 * An uploaded mark, entry [054]. It is no longer a 32px square beside a wordmark: the logo
 * IS the brand block now, so it takes the block's width and is capped on height instead.
 *
 * A logo already contains the name, so there is no wordmark beside it to leave room for.
 * The default square below is what renders when nothing is uploaded, and that one keeps its
 * fixed size because it sits beside type.
 */
.c-nav__mark--image {
  border-color: transparent; background: none; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: flex-start;
  width: auto; height: auto; max-width: 100%; flex: 1 1 auto; min-width: 0;
}

.c-nav__mark-img { max-width: 100%; max-height: var(--space-9); width: auto; object-fit: contain; }

/*
 * On the rail the mark becomes the icon and has to fit the collapsed width, so it goes back
 * to a square. --icon-face is the same 32px the header's controls use, which is what puts
 * the rail's mark on the same line as everything else in the row.
 */
.c-nav--rail .c-nav__mark--image { flex: 0 0 auto; width: var(--icon-face); height: var(--icon-face); }
.c-nav--rail .c-nav__mark-img { max-height: 100%; }

/*
 * The letters that stand in when nothing is uploaded, entry [090].
 *
 * Same box as the default square above, so the brand block does not change height between an
 * installation that has an icon and one that does not. It only adds what is needed to centre
 * type in it, because the square was built to hold nothing and had no reason to say how text
 * inside it should sit.
 *
 * --ink on --brand-subtle, which is the pair identity.css already measured at 14.9:1 for the
 * avatar. The same two letters, the same fill, the same contrast argument: this is the suite's
 * no-asset mark, drawn in the one place that had been drawing an empty box instead.
 */
.c-nav__mark--letters {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  line-height: 1;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--ink);
}

/* ---- nav-rail.css ---- */
/*
 * nav-rail.css. The collapsed sidebar.
 *
 * Split from nav.css because that file is at its 200 line cap, and because the split
 * is a real one: nav.css is the sidebar, this is a state the sidebar can be put into.
 *
 * ## The rail is a wide screen state and the drawer is a narrow screen state
 *
 * Every rule here lives inside `min-width: 1181px`, which is the same inverse of the
 * drawer breakpoint that layout.css already uses for the scrim. Below 1180px the
 * sidebar is off canvas and opens over the page, and a 64px drawer would be a control
 * with nothing usable in it. So the two states cannot both apply: a person who
 * collapsed the rail on a desktop and then opens the same account on a phone gets the
 * normal drawer, and the stored preference is waiting when they go back.
 *
 * The control that collapses it is not styled here and is not in the sidebar at all.
 * It is `.c-topbar__collapse`, in topbar.css, entry [044].
 *
 * ## Why the labels are not hidden
 *
 * There is no `display: none` on a label anywhere here, and no second set of rules to
 * put them back. The rail is narrow, `.c-nav__label` already carries
 * `flex: 1 1 auto; min-width: 0; overflow: hidden`, so at 64px it simply has no room
 * and clips. Widening the sidebar gives the room back. One property drives the whole
 * state change, the text never leaves the document, and a screen reader reads the
 * sidebar identically whether it is railed or not.
 *
 * ## The rail does not widen on hover, and this file is where that was undone
 *
 * It used to. `.l-shell__nav:hover` and `:focus-within` took it back to full width and
 * overlaid the page, which answered the "what is this glyph" question without a
 * tooltip and without script. Two things were wrong with it. Passing the pointer
 * across the left edge of the screen on the way somewhere else threw a 248px panel
 * over the page, and `:focus-within` meant tabbing into the sidebar to reach one row
 * opened the whole thing. A rail is a decision a person made; it should stay made
 * until they unmake it with the control.
 *
 * What replaced it is a real tooltip: `data-tip` on the row, `tooltip.css` for the
 * look, `tooltip.js` for the placement, and a single surface that is a child of body
 * so the `overflow-y: auto` on `.l-shell__nav` has nothing to clip. It works on
 * keyboard focus as well as hover, which is the one thing `title` never does.
 */

@media (min-width: 1181px) {
  /*
   * The column. Both the grid track and the sidebar take the rail width now: with no
   * hover state there is nothing left that needs to grow past its own track.
   */
  .l-shell--rail { grid-template-columns: var(--nav-rail) minmax(0, 1fr); }

  .l-shell--rail .l-shell__nav {
    width: var(--nav-rail);
    overflow-x: hidden;
    transition: width var(--duration-base) var(--ease);
  }

  /*
   * Centre the glyph while the row is narrow. The row keeps its full height, so it is
   * still a 44px target, and it keeps its bar (the ::before in the groups' inset) so the current
   * screen is still marked in the rail.
   */
  .c-nav--rail .c-nav__item { padding-left: var(--space-5); padding-right: var(--space-5); }

  /*
   * Text that cannot clip gracefully. `visibility` rather than `display` so the block
   * keeps its height and the rows do not jump when the sidebar widens.
   */
  .c-nav--rail .c-nav__group-title,
  .c-nav--rail .c-nav__brand-text,
  .c-nav--rail .c-nav__who {
    visibility: hidden;
    white-space: nowrap;
  }

  /*
   * The count bubble survives the rail, because an unread count is the one thing a
   * person collapses the sidebar and still wants to see. It loses its number and
   * becomes a dot on the glyph, so it fits in 64px without overlapping it.
   *
   * The number is still in the DOM and still read aloud. What the tooltip carries is
   * the label, not the count, so a sighted person who needs the figure expands the
   * sidebar: a dot says "look here", which is all a 64px rail can honestly say.
   */
  .c-nav--rail .c-nav__row { position: relative; }

  .c-nav--rail .c-nav__badge {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    min-width: var(--space-3);
    height: var(--space-3);
    padding: var(--space-0);
    overflow: hidden;
    text-indent: -100vw;
  }

  /* The sidebar foot. The sign out button keeps its target and loses its word. */
  .c-nav--rail .c-nav__foot { padding-left: var(--space-5); padding-right: var(--space-5); }
}

/*
 * Width is an animated property here, which is exactly the kind of movement someone
 * with a vestibular disorder asks the browser to stop. The panel honours that request
 * everywhere else and this is not an exception.
 */
@media (prefers-reduced-motion: reduce) {
  .l-shell--rail .l-shell__nav { transition: none; }
}

/* ---- notice.css ---- */
/*
 * notice.css. The locked wall on 19 screens and the empty state on every table with
 * no rows. One component, two variants, because they differ in copy and tone only.
 *
 * It draws no border and no background. Whatever holds it, a panel body or a table
 * cell spanning every column, already has both, and a second frame inside the first
 * is the thing that makes an empty table look broken rather than quiet.
 *
 * DERIVED, not read from the template: the template has no notice. It fills the same
 * role as .alert, a sentence about state sitting on the card surface, so it takes the
 * .alert box (app.css 8540): 18px vertical, 1.5rem horizontal, 0.875rem on a 1.25rem
 * line. The heading takes .card-title. That also puts it on the same vertical rhythm
 * as a table row, which matters because the empty state renders inside a table cell.
 *
 * The 18px is written as --space-6 minus --space-1 because no token carries it. See
 * the note in banner.css and the report.
 *
 * The one measurement not from tokens.css is the 58ch reading width on the sentence.
 * tokens.css has no measure token, and ch is a relative unit tied to the font rather
 * than a hardcoded pixel width, so this needs no new token.
 */

.c-notice {
  padding: var(--alert-pad-y) var(--cell-pad-x);
  text-align: center;
}

.c-notice__heading {
  margin-bottom: var(--space-3);
  font-family: var(--font-sans);
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
  letter-spacing: normal;
  color: var(--ink);
}

.c-notice__reason,
.c-notice__next {
  max-width: 58ch;
  margin-left: auto;
  margin-right: auto;
  font-size: var(--text-sm);
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

.c-notice__next {
  margin-top: var(--space-2);
  color: var(--ink-3);
}

.c-notice__code {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--ink-2);
}

/*
 * Locked is amber because it is a wall a person can do something about, and the
 * heading says so in words as well. Empty is quiet: nothing is wrong.
 */
.c-notice--locked .c-notice__heading { color: var(--warning); }
.c-notice--empty .c-notice__heading { color: var(--ink-2); }

/*
 * Error is red, and unlike the other two it tints the surface as well as the heading.
 * A fault should not read like an unused screen, which is what gap G7 recorded: an
 * error page rendered as "empty" gets the tone of a list nobody has filled in.
 * Colour is never the only signal here: the heading says what happened in words.
 */
.c-notice--error .c-notice__heading { color: var(--danger); }

.c-notice--error {
  border-radius: var(--radius-md);
  background: var(--danger-subtle);
}

/* The one way out, gap G6. Inside the centred block rather than floating under it. */
.c-notice__action {
  margin-top: var(--space-5);
}

/* ---- notice.gate.css ---- */
/*
 * notice.gate.css. The sampled wall, entry [237]: example rows in flow behind a veil, the
 * wall card over them, a tick line inside the card, and the Example strip under the block.
 * Every rule here is reached by `sample`, `gain` or `foot` on shared/views/components/
 * notice.php. notice.preview.css keeps the empty furniture from [175] as it was.
 *
 * `.gate` after the dot so the build emits it after notice.css and notice.preview.css.
 *
 * ## The floor and the veil share one grid cell, and the block is as tall as the taller
 *
 * The [175] preview is absolutely positioned inside a fixed height block, because furniture
 * has no height of its own. A sample is real markup with a real height (a four row table, a
 * three row settings list), so it lays out in flow, and the veil sits over it: a gradient from
 * nearly clear at the top to nearly opaque at the bottom, so the sample's head shows through
 * and its rows fade under the card, which is the approved file's own picture.
 *
 * Both are `grid-area: 1 / 1` rather than the veil being absolute. The first version made the
 * veil absolute and sized the block from the sample alone, and the alerts screen, whose whole
 * Pro format is two settings rows, measured a 238px block under a 286px card with the Upgrade
 * button 200px below the fold (found by the screen's agent at 1280, worse at 360). In one grid
 * cell the block is as tall as whichever of the two is taller, so a short sample gets a taller
 * floor and the card is never clipped.
 *
 * The selector is doubled, `.c-notice--walled.c-notice--sampled`, because notice.preview.css
 * sets display, padding and border on `.c-notice--walled` and the build emits this file first
 * (g before p); at equal specificity the later rule won and the sample shrank to its content
 * width, centred, which the health screen's agent saw. Two classes outrank one.
 */
.c-notice--walled.c-notice--sampled {
  display: grid;
  align-items: stretch;
  padding: 0;
  border: 0;
  border-radius: 0;
}

.c-notice--sampled > .c-notice__sample,
.c-notice--sampled > .c-notice__veil {
  grid-area: 1 / 1;
  min-width: 0;
}

.c-notice__sample {
  filter: blur(3px);
  opacity: 0.62;
  user-select: none;
  pointer-events: none;
  transition: filter var(--duration-base) var(--ease), opacity var(--duration-base) var(--ease);
}

.c-notice--sampled:hover .c-notice__sample {
  filter: blur(2px);
  opacity: 0.72;
}

.c-notice__veil {
  position: relative;
  display: grid;
  place-items: center;
  padding: var(--space-6);
  background: linear-gradient(
    180deg,
    rgb(255 255 255 / 0.35) 0%,
    rgb(255 255 255 / 0.86) 55%,
    rgb(255 255 255 / 0.96) 100%
  );
}

/* The wall inside a veil keeps its own card; the veil brings the centring. */
.c-notice__veil .c-notice__wall {
  max-width: 27rem;
  padding: var(--space-7) var(--space-7);
  text-align: center;
  box-shadow: var(--shadow-lg);
}

.c-notice__veil .c-notice__mark { margin: 0 auto var(--space-4); }

/*
 * The gain line. One thing the plan change gives, with a tick, on the brand tint. It sits
 * between the reason and the button so the eye reads claim, evidence, action.
 */
.c-notice__gain {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  margin: 0 0 var(--space-5);
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  line-height: var(--leading-base);
  color: var(--ink);
  background: var(--action-subtle);
  border: var(--border-width) solid var(--action-border);
  /* The file's 8px, entry [241]; the radii are the file's since [239]. */
  border-radius: var(--radius-sm);
}

.c-notice__gain-tick {
  width: var(--space-5);
  height: var(--space-5);
  flex: none;
  fill: var(--success-ink);
}

/*
 * The Example strip. Rendered whenever a sample is, and the component does not offer a way
 * to leave it out: that is the ruling's mitigation, in the contract. Same ground and rule as
 * a panel's note, so under a panel it reads as the panel's own foot.
 */
.c-notice__foot {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: 0;
  padding: var(--space-4) var(--card-pad);
  background: var(--ground);
  border-top: var(--border-width) solid var(--line-soft);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

.c-notice__foot-text { min-width: 0; }

/* ---- notice.preview.css ---- */
/*
 * notice.preview.css. The wall a plan gate puts over a screen: the shape of what is
 * behind it, out of focus, with the sentence and the upgrade button in focus on top.
 *
 * Entry [175]. An extension file rather than more of notice.css, which is the split
 * nav.aside.css and table.narrow.css already make: notice.css is the sentence block on
 * every empty table in the suite, and this is one state that block can be put into.
 *
 * ## WHAT IS BLURRED HOLDS NOTHING, AND THAT IS THE POINT
 *
 * Every element this file styles is an empty <span>. The component that emits them has
 * no slot that could carry a value and the caller never reads the repository, so there
 * is no data in the response to hide. A `filter: blur()` over real rows is a paywall the
 * element inspector defeats in four seconds; this is furniture, drawn so a customer can
 * see the SHAPE of the screen they would be buying.
 *
 * `user-select: none` and `pointer-events: none` are not the security. They stop the
 * placeholder behaving like content, which is a different and much smaller claim.
 *
 * ## THIS IS THE ONE NOTICE THAT DRAWS ITS OWN SURFACE
 *
 * notice.css says it draws no border and no background, because a panel body or a table
 * cell is always around it and a second frame inside the first is what makes an empty
 * table look broken. Nothing is around this one: the locked page renders it straight into
 * the stage. So it paints the panel it is standing in for, which is also what lets the
 * shapes read as a screen rather than as marks on the page background.
 *
 * DERIVED, not read from the template: the template has no wall. Every measurement is a
 * token except two, each stated as what it is. The blur radius is a judgement about
 * legibility, not a spacing step. The 58ch reading width is notice.css's own, for the
 * reason recorded there: ch is tied to the font rather than to a pixel count.
 */

/*
 * The vertical padding is what gives the block its height, so no min-height and no magic
 * number: the card sets the height, --space-11 either side sets the room around it, and
 * the placeholder fills whatever that comes to.
 */
.c-notice--walled {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: var(--space-11) var(--space-7);
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
}

/*
 * Absolute rather than a background image, because the shapes have to keep the
 * proportions of a real screen and a gradient cannot: the point of the blur is that the
 * FORM survives it.
 */
.c-notice__preview {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  padding: var(--space-7);
  filter: blur(6px);
  user-select: none;
  pointer-events: none;
}

.c-notice__shape {
  display: flex;
  gap: var(--space-5);
}

.c-notice__shape--tiles > .c-notice__tile,
.c-notice__shape--chart > .c-notice__col {
  flex: 1 1 0;
}

/*
 * The rows take whatever height is left and spread through it, which is what makes the
 * block read as a table rather than as a stack of five things at the top of a big empty
 * card. A shape above them keeps its own height and this absorbs the rest.
 */
.c-notice__shape--rows {
  flex: 1 1 auto;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-4);
}

.c-notice__shape--chart {
  align-items: flex-end;
  flex: 0 0 auto;
  height: var(--plot-h-sm);
}

.c-notice__tile,
.c-notice__bar,
.c-notice__col {
  display: block;
  flex: 0 0 auto;
  background: var(--line);
  border-radius: var(--radius-md);
}

.c-notice__tile { height: var(--space-11); }
.c-notice__col  { background: var(--muted-dim); }
.c-notice__bar  { height: var(--space-6); }

/* The head bar reads as a header: darker, and it does not run the full width. */
.c-notice__bar:first-child {
  width: 38%;
  background: var(--muted-dim);
}

/*
 * The rows taper rather than all sitting at one width, which is what stops the block
 * reading as a loading skeleton. nth-child rather than a modifier class per width: the
 * markup is furniture and should carry no vocabulary at all.
 */
.c-notice__bar:nth-child(3) { width: 94%; }
.c-notice__bar:nth-child(5) { width: 86%; }
.c-notice__bar:nth-child(7) { width: 72%; }

.c-notice__col:nth-child(odd)  { height: 62%; }
.c-notice__col:nth-child(3n)   { height: 90%; }
.c-notice__col:nth-child(3n+2) { height: 44%; }

/*
 * The card in focus. It carries its own ground and a shadow, so the sentence never sits
 * on a shape edge: that join is the one place the contrast could fail.
 */
.c-notice__wall {
  position: relative;
  width: 100%;
  /* The approved file's card, entry [241]: 430px wide, 24px in, 12px corners. */
  max-width: 27rem;
  padding: var(--space-7);
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  animation: c-notice-lift var(--duration-base) var(--ease) both;
}

/*
 * The heading is INK here, not the amber notice.css gives a role gate. Amber is the
 * colour of something that went wrong, and nothing has: this is a screen on a plan the
 * customer has not bought. The lock glyph and the sentence carry the fact, and the one
 * saturated thing on the block is the button, which is where the person should look.
 */
.c-notice--walled .c-notice__heading { color: var(--ink); }

.c-notice__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-face);
  height: var(--icon-face);
  margin-bottom: var(--space-5);
  color: var(--brand);
  background: var(--brand-subtle);
  border-radius: var(--radius-full);
}

.c-notice__mark svg { width: var(--space-6); height: var(--space-6); }

@keyframes c-notice-lift {
  from { opacity: 0; transform: translateY(var(--space-4)); }
  to   { opacity: 1; transform: none; }
}

/*
 * At a phone width the shapes have no room to say much and the card is the screen. The
 * placeholder stays, tighter, so the page is not one sentence on an empty ground.
 */
@media (max-width: 640px) {
  .c-notice--walled { padding: var(--space-8) var(--space-4); }
  .c-notice__preview { padding: var(--space-4); gap: var(--space-4); }
  .c-notice__shape--chart { height: var(--space-11); }
  .c-notice__wall { padding: var(--space-6) var(--space-5); }
}

/* ---- pagination.css ---- */
/*
 * pagination.css. Server rendered page links, sitting below a table.
 *
 * DERIVED, not read from the template: the template ships no pagination component.
 * A page link is a small button, so it takes .btn.btn-sm (app.css 8776): 32px tall,
 * 0.5rem by 0.75rem, 0.75rem type on a 1rem line, --radius-md. --leading-tight
 * resolves to that 1rem line box at this size.
 *
 * The visual chip stays small, but every link is still a 44px target: --touch-min is
 * laid over each one by a pseudo element, so the hit area meets house rule 10 without
 * the row growing to twice its height. The list gap is set so two neighbouring hit
 * areas barely meet.
 *
 * Figures are monospace because a person compares them, and the current page is
 * marked by a filled chip as well as by aria-current, so colour is never the only
 * signal that says where you are.
 */

.c-pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-5);
}

/*
 * --space-4 and not --space-3, entry [064]. The header above says the gap is set so two
 * neighbouring hit areas barely meet, and at 8px that stopped being true for the numbered
 * links: a page chip is 33px wide, the --touch-min overlay is 44, so it extends 5.5px each
 * side and two of them overlapped by 3px. elementFromPoint at the edge of "Page 1" returned
 * "Page 2", which is the failure mode a hit area is supposed to prevent rather than cause.
 * 12px makes the pitch 45px against a 44px overlay, so they meet and do not cross.
 */
.c-pagination__list {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.c-pagination__item { display: flex; }

.c-pagination__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--control-h-sm);
  padding: var(--space-3) var(--space-4);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--ink-2);
  transition: background var(--duration-fast) var(--ease), color var(--duration-fast) var(--ease);
}

/* The 44px hit area. Laid over the chip, so it costs the row no height. */
.c-pagination__link::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100%;
  min-width: var(--touch-min);
  height: var(--touch-min);
  transform: translate(-50%, -50%);
}

.c-pagination__link:hover {
  background: var(--surface-2);
  color: var(--ink);
  text-decoration: none;
}

.c-pagination__link--step {
  font-family: var(--font-sans);
  font-weight: var(--weight-semi);
  letter-spacing: normal;
}

.c-pagination__link--here {
  background: var(--brand);
  border-color: var(--brand-border);
  color: var(--brand-ink);
  font-weight: var(--weight-semi);
}

.c-pagination__link--here:hover {
  background: var(--brand-hover);
  color: var(--brand-ink);
}

.c-pagination__link[disabled] {
  background: none;
  border-color: var(--line-soft);
  color: var(--ink-3);
  cursor: not-allowed;
}

.c-pagination__link[disabled]:hover {
  background: none;
  color: var(--ink-3);
}

.c-pagination__gap {
  display: block;
  padding: var(--space-3) var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--ink-3);
}

.c-pagination__count {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

@media (max-width: 640px) {
  .c-pagination { justify-content: flex-start; }
}

/* ---- panel.css ---- */
/*
 * panel.css. The shell every screen is built out of. design.md section 5.
 *
 * Geometry is the template's .card (app.css 11252): radius 0.375rem, white,
 * shadow-base, with a body that pads at --card-pad, which is card-body p-6, the shape
 * used 194 times across the 74 pages. Title is .card-title (app.css 7879), footnote is
 * .card-text (app.css 7899). See docs/component-specs.md.
 *
 * Two variants, both props: plain is the card with a titled header, record is the full
 * width identity header on app-detail and site-detail. Nothing outside .c-panel is
 * styled here.
 */

.c-panel {
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  min-width: 0;
}

/*
 * A panel that is a grid item ENDS AT ITS OWN CONTENT. Entry [243], reversing [048].
 *
 * [048] stretched a panel in an l-grid to the tallest in its row and let the body take the
 * slack, so every card's footnote sat on the row's floor: "the note strip sat under the body
 * and left the remainder blank below it, which reads as an unfinished card". The approved
 * Sitemapify file draws the other reading, a card that ends where its note ends beside a
 * taller one, and the owner ruled the file's (the fourth diff list's C9). `align-self: start`
 * is the whole change: a grid still stretches its items by default, and this opts the panel
 * out. The suite's hub rows take the same shape, all thirteen apps being fixture only.
 */
.l-grid > .c-panel { align-self: start; }

/* The same reading in a split. Entry [272]: wave 3 of [271] found the wide pane's panel stretched to
   the side column's height on ten apps (askbot answer box, alttextai scan, schemaai overview), and
   seven views had wrapped each pane in an l-grid whose only job was to reach the rule above. */
.l-split > .c-panel,
.l-split > .c-notice { align-self: start; }

.c-panel__head {
  display: flex;
  align-items: center;
  /* Wraps, as the approved file's own `.panel-head` does (`flex-wrap: wrap`, checked in entry
     [243] after an hour as nowrap: at 360 the file puts the tool under the title too). A pill
     under an 18px title in a narrow column is the size rulings meeting, not the wrap. */
  flex-wrap: wrap;
  gap: var(--space-5);
  padding: var(--card-pad);
  border-bottom: var(--border-width) solid var(--line-soft);
  background: var(--surface);
}

/*
 * .card-title. 1.125rem, weight 500, no text-transform (entry [047]). --leading-tight
 * resolves to the
 * template's 24px line box at this size, which is why no literal is needed here.
 */
.c-panel__title {
  font-family: var(--font-sans);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: -0.02em;
  color: var(--ink);
  flex: 0 1 auto;
  min-width: 0;
}

.c-panel__tools {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.c-panel__body {
  padding: var(--card-pad);
  min-width: 0;
}

/* A table pads its own cells, so it sits flush against the panel edge. */
.c-panel__body--flush { padding: 0; }

/*
 * The rule under the header, then 1.25rem before the body starts. The :not() keeps a
 * flush body flush, since this selector would otherwise outrank the modifier.
 */
.c-panel__head + .c-panel__body:not(.c-panel__body--flush) {
  padding-top: var(--space-6);
}

/*
 * The intro, entry [130]. The note's type in the place the template puts an explanation:
 * settings.html:2196 sets it as `text-slate-600 text-sm` INSIDE the card body, between
 * the title and the control, with no rule and no fill. So: the note's type, none of the
 * note's strip treatment. No padding-bottom, because the body opens with its own.
 */
.c-panel__intro {
  padding: var(--space-6) var(--card-pad) 0;
  color: var(--ink-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
}

/* An intro opens the body, so the body stops adding a second top offset under it. */
.c-panel__intro + .c-panel__body:not(.c-panel__body--flush) { padding-top: var(--space-5); }

/* The footnote strip. Twenty two screens carry one. Type is .card-text. */
.c-panel__note {
  padding: var(--space-6) var(--card-pad);
  border-top: var(--border-width) solid var(--line-soft);
  background: var(--surface-2);
  /* The approved file's faintest ink, ruled in [243] and blocked by the floor until the token
     moved in [244]: --ink-3 measures 4.94:1 on this ground now, 2.45 before. */
  color: var(--ink-3);
  font-size: var(--text-sm);
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
}

/* record. Identity on the left, actions on the right, no header rule. */
.c-panel--record .c-panel__head {
  align-items: flex-start;
  gap: var(--space-6);
  padding: var(--card-pad);
  background: var(--surface);
  border-bottom: 0;
}

.c-panel--record .c-panel__identity {
  flex: 1 1 auto;
  min-width: 0;
}

/* The template's .card-title at its medium step: 1.25rem, still weight 500. */
.c-panel--record .c-panel__title {
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
  color: var(--ink);
}

.c-panel--record .c-panel__tools { align-items: center; }

/* The identity line under the record name: ids a person copies, so monospace. */
.c-panel__sub {
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-3);
  overflow-wrap: anywhere;
}

/*
 * The action row, page-templates.md gap G19. Below the body, above the note, right
 * aligned with the primary action last, and it wraps rather than overflowing on a phone.
 * It carries the same side padding as the body so its right edge lines up with the
 * content above it rather than with the card border.
 */
.c-panel__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: 0 var(--space-6) var(--space-6);
}

/* A form inside the row must not stretch: each one is a single control. */
.c-panel__actions > form {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: flex-end;
  margin: 0;
}

/* Below the small breakpoint the card keeps its rhythm but gives back side padding. */
@media (max-width: 640px) {
  .c-panel__actions {
    justify-content: flex-start;
  }

  .c-panel__head,
  .c-panel__actions,
  .c-panel__note,
  .c-panel__body:not(.c-panel__body--flush) {
    padding-left: var(--space-5);
    padding-right: var(--space-5);
  }
}

/* ---- panel.head.css ---- */
/*
 * panel.head.css. The two head props entry [237] added: the toned icon disc before the
 * title and the count badge after it. Its own file because panel.css sits at 196 lines
 * against the 200 cap, and the head row is the seam: everything here is reached by `icon`,
 * `icon_tone` or `badge` on shared/views/components/panel.php and nothing else.
 *
 * `.head` after the dot so the build emits it after panel.css (table.narrow.css records
 * why a hyphen would sort it first).
 */

/*
 * The disc. --icon-face is the stat tile's disc and the approved file draws the two at the
 * same size, 32px, with a rounded square rather than a circle so it reads as a heading
 * mark and not as the tile's figure. Tinted with the semantic subtle and inked with the
 * semantic solid; brand is the default because a heading is not a status.
 */
.c-panel__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--icon-face);
  height: var(--icon-face);
  /* The file's box is 9px on 32px, a rounded square and not a disc; 8 is the step. [241] */
  border-radius: var(--radius-sm);
  background: var(--action-subtle);
  color: var(--action-text);
}

.c-panel__icon svg {
  width: var(--space-6);
  height: var(--space-6);
  fill: currentColor;
}

.c-panel__icon--success {
  background: var(--success-subtle);
  color: var(--success-ink);
}

.c-panel__icon--warning {
  background: var(--warning-subtle);
  color: var(--warn-ink);
}

.c-panel__icon--danger {
  background: var(--danger-subtle);
  color: var(--danger-ink);
}

/* The badge sits against the title, and tools keep their margin-left: auto. */
.c-panel__badge {
  display: inline-flex;
  align-items: center;
}

/*
 * The lock beside a paid section's title, entry [291] (`locked` on panel.php): small, the title's
 * own ink at rest weight, so it reads as a property of the title and not as a second heading.
 */
.c-panel__lock {
  display: inline-flex;
  align-items: center;
  flex: none;
  margin-left: calc(var(--space-3) * -1);
  color: var(--ink-3);
}

.c-panel__lock svg {
  width: var(--space-5);
  height: var(--space-5);
  fill: currentColor;
}

/* A panel reached by its anchor (`id` on panel.php, entry [293]) lands with its head below the
   sticky top bar rather than under it. */
.c-panel[id] { scroll-margin-top: calc(var(--bar-height) + var(--space-5)); }

/* ---- plan-tiers.css ---- */
/*
 * plan-tiers. The row of plan cards, the approved Sitemapify file's card, entry [241].
 *
 * Until [241] the row was three `panel`s holding a `kv` and `bar-row`s: the hub's plan card,
 * designed for a different job. The owner's ruling rebuilt it to the file's: a ribbon over the
 * top edge, the name, a who line, the price as a figure, a mono bill line, spec rows with mono
 * bold values, "+" bullets that wrap, a block button. Every size is the file's snapped to the
 * scale [241 ruling 14]: 19px name to 18, 12.5px lines to 12, 34px price as --text-5xl, 10px
 * ribbon to 12, 18px gap to 20, 22px side padding to 24. Shadows are the suite's [ruling 13].
 *
 * The file's row is one column under 1180 rather than two, so three cards never leave one
 * orphaned under a pair; this grid is its own rather than l-grid--3 for that reason.
 */
.c-plan-tiers {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-6);
  align-items: start;
}

/* Botgate sells two tiers; a three column grid holding two cards reads as a missing plan. */
.c-plan-tiers--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

@media (max-width: 1180px) {
  .c-plan-tiers,
  .c-plan-tiers--2 { grid-template-columns: minmax(0, 1fr); }
}

/* Inside the sign in card the container is 440px whatever the viewport is (entry [272], from wave 3
   of [271]): three 130px cards overflowed their own edges on every three tier app's signed out
   /plans at 1280 and 2560. One column there at every width, until the grid can read its container
   rather than the viewport. */
.c-login .c-plan-tiers,
.c-login .c-plan-tiers--2 { grid-template-columns: minmax(0, 1fr); }

.c-plan-card {
  position: relative;
  padding: var(--space-7);
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

/* The featured card's border is the action blue at 2px, as the file draws it. The extra pixel
   comes off the padding so the card's text lines up with its neighbours'. */
.c-plan-card--featured {
  padding: calc(var(--space-7) - var(--border-width));
  border-width: calc(var(--border-width) * 2);
  border-color: var(--action);
  box-shadow: var(--shadow-md);
}

/* Over the top edge, left aligned with the card's text. The featured card's words come from
   the manifest; the current card's are "Your plan" in green. The words on the action fill are
   --action-ink (white on the suite blue 5.17:1, the navy on Sitemapify's orange 6.22:1, entry
   [243]); white on --success-ink measures 5.02. */
.c-plan-card__ribbon {
  position: absolute;
  top: calc(-1 * var(--space-4));
  left: var(--space-7);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--action);
  color: var(--action-ink);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  line-height: var(--leading-tight);
  white-space: nowrap;
}

.c-plan-card__ribbon--current { background: var(--success-ink); color: var(--brand-ink); }

.c-plan-card__name {
  margin: 0;
  font-family: var(--font-cond);
  font-size: var(--text-lg);
  font-weight: var(--weight-heavy);
  letter-spacing: -0.02em;
  line-height: var(--leading-tight);
  color: var(--ink);
}

/* Two lines tall whether it takes one or two, so the prices across the row sit level. */
.c-plan-card__who {
  margin: var(--space-1) 0 0;
  min-height: calc(2 * var(--text-xs) * var(--leading-loose));
  font-size: var(--text-xs);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

.c-plan-card__price {
  margin: var(--space-3) 0 0;
  font-size: var(--text-5xl);
  font-weight: var(--weight-heavy);
  letter-spacing: -0.02em;
  line-height: var(--leading-tight);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* The figure never breaks from its currency sign. */
.c-plan-card__figure { white-space: nowrap; }

.c-plan-card__unit {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  letter-spacing: 0;
  color: var(--ink-2);
}

/* Kept one line tall when empty (a free tier) so the spec rule below lands at the same height. */
.c-plan-card__bill {
  margin: var(--space-1) 0 0;
  min-height: calc(var(--text-xs) * var(--leading-base));
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-3); /* the file's faint ink; the token clears 4.5 since [244] */
}

.c-plan-card__specs,
.c-plan-card__features {
  margin: var(--space-5) 0 0;
  padding: var(--space-4) 0 0;
  border-top: var(--border-width) solid var(--line-soft);
}

.c-plan-card__spec {
  display: flex;
  justify-content: space-between;
  /* Baseline, not center: the mono value's line box is shorter than the sans label's, and
     centred it floats a pixel or two above the label. */
  align-items: baseline;
  gap: var(--space-4);
  padding: var(--space-2) 0;
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink);
}

.c-plan-card__spec dt { margin: 0; color: var(--ink-2); }

.c-plan-card__spec dd {
  margin: 0;
  font-family: var(--font-mono);
  font-weight: var(--weight-semi);
  text-align: right;
}

.c-plan-card__features { list-style: none; }

.c-plan-card__feature {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  font-size: var(--text-xs);
  line-height: var(--leading-loose);
  color: var(--ink);
}

.c-plan-card__mark {
  flex: 0 0 auto;
  color: var(--success-ink);
  font-weight: var(--weight-heavy);
}

/* The words a tier has not, in the faintest ink; the token clears 4.5 since [244]. */
.c-plan-card__feature--off { color: var(--ink-3); }
.c-plan-card__feature--off .c-plan-card__mark { color: var(--line); }

.c-plan-card__action { margin-top: var(--space-5); }

/* ---- progress.css ---- */
/*
 * progress.css. A long running job's readout. Entry [167].
 *
 * Two columns: the derived percentage on the left at display size, everything that changes
 * in words on the right. It collapses to one column at --bp-sm, where a 2.5rem figure beside
 * a wrapping sentence leaves the sentence about eleven characters wide.
 *
 * ## Every value traces to a token, and the mockup's own numbers do not appear
 *
 * docs/appslist/sitemapify/sitemapify-first-scan.html draws this as a 104px conic-gradient
 * ring with a 78px hole, a 21px figure inside it, and a 7px track in #2563EB on #DBEAFE.
 * NONE of that crossed over, house rule 9: those are the mockup's own palette and its own
 * measurements, and `tokens.css` is what the suite is built from.
 *
 * The ring was cut here and is BACK AS A MODE, entry [176], and the two positions do not
 * contradict each other. What was cut was the mockup's arrangement: a ring AND a track under
 * it, one number in two shapes, free to disagree the moment somebody edits one of them. What
 * exists now is `meter` in shape => ring, which REPLACES the track rather than sitting above
 * it, reads the same `--c-meter-fill`, and is drawn by the same component. Still one number,
 * still one element, and none of the mockup's own measurements or colours came with it.
 *
 *   figure     --text-display / --leading-display, the 2.5rem step tokens.css already
 *              carries for a headline figure
 *   label      the same --text-sm, weight 500, --ink-2 that .c-meter__label and
 *              .c-stat__label both use, so the three read as one family
 *   count      monospace and tabular, the project's rule for a figure a person watches
 *              change (state-of-play section 3)
 *   note       --text-xs and --ink-2, the same footnote weight .c-stat__delta uses
 *
 * ## The note is monospace and that is deliberate
 *
 * It carries a path and a wall clock, "Last checked /blog at 08:34:11", and both are strings
 * a person compares character by character against what they know their site to be. It is
 * also the one line that must be seen to CHANGE while the counter above it does not, which
 * is the whole reason this component exists, and tabular figures are what make a changing
 * timestamp read as movement rather than as reflow.
 */

.c-progress {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--space-6);
  min-width: 0;
}

/*
 * min-width holds the column still while the number inside it goes from 5% to 100%. Without
 * it the whole right hand side steps left by a character on the way past ten per cent, which
 * on a screen that repaints every three seconds is a page that will not sit still.
 */
.c-progress__figure {
  margin: var(--space-0);
  min-width: var(--space-11);
  font-family: var(--font-mono);
  font-size: var(--text-display);
  font-weight: var(--weight-bold);
  line-height: var(--leading-display);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-wide);
  color: var(--ink);
}

.c-progress__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

.c-progress__lamp { display: block; }

.c-progress__label {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

.c-progress__count {
  font-family: var(--font-mono);
  font-size: var(--text-md);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.c-progress__note {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  overflow-wrap: anywhere;
}

/* An empty note takes no space. The element still exists so a poll can fill it. */
.c-progress__note:empty,
.c-progress__count:empty { display: none; }

/*
 * The tone reaches the figure as well as the track. A failed scan whose big number is still
 * in --ink says the same thing a running one does, and the number is the part a person reads
 * first. It never reaches the label or the note: those are the signals that do not depend on
 * colour, which is design.md section 7.
 */
.c-progress--success .c-progress__figure { color: var(--success); }
.c-progress--warning .c-progress__figure { color: var(--warning); }
.c-progress--danger  .c-progress__figure { color: var(--danger); }
.c-progress--info    .c-progress__figure { color: var(--info-dim); }
.c-progress--ai      .c-progress__figure { color: var(--ai); }
.c-progress--mute    .c-progress__figure { color: var(--ink-3); }
.c-progress--brand   .c-progress__figure { color: var(--brand); }

/*
 * Ring mode. Entry [176], and the paragraph at the top of this file is the argument it had
 * to answer.
 *
 * The objection there was to a ring AND a track: one number in two shapes, free to disagree
 * the first time somebody edits one of them. Ring mode has no track. `meter` in shape => ring
 * is the same element reading the same custom property, and the figure moves inside the hole,
 * so what was two renderings is one.
 *
 * The figure is centred by stacking, not by measuring: both children of the dial occupy
 * grid-area 1/1, so the percentage sits over the middle of the ring at every ring size and
 * nothing has to know what that size is.
 */
.c-progress--ring .c-progress__dial {
  display: grid;
  place-items: center;
  min-width: 0;
}

.c-progress--ring .c-progress__dial > * { grid-area: 1 / 1; }

/*
 * position: relative, and it is load bearing rather than tidy. `meter` draws the ring's hole
 * as an ABSOLUTELY positioned element inside the track, and a positioned element paints above
 * a static one whatever the DOM order. Without this the hole covered the figure and the ring
 * rendered with nothing in the middle of it, which is what the first screenshot showed.
 */
.c-progress--ring .c-progress__figure { position: relative; }

/*
 * Smaller inside the ring than above a track. The ring is 6.5rem across with a hole about
 * 4rem wide, and --text-display at 2.5rem leaves "100%" wider than the hole it sits in.
 */
.c-progress--ring .c-progress__figure {
  min-width: 0;
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
}

/* House rule 10. One column, the figure above the words rather than beside them. */
@media (max-width: 640px) {
  .c-progress {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-4);
  }
}

/* ---- public.css ---- */
/*
 * public.css. The public page frame: the marketing site, the scan form and the report.
 *
 * A PAGE FRAME AND NOT A COMPONENT. There is no shared/views/components/public.php, nothing
 * calls component('public'), it takes no props and it is not on the styleguide. That is
 * exactly what login.css and auth-panel.css are, and docs/design.md 5.1 records the pattern:
 * such a file lives in this folder only because build-library-css.sh concatenates the folder,
 * and it keeps the c- prefix so the class discipline still holds. The markup is
 * shared/views/layouts/public.php.
 *
 * ## --font-display is declared HERE, and that is the point of it being here
 *
 * tokens.css is identical in every app in this suite, by design, and it is the one place a
 * token may be defined. A marketing serif is not a suite token: thirteen control panels have
 * no use for one, and putting it in tokens.css would ship the declaration to every screen in
 * the project so that one site could use it. So it is declared on .c-public, scoped to the
 * frame that has a use for it, in the same way meter.css declares --c-meter-ring: a value no
 * other component has an opinion about belongs to the thing that has the opinion.
 *
 * Newsreader is vendored as two static faces, weights 500 and 600, in vendor/fonts.css.
 * font-src is 'self' with no external origins, so there is no other way to serve it, and the
 * fallback stack is a real one rather than a bare `serif`: Georgia is on every platform this
 * site is read on and its proportions are near enough that a first paint before the face
 * arrives does not reflow into a different page.
 *
 * ## The width, and why it is a local custom property
 *
 * A marketing page needs a measure, which none of the panel's layouts do: the shell sizes
 * itself against --nav-width and the auth frame against --auth-width, and neither is a reading
 * column. --c-public-measure is this frame's own geometry, the same kind of value
 * --c-meter-ring is, and it is not a spacing step because nothing else has a view on it.
 *
 * The three bands centre their content without an extra wrapper element: padding-inline takes
 * whichever is larger, the gutter or half of what is left over past the measure. That keeps
 * the masthead rule and the footer rule full bleed while the words inside them line up with
 * the page, which is what a wrapper div would otherwise be for.
 */

/*
 * ## THE MEASURES AND THE RHYTHM ARE DECLARED HERE, ONCE, AND NOWHERE ELSE
 *
 * Entry [213]. Every one of these was a number written on whichever section wanted it, and the
 * set of them was the fault rather than any single value. Measured on the built site before the
 * change: the entry column said 46rem, `.c-report` said 52rem, `.c-waiting` said 46rem again and
 * the capture said 34rem, so four screens disagreed about where a column ends. The gap between
 * one section and the next was 48px in `.c-band`, 48 above and below in `.c-cta`, 32 in
 * `.c-split`, 40 on the app hero and 0 between the filter row and the grid on /apps, so the home
 * page ran 96px between the hero and the split and 48 between the split and the groups.
 *
 *   --c-public-measure  the page. The masthead, the content and the footer share this edge.
 *   --c-public-read     one column of prose. A screen that is a document sits on it.
 *   --c-public-form     a capture form. A control metric and not a page one: an email field
 *                       832px wide is a field nobody can aim at.
 *   --c-public-rhythm   the gap between one SECTION and the next. One number, every screen,
 *                       every width.
 *   --c-public-step     the gap INSIDE a section, between the heading block and the block it
 *                       names. Smaller than the rhythm on purpose: the rhythm separates two
 *                       arguments and the step separates a heading from its own body.
 *
 * A section that wants a different gap does not get one. If a screen needs a measure that is
 * not here, it is added here and it changes every screen at once, which is the property the
 * suite's own tokens.css has and the reason this frame copies the shape.
 */

.c-public {
  --font-display: Newsreader, Georgia, 'Times New Roman', serif;
  --c-public-measure: 1180px;
  --c-public-read: 52rem;
  --c-public-form: 34rem;
  --c-public-rhythm: var(--space-10);
  --c-public-step: var(--space-7);
  --c-public-gutter: var(--space-5);

  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: var(--ground);
}

.c-public__bar,
.c-public__main,
.c-public__foot {
  padding-inline: max(var(--c-public-gutter), calc((100% - var(--c-public-measure)) / 2));
}

@media (min-width: 640px) {
  .c-public {
    --c-public-gutter: var(--space-7);
  }
}

/* ---- the page ----------------------------------------------------------------------- */

.c-public__main {
  flex: 1 1 auto;
  min-width: 0;
  padding-block: var(--space-8);
}

@media (min-width: 900px) {
  .c-public__main {
    padding-block: var(--space-10);
  }
}

/* ---- the rhythm, and it is ONE declaration ------------------------------------------ */

/*
 * EVERY DIRECT CHILD OF A FLOW IS A SECTION, AND THE GAP BETWEEN TWO SECTIONS IS THE RHYTHM.
 *
 * That is the whole system and this is the whole of it. A section that wants to sit further
 * from its neighbour does not write a margin: it is either a section or it is part of one, and
 * if it is part of one it goes inside the box that IS the section. `parts/home-split.php` and
 * `apps.php` were both edited into a wrapping section for exactly that reason rather than being
 * given margins of their own.
 *
 * There are two flows on this site. `.c-public__main` is the one on a screen whose sections sit
 * straight in the page, which is the home page, the app grid and an app page; `.c-public__column`
 * is the one on a screen that is a single column, which is the report and the waiting room. They
 * take the same rule because they are the same idea at two depths, and a screen that grows a
 * third flow tomorrow joins this selector rather than inventing a number.
 *
 * Adjacent margins collapse, so a component that carries a bottom margin of its own (`banner` is
 * 20px) contributes nothing extra here: the pair resolves to the larger, which is the rhythm.
 */
.c-public__main > * + *,
.c-public__column > * + * {
  margin-block-start: var(--c-public-rhythm);
}

/*
 * THE COLUMN, and the width is a MODIFIER because the width is the only thing the two kinds of
 * column disagree about. The report and the two settled waiting states are documents and want a
 * reading measure; the scan stage is two panes side by side and wants the page, which is what
 * `.c-waiting--stalled` and the plain column on `report-running.php` say between them.
 *
 * It centres on the page rather than sitting at the gutter. Entry [211] fixed that on the scan
 * screen and again on the report, one at a time, because each carried its own copy of the
 * declaration; there is one copy now and a third screen cannot miss it.
 */
.c-public__column {
  min-width: 0;
}

.c-public__column--read {
  max-width: var(--c-public-read);
  margin-inline: auto;
}

/* ---- the footer --------------------------------------------------------------------- */

/*
 * THE BAND, and nothing inside it. Entry [212]: it was a single flex row holding two spans, and
 * the sections mockup replaced that with a grid of link columns. What belongs to the FRAME is
 * the surface, the rule above it and the gutter that lines it up with the masthead and the page,
 * because those three are what make it the same band on all five screens. See public-foot.css.
 */
.c-public__foot {
  padding-block: var(--space-9) var(--space-7);
  background: var(--surface);
  border-top: var(--border-width) solid var(--line);
  font-size: var(--text-sm);
  color: var(--ink-3);
}

/* ---- public-agent.css ---- */
/*
 * public-agent.css. The entry screen's agent, its console and the ambience around them.
 *
 * Entry [210], task 5.138. Layout and motion from
 * `docs/appslist/common/aispeedforce-agent-scan-v2.html` screen 1, which owns exactly those and
 * copy and states. Every colour here is a token: the mockup's own purple ramp is a prototype's
 * and house rule 9 says colour comes from the template, which is why the dark set in
 * `themes/public.css` was derived at the suite navy's hue instead.
 *
 * A PAGE FRAME'S COMPANION AND NOT A COMPONENT. Nothing calls component('public-agent'). It sits
 * in this folder for the reason `public.css` and `public-report.css` do: the build concatenates
 * the folder, and the `c-` prefix keeps the class discipline.
 */

/* ---- the hero figure --------------------------------------------------------------------- */

/*
 * The stage the mascot stands on. `mascot` draws the figure and its motion and is used unchanged;
 * this is the ring around it and the space it floats in, which is chrome rather than the agent.
 */
.c-entry__stage {
  position: relative;
  display: grid;
  place-items: center;
  width: 11.875rem;
  height: 11.875rem;
  margin: 0 auto var(--space-2);
}

/*
 * The dashed orbit. `mascot` has an --orbiting variant of its own and it is NOT used here: that
 * one spins the figure's own stage, and this is a ring the figure sits inside. Two different
 * things that would fight for the same transform.
 */
.c-entry__orbit {
  position: absolute;
  inset: 0;
  border: var(--border-width) dashed var(--line);
  border-radius: var(--radius-full);
  animation: c-entry-spin 26s linear infinite;
}

.c-entry__orbit::after {
  content: "";
  position: absolute;
  top: -0.25rem;
  left: 50%;
  width: 0.4375rem;
  height: 0.4375rem;
  border-radius: var(--radius-full);
  background: var(--brand-accent);
  box-shadow: 0 0 0.75rem var(--brand-accent);
  transform: translateX(-50%);
}

@keyframes c-entry-spin {
  to { transform: rotate(360deg); }
}

/* ---- public-appdata.css ---- */
/*
 * public-appdata.css. The app page's DECLARED sections. Entry [212], task 5.140.
 *
 * The checks in the free scan, the plans and the others in the same category. None of the three
 * is copy: the checks are `check_registry` rows whose `fix_app_slug` names this app, the plans
 * are `<slug>/manifest.php`, and the related row is `apps.category`. So all three render for all
 * thirteen, and they are why an app with no editorial still has a page worth reading.
 *
 * Split from `public-detail-2.css` for the 200 line cap, on the seam the page has: see the header
 * of `public-appstory.css` for the other half.
 */

/* ---- THE CHECK ROWS MOVED OUT IN ENTRY [214] --------------------------------------------- */

/*
 * `.c-checks` was declared here because this section is where it was first needed. It is one of
 * the five shapes of the app pages' graphic vocabulary now and lives in `public-checks.css`,
 * widened to take a second line under a row and a tile that is a glyph rather than a monogram.
 * `parts/app-checks.php` renders it unchanged.
 */

/* ---- the plans -------------------------------------------------------------------------- */

/*
 * FLEX AND NOT THREE FIXED TRACKS, because the tier count is the app's own. Sitemapify declares
 * three, BotGate declares two, and a `repeat(3, 1fr)` grid drew BotGate's two at a third each
 * with an empty third beside them, which reads as a plan that failed to render. Each card takes
 * an equal share of whatever the row has.
 */
.c-plans {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
}

.c-plan { flex: 1 1 16rem; }

.c-plan {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-7);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.c-plan--pick { border-color: var(--brand); }

/*
 * The flag sits ON the border rather than inside the card, which is what the mockup draws. It is
 * a <p> and not a pseudo element, because "Most sites" is a claim in words and a reader who
 * cannot see the card still needs to hear it.
 */
.c-plan__flag {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: var(--space-6);
  transform: translateY(-50%);
  margin: 0;
  padding: var(--space-1) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--brand);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  color: var(--brand-ink);
}

.c-plan > .c-tag { align-self: start; }

.c-plan__amt {
  margin: var(--space-3) 0 0;
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  line-height: 1;
  color: var(--ink);
}

.c-plan__say {
  flex: 1 1 auto;
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

.c-plan__act { margin: var(--space-5) 0 0; }

/* ---- others in the same category --------------------------------------------------------- */

.c-related {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-5);
}

.c-rel {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  padding: var(--space-6);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--ink);
}

.c-rel:hover,
.c-rel:focus-visible {
  border-color: var(--brand-border);
  text-decoration: none;
}

.c-rel__tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-9);
  height: var(--space-9);
  flex: none;
  border: var(--border-width) solid var(--brand-border);
  border-radius: var(--radius-md);
  background: var(--brand-subtle);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  color: var(--brand);
}

.c-rel__nm { display: grid; min-width: 0; }
.c-rel__name { font-size: var(--text-sm); font-weight: var(--weight-medium); }
.c-rel__cat { font-size: var(--text-xs); color: var(--ink-3); }

@media (max-width: 900px) {
  .c-related { grid-template-columns: 1fr; }

  /*
   * AND THE PLANS STACK RATHER THAN WRAP. `flex: 1 1 16rem` fits two of Sitemapify's three tiers
   * on a 720px row and stretches the third across the whole of the next one, so Agency was drawn
   * twice the width of Free and Pro and read as the recommended tier while Pro carried the flag.
   * Measured at 768. A tier is only comparable against a tier the same size as it.
   */
  .c-plan { flex: 1 1 100%; }
}

/* ---- public-apps.css ---- */
/*
 * public-apps.css. The app grid, the chips and the one app page. Entry [201], task 5.130.
 *
 * ## THE CARD IS SIZED BY THE SENTENCE, and section 7.1 is explicit that it is
 *
 * Ruling 3: the card renders `apps.tagline` verbatim, and those are sentences of 60 to 82
 * characters where the mockup's own labels were 30 to 40. So the mockup's `minmax(250px, 1fr)`
 * becomes a wider minimum and the description gets its own line box.
 *
 * MEASURED IN THE BROWSER AND NOT GUESSED, which is what section 7.1 asks for. At 250px the
 * longest tagline in the registry, LLMify's at 82 characters, took six lines and left the two
 * shortest cards half empty. The grid column minimum here is 17rem, which is 272px, and it is
 * expressed in rem so it tracks the root size rather than pinning to a device pixel.
 *
 * `l-grid--3` supplies the three column shape and its own breakpoints; what this adds is the
 * minimum, so a column can never be narrower than the sentence needs. The two rules do not
 * fight: l-grid states the count and the auto-fit here states the floor.
 */

.c-app-grid {
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
}

/*
 * AND IT STOPS AT THREE. `auto-fit` states a floor and no ceiling, so the comment above about
 * `l-grid--3` supplying the count was describing an intention the browser never carried out:
 * measured at 1440 the grid drew FOUR columns of 280px, which is under the 272px floor the
 * paragraph above was written to defend and a narrower card than 768 gets. The floor still does
 * the work everywhere below 1181, where it draws three at 1024 and two at 768.
 */
@media (min-width: 1181px) {
  .c-app-grid {
    grid-template-columns: repeat(3, minmax(17rem, 1fr));
  }
}

/*
 * The cell exists so a hidden card leaves no gap. `hidden` is the attribute and not a class,
 * which is what public-filter.js toggles.
 */
.c-app-grid__cell {
  display: flex;
  min-width: 0;
}

/*
 * AND A HIDDEN CELL IS ACTUALLY HIDDEN. Entry [213], found while shooting the filtered grid.
 *
 * The paragraph above used to end "and base.css already carries [hidden] as display:none". It
 * does not, and never has: `display: none` on `[hidden]` is the BROWSER's rule, in the user agent
 * sheet, which any author declaration beats. `display: flex` on this class is an author
 * declaration, so /apps?group=AI%20access rendered all thirteen cards under a chip that says
 * three, both from the route's own `hidden` attributes and from every press of a chip.
 *
 * The attribute selector is what puts it back at author weight rather than an `!important`,
 * because the two rules are the same idea and only need to be in the right order.
 */
.c-app-grid__cell[hidden] {
  display: none;
}

.c-app-card {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
}

/*
 * The card stretches and its actions sit on the floor, so a row of three cards has one line of
 * buttons across it whatever the taglines did to the heights above them.
 */
.c-app-card > .c-panel {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-width: 0;
}

.c-app-card > .c-panel > .c-panel__actions {
  margin-block-start: auto;
}

/* The sentence. Its own line box, per section 7.1, and never truncated. */
.c-app-card__say {
  margin-block: var(--space-4) 0;
  font-size: var(--text-sm);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

/*
 * An app that has not opened yet is not a fault, so it is not dimmed and not tinted. The only
 * difference is the `tag` inside it, which says "Coming soon" in words. Colour is never the only
 * signal, and here it is not a signal at all.
 */
/*
 * NOTHING. Entry [211], on the owner's list: it was `border-style: dashed` and read as an
 * unfinished card rather than an unreleased app. The `tag` inside it already says "Coming soon"
 * in words, which is the whole signal, and the paragraph above is the argument for why that is
 * enough. A dashed edge was a second signal saying something slightly different.
 *
 * The modifier stays on the element because `app-card.php` sets it and something may want it
 * later. It carries no rule.
 */

/* ---- the chips ---------------------------------------------------------------------- */

/*
 * `l-filters` supplies the row. What this adds is that the chips wrap instead of the last one
 * pushing to the end, because l-filters puts `margin-inline-start: auto` on its last child and a
 * chip set has no trailing control.
 */
/*
 * EQUAL PILLS, NOT A TAB BAR. Entry [211], on the owner's list.
 *
 * `l-filters` is a filter BAR: its last child carries `margin-inline-start: auto` so a submit can
 * sit at the far right, and `> :first-child { flex: 1 1 auto }` lets the leading control take the
 * slack. A chip set has neither. The first rule made "All apps" grow to roughly three times the
 * width of every other chip, and the row read as a stretched tab bar with one tab selected.
 *
 * A chip is a pill: it is as wide as its own word and no wider, and every one of them is the same
 * height. Both of `l-filters`'s assumptions are turned off here rather than the row being rebuilt,
 * because everything else that primitive supplies is what a chip row wants.
 */
.c-chips {
  flex-wrap: wrap;
  gap: var(--space-3);
}

/*
 * BOTH SELECTORS MATCH `l-filters`'s OWN. `.c-chips > *` is one class and loses to
 * `.l-filters > :first-child`, which is a class and a pseudo class, so the first chip went on
 * taking the slack and "All apps" stayed three times the width of its neighbours through a
 * whole build and screenshot pass. Written at equal weight it wins on source order, because
 * library.css is linked after layout.css.
 */
.c-chips > * { flex: 0 0 auto; }
.c-chips > :first-child { flex: 0 0 auto; }
.c-chips > :last-child { margin-inline-start: 0; }

/* And the shape says which of the two it is. A `button` is a rounded rectangle, which beside
   five siblings of the same height reads as a tab bar whatever the widths are. The pill radius
   is the whole difference between a row of tabs and a row of chips. */
.c-chips > .c-button {
  border-radius: var(--radius-pill);
}

/* ---- one app ------------------------------------------------------------------------ */

/*
 * AND NOW THERE IS NOTHING LEFT HERE AT ALL. Entry [212] took `c-appintro`, its tile, its body,
 * its state row and its actions out when the app page was rebuilt on the sections mockup, and
 * this heading was left holding the capture alone. Entry [214] moved the capture to
 * `public-notify.css`, on the seam that this file's own title draws: the grid and the chips are
 * /apps, and the capture is the last section of an app PAGE and the only block on this mount
 * that takes a POST. The split was forced by the 200 line cap and it landed where the file was
 * already two files.
 *
 * The heading stays because the file is still named for the app page and something belonging to
 * it will come back. It carries no rule, which is what `.c-app-card--soon` above says about
 * itself for the same reason.
 */

/* ---- public-appstory.css ---- */
/*
 * public-appstory.css. The app page's EDITORIAL sections. Entry [212], task 5.140.
 *
 * The six feature cards, the before and after, and the questions. Every one of them renders only
 * when `app-copy.php` has an entry for the slug, which today is Sitemapify and nothing else: the
 * owner's instruction for this build was to use that page as the shape and stop on the other
 * twelve rather than writing copy for them.
 *
 * ## Why the split runs here and not somewhere tidier
 *
 * `public-detail-2.css` held all six sections below the hero and went over the 200 line cap. The
 * seam is the one the page itself has: these three appear when somebody has written words, and
 * the three in `public-appdata.css` appear because something in the estate declares them. A file
 * that goes empty for twelve of thirteen apps and one that never does are different files.
 */

/* ---- the six feature cards ------------------------------------------------------------- */

.c-feat {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-5);
}

.c-fcard {
  padding: var(--space-7);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.c-fcard__ic {
  display: grid;
  place-items: center;
  width: var(--space-9);
  height: var(--space-9);
  margin-block-end: var(--space-5);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  color: var(--brand);
}

.c-fcard__ic svg { fill: none; stroke: currentColor; stroke-width: 1.6; }

.c-public .c-fcard__head { margin: 0; font-size: var(--text-md); }

.c-fcard__body {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

/* ---- THE BEFORE AND AFTER MOVED OUT IN ENTRY [214] ------------------------------------- */

/*
 * `.c-ba` was declared here because this section is where it was first needed. It is one of the
 * five shapes of the app pages' graphic vocabulary now and lives in `public-ba.css`, widened to
 * take any two panes rather than only two blocks of code. This section renders it unchanged.
 */

/* ---- the questions ---------------------------------------------------------------------- */

.c-faq {
  border-top: var(--border-width) solid var(--line);
}

.c-faq__item { border-bottom: var(--border-width) solid var(--line); }

/*
 * `list-style: none` AND the WebKit pseudo element, because the two engines draw the marker in
 * two different places and turning off one leaves the other. The plus sign after the text is the
 * affordance, and it flips to a minus when the browser opens the element.
 */
.c-faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  padding-block: var(--space-5);
  list-style: none;
  cursor: pointer;
  font-size: var(--text-md);
  color: var(--ink);
}

.c-faq__q::-webkit-details-marker { display: none; }

.c-faq__q::after {
  content: "+";
  flex: none;
  font-size: var(--text-xl);
  line-height: 1;
  color: var(--brand);
}

.c-faq__item[open] .c-faq__q::after { content: "\2212"; }

.c-faq__a {
  max-width: 70ch;
  margin: 0;
  padding-block-end: var(--space-6);
  font-size: var(--text-sm);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

/*
 * TWO COLUMNS BETWEEN 640 AND 820, which is where a 768 tablet sits and where this went straight
 * from three cards to one. Six feature cards at 720px wide is a stack of banners: measured at
 * 768 the first card ran the full column and its two sentences set to about 100 characters.
 * Three tracks still fit at 1024, so the step down is at 820 and not at 1180.
 *
 * The pair's own step down at this width went to `public-ba.css` with the block, entry [214].
 */
@media (max-width: 900px) {
  .c-feat { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 640px) {
  .c-feat { grid-template-columns: 1fr; }
}

/* ---- public-ba.css ---- */
/*
 * public-ba.css. THE PAIR: two panes and the relation between them. Entry [214], task 5.143.
 *
 * One of the five shapes in the app pages' graphic vocabulary. `parts/fig-ba.php` is the view and
 * carries the contract. `public-fig.css` is the frame this sits in.
 *
 * ## IT IS `.c-ba`, MOVED AND WIDENED, AND NOT A NEW BLOCK
 *
 * Entry [212] wrote it for the app page's before and after, and it drew two `<pre>` blocks with
 * an arrow between them. Reading the thirteen prototypes in entry [214] found TWELVE of the
 * thirteen drawing a two pane comparison, and only about half of them are a before and an after:
 *
 *   a change    Merchant Feed's `Now` and `New` product title, AI SEO Copilot's live meta tag
 *               against its draft, AltTextAI's empty alt against a written one, BotGate's
 *               robots.txt before and after a rule, LLMify's version diff, Redirectify's dead
 *               path against the redirect that answers it.
 *   two sources EntityGraph's site against its Google Business Profile, Indexify's canonical
 *               against Google's canonical, AI Visibility's brand against a competitor's.
 *   two views   SchemaAI's search result today against the one it could earn, AltTextAI's image
 *               as a person sees it against what a screen reader hears, Lookout's clean session
 *               against one that hit a failure.
 *
 * The widening is that a PANE TAKES ANY BODY. It was a `<pre class="c-ba__code">` built inside the
 * view; it is now whatever the caller puts there, which on these pages is usually a `c-reads` and
 * is sometimes a list, a preview or a line of prose. `__code`, `__add` and `__gone` stay for the
 * app page's own diff, which is what they were written for and still the commonest body.
 *
 * The other change is a rename: `--after` became `--mark`, for the reason written beside it.
 */

.c-ba {
  display: grid;
  grid-template-columns: 1fr auto 1fr;

  /*
   * THE PANES ARE THE SAME HEIGHT AND THE ARROW IS CENTRED, which needs two declarations rather
   * than one. `align-items: center` did both and the shorter pane floated in the middle of the
   * taller one, which reads as a pane that failed to fill rather than as a comparison: seen on
   * /apps/botgate, where the file Wix gives you is four lines against the seven beside it.
   *
   * Stretch is the default for the panes; the arrow opts back out. A comparison is two things
   * measured against each other and the eye reads the edges, so the edges have to line up.
   */
  align-items: stretch;
  gap: var(--space-5);
}

.c-ba__pane {
  padding: var(--space-6);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

/*
 * THE MARKED PANE. It was `--after` and the rename is the widening, entry [214]: "after" says the
 * right hand pane is the LATER of two states, which is true of a diff and false of the other two
 * comparisons the thirteen apps draw. LLMify sets a file that exists beside one that does not,
 * EntityGraph sets what a site says beside what Google says, SchemaAI sets what a search result
 * shows today beside what it could show, and none of those is a before and an after. What the
 * modifier actually does is say WHICH PANE THE FIGURE IS ABOUT, so it is named for that.
 */
.c-ba__pane--mark { border-color: var(--brand-border); }

.c-ba__cap {
  margin: 0 0 var(--space-4);
  font-size: var(--text-xs);
  color: var(--ink-3);
}

.c-ba__pane--mark .c-ba__cap { color: var(--brand); }

.c-ba__code {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-loose);
  color: var(--ink-2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.c-ba__add { color: var(--success); }
.c-ba__gone { color: var(--danger); }

.c-ba__arrow {
  align-self: center; color: var(--ink-3); }
.c-ba__arrow svg { fill: none; stroke: currentColor; stroke-width: 1.7; display: block; }


/*
 * ONE COLUMN UNDER 820, and the arrow turns to point down the page, which is where the second
 * pane now is. It travelled here from `public-appstory.css` with the block in entry [214]: a
 * responsive rule left behind in the file its block used to live in is a rule the next reader
 * cannot find from the selector.
 */
@media (max-width: 900px) {
  .c-ba { grid-template-columns: 1fr; }
  .c-ba__arrow {
  align-self: center; transform: rotate(90deg); justify-self: center; }
}

/* ---- public-brand.css ---- */
/*
 * public-brand.css. The masthead: the bar it sits in, the logo, and the links beside it.
 * Entry [213].
 *
 * Split out of `public.css` so the frame and the brand are two files with two owners. The frame
 * owns the measure, the gutter and the vertical rhythm every screen shares; this owns what the
 * top of the page is made of. The footer is `public-foot.css` for the same reason and has been
 * since [212].
 *
 * ## THE MARK IS AN IMAGE AND THE RULES BELOW STOPPED BEING ABOUT TYPE
 *
 * The owner ruled on 2026-09-10 that the uploaded hub logo goes in as an image rather than as an
 * icon beside a wordmark, so `.c-public__mark` is now a box round a picture on every render this
 * mount makes. Its type rules are still here because they still have a job: `parts/masthead.php`
 * falls back to a text wordmark when no mark has ever been pushed, and that state has to look
 * like a masthead too.
 */

/* ---- the masthead ------------------------------------------------------------------- */

/*
 * Not sticky, deliberately, and it is the one place this differs from the panel's .l-bar. The
 * panel's bar carries controls a person reaches for at any scroll depth; this one carries a
 * wordmark and at most a few links, and a public report is a long document that a fixed band
 * would take 64px off the top of on every phone.
 */
.c-public__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3) var(--space-5);
  min-height: var(--bar-height);
  padding-block: var(--space-3);
  background: var(--surface);
  border-bottom: var(--border-width) solid var(--line);
}

/*
 * The mark. inline-flex on --touch-min rather than on the content's own box: it is a link to the
 * home page, it is the first thing a thumb finds, and house rule 10 puts the floor at 44px. The
 * 48px logo clears that on its own and the text fallback does not, so the floor is written once
 * here and covers both.
 *
 * `margin-inline-end: auto` AND NOT the bar's `justify-content`, entry [213]. The bar held two
 * children and space-between put one at each end. `parts/masthead.php` leaves a slot for the
 * theme toggle between the mark and the nav, and with three children space-between would strand
 * the toggle in the middle of the page. Pushing off the mark instead gives the same picture with
 * two children and the right one with three: the mark holds the left edge, everything after it
 * sits together on the right.
 *
 * The display face is for the FALLBACK span only. A serif wordmark over a sans page is what the
 * frame exists to allow, and when the logo is on screen none of it applies to anything.
 */
.c-public__mark {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  margin-inline-end: auto;
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  color: var(--ink);
}

.c-public__mark:hover {
  color: var(--ink);
  text-decoration: none;
}

/*
 * The hub's own logo, pushed into `shared/assets/brand/` by the backfill CLI and named in
 * `_private/brand-marks.php`. Entry [213], and it replaces `.c-public__markicon`, which drew the
 * 32px icon beside a live wordmark until the owner ruled that shape out.
 *
 * 64px, AND IT MAKES THE BAR 80. `--bar-height` is 64 and `padding-block` is --space-3, so 48 is
 * the largest mark that fits inside the bar's own token. 48 was built and shot first and it is
 * the wrong answer: this logo is a SQUARE emblem with the words "AI SpeedForce" on a banner
 * across its lower third, and at 48 that banner is texture. Rendered at 32, 40, 48, 64 and 80
 * over both grounds, 64 is where it becomes words. A masthead whose brand cannot be read is not
 * doing the one thing a masthead does, so the mark wins and the bar grows to 80.
 *
 * IT CAN GROW BECAUSE THE 64 IS A FLOOR AND THIS BAR IS NOT STICKY. `min-height`, not `height`,
 * and the note at the top of this file says why nothing here pins: the extra 16px is paid once
 * at the top of the document rather than on every screenful of a long report.
 *
 * NO RADIUS AND NO PLATE, which is where this differs from the icon rule it replaces. The file is
 * a transparent WebP of an ellipse, a bolt and a robot: there is no square edge to soften and a
 * radius would clip the bolt where it runs out past the ellipse. Measured on both grounds, the
 * artwork carries its own ground and needs neither a plate under it nor a variant.
 */
/*
 * HEIGHT IS THE MEASUREMENT AND WIDTH FOLLOWS THE FILE. The mark is 200 by 160, not square,
 * and a fixed width and height together stretched it to fit a box it was never drawn in.
 * The derivative keeps the original's ratio since the fix in BrandDerivatives, so `auto`
 * here is the width the artwork actually has at 64 tall.
 */
.c-public__logo {
  width: auto;
  height: var(--space-11);
  flex: none;
}

/*
 * The masthead's links. It wraps rather than scrolling: at 360px a wordmark and three links
 * do not fit on one line, and a masthead that scrolls sideways hides the last item behind an
 * edge nothing indicates.
 */
.c-public__nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

/* ---- public-checks.css ---- */
/*
 * public-checks.css. THE ROWS: a labelled list where every row carries a state. Entry [214].
 *
 * One of the five shapes in the app pages' graphic vocabulary, task 5.143.
 * `parts/fig-checks.php` is the view and carries the contract. `public-fig.css` is the frame.
 *
 * ## IT IS `.c-checks`, MOVED AND WIDENED, AND NOT A NEW BLOCK
 *
 * Entry [212] wrote it for the app page's "checks in the free scan". It is the ONE shape all
 * thirteen prototypes draw, without exception, and the thing being listed is different every
 * time: BotGate's twenty nine crawlers in three groups, Merchant Feed's seventeen Google spec
 * attributes, AltTextAI's WCAG criteria, LLMify's six audit results, EntityGraph's nine profile
 * links, SchemaAI's schema types by page type, Indexify's metering axes, Sitemapify's issue list,
 * AskBot's four promises, Lookout's eight gaps in Wix, Redirectify's six things Wix does not
 * have, AI Visibility's engines and AI SEO Copilot's ranked issues.
 *
 * A row is always the same three things: what it is, optionally one line saying more, and where
 * it stands. That is why this is one shape and not thirteen tables.
 *
 * ## THE WIDENING IS TWO THINGS
 *
 * `__say`, a second line under the name, which the app page deliberately does NOT use: its own
 * header says why, and the reason has not changed. Eighteen check explanations are eighteen
 * sentences nobody has written. An app page whose OWN documentation carries that sentence may
 * print it, and eleven of the thirteen do.
 *
 * `__tile--glyph`, so the lead square can hold an icon from `partials/icons.php` instead of a
 * monogram or an uploaded mark. A list of bots or of spec fields has no logo per row.
 */

.c-checks {
  margin: 0;
  padding: 0;
  list-style: none;
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
}

.c-checks__row {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  padding: var(--space-5) var(--space-6);
  border-bottom: var(--border-width) solid var(--line-soft);
}

.c-checks__row:last-child { border-bottom: 0; }

.c-checks__tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-8);
  height: var(--space-8);
  flex: none;
  border-radius: var(--radius-md);
  background: var(--brand-subtle);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  color: var(--brand);
}

.c-checks__tile img { border-radius: var(--radius-sm); }

.c-checks__nm { flex: 1 1 auto; min-width: 0; font-size: var(--text-sm); }

.c-checks__nm > p { margin: 0; }

/*
 * THE GROUP GOES UNDER THE NAME ON A PHONE. Measured at 375 on /apps/botgate: the group heading
 * is longer than the check name it sits beside, `tag` holds a 90px floor and does not wrap, and
 * the pill ran straight across "AI crawler rules set" on the line to its left.
 *
 * A grid rather than a wrapping flex row, because a wrapped tag would take the whole line and a
 * pill stretched to 327px stops reading as a pill. The tile keeps the first line with the name,
 * which is the order the row is read in anyway.
 */
@media (max-width: 640px) {
  .c-checks__row {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-3) var(--space-5);
    align-items: center;
  }

  .c-checks__row > .c-tag {
    grid-column: 2;
    justify-self: start;
  }
}


/*
 * The second line, and it is `--ink-2` rather than `--ink-3`: it is the row's own content and not
 * a caption on it. `c-app-card__say` sets the same relationship on a card for the same reason.
 */
.c-checks__say {
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

/* With a second line the row is no longer one line tall, so the tile and the tag go to the top of
   it rather than to its middle. A 32px square centred against three lines of text floats. */
.c-checks__row--tall {
  align-items: flex-start;
}

/*
 * A GLYPH INSTEAD OF A MONOGRAM. The tile keeps its tint and its size, so a list whose rows have
 * icons and a list whose rows have monograms are the same list at the same rhythm.
 */
.c-checks__tile--glyph svg {
  width: var(--space-6);
  height: var(--space-6);
  display: block;
}

/*
 * INSIDE A FIGURE, THE LIST DROPS ITS OWN EDGE. `c-fig` is already a bordered card and
 * `c-fig__body--flush` exists so the body can reach its edges; a bordered list inside a bordered
 * frame draws two hairlines a pixel apart, which reads as a rendering fault rather than as a
 * nested box. The rows keep their own rules between them, which is what makes it a list.
 *
 * The app page's own check section renders this list OUTSIDE a figure and is unaffected: it is
 * the section's whole body there and the border is what makes it a card.
 */
.c-fig__body--flush > .c-checks {
  border: 0;
  border-radius: 0;
  background: none;
}

/* ---- public-detail.css ---- */
/*
 * public-detail.css. The app page's hero and its fact table. Entry [212], task 5.140.
 *
 * From the sections mockup's detail screen. The rest of that page is `public-detail-2.css`: the
 * before and after, the check list, the plans, the questions and the related row. Two files
 * because one would be over the 200 line cap, and the seam is the hero against everything under
 * it, which is also how the page reads.
 */

/*
 * THE RULE UNDER THE HERO SITS ON THE RHYTHM, above and below. It carried 32px of padding above
 * it, 40 below it and a 40px margin under that, which put 72px between the hero's last line and
 * the first section on the app page and 80px of empty page above the icon at 1024. The top is
 * `.c-public__main`'s own padding now and the bottom is the same number as every other gap on
 * the site, so the rule reads as the end of a section rather than as a band of its own.
 */
.c-dhero {
  display: grid;
  grid-template-columns: 1fr 21rem;
  gap: var(--space-9);
  align-items: start;
  padding-block-end: var(--c-public-rhythm);
  border-bottom: var(--border-width) solid var(--line);
}

.c-dhero__say { min-width: 0; }

.c-dhero__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin-block-end: var(--space-5);
}

/*
 * The icon at detail size. Same frame as the grid's tile and the strip's, one step larger: the
 * tint and the border are what make thirteen uploaded PNGs of thirteen different shapes read as
 * one product family rather than as a row of stickers.
 */
.c-dhero__tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-11);
  height: var(--space-11);
  flex: none;
  border: var(--border-width) solid var(--brand-border);
  border-radius: var(--radius-lg);
  background: var(--brand-subtle);
  font-size: var(--text-lg);
  font-weight: var(--weight-semi);
  letter-spacing: var(--tracking-wide);
  color: var(--brand);
}

.c-dhero__img {
  width: var(--space-10);
  height: var(--space-10);
  border-radius: var(--radius-md);
}

.c-dhero__pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
}

.c-dhero__tag {
  max-width: 46ch;
  margin: var(--space-5) 0 0;
  font-size: var(--text-md);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

.c-dhero .l-actions { margin-block-start: var(--c-public-step); }

/* ---- the fact table ------------------------------------------------------------------- */

/*
 * Rows of a key and a value, in a bordered card. It is deliberately NOT a `<table>`: nothing here
 * is tabular in two dimensions, there is no column to compare down, and a table would announce
 * "column 1 of 2" on every row of what is a list of five facts.
 */
.c-dfacts {
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
}

.c-dfacts__row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-5);
  margin: 0;
  padding: var(--space-4) var(--space-6);
  border-bottom: var(--border-width) solid var(--line-soft);
  font-size: var(--text-sm);
}

.c-dfacts__row:last-child { border-bottom: 0; }

/* The key does not wrap. "Free plan" broke over two lines against a value that fitted on one,
   which put the row's two halves on different baselines. The VALUE is the half that wraps. */
.c-dfacts__k {
  flex: none;
  color: var(--ink-3);
}

.c-dfacts__v {
  text-align: end;
  color: var(--ink-2);
}

/*
 * THE TABLE GOES UNDER THE HERO AND NOT BESIDE IT under 820. A 21rem column beside a heading that
 * has wrapped to three lines is two narrow columns of nothing, and the facts are the second thing
 * a reader wants rather than the first.
 */
@media (max-width: 900px) {
  .c-dhero { grid-template-columns: 1fr; gap: var(--space-7); }
}
/* ---- public-entry.css ---- */
/*
 * public-entry.css. The entry screen's own furniture: the console, the chips, the line the agent
 * says while nothing is happening, and the four figures.
 *
 * Entry [210], task 5.138, from the v2 mockup's screen 1. Split from `public-agent.css` on the
 * seam that file's header implies: the AGENT's chrome is drawn on two screens, the entry and
 * the scan, and everything here is drawn on one.
 *
 * Every colour is a token. The ramp behind them is the v2 mockup's own plum and magenta with
 * its lightness solved, entry [211]: see themes/public.css for what moved and why.
 *
 * The four figures were here until [211] and are now `public-figures.css`, split on the seam
 * the cap forced: they are a row of counted numbers and nothing else on this screen is.
 */

/* ---- the console ------------------------------------------------------------------------- */

/*
 * One row: a fixed scheme, the address, and the button. It is a `field` and a `button` inside a
 * frame rather than a new control, so the input keeps its label, its 44px height and its focus
 * ring from the library.
 */
.c-console {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: 37.5rem;
  margin: var(--c-public-step) auto 0;
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-5);
  background: var(--surface);
  border: var(--border-width) solid var(--surface-3);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}

/*
 * AND IT IS DRAWN AT EVERY WIDTH. It used to be `display: none` under 640 for room, and what
 * that left on a phone was an empty dark box beside a magenta button: the field's label is
 * visually hidden by design and the input carries no placeholder, so `https://` was the only
 * thing saying what the box was for. The console wraps under 640, which is where the room is.
 */
.c-console__scheme {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-md);
  color: var(--brand-accent);
}

.c-console__field {
  flex: 1;
  min-width: 0;
}

/* The label is printed above by the field component and is not wanted inside a console. */
.c-console__field .c-field__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.c-console__field .c-field__input {
  font-family: var(--font-mono);
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}

/* ---- the chips ---------------------------------------------------------------------------- */

.c-filekeys {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: center;
  margin-top: var(--c-public-step);
  padding: 0;
  list-style: none;
}

.c-filekeys__key {
  padding: var(--space-2) var(--space-5);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-3);
  background: var(--surface);
  border: var(--border-width) solid var(--surface-3);
  border-radius: var(--radius-pill);
  transition: color var(--duration-slow) var(--ease),
              border-color var(--duration-slow) var(--ease),
              background var(--duration-slow) var(--ease);
}

/* Lit by public-entry.js, one at a time, on a timer. It names a FILE the scan reads and claims
   nothing about any site, which is what lets it move without measuring anything. */
.c-filekeys__key.is-lit {
  color: var(--brand-accent);
  background: var(--brand-subtle);
  border-color: var(--brand-border);
}

/* ---- what the agent says while nothing is happening ------------------------------------------ */

/*
 * THE HERO KEEPS THE SAME TWO GAPS THE REST OF THE SITE KEEPS. Entry [213]. The console was 32
 * under the lede, the chips 40 under the console's footnote, this 40 under the chips and the
 * figures 48 under it, which is four numbers inside one section on the screen a visitor meets
 * first. They are `--c-public-step` between blocks and `--c-public-rhythm` before the figures,
 * which is the row that closes the hero, and both come off the frame.
 */
.c-saybar {
  display: flex;
  gap: var(--space-4);
  align-items: flex-start;
  justify-content: center;
  max-width: 40rem;
  min-height: 3.5rem;
  margin: var(--c-public-step) auto 0;
}

.c-saybar__quote {
  flex: none;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  line-height: 1;
  color: var(--surface-3);
}

.c-saybar__line {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
  color: var(--ink-2);
  text-align: left;
}

@media (max-width: 640px) {
  /*
   * THE FIGURE SHRINKS WITH THE STAGE OR IT HANGS OUT OF IT. `mascot--lg` is 11rem and this
   * box was 8rem, so on a phone the robot overflowed by 48px in both directions and the top
   * half of it sat across the headline. The stage is chrome and the figure is the component,
   * so the size goes on the component's own variable rather than on a wrapper it does not read.
   */
  .c-entry__stage { width: 9rem; height: 9rem; }
  .c-entry__stage .c-mascot--lg { --c-mascot-size: 7rem; }
  .c-console { flex-wrap: wrap; padding: var(--space-4); }
}

/*
 * ONE COLUMN, AND EVERYTHING SITS IN IT. Entry [211], on the owner's list: the figures row and the
 * button below the fold were not aligned with the centred text, because only the heading and the
 * lede had a measure and everything else was as wide as the frame. The measure is on the COLUMN
 * and its children inherit it, which is one rule instead of one per block.
 *
 * AND THE MEASURE IS THE FRAME'S, entry [213], not 46rem written here. It was one of three
 * numbers for one idea, beside `.c-report`'s 52rem and `.c-waiting`'s 46rem, and the three
 * screens they governed did not line up with each other at any width.
 *
 * The padding went with it. `.c-public__main` sets the space above the first section and the
 * rhythm sets the space below the last one, so 24px on top of the frame's own 48 was the entry
 * screen starting lower than every other screen on the site.
 */
.c-entry {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

/*
 * The stage is the one child that is EXCLUDED rather than given a width back. `public-agent.css`
 * already sizes it, 11.875rem square and 8rem square under 640, and a second declaration here
 * only overrode the width: the phone kept the 8rem HEIGHT and the figure hung out of the top of
 * a 190 by 128 box, straight across the headline. Found at 375 and worth the sentence, because
 * the same shape catches anything else that is sized by its own component.
 */
.c-entry > *:not(.c-entry__stage) {
  width: 100%;
  max-width: var(--c-public-read);
}

/* The heading is the one thing narrower than the column: a display line runs to 22ch and then
   wraps, which is what keeps it a headline rather than a paragraph. */
.c-entry > h1 { max-width: 22ch; }


/* ---- public-fig.css ---- */
/*
 * public-fig.css. THE FRAME EVERY FIGURE SITS IN. Entry [214], task 5.143.
 *
 * ## What this is for
 *
 * Thirteen app detail pages are written by four agents working in parallel. Left alone they grow
 * thirteen illustration styles. This file and the five beside it are the whole graphic vocabulary
 * those pages may draw with, and this one is the part they share: a caption, a body, and an
 * optional line underneath. Nothing else about a figure is negotiable, which is what makes five
 * different shapes read as one family.
 *
 * The five shapes are `c-reads` (public-reads.css), `c-ba` (public-ba.css), `c-checks`
 * (public-checks.css), `c-run` (public-run.css) and `c-net` (public-net.css). Three of the five
 * already existed and were widened rather than replaced; two are new. `parts/fig.php` is the view
 * and its header carries the contract wave 2 reads.
 *
 * ## THE CAPTION IS NOT DECORATION AND IT IS WHY THE FRAME EXISTS
 *
 * A picture on a marketing page with no words on it is asserting something the reader has to
 * guess at, and on this site a guess is exactly what may not be printed: docs/public-scan.md
 * 11.13 rules that a figure nobody measured does not appear. So every figure names what it is
 * showing, in the caller's own words, and `parts/fig.php` refuses to render at all when there is
 * no caption. That refusal is the enforcement.
 *
 * ## NO MARGIN, EVER
 *
 * A figure is the BODY of a section and never a section itself. `public.css` owns the one gap
 * between one section and the next, `tools/verify/public-rhythm.php` holds it at one declaration,
 * and a figure that carried a margin would be the sixteenth number that entry [213] removed
 * fifteen of.
 */

.c-fig {
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
}

/*
 * The caption strip. `--surface-2` rather than the card's own fill, so the words read as a label
 * ON the figure rather than as the first line of it: a step away from the ground in both palettes,
 * which is the same reasoning `public-theme-fit.css` gives for the mascot's shell.
 */
.c-fig__cap {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3) var(--space-5);
  flex-wrap: wrap;
  margin: 0;
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--border-width) solid var(--line);
  background: var(--surface-2);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--ink-2);
}

/* The right hand half of the caption: what this is OF, in the product's own vocabulary. A file
   name, a page type, a bot. Weaker than the caption because it qualifies it. */
.c-fig__of {
  font-family: var(--font-mono);
  font-weight: var(--weight-normal);
  color: var(--ink-3);
}

.c-fig__body {
  padding: var(--space-5);
}

/*
 * The body of a figure whose shape draws its own edges. `c-checks` is a bordered list and
 * `c-fig__body`'s padding would put a border inside a border with 16px of nothing between them.
 */
.c-fig__body--flush {
  padding: 0;
}

/*
 * The line underneath. On this site it is nearly always the sentence that says what the figure is
 * NOT: `parts/home-split.php` carries the precedent and the argument, "an example of what the two
 * sides look like, not a scan". A figure built from an app's own documentation and read as a
 * measurement of somebody's site is the one misreading this vocabulary has to be able to prevent.
 */
.c-fig__note {
  margin: 0;
  padding: var(--space-4) var(--space-5);
  border-top: var(--border-width) solid var(--line-soft);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

/* ---- public-figures.css ---- */
/*
 * public-figures.css. The four counted numbers under the entry screen's console.
 *
 * Split out of `public-entry.css` in entry [211] when that file passed the 200 line cap. The
 * seam is the one house rule 5 asks for rather than a shave: this is a self contained row with
 * its own element, its own dividers and its own phone layout, and it shares nothing with the
 * console, the file keys or the line the agent says.
 *
 * WHAT THE NUMBERS ARE IS NOT DECIDED HERE. `parts/entry-hero.php` counts the registry and the
 * mounts rather than typing 18 and 13, which is that file's header and the reason the row stays
 * true when a check is disabled or a fourteenth app ships.
 */
/* ---- the four figures --------------------------------------------------------------------- */

/* The row closes the hero, so it takes the section rhythm rather than a number of its own. */
.c-figures {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: var(--c-public-rhythm);
  padding: 0;
  list-style: none;
  border-block: var(--border-width) solid var(--surface-3);
}

.c-figures__item {
  padding: var(--space-6) var(--space-8);
  text-align: center;
  border-right: var(--border-width) solid var(--surface-3);
}

.c-figures__item:last-child { border-right: 0; }

.c-figures__value {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  line-height: 1;
  color: var(--ink);
}

.c-figures__label {
  margin: var(--space-2) 0 0;
  font-size: var(--text-xs);
  color: var(--ink-3);
}


@media (max-width: 640px) {
  .c-figures__item { padding: var(--space-5) var(--space-6); }

  /*
   * TWO BY TWO, and the dividers follow. Four figures wrapped as a centred flex row put the
   * second row's items under the gaps in the first and left one divider running into open
   * space, which is a row failing to look like a row. A grid puts them on a lattice; the two
   * rules under it turn the divider off down the right hand column and add one under the top
   * pair, so the lattice is drawn rather than implied.
   */
  .c-figures { display: grid; grid-template-columns: 1fr 1fr; }
  .c-figures__item:nth-child(2n) { border-right: 0; }
  .c-figures__item:nth-child(-n+2) {
    border-bottom: var(--border-width) solid var(--surface-3);
  }
}

/* ---- public-foot.css ---- */
/*
 * public-foot.css. The public site's footer, inside the frame's own band. Entry [212], task 5.140.
 *
 * `public.css` owns the BAND: the surface, the top rule and the gutter that lines it up with the
 * masthead and the page. This owns what is inside it, which is a grid of link columns over a
 * single line of small print.
 *
 * ## Four columns and not the mockup's five
 *
 * The mockup's fifth is Company, and `parts/foot.php` says why it is absent: none of its six
 * addresses is served by anything in this estate. The grid is written as a repeating track rather
 * than five named ones so the column count follows the markup, which is what lets that stay a
 * decision in the view instead of a number in two files.
 */

.c-foot {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}

.c-foot__grid {
  display: grid;
  grid-template-columns: minmax(16rem, 1.4fr) minmax(18rem, 2fr) minmax(9rem, 1fr);
  gap: var(--space-8) var(--space-7);
}

/* ---- the brand column ---------------------------------------------------------------- */

/*
 * The type here is for the FALLBACK span only, the same way the masthead's is: `parts/foot.php`
 * writes a text wordmark when no mark has ever been pushed. With the logo on screen the link is a
 * box round a picture and none of these declarations reaches anything.
 */
.c-foot__mark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--touch-min);
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--weight-semi);
  color: var(--ink);
}

.c-foot__mark:hover,
.c-foot__mark:focus-visible {
  color: var(--ink);
  text-decoration: none;
}

/*
 * The hub's own logo at footer size. Entry [213], and it replaces `.c-foot__markicon`, which drew
 * the 32px icon beside a live wordmark until the owner ruled the logo goes in as the whole mark.
 *
 * 64px, THE SAME AS THE MASTHEAD, and it is the same measurement rather than a matching pair.
 * The file is a square emblem with the words "AI SpeedForce" on a banner across its lower third,
 * and rendered at 32, 40, 48, 64 and 80 over both grounds, 64 is where that banner stops being
 * texture and becomes words. The mark is this column's whole heading, so the size it is legible
 * at is the size it goes in at.
 *
 * NO RADIUS AND NO PLATE. The file is a transparent WebP of an ellipse, a bolt and a robot, so
 * there is no square edge to soften, and a radius would clip the bolt where it runs out past the
 * ellipse. The artwork carries its own ground on light and on dark alike.
 */
/*
 * HEIGHT IS THE MEASUREMENT AND WIDTH FOLLOWS THE FILE. The mark is 200 by 160, not square,
 * and a fixed width and height together stretched it to fit a box it was never drawn in.
 * The derivative keeps the original's ratio since the fix in BrandDerivatives, so `auto`
 * here is the width the artwork actually has at 64 tall.
 */
.c-foot__logo {
  width: auto;
  height: var(--space-11);
  flex: none;
}

.c-foot__blurb {
  max-width: 34ch;
  margin-block: var(--space-4) 0;
  font-size: var(--text-xs);
  line-height: var(--leading-loose);
  color: var(--ink-3);
}

/* ---- the link columns ----------------------------------------------------------------- */

/* A column is a heading over a list and nothing else, so it carries no rule of its own beyond
   being a grid cell. It is a class because the wide layout gives one of them a span. */
.c-foot__col { min-width: 0; }

/*
 * The heading is a real h2 and the second app column's is visually hidden rather than absent.
 * Thirteen names split down the middle are one list drawn in two places, so a screen reader gets
 * "More apps" where a reader gets a column that lines up with the one beside it. An empty <h2>
 * would have left the second list with no name at all, and a &nbsp; like the mockup's would have
 * announced a blank.
 */
/*
 * `.c-public h2` IS THE DISPLAY SERIF AT --text-2xl and it beats a single class, so this carries
 * the frame's class as well as its own. Measured: the first pass wrote `font-family: var(--font-sans)`
 * on `.c-foot__head` alone and both column headings rendered as 20px Newsreader, which is the
 * page's heading voice on what is a label over a link list.
 */
.c-public .c-foot__head {
  margin: 0 0 var(--space-4);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  letter-spacing: var(--tracking-wide);
  color: var(--ink);
}

/*
 * NO GAP, and the 44px comes from the link. `.c-public__link` is an inline-flex on --touch-min
 * because house rule 10 puts a thumb target at 44px, so a footer of thirteen already has 44px
 * rows. Adding a gap on top of that made them 52px apart and the column ran a screen and a half
 * down the page. Nothing here shrinks the target: the rows simply meet.
 */
.c-foot__list {
  display: grid;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

/*
 * COLUMNS AND NOT A GRID, because the fill order is the whole requirement. Thirteen links read
 * down the first column and then down the second; a grid does it across, and `grid-auto-flow:
 * column` needs a row count, which is the app count in a stylesheet. `columns: 2` needs neither
 * and re-balances itself when the fourteenth app ships.
 */
.c-foot__list--split {
  display: block;
  columns: 2;
  column-gap: var(--space-7);
}

.c-foot__list--split > li { break-inside: avoid; }

.c-foot__list .c-public__link {
  padding-inline: 0;
  font-size: var(--text-xs);
  color: var(--ink-3);
}

/* ---- the small print -------------------------------------------------------------------- */

.c-foot__bar {
  margin: 0;
  padding-block-start: var(--space-6);
  border-top: var(--border-width) solid var(--line-soft);
  font-size: var(--text-xs);
  color: var(--ink-3);
}

/*
 * TWO COLUMNS UNDER 820, not four and not one. The two app lists beside each other is the whole
 * reason the thirteen were split, and stacking them into a single column of thirteen would undo
 * it on the screen where a long column costs the most.
 */
@media (max-width: 900px) {
  .c-foot__grid { grid-template-columns: 1fr 1fr; }
  .c-foot__brand { grid-column: 1 / -1; }
  .c-foot__col--apps { grid-column: 1 / -1; }
}
/* ---- public-gauge.css ---- */
/*
 * public-gauge.css. The report's verdict bar, its score ring and the four group rows.
 *
 * Entry [210], task 5.138, from the v2 mockup's screen 3, which owns the layout, the motion and
 * the copy. Every colour is a token.
 *
 * ## THE RING IS `meter` AND NOTHING HERE DRAWS ONE
 *
 * The library already bends a percentage into a circle from `--c-meter-fill`, which meter sets
 * from its data attributes ([293]), and `scan-progress.js` already writes that property. The mockup
 * draws an SVG stroke-dashoffset instead; the owner ruled one technique in the suite, so this
 * sizes the component's own `--c-meter-ring` and puts the number in the middle of it. No second
 * gradient, no second escape hatch, no change to the component.
 */

.c-verdictbar {
  display: grid;
  grid-template-columns: 8rem 1fr;
  gap: var(--space-7);
  align-items: center;
  padding: var(--space-7);
  background: var(--surface);
  border: var(--border-width) solid var(--surface-3);
  border-radius: var(--radius-lg);
}

/* The agent, saying one true thing about a report that is already on the page. `agent-strip`'s own
   rule, and the figure is the same `mascot`. */
.c-verdictbar__says {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
  color: var(--ink);
}

.c-verdictbar__who {
  margin: 0 0 var(--space-2);
  font-size: var(--text-xs);
  color: var(--ink-3);
}

/* ---- the score, bent into a circle -------------------------------------------------------- */

/* No margin. The ring and the four groups are a section of the report and public.css sets the
   gap above them, the same one the verdict bar and the strip get. */
.c-gauge {
  display: grid;
  grid-template-columns: 15.625rem 1fr;
  gap: var(--space-9);
  align-items: center;
}

/*
 * The ring at the mockup's size. `--c-meter-ring` is the component's OWN local custom property,
 * declared on `.c-meter--ring` and documented there as its geometry, so setting it from a page
 * frame is the mechanism working rather than an override reaching inside a component.
 */
.c-gauge__ring {
  position: relative;
  display: grid;
  place-items: center;
}

.c-gauge__ring .c-meter--ring {
  --c-meter-ring: 15.625rem;
}

/* The component prints its own label above the track and this page prints the number inside it,
   so the label is dropped by the caller rather than hidden here. */
.c-gauge__middle {
  position: absolute;
  display: grid;
  place-items: center;
  text-align: center;
}

.c-gauge__number {
  margin: 0;
  font-family: var(--font-display);
  font-size: 4.125rem;
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.c-gauge__of {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--ink-3);
}

/* ---- the four groups ---------------------------------------------------------------------- */

.c-groups {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.c-groups__row {
  display: grid;
  grid-template-columns: 1fr 8rem 2.75rem;
  gap: var(--space-5);
  align-items: center;
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border: var(--border-width) solid var(--surface-3);
  border-radius: var(--radius-lg);
}

.c-groups__name {
  margin: 0;
  font-size: var(--text-base);
  color: var(--ink);
}

.c-groups__sub {
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
  color: var(--ink-3);
}

.c-groups__value {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-xl);
  text-align: right;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* A group with no score of its own says so in words and the bar is absent, not empty. Section
   4.2: a bar at zero is a measurement nobody took. */
.c-groups__none {
  grid-column: 2 / span 2;
  margin: 0;
  font-size: var(--text-xs);
  color: var(--ink-3);
  text-align: right;
}

@media (max-width: 900px) {
  .c-gauge { grid-template-columns: 1fr; gap: var(--space-7); }
  .c-gauge__ring { margin: 0 auto; }
  .c-gauge__ring .c-meter--ring { --c-meter-ring: 12rem; }
  .c-verdictbar { grid-template-columns: 1fr; }
}

/*
 * THE BAR GOES UNDER THE NAME ON A PHONE. Measured at 375: the row is three tracks, and an 8rem
 * meter beside a 2.75rem figure leaves about 120px for the label, which set "Do they understand
 * what you sell" to three lines against a bar one line tall. The name and its score stay on the
 * first line, where they are the two things being compared, and the bar takes the width under
 * them rather than a twelfth of it.
 *
 * Every cell is placed explicitly. A group with no score renders `.c-groups__none` INSTEAD of a
 * meter, so auto placement would put the second row's cell wherever the missing one left a hole,
 * and this row is the one that has to keep saying "no score" in the same place as a number.
 */
@media (max-width: 640px) {
  .c-groups__row {
    grid-template-columns: 1fr auto;
    gap: var(--space-3) var(--space-5);
  }

  .c-groups__row > div { grid-area: 1 / 1; }

  .c-groups__value,
  .c-groups__none { grid-area: 1 / 2; text-align: right; }

  .c-groups__row > .c-meter { grid-area: 2 / 1 / 3 / 3; }
}

/* ---- public-home.css ---- */
/*
 * public-home.css. The landing page's own sections. Entries [201], [210] and [212].
 *
 * The tile strip, the section band's furniture, and the closing call to action. The four group
 * cards and the three steps are `public-sections.css`, and the hero is `public-entry.css`.
 *
 * ## WHAT LEFT THIS FILE IN [212], and why it was safe to take out
 *
 * `.c-hero` and `.c-scanbox` were the v1 mockup's hero: a measure, a rule above the strip, and
 * a form that became a column under 640. The v2 agent mockup replaced that whole block with
 * `parts/entry-hero.php` in entry [210] and nothing has rendered either class since. Checked by
 * grep across shared/views and publicsite before cutting, which is the only way this is a
 * deletion rather than a guess: a class nobody writes is dead, and a class one page writes is
 * a page somebody is about to break.
 */
/* ---- the tile strip ----------------------------------------------------------------- */

/*
 * Thirteen monograms, wrapping. Ruling 4: monogram tiles stand.
 *
 * They are LINKS and therefore 44px square, house rule 10, which also settles what size a
 * monogram is: two letters inside a 44px tile at --text-sm, which is the same relationship
 * identity.css draws between its avatar and its initials.
 *
 * The colour is the suite brand and there is no per app hue here. design.md's section 3a hue
 * table governed --brand until entry [134] and governs nothing now: the suite is one navy and
 * identity is carried by --brand-accent, which is a per app file and no per app file is loaded
 * on this mount. Thirteen tiles in thirteen colours would be thirteen values invented in a
 * stylesheet, which is exactly what that entry removed.
 */
/* One margin, above. The strip only ever renders inside the closing band, and the band's own
   padding is what sits under it: the second value was drawing a gap nothing needed. */
.c-tilestrip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: var(--c-public-step) 0 0;
  padding: 0;
  list-style: none;
}

.c-tilestrip__tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  border: var(--border-width) solid var(--brand-border);
  border-radius: var(--radius-lg);
  background: var(--brand-subtle);
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  letter-spacing: var(--tracking-wide);
  color: var(--brand);
}

.c-tilestrip__tile:hover,
.c-tilestrip__tile:focus-visible {
  background: var(--brand);
  color: var(--brand-ink);
  text-decoration: none;
}

/*
 * AND THE TILE HOLDS AN IMAGE NOW, entry [212]. The tint and the border stay, because they are
 * what makes thirteen icons of thirteen different shapes read as one row: the uploaded PNGs have
 * their own grounds and their own margins, and a plain row of them looks like a row of stickers.
 * The tile is the frame; the icon sits inside it and does not fill it.
 */
.c-tilestrip__img {
  width: var(--space-8);
  height: var(--space-8);
  border-radius: var(--radius-md);
}

/*
 * The one control below the fold, centred with the column above it. Entry [211]: everything in
 * the hero is centred on a measure and this sat at the frame's gutter, which made it the only
 * thing on the page out of line.
 */
.c-band__foot {
  margin-top: var(--space-8);
  text-align: center;
}

/* ---- the band ------------------------------------------------------------------------ */

/*
 * The small brand coloured line above a section heading. It is a LABEL and not a heading, so it
 * is a <p>: three words in the accent above an h2 would read as a two level heading structure to
 * anything that navigates by them, and it is neither.
 */
/* No margin under it. The gap to the heading belongs to the pair and public-text.css sets it
   there, once, for every band on the site. */
.c-band__eyebrow {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--brand);
}

/* ---- the closing call to action ------------------------------------------------------ */

/*
 * The one tinted block on the site. Two accents at low alpha rather than one, which is the
 * mockup's own gradient: the magenta is the brand and the indigo is `--ai`, and the pair is the
 * only place those two meet. Both are composited over `--surface` by the browser, so nothing
 * here is a colour value that is not already a token.
 */
/* No margin. It is a section like any other and public.css sets the gap above it; it was the
   only block on the site that also set one BELOW itself, which is what made the footer sit
   further from the last section on the home page than any two sections sit from each other. */
.c-cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6) var(--space-8);
  padding: var(--space-8);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background:
    linear-gradient(135deg, rgb(from var(--brand) r g b / 0.14),
      rgb(from var(--ai) r g b / 0.08)),
    var(--surface);
}

.c-cta__say {
  flex: 1 1 20rem;
  min-width: 0;
}

.c-public .c-cta__head {
  margin: 0;
  font-size: var(--text-2xl);
}

.c-cta__act { margin: 0; flex: none; }


/* ---- public-net.css ---- */
/*
 * public-net.css. THE NET: one thing, and what is linked to it. Entry [214], task 5.143.
 *
 * One of the five shapes in the app pages' graphic vocabulary. `parts/fig-net.php` is the view
 * and carries the contract. `public-fig.css` is the frame this sits in.
 *
 * ## NEW, AND IT IS THE OWNER'S "NODE GRAPH", NARROWED TO THE ONE CASE THAT IS REAL
 *
 * Three of the thirteen draw connections and all three draw the SAME connection: a hub with
 * satellites and a named relation on each spoke.
 *
 *   SchemaAI     its §1 gap 2, in as many words: "Wix emits isolated per-page schema. No @id
 *                cross-linking, no sameAs, no Organization to Person to Service to Product
 *                relationships." Its prototype draws an Organization at the centre with eight
 *                satellites and the edges labelled `publisher`, `department`, `employee`,
 *                `blogPost`, `provider`, `brand`, `organizer`.
 *   EntityGraph  its §2: "Give your business one canonical @id that every page's markup points
 *                at". Same picture, same edge names, on graph paper.
 *   Lookout      a visitor journey through pages, which is a different graph and is NOT this one:
 *                it has no hub and its edges are volumes rather than relations.
 *
 * So this draws a STAR and does not pretend to draw an arbitrary graph. Two apps need a star, and
 * a general graph renderer for two callers is a layout engine nobody asked for. Lookout's journey
 * is a `c-run` with branches it does not have; if it turns out to need the branches, that is a
 * decision for whoever writes that page, with this file's limit written down here.
 *
 * ## IT IS LAID OUT AS SPOKES AND NOT AS A CIRCLE, and that is a legibility decision
 *
 * A radial diagram needs node coordinates computed from the node count, and its labels are set at
 * whatever size is left after the circle has taken the width. At 375 that is unreadable, and this
 * site's rule is that everything works from 360 up. Spokes read at every width, wrap without
 * arithmetic, and say the same thing: which nodes hang off the hub, and under what name.
 *
 * ## A MISSING NODE IS PART OF THE SHAPE
 *
 * Both apps sell the ABSENCE. SchemaAI's prototype draws one satellite dashed and red because the
 * location is not modelled; EntityGraph's draws Wikidata as a ghost on a dashed edge. A star that
 * can only draw what is there cannot make either app's argument, so `--gone` is a first class
 * state and it is a dashed outline rather than a colour, for the reason `verdict-strip` gives.
 */

.c-net {
  display: grid;
  gap: var(--space-5);
}

/*
 * The hub. Filled, because it is the one node everything else is described against, and the fill
 * is the brand pairing the library already uses for a control that is on.
 */
.c-net__hub {
  justify-self: start;
  max-width: 100%;
  padding: var(--space-4) var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--brand);
  color: var(--brand-ink);
}

.c-net__k {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

.c-net__name {
  display: block;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
}

/*
 * ONE TRUNK, ON THE LIST, AND NOT ONE RULE PER ROW. The first pass put the border on the spoke
 * and drew five short lines with a gap between each pair, which reads as five separate rows
 * rather than as five things coming off one thing. The line belongs to the set.
 */
.c-net__spokes {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: 0 0 0 var(--space-6);
  border-inline-start: var(--border-width) solid var(--line);
  list-style: none;
}

/* One spoke: the relation, then the node. */
.c-net__spoke {
  display: grid;
  grid-template-columns: minmax(6rem, auto) 1fr;
  align-items: center;
  gap: var(--space-3) var(--space-5);
}

/*
 * The relation's own name, in the schema.org vocabulary the two apps actually use: `sameAs`,
 * `publisher`, `department`. Monospace because it is an identifier a reader will type, which is
 * state-of-play.md section 3's test for the face.
 */
.c-net__edge {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-3);
}

/*
 * The node is as wide as its own name. `1fr` would run every node to the edge of the figure, and
 * five full width boxes down a page read as rows in a table rather than as things hanging off a
 * hub: the shape stops saying anything the moment the boxes are all the same size.
 */
.c-net__node {
  justify-self: start;
  max-width: 100%;
  min-width: 0;
  padding: var(--space-3) var(--space-5);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
}

.c-net__node .c-net__k { color: var(--ink-3); }

/*
 * NOT THERE. Dashed and dimmed, and no danger colour: a node a site has never created is not a
 * fault the way a broken link is, it is a gap, and the two apps that draw it are selling the
 * difference. The word beside it says which, because the shape alone cannot.
 */
.c-net__node--gone {
  border-style: dashed;
  border-color: var(--muted-dim);
  background: none;
  color: var(--ink-3);
}

/*
 * ONE COLUMN UNDER 640. The relation goes above its node rather than beside it: at 375 a 6rem
 * label column leaves about 200px for a node name, and `Organization` wraps to three lines in it.
 */
@media (max-width: 640px) {
  .c-net__spoke {
    grid-template-columns: 1fr;
    gap: var(--space-2);
  }
}

/* ---- public-notify.css ---- */
/*
 * public-notify.css. The capture at the foot of an app page. Entry [214], task 5.143.
 *
 * ## WHY IT IS ITS OWN FILE
 *
 * Split from `public-apps.css` under the 200 line cap, and the seam is the one that file's own
 * title draws: "the app grid, the chips and the one app page". The grid and the chips belong to
 * /apps and are read together; this is the last SECTION of an app page and the only block on
 * this mount that takes a POST. `publicsite/routes.php` has a route for it, `NotifyStore` has a
 * table for it, and neither has anything to do with a filter chip.
 *
 * ## WHAT IT ADDS, WHICH IS TWO RULES, BECAUSE THE BLOCK ALREADY EXISTED
 *
 * The capture renders `c-cta`, the closing band in `public-home.css`: words on the left, the way
 * out of the page on the right, wrapping to a stack when the row runs out. The home page and the
 * app grid both close with it and an app page now closes with it too, because a capture is that
 * band with a form where the button goes.
 *
 * It was `<div class="c-notify">` holding a `panel`, capped at `--c-public-form`. Measured at
 * 1440 on all thirteen app pages: a 544px card against the left gutter of an 1180px flow, 636px
 * of empty row beside it, under a plans row and a related row that both span the page. The number
 * was right and the element was wrong. Entry [213] settled that every direct child of a flow is a
 * SECTION and a section is the page wide, so a section that narrows itself is the one shape the
 * system has no room for.
 *
 * `--c-public-form` survives and moves one element in, onto the act slot. public.css already
 * describes it as "a capture form. A control metric and not a page one: an email field 832px wide
 * is a field nobody can aim at", and the act slot is the control. It is still declared exactly
 * once, on `.c-public`, which is what `tools/verify/public-rhythm.php` asserts.
 *
 * This file emits after `public-home.css`, so the one rule that overrides `c-cta` at equal
 * specificity wins on source order. See the note in build-library-css.sh.
 */

/*
 * The act slot grows into whatever the words leave and stops at the form measure. `flex: none` is
 * what `.c-cta__act` gives a button, and that is right for a button: a button is as wide as its
 * own label. A field is not. Left at `none` the input took its browser default of about 170px
 * with the row's slack sitting empty beside it.
 *
 * The basis and the cap are the same number on purpose. Above roughly 900px both children are
 * over their basis and the cap holds the field at a width somebody can aim at; below it the basis
 * is what makes the pair wrap rather than crushing the form to nothing.
 */
.c-notify__act {
  flex: 1 1 var(--c-public-form);
  max-width: var(--c-public-form);
  min-width: 0;
}

/*
 * AND THE HEADING JOINS THE PAGE IT IS ON. `.c-cta__head` is `--text-2xl`, which is right on the
 * home page and on /apps, where the closing band is the only h2 below the lead and has nothing to
 * disagree with. An app page runs six h2 headings above this one at `--text-xl`, set by
 * public-text.css, so a seventh at 24px reads as a louder section rather than as the last one.
 */
.c-notify .c-cta__head {
  font-size: var(--text-xl);
}

/*
 * The field and the button are a column on a phone and a row above 640, where the button sits on
 * the field's baseline rather than under it. `align-items: flex-end` is what puts them there:
 * `field` renders a label above its input, so the two controls have different heights and only
 * their bottoms line up.
 */
.c-notify__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  align-items: stretch;
}

@media (min-width: 640px) {
  .c-notify__form {
    flex-direction: row;
    align-items: flex-end;
  }

  .c-notify__form > .c-field {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
  }
}

/* ---- public-panes.css ---- */
/*
 * public-panes.css. The scan screen's checks feed and probe rail, and the two states that move.
 *
 * Entry [226]. The feed rules moved here from `public-wire.css` when that file crossed its cap;
 * the rail and the reading state are new. The seam: `public-wire.css` is what was fetched, this is
 * what is being worked through.
 *
 * ## THE TWO STATES, AND WHAT EACH IS ALLOWED TO MEAN
 *
 * `is-reading` on a rail step, a check row or a square says one thing: the file this reads is the
 * one `current_phase` names. It is a pulse, because it is the one thing on the page that is in
 * motion in fact. `is-done` on a rail step says the probe before the one in flight has run, which
 * follows from `Probes::run()`'s fixed order. Neither is a verdict, and neither colours anything
 * with a verdict's colour: those three are `--pass`, `--warning` and `--issue` and arrive only
 * with a result.
 *
 * base.css reduces every animation to 0.01ms under prefers-reduced-motion, so the pulse is a
 * steady mark there and needs no block of its own.
 */

/* ---- the probe rail ------------------------------------------------------------------------ */

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

.c-rail__step {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  font-size: var(--text-xs);
  color: var(--ink-3);
}

.c-rail__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-pill);
  background: var(--muted-dim);
}

.c-rail__name { white-space: nowrap; }

.c-rail__step.is-done { color: var(--ink-2); }
.c-rail__step.is-done .c-rail__dot { background: var(--brand); }
.c-rail__step.is-reading { color: var(--ink); font-weight: var(--weight-medium); }
.c-rail__step.is-reading .c-rail__dot { background: var(--brand); animation: c-panes-pulse 1.2s var(--ease) infinite; }

@keyframes c-panes-pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--brand-subtle); }
  50% { box-shadow: 0 0 0 0.375rem var(--brand-subtle); }
}

/* ---- what the agent is working through --------------------------------------------------------------------- */

/*
 * No height cap since entry [226]: all eighteen checks are on the page from the first paint and
 * every one is meant to be read. TWO COLUMNS where there is room, so eighteen rows stand nine
 * high beside a files pane of about that height, and one column under 820px where the panes
 * stack anyway. A row that lands moves to the top and takes two lines, which the grid absorbs.
 */
.c-feed {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (max-width: 900px) {
  .c-feed { grid-template-columns: minmax(0, 1fr); }
}

.c-feed__row {
  display: flex;
  gap: var(--space-4);
  padding-block: var(--space-4);
  border-bottom: var(--border-width) solid var(--surface-3);
  animation: c-feed-in var(--duration-base) var(--ease);
}

@keyframes c-feed-in {
  from { opacity: 0; transform: translateY(0.375rem); }
  to { opacity: 1; transform: none; }
}

.c-feed__dot {
  flex: none;
  width: 0.5625rem;
  height: 0.5625rem;
  margin-top: var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--muted);
}

.c-feed__dot--pass { background: var(--success); }
.c-feed__dot--warning { background: var(--warning); }
.c-feed__dot--issue { background: var(--danger); }

.c-feed__name {
  margin: 0;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--ink);
}

.c-feed__evidence {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  color: var(--ink-2);
}

/* Empty until a verdict lands; the row is one line until then and two after. */
.c-feed__evidence:empty { display: none; }


/* Which file a check reads, on its own line under the name so every row is two lines and the
   columns stay level. Same size and colour as the rail's resting step. */
.c-feed__reads {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  color: var(--ink-3);
}

/* A check whose file is being read now: the dot takes the brand and pulses. A verdict's colour
   replaces it the moment one lands, and `is-landed` stops the pulse for good. */
.c-feed__row.is-reading .c-feed__dot { background: var(--brand); animation: c-panes-pulse 1.2s var(--ease) infinite; }
.c-feed__row.is-landed .c-feed__dot { animation: none; }

/* ---- the squares' resting state, scoped to the stage ------------------------------------- */

/*
 * SCOPED, NOT A COMPONENT EDIT. `verdict-strip` draws idle as hollow and dashed, which on the
 * report means "no measurement" and is right there. On the stage every square is idle at first
 * paint and dashed read as unfilled rather than as waiting, the owner's word. Inside `.c-stage`
 * only, idle is a solid quiet tile, and the one whose file is being read carries the pulse.
 */
.c-stage .c-verdict-strip__cell--idle {
  background: var(--surface-3);
  border-style: solid;
  border-color: var(--surface-3);
}

.c-stage .c-verdict-strip__cell--idle.is-reading {
  background: var(--brand-subtle);
  border-color: var(--brand-subtle);
  animation: c-panes-pulse 1.2s var(--ease) infinite;
}

/* ---- public-reads.css ---- */
/*
 * public-reads.css. THE READOUT: text as a machine writes it or reads it. Entry [214], 5.143.
 *
 * One of the five shapes in the app pages' graphic vocabulary. `parts/fig-reads.php` is the view
 * and carries the contract. `public-fig.css` is the frame this sits in.
 *
 * ## IT IS `.c-reads`, MOVED AND WIDENED, AND NOT A NEW BLOCK
 *
 * Entry [212] wrote it for one block on the home page: the pane that shows what an assistant gets
 * out of a shop that has no llms.txt. Reading the thirteen app prototypes in entry [214] found
 * that NINE of them make a readout their loudest element and two make it the whole product.
 * LLMify draws `llms-full.txt` as its hero, BotGate keeps the site's robots.txt on screen at all
 * times, SchemaAI shows the JSON-LD it would write, Redirectify shows the CSV it hands to Wix,
 * Sitemapify shows the redirect that hosts its HTML sitemap, Lookout shows a stack trace,
 * EntityGraph shows Wikidata statements, Merchant Feed shows the fetch log and AltTextAI shows
 * the scan console. It was already the most reused picture on the estate and it was filed under
 * one home page block.
 *
 * ## THE WIDENING IS ONE MODIFIER AND ONE SPAN
 *
 * `--say` sets the prose face, because three apps draw the same thing in sentences rather than in
 * a file: AI Visibility highlights a brand's name inside a real assistant answer, EntityGraph
 * marks the claims in one that are about a different company, and AskBot prints the visitor's
 * question exactly as they typed it. Marked text inside a machine's own words is one shape, and
 * whether the machine wrote a file or a paragraph is a face.
 *
 * `__mark` is the attention span the file did not have. `__have`, `__miss` and `__q` mean what
 * they meant: an assistant could read this, an assistant could not, and this is the question.
 * They are `--success` and `--danger` and NOT a pair invented here, because they mean the same
 * thing they mean on a report.
 */

/*
 * A `<pre>` that WRAPS. Left alone, seven aligned lines of monospace force a horizontal scrollbar
 * on a phone, and a readout the reader has to drag sideways makes the opposite point to the one
 * intended. `pre-wrap` keeps the alignment where there is room and gives it up where there is not.
 */
.c-reads {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-loose);
  color: var(--ink-2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/*
 * The tint behind `__miss` is `--danger-subtle`, the same pairing `tag` renders and which
 * `tools/verify/public-dark.php` measures at 4.71 on this surface.
 */
.c-reads__miss {
  padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--danger-subtle);
  color: var(--danger);
}

.c-reads__have { color: var(--success); }
.c-reads__q { color: var(--ink-3); }



/*
 * PROSE, for a readout that is an answer rather than a file. The face changes and nothing else
 * does: the marks below still mean what they mean, and a marked name inside a sentence is read
 * the same way a marked value inside a file is.
 *
 * `white-space: normal` because a paragraph is not aligned and `pre-wrap` would keep every line
 * break the caller's source happened to have in it.
 */
.c-reads--say {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  white-space: normal;
  overflow-wrap: break-word;
  color: var(--ink);
}

/*
 * THE ATTENTION MARK. The one span that says "this is the part to look at" without saying whether
 * it is good or bad, which the three tones above cannot: a brand's own name inside an assistant's
 * answer is neither a pass nor a failure, it is the thing being counted.
 *
 * `--brand-subtle` with `--brand` on it is the pairing `themes/public.css` solves in both
 * palettes and measures at 4.59 in light. It is the grade pill's pairing and this is the same
 * job: a tinted run of text that has to stay readable on either ground.
 */
.c-reads__mark {
  padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--brand-subtle);
  color: var(--brand);
}

/* ---- public-report.css ---- */
/*
 * public-report.css. The report, and the waiting room that turns into it. Entry [201], 5.130.
 *
 * ## THE SCORE IS THE ONLY PLACE IN THIS SUITE WITH TYPE THIS LARGE, and it is a token
 *
 * --text-display is 2.5rem and is the h1 size on this frame. The figure is set from the same
 * token at a heavier weight rather than from a number typed here, so the score and the headline
 * above it are one scale. Nothing invents a size: house rule 9, and check-responsive.sh refuses
 * a font-size in pixels inside a component anyway.
 *
 * The digits are tabular. Every figure a person compares row to row in this suite is, which is
 * the monospace decision in state-of-play section 3, and a score beside a group score is exactly
 * that comparison.
 */

/*
 * ## WHAT LEFT THIS FILE IN [213] AND WHY NONE OF IT WAS SAFE TO KEEP
 *
 * `.c-report` was a 52rem measure and a `margin-inline`, written here, beside a 46rem one written
 * on `.c-waiting` and a third on the entry column. All three are `--c-public-read` on the frame
 * now and the wrapper on this screen is `.c-public__column--read`, so a fourth screen cannot
 * disagree with the other three about where a column ends.
 *
 * `.c-score` and `.c-report__bars` went with it, and they went because they are DEAD rather than
 * because they were in the way: entry [210] replaced the number over a list of bars with the ring
 * and the group rows in `public-gauge.css`, and nothing has rendered either class since.
 * `tools/verify/public-pages.php` asserts `c-score__n` is ABSENT from a suppressed report, which
 * is the check that would have caught it coming back. Grepped across shared/views and publicsite
 * before cutting, the same way [212] cut `.c-hero`.
 */

/*
 * The tally under the strip. It is words and not a legend of coloured squares: a key that says
 * "green means passed" is colour as the only signal wearing a caption, and the strip already
 * carries every cell's verdict as text in its accessible name and its title.
 */
/* The tally under the strip. No margin: the strip and its tally are one section, so the step
   inside a band puts them the same distance apart as any heading and its body. */
.c-report__key {
  margin: 0;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

/* ---- one finding -------------------------------------------------------------------- */

/*
 * The severity modifier is a LEFT EDGE and not a fill. A panel tinted red behind a paragraph of
 * evidence costs the evidence its contrast, and there are up to eighteen of these down one page.
 * The `tag` in the panel head carries the word.
 */
/* No margin of its own. Eighteen findings are the body of one section and the step inside a band
   spaces them, which is why the gap between two findings is now the same as the gap between the
   heading and the first of them. It was 20 and 24. */
.c-finding {
  display: block;
}

.c-finding > .c-panel {
  border-left-width: var(--space-2);
}

.c-finding--issue > .c-panel {
  border-left-color: var(--danger);
}

.c-finding--warning > .c-panel {
  border-left-color: var(--warning);
}

.c-finding__evidence {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

/*
 * The cross sell. It sits under the evidence with a rule above it, so a person reading the
 * finding meets what happened before they meet what we sell.
 */
.c-finding__fix {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-block-start: var(--space-5);
  padding-block-start: var(--space-5);
  border-top: var(--border-width) solid var(--line-soft);
}

/* ---- what was not measured ---------------------------------------------------------- */

.c-unmeasured {
  padding: var(--card-pad);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface-3);
}

.c-unmeasured__list {
  margin: var(--space-5) 0 0;
}

.c-unmeasured__list > dt {
  margin-block-start: var(--space-5);
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  color: var(--ink);
}

.c-unmeasured__list > dd {
  margin: 0;
  padding-inline-start: 0;
}

.c-unmeasured__why {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  color: var(--ink-2);
}

/*
 * The exact `Disallow` line lives in this sentence, which is why it is set apart rather than run
 * in with the reason above it. Section 4.6: every withheld check is NAMED, with the line that
 * withheld it and the user agent group it appeared under. Not summarised, not counted, named.
 */
.c-unmeasured__evidence {
  margin: var(--space-2) 0 0;
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

/* ---- public-run.css ---- */
/*
 * public-run.css. THE RUN: an ordered sequence with a named end. Entry [214], task 5.143.
 *
 * One of the five shapes in the app pages' graphic vocabulary. `parts/fig-run.php` is the view
 * and carries the contract. `public-fig.css` is the frame this sits in.
 *
 * ## NEW, AND IT IS THE ONE THE OWNER'S LIST HAD AS "A TIMELINE OR SERIES"
 *
 * It is not a series. A series plots measured values against time and there is nothing to plot:
 * these are marketing pages for apps that have no customers yet, and docs/public-scan.md 11.13
 * rules that a figure with no measurement behind it does not render. A chart with invented
 * numbers is the exact thing that section exists to forbid.
 *
 * What the thirteen documents actually describe is an ORDER, not a quantity. Six of them state a
 * sequence in words and one of them states it as a diagram:
 *
 *   AI SEO Copilot  its own §4: "Crawl + GSC pull to detect issue to AI drafts fix to user
 *                   approves to API writes to verify".
 *   Merchant Feed   its §3 transformation layer, drawn in the prototype as five stages that
 *                   narrow: catalogue, rules applied, excluded, blocking issues, submitted.
 *   Redirectify     its §2 beacon path, and the prototype's route strip, which is the same shape
 *                   ending at a wall instead of at a destination.
 *   BotGate         its §3: robots.txt names a path, a bot fetches it, the fetch is logged,
 *                   reverse DNS answers, the verdict follows.
 *   Sitemapify      its §5 install to instant audit, and the prototype's six step scan.
 *   Lookout         a publish, then a regression, then an alert.
 *
 * ## THE END IS PART OF THE SHAPE AND NOT A SIXTH STEP
 *
 * Redirectify's whole product is that some routes end at a wall: its prototype draws the terminal
 * as hazard striping, and a "dead end" that renders as one more step in a row says the opposite of
 * what it means. So the run has an END, it is drawn differently from a step, and it carries one of
 * three tones: it arrived, it stopped, or it is still open.
 *
 * The tones are `--success`, `--danger` and `--muted`, which mean here exactly what they mean on a
 * report. Nothing on this shape is a colour value.
 */

.c-run {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.c-run__step {
  display: flex;
  flex: 1 1 9rem;
  min-width: 0;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface-2);
}

/*
 * The step's name, in the mono face. It is a stage in a machine's own process and reads as one:
 * `state-of-play.md` section 3 records why this suite uses monospace at all, and "a thing a
 * person compares character by character" covers a stage name in a pipeline as well as an id.
 */
.c-run__k {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--ink);
}

.c-run__say {
  margin: 0;
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

/*
 * The connector. It is a separate flex item rather than a pseudo element on the step, because a
 * run wraps: with the arrow attached to a step, the step that lands at the end of a row carries an
 * arrow pointing off the edge of the figure at nothing.
 */
.c-run__link {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  color: var(--ink-3);
}

.c-run__link svg {
  width: var(--space-6);
  height: var(--space-6);
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
}

/* ---- the end ------------------------------------------------------------------------------ */

.c-run__end {
  display: flex;
  flex: 1 1 9rem;
  min-width: 0;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
}

.c-run__end--done {
  border-color: var(--success);
  background: var(--success-subtle);
}

.c-run__end--done .c-run__k { color: var(--success-dim); }

/*
 * A ROUTE THAT TERMINATES AT A WALL, and the stripes are the wall. Redirectify's prototype draws
 * it as hazard striping and it is the right picture: a border colour alone reads as "this step is
 * bad", and what this says is "there is no next step".
 *
 * The gradient is two tokens and no third colour. `--danger-subtle` is the tint `tag` uses behind
 * `--danger` and `tools/verify/public-dark.php` measures it at 4.71 on this surface, so the words
 * on top stay readable in both palettes.
 */
.c-run__end--stop {
  border-color: var(--danger);
  background: repeating-linear-gradient(135deg, var(--danger-subtle) 0 var(--space-3),
    transparent var(--space-3) var(--space-6));
}

.c-run__end--stop .c-run__k { color: var(--danger-dim); }

/* Still open. No fill and no colour, because "nothing has happened yet" is not a verdict. The
   dashed edge is `verdict-strip`'s own answer to the same question and is the shape, not a hue. */
.c-run__end--open {
  border-style: dashed;
  border-color: var(--muted-dim);
}

.c-run__end--open .c-run__k { color: var(--ink-3); }

/*
 * ONE COLUMN UNDER 640, with the arrows turning to point down the page. Above that the steps wrap
 * on their own: `flex: 1 1 9rem` fits five stages at 1180, three at 768 and two at 640, and a run
 * that wraps mid sequence still reads because every connector is between the pair it joins.
 */
@media (max-width: 640px) {
  .c-run { flex-direction: column; }
  .c-run__link { transform: rotate(90deg); align-self: center; }

  /*
   * AND THE BASIS COMES OFF. `flex-basis` is on the MAIN axis, so the 9rem that is a minimum
   * width in a row becomes a minimum HEIGHT in a column: measured at 375 every step was 144px
   * tall with one line of text at the top of it and 100px of empty card under it. A step in a
   * column is as tall as what is in it.
   */
  .c-run__step,
  .c-run__end { flex: 0 0 auto; }
}

/* ---- public-scout.css ---- */
/*
 * public-scout.css. The agent on the scan screen: the ring it stands in, and the panel it
 * speaks from. Entry [216].
 *
 * Split out of `public-stage.css` when that file passed the 200 line cap, and the seam is the one
 * the screen has. `public-stage.css` is the SHELL: the head, the counters, the strip and the two
 * panes, all of which are the same furniture whatever is happening. This is the FIGURE, which is
 * the one part of the screen a reader looks at rather than reads, and it is drawn from two
 * components the rest of the site also uses, `mascot` and `meter`.
 *
 * The ring fills on checks landed out of checks registered, which is a measurement. It has never
 * moved on a timer and `public-stage.js` records why: a bar that moves on a clock is a picture of
 * a scan, and this project refuses those.
 */

/* ---- the agent, and the ring that fills as checks land --------------------------------------- */

.c-stage__agent {
  display: grid;
  grid-template-columns: 9.375rem 1fr;
  gap: var(--space-7);
  align-items: center;
  padding: var(--space-7) 0 var(--space-3);
}

.c-stage__hold {
  position: relative;
  display: grid;
  place-items: center;
  width: 9.375rem;
  height: 9.375rem;
}

/* The ring is `meter` in ring shape, sized by its own local property. See public-gauge.css. */
.c-stage__hold .c-meter--ring {
  --c-meter-ring: 9.375rem;
  position: absolute;
  inset: 0;
}

/* ---- what the agent is saying ----------------------------------------------------------------- */

.c-speech {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 6.25rem;
  padding: var(--space-6) var(--space-7);
  background: var(--surface);
  border: var(--border-width) solid var(--surface-3);
  border-radius: var(--radius-lg);
}

/* The tail. A rotated square rather than a border triangle, so it takes the panel's own border
   and background and cannot drift from them. */
.c-speech::before {
  content: "";
  position: absolute;
  top: 2.75rem;
  left: -0.5rem;
  width: 0.9375rem;
  height: 0.9375rem;
  background: var(--surface);
  border-left: var(--border-width) solid var(--surface-3);
  border-bottom: var(--border-width) solid var(--surface-3);
  transform: rotate(45deg);
}

.c-speech__who {
  margin: 0 0 var(--space-2);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--ink-3);
}

.c-speech__line {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-xl);
  line-height: var(--leading-loose);
  color: var(--ink);
}

/* ---- public-sections.css ---- */
/*
 * public-sections.css. The four group cards and the three steps. Entry [212], task 5.140.
 *
 * Both were `panel` calls in an `l-grid` until this entry, and both stop being that here, for
 * the same reason and only that reason: the sections mockup gives each a shape `panel` does not
 * have. A group card carries a strap line under its heading and a ruled list with no bullets;
 * a step carries a numbered key above its heading. Adding two variants to a component thirteen
 * control panels also render, so that one marketing page can have them, is the direction house
 * rule 7 is pointing away from.
 *
 * ## `.c-gcard__num` IS GONE AND `.c-step__k` STAYS, and the difference is the whole rule
 *
 * Entry [213], the owner's ruling of 2026-09-10. The group card drew `count($checks)` at 4rem in
 * `--surface-3`, absolutely positioned in the top corner, and the four cards read 6, 5, 4, 3 in
 * reading order: a descending run of single digits with no unit and no word beside any of them.
 * `parts/home-groups.php` carries the argument for removing it rather than labelling it.
 *
 * The step key survives the same test because it passes it. Its numerals are 1, 2, 3 and they
 * ARE a sequence: they number three steps that happen in that order, and the filled `--radius-md`
 * plate at `--space-9` is the shape a reader already knows means "step". The test is not "no
 * numeral without a word", which would take the steps out too. It is "no numeral a reader could
 * take for a sequence when it is not one".
 *
 * Two rules went with the numeral. `.c-gcard` no longer needs `overflow: hidden`, which existed
 * to crop the watermark at the card's corner radius, and `.c-gcard__head` no longer needs
 * `position: relative`, which existed to lift the heading over it. Measured at 375 that lift was
 * failing anyway: the heading wraps to two lines there and the glyph crossed it on two of the
 * four cards.
 */

/* ---- the four groups ------------------------------------------------------------------ */

.c-quad {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);
}

.c-gcard {
  padding: var(--space-7);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.c-public .c-gcard__head {
  margin: 0;
  font-size: var(--text-lg);
}

.c-gcard__sub {
  margin: var(--space-2) 0 0;
  font-size: var(--text-xs);
  color: var(--ink-3);
}

.c-gcard__list {
  margin: var(--space-5) 0 0;
  padding: 0;
  list-style: none;
}

/*
 * A rule ABOVE each row rather than below, so the list opens with a line under the strap and
 * closes flush with the card's own padding. A trailing rule with nothing under it reads as a
 * row that failed to render.
 */
.c-gcard__list > li {
  padding-block: var(--space-3);
  border-top: var(--border-width) solid var(--line-soft);
  font-size: var(--text-sm);
  color: var(--ink-2);
}

/* ---- the three steps ------------------------------------------------------------------- */

.c-steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-5);
}

.c-step {
  padding: var(--space-7);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.c-step__k {
  display: grid;
  place-items: center;
  width: var(--space-9);
  height: var(--space-9);
  margin-block-end: var(--space-5);
  border-radius: var(--radius-md);
  background: var(--brand-subtle);
  font-family: var(--font-display);
  font-size: var(--text-md);
  color: var(--brand);
}

.c-public .c-step__head {
  margin: 0;
  font-size: var(--text-md);
}

.c-step__body {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

/*
 * 768 IS THE WIDTH NOBODY LOOKED AT, and both of these were wrong there. Entry [213], found by
 * the layout pass: every other grid on this site steps three to two to one across 820 and 640,
 * and these two went straight from their desktop shape to a single column at 820. At 768 that
 * ran a group card and a step card the full 720px of the frame, so a step's two sentences set to
 * about a hundred characters a line, which is roughly twice a comfortable measure.
 *
 * The quad keeps two columns down to 640, because two of four is still a pair. The steps DO go
 * to one at 820: two of three leaves an orphan on the second row, and a numbered sequence broken
 * two-then-one reads as two steps and an afterthought. Different answers because they are
 * different shapes, not because one of them was overlooked.
 */
@media (max-width: 900px) {
  .c-steps { grid-template-columns: 1fr; }
}

@media (max-width: 640px) {
  .c-quad { grid-template-columns: 1fr; }
}
/* ---- public-split.css ---- */
/*
 * public-split.css. "What a visitor sees" against "what an assistant actually reads".
 * Entry [212], task 5.140, from the sections mockup.
 *
 * One block, two panes, and the whole argument of the product is the gap between them. The panes
 * are deliberately UNEQUAL in treatment: the left sits on `--surface` and the right on the ground,
 * so the second reads as what is left when the design is taken away.
 */

/* No margin of its own. It is the body of a section, `parts/home-split.php` is the section, and
   public.css sets the one gap between one section and the next. */
.c-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.c-split__pane { padding: var(--space-6); min-width: 0; }

.c-split__pane--seen {
  background: var(--surface);
  border-inline-end: var(--border-width) solid var(--line);
}

.c-split__pane--read { background: var(--ground); }

.c-split__cap {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0 0 var(--space-5);
  font-size: var(--text-xs);
  color: var(--ink-3);
}

.c-split__dot {
  width: 0.4375rem;
  height: 0.4375rem;
  flex: none;
  border-radius: var(--radius-full);
  background: var(--ink-3);
}

/*
 * --ai MEANS "A MACHINE READ THIS" and is never decorative: `tokens.css` says so and `lamp.css`
 * is the other place it is used. The dot beside "what an assistant actually reads" is the one
 * use on this page and it is exactly what the token is for.
 */
.c-split__dot--ai { background: var(--ai); }

/* ---- THE READOUT MOVED OUT IN ENTRY [214] -------------------------------------------- */

/*
 * `.c-reads` was declared here because this block is where it was first needed. It is one of the
 * five shapes of the app pages' graphic vocabulary now and lives in `public-reads.css`, widened
 * to take prose as well as a monospace file. Nothing about this block changed: it still renders
 * `<pre class="c-reads">` and the rules it gets are the same ones, one file along.
 *
 * The seam is worth naming. This file is ONE BLOCK on the home page, the argument the product
 * makes; that file is a PRIMITIVE thirteen app pages draw with. A shape used by one screen and a
 * shape used by thirteen are different files even when the rules are identical.
 */

/*
 * ONE COLUMN UNDER 820, with the divider becoming the rule between them. Side by side is the
 * comparison; stacked, the border has to say which pane is which, and an inline-end rule on a
 * full width pane is a line down the middle of nothing.
 */
@media (max-width: 900px) {
  .c-split { grid-template-columns: 1fr; }

  .c-split__pane--seen {
    border-inline-end: 0;
    border-block-end: var(--border-width) solid var(--line);
  }
}
/* ---- public-stage.css ---- */
/*
 * public-stage.css. The scan screen: the agent with a ring round it, and the two panes below.
 *
 * Entry [210], task 5.138, from the v2 mockup's screen 2, which owns layout, motion, copy and
 * states. Every colour is a token.
 *
 * ## THE PINS ARE NOT HERE, on the owner's ruling
 *
 * The mockup drops four labelled markers onto the site preview at absolute coordinates, and those
 * coordinates describe a drawing rather than a site. Nothing in the artefact bag carries an
 * element position, so the real screen either measures them or does without. It does without: the
 * sweep claims nothing, and a pin at an invented position would be the page asserting where a
 * problem is on a page it has only read the source of.
 */

/* The screen itself. It is the poller's child, so it carries no width of its own: the
   waiting room around it is already inside the frame's measure. */
.c-stage { display: block; }

/* Which site is being read. `strong` inside it is the host, and the host is hostile input
   that went through e() before it got here. */
.c-stage__site {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--ink-2);
}

.c-stage__site strong {
  font-weight: var(--weight-medium);
  color: var(--ink);
}

.c-stage__head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4) var(--space-7);
  align-items: center;
  justify-content: space-between;
  padding-bottom: var(--space-5);
  border-bottom: var(--border-width) solid var(--surface-3);
}

.c-stage__meta {
  display: flex;
  gap: var(--space-7);
  margin: 0;
  padding: 0;
  font-size: var(--text-xs);
  color: var(--ink-3);
  list-style: none;
}

.c-stage__meta b {
  color: var(--ink);
  font-family: var(--font-mono);
  font-weight: var(--weight-medium);
  font-variant-numeric: tabular-nums;
}

/* ---- the eighteen idle squares ------------------------------------------------------------ */

/*
 * MOVED HERE FROM `public-waiting.css` IN ENTRY [213], as `.c-stage__strip` and not
 * `.c-waiting__strip`. It is drawn by `parts/stage.php`, inside `.c-stage`, and it was named
 * after the screen around it rather than after the thing that draws it: the waiting room has
 * three states and only this one has a strip. The name was the reason its spacing sat in a file
 * that knows nothing about the stage.
 */
/* The stage's own blocks sit a step apart, the same step a section's body sits under its
   heading. This was 40 where its neighbours were 24. */
.c-stage__strip {
  margin-block: var(--c-public-step);
}

/* ---- the two panes ------------------------------------------------------------------------ */

/*
 * TWO EQUAL COLUMNS, AND `minmax(0, 1fr)` RATHER THAN `1fr`. Entry [216]: `1fr` is
 * `minmax(auto, 1fr)`, so a grid column is never allowed to be narrower than its content's own
 * minimum, and the fetch log now carries real URLs. One Wix sitemap address pushed the left pane
 * to roughly twice the right one. `.c-wire__url` already truncates and could not, because the
 * track it sits in was growing to fit it. Measured on a real run of makkpress.com.
 */
/*
 * TWO TO THREE AND TOP ALIGNED, entry [226]. The right pane holds eighteen check names and their
 * evidence and the left holds a rail and a dozen file rows, so equal columns left the left pane
 * half empty under a right one that was clipped. The checks take the width they read at, and
 * `align-items: start` lets each pane be as tall as what is in it rather than stretching the
 * shorter one to the taller one's height with nothing to fill it.
 */
.c-stage__cols {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  gap: var(--space-6);
  align-items: start;
  margin-top: var(--space-7);
}

.c-pane {
  padding: var(--space-6);
  background: var(--surface);
  border: var(--border-width) solid var(--surface-3);
  border-radius: var(--radius-lg);
}

.c-pane__title {
  margin: 0 0 var(--space-5);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  letter-spacing: var(--tracking-wide);
  color: var(--ink-3);
}

@media (max-width: 900px) {
  .c-stage__cols { grid-template-columns: 1fr; }
  .c-stage__agent { grid-template-columns: 6rem 1fr; gap: var(--space-5); }
  .c-stage__hold { width: 6rem; height: 6rem; }
  .c-stage__hold .c-meter--ring { --c-meter-ring: 6rem; }

  /*
   * AND THE FIGURE COMES DOWN WITH THE RING. Entry [211]. `mascot--md` is 7rem and the ring here
   * is 6rem, so on a phone the robot was WIDER than the ring round it and its arms crossed the
   * arc: the ring stopped reading as a ring and became a plate the figure was standing on.
   * 4.5rem inside 6rem is the same three quarters the desktop pair has.
   */
  .c-stage__hold .c-mascot--md { --c-mascot-size: 4.5rem; }
  .c-speech::before { display: none; }
  .c-speech__line { font-size: var(--text-lg); }
  .c-stage__meta { flex-wrap: wrap; gap: var(--space-4); }
}

/*
 * AND SCOUT MOVES ABOVE WHAT IT IS SAYING ON A PHONE. Measured at 375: a 96px figure beside the
 * panel left 215px for a display face at 18px, which set the line the agent says to about four words
 * and six lines. The figure is centred over the panel and the panel takes the column.
 */
@media (max-width: 640px) {
  .c-stage__agent { grid-template-columns: 1fr; }
  .c-stage__hold { margin-inline: auto; }
}

/* A second title inside one pane, under the preview it follows. */
.c-pane__title--after { margin-top: var(--space-6); }

/* ---- public-text.css ---- */
/*
 * public-text.css. The prose the public frame needs and the panel never did. Entry [201], 5.130.
 *
 * An extension of public.css rather than part of it, for two reasons and not only the cap.
 * public.css is the FRAME: the masthead, the measure, the footer and the display face, and it is
 * the file somebody opens to change the shape of the page. This is the type inside that frame,
 * and the two get edited for different reasons. The build script emits a base before its
 * extensions, so `public.css` is always above this one and the cascade holds.
 *
 * ## Why a lead paragraph is a class here and an element default elsewhere
 *
 * design.md section 5.1 files "lead paragraph" under layout primitives, handled by an element
 * default in base.css. That default is sized for a control panel, where a lead sits over a table
 * at 14px. A marketing page reads at a longer measure and a larger size, and the difference is
 * the whole reason the display face exists on this frame at all. It is scoped to .c-public, so
 * nothing in the other fourteen mounts can pick it up.
 */

/* ---- the display type ---------------------------------------------------------------- */

/*
 * MOVED HERE FROM public.css IN ENTRY [213], and the seam is the one this file's header already
 * draws: public.css is the FRAME, the shape of the page, and this file is the TYPE inside it.
 * The frame needed the room for the measures and the rhythm and the type had no business being
 * the thing that kept it under the cap. `--font-display` itself stays declared on `.c-public`,
 * because it is the frame's property and every rule below reads it from there.
 *
 * Set by the frame rather than by a class the page has to remember. A public page writes <h1>
 * and gets the serif; nothing else in the suite does, because nothing else is inside .c-public.
 *
 * Two weights and no more, which is exactly what is vendored: 500 for the h1, where the size
 * carries the emphasis and a heavier face at 40px reads as shouting, and 600 for the h2, where
 * the size no longer does.
 *
 * The h1 steps down to --text-2xl under 640px. 40px over a 360px viewport is about nine
 * characters to the line, which breaks a headline into a column.
 */
.c-public h1 {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--weight-medium);
  line-height: var(--leading-display);
  letter-spacing: normal;
}

@media (min-width: 640px) {
  .c-public h1 {
    font-size: var(--text-display);
  }
}

.c-public h2 {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
}

/* ---- the prose ----------------------------------------------------------------------- */

/*
 * The measure is in CHARACTERS and not pixels. 66ch is the readable line the typographic
 * literature settles on, and it holds at every viewport without a breakpoint, which is what a
 * pixel width would need three of.
 */
.c-public__lede {
  max-width: 66ch;
  margin-block: var(--space-5) 0;
  font-size: var(--text-md);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

@media (min-width: 640px) {
  .c-public__lede {
    font-size: var(--text-lg);
  }
}

.c-public__small {
  max-width: 66ch;
  margin-block: var(--space-4) 0;
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

/*
 * A link in the masthead or the footer. It is a real anchor with a 44px hit area, house rule 10,
 * and the underline arrives on hover rather than sitting there: a masthead of permanently
 * underlined words reads as a list of errors.
 *
 * ## THE 44 WAS ONLY THE HEIGHT, AND HALF A TARGET IS NOT A TARGET
 *
 * Entry [214]. `min-height` was here from the start and `min-width` was not, so the box was 44px
 * tall and as wide as the word. `public-foot.css` then sets `padding-inline: 0` and `--text-xs`
 * on the footer's copies, and at 12px the two shortest app names in the registry do not reach the
 * floor: measured on the live footer at 375, 768 and 1440, "LLMify" is 39px wide and "AskBot" is
 * 41. Fourteen screen and width pairs, every one of them a link a thumb has to find in a column
 * of seventeen.
 *
 * NOBODY SAW IT because `tools/touch-probe.js` was looking at `main button` and `main a.c-button`
 * and this is neither a button nor inside `main`. Widened in the same entry, and its header
 * carries what that widening found.
 *
 * `min-width` and not `padding-inline`, because padding would move the words: these sit in a
 * left aligned column and a footer of seventeen links indented by 3px each is a visible change to
 * fix an invisible one. A minimum on the box grows the target to the right, into the gutter the
 * column already has.
 */
.c-public__link {
  display: inline-flex;
  align-items: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  padding-inline: var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--ink-2);
}

.c-public__link:hover,
.c-public__link:focus-visible {
  color: var(--brand);
}

.c-public__foot-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* ---- a section, and what it is made of ----------------------------------------------- */

/*
 * THE LEAD OF A SCREEN: the h1 and the sentence under it, in one box, because they are one
 * section and not two. Without the box the flow rule in public.css cannot tell a heading from
 * an argument and puts the full 48px rhythm between a title and its own lede.
 *
 * The measure is the reading column and not the page. A 40px display headline set across the
 * whole 1180px runs to about 90 characters, which is a paragraph wearing a large face, and the
 * lede under it is capped at 66ch already, so the two would disagree about where a line ends.
 */
.c-lead {
  max-width: var(--c-public-read);
}

/*
 * A BAND IS A SECTION. It used to be the RHYTHM, one margin on one class, and every screen that
 * wanted a gap between two blocks reached for a different number instead. public.css owns the
 * rhythm now, so what is left here is what a band is on the inside: an eyebrow, a heading, a
 * lede, and the body they name.
 *
 * One step between the heading block and the body, whatever the body is, so the app page's six
 * feature cards, the four group cards, the plan row, the check list, the before and after, the
 * questions and the filter row on /apps all sit the same distance under their heading. Measured
 * before the change they were 24, 24, 28, 24, 24, 24 and 0, and the 0 is why the chips on /apps
 * looked stuck to the sentence above them.
 */
.c-band > * + * {
  margin-block-start: var(--c-public-step);
}

/* The eyebrow, the heading and the lede are one block and are set to read as one: tighter than
   the step, and the same two gaps on every screen that carries them. */
.c-band > .c-band__eyebrow + h2 {
  margin-block-start: var(--space-3);
}

.c-band > h2 + .c-public__lede {
  margin-block-start: var(--space-5);
}

/*
 * The check lists. A real <ul> with the marker replaced by a rule down the side rather than a
 * bullet glyph: at four items to a card the bullets were the loudest thing in the panel.
 *
 * The pass variant adds nothing but a colour, because the words already say "passed" in the
 * heading above it and colour is never the only signal. design.md section 7.
 */
.c-checklist {
  margin: 0;
  padding: 0;
  list-style: none;
}

.c-checklist > li {
  padding: var(--space-3) 0 var(--space-3) var(--space-5);
  border-left: var(--border-width) solid var(--line);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

.c-checklist--pass > li {
  border-left-color: var(--success);
}

/* ---- public-theme-fit.css ---- */
/*
 * public-theme-fit.css. What the shared library needs on THIS mount's grounds.
 *
 * ## IT WAS public-dark-fit.css UNTIL ENTRY [213], AND THE NAME STOPPED BEING TRUE
 *
 * It held five corrections for a dark ground. The site now ships two palettes, so a file called
 * "dark fit" would be half a lie: two of the five turned out to be corrections for THIS MOUNT
 * rather than for darkness, and only two are dark's alone. A file whose name says when its rules
 * apply, wrongly, is worse than one that has to be read. It also held the toggle's label, which
 * was never a fit for anything and left in entry [214]: see the note at the foot of the file.
 *
 * ## WHY IT IS A THEME SCOPED OVERRIDE AND NOT AN EDIT TO THE COMPONENT
 *
 * `mascot`, `verdict-strip` and `panel` are drawn for the twelve light mounts and are right there.
 * A second ground is one mount's problem and the theme layer is what the suite has for exactly
 * that: `.theme-public` sits on <body>, so these reach every component on this site and no
 * component anywhere else. Editing the components would move thirteen panels to fix one.
 *
 * Every rule below is a SINGLE property on a class the component already renders. None of them
 * changes a layout, a size or a state, so a component that gains a variant tomorrow gains it here
 * too without this file knowing.
 *
 * ## THE DARK ONLY RULES ARE WRITTEN TWICE, MECHANICALLY
 *
 * CSS cannot say "this selector OR this media condition" in one rule, and dark is exactly that:
 * the system asks and nothing overrides, or the reader asked. `themes/public.css` carries the same
 * duplication for the same reason and its header says so.
 */

/* ---- BOTH MODES. These correct for the MOUNT, not for the dark. ------------------------- */

/*
 * 1. The agent is a figure with a halo, not a figure on a plate.
 *
 * `c-mascot__glow` is a flat `<circle r="94">` at `--brand-subtle`, half opaque. On a white card
 * that reads as a soft tint under the robot. On the plum ground it read as a light coloured disc
 * with a hard edge, a sticker pasted on the page, and in LIGHT mode the mount's own
 * `--brand-subtle` is rgb(252 241 248), which against a rgb(248 242 251) ground is nothing at all.
 * The accent at a tenth is a halo on both: dark magenta over light, light magenta over dark.
 */
.theme-public .c-mascot__glow {
  fill: var(--brand);
  opacity: 0.1;
}

/*
 * 2. And the body sits ON the ground rather than on a lighter panel. `--surface` is right when the
 * figure stands on a card and one step wrong when it stands on the ground, which on this site is
 * where it always stands. `--surface-2` is a step AWAY from the ground in both modes, lighter in
 * dark and darker in light, so the figure separates either way.
 */
.theme-public .c-mascot__shell { fill: var(--surface-2); }

/*
 * 3. The ring is a ring, not a plate.
 *
 * `meter`'s ring covers the middle of its track with a `--surface` disc, drawn for a white card
 * where `--surface` IS the card and the disc is invisible. On this mount the ring always stands on
 * the GROUND: in dark that disc was a step lighter than what it sat on, and in light it is four
 * points lighter, which is a faint plate rather than a loud one. Painting it in the ground is
 * right in both, because the statement is "there is nothing behind the figure" either way.
 *
 * IT IS THE GROUND AND NOT `transparent`, which was the first answer and was wrong. The disc is
 * absolutely positioned INSIDE the track, so transparent shows the track's own conic gradient
 * through it: the hole is cut by painting over, not by declining to paint. Measured, not assumed.
 */
.theme-public .c-meter--ring .c-meter__fill {
  background: var(--ground);
}

/* And thin, on the scan screen only. 16px of stroke on a 150px circle is a wheel; the report's
   gauge is a different job, it is the score itself, and it keeps its weight. */
.theme-public .c-stage__hold .c-meter--ring .c-meter__fill {
  inset: var(--space-3);
}

/* ---- DARK ONLY. Both of these are correct as drawn on a light ground. -------------------- */

/*
 * 4. Solid lines, everywhere.
 *
 * The idle check square. `verdict-strip` draws it hollow with a dashed edge, and its own header
 * gives the reason: the SHAPE says "no measurement" so the colour does not have to. That reads on
 * white and it reads on this mount's light plum. Eighteen dashed outlines on a DARK ground read as
 * eighteen unfinished boxes, so there, and only there, a filled muted block says the same thing:
 * visibly not one of the three verdict colours, and visibly a square rather than an outline of one.
 *
 * 5. And a panel has an edge in dark, which on a light ground it does not need. `panel` separates
 * itself with `--shadow-sm`, and a shadow is a darkening: on a plum ground it lands on something
 * already darker than itself and does nothing, and every card on /apps was a floating rectangle
 * with no boundary. On the light palette the shadow works exactly as it does on the other thirteen
 * mounts, so the hairline would be a second boundary doing the first one's job.
 *
 * THE BORDER IS FULLY STATED, and the mechanism is worth the line. `.c-panel` declares no `border`
 * at all, so the first pass at this set `border-style: solid` on its own and CSS filled the other
 * two from their initial values: `medium` and `currentColor`. Thirteen cards drew a 3px border in
 * the ink colour. A border-style with no width and no colour beside it is not an override, it is a
 * new border.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-public .c-verdict-strip__cell--idle {
    background: var(--surface-3);
    border-style: solid;
    border-color: var(--surface-3);
  }

  :root:not([data-theme="light"]) .theme-public .c-panel {
    border: var(--border-width) solid var(--line);
  }
}

:root[data-theme="dark"] .theme-public .c-verdict-strip__cell--idle {
  background: var(--surface-3);
  border-style: solid;
  border-color: var(--surface-3);
}

:root[data-theme="dark"] .theme-public .c-panel {
  border: var(--border-width) solid var(--line);
}

/* ---- AND THE TOGGLE IS NO LONGER HERE ------------------------------------------------------ */

/*
 * Entry [214]. This file used to end with three rules that showed one of the toggle's three
 * words and hid the other two. The owner ruled the words out on 2026-09-10 and the control became
 * three icon buttons with a selected state, which is a whole control rather than a correction, so
 * it left for `public-themeset.css`.
 *
 * The mechanism did not change and is worth naming here, because it is what every rule above is
 * NOT: those five are this mount's corrections to SHARED components on grounds they were not
 * drawn for. The toggle is this mount's own control and always was. It was here because it was
 * small, and small is not a seam.
 */

/* ---- public-themeset.css ---- */
/*
 * public-themeset.css. The masthead's three theme controls. Entry [214], task 5.143.
 *
 * `parts/theme-toggle.php` is the markup and `js/components/public-theme.js` is the behaviour.
 * This file draws the control and, more importantly, says which of the three is ON.
 *
 * ## IT IS ONE FILE BECAUSE IT IS ONE CONTROL
 *
 * The label rules used to sit at the bottom of `public-theme-fit.css`, which is otherwise a set
 * of corrections this MOUNT makes to shared components on its own grounds. The toggle is not a
 * correction to anything, and it grew from three <span>s to a group of three buttons with a
 * selected state in entry [214]. A file whose header says "what the shared library needs on this
 * mount's grounds" is the wrong place for a control this mount owns outright, and it was already
 * the wrong place when it held three spans.
 *
 * ## THE SELECTED STATE IS READ OFF <html>, WHICH IS WHERE THE PALETTE READS IT
 *
 * The rule at the bottom is the same shape the label rules were, and it is the whole reason this
 * control needs no script to look right. `themes/public.css` decides the colours off `data-theme`
 * on the root element; this decides which control is filled off the SAME attribute. They cannot
 * disagree, because there is nothing to keep in step: it is one attribute with two readers.
 *
 * So the correct control is filled in the first paint, before `app.js` has loaded, on a page
 * where JavaScript is blocked, and in the same frame the attribute changes. The alternative, a
 * script adding a class, is a second copy of the state and a visible correction after the page
 * has painted. `aria-pressed` is the one thing the script does write, and public-theme.js's
 * header says why that one cannot be done here.
 *
 * NO ATTRIBUTE IS THE THIRD STATE and not a missing one. `layouts/public.php` documents it: an
 * explicit choice stamps `data-theme`, and no choice stamps nothing so `prefers-color-scheme`
 * answers. `:not([data-theme])` is that state, exactly.
 */

/*
 * A SEGMENTED SET AND NOT THREE LOOSE BUTTONS. The three are one decision, so they sit in one
 * pill: the container carries the edge and the ground, and the buttons inside carry nothing until
 * one of them is on. Three separately bordered controls in a masthead read as three unrelated
 * actions, which is what a reader would have to test one at a time to disprove.
 */
.c-themeset {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
}

/*
 * 44px, house rule 10, and it is the box rather than the painted pill that has to be 44. That is
 * the trade `icon-button.css` records for the header's 32px circle and `secret.css` for its 38px
 * column: the picture is what the design asks for and the target is what a thumb needs. Here they
 * are the same size, because a masthead has the room and three circles in a row with gaps between
 * them are easier to hit apart than three 44px boxes touching.
 *
 * `position: relative` is load bearing and not tidiness. The name inside each control is a `.l-sr`
 * span, which is `position: absolute`, and an absolutely positioned element with no positioned
 * ancestor resolves against the initial containing block instead of being clipped. `table.css`
 * carries the measurement of what that costs: fifty such spans once added 3128px of empty
 * scrollable overflow to /admin/jobs.
 */
.c-themeset__btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  flex: 0 0 auto;
  padding: 0;
  border-radius: var(--radius-pill);
  color: var(--ink-3);
  transition: background var(--duration-base) var(--ease), color var(--duration-base) var(--ease);
}

/*
 * The glyph is 20px, which is `icon-button.css`'s 1.25rem and the size every other icon in a
 * control on this site is drawn at. `stroke-width` is the sprite's own 1.5 and is not restated:
 * the symbols carry it, and a value here would be a second opinion about heroicons.
 */
.c-themeset__icon {
  width: var(--space-6);
  height: var(--space-6);
  display: block;
}

.c-themeset__btn:hover,
.c-themeset__btn:focus-visible {
  color: var(--brand);
}

/*
 * ON. The filled pill is `--brand` with `--brand-ink` on it, which is the pair the library
 * already uses for a control in its active state (`.c-iconbtn[aria-expanded="true"]`) and the one
 * pair `themes/public.css` solves in BOTH palettes: white on the light magenta at 5.05, and the
 * near black on the dark magenta at 5.65. Both are text ratios and this is a 1.5px stroke, which
 * needs 3.0, so it clears in both with room.
 *
 * Colour is not the only signal. The filled pill is a SHAPE where the other two have none, which
 * is the same reasoning `verdict-strip.css` gives for drawing its idle cell hollow.
 */
:root:not([data-theme]) .theme-public .c-themeset__btn[data-theme-when="system"],
:root[data-theme="light"] .theme-public .c-themeset__btn[data-theme-when="light"],
:root[data-theme="dark"] .theme-public .c-themeset__btn[data-theme-when="dark"] {
  background: var(--brand);
  color: var(--brand-ink);
}

@media (prefers-reduced-motion: reduce) {
  .c-themeset__btn { transition: none; }
}

/* ---- public-waiting.css ---- */
/*
 * public-waiting.css. The three states of /report/{token} before there is a report: queued,
 * scanning, and stalled.
 *
 * Split from `public-report.css` in entry [211] on the seam those two already had. That file is
 * the REPORT: a score, four groups, eighteen findings and a cross sell. This is the page a
 * person is looking at while there is none of that, which is a different screen at the same
 * address and shares no element with it but the masthead.
 *
 * ## THERE IS ONE RULE LEFT IN IT, AND THAT IS THE POINT OF ENTRY [213]
 *
 * It held four. `.c-waiting` was a 46rem measure and a `margin-inline`, which is the reading
 * column, written here for the third time on this site: `.c-report` said 52rem and the entry
 * column said 46rem again. All three read `--c-public-read` from the frame now and the wrapper
 * on this screen is `.c-public__column`.
 *
 * `.c-waiting__state` and `.c-waiting__note` were dead, and one of them had been dead in a way
 * nothing could see: a stray line of shell heredoc marker, `OLD_MARKER`, sat between the closing
 * brace above them and the selector, so the browser parsed the pair as one descendant selector
 * and `.c-waiting__state` had not applied since it was written. Nothing renders either class, so
 * the fix and the cut are the same edit. `.c-waiting__strip` moved to `public-stage.css` as
 * `.c-stage__strip`, because the stage is what draws it.
 */

/* ---- the waiting room ---------------------------------------------------------------- */

/*
 * THE MEASURE IS A PROPERTY OF THE STATE, AND IT IS THE ONLY LAYOUT DIFFERENCE BETWEEN THEM.
 *
 * 46rem was the default here and the scan stage opted out of it with `--wide`, which is the safer
 * way round for a page that is mostly prose and the wrong way round for a page that is mostly
 * panes: the default was a number, and the exception was a second number, and neither said what
 * kind of screen it was describing.
 *
 * The stalled room and the queued one are DOCUMENTS, a heading over a notice over a panel, and
 * they take the frame's reading column. The scanning state is two panes side by side and takes
 * the page, which is what `report-running.php` renders when this class is absent. So the state
 * carries the measure, the measure is the frame's own token, and there is nothing here to
 * disagree with the report about.
 */
.c-waiting--stalled {
  max-width: var(--c-public-read);
  margin-inline: auto;
}

/* ---- public-wire.css ---- */
/*
 * public-wire.css. The site preview with its sweep, and the list of files opened. The findings
 * feed and the probe rail are `public-panes.css` since entry [226].
 *
 * Entry [210], task 5.138, from the v2 mockup's screen 2. Split from `public-stage.css` on the
 * seam that file names: the STAGE is the agent and the frame around the panes, and this is what goes
 * inside them.
 */

/* ---- the site preview, and the sweep ------------------------------------------------------ */

/*
 * A SKELETON AND NEVER A SCREENSHOT. We read a page's source, we do not render it, so anything
 * that looked like the customer's actual page would be a picture of something nobody took. Grey
 * blocks say "a page" and claim nothing about which one.
 */
.c-preview {
  position: relative;
  height: 13.25rem;
  overflow: hidden;
  background: var(--ground);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
}

.c-preview__bar {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  height: 1.375rem;
  padding-inline: var(--space-3);
  background: var(--surface-3);
}

.c-preview__dot {
  width: 0.375rem;
  height: 0.375rem;
  border-radius: var(--radius-full);
  background: var(--line);
}

.c-preview__addr {
  flex: 1;
  height: 0.5625rem;
  margin-left: var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--line);
}

.c-preview__page {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
}

.c-preview__block {
  border-radius: var(--radius-md);
  background: var(--surface-3);
}

.c-preview__block--head { height: 1rem; width: 56%; }
.c-preview__block--text { height: 0.5rem; width: 78%; }
.c-preview__block--text2 { height: 0.5rem; width: 64%; }

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

.c-preview__card {
  height: 3.25rem;
  border-radius: var(--radius-lg);
  background: var(--surface-3);
}

/*
 * The sweep. Decoration, and the one thing on this screen that moves without a measurement behind
 * it: it says a scan is happening, which the state already says in words beside it. It carries no
 * position, no count and no verdict, so there is nothing for it to be wrong about.
 */
.c-preview__sweep {
  position: absolute;
  inset-inline: 0;
  top: -4.625rem;
  height: 4.625rem;
  pointer-events: none;
  background: linear-gradient(180deg, transparent, var(--brand-subtle) 62%,
              var(--brand-accent) 97%, transparent);
  animation: c-preview-sweep 2600ms linear infinite;
}

@keyframes c-preview-sweep {
  from { top: -4.625rem; }
  to { top: 13.25rem; }
}

/* ---- the files opened --------------------------------------------------------------------- */

/* No height cap since entry [226]: the pane fills the height the files need, and the check pane
   beside it is eighteen rows tall from the first paint, so the two hold each other level. */
.c-wire {
  margin: 0;
  padding: 0;
  list-style: none;
}

.c-wire__row {
  display: flex;
  gap: var(--space-3);
  align-items: baseline;
  padding-block: var(--space-2);
  font-size: var(--text-xs);
  border-bottom: var(--border-width) solid var(--surface-3);
}

.c-wire__status {
  flex: none;
  font-family: var(--font-mono);
  font-weight: var(--weight-medium);
  color: var(--success);
}

.c-wire__status--no { color: var(--danger); }

.c-wire__url {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  font-family: var(--font-mono);
  color: var(--ink-2);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The size is the quietest thing on the row, entry [226]: the owner's word was that the file rows
   read louder than the checks, and the number was the loud part. */
.c-wire__bytes {
  flex: none;
  font-size: var(--text-2xs);
  color: var(--ink-3);
}

/* The probe the fetch belonged to: a small pill on the left, from the fetch row's own column. */
.c-wire__phase {
  flex: none;
  min-width: 4.25rem;
  padding: 0 var(--space-2);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semi);
  letter-spacing: var(--tracking-wide);
  color: var(--ink-3);
  background: var(--surface-2);
  border-radius: var(--radius-pill);
}

/* ---- rack.css ---- */
/*
 * rack.css. The channel rack on the overview screen.
 *
 * The root also carries .l-rail, which owns the flex row, the gap, the horizontal
 * scroll and the scroll snapping. Nothing here repeats any of it. This file styles
 * the frame around the strip and one channel.
 *
 * No template equivalent, so a channel is derived: it is the KPI tile from
 * statistics-widgets.html:2336, which is p-4 (1rem) on a rounded surface, wearing
 * .card's radius (0.375rem, app.css 11252) and shadow-base (app.css 7418). The tile's
 * own type carries over too: the label at 0.875rem weight 500 and the figure at
 * 1.125rem weight 500. Phase 2 had the figure at --text-lg with no weight and the
 * name in uppercase --text-base, which is the prototype's masthead, not the tile.
 */

.c-rack {
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface-2);
  /* Sides and top only. The bottom padding is l-rail's, and it is what keeps the
     horizontal scrollbar off the last channel. Do not turn this into a shorthand. */
  padding-top: var(--space-4);
  padding-right: var(--space-4);
  padding-left: var(--space-4);
}

.c-rack__channel {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* 8rem. Narrow enough that twelve apps read as one instrument, wide enough for a
     twelve bar meter under the app name once p-4 is taken off both sides. */
  min-width: calc(var(--space-11) + var(--space-11));
  /* p-4 on three sides. The left side carries the identity stripe, so it takes the
     next step up and the text still starts on a straight edge. */
  padding: var(--space-5) var(--space-5) var(--space-5) var(--space-6);
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  transition: background var(--duration-fast) var(--ease);
}

.c-rack__channel:hover { background: var(--surface-3); }

/* The identity stripe. A semantic tone, never a per app hex. */
.c-rack__strip {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: var(--space-1);
  border-top-left-radius: var(--radius-lg);
  border-bottom-left-radius: var(--radius-lg);
  background: var(--muted);
}

.c-rack__strip--brand   { background: var(--brand); }
.c-rack__strip--success { background: var(--success); }
.c-rack__strip--warning { background: var(--warning); }
.c-rack__strip--danger  { background: var(--danger); }
.c-rack__strip--info    { background: var(--info); }
.c-rack__strip--ai      { background: var(--ai); }
.c-rack__strip--mute   { background: var(--muted); }

/* Stacked, not a row: lamp renders its state as a word, and a word plus an app name
   side by side will not fit a channel this narrow. */
.c-rack__top {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  min-width: 0;
  max-width: 100%;
}

/* The tile's label: 0.875rem, weight 500. */
.c-rack__name {
  font-family: var(--font-cond);
  font-weight: var(--weight-medium);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

a.c-rack__name:hover { color: var(--brand-hover); }

.c-rack__meter { min-width: 0; }

.c-rack__read {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-top: auto;
}

/* The tile's figure: 1.125rem, weight 500. Monospace, because it is a figure and a
   column of figures is read down rather than across. */
.c-rack__value {
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
  color: var(--ink);
}

.c-rack__note {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.c-rack__tag { display: flex; }

/* House rule 10. Below the small breakpoint this component's arrangement stops working. */
@media (max-width: 640px) {
  .c-rack__channel { min-width: 100%; }
  .c-rack { padding-bottom: var(--space-3); }
}

/* ---- reality-band.css ---- */
/*
 * reality-band.css. The navy band the overview opens with. Entry [237], from the approved
 * Sitemapify file's `.reality`, `.reality-top`, `.reality-actions`. The facts row is
 * reality-band.facts.css, split as specced because the two together cross the 200 line cap.
 *
 * ## Colour
 *
 * The ground is --brand and a gradient through --brand-hover to a third, lighter stop, the
 * approved file's three (its #1B4A80 end is not a token, so the third stop is the hover navy
 * mixed a fifth of the way to --brand-border, which lands within four units of it). Entry
 * [243], the owner's ruling on the third list's C7; until then the gradient ran on the two. The lit figure is --brand-accent, 6.22:1 on the navy, the one
 * per app value and the reason the band is the one place the accent carries a number (5.4:1
 * at the gradient's light end, still over 3:1 for 24px text). Text on the navy is --brand-ink
 * at two opacities: full for the headline, 0.72 for the lede, which the approved file states
 * and which measures 8.1:1 on --brand and 7.2:1 on --brand-hover; the facts file uses 0.62 for
 * a note, 6.4:1 and 5.8:1.
 *
 * The ghost button (the second action) is white at 10 per cent with a 26 per cent border, the
 * approved file's own, and both are rgb() of --brand-ink rather than a new colour.
 */
.c-reality-band {
  padding: var(--space-7) var(--space-8);
  margin-bottom: var(--space-5);
  color: var(--brand-ink);
  background: linear-gradient(135deg, var(--brand) 0%, var(--brand-hover) 55%,
    color-mix(in srgb, var(--brand-hover) 80%, var(--brand-border)) 100%);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}

.c-reality-band__top {
  display: flex;
  align-items: flex-start;
  gap: var(--space-6);
  margin-bottom: var(--space-7);
}

.c-reality-band__figure-mark { flex: none; }

/*
 * Basis 0, not auto: with auto the words' basis is their max-content width, a 64ch lede, and
 * at 768 that is wider than the row minus the mascot, so the words wrapped under the figure.
 * At basis 0 they share the line with the mascot at any width and shrink into it, and the
 * floor keeps a phone from squeezing the sentence into a column beside the figure.
 */
.c-reality-band__words {
  flex: 1 1 0;
  min-width: 14rem;
}

.c-reality-band__eyebrow {
  margin: 0 0 var(--space-3);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--action-border);
}

.c-reality-band__headline {
  margin: 0;
  max-width: 32ch;
  font-size: var(--text-3xl);
  font-weight: var(--weight-heavy);
  line-height: var(--leading-tight);
  letter-spacing: -0.03em;
  color: var(--brand-ink);
}

.c-reality-band__lit {
  font-style: normal;
  color: var(--brand-accent);
}

.c-reality-band__lede {
  margin: var(--space-3) 0 0;
  max-width: 64ch;
  font-size: var(--text-sm);
  line-height: var(--leading-loose);
  color: rgb(255 255 255 / 0.72);
}

.c-reality-band__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  flex: none;
  min-width: 12rem;
}

/* Any action after the first is the ghost: the same button, on glass. */
.c-reality-band__action--ghost .c-button {
  background: rgb(255 255 255 / 0.1);
  border-color: rgb(255 255 255 / 0.26);
  color: var(--brand-ink);
}

.c-reality-band__action--ghost .c-button:not([disabled]):hover {
  background: rgb(255 255 255 / 0.18);
  color: var(--brand-ink);
}

@media (max-width: 1180px) {
  .c-reality-band__top { flex-wrap: wrap; }

  .c-reality-band__actions {
    flex-direction: row;
    width: 100%;
    min-width: 0;
  }

  .c-reality-band__action { flex: 1 1 0; }
}

@media (max-width: 900px) {
  .c-reality-band { padding: var(--space-6); }

  .c-reality-band__headline { font-size: var(--text-xl); }
}

@media (max-width: 640px) {
  .c-reality-band__actions { flex-direction: column; }
}

/* ---- reality-band.facts.css ---- */
/*
 * reality-band.facts.css. The facts row under the band's sentence. Entry [237], from the
 * approved Sitemapify file's `.facts` and `.fact`. Split from reality-band.css as specced,
 * the seam being the row itself: everything here is reached by the `facts` prop.
 *
 * The grid is auto-fit and not repeat(3). The approved file drew three facts and the build
 * ships two, and a fixed three with two cells filled is a hole a reader counts. Each cell has
 * a floor of 14rem so three still fit at 1180 and drop to one column at a phone width.
 *
 * The cells are glass on the navy: a 1px gap of white at 14 per cent draws the rules, and each
 * cell is white at 6 per cent, both the approved file's own figures and both rgb() of
 * --brand-ink. The lit figure is the accent, the plain one is white.
 */
.c-reality-band__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--border-width);
  margin: 0;
  padding: 0;
  list-style: none;
  background: rgb(255 255 255 / 0.14);
  border-radius: var(--radius-md);
  overflow: hidden;
}

/* One column under 1180, the approved file's own break for its facts (entry [243]; auto-fit at
   14rem had kept two across down to 768). */
@media (max-width: 1180px) {
  .c-reality-band__facts { grid-template-columns: minmax(0, 1fr); }
}

.c-reality-band__fact {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-6);
  background: rgb(255 255 255 / 0.06);
  min-width: 0;
}

.c-reality-band__fact-figure {
  font-size: var(--text-4xl);
  font-weight: var(--weight-heavy);
  line-height: 1.05;
  letter-spacing: -0.035em;
  font-variant-numeric: tabular-nums;
  color: var(--brand-ink);
}

.c-reality-band__fact-figure--accent { color: var(--brand-accent); }

.c-reality-band__fact-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  line-height: var(--leading-base);
  color: var(--brand-ink);
}

.c-reality-band__fact-note {
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: rgb(255 255 255 / 0.62);
}

/* ---- review.css ---- */
/*
 * review.css. "Rate Sitemapify" at the foot of the sidebar and the review modal it opens.
 * Entry [291], built to the REVIEW MODAL of docs/appslist/sitemapify/sitemapify-ui-fixes.html.
 * Markup: shared/views/pages/sitemapify/review-modal.php. Behaviour: js/components/review.js.
 *
 * The box is the shared dialog (dialog.css); the 1 to 10 row is the shared scale (scale.css) of
 * shared buttons at their default size, so every score keeps rule 10's 44px target. This file
 * owns what the approved file adds: the dashed sidebar row, the ends under the scale, the
 * follow-up slot, the foot, and the thank-you step.
 */

/*
 * THE ROW ABOVE THE PLAN BOX. nav.php prints a mount's aside inside one `.c-nav__aside`, which
 * nav.aside.css frames as the plan box. ReviewChrome wraps the plan in its own `.c-nav__aside`,
 * so the outer one gives its box away here and the row and the plan box become two children of
 * the sidebar foot, one above the other, each with its own frame. No frame is restated.
 */
.c-nav__aside:has(> .c-review__rate) { display: contents; }

.c-review__rate {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-4);
  border: var(--border-width) dashed var(--line);
  border-radius: var(--radius-md);
  background: none;
  color: var(--ink-2);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.c-review__rate:hover {
  border-color: var(--brand-border);
  background: var(--brand-subtle);
  color: var(--brand);
}

/* [296]: the tint behind a hovered row took --ink-3 from 5.17:1 to 4.32:1, a hover that made the
   words harder to read. One ink step darker on the tint is 6.33:1, above its own resting figure. */
.c-review__rate:hover .c-review__rate-note { color: var(--ink-2); }

.c-review__rate:focus-visible {
  outline: var(--focus-width) solid var(--brand);
  outline-offset: var(--focus-offset);
}

.c-review__star {
  flex: none;
  width: var(--space-6);
  height: var(--space-6);
  fill: none;
  stroke: var(--brand-accent); /* the file's orange star, the app's own accent */
  stroke-width: 1.6;
  stroke-linejoin: round;
}

.c-review__rate-words { display: flex; flex-direction: column; min-width: 0; }

.c-review__rate-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
}

.c-review__rate-note {
  font-size: var(--text-xs);
  color: var(--ink-3);
}

/* The rail hides the plan box whole, and the row goes with it: a dashed box in 64px says nothing. */
.l-shell--rail .c-review__rate { display: none; }

/*
 * STEP 1. The two forms share one grid: the answer form lays its children out in it (display:
 * contents), and the dismiss form takes the foot's left cell, so "Not now" sits left and Send
 * right as the file draws them, while each button stays inside the form it submits.
 */
.c-review__ask {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "scale scale" "ends ends" "follow follow" "later send";
  align-items: center;
}

.c-review__ask[hidden] { display: none; }

.c-review__form { display: contents; }

/* Ten 44px targets and their gaps need 476px inside the card's padding: the review box is wider than
   the spoken dialog's 30rem, and still 100% less the gutter on a phone (dialog.css). */
.c-dialog:has(.c-review) { max-width: 34rem; }

/* A grid rather than scale.css's wrapping row: ten equal cells, two rows of five on a phone. Scoped
   under .c-review because scale.css is emitted after this file and would win at equal weight. */
.c-review .c-review__scale {
  grid-area: scale;
  display: grid;
  grid-template-columns: repeat(10, minmax(0, 1fr));
  gap: var(--space-2);
  margin-top: 0;
}

.c-review__scale .c-button {
  min-width: 0;
  padding-left: 0;
  padding-right: 0;
}

/* The chosen score: the file's navy fill, one at a time, so it is never six primaries in a row. */
.c-review__scale .c-button[aria-pressed="true"] {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-ink);
}

.c-review__ends {
  grid-area: ends;
  display: flex;
  justify-content: space-between;
  margin-top: var(--space-3);
  font-size: var(--text-xs);
  color: var(--ink-3);
}

.c-review__follow { grid-area: follow; }
.c-review__follow:not(:empty) { margin-top: var(--space-6); }

.c-review__later { grid-area: later; margin-top: var(--space-7); }

.c-review__send {
  grid-area: send;
  justify-self: end;
  margin-top: var(--space-7);
}

/* Under the small breakpoint ten cells would be 26px wide, under the 44px target: two rows of five. */
@media (max-width: 640px) {
  .c-review .c-review__scale { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

/*
 * STEP 2, the thanks. The dialog's own heading and figure belong to step 1, so they go while it
 * shows; review.js sets data-review-step on the dialog and points its label at this heading.
 */
.c-dialog[data-review-step="thanks"] .c-dialog__top { display: none; }
.c-dialog[data-review-step="thanks"] .c-dialog__slot { margin-top: 0; }

.c-review__thanks { text-align: center; padding-top: var(--space-5); }

.c-review__tick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--touch-min);
  height: var(--touch-min);
  margin-bottom: var(--space-5);
  border-radius: var(--radius-full);
  background: var(--success-subtle);
  color: var(--success-ink);
}

.c-review__tick svg { width: var(--space-7); height: var(--space-7); fill: currentColor; }

.c-review__thanks-title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  color: var(--ink);
}

.c-review__thanks-text {
  margin: var(--space-3) auto 0;
  max-width: 34ch;
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

.c-review__foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-4);
  margin-top: var(--space-7);
}

@media (max-width: 640px) {
  .c-review__foot > * { flex: 1 1 100%; }
}

/* ---- scale.css ---- */
/*
 * scale. A row of equal answers, 1 to 10, that wraps on a phone. Entry [287]: the review prompt
 * (shared/views/pages/sitemapify/review-modal.php, which lays the row out as a grid in review.css). The buttons are the shared button component at
 * its default size, so each keeps rule 10's 44px touch target; this only lays them out.
 */
.c-scale {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

/* ---- search.css ---- */
/*
 * search. Input plus an optional submit, laid out with flex.
 *
 * This is the real input group shape from the template (a flex row with an append
 * slot), not the demo page that fakes one with absolute positioning. The only thing
 * positioned absolutely is the decorative magnifier inside the box, which is the
 * documented left icon pattern and negotiates no width with anything.
 *
 * The input carries .form-control's metrics, app.css 9534: 38px, 0.5rem by 0.75rem,
 * 0.25rem radius. The left padding is the same 0.75rem plus the icon and a gap, so
 * the text starts clear of the magnifier instead of under it.
 *
 * No fixed width. This component goes in a panel's tools slot and lets the container
 * decide how wide it is.
 */

.c-search {
  display: flex;
  align-items: stretch;
  width: 100%;
  max-width: 100%;
}

/*
 * The focus ring sits on this wrapper rather than on the input, which is how the
 * template's .inputGroup:focus-within works at app.css 10298: one ring around the
 * whole group, so an append slot cannot be left outside it.
 */
.c-search__box {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  border-radius: var(--radius-md);
  transition: box-shadow var(--duration-fast) var(--ease);
}

.c-search__box:focus-within {
  box-shadow: 0 0 0 var(--border-width) var(--brand);
}

.c-search__icon {
  position: absolute;
  left: var(--input-pad-x);
  top: 50%;
  width: var(--space-5);
  height: var(--space-5);
  transform: translateY(-50%);
  fill: none;
  stroke: var(--ink-3);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  pointer-events: none;
}

.c-search__input {
  display: block;
  width: 100%;
  min-height: var(--input-h);
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  padding:
    var(--input-pad-y)
    var(--input-pad-x)
    var(--input-pad-y)
    calc(var(--input-pad-x) + var(--space-5) + var(--space-3));
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink);
  transition: border-color var(--duration-fast) var(--ease);
}

.c-search__input::placeholder {
  color: var(--ink-3);
  font-weight: var(--weight-normal);
}

.c-search__input:hover:not([disabled]) {
  border-color: var(--muted-dim);
}

.c-search__input:focus {
  border-color: var(--brand);
  outline: none;
}

.c-search__box:focus-within .c-search__icon,
.c-search__input:not(:placeholder-shown) ~ .c-search__icon {
  stroke: var(--ink-2);
}

/*
 * The append slot. Square corners where it meets the input, so they read as one
 * control, and the same 38px height so the seam is straight.
 */
.c-search__submit {
  flex: 0 0 auto;
  min-height: var(--input-h);
  margin-left: calc(var(--border-width) * -1);
  background: var(--surface-2);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  padding: var(--input-pad-y) var(--control-pad-x);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  line-height: var(--leading-base);
  color: var(--ink-2);
  transition:
    background-color var(--duration-fast) var(--ease),
    border-color var(--duration-fast) var(--ease),
    color var(--duration-fast) var(--ease);
}

.c-search__submit:hover:not([disabled]) {
  background: var(--surface-3);
  color: var(--ink);
}

.c-search__submit:active:not([disabled]) {
  background: var(--line);
}

/* The addon answers the input's focus, app.css 10298. */
.c-search:focus-within .c-search__submit {
  border-color: var(--brand);
}

.c-search--has-submit .c-search__box,
.c-search--has-submit .c-search__input {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

/*
 * Disabled, entry [068]. field and select already had this treatment and search did not,
 * because until [068] the component had no prop that could reach the state.
 *
 * Three parts, because search is three elements pretending to be one control: the input
 * takes field.css's exact disabled treatment, the submit dims to button.css's 0.34, and the
 * icon goes with them. Dimming only the input would leave a bright magnifier and a live
 * looking button attached to a dead box, which reads as a control that is half working
 * rather than one that is off.
 *
 * Hover and active are scoped :not([disabled]) at their own rules above rather than reset
 * here, which is the same choice and the same reason as button.css:91.
 */
.c-search__input[disabled] {
  background: var(--surface-2);
  color: var(--ink-3);
  border-color: var(--line-soft);
  cursor: not-allowed;
}

.c-search--disabled .c-search__icon {
  opacity: 0.34;
}

.c-search__submit[disabled] {
  opacity: 0.34;
  cursor: not-allowed;
}

/* ---- secret.css ---- */
/*
 * secret. A one line credential readout that fits inside a table cell.
 *
 * No template equivalent, so the metrics are derived from .form-control (app.css
 * 9534): it occupies an input's slot in the cell, so it takes the input's 0.875rem
 * type in a 1.25rem line box and the input's 0.75rem horizontal rhythm as its gap.
 *
 * The slot's own height is --input-h (38px), but this one holds a control a person
 * has to hit, so the row stands at --touch-min instead. It is set on the root rather
 * than on the button alone, so a "not set" cell and a "masked" cell are the same
 * height down a column.
 *
 * Every state is a word as well as a shade, so a person who cannot tell the greys
 * apart still reads "not set" or "hidden" and knows which one this is.
 */

.c-secret {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  min-height: var(--touch-min);
  gap: var(--input-pad-x);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
}

.c-secret [hidden] {
  display: none;
}

/* not set and hidden. Both are absences, so both stay quiet. */
.c-secret__state {
  color: var(--ink-3);
}

.c-secret__mask {
  color: var(--ink-2);
  letter-spacing: var(--tracking-caps);
}

/*
 * The plaintext. Breaks anywhere, because a key is one long unspaced string and a
 * cell that will not break it pushes the whole table sideways.
 */
.c-secret__value {
  color: var(--ink);
  word-break: break-all;
  -webkit-user-select: all;
  user-select: all;
}

/* .btn type (app.css 8731): 0.875rem at weight 600. No text-transform, entry [047]. */
.c-secret__button {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  line-height: var(--leading-base);
  color: var(--brand);
  padding: var(--space-0);
  border-bottom: var(--border-width) solid transparent;
  transition: color var(--duration-fast) var(--ease), border-color var(--duration-fast) var(--ease);
}

.c-secret__button:hover {
  color: var(--brand-hover);
  border-bottom-color: var(--brand-border);
}

.c-secret__button[disabled] {
  color: var(--ink-3);
  border-bottom-color: transparent;
}

/* A refusal or a failure. Red, and a full sentence saying what to do next. */
.c-secret__message {
  flex-basis: 100%;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--danger);
}

/* ---- select.css ---- */
/*
 * select. Two shapes of the same control.
 *
 * form: label above, full width, sized like a field so a form reads as one column.
 * bar:  label beside, compact, the shape the top bar env and role switchers need.
 *
 * The template styles a select as .form-control (app.css 9534), so the form variant
 * carries exactly the field's metrics: 38px, 0.5rem by 0.75rem, 0.25rem radius.
 *
 * The native arrow is kept, which is why there is no background image here and no
 * hardcoded colour with it. color-scheme tells the browser to draw the control and
 * its dropdown in the light palette the rest of the suite uses.
 */

.c-select {
  display: block;
}

.c-select__label {
  display: block;
  width: 100%;
  margin-bottom: var(--space-3);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-control);
  color: var(--ink-2);
  cursor: pointer;
}

.c-select__control {
  display: block;
  width: 100%;
  min-height: var(--input-h);
  color-scheme: light;
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--input-pad-y) var(--input-pad-x);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink);
  cursor: pointer;
  transition:
    border-color var(--duration-fast) var(--ease),
    box-shadow var(--duration-fast) var(--ease);
}

.c-select__control:hover:not([disabled]) {
  border-color: var(--muted-dim);
}

/* The same 1px ring the field gets, app.css 568. */
.c-select__control:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 var(--border-width) var(--brand);
  outline: none;
}

.c-select__control[disabled] {
  background: var(--surface-2);
  color: var(--ink-3);
  border-color: var(--line-soft);
  cursor: not-allowed;
}

/*
 * Bar. One row, the label beside the control, the whole thing sized to its content
 * so it can sit in a top bar or in a panel's tools slot without pushing anything
 * around. It keeps the 38px control height so it lines up with a search box next to
 * it, and the border moves to the wrapper so label and control read as one control.
 */
.c-select--bar {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--input-h);
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--space-0) var(--input-pad-x);
  transition: box-shadow var(--duration-fast) var(--ease);
}

/*
 * Focus sits on the WRAPPER, not on the control, which is how the template's
 * .inputGroup:focus-within works at app.css 10298. Without it the ring would be
 * drawn inside the border the wrapper already carries.
 */
.c-select--bar:focus-within {
  box-shadow: 0 0 0 var(--border-width) var(--brand);
  border-color: var(--brand);
}

.c-select--bar .c-select__label {
  width: auto;
  margin-bottom: var(--space-0);
  font-size: var(--text-xs);
  color: var(--ink-3);
  white-space: nowrap;
}

/*
 * align-self: stretch, entry [064]. The bar is --input-h tall and the <select> inside it was
 * only as tall as its own text, 18px, so the thing a thumb actually has to hit was 18px in a
 * 44px box: tapping the bar anywhere above or below the words hit the wrapper and nothing
 * happened. min-height stays 0 because the stretch is what sets the height now, and the bar
 * still owns the border and the padding.
 */
.c-select--bar .c-select__control {
  width: auto;
  align-self: stretch;
  /* Over the border, not inside it. Stretch alone gives the content box, which is --input-h
     less the bar's two 1px borders: 42px against a 44px floor. The select paints nothing of
     its own, so covering the border costs nothing and buys the two pixels. */
  margin-block: calc(var(--border-width) * -1);
  min-height: var(--space-0);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  padding: var(--space-0);
}

.c-select--bar .c-select__control:focus {
  box-shadow: none;
}

/* ---- setup-signin.css ---- */
/*
 * setup-signin.css. The "Sign in with ..." row in the setup card's light body. Entries [291] and
 * [292], the `.oauth` and `.oauth.done` of sitemapify-v3.html (the navy stage of [291] became a
 * light body under the hero, so the rows are the mockup's white rows now).
 *
 *   solid  the step's main action (Google): --surface, a --line edge and the small shadow.
 *   glass  the optional one (Microsoft): the same row on --surface-2, one step quieter.
 *   done   connected: --success-subtle with a --success edge and --success-ink words.
 *
 * Hover is the mockup's orange wash. Every row is at least 54px, the approved height, well over
 * the 44px floor. The detail line is the account once connected and an address can be 320
 * characters, so it wraps anywhere.
 */

.c-setup-signin {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  min-width: 0;
  min-height: calc(var(--touch-min) + var(--space-4));
  padding: var(--space-4) var(--space-6);
  border: var(--border-width) solid transparent;
  border-radius: var(--radius-md);
  font-size: var(--text-md);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  text-align: left;
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease);
}

.c-setup-signin--solid, .c-setup-signin--glass {
  border-color: var(--line);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-sm);
}

.c-setup-signin--glass { background: var(--surface-2); box-shadow: none; }

.c-setup-signin--solid:hover, .c-setup-signin--glass:hover {
  border-color: color-mix(in srgb, var(--brand-accent) 45%, var(--surface));
  background: color-mix(in srgb, var(--brand-accent) 8%, var(--surface));
  text-decoration: none;
}

.c-setup-signin--done {
  border-color: color-mix(in srgb, var(--success) 50%, var(--surface));
  background: var(--success-subtle);
  color: var(--success-ink);
}

.c-setup-signin:focus-visible { outline: var(--focus-width) solid var(--brand); outline-offset: var(--focus-offset); }

.c-setup-signin__mark {
  flex: none;
  width: var(--space-6);
  height: var(--space-6);
}

.c-setup-signin__lines {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.c-setup-signin__title { overflow-wrap: anywhere; }

.c-setup-signin__detail {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: var(--leading-base);
  overflow-wrap: anywhere;
  opacity: 0.75;
}

.c-setup-signin--solid .c-setup-signin__detail, .c-setup-signin--glass .c-setup-signin__detail { color: var(--ink-3); opacity: 1; }

.c-setup-signin__glyph {
  flex: none;
  width: var(--space-5);
  height: var(--space-5);
  opacity: 0.6;
}

.c-setup-signin--done .c-setup-signin__glyph { opacity: 1; }

/* ---- setup-stage.chips.css ---- */
/*
 * setup-stage.chips.css. The figures the mascot holds up, either side of him on the navy hero.
 * Entry [292], the `.wchips` and `.chip` of sitemapify-v3.html.
 *
 * Each side is a column of chips, pushed towards the mascot (the left column ends at him, the right
 * one starts at him) from --bp-sm up; below it each side is a centred row that wraps, above and
 * below him, the mockup's stack. A chip's figure is the real read, never shortened: a 6-digit URL
 * count is the longest a site of this suite reaches, and the chip grows to hold it rather than
 * clipping it (the column's minimum is sized for "999,999" at the figure size).
 *
 * Colour: glass on the navy, --brand-ink at nine per cent with a twenty per cent edge. The figure
 * is white; `accent` is --brand-accent (progress, and clicks at stake), `good` --success, `bad`
 * --danger mixed a third of the way to white so it reads on the navy (the mockup's #F87171).
 */

.c-setup__chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4);
  min-width: 0;
}

.c-setup__chips:empty { display: none; }

.c-setup__chip {
  display: inline-flex;
  flex-direction: column;
  min-width: 8.25rem;
  max-width: 100%;
  padding: var(--space-4) var(--space-5);
  border: var(--border-width) solid color-mix(in srgb, var(--brand-ink) 20%, transparent);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--brand-ink) 9%, transparent);
  text-align: left;
  animation: c-setup-chip-in 0.45s var(--ease) both;
}

.c-setup__chip + .c-setup__chip { animation-delay: 0.1s; }
.c-setup__chips--right .c-setup__chip { animation-delay: 0.2s; }
.c-setup__chips--right .c-setup__chip + .c-setup__chip { animation-delay: 0.3s; }

.c-setup__chip-key {
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--brand-ink) 62%, transparent);
}

.c-setup__chip-value {
  margin-top: var(--space-1);
  font-size: var(--text-2xl);
  font-weight: var(--weight-heavy);
  line-height: var(--leading-tight);
  letter-spacing: -0.03em;
  color: var(--brand-ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.c-setup__chip-value--accent { color: var(--brand-accent); }
.c-setup__chip-value--good { color: var(--success); }
.c-setup__chip-value--bad { color: color-mix(in srgb, var(--danger) 70%, var(--brand-ink)); }

.c-setup__chip-note {
  margin-top: var(--space-1);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--c-setup-faint);
}

/* A state rather than a figure: a tick and its words, on --success glass. */
.c-setup__chip--tick {
  flex-direction: row;
  align-items: center;
  gap: var(--space-3);
  border-color: color-mix(in srgb, var(--success) 42%, transparent);
  background: color-mix(in srgb, var(--success) 14%, transparent);
}

.c-setup__chip--tick svg { flex: none; width: var(--space-6); height: var(--space-6); color: var(--success); }

.c-setup__chip-label {
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  color: color-mix(in srgb, var(--success) 22%, var(--brand-ink));
}

@keyframes c-setup-chip-in {
  from { opacity: 0; transform: translateY(var(--space-3)); }
  to { opacity: 1; transform: none; }
}

@media (min-width: 640px) {
  .c-setup__chips {
    flex: 1 1 0;
    flex-direction: column;
    flex-wrap: nowrap;
    max-width: 10.75rem;   /* the mockup's 172px column */
  }

  .c-setup__chips--left { align-items: flex-end; }
  .c-setup__chips--right { align-items: flex-start; }
  .c-setup__chips:empty { display: flex; }
}

@media (prefers-reduced-motion: reduce) {
  .c-setup__chip { animation: none; }
}

/* ---- setup-stage.css ---- */
/*
 * setup-stage.css. The frame of a stepped first run: a CENTRED CARD at most 820px wide and as tall
 * as its content, with a white top bar, the navy hero (the mascot and his chips), the light body and
 * the footer. Entries [290], [291] and [292], the wizard of sitemapify-v3.html (.wizcard, .wc-top,
 * .wc-hero, .wstage, .wc-body, .wc-foot). The chips are setup-stage.chips.css, the words
 * setup-stage.words.css, the props and every animation setup-stage.motion.css, split for the cap.
 *
 * ## Colour, every value from tokens.css, composed rather than copied from the mockup
 *
 *   the card       --surface with a --line edge, the mockup's white card on the page ground.
 *   orange         --brand-accent, the owner's "orange for progress, dots and Next": the eyebrow,
 *                  the current rail segment, the current dot, Next, and a progress figure.
 *   --c-setup-lit  the hero's glow, the approved #1B4A80: --brand-hover a fifth of the way to
 *                  --brand-border, the stop reality-band.css composes. The middle is --brand-hover.
 *   --c-setup-deep the hero's floor, the approved #0B2340: --brand mixed 40 per cent towards --ink.
 *   --c-setup-sky  the approved --blue-300: --brand-border 45 per cent towards white.
 */

.c-setup {
  --c-setup-lit: color-mix(in srgb, var(--brand-hover) 80%, var(--brand-border));
  --c-setup-deep: color-mix(in srgb, var(--brand) 60%, var(--ink));
  --c-setup-sky: color-mix(in srgb, var(--brand-border) 55%, var(--brand-ink));
  --c-setup-faint: color-mix(in srgb, var(--brand-ink) 55%, transparent);

  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 51.25rem;   /* the mockup's 820px card */
  min-width: 0;
  margin: var(--space-7) auto;
  overflow: hidden;
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-md);
  color: var(--ink);
}

/* ---- the top bar: where you are, and Skip setup ---- */
.c-setup__top {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-7);
  border-bottom: var(--border-width) solid var(--line-soft);
}

.c-setup__eyebrow {
  margin-right: auto;
  white-space: nowrap;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--action-text);   /* [293]: the orange as text on white was 2.30:1; the rail beside it carries the orange */
}

.c-setup__rail {
  display: flex;
  flex: 0 1 9.375rem;
  gap: var(--space-2);
  min-width: 3rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.c-setup__seg {
  position: relative;
  flex: 1 1 0;
  height: var(--space-2);
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: var(--line);
}

.c-setup__seg--done { background: var(--success); }

.c-setup__skip { display: flex; }

/* ---- the navy hero: the mascot holding his own figures ---- */
.c-setup__hero {
  display: grid;
  place-items: center;
  padding: var(--space-7);
  background: var(--brand);
  background: radial-gradient(35rem 20rem at 50% -25%, var(--c-setup-lit) 0%, var(--brand-hover) 48%, var(--c-setup-deep) 100%);
  color: var(--brand-ink);
}

.c-setup__figs {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  max-width: 40rem;
  min-width: 0;
}

.c-setup__stage {
  position: relative;
  display: grid;
  flex: none;
  place-items: center;
  width: 14.375rem;
  max-width: 100%;
  height: 12.875rem;
}

.c-setup__halo {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 12.25rem;
  height: 12.25rem;
  margin: -6.125rem 0 0 -6.125rem;
  border-radius: var(--radius-full);
  background: radial-gradient(circle, color-mix(in srgb, var(--brand-accent) 17%, transparent) 0%,
    color-mix(in srgb, var(--brand-border) 18%, transparent) 36%, transparent 70%);
}

.c-setup__stage .c-mascot { position: relative; z-index: 3; }
.c-setup__stage .c-mascot--lg { --c-mascot-size: 9.125rem; }

/* The mascot's own pale disc is for a white panel; on the navy the halo above is the glow. */
.c-setup .c-mascot__glow { fill: none; }

/* ---- the body: the step's words, on the white card ---- */
.c-setup__screen {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  padding: var(--space-7) var(--space-10);
  text-align: center;
}

/* ---- the footer: Back, the dots, Next ---- */
.c-setup__nav {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-7);
  border-top: var(--border-width) solid var(--line-soft);
  background: var(--surface-2);
}

.c-setup__back, .c-setup__next { display: flex; flex: 1 1 0; min-width: 0; }
.c-setup__next { justify-content: flex-end; }
.c-setup__nav form, .c-setup__skip form { display: flex; margin: 0; }
.c-setup__dots { display: flex; flex: none; align-items: center; margin: 0; }

/* A dot is a 44px target around a small disc: the control is the button, the disc its picture. */
.c-setup__dot {
  display: grid;
  place-items: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: none;
  color: inherit;
  cursor: pointer;
}

span.c-setup__dots .c-setup__dot { min-width: var(--space-6); min-height: var(--space-6); }
.c-setup__dot::before {
  content: "";
  width: var(--space-3);
  height: var(--space-3);
  border-radius: var(--radius-full);
  background: var(--line);
  transition: transform var(--duration-base) var(--ease), background-color var(--duration-base) var(--ease);
}

.c-setup__dot--done::before { background: var(--success); }
.c-setup__dot--current::before { background: var(--brand-accent); transform: scale(1.35); }
button.c-setup__dot:hover::before { transform: scale(1.35); }
.c-setup__dot:focus-visible { outline: var(--focus-width) solid var(--brand); outline-offset: calc(var(--space-3) * -1); }

/* ---- the chips beside the mascot from --bp-sm; stacked round him below it ---- */
@media (min-width: 640px) {
  .c-setup__figs { flex-direction: row; justify-content: center; gap: 0; }
}

@media (max-width: 640px) {
  .c-setup { margin-block: var(--space-4); }
  .c-setup__top { padding-inline: var(--space-5); }
  .c-setup__hero { padding: var(--space-6) var(--space-5); }
  .c-setup__screen { padding: var(--space-6) var(--space-5); }
  .c-setup__nav { flex-wrap: wrap; padding: var(--space-4) var(--space-5); }
  .c-setup__dots { order: -1; flex-basis: 100%; justify-content: center; }
}

/* ---- setup-stage.motion.css ---- */
/*
 * setup-stage.motion.css. The props behind the mascot and their motion, and the live rail segment.
 * Entries [290], [291] and [292], the approved onboarding's `.line`, `.orbit`, `.badge-orb`, `.page` and
 * `.spark`.
 *
 * Decoration only: setup-stage.php renders a fixed set of empty, aria-hidden elements per decor and
 * every one is placed and moved from here (the approved file placed them with inline styles from
 * its script; here each position is an nth-child rule). The mascot's own motion is
 * mascot.motion.css's. prefers-reduced-motion stops all of it at the foot of this file: the props
 * stay where they are drawn, still, which is a picture rather than a missing one.
 */

.c-setup__props { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
.c-setup__props > * { position: absolute; display: block; }

.c-setup__halo { animation: c-setup-breathe 4s var(--ease) infinite; }

@keyframes c-setup-breathe {
  0%, 100% { transform: scale(1); opacity: 0.85; }
  50% { transform: scale(1.08); opacity: 1; }
}

/* The current rail segment sweeps in the orange, the approved `.live`. */
.c-setup__seg--current::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 86%;
  border-radius: inherit;
  background: var(--brand-accent);
  animation: c-setup-sweep 2.4s var(--ease) infinite;
}

@keyframes c-setup-sweep {
  0% { width: 0; }
  70%, 100% { width: 86%; }
}

/* ---- sitemap: lines flowing in from both sides ---- */

.c-setup__line {
  height: var(--space-3);
  border-radius: var(--radius-pill);
  background: linear-gradient(90deg, color-mix(in srgb, var(--c-setup-sky) 15%, transparent), var(--brand-border));
  animation: c-setup-flow 2.2s var(--ease) infinite;
}

.c-setup__line--in {
  background: linear-gradient(270deg, color-mix(in srgb, var(--c-setup-sky) 15%, transparent), var(--brand-border));
  animation-name: c-setup-flow-in;
}

.c-setup__line:nth-child(1) { top: 17%; left: -2%; width: 22%; }
.c-setup__line:nth-child(2) { top: 25%; right: -2%; width: 18%; animation-delay: 0.18s; }
.c-setup__line:nth-child(3) { top: 32%; left: -6%; width: 27%; animation-delay: 0.35s; }
.c-setup__line:nth-child(4) { top: 40%; right: -6%; width: 24%; animation-delay: 0.53s; }
.c-setup__line:nth-child(5) { top: 51%; left: -4%; width: 19%; animation-delay: 0.7s; }
.c-setup__line:nth-child(6) { top: 59%; right: -4%; width: 15%; animation-delay: 0.88s; }
.c-setup__line:nth-child(7) { top: 67%; left: -8%; width: 25%; animation-delay: 1.05s; }
.c-setup__line:nth-child(8) { top: 75%; right: -8%; width: 21%; animation-delay: 1.23s; }

@keyframes c-setup-flow {
  0% { opacity: 0; transform: translateX(-40%) scaleX(0.5); }
  40%, 75% { opacity: 0.95; transform: none; }
  100% { opacity: 0; transform: translateX(28%) scaleX(0.4); }
}

@keyframes c-setup-flow-in {
  0% { opacity: 0; transform: translateX(40%) scaleX(0.5); }
  40%, 75% { opacity: 0.95; transform: none; }
  100% { opacity: 0; transform: translateX(-28%) scaleX(0.4); }
}

/* ---- connect: an orbit, and the Google and Bing marks riding it ---- */

.c-setup__orbit {
  top: 50%;
  left: 50%;
  width: 12.5rem;
  height: 12.5rem;
  margin: -6.25rem 0 0 -6.25rem;
  border: var(--border-width) dashed color-mix(in srgb, var(--brand-accent) 45%, transparent);
  border-radius: var(--radius-full);
  animation: c-setup-turn 12s linear infinite;
}

.c-setup__orbit::before, .c-setup__orbit::after {
  content: "";
  position: absolute;
  left: 50%;
  width: var(--space-3);
  height: var(--space-3);
  margin-left: calc(var(--space-3) / -2);
  border-radius: var(--radius-full);
  background: var(--brand-border);
  box-shadow: 0 0 var(--space-4) var(--brand-border);
}

.c-setup__orbit::before { top: calc(var(--space-3) / -2); }
.c-setup__orbit::after { bottom: calc(var(--space-3) / -2); background: var(--c-setup-sky); }

.c-setup__orb {
  top: 0;
  left: 50%;
  display: grid;
  place-items: center;
  width: var(--touch-min);
  height: var(--touch-min);
  margin-left: calc(var(--touch-min) / -2);
  border: var(--border-width) solid color-mix(in srgb, var(--brand-ink) 26%, transparent);
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--brand-ink) 12%, transparent);
  animation: c-setup-bob 3s var(--ease) infinite;
}

.c-setup__props > .c-setup__orb { display: grid; }
.c-setup__orb--low { top: auto; bottom: 0; animation-delay: 1.4s; }
.c-setup__orb svg { width: var(--space-6); height: var(--space-6); }

@keyframes c-setup-turn { to { transform: rotate(360deg); } }

@keyframes c-setup-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(calc(var(--space-3) * -1)); }
}

/* ---- pages: pages streaming up past the mascot, some ticked, some flagged ---- */

.c-setup__page {
  width: var(--space-7);
  height: var(--space-8);
  border: var(--border-width) solid color-mix(in srgb, var(--brand-ink) 30%, transparent);
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--brand-ink) 14%, transparent);
  animation: c-setup-stream 2.6s linear infinite;
}

.c-setup__page--ok { border-color: color-mix(in srgb, var(--success) 75%, transparent); background: color-mix(in srgb, var(--success) 16%, transparent); }
.c-setup__page--bad { border-color: color-mix(in srgb, var(--danger) 80%, transparent); background: color-mix(in srgb, var(--danger) 18%, transparent); }

.c-setup__page:nth-child(1) { left: 6%; top: 51%; }
.c-setup__page:nth-child(2) { left: 18%; top: 58%; animation-delay: 0.42s; }
.c-setup__page:nth-child(3) { right: 18%; top: 64%; animation-delay: 0.84s; }
.c-setup__page:nth-child(4) { right: 6%; top: 51%; animation-delay: 1.26s; }
.c-setup__page:nth-child(5) { left: 1%; top: 58%; animation-delay: 1.68s; }
.c-setup__page:nth-child(6) { right: 1%; top: 64%; animation-delay: 2.1s; }

@keyframes c-setup-stream {
  0% { opacity: 0; transform: translateY(2rem) scale(0.7) rotate(-8deg); }
  18%, 72% { opacity: 1; }
  100% { opacity: 0; transform: translateY(-3.4rem) scale(1.05) rotate(8deg); }
}

/* ---- done: sparks ---- */

.c-setup__spark {
  width: var(--space-3);
  height: var(--space-3);
  border-radius: var(--radius-xs);
  background: var(--brand-accent);
  animation: c-setup-burst 2.4s ease-out infinite;
}

.c-setup__spark:nth-child(1) { left: 9%; top: 26%; }
.c-setup__spark:nth-child(2) { right: 9%; top: 23%; animation-delay: 0.3s; }
.c-setup__spark:nth-child(3) { left: 3%; top: 63%; animation-delay: 0.6s; }
.c-setup__spark:nth-child(4) { right: 4%; top: 58%; animation-delay: 0.9s; }
.c-setup__spark:nth-child(5) { left: 16%; top: 7%; animation-delay: 1.2s; }
.c-setup__spark:nth-child(6) { right: 16%; top: 6%; animation-delay: 1.5s; }

@keyframes c-setup-burst {
  0% { opacity: 0; transform: scale(0.4); }
  25% { opacity: 1; }
  100% { opacity: 0; transform: scale(1.5) translateY(-1.5rem); }
}

@media (prefers-reduced-motion: reduce) {
  .c-setup__halo, .c-setup__props > *, .c-setup__seg--current::after { animation: none; }
  .c-setup__line, .c-setup__page, .c-setup__spark { opacity: 0.7; }
}

/* ---- setup-stage.words.css ---- */
/*
 * setup-stage.words.css. What sits in the setup card's light body: heading, lede, the waiting and
 * result lines, the path opened last, notes, the callout, the sign in choices, and the controls.
 * Entries [291] and [292], the `.wc-body`, `.wtrust`, `.wbtn` and `.skipl` of sitemapify-v3.html.
 *
 * .c-setup__choices and .c-setup__note are ALSO printed on light panels by search-connect.php
 * (Search performance, the overview), so their base look is the panel's and the card's spacing is
 * scoped under .c-setup.
 *
 * The buttons: Next and the step's main action are --brand-accent, the owner's orange, with the
 * navy --brand as their words: white on this orange measures 2.2:1, under the 4.5:1 floor for
 * words, where the navy measures over 6:1. Back and Skip setup are words, the mockup's ghost and
 * `.skipl`.
 */

.c-setup__heading {
  margin: 0;
  font-size: var(--text-3xl);
  font-weight: var(--weight-heavy);
  line-height: var(--leading-display);
  letter-spacing: -0.03em;
  color: var(--brand);
  overflow-wrap: break-word;
  text-wrap: balance;
}

/* [293]: the lit word was the logo orange as text on white, 2.30:1. It is the ink step now, and
   the orange stays as the line under it, where it marks the word without having to be read. */
.c-setup__lit {
  font-style: normal;
  color: var(--action-text);
  text-decoration: underline var(--brand-accent);
  text-decoration-thickness: 0.12em;
  text-underline-offset: 0.14em;
}

.c-setup__lede {
  max-width: 50ch;
  margin: var(--space-4) 0 0;
  font-size: var(--text-md);
  line-height: var(--leading-loose);
  color: var(--ink-2);
}

/* The sentence for a scan with no count yet, and Google's three figures on step 4, as words. */
.c-setup__counter {
  max-width: 52ch;
  margin: var(--space-5) 0 0;
  font-size: var(--text-sm);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* The path the scan opened last. A URL can be 512 characters here: it wraps anywhere, never wider. */
.c-setup__last {
  max-width: 100%;
  margin: var(--space-4) 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-3);
  overflow-wrap: anywhere;
}

.c-setup__note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  max-width: 52ch;
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-3);
  text-align: left;
}

.c-setup__note svg, .c-setup__callout svg {
  flex: none;
  width: var(--space-5);
  height: var(--space-5);
}

.c-setup .c-setup__note {
  display: inline-flex;
  max-width: 46ch;
  margin-top: var(--space-6);
  font-size: var(--text-xs);
}

.c-setup__callout {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  max-width: 46ch;
  margin: var(--space-6) 0 0;
  padding: var(--space-4) var(--space-6);
  border: var(--border-width) solid color-mix(in srgb, var(--brand-accent, var(--warning)) 40%, var(--surface));
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--brand-accent, var(--warning)) 10%, var(--surface));
  font-size: var(--text-sm);
  color: var(--ink-2);
  text-align: left;
}

.c-setup__callout svg { color: var(--brand-accent, var(--warning)); }

/* ---- the sign in choices and the step's own actions ---- */

.c-setup__choices {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  width: 100%;
  text-align: left;
}

.c-setup .c-setup__choices { max-width: 25rem; margin-top: var(--space-6); }

.c-setup__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4);
  margin-top: var(--space-6);
}

.c-setup__actions form { display: flex; margin: 0; }

.c-setup .c-button--primary {
  --c-button-ring: var(--brand);
  background: var(--brand-accent);
  border-color: var(--brand-accent);
  color: var(--brand);
}

.c-setup .c-button--primary:not([disabled]):hover {
  background: color-mix(in srgb, var(--brand-accent) 88%, var(--ink));
  border-color: color-mix(in srgb, var(--brand-accent) 88%, var(--ink));
  color: var(--brand);
}

/* Back and Skip setup are words, the mockup's ghost button and `.skipl`. */
.c-setup__back .c-button--outline, .c-setup__skip .c-button--outline { background: none; border-color: transparent; color: var(--ink-2); }
.c-setup__back .c-button--outline:not([disabled]):hover { background: var(--surface-3); color: var(--ink); }
.c-setup__skip .c-button--outline { padding-inline: var(--space-3); font-weight: var(--weight-medium); color: var(--ink-3); }
.c-setup__skip .c-button--outline:not([disabled]):hover { background: none; color: var(--ink); text-decoration: underline; }

@media (max-width: 640px) {
  .c-setup__heading { font-size: var(--text-xl); }
  .c-setup__actions, .c-setup__actions form, .c-setup__actions .c-button { width: 100%; }
}

/* ---- sitemap.css ---- */
/*
 * c-sitemap. The rendered site index a customer copies out of this app and into their own page.
 *
 * Entry [200]. It is a PREVIEW of their content, not furniture: what it looks like here is what
 * the words look like, and what they will look like once pasted is their site's business. So this
 * is deliberately plain, and the one thing it must do is read as a document rather than as part
 * of the panel around it.
 *
 * The links are real anchors pointing at the customer's own pages. They are not styled as the
 * suite's links, because a person is about to copy this block and a colour from our palette
 * arriving in their site would be a decision we made for them.
 */

.c-sitemap {
  max-height: 32rem;
  overflow-y: auto;
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--ink);
}

.c-sitemap__title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
}

.c-sitemap__heading {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

.c-sitemap__heading:first-of-type {
  margin-top: 0;
}

.c-sitemap__list {
  margin: 0;
  padding-left: var(--space-5);
}

.c-sitemap__list li {
  margin-bottom: var(--space-1);
}

/* The one line in the block that is OURS rather than theirs, and it says so in its own words. */
.c-sitemap__more {
  list-style: none;
  margin-left: calc(var(--space-5) * -1);
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/*
 * The preview links are INERT ON THIS PAGE, entry [251] A7. Their hrefs are root-relative so they
 * resolve on the customer's own domain once pasted; on our screen the same href points at us and
 * 404s, and there are up to a sitemap's worth of them. This rule lives in the stylesheet, not on
 * the anchors, so it never rides along in the copied markup: `[data-copy-source]` copies the
 * element's innerHTML, which carries no class of ours and no pointer-events. The `tabindex="-1"`
 * on each anchor keeps them out of the tab order and IS stripped from the copy by copy-sitemap.js.
 */
.c-sitemap[data-copy-source] a {
  pointer-events: none;
  cursor: text;
}

/* ---- spark.css ---- */
/*
 * spark.css. The small trend line along a stat tile's bottom edge. Entry [292].
 *
 * The line is drawn in a 100 by 30 viewBox stretched to the box, so the stroke keeps its width
 * whatever the tile's size (non-scaling-stroke). Colour is the tone's token and nothing else:
 *   blue     --brand-border, the suite's one blue (Sitemapify's --action is its orange since [243])
 *   orange   --brand-accent, the logo's orange
 *   danger, warning, success   the three inks, readable as a line on white
 */
.c-spark {
  display: block;
  width: 100%;
  height: var(--space-8);
  overflow: visible;
  --c-spark-ink: var(--brand-border);
}

.c-spark__line {
  fill: none;
  stroke: var(--c-spark-ink);
  stroke-width: 1.8;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

.c-spark--blue    { --c-spark-ink: var(--brand-border); }
.c-spark--orange  { --c-spark-ink: var(--brand-accent); }
.c-spark--danger  { --c-spark-ink: var(--danger-ink); }
.c-spark--warning { --c-spark-ink: var(--warn-ink); }
.c-spark--success { --c-spark-ink: var(--success-ink); }

/* ---- stack-bar.css ---- */
/*
 * stack-bar.css. One horizontal bar in parts, and a legend of counts under it. Entry [292].
 *
 * The parts are rects in a 1000 unit viewBox stretched to the column; the rounded ends are the
 * svg's own clip. Tones, all tokens: success (indexed), warning (crawled and skipped), mute (found,
 * not crawled yet, the mockup's grey), rest (what the sources leave unaccounted, a hairline grey).
 */
.c-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
  margin: 0;
}

.c-stack__bar {
  display: block;
  width: 100%;
  height: calc(var(--space-8) + var(--space-1));
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-3);
}

.c-stack__seg { stroke: none; }
.c-stack__seg--success { fill: var(--success-ink); }
.c-stack__seg--warning { fill: var(--warn-ink); }
.c-stack__seg--mute { fill: var(--muted-dim); }
.c-stack__seg--rest { fill: var(--line-soft); }

.c-stack__legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-xs);
  color: var(--ink-2);
}

.c-stack__legend li { display: inline-flex; align-items: center; gap: var(--space-3); min-width: 0; }
.c-stack__legend b { font-family: var(--font-mono); font-weight: var(--weight-semi); color: var(--ink); }

.c-stack__key {
  flex: none;
  width: var(--space-4);
  height: var(--space-4);
  border-radius: var(--radius-xs);
  border: var(--border-width) solid var(--line);
}

.c-stack__key--success { background: var(--success-ink); border-color: var(--success-ink); }
.c-stack__key--warning { background: var(--warn-ink); border-color: var(--warn-ink); }
.c-stack__key--mute { background: var(--muted-dim); }
.c-stack__key--rest { background: var(--line-soft); }

/* ---- stat.css ---- */
/*
 * stat.css. One KPI tile. design.md section 5.
 *
 * Rebuilt 2026-08-25, entry [050], from the template's disc tile rather than from the one
 * slate block the first build measured. docs/ui-patterns.md section 8 has the survey and
 * stat.php has the argument. The short version: the template's KPI tile is a WHITE card
 * (`.card` app.css 11252 plus `.card-body`) carrying a 48px tinted disc, and the slate block
 * we copied is a different tile that only exists inside one.
 *
 * Geometry, all app.css:
 *   surface        .card, 11252: white, radius 0.375rem
 *   padding        card-body pt-4 pb-3 px-4 on the disc tiles: 1rem, 0.75rem, 1rem
 *   disc           h-12 w-12 (4467, 4695) = 3rem, rounded-full (5635), glyph text-2xl (6913)
 *   gap            space-x-3 = 0.75rem
 *   label          text-sm (6901) weight 500 (6979), slate-600 (7154), mb-1
 *   value          text-lg (6909) weight 500, slate-900
 *   delta          text-xs (6897), slate-600, with the figure in the tone colour
 *
 * The hairline is ours and is not the template's: its tile sits on a white page where a card
 * reads on its own, and ours sits on --ground. It is --line at 1px, the same edge every other
 * surface in this panel carries, rather than the shadow the template's card uses, because a
 * row of four shadows above a bordered panel is two elevation systems on one screen.
 */

.c-stat {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-5) var(--space-4);
  background: var(--surface);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius-lg);
  min-width: 0;
}

/*
 * The disc. A tint behind the glyph, the same hue solid on top. The template hardcodes the
 * pair per tile as an arbitrary-value background beside a tone class (see
 * ecommerce-dashboard.html, the shopping-cart and arrow-trending-up tiles), which is why
 * tools/check.sh forbids writing one here. Ours takes both from the tone tokens, so a tile
 * re-skins with everything else and an app that changes its brand block gets matching discs.
 */
.c-stat__disc {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  /* 44px, entry [240]: the owner's ruling on the approved file's tile. At 32 the disc read as a
     bullet and the tile lost its anchor; --touch-min is the one 44px token. */
  width: var(--touch-min);
  height: var(--touch-min);
  border-radius: var(--radius-full);
  background: var(--muted-subtle);
  color: var(--ink-2);
}

.c-stat__glyph {
  width: var(--space-7);
  height: var(--space-7);
  fill: none;
}

.c-stat__body { min-width: 0; flex: 1 1 auto; }

.c-stat__label {
  display: block;
  margin-bottom: var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-base);
  letter-spacing: normal;
  color: var(--ink-2);
}

/*
 * The figure, entry [239]: the approved Sitemapify file's tile value, sans at the 2xl step in
 * the heavy weight with tight tracking. It was the mono face at lg/medium from the template's
 * KPI tile; the owner ruled the file's type steps structural and this is where they land.
 */
.c-stat__value {
  font-family: var(--font-sans);
  font-size: var(--text-2xl);
  font-weight: var(--weight-heavy);
  line-height: var(--leading-tight);
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  overflow-wrap: anywhere;
}

/*
 * Two parts, which is the correction. The template colours the FIGURE and leaves the words
 * plain: `-60%` in warning, then "From last Week" in slate-600. Colouring the whole line, as
 * the first build did, spends the signal on the sentence instead of on the number.
 */
.c-stat__delta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-2);
}

.c-stat__figure {
  flex: none;
  font-family: var(--font-mono);
  font-weight: var(--weight-semi);
  font-variant-numeric: tabular-nums;
}

.c-stat__text { min-width: 0; }

.c-stat__figure--up { color: var(--success); }
.c-stat__figure--down { color: var(--danger); }
.c-stat__figure--flat { color: var(--ink-2); }

/* The direction word, for a screen reader only. The glyph and the colour carry it on
   screen, and repeating the word there would be noise in a dense panel. */
.c-stat__word {
  position: absolute;
  width: var(--space-1);
  height: var(--space-1);
  margin: calc(var(--space-1) * -1);
  padding: var(--space-0);
  border: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}

/*
 * Tone colours the disc and the figure. It never colours the label or the value: the label
 * is the signal that does not depend on colour, and a figure that changes hue by tone would
 * make two different things (direction and category) fight over one channel.
 */
.c-stat--success .c-stat__disc { background: var(--success-subtle); color: var(--success-ink); }
.c-stat--warning .c-stat__disc { background: var(--warning-subtle); color: var(--warn-ink); }
.c-stat--danger  .c-stat__disc { background: var(--danger-subtle);  color: var(--danger-ink); }
.c-stat--info    .c-stat__disc { background: var(--info-subtle);    color: var(--info); }
.c-stat--ai      .c-stat__disc { background: var(--ai-subtle);      color: var(--ai); }
.c-stat--mute    .c-stat__disc { background: var(--muted-subtle);   color: var(--ink-3); }
.c-stat--brand   .c-stat__disc { background: var(--action-subtle);  color: var(--action-text); }

/* House rule 10. The disc holds its size; the tile tightens around it. */
@media (max-width: 640px) {
  .c-stat { padding: var(--space-4); gap: var(--space-3); }
}

/* ---- stat-row.css ---- */
/*
 * stat-row.css. The one row of stat tiles, with an optional small section heading over it.
 * Entry [292], from sitemapify-v3.html's .sectitle and .bigstats: an uppercase caption, a hairline
 * to the right, a muted note at the end, then the tiles. Entry [296], the owner's ruling, made it
 * the row on every Sitemapify screen with figures: one tile, one height, one gap, and the count
 * the figures have.
 *
 * COLUMNS. One under --bp-sm, two up to --bp-xl, and from --bp-xl the count stat-row.php wrote
 * as the modifier. The count waits for xl because the dashboard's row sits beside the rail: at 1440
 * its main column is about 850px, four tiles of about 200, and below that four iconed tiles would
 * break a six digit figure. Every row takes the same breakpoints so a tile is the same width class
 * on every screen at one viewport.
 *
 * HEIGHT. Every tile in a row reserves the trend band (stat.trend.css), whether it draws a line or
 * not, so a row with trends and a row without are the same height, and `grid-auto-rows: 1fr` makes
 * every line of tiles in one row as tall as its tallest. "A tile with no trend leaves the space
 * empty rather than shrinking."
 */
.c-stat-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

.c-stat-row__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-width: 0;
}

.c-stat-row__title {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-3);
}

.c-stat-row__rule {
  flex: 1 1 auto;
  height: var(--border-width);
  background: var(--line);
}

.c-stat-row__note {
  flex: 0 1 auto;
  min-width: 0;
  font-size: var(--text-xs);
  color: var(--ink-3);
  text-align: right;
}

.c-stat-row__tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  gap: var(--space-5);
}

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

.c-stat-row__tiles > .c-stat { padding-bottom: var(--c-stat-band); }

@media (min-width: 1440px) {
  .c-stat-row__tiles--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .c-stat-row__tiles--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@media (max-width: 640px) {
  .c-stat-row__tiles { grid-template-columns: minmax(0, 1fr); }
}

/* ---- stat-row.tile.css ---- */
/*
 * stat-row.tile.css. The iconed tile's arrangement inside a stat row. Entry [296].
 *
 * THE DISC AND THE LABEL SHARE THE FIRST LINE, the figure and the delta run the tile's full width
 * under them. The row's count lands at --bp-xl, and the dashboard's row sits beside the rail: at
 * 1440 its tiles are about 200px. With the disc as a column beside the body (stat.css, the
 * standalone tile) the body had 110px, which broke a nine character figure ("2,968,500") in two and
 * set every label and delta on two or three lines. Beside the label only, the disc costs the figure
 * nothing, and a label on two lines (40px) still fits the disc's 44, so the first line is one
 * height whatever the label.
 *
 * ONE HEIGHT IN EVERY ROW ON EVERY SCREEN. The owner's ruling: "What must be uniform is the tile,
 * the height and the spacing, not the count." So every part that could vary is given its tallest
 * height here, not left to the copy: the first line is the disc's, the figure keeps the 2xl line
 * box when it is a state in words ("Not connected", set smaller), the delta keeps two lines, since
 * "7 broken, 4 conflicting, 3 unlinked" takes two at 200px, and stat-row.css reserves the trend
 * band on every tile. A tile whose delta runs to a third line grows its own row, and only that row.
 */
.c-stat-row__tiles > .c-stat--iconed {
  display: grid;
  grid-template-columns: var(--touch-min) minmax(0, 1fr);
  align-content: start;
  column-gap: var(--space-4);
  row-gap: var(--space-2);
}

.c-stat-row__tiles > .c-stat--iconed > .c-stat__body { display: contents; }

.c-stat-row__tiles > .c-stat--iconed .c-stat__disc { grid-column: 1; grid-row: 1; }

.c-stat-row__tiles > .c-stat--iconed .c-stat__label {
  grid-column: 2;
  grid-row: 1;
  align-self: center;
  margin-bottom: 0;
}

.c-stat-row__tiles > .c-stat--iconed .c-stat__value,
.c-stat-row__tiles > .c-stat--iconed .c-stat__delta { grid-column: 1 / -1; }

.c-stat-row__tiles > .c-stat--iconed .c-stat__value { margin-top: var(--space-2); }

.c-stat-row__tiles .c-stat__value {
  display: flex;
  align-items: center;
  min-height: calc(var(--text-2xl) * var(--leading-tight));
}

.c-stat-row__tiles .c-stat__delta {
  align-content: flex-start;
  margin-top: 0;
  min-height: calc(var(--text-xs) * var(--leading-base) * 2);
}

/* ---- stat.trend.css ---- */
/*
 * stat.trend.css. Two additions to the stat tile, entry [292]: a trend line along its bottom edge
 * (the `spark` component, drawn only with two or more points) and a dimmed value for a tile whose
 * value is a state in words, "Not connected", rather than a figure.
 *
 * The tile makes room for the line only when there IS a line, so a tile with no trend is exactly
 * the [240] tile. Entry [296]: the line is drawn in the iconed tile the same way, along the whole
 * tile's bottom edge under the disc and the body, since the icon tile is now the one tile.
 */
.c-stat {
  /* The band the line sits in: its own height plus the air under the delta. stat-row.css
     reserves it on every tile of a row, entry [296], so it is named once here. */
  --c-stat-band: calc(var(--space-8) + var(--space-3));
  position: relative;
  overflow: hidden;
}

.c-stat:has(> .c-spark) { padding-bottom: var(--c-stat-band); }

.c-stat > .c-spark {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--space-2);
  opacity: 0.6;
}

.c-stat__value--dim {
  font-size: var(--text-md);
  font-weight: var(--weight-semi);
  letter-spacing: normal;
  color: var(--ink-3);
}

/* ---- table.css ---- */
/*
 * table.css. The data grid. design.md section 5.
 *
 * Geometry is the template's .table-th (app.css 10772) and .table-td (app.css 10798):
 * 1.25rem by 1.5rem in every cell, which computes to a 56px heading row and a 60px
 * body row. The old 32px and 36px rows came from the prototype and were about half.
 *
 * Three things the purchased template does not do, all of them here: a sticky
 * header, right aligned tabular numeric cells, and a single line truncating cell.
 * All three are driven by props, so a screen never writes a modifier by hand.
 */

/*
 * position: relative is LOAD BEARING and is not a styling choice. Entry [059].
 *
 * This element is a scroll container (overflow-x: auto, and --sticky caps its height), but a
 * `static` scroll container is NOT the containing block for an absolutely positioned
 * descendant. Such a descendant resolves against the initial containing block instead, which
 * means it is NOT CLIPPED HERE, and its document position is added to the ROOT's scrollable
 * overflow even though it is invisible and the wrapper is only 540px tall.
 *
 * Every icon only row action carries a `.l-sr` label, and `.l-sr` is `position: absolute`. On
 * /admin/jobs that put 50 invisible 1px spans at document offsets down to 4161px inside a
 * wrapper ending at 832px, so the page scrolled to 4161 with 3128px of empty ground below the
 * footer. Thirteen screens. Measured, and measured again with every span set to `static`,
 * which returned the document to 1033 exactly.
 *
 * relative makes this element the containing block, so those spans clip with everything else.
 * It deliberately does NOT set a z-index: relative with z-index auto opens no stacking context,
 * so the --z-raised header from entry [058] still orders against the page as it must.
 */
.c-table {
  position: relative;
  min-width: 0;
  overflow-x: auto;
}

.c-table__grid {
  width: 100%;
  font-size: var(--text-sm);
}

.c-table__th {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  /* 700 and 0.11em are the approved file's, entry [242]; --ink-3 its colour, [241 ruling 4].
     The token itself moved in [244] (it measured 2.45:1 on this ground; it is 4.94 now), so the
     faintest ink is still the faintest ink and clears the floor. */
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: 0.11em;
  text-transform: uppercase;
  text-align: left;
  color: var(--ink-3);
  padding: var(--cell-pad-y) var(--cell-pad-x);
  border-bottom: var(--border-width) solid var(--line);
  background: var(--surface-2);
  white-space: nowrap;
}

/*
 * Sticky header. The background is what makes it work: without an opaque ground the
 * rows scroll visibly through the headings.
 */
/*
 * A sticky header needs something that actually scrolls. The wrapper already sets
 * overflow-x: auto, and CSS computes the other axis to auto with it, so the wrapper
 * is the scroll container rather than the page. Without a height it never overflows
 * vertically, the header pins to a container that cannot scroll, and the whole thing
 * is inert. That was found in review, not by reading the spec.
 *
 * So a sticky table caps its own height. The value is a component local property with
 * a viewport relative default: a screen composes the height it wants by setting
 * --c-table-max, and nothing here hardcodes a pixel count.
 */
.c-table--sticky {
  max-height: var(--c-table-max, 60vh);
  overflow-y: auto;
}

/*
 * --z-raised, NOT --z-sticky. Entry [058].
 *
 * The header pins to the wrapper above, and the wrapper scrolls WITH THE PAGE, so a pinned
 * header travels up through the top bar on its way out. Both were --z-sticky, the table comes
 * after the top bar in the DOM, and equal z-index is settled by document order, so the header
 * won and sliced the bar as it passed.
 *
 * It cut the profile menu in half too, and that is the same bug rather than a second one: the
 * menu is --z-overlay, which looks like it should win and cannot, because it lives inside
 * .l-bar and a z-index inside a stacking context only orders it against its siblings there.
 * The menu could never beat a page element that .l-bar itself could not beat.
 *
 * tokens.css carries the whole ladder and design.md section 3a carries the reasoning.
 */
.c-table--sticky .c-table__th {
  position: sticky;
  top: 0;
  z-index: var(--z-raised);
}

.c-table__td {
  padding: var(--cell-pad-y) var(--cell-pad-x);
  font-size: var(--text-sm);
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
  border-bottom: var(--border-width) solid var(--line-soft);
  vertical-align: middle;
  color: var(--ink-2);
}

/*
 * The dense variant. Applied automatically above the column threshold in table.php, and
 * forceable either way with the `density` prop.
 *
 * This is OURS and the template has no equivalent, which is worth stating plainly rather
 * than implying a line number. The template does not need one: its widest table is 9 columns
 * and it averages 5.7, where ours average 7.7 with seven tables at ten. At --cell-pad-x that
 * is 480px of horizontal padding before a single character of content, which is most of why
 * 20 of 25 screens run their table off the page at 1440.
 *
 * Only the horizontal padding moves. The row height does not: --cell-pad-y stays, because a
 * shorter row is harder to track across ten columns, which is the opposite of the problem
 * being solved.
 */
.c-table--dense .c-table__th,
.c-table--dense .c-table__td { padding-inline: var(--cell-pad-x-dense); }

.c-table__body .c-table__row:last-child .c-table__td { border-bottom: 0; }

.c-table__body .c-table__row:hover { background: var(--surface-2); }

/* The message when rows is empty. Rendered as a cell, or alone when there are no
 * columns either. */
.c-table__empty {
  padding: var(--space-8) var(--cell-pad-x);
  text-align: center;
  color: var(--ink-3);
  font-size: var(--text-sm);
}

/* ---- table.arrive.css ---- */
/*
 * table.arrive.css. The entrance a row gets in a table that fills while somebody watches it.
 * Entry [176].
 *
 * OPT IN, through table.php's `arrive` prop, and one screen uses it: the partial results of a
 * running scan. Every other table in the suite is a list that was already complete when the
 * page rendered, and reanimating those on every load would be motion with nothing behind it.
 *
 * The animation is on the ROW rather than applied by a script, which is what lets a row cloned
 * from a <template> by shared/assets/js/components/scan-progress.js arrive with it: the module
 * writes no class names, by its own rule, and a row landing in the body picks this up because
 * it is in the body.
 *
 * `both` fills backwards as well as forwards, so a row is invisible for the frame between
 * being appended and the animation starting rather than flashing at full opacity first.
 *
 * It is the timeline's entrance, --duration-base on the shared easing, deliberately: the feed
 * and the table are the two halves of one arrival and two different entrance timings would
 * read as two unrelated things happening.
 */

.c-table--arrive .c-table__row {
  animation: c-table-arrive var(--duration-base) var(--ease) both;
}

@keyframes c-table-arrive {
  from { opacity: 0; transform: translateY(var(--space-2)); }
  to   { opacity: 1; transform: none; }
}

/*
 * Off outright, and pinned open. base.css collapses the duration to 0.01ms, which lands a two
 * keyframe animation on its last frame and would be harmless here, but a row that depends on
 * an animation to become visible is a row that disappears if anything about that ever changes.
 */
@media (prefers-reduced-motion: reduce) {
  .c-table--arrive .c-table__row {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* ---- table.cell.css ---- */
/*
 * table.cell.css. What ONE CELL does with its value.
 *
 * Split out of table.css 2026-08-27, entry [062], at 236 lines against the 200 line cap in
 * house rule 5. The seam is a real one and table.narrow.css is the precedent for drawing it
 * here: table.css is the grid and its chrome, the wrapper, the head, the sticky, the density,
 * the row rules. This file is the treatment a column asks for, and every rule in it is
 * chosen per column by a prop on the component rather than by the table.
 *
 * The build script ranks a base ahead of its extensions, so this lands after table.css in
 * library.css whatever the filename sorts to.
 */

/*
 * A figure a person reads down a column is right aligned and tabular, so the digits
 * line up and two numbers can be compared without counting places.
 */
.c-table__th--right,
.c-table__td--right { text-align: right; }

.c-table__td--right {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.c-table__th--center,
.c-table__td--center { text-align: center; }

/* The identifier column, entry [239]: the mono face, left aligned, one size down, breaking
   inside a long URL rather than widening the table. */
.c-table__td--mono {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  overflow-wrap: anywhere;
}

/*
 * The truncating cell. max-width 0 lets the table's own layout hand this cell
 * whatever is left over, and the ellipsis takes the rest.
 */
.c-table__td--truncate {
  max-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A short value that must stay on one line inside a wrapping cell, a date such as "26 Sep".
   Entry [295]: a span, not a column prop, so the cell keeps its own wrapping for anything else. */
.c-table__nowrap { white-space: nowrap; }

/*
 * The clamping cell, sweep item 2, entry [062]. Two lines and then an ellipsis.
 *
 * ## The template has no answer to copy, and that IS the answer
 *
 * It never puts prose in a table. Measured rather than asserted: across basic-table.html
 * and advance-table.html, 876 body cells, the LONGEST is 27 characters and it is an email
 * address. Its tables carry identifiers, figures, status pills and actions. The one place
 * it truncates a sentence is email.html 2391, and that is a list of cards, not a table.
 *
 * So house rule 9 gives nothing to copy, because the template never creates the situation.
 * What it gives is the rule underneath: a row holds a value you scan, not one you read.
 * docs/design.md section 6a reached the same conclusion from the other direction and this
 * is that rule applied to a sentence instead of an address.
 *
 * ## Then why does this exist at all
 *
 * Because on five screens the sentence IS the row. An error list whose error text lives
 * somewhere else is not an error list. Those columns cannot leave, so the only question is
 * how much of the value the row shows, and one line was showing 20 to 34 per cent:
 * "RuntimeException: Goo" is not a shorter version of the message, it is a different string
 * that happens to start the same way.
 *
 * ## Why the clamp is on a span and not on the cell
 *
 * -webkit-line-clamp needs `display: -webkit-box`, and a <td> with that declaration is no
 * longer a table cell: it leaves the table formatting context and the column collapses to
 * its one character minimum. Measured on the first attempt, which put the clamp on the td:
 * every clamped column went to 32px and the settings rows went to 270px tall. The cell keeps
 * `display: table-cell` and an inner span does the clamping.
 *
 * The floor is 11rem, and it is the same number and the same argument as identity.css 126:
 * two thirds of a 16 character domain at 0.875rem, wide enough that break-word almost never
 * fires and narrow enough that a nine column table still fits. `max-width: 0` is deliberately
 * NOT used here the way it is on --truncate, because a cell told its maximum is zero and
 * allowed to wrap takes one character per line rather than the leftover width.
 *
 * Two lines, never three: a third line makes the row taller than the 44px touch target
 * beside it and turns a list into a stack of paragraphs, which section 10 already ruled
 * against for forms.
 */
.c-table__clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: break-word;
}

/*
 * There is no text-transform here at all, and that is the point.
 *
 * The template capitalises every body cell (.table-td, app.css 10798). We copied it, then
 * spent three exemptions narrowing it back off identifiers, figures and matrix cells,
 * which was the signal it was wrong rather than nearly right. A body cell holds DATA. CSS
 * that retitles data is lying about the row: a slug, a plan code, an email address and a
 * person's own spelling of their own name all come out of the database correct and go onto
 * the page altered. Dropped whole 2026-08-24, entry [047], along with the three exemptions
 * that existed only to undo it.
 */

/*
 * matrix. Same markup, a fixed column axis (plan tiers, roles) rather than record
 * fields, so every cell is a value in the same units and reads as monospace.
 */
.c-table--matrix .c-table__td {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

.c-table__yes { color: var(--success); }
.c-table__no { color: var(--ink-3); }

/*
 * The four state cell, entry [239]: the approved file's presence grid. Listed is the brand
 * tint, recent (the last few columns) the action colour, not listed a ring in the danger ink on
 * nothing, and the day the page dropped out the danger ink filled. Sixteen pixels tall, the
 * file's own, on the swatch radius; the cell padding around it comes from the matrix rule.
 */
.c-table__cell {
  display: block;
  min-width: var(--space-5);
  height: var(--space-5);
  border-radius: var(--radius-xs);
  background: var(--action-subtle);
}

.c-table__cell--recent { background: var(--action); }
.c-table__cell--edge   { background: var(--danger-ink); }

.c-table__cell--off {
  background: transparent;
  box-shadow: inset 0 0 0 var(--border-width) var(--danger-ink);
}

.c-table--matrix .c-table__td:has(> .c-table__cell) { padding: var(--space-2) var(--space-1); }

/*
 * sub. A second line under a cell's own value: the reason a scheduled task cannot start, and
 * whatever earns the same treatment later. Entry [191].
 *
 * A LINE, NOT A COLUMN, and that is the decision worth writing down. `scheduled_tasks.last_error`
 * was stored from the scheduler's first run and rendered nowhere, so the only place to read WHY a
 * task is failing was the database row. Giving it a tenth column would leave five of seven rows
 * empty, and a column that is usually blank makes every row harder to read to help two of them.
 *
 * The type is --text-xs on --ink-2, which is the footnote weight .c-stat__delta and
 * .c-progress__note already use, so a secondary line reads the same everywhere in the suite.
 */
.c-table__sub {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-xs);
  color: var(--ink-2);
}

/* ---- table.narrow.css ---- */
/*
 * table.narrow.css. What a table becomes below the small breakpoint.
 *
 * Split out of table.css 2026-08-26, entry [058], because the two together are 234 lines and
 * house rule 5 caps a component stylesheet at 200. The same seam and the same reason as
 * auth-panel.css out of login.css. It is a real seam rather than an arbitrary cut: everything
 * here is inside a media query and none of it applies to the table this file's partner
 * describes. table.css is the data grid, this is the phone.
 *
 * There is no separate partial and no separate component. It is concatenated into library.css
 * by shared/assets/build-library-css.sh in the same pass, and the class names are table's.
 *
 * ## The name is `table.narrow.css` and not `table-narrow.css`, on purpose
 *
 * The build script globs `*.css` and concatenates in plain alphabetical order. A hyphen (0x2D)
 * sorts BEFORE a dot (0x2E), so `table-narrow.css` would be emitted ahead of `table.css`, and
 * an override file ahead of the base it overrides is a cascade bug waiting for its first equal
 * specificity collision. There is already one: `.c-table__th, .c-table__td { padding }` here
 * and in table.css are both (0,1,0), a media query adding no specificity, so whichever comes
 * last wins. Measured with the hyphen: a matrix table at 375px kept the desktop 20px/24px
 * cells instead of stepping down to 16px/20px. `.narrow` sorts after `.css` and fixes it.
 *
 * Only the matrix variant was affected, because every other narrow rule is scoped
 * `.c-table:not(.c-table--matrix)` at (0,3,0) and outranks the base whatever the order. That
 * is luck, not design, which is why the name carries the reason.
 *
 * Breakpoint is --bp-md, 768px since entry [239] (820 from [064] to [239]), raised from --bp-sm in entry [064]. At 640 a table stayed a
 * grid all the way down through tablet portrait, and nine screens ran off the page at 768
 * and scrolled sideways instead of stacking. House rule 10 says a table that cannot fit
 * COLLAPSES; 768 is where ours stop fitting, so that is where they collapse now. Written out
 * because a media prelude cannot read a custom property, which house rule 10 allows so long
 * as the number is on the token scale.
 */

/*
 * On a phone a 1.5rem gutter on both sides of every cell costs more than it buys:
 * the wrapper scrolls, so the cost is a person dragging a table sideways to read a
 * column that would otherwise fit. The cells step down one rung of the same scale.
 * Breakpoint is --bp-sm, written out because a media prelude cannot read a token.
 */
@media (max-width: 900px) {
  .c-table__th,
  .c-table__td {
    padding: var(--space-4) var(--space-5);
  }

  .c-table__empty {
    padding: var(--space-7) var(--space-5);
  }
}

/*
 * Stacked rows below the small breakpoint, house rule 10. Each cell takes its column
 * name from the data-label the partial emits, and stays a table to a screen reader.
 * The matrix variant is exempt: its columns ARE the axis, so stacking would repeat
 * that axis on every row and destroy the comparison. It keeps the scroll wrapper.
 */
@media (max-width: 900px) {
  .c-table:not(.c-table--matrix) .c-table__head {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  /*
   * The TABLE and the TBODY go block too, entry [063]. Stacking the rows and the cells is
   * not enough on its own: a <table> keeps display:table, so the table layout algorithm
   * still runs and still computes a min-content width from its cells, and the stacked
   * rows are then laid out inside a table that is wider than the wrapper. Measured at
   * 375: /admin/sites stacked correctly and was still 540px wide in a 343px wrapper, and
   * /admin/audit 642px, so both asked a person to drag a stacked list sideways. House
   * rule 10 says a table that cannot fit COLLAPSES rather than overflows, and it had only
   * been half collapsing since the narrow variant was written.
   */
  .c-table:not(.c-table--matrix) .c-table__grid,
  .c-table:not(.c-table--matrix) .c-table__body {
    display: block;
    width: 100%;
  }

  .c-table:not(.c-table--matrix) .c-table__row {
    display: block;
    border-bottom: var(--border-width) solid var(--line);
    padding: var(--space-4) 0;
  }

  .c-table:not(.c-table--matrix) .c-table__td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-5);
    border: 0;
    padding: var(--space-2) var(--space-5);
    text-align: left;
  }

  .c-table:not(.c-table--matrix) .c-table__td[data-label]:not([data-label=""])::before {
    content: attr(data-label);
    flex: 0 0 auto;
    font-size: var(--text-xs);
    font-weight: var(--weight-semi);
    text-transform: uppercase;
    letter-spacing: var(--tracking-caps);
    color: var(--ink-3);
  }

  /* max-width:0 would collapse the value and its label once the cell is a block. */
  .c-table:not(.c-table--matrix) .c-table__td--truncate {
    max-width: none;
    overflow: visible;
    text-overflow: clip;
    white-space: normal;
  }

  /* A stacked row is a card, and a column of cards scrolls with the page. Entry [272]: the 60vh
     cap that gives a pinned header something to pin to has no header to pin at this width and
     nested a second scroll region inside the rows, cutting the last card (merchantfeed overview,
     sitemapify overview, lookout and redirectify lists at 360). The matrix keeps its wrapper. */
  .c-table--sticky:not(.c-table--matrix) {
    max-height: none;
    overflow-y: visible;
  }

  /* A tag is one line in a column; in a stacked card the cell is a row with the label on the
     left, and a sentence long tag ("Not linked from your other pages", sitemapify audit at
     360) was clipped at the card edge. It wraps here, keeping its pill. Entry [273]. */
  .c-table:not(.c-table--matrix) .c-table__td .c-tag {
    white-space: normal;
    min-width: 0;
  }
}

/* ---- tabs.css ---- */
/*
 * tabs.css. The segmented link strip. Entry [161].
 *
 * Geometry and colour are the template's .nav-pills, app.css 1096 to 1119: an inactive pill is
 * a muted ground with secondary ink and no shadow, the current one is a brand fill with white
 * ink and the template's small lift. Nothing here is invented, and nothing is taken from
 * hub-admin.html, per house rule 9.
 *
 * The strip is a track with the pills inside it, which is what the template's own segmented
 * controls do and what keeps two tabs from reading as two unrelated buttons.
 */

.c-tabs {
  display: inline-flex;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--muted-subtle);
  border: var(--border-width) solid var(--line);
  max-width: 100%;
  overflow-x: auto;
}

.c-tabs__tab {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  /* The 44px touch minimum, checked by tools/check-touch.sh. */
  min-height: var(--touch-min);
  padding: var(--space-2) var(--space-6);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  color: var(--ink-2);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--duration-fast) var(--ease), color var(--duration-fast) var(--ease);
}

.c-tabs__tab:hover {
  background: var(--surface);
  color: var(--ink);
}

.c-tabs__tab--current,
.c-tabs__tab--current:hover {
  background: var(--brand);
  color: var(--brand-ink);
  box-shadow: var(--shadow-sm);
}

/* In the column a tab lays out, the label must not be compressed to make room for the note.
   A tab is at most two short lines and the first one is the one that has to read. */
.c-tabs__label { flex: none; }

/* The second line, a saving or a qualifier. Smaller and quieter than the label it sits under,
   and it inherits the current tab's ink so it stays legible on the brand fill. */
.c-tabs__note {
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  opacity: 0.85;
}

@media (max-width: 640px) {
  .c-tabs { display: flex; width: 100%; }
  .c-tabs__tab { flex: 1 1 0; padding-left: var(--space-3); padding-right: var(--space-3); }
}

/* ---- tag.css ---- */
/*
 * tag.css. The small monospace pill. design.md section 5.
 *
 * Geometry is the template's .badge, app.css 8041: 24px tall, 0.25rem by 0.5rem of
 * padding on a 1rem line box, 0.75rem semibold, no text-transform (entry [047]), and
 * the pill radius from
 * .badge.pill at app.css 8055.
 *
 * No border, entry [242]. From [047] to [241] the pill carried a 1px border in its variant's
 * dim tone, "because a variant here has to read on a white card where a tinted ground alone
 * would not carry it"; the approved Sitemapify file's chips have none and the owner's ruling
 * took the file's on every border, weight and letter spacing the rulings before it had not
 * named. The box is still 24px: the padding is the full 0.25rem now that no border shares it.
 *
 * Six variants, all props. Each pairs an ink and a tinted ground from the
 * same semantic family, so the pill reads at a glance without shouting. sig is the
 * brand colour, and ai is the suite wide indigo that means a number came from a
 * crawler or a model. Never use ai decoratively.
 */

.c-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-semi);
  line-height: var(--leading-tight);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--muted-subtle);
  color: var(--ink-2);
  white-space: nowrap;
  vertical-align: middle;
}

/*
 * A status column, where every row carries exactly one of a small set of words. The template
 * gives that pill `min-w-[90px]` and centres it (docs/ui-patterns.md 2.4), so the column does
 * not ripple in and out row by row as "queued", "running" and "failed" go past. Entry [048].
 *
 * It is a prop rather than the default because most tags here are NOT statuses: the app slugs
 * on the customers row are a variable length list, and 90px each would turn three of them into
 * a paragraph. The caller knows which kind it has.
 */
.c-tag--fixed {
  min-width: var(--tag-min);
  justify-content: center;
}

.c-tag__icon {
  width: var(--space-5);
  height: var(--space-5);
  flex: none;
  fill: currentColor;
}

.c-tag__text { min-width: 0; }

/*
 * THE TEXT IS INK, NOT THE SEMANTIC SOLID. Entry [238], the owner's ruling: the tag is the most
 * repeated text on a screen, six severity chips a row at 12px on Sitemapify health, and the
 * solids measured 2.0 to 3.3:1 on their own tints. Each variant's text is now the ink that
 * clears 4.5:1 on its tint (5.79, 4.65, 4.71 measured; --ai-dim 5.45; --ink-2 7.24). The
 * fills are unchanged, so the pill looks the same from across the room and reads
 * up close. Suite wide, as ruled.
 */
.c-tag--success {
  color: var(--success-ink);
  background: var(--success-subtle);
}

.c-tag--warning {
  color: var(--warn-ink);
  background: var(--warning-subtle);
}

.c-tag--danger {
  color: var(--danger-ink);
  background: var(--danger-subtle);
}

.c-tag--brand {
  color: var(--action-text);
  background: var(--action-subtle);
  /* The file's blue chip is 500 where its severity chips are 600. [242] */
  font-weight: var(--weight-medium);
}

.c-tag--ai {
  color: var(--ai-dim);
  background: var(--ai-subtle);
}

.c-tag--mute {
  color: var(--ink-2);
  background: var(--muted-subtle);
}

/*
 * dot, entry [237]. A square swatch in the text's own colour, the approved file's severity
 * chip. Square and not round so it is not read as a `lamp`, which pulses and stands alone;
 * this is punctuation inside a word.
 */
.c-tag__dot {
  width: var(--space-3);
  height: var(--space-3);
  flex: none;
  border-radius: var(--radius-xs);
  background: currentColor;
}

/*
 * solid, entry [237]. The variant's colour becomes the fill and the ink goes light. Only
 * brand is given a solid form: white on the navy measures 14.28:1. White on --danger-dim
 * measures 4.07:1 and on --danger 3.32:1, under the 4.5 a 12px word needs, so a solid
 * danger, warning or success falls back to the tinted pill rather than paint a number that
 * fails. The approved file's solid pills (Silent, Locked, Agency) are all the navy.
 */
.c-tag--solid.c-tag--brand {
  color: var(--brand-ink);
  background: var(--brand);
  /* The file's navy pills (Silent, Locked, Agency) are 700 where its tinted chips are 600. [242] */
  font-weight: var(--weight-bold);
}

/* ---- timeline.css ---- */
/*
 * timeline.css. An ordered ledger with a rail down its markers. Entry [170].
 *
 * DERIVED, not read from the template: the template ships no timeline. It is built out of
 * two objects that already exist here, so a timeline sitting beside a bar row list in the
 * same panel lines up with it.
 *
 *   the row rhythm is bar-row's, --cell-pad-y above and below the text, which is the
 *   template's .table-td (app.css 10798) and therefore the same 60px a table row stands at
 *   when it holds one line
 *   the marker is lamp's dot (lamp.css), --space-4 rather than --space-3 because it sits
 *   alone on a rail here instead of beside a word, and a 8px dot on a 1px rail reads as a
 *   kink in the line rather than as a point on it
 *
 * The rail is what makes this a sequence rather than a list of peers, and it is the only
 * thing here that is not borrowed. It runs from the marker's centre to the NEXT marker's
 * centre, which is `top: var(--space-6); height: 100%` on an item with no margin between
 * items: the segment ends exactly --space-6 past the item's own bottom edge, which is where
 * the next marker's centre is. The last item draws none, because a rail leaving the last
 * entry points at nothing.
 */

.c-timeline {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
  min-width: 0;
}

.c-timeline__item {
  display: grid;
  grid-template-columns: var(--space-5) minmax(0, 1fr) auto;
  gap: var(--space-2) var(--space-4);
  align-items: start;
  padding: var(--cell-pad-y) 0;
  position: relative;
  min-width: 0;
  animation: c-timeline-in var(--duration-base) var(--ease) both;
}

/*
 * The rail. Behind the markers, so the marker's own ring covers the line rather than the
 * line crossing the dot. --border-width is the same hairline every divider in the suite
 * uses, on --line, which is what a table draws between rows.
 */
.c-timeline__item::before {
  content: "";
  position: absolute;
  left: calc(var(--space-5) / 2);
  top: var(--space-6);
  height: 100%;
  width: var(--border-width);
  background: var(--line);
  transform: translateX(calc(var(--border-width) / -2));
}

.c-timeline__item:last-child::before { display: none; }

/*
 * The marker. A ring on the surface by default rather than a filled dot, so an entry with
 * no tone reads as a point on the line and a toned one reads as an event.
 */
.c-timeline__mark {
  grid-column: 1;
  justify-self: center;
  position: relative;
  width: var(--space-4);
  height: var(--space-4);
  margin-top: var(--space-2);
  flex: none;
  border-radius: var(--radius-full);
  background: var(--surface);
  box-shadow: inset 0 0 0 var(--border-width) var(--muted);
}

.c-timeline__text {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.c-timeline__title {
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  line-height: var(--leading-base);
  color: var(--ink);
}

/*
 * overflow-wrap rather than a truncation: the detail line carries a URL path, and a path cut
 * off at the panel edge is a path nobody can act on. Wrapping costs a second line and keeps
 * the whole value.
 */
.c-timeline__detail {
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  color: var(--ink-3);
  overflow-wrap: anywhere;
}

/* Tabular figures, so a column of counts lines up digit under digit as it changes. */
.c-timeline__value {
  grid-column: 3;
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  white-space: nowrap;
}

.c-timeline__empty {
  margin: 0;
  padding: var(--cell-pad-y) 0;
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--ink-3);
}

/* One line each, and the ring goes with the fill so a marker is still a marker on a
   surface that is not white. The vocabulary is the suite's, not this component's. */
.c-timeline__item--brand   .c-timeline__mark { background: var(--brand);   box-shadow: inset 0 0 0 var(--border-width) var(--brand); }
.c-timeline__item--success .c-timeline__mark { background: var(--success); box-shadow: inset 0 0 0 var(--border-width) var(--success); }
.c-timeline__item--warning .c-timeline__mark { background: var(--warning); box-shadow: inset 0 0 0 var(--border-width) var(--warning); }
.c-timeline__item--danger  .c-timeline__mark { background: var(--danger);  box-shadow: inset 0 0 0 var(--border-width) var(--danger); }
.c-timeline__item--info    .c-timeline__mark { background: var(--info);    box-shadow: inset 0 0 0 var(--border-width) var(--info); }
.c-timeline__item--ai      .c-timeline__mark { background: var(--ai);      box-shadow: inset 0 0 0 var(--border-width) var(--ai); }

/*
 * The tail: the sequence has not ended. It pulses at --duration-slow for the reason
 * lamp.css gives for the same token, which is that --duration-fast and --duration-base are
 * interaction timings and either would flash past the 3Hz threshold in WCAG 2.3.1.
 */
.c-timeline__item--live .c-timeline__mark {
  animation: c-timeline-pulse var(--duration-slow) var(--ease) infinite;
}

.c-timeline__item--live .c-timeline__detail { color: var(--ink-2); }

@keyframes c-timeline-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}

/* A new entry arrives rather than appearing, which is what says one was added. */
@keyframes c-timeline-in {
  from { opacity: 0; transform: translateY(var(--space-2)); }
  to   { opacity: 1; transform: none; }
}

/*
 * base.css already collapses every animation to 0.01ms under prefers-reduced-motion, which
 * stops both of these. This pins the two end states as well, so a reader who asked for no
 * motion gets a solid marker and a fully visible entry rather than whichever frame the
 * animation happened to stop on.
 */
@media (prefers-reduced-motion: reduce) {
  .c-timeline__item { animation: none; opacity: 1; transform: none; }
  .c-timeline__item--live .c-timeline__mark { animation: none; opacity: 1; }
}

/*
 * numbered, entry [237]. The marker is a digit in a tinted disc and the rail is gone: this is
 * a set of instructions, read in order and performed once, not a record of events. The disc
 * is a 24px circle so the digit has a line box; the column widens to hold it.
 */
.c-timeline--numbered .c-timeline__item {
  grid-template-columns: var(--space-7) minmax(0, 1fr) auto;
}

.c-timeline--numbered .c-timeline__item::before { display: none; }

.c-timeline--numbered .c-timeline__mark {
  width: var(--space-7);
  height: var(--space-7);
  margin-top: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  line-height: 1;
  color: var(--action-text);
  background: var(--action-subtle);
  box-shadow: none;
}

/* ---- tooltip.css ---- */
/*
 * tooltip.css. One surface, moved around the page by tooltip.js. Entry [044].
 *
 * ## Why this exists rather than the rail widening on hover
 *
 * The collapsed sidebar is 64px and its labels are clipped. Something has to answer
 * "what is this glyph". `title` does not: it never fires on keyboard focus, it is
 * slow, and its look is the operating system's. A per row element does not either,
 * because `.l-shell__nav` is `overflow-y: auto` and would clip it on the one axis it
 * has to escape.
 *
 * So the element is a child of `body`, outside every scrolling ancestor, and it is
 * `position: fixed`, whose containing block is the viewport rather than the scroller.
 * That is the whole trick, and it is the reason nothing here fights an overflow.
 *
 * ## The two numbers this file cannot know
 *
 * Where the row is. They arrive as `--c-tip-x` and `--c-tip-y`, set by tooltip.js on
 * this element and nowhere else. That is the same contract meter already has (see
 * meter.php), and the same reason the policy in Core\Response separates `style-src`
 * from `style-src-attr`: a measured number is not a rule, and a rule never arrives
 * this way. No colour, no size, no layout is ever set from script.
 *
 * ## It is decoration
 *
 * `aria-hidden` in the markup, and no `aria-describedby` pointing at it. The label it
 * repeats is still in the sidebar's DOM and still in the accessibility tree, only
 * visually clipped, so a screen reader reads the row correctly with this element
 * doing nothing at all. Announcing it as a description would say every row twice.
 */

.c-tip {
  position: fixed;
  left: var(--c-tip-x, 0);
  top: var(--c-tip-y, 0);
  z-index: var(--z-toast);
  max-width: 15rem;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--ink);
  color: var(--surface);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-shadow: var(--shadow-md);

  /*
   * It must never be under the pointer. A tooltip that can be hovered is a tooltip
   * that can steal the mouseout from the thing it describes, and then it never
   * closes.
   */
  pointer-events: none;
}

.c-tip[hidden] { display: none; }

/*
 * Placement. The default is to the right of the row, which is where a sidebar rail
 * wants it: `--c-tip-x` is the row's right edge and `--c-tip-y` its vertical centre.
 *
 * `bottom` is for a control in the top bar: `--c-tip-x` is that control's horizontal
 * centre and `--c-tip-y` its lower edge.
 *
 * `above` is for a row action, entry [058]. Row actions sit at the RIGHT EDGE of every table,
 * so the default rightward tip opened straight off the viewport. Above, centred on the
 * trigger, is the one direction that has room on every screen. It flips to `bottom` for a
 * control in the first row, where above would land under the top bar; tooltip.js decides,
 * and the rule is one comparison rather than a search.
 *
 * The gap between the two is a margin rather than a number added in script, so the
 * offset stays on the spacing scale and script keeps handing over measurements only.
 */
.c-tip {
  transform: translateY(-50%);
  margin-left: var(--space-4);
}

.c-tip--bottom {
  transform: translateX(-50%);
  margin-left: var(--space-0);
  margin-top: var(--space-4);
}

.c-tip--above {
  transform: translate(-50%, -100%);
  margin-left: var(--space-0);
  margin-top: calc(var(--space-4) * -1);
}

/*
 * The pointer. A rotated square rather than a border triangle, so it inherits the
 * background and there is one colour to change instead of four.
 */
.c-tip::after {
  content: "";
  position: absolute;
  width: var(--space-3);
  height: var(--space-3);
  background: var(--ink);
  transform: rotate(45deg);
}

.c-tip::after {
  left: calc(var(--space-3) / -2);
  top: 50%;
  margin-top: calc(var(--space-3) / -2);
}

.c-tip--bottom::after {
  left: 50%;
  top: calc(var(--space-3) / -2);
  margin-top: 0;
  margin-left: calc(var(--space-3) / -2);
}

.c-tip--above::after {
  left: 50%;
  top: auto;
  bottom: calc(var(--space-3) / -2);
  margin-top: 0;
  margin-left: calc(var(--space-3) / -2);
}

/*
 * Fading in is the difference between a tooltip and a flicker, and it is also motion
 * that somebody asked the browser to stop.
 */
.c-tip { opacity: 1; transition: opacity var(--duration-fast) var(--ease); }

.c-tip--entering { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .c-tip { transition: none; }
}

/* ---- topbar.css ---- */
/*
 * topbar.css. The screen title pair, and the two controls that act on the sidebar.
 *
 * The root also carries .l-bar, which already provides sticky, height, padding, the
 * bottom rule, the surface and the flex row. Only what is inside the bar is here.
 *
 * .app-header, app.css 12671, is 0.75rem vertical and 15px horizontal padding rising
 * to 1.5rem both ways at the medium breakpoint. l-bar already resolves to that shape:
 * --bar-height is 64px, which is 0.75rem around a 40px control, and its horizontal
 * padding is --space-7 (1.5rem) falling to --space-5. So the bar's own metrics are
 * correct and stay in layout.css rather than being restated here.
 *
 * The type inside it was not correct. The title was --text-lg uppercase bold, which
 * is the prototype's masthead. The template's .card-title (app.css 7879) is the same
 * 1.125rem at weight 500 with a 24px line box, no text-transform (entry [047]), and
 * the subtitle takes
 * .card-text (app.css 7899): 0.875rem, weight 400, 1.25rem line box, slate-600.
 */

/*
 * align-items: baseline sits the subtitle on the title's baseline, which is right for two
 * pieces of type and wrong for the help control beside them: a 44px box on a text baseline
 * hangs below the row. `center` is what puts all three on one line. Entry [056].
 */
.c-topbar__heading {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  /* flex 1 1 0 at every width, not only under 900px. Entry [272]: above it the heading was as
     wide as its subtitle, so a sentence there pushed the title pair under the collapse control at
     1280 on six apps (wave 3 of [271]); with a basis of 0 the subtitle ellipsises instead. */
  flex: 1 1 0;
  min-width: 0;
}

/* 1.125rem on --leading-tight is a 1.5rem line box, which is .card-title exactly. */
.c-topbar__title {
  font-family: var(--font-cond);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: -0.02em;
  color: var(--ink);
  white-space: nowrap;
}

.c-topbar__subtitle {
  font-size: var(--text-sm);
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
  color: var(--ink-2);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * The bar sits beside the sidebar, and the sidebar becomes a drawer at 1180px. The
 * title pair stacks at the same width for the same reason. This file used to say
 * 820px, which left the two disagreeing across 360px of viewport.
 */
/* Entry [240]: the title keeps its row with the help control at every width, as the approved
   file draws it; only the subtitle drops under it below --bp-md. */
@media (max-width: 900px) {
  /* Entry [241]: one row. The heading takes what the tools leave and its title WRAPS inside
     that ("Plan and billing" is two lines at 360, as the file draws it) rather than keeping its
     one line and pushing the tools under it. The subtitle, which the approved file never draws
     in the bar, is not drawn here. */
  .c-topbar__heading {
    flex: 1 1 0;
    min-width: 0;
    align-items: center;
    padding: var(--space-0);
  }

  .c-topbar__title { white-space: normal; }
  .c-topbar__subtitle { display: none; }
}

/*
 * The lead slot, entry [044]. It holds both sidebar controls and is present at every
 * width; the two media queries below decide which of them is on screen.
 *
 * They are never both visible and they are never both useful. Below the drawer
 * breakpoint the sidebar is off canvas, so the only thing worth offering is "show it",
 * and collapsing something already off canvas would be a control that does nothing.
 * Above it the sidebar is always on screen, so the only thing worth offering is
 * "give the page that width back".
 */
.c-topbar__lead {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
}

/*
 * Both toggles are .c-iconbtn now, entry [045], so their shape, their hover and their
 * badge slot come from icon-button.css and this file only decides which one is on
 * screen. Before that they were a 44px rounded square defined here, which meant the
 * header had two kinds of icon button in one row: these on the left and the bell on the
 * right. The template's row has one.
 */
.c-topbar__collapse { display: none; }

/* One glyph, turned around, rather than a second symbol in the sprite. It points at
   the sidebar when the sidebar is there to point at, and away from it when it is not. */
.c-topbar__collapse-glyph { transition: transform var(--duration-base) var(--ease); }

.l-shell--rail .c-topbar__collapse-glyph { transform: rotate(180deg); }

/*
 * The mobile brand mark, entry [045]. index.html 580: the template puts the compact
 * square in the header and hides it at the width where the sidebar appears.
 *
 * Ours is here for a sharper reason than symmetry. Below the drawer breakpoint the
 * sidebar is off canvas, and the sidebar is the only place this panel says which
 * installation you are looking at. So on a phone, until now, nothing on screen said
 * whose control plane this was. It is the nav's own mark, .c-nav__mark, rather than a
 * second one: same 32px, same fallback square, and one uploaded icon feeds both.
 */
.c-topbar__mark {
  display: none;
  align-items: center;
  flex: 0 0 auto;
}

/* The site chip leaves under --bp-md, the approved file's own rule for it: the domain is on the
   stacked sidebar's own mark by then and the bar has four controls in 360px. */
@media (max-width: 900px) {
  .l-bar__tools [data-topbar="site"] { display: none; }
}

/* Entry [240]: no drawer, so no hamburger and no compact mark; the sidebar is on screen at every
   width, stacked above the page under --bp-md. Entry [241], the owner's ruling: the collapse
   control is drawn at every width as the approved file draws it (under --bp-md the rail state
   it toggles has nothing to act on, nav-rail.css being scoped above 900), and the bar is ONE
   row: the tools' labels are hidden and their icons stand alone. */
.c-topbar__collapse { display: inline-flex; }

@media (max-width: 900px) {
  .l-bar__tools .c-button .c-button__label { display: none; }
  /* Icon only, so the icon-only rule's shape: a --touch-min square, honestly 44 wide, not the
     file's 40 [241 ruling 10]. check-slow measured 38x36 on the label-less button before this. */
  .l-bar__tools .c-button {
    min-width: var(--touch-min);
    padding-inline: var(--space-0);
    justify-content: center;
  }
  .c-topbar__heading, .c-topbar__lead { gap: var(--space-2); }
}

@media (prefers-reduced-motion: reduce) {
  .c-topbar__collapse-glyph { transition: none; }
}

/* Entry [241]: the approved file's control sizes where the 44px floor allows. The collapse
   control's painted face is its 38px navy circle (the hit area stays --touch-min), and the
   account avatar beside it is its 34px. Both are the file's own numbers, not scale steps. */
.c-topbar__collapse .c-iconbtn__face {
  width: 2.375rem;
  height: 2.375rem;
  background: var(--brand);
  color: var(--brand-ink);
}

.l-bar__tools .c-menu__profile .c-avatar--md {
  width: var(--control-h-sm);
  height: var(--control-h-sm);
}

/* The site chip, entry [242]: the approved file's pill. A 400 weight domain in the muted ink on a
   999px radius, the hover taking the action border and the action's darker ink. Its height is
   the outline sm button's, 34, [241 ruling 10]. */
.l-bar__tools [data-topbar="site"] {
  border-radius: var(--radius-pill);
  font-weight: var(--weight-normal);
  color: var(--ink-2);
}

.l-bar__tools [data-topbar="site"]:not([disabled]):hover {
  border-color: var(--action-border);
  color: var(--action-text);
}

/* ---- verdict-strip.css ---- */
/*
 * verdict-strip.css. Eighteen categorical verdicts in one row.
 *
 * No template source: the template has no indicator strip, in the same way it has no lamp
 * (docs/design.md 5.2). The geometry is derived from the two things it has to sit beside on
 * the report page. The label is meter.css's label exactly, because a strip and a meter stand
 * in the same column and their labels have to be one typeface at one size. The cell height is
 * the spacing scale rather than a measurement, since nothing in the template is this shape.
 *
 * ## Every cell is a class, and there is no style attribute anywhere
 *
 * The Content Security Policy allows no style attribute at all since [293] (style-src-attr
 * 'none'); even meter's fill percentage arrives as data attributes. This component is
 * categorical: four states, four rules, nothing to compute. House rule 3.
 *
 * ## Four states, and adding a fifth is a decision, not an edit
 *
 * pass, warning, issue, idle. `not_measured` is normalised to `idle` in the partial. The
 * verdict names are the scanner's own, from the scan_results ENUM, and are mapped to semantic
 * tokens here rather than renamed on the way in: see the header of verdict-strip.php.
 *
 * ## Colour is not the only signal, and here is where the words are
 *
 * design.md section 7. Eighteen labels cannot be drawn beside eighteen 18px cells, so the
 * words live in three other places rather than nowhere: visually hidden text inside every
 * cell, the same words in `title` on hover and the list's own tally in its accessible name.
 * The report page then prints all eighteen again underneath as findings, each with a `tag`
 * carrying the verdict as a word. The strip is the glance; it is never the only telling.
 *
 * `idle` additionally differs in SHAPE and not only in colour: it is hollow with a dashed
 * edge where the three measured states are solid. That is the one distinction worth drawing
 * without colour, because "we did not measure this" against "we measured it and it passed" is
 * the pair a reader most needs kept apart, and grey against green is the pair colour vision
 * deficiency most often loses. It is a hollow, not a height: nothing here encodes magnitude,
 * because there is no magnitude to encode.
 */

.c-verdict-strip {
  display: block;
  min-width: 0;
}

/* meter.css's label, to the value. .form-label, app.css 9503. */
.c-verdict-strip__label {
  display: block;
  margin-bottom: var(--space-3);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-base);
  letter-spacing: normal;
  color: var(--ink-2);
}

/*
 * The row. A real <ul>, so its markers are reset here; the partial states role="list" beside
 * it, because WebKit drops list semantics from a list with no marker.
 *
 * The gap is --space-1, 2px, which is the smallest step on the scale. Eighteen cells and
 * seventeen gaps inside a 360px viewport leaves each cell about 18px wide, which is a
 * legible block. Nothing is fixed width, so the same eighteen stretch to fill 2560px.
 */
.c-verdict-strip__cells {
  display: flex;
  align-items: stretch;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
  min-width: 0;
}

/*
 * flex-basis 0 with a min-width floor: every cell takes an equal share of whatever width
 * there is, and none of them can be squeezed out of existence when there are many.
 *
 * The cells are not interactive and carry no tab stop, so the 44px floor in house rule 10
 * does not apply to them. Nothing here is pressable; the per check controls are `button`
 * calls in the findings list below the strip, which are.
 */
.c-verdict-strip__cell {
  flex: 1 1 0;
  min-width: var(--space-2);
  height: var(--space-8);
  border: var(--border-width) solid transparent;
  border-radius: var(--radius-xs);
  background: var(--muted);
}

.c-verdict-strip__cell--pass {
  background: var(--success);
  border-color: var(--success-dim);
}

.c-verdict-strip__cell--warning {
  background: var(--warning);
  border-color: var(--warning-dim);
}

.c-verdict-strip__cell--issue {
  background: var(--danger);
  border-color: var(--danger-dim);
}

/* Hollow, and dashed. The shape says "no measurement", the colour only agrees with it. */
.c-verdict-strip__cell--idle {
  background: var(--muted-subtle);
  border-style: dashed;
  border-color: var(--muted-dim);
}

