/*
 * Console-only branding. AdminJS also loads this stylesheet on the sign-in
 * page, so every rule is anchored to the sidebar, which the sign-in DOM does
 * not render.
 */

/* Figma 124:5218: the colour and logo strip at the top of the side navigation. */
#app [data-css='sidebar'] > [data-css='sidebar-logo'] {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 64px;
  width: 100%;
  height: 64px;
  padding: 0 !important;
  overflow: hidden;
  background: #191a1d;
}

/* Keep the exported blur layer at the same bounds used in the Figma frame. */
#app [data-css='sidebar'] > [data-css='sidebar-logo']::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: -174% -21.48% -301.33% -20.55%;
  background: url('/admin-assets/svgs/sidebar-glow.svg') center / 100% 100%
    no-repeat;
  pointer-events: none;
}

#app [data-css='sidebar'] > [data-css='sidebar-logo'] > img {
  position: relative;
  z-index: 1;
  display: block;
  width: 80px;
  height: 40px;
  max-width: none;
  content: url('/admin-assets/svgs/sidebar-logo.svg');
}

/* ---------------------------------------------------------------------------
 * The navigation tree (`src/components/sidebar-nav.ts`).
 *
 * 🔴 These rules only reach the DOM because we stopped using the design
 * system's `Navigation`. `NavigationElementProps` has no `className` and drops
 * `id` before render, so every state it drew was a generated styled-components
 * class this stylesheet could not name. `sidebar-nav.ts` explains the swap.
 *
 * ## The gradient
 *
 * Figma 1257:32928 — the app's `overlayHeader`, at the frame's own 40/60
 * stops: rose bleeds in from the left edge, obsidian holds the middle two
 * fifths, sky bleeds in from the right. It is the one piece of chrome in the
 * product that carries both accent colours at once, which is why
 * it is the right thing to spend on "you are here" and the wrong thing to
 * spend on anything else in this file.
 *
 *   Electric Rose/80  #ff2e92cc      Obsidian Night/100  #191a1d
 *   Electric Sky/80   #00a3e0cc      Pure White/100      #ffffff
 *
 * 🔴 **The base stays OBSIDIAN, as Figma draws it.** A white-based version was
 * built on 2026-09-01 and reverted the same day: inverting it to lighten the
 * row cost the pill its shape — the tinted ends over #fff left the middle of
 * the row looking like bare panel, and the 1px ring added to get the shape
 * back read as a form field rather than a selection. The weight of a dark row
 * is what makes it legible as "you are here" from across the panel. If it ever
 * needs to sit lighter, move the ALPHA, not the base.
 *
 * ⚠️ The alphas differ per surface, and there is no single number to quote:
 *
 *   the pill                      0.45  — Figma's frame draws 0.8, on a 390px
 *                                         app header where the gradient IS the
 *                                         surface. This is a 268px row
 *                                         repeating down a white panel, and at
 *                                         0.8 the rose end read as a second
 *                                         brand colour rather than as a state
 *   the rail and the guide line   0.8   — a 1–3px mark any lighter than that
 *                                         disappears against #e7e8ed
 *
 * Read the alpha off the rule, never off this block.
 *
 * 🔴 The second layer is not decoration. The first is translucent at both
 * ends, so without an opaque obsidian beneath it the pill would take its ends
 * from the sidebar's white and read as pastel. Figma draws the same two layers
 * for the same reason.
 *
 * ## Three states, three weights
 *
 * The design system drew selected as `rgba(#191A1D, 0.2)` and hover as
 * `rgba(text, 0.2)` — the same colour at the same alpha, on a theme where
 * `text` IS `#191A1D`. So the sidebar had one visual state doing two jobs and
 * an operator could not tell the row under the cursor from the page on screen.
 *
 *   the page on screen        the gradient pill, white text
 *   the section it is in      ink text at 600, and a gradient rail or guide
 *   everything else           grey60 icon, ink text at 400, flat hover
 *
 * ⚠️ The middle one is ours to compute — see `hasSelectedDescendant`. AdminJS
 * marks leaf resources only.
 * ------------------------------------------------------------------------- */

#app [data-css='sidebar'] .vrto-nav {
  padding: 16px;
}

/* The "Pages" heading. Same treatment AdminJS's `Label uppercase` gave it, in
   CSS, because the `Label` atom went with `Navigation`. */
#app [data-css='sidebar'] .vrto-nav__heading {
  margin: 0 0 8px 12px;
  font-size: 12px;
  font-weight: 400;
  line-height: 16px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  /* 🔴 #6b6c71 (5.24:1), not the design system's grey60 family. This was
     #898a8f, which measures 3.45:1 on white — under WCAG 1.4.3's 4.5:1 for
     12px text. The icon and chevron below keep that value because 3.45:1
     clears the 3:1 that 1.4.11 asks of a non-text glyph. */
  color: #6b6c71;
}

#app [data-css='sidebar'] .vrto-nav__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The row. `position: relative` is load-bearing — both the collapsed-section
   rail and the sub-list guide are `::before` boxes positioned against it. */
#app [data-css='sidebar'] .vrto-nav__item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 12px;
  border-radius: 8px;
  color: #191a1d;
  font-size: 14px;
  font-weight: 400;
  line-height: 20px;
  text-decoration: none;
  background: transparent;
  cursor: pointer;
  user-select: none;
  transition:
    background-color 0.12s ease,
    color 0.12s ease;
}

/* 🔴 A heading is a `<button>` — see the `interactive` const in
   `sidebar-nav.ts`. A button is not an anchor: it does not fill its `<li>`,
   it centres its text and it brings a border and a UA font. Scoped to
   `button` so an anchor row's box is untouched; everything else the row needs
   (font size, weight, line-height, transparent background) is already
   declared above and applies to both. */
#app [data-css='sidebar'] button.vrto-nav__item {
  box-sizing: border-box;
  width: 100%;
  border: 0;
  font-family: inherit;
  text-align: left;
}

@media (prefers-reduced-motion: reduce) {
  #app [data-css='sidebar'] .vrto-nav__item {
    transition: none;
  }
}

#app [data-css='sidebar'] .vrto-nav__icon,
#app [data-css='sidebar'] .vrto-nav__chevron {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  color: #898a8f;
}

/* The chevron is the only thing in the row that is not the row's subject, so
   it sits at the far end and stays quiet at every state. */
#app [data-css='sidebar'] .vrto-nav__chevron {
  margin-left: auto;
}

#app [data-css='sidebar'] .vrto-nav__text {
  min-width: 0;
  flex-grow: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Flat, and nowhere near the selected treatment. A 20% black wash was what
   made hover and selected the same row. */
#app [data-css='sidebar'] .vrto-nav__item:hover {
  background-color: #f4f5f7;
}

/* `:focus-visible`, and the console's one focus ring — the same rule and the
   same reason as the collapse button above. */
#app [data-css='sidebar'] .vrto-nav__item:focus-visible {
  outline: 2px solid #00a3e0;
  outline-offset: -2px;
}

/* ── The page on screen ─────────────────────────────────────────────────── */

#app [data-css='sidebar'] .vrto-nav__item.is-selected {
  color: #fff;
  font-weight: 500;
  background-image:
    linear-gradient(
      90deg,
      rgba(255, 46, 146, 0.45) 0%,
      #191a1d 40%,
      #191a1d 60%,
      rgba(0, 163, 224, 0.45) 100%
    ),
    linear-gradient(90deg, #191a1d 0%, #191a1d 100%);
}

/* 🔴 Both glyphs, and `inherit` rather than `#fff`. The chevron only ever
   appears on a selected row if a heading is itself the current page, which no
   section is today — writing the colour twice would be a rule nobody could
   test. Inheriting keeps it correct if one ever is. */
#app [data-css='sidebar'] .vrto-nav__item.is-selected .vrto-nav__icon,
#app [data-css='sidebar'] .vrto-nav__item.is-selected .vrto-nav__chevron {
  color: inherit;
}

/* Hover must not lift the pill off its own gradient. */
#app [data-css='sidebar'] .vrto-nav__item.is-selected:hover {
  background-color: transparent;
}

/* ── The section that page is in ────────────────────────────────────────── */

#app [data-css='sidebar'] .vrto-nav__item.is-current-section {
  font-weight: 600;
}

#app [data-css='sidebar'] .vrto-nav__item.is-current-section .vrto-nav__icon {
  color: #191a1d;
}

/* 🔴 A COLLAPSED heading only. Open, the guide line below already draws the
   thread from this heading down to the pill, and two gradient marks on one
   branch read as two selections.
   `sidebar-resource-section.test.ts` states the case this rail answers: a
   shared link lands on a resource whose heading the operator last left closed,
   and until now nothing on the sidebar said so. */
#app
  [data-css='sidebar']
  .vrto-nav__item.is-current-section:not(.is-open)::before {
  content: '';
  position: absolute;
  top: 8px;
  bottom: 8px;
  left: 0;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: linear-gradient(
    180deg,
    rgba(255, 46, 146, 0.8) 0%,
    rgba(0, 163, 224, 0.8) 100%
  );
}

/* ── The children ───────────────────────────────────────────────────────── */

/* Indented by the icon's own width and gap, so a child's label starts under
   its parent's label rather than under its parent's icon. The guide sits on
   the icon's centre line. */
#app [data-css='sidebar'] .vrto-nav__list--sub {
  position: relative;
  margin: 2px 0 8px;
  padding-left: 20px;
}

#app [data-css='sidebar'] .vrto-nav__list--sub::before {
  content: '';
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: 20px;
  width: 1px;
  background: #e7e8ed;
}

/* The thread. Vertical, because the list is — the same two colours as the
   pill, turned through ninety degrees. */
#app
  [data-css='sidebar']
  .vrto-nav__item.is-current-section
  + .vrto-nav__list--sub::before {
  background: linear-gradient(
    180deg,
    rgba(255, 46, 146, 0.8) 0%,
    rgba(0, 163, 224, 0.8) 100%
  );
}

#app [data-css='sidebar'] .vrto-nav__item--sub {
  margin-left: 12px;
  padding-left: 12px;
}

/* ---------------------------------------------------------------------------
 * Collapsing the sidebar (`src/components/sidebar-toggle.ts`,
 * `src/components/top-bar.ts`, `public/console.js`).
 *
 * 🔴 EVERY number in this block is 1324px or derived from it, and it is not a
 * round number somebody liked. It is the last entry in the design system's
 * breakpoint array (`@adminjs/design-system/src/theme.ts` — the comment there
 * reads `1024 + sidebarWidth`), and it is the width at which `StyledSidebar`
 * switches from `position: absolute` to `position: initial`.
 *
 * Below it the sidebar is an overlay drawer that AdminJS slides in from
 * `left: -300px` and its own hamburger controls. Zeroing the width there would
 * take the drawer away and leave the hamburger pointing at nothing. So the
 * collapsed rule is scoped to the in-flow side of that line, and the button is
 * hidden on the other side of it. Move one of these queries and you have to
 * move the other, and neither can move without AdminJS moving first.
 * ------------------------------------------------------------------------- */

/* The transition lives outside the media query, and it RESTATES `left`.
 * `StyledSidebar` declares `transition: left 0.25s ease-in-out` for the mobile
 * drawer; a bare `transition: width` here would replace that declaration
 * wholesale and the drawer would start snapping. Both, or neither. */
#app [data-css='sidebar'] {
  transition:
    width 0.16s ease,
    left 0.25s ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
  #app [data-css='sidebar'] {
    transition: none;
  }
}

/* The whole collapse. `width: 0` rather than `display: none` so the width can
 * animate; `overflow: hidden` because the sidebar's children keep their own
 * widths and would otherwise spill across the content pane while it runs.
 *
 * The border is a separate declaration on purpose — a 1px rule left standing
 * at the window's left edge reads as a broken layout rather than as a closed
 * panel. `min-width` because the sidebar is a flex item and `flex-shrink: 0`
 * with an `auto` basis will refuse to go below its content's width without it.
 *
 * Specificity, deliberately: `html[…] #app [data-css='sidebar']` outranks the
 * single generated class styled-components writes the 300px into, so no
 * `!important` is needed here — which matters, because an `!important` width
 * would also win against the mobile drawer's rules below 1324px. */
@media (min-width: 1324px) {
  html[data-vrto-sidebar='collapsed'] #app [data-css='sidebar'] {
    width: 0;
    min-width: 0;
    border-right-width: 0;
    overflow: hidden;
  }
}

/* --- The top bar (`src/components/top-bar.ts`) ---------------------------- */

/* The override renders a plain `Box` rather than AdminJS's `NavBar` styled
 * component, so the three declarations that made it a bar live here. The
 * colours are literals and not `var(--vrto-*)`: those tokens are declared on
 * the PAGE ROOTS (see the block below), and the bar is a sibling of every one
 * of them rather than a descendant. */
#app .vrto-topbar {
  height: 64px;
  border-bottom: 1px solid #e7e8ed;
  background: #fff;
}

/* --- The collapse button -------------------------------------------------- */

/* Hidden below 1324px, where AdminJS's own hamburger is the control and the
 * sidebar is a drawer. Exactly one of the two is ever on screen. */
#app .vrto-sidebar-toggle {
  display: none;
}

@media (min-width: 1324px) {
  #app .vrto-sidebar-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    /* 16px from the pane's edge, which lines the button up with the 24px
       gutter `Version` sets beside it once its own padding is counted. */
    margin: 0 4px 0 16px;
    padding: 0;
    border: 0;
    border-radius: 6px;
    background: none;
    color: #5c5f66;
    cursor: pointer;
  }

  #app .vrto-sidebar-toggle:hover {
    background: #f7f7f9;
    color: var(--vrto-ink, #191a1d);
  }

  /* `:focus-visible` and not `:focus` — the ring is for the keyboard. A ring
     that appears on every mouse click on a control an operator hits dozens of
     times a day is the reason people ask for `outline: none`. */
  #app .vrto-sidebar-toggle:focus-visible {
    outline: 2px solid #00a3e0;
    outline-offset: 2px;
  }

  #app .vrto-sidebar-toggle__icon {
    display: block;
  }
}

/* ---------------------------------------------------------------------------
 * Dashboard (`src/components/dashboard.ts`).
 *
 * Scoped under `.vrto-dashboard`, which the sign-in DOM does not render — this
 * stylesheet is loaded on both pages.
 * ------------------------------------------------------------------------- */

/* 🔴 On BOTH page roots, not on `.vrto-dashboard` alone. `.vrto-operators` is a
 * sibling of it (`components/operators.ts` renders its own `main`), not a
 * descendant, so tokens declared only on the dashboard did not cascade there:
 * all nine `var(--vrto-*)` uses in the operators block below resolved to
 * nothing and every declaration holding one was dropped — the table borders,
 * the muted intro, the dark Add button. Still not on `:root`, because this
 * stylesheet is loaded on the sign-in page too. `.vrto-support` is the third
 * root — the Set New Password drawer on Users → show — and `.vrto-docs` the
 * fourth, the Documents page.
 *
 * 🔴 `.vrto-listhead` is the SEVENTH, and it is the first one that is not a
 * custom page. It is the header of every stock resource LIST
 * (`src/components/list-header.ts`), so it renders inside AdminJS's own layout
 * rather than inside a `main` this console wrote — which is exactly why it has
 * to be named here. Sitting under a page root is what the other six get for
 * free; this one does not, and every `var(--vrto-*)` in its block below would
 * resolve to nothing without this line. Reproduced once already on
 * `.vrto-operators`, described above.
 *
 * 🔴 `.vrto-pagehead` is the NINTH, and it is the shared row that the list and
 * show headers are both built out of (`components/list-header.ts`,
 * `components/show-header.ts`). `.vrto-listhead` below it is now only the
 * list's own filter button and panel; the row's own rule, which is a
 * `var(--vrto-line)`, hangs off THIS name. Leaving it out would drop that
 * border on both screens at once — the fourth time, in the widest place yet.
 *
 * 🔴 `.vrto-show` is the EIGHTH, and it was added because the omission had
 * already bitten. It is the Users → show wrapper
 * (`resources/user.resource.ts`, `actions.show.layout`), and like
 * `.vrto-listhead` it renders inside AdminJS's own layout. Before it existed,
 * the nutrition panel on that page emitted `.vrto-support__row` and
 * `.vrto-support__error` with no `--vrto-*` root above them: `var(--vrto-muted)`
 * resolved to nothing, so the "muted" row LABELS rendered at the same
 * `#191a1d` as their values — the panel whose whole job is comparing two
 * numbers, drawing both halves in one colour — and the error box lost its left
 * border entirely. That is the third time this list has been the bug.
 *
 * 🔴 `.vrto-mr` is the TENTH, and it was the FOURTH time — found 2026-09-05,
 * while adding the eleventh. Recipe coverage renders its own `main` and was
 * never named here, so **8 of the 11** `var(--vrto-*)` uses in its block
 * resolved to nothing: four `--vrto-muted`, three `--vrto-line` and one
 * `--vrto-ink`. Every declaration holding one was dropped — the muted captions
 * rendered as body ink, the group cards lost their borders. The page had
 * already shipped once with no stylesheet at all (see its own block below);
 * this is the same page losing its colours a second way.
 *
 * ⚠️ This paragraph said ELEVEN, and named `--vrto-hover`, `--vrto-accent` and
 * `--vrto-danger` among the dead. Those three are the block's only
 * `var(--token, fallback)` uses, so they rendered their fallback exactly as
 * written — before the fix and after it. Counting every `var()` rather than
 * the bare ones inflated the finding by three and named the wrong three, in
 * the very note that exists because this list keeps being confidently wrong.
 * A `var()` with a fallback is not evidence of anything.
 *
 * 🔴 `.vrto-cv` is the ELEVENTH — Cutover → Exercise videos, another page root
 * rendering its own `main`. Added here in the same edit that created it,
 * because reading this note is what stopped it being the fifth. */
.vrto-dashboard,
.vrto-operators,
.vrto-support,
.vrto-show,
.vrto-docs,
.vrto-match,
.vrto-amt,
.vrto-mr,
.vrto-cv,
.vrto-listhead,
.vrto-pagehead {
  --vrto-ink: #191a1d;
  --vrto-muted: #5c5f66;
  --vrto-line: #e7e8ed;
  --vrto-accent: #ff623e;
  /* 🔴 A SECOND red, and the difference is contrast, not taste.
   *
   * `--vrto-accent` is the brand orange-red and it is fine as a border or a
   * 3px rule — non-text, so no ratio applies. It is NOT fine as text or behind
   * white text: 2.97:1 against white, where AA wants 4.5:1 at the 12–13px this
   * console sets. It was doing both jobs, and failed at the second in the two
   * places that matter most — the danger menu item and the destructive confirm
   * button, which are the controls it is most important a person can read.
   *
   * 5.3:1 against white, so it clears AA in both directions: as ink on the
   * white list background, and as a background under white button text. Keep
   * `--vrto-accent` for decoration; use this the moment a glyph is involved. */
  --vrto-danger: #c8341a;

  /* The rest of the Figma palette, from the Colour frame on the `📄 Read Me`
   * page of `3gH5znDTt43rJBwpQ4NNEO`. Added for the Documents page, which
   * needs a status colour for each of four states, and has no licence to
   * invent them. */
  --vrto-sky: #00a3e0; /* Electric Sky — brand primary */
  --vrto-rose: #ff2e92; /* Electric Rose */
  --vrto-green: #00d084;
  --vrto-yellow: #ffea70;
  --vrto-deep: #003c5d;
}

/* 🔴 `.vrto-docs` is deliberately NOT here — it takes the tokens above and
 * sets its own 880px measure in the Documents block below. */
.vrto-dashboard,
.vrto-operators,
.vrto-support {
  /* Left-aligned: no `margin: 0 auto`. One tile centred in a 1180px column
   * reads as a page that failed to load the rest of itself. Two of these three
   * roots lift the cap again below — the dashboard and the drawer — so this
   * number only actually governs `.vrto-operators`. */
  max-width: 1180px;
  padding: 32px 24px 64px;
  color: var(--vrto-ink);
}

/* No 1180px cap here — it left a dead band on the right of any window wider
 * than about 1500px, and the band was visible as three separate things: the
 * full-width chart tile stopping short of the edge, the period selector
 * right-aligning to the column rather than the page, and plain empty space.
 * A metrics page has no line-length argument for a cap; `.vrto-operators`
 * keeps one because its table cells hold prose. The padding stays — it is what
 * holds the tiles off the sidebar. */
.vrto-dashboard {
  max-width: none;
}

.vrto-dashboard h1,
.vrto-operators h1 {
  margin: 0;
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* The heading and the period selector on one row, and wrapping to two on a
 * narrow window rather than squeezing the select to nothing. */
.vrto-dashboard__header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px 24px;
}

/* One selector for the page, not one per tile — `dashboard.ts` says why. */
.vrto-dashboard__range {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  font-size: 13px;
  /* Right edge of the header even when the heading wraps to its own line. */
  margin-left: auto;
}

.vrto-dashboard__range label {
  font-weight: 600;
}

/* Width only. The box belongs to `.vrto-select`, the same as on Operators —
   see the note above that rule for why a page never restyles the control. */
.vrto-dashboard__range .vrto-select {
  min-width: 140px;
}

/* Flex rather than a grid of `1fr` columns: a single tile in a grid stretches
 * to the full width of the page. These size to their content and pack left. */
.vrto-dashboard__tiles {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 20px;
}

.vrto-tile {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  gap: 4px;
  min-width: 220px;
  padding: 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
  text-decoration: none;
  color: inherit;
}

.vrto-tile--link:hover {
  border-color: var(--vrto-ink);
}

.vrto-tile__value {
  font-size: 30px;
  font-weight: 700;
  line-height: 1.1;
}

.vrto-tile__label {
  font-size: 13px;
  font-weight: 600;
}

/* The one tile that draws a shape. Wide because a 30-point line in a 220px box
   is a smudge, and `flex: 1 1 100%` puts it on its own row under the counts
   rather than wedging it between two of them. */
.vrto-tile--wide {
  flex: 1 1 100%;
  min-width: 100%;
}

/* The trend chart. Text is HTML and geometry is SVG — `dashboard.ts` says why
   the two cannot be mixed under a non-uniform stretch. */
.vrto-chart {
  margin-top: 12px;
}

.vrto-chart__body {
  display: flex;
  gap: 8px;
}

/* 🔴 This height is the `viewBox` height in `CHART`, and the two must stay
   equal — that is what makes the vertical scale 1:1 and lets the HTML marker
   and readout be positioned from the polyline's own fractions. */
.vrto-chart__plot,
.vrto-chart__axis-y {
  height: 120px;
}

/* The peak and zero labels, absolutely positioned onto their own gridlines
   rather than pushed to the ends of a flex column, so they line up exactly. */
.vrto-chart__axis-y {
  position: relative;
  flex: 0 0 auto;
  width: 34px;
}

.vrto-chart__tick {
  position: absolute;
  right: 0;
  transform: translateY(-50%);
  color: var(--vrto-muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.vrto-chart__plot {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}

/* Keyboard reaches this, so it must show where it is. Offset outward so the
   ring does not sit on top of the baseline.

   🔴 `--vrto-danger`, NOT `--vrto-accent`. The token note at the top of this
   file measures accent at 2.97:1 against white and restricts it to non-text
   decoration on that basis — but WCAG 2.1 SC 1.4.11 asks 3:1 of a focus
   indicator too, and the plot sits on white, so accent was a hair under the
   threshold for the one thing on this chart a keyboard operator cannot do
   without. It is the only signal of where they are in the series. */
.vrto-chart__plot:focus-visible {
  outline: 2px solid var(--vrto-danger);
  outline-offset: 3px;
}

/* `display: block` because an inline SVG sits on the text baseline and leaves a
   descender's worth of gap under it. `overflow: visible` so a stroke centred on
   the peak or the baseline is not clipped in half. */
.vrto-chart__svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.vrto-chart__grid {
  stroke: var(--vrto-line);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.vrto-chart__line {
  fill: none;
  stroke: var(--vrto-accent);
  stroke-width: 1.5;
  /* Non-scaling so the stretch above cannot thin the line to nothing at one
     end of a 90-day series and fatten it at the other. */
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.vrto-chart__cursor {
  stroke: var(--vrto-ink);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  vector-effect: non-scaling-stroke;
}

/* HTML, not an SVG `<circle>`, which the horizontal stretch would flatten into
   an ellipse — wider on a 7-day range than a 90-day one. */
.vrto-chart__dot {
  position: absolute;
  width: 7px;
  height: 7px;
  margin: -4px 0 0 -4px;
  border: 1.5px solid #fff;
  border-radius: 50%;
  background: var(--vrto-accent);
  pointer-events: none;
}

.vrto-chart__readout {
  position: absolute;
  top: -4px;
  z-index: 1;
  display: flex;
  gap: 6px;
  align-items: baseline;
  padding: 3px 7px;
  border: 1px solid var(--vrto-line);
  border-radius: 5px;
  background: #fff;
  box-shadow: 0 1px 4px rgb(0 0 0 / 12%);
  font-size: 11px;
  line-height: 1.3;
  white-space: nowrap;
  /* The pointer is tracked on the plot, so the readout must never become the
     thing under it — it would flicker as it stole and lost its own hover. */
  pointer-events: none;
}

/* Centred over its day, except at the ends, where centring would push half of
   it outside the plot and the browser would clip it. */
.vrto-chart__readout--mid {
  transform: translateX(-50%);
}

.vrto-chart__readout--start {
  transform: translateX(0);
}

.vrto-chart__readout--end {
  transform: translateX(-100%);
}

.vrto-chart__readout-value {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.vrto-chart__readout-day {
  color: var(--vrto-muted);
}

/* Indented by the y gutter and its gap, so the first date sits under the first
   point rather than under the scale beside it. */
.vrto-chart__axis-x {
  display: flex;
  justify-content: space-between;
  margin: 6px 0 0 42px;
  color: var(--vrto-muted);
  font-size: 11px;
}

.vrto-dashboard__loading,
.vrto-operators__loading {
  padding: 32px 0;
  font-size: 14px;
  color: var(--vrto-muted);
}

.vrto-dashboard__error {
  margin: 20px 0;
  padding: 12px 16px;
  border-left: 3px solid var(--vrto-accent);
  background: #fff5f2;
  font-size: 13px;
}

/* A refresh that failed, said beside the period picker while the previous
   period's tiles stay on screen — `dashboard.ts` explains why this is not the
   full-page error above.

   🔴 `flex-basis: 100%` inside `.vrto-dashboard__header`, which is a wrapping
   flex row: without it this sits in the row and squeezes the heading and the
   select. It takes its own line under both.

   `--vrto-danger`, not `--vrto-accent`: this is a glyph, and the token note at
   the top of this file puts accent at 2.97:1 against white. */
.vrto-dashboard__stale {
  flex-basis: 100%;
  margin: 0;
  padding: 8px 12px;
  border-left: 3px solid var(--vrto-danger);
  background: #fff5f2;
  color: var(--vrto-ink);
  font-size: 13px;
}

/* Not an error, and styled so it cannot be mistaken for one. `content` holds no
   `people.read`, so it gets no tiles — see `components/dashboard.ts`. Muted
   body text rather than the bordered red panel above, because nothing is
   wrong. */
.vrto-dashboard__empty {
  margin: 20px 0;
  font-size: 13px;
  color: var(--vrto-muted);
}

/* Operators — Phase 4's assignment screen. Same tokens and page chrome as the
   dashboard (declared on both roots above). The roster is the page; Add and
   Invite are header actions that open one panel, never two stacked cards. */
.vrto-operators__header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 24px;
}

.vrto-operators__meta {
  max-width: 62ch;
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--vrto-muted);
}

.vrto-operators__toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.vrto-operators__action,
.vrto-operators__cancel {
  padding: 8px 14px;
  border: 1px solid var(--vrto-line);
  border-radius: 4px;
  background: #fff;
  color: var(--vrto-ink);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

.vrto-operators__action:hover,
.vrto-operators__cancel:hover {
  border-color: var(--vrto-ink);
}

.vrto-operators__action[aria-expanded='true'] {
  background: #f7f7f9;
}

.vrto-operators__action--primary {
  border-color: var(--vrto-ink);
  background: var(--vrto-ink);
  color: #fff;
}

.vrto-operators__action--primary:hover {
  border-color: var(--vrto-ink);
}

/* The per-row `Add role` control is `.vrto-menu` now, at the foot of this file.
   It used to be `.vrto-operators__grant`, a `<select>` that granted a Cognito
   group from `onChange` — see `components/ui/menu-button.ts` for why that was
   the wrong element. */

.vrto-operators__add button:disabled,
.vrto-operators__invite button:disabled {
  opacity: 0.5;
  cursor: default;
}

.vrto-operators__panel {
  margin: 0 0 24px;
  padding: 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
}

.vrto-operators__panel-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}

.vrto-operators__panel-head h2 {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
}

.vrto-operators__hint {
  max-width: 62ch;
  margin: 0 0 16px;
  font-size: 13px;
  color: var(--vrto-muted);
}

/* Add and Invite share every rule below. They are two forms because the
   operations mean different things — see the component — and one visual
   treatment because they are the same kind of control in the same place. A
   second set of selectors would be where they silently drift apart. */
.vrto-operators__add,
.vrto-operators__invite {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px;
}

.vrto-operators__add label,
.vrto-operators__invite label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12px;
  font-weight: 600;
}

.vrto-operators__add input,
.vrto-operators__invite input {
  padding: 8px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 4px;
  font-size: 13px;
}

/* Width only. 🔴 The box itself belongs to `.vrto-select` further down, and
   this selector outranks it (0,2,0 against 0,1,0) — every property repeated
   here would win, which is how the Role picker kept the browser's own chrome
   after it became a shared component. Layout here, appearance there. */
.vrto-operators__add input,
.vrto-operators__add .vrto-select,
.vrto-operators__invite input,
.vrto-operators__invite .vrto-select {
  min-width: 240px;
}

.vrto-operators__add button[type='submit'],
.vrto-operators__invite button[type='submit'] {
  padding: 9px 16px;
  border: 1px solid var(--vrto-ink);
  border-radius: 4px;
  background: var(--vrto-ink);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

/* One focus ring for the whole console, and 🔴 one selector list for it. A
   second block is where the two silently drift apart — the same reason the Add
   and Invite forms share every rule above. Anything focusable added to `ui/`
   joins this list. */
.vrto-operators__action:focus-visible,
.vrto-operators__cancel:focus-visible,
.vrto-operators__add button:focus-visible,
.vrto-operators__invite button:focus-visible,
.vrto-operators__add input:focus-visible,
.vrto-operators__invite input:focus-visible,
.vrto-select:focus-visible,
.vrto-menu__trigger:focus-visible,
.vrto-menu__item:focus-visible,
.vrto-dialog__cancel:focus-visible,
.vrto-dialog__confirm:focus-visible {
  outline: 2px solid var(--vrto-ink);
  outline-offset: 2px;
}

.vrto-operators__table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.vrto-operators__table th,
.vrto-operators__table td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--vrto-line);
  text-align: left;
  vertical-align: middle;
}

.vrto-operators__table th {
  font-size: 12px;
  font-weight: 600;
  color: var(--vrto-muted);
}

.vrto-operators__table th:last-child,
.vrto-operators__table td:last-child {
  text-align: right;
}

.vrto-operators__table td.vrto-operators__empty {
  padding: 20px 12px;
  color: var(--vrto-muted);
  text-align: left;
}

/* 🔴 The gap lives here, not as a margin on the tag.
   A `margin-bottom` on the pill made the Access cell's content box taller at
   the bottom only, and under `vertical-align: middle` that pushed the tags'
   bottom edge below the Add-role trigger's in the next column — the two cells
   visibly stopped sharing a bottom line. `gap` spaces wrapped rows without
   adding anything outside the last one. */
.vrto-operators__roles {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* The tag carries its own remove control, so it is a flex row rather than a
   block of text: the remove glyph has to sit on the baseline of the label
   without the pill growing taller than the ones beside it.

   Figma 1390:13107 (`pillButton`): Cloud Dancer/60 over white, obsidian
   text, 6×12 padding, 24px radius. The × sits inside that padding rather
   than growing a second outline around it. */
.vrto-operators__role {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 6px 8px 6px 12px;
  border: 0;
  border-radius: 24px;
  color: var(--vrto-ink);
  font-size: 12px;
  font-weight: 600;
  line-height: 18px;
  letter-spacing: -0.2px;
  background-image:
    linear-gradient(
      90deg,
      rgba(231, 232, 237, 0.6) 0%,
      rgba(231, 232, 237, 0.6) 100%
    ),
    linear-gradient(90deg, #fff 0%, #fff 100%);
}

/* Deliberately larger than the glyph. 18px square is the smallest this can be
   and still be hit reliably with a mouse, and the pill's padding is what keeps
   it from looking oversized. Removing a role is confirmed either way. */
.vrto-operators__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--vrto-muted);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}

.vrto-operators__remove:hover {
  background: var(--vrto-ink);
  color: #fff;
}

.vrto-operators__remove:focus-visible {
  outline: 2px solid var(--vrto-ink);
  outline-offset: 1px;
}

.vrto-operators__remove:disabled {
  opacity: 0.4;
  cursor: default;
}

.vrto-operators__remove:disabled:hover {
  background: transparent;
  color: var(--vrto-muted);
}

.vrto-operators__none {
  color: var(--vrto-muted);
  font-size: 12px;
}

/* Not a permission check — the server refuses removing your own admin group.
   This only says which row is yours. */
.vrto-operators__self {
  margin-left: 6px;
  color: var(--vrto-muted);
  font-size: 12px;
}

.vrto-operators__row-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
}

.vrto-operators__notice,
.vrto-operators__error {
  margin: 0 0 16px;
  padding: 12px 16px;
  border-left: 3px solid var(--vrto-line);
  background: #f7f7f9;
  font-size: 13px;
}

.vrto-operators__error {
  border-left-color: var(--vrto-accent);
  background: #fff5f2;
}

/* --- The permission breakdown -------------------------------------------- */

/* The note under each picker. Two lines: the role's own sentence, then the
   measured counts. They are separated by weight rather than by a rule, because
   a bordered box under a form field reads as a validation error. */
.vrto-operators__grant {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-width: 52ch;
  margin: 0;
  font-size: 12px;
  line-height: 1.45;
}

.vrto-operators__grant-copy {
  color: var(--vrto-ink);
}

.vrto-operators__grant-counts {
  color: var(--vrto-muted);
}

/* Sits under the role tags, inside the same Access cell. Indented so it reads
   as belonging to the tags above it rather than as a second row of content. */
.vrto-operators__together {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--vrto-muted);
}

/* Native `<details>`, the console's only disclosure — `doc-hub.ts` uses the
   same one in four places. 🔴 The summary is styled to match an `<h2>` the way
   `.vrto-docs__block > summary` is; a `<summary>` that looks like body text
   reads as a stray line rather than as a section somebody can open. */
.vrto-operators__matrix {
  margin: 24px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--vrto-line);
}

.vrto-operators__matrix > summary {
  margin: 0;
  color: var(--vrto-muted);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}

.vrto-operators__matrix > summary:hover {
  color: var(--vrto-ink);
}

.vrto-operators__matrix-key {
  max-width: 78ch;
  margin: 8px 0 12px;
  font-size: 12px;
  color: var(--vrto-muted);
}

/* Seventeen rows by six columns does not fit a narrow window, and the fix is
   scrolling the grid rather than shrinking the type on a table of single
   letters. */
.vrto-operators__matrix-scroll {
  overflow-x: auto;
}

/* 🔴 Overrides the `text-align: right` that `.vrto-operators__table` puts on
   its last column. That rule exists for the roster's Actions column; here the
   last column is a role like any other, and one right-aligned column of
   letters among five left-aligned ones reads as a rendering fault. */
.vrto-operators__matrix .vrto-operators__table th:last-child,
.vrto-operators__matrix .vrto-operators__table td:last-child {
  text-align: left;
}

.vrto-operators__matrix .vrto-operators__table th[scope='row'] {
  font-weight: 400;
  color: var(--vrto-ink);
  white-space: nowrap;
}

/* Tabular figures so R/RW/RWD line up down a column and a fuller cell is
   visibly fuller. */
.vrto-operators__cell {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum';
  letter-spacing: 0.5px;
}

/* The "no access" cell is an em dash and must not read as an empty cell that
   failed to render — it is muted, not invisible. */
.vrto-operators__cell--none {
  color: var(--vrto-muted);
}

/* The marker for a screen-specific action the letters cannot express — Set New
   Password among them. Accent-coloured because it is the one thing on this
   grid somebody could miss and should not. */
.vrto-operators__cell-extra {
  margin-left: 2px;
  color: var(--vrto-accent);
  font-weight: 700;
}

.vrto-operators__kind {
  color: var(--vrto-muted);
  font-size: 11px;
}

/* 🔴 Visually hidden, NOT `display: none` — a screen reader skips the second
   one, and the point of this class is being read out. It carries the sentence
   each grid cell's `title` spells, because a `title` on a non-focusable `td`
   is reachable by a mouse and by nothing else.

   `clip-path` plus a 1px box rather than `text-indent` or a negative offset:
   the offset variants get read in the wrong order by some screen readers when
   the hidden text sits inside a table cell. */
.vrto-operators__sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The named list of everything the grid marks with a `+`. Visible text, so it
   needs no hover and no focus — see `extrasRoster` in
   `src/components/operators.ts` for why it exists at all. */
.vrto-operators__extras {
  max-width: 78ch;
  margin: 14px 0 0;
  font-size: 12px;
  color: var(--vrto-muted);
}

.vrto-operators__extras h3 {
  margin: 0 0 4px;
  font-size: 12px;
  font-weight: 600;
  color: var(--vrto-ink);
}

.vrto-operators__extras ul {
  margin: 0;
  padding-left: 18px;
}

.vrto-operators__extras li {
  margin: 2px 0;
}

/* ---------------------------------------------------------------------------
 * Documents (`src/components/doc-hub.ts`).
 *
 * Scoped under `.vrto-docs`, which the sign-in DOM does not render — this
 * stylesheet is loaded on both pages.
 *
 * 🔴 Deliberately not built from AdminJS's design system. The screen this
 * replaces was a 57-row spreadsheet; a `@adminjs/design-system` table would
 * have reproduced it with nicer borders. Rows here are buttons, blocks are
 * native `<details>`, and there is no filter bar, no pagination and no column
 * sorting — eleven rows need none of it, and every one of them is a place for
 * the screen to grow back into what it replaced.
 *
 * `Zalando Sans` is declared `@font-face` in `login.css`, which AdminJS loads
 * on this page too (`assets.styles` in `src/index.ts`). The console's other
 * screens keep Roboto — see the `font` note in that file.
 * ------------------------------------------------------------------------- */

.vrto-docs {
  max-width: 880px;
  padding: 32px 24px 64px;
  color: var(--vrto-ink);
  font-family: 'Zalando Sans', sans-serif;
}

.vrto-docs h1 {
  margin: 0;
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.vrto-docs__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

.vrto-docs__synced,
.vrto-docs__muted {
  color: var(--vrto-muted);
  font-size: 13px;
}

/* ---- list ---------------------------------------------------------------- */

.vrto-docs__list {
  margin: 24px 0 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--vrto-line);
}

.vrto-docs__row {
  border-bottom: 1px solid var(--vrto-line);
}

/* A whole row is one button. Not a link: there is no URL for a section — the
 * screen keeps its selection in component state, so a router is one more thing
 * to keep in step with nothing. */
.vrto-docs__rowbtn {
  display: grid;
  /* The fourth column holds either `18/20` or the words `not assessed`.
   * 88px fitted the first and wrapped the second onto two lines. */
  grid-template-columns: 12px 1fr 96px 116px 48px;
  align-items: center;
  gap: 16px;
  width: 100%;
  padding: 14px 8px;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.vrto-docs__rowbtn:hover {
  background: #fafbfc;
}

.vrto-docs__name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vrto-docs__dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  /* Not-started is an outline, not a colour: an empty state should not compete
   * with the three that mean something happened. */
  border: 1.5px solid var(--vrto-muted);
}

.vrto-docs__dot--in-progress {
  border-color: var(--vrto-yellow);
  background: var(--vrto-yellow);
}

.vrto-docs__dot--built {
  border-color: var(--vrto-sky);
  background: var(--vrto-sky);
}

.vrto-docs__dot--verified {
  border-color: var(--vrto-green);
  background: var(--vrto-green);
}

.vrto-docs__bar {
  height: 6px;
  border-radius: 3px;
  background: var(--vrto-line);
  overflow: hidden;
}

.vrto-docs__barfill {
  display: block;
  height: 100%;
  border-radius: 3px;
  background: var(--vrto-sky);
}

.vrto-docs__ratio {
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--vrto-muted);
}

.vrto-docs__flags {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
}

.vrto-docs__count {
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 10px;
  background: var(--vrto-ink);
  color: #fff;
  font-size: 12px;
  text-align: center;
}

/* Stale — a link citing a document that has since changed. Rose rather than
 * the accent orange, so it never reads as the same thing as an error. */
.vrto-docs__stale {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--vrto-rose);
}

/* ---- detail -------------------------------------------------------------- */

.vrto-docs__back {
  margin-bottom: 16px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 13px;
  color: var(--vrto-muted);
  cursor: pointer;
}

.vrto-docs__back:hover {
  color: var(--vrto-ink);
}

.vrto-docs__detailhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.vrto-docs__status {
  padding: 6px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  background: #fff;
  font: inherit;
  font-size: 13px;
}

.vrto-docs__badge {
  padding: 4px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  font-size: 12px;
}

.vrto-docs__badge--built {
  border-color: var(--vrto-sky);
  color: var(--vrto-deep);
}

.vrto-docs__badge--verified {
  border-color: var(--vrto-green);
  color: #04663f;
}

.vrto-docs__sub {
  margin: 4px 0 0;
  color: var(--vrto-muted);
  font-size: 13px;
}

.vrto-docs__sub a,
.vrto-docs__linkhead a {
  color: var(--vrto-deep);
}

.vrto-docs__note {
  margin: 12px 0 0;
  font-size: 14px;
}

.vrto-docs__counts {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  margin: 20px 0 0;
  padding: 0 0 20px;
  border-bottom: 1px solid var(--vrto-line);
  list-style: none;
  font-size: 13px;
  color: var(--vrto-muted);
}

.vrto-docs__counts strong {
  font-size: 20px;
  font-weight: 700;
  color: var(--vrto-ink);
}

/* ---- blocks -------------------------------------------------------------- */

.vrto-docs__block {
  margin-top: 24px;
}

.vrto-docs__block h2,
.vrto-docs__block > summary {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
}

.vrto-docs__block > summary {
  padding: 8px 0;
  cursor: pointer;
  color: var(--vrto-muted);
}

.vrto-docs__block > summary:hover {
  color: var(--vrto-ink);
}

/* Gaps get the accent rule down the left. It is the only block open by
 * default and the only one that names something outstanding. */
.vrto-docs__block--gap {
  padding-left: 16px;
  border-left: 3px solid var(--vrto-accent);
}

.vrto-docs__links,
.vrto-docs__comments,
.vrto-docs__history {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}

.vrto-docs__link {
  padding: 12px 0;
  border-top: 1px solid var(--vrto-line);
}

.vrto-docs__links > li:first-child {
  border-top: 0;
}

.vrto-docs__linkhead {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 14px;
}

.vrto-docs__tag {
  padding: 1px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  font-size: 11px;
  color: var(--vrto-muted);
}

.vrto-docs__tag--low {
  border-color: var(--vrto-yellow);
  background: var(--vrto-yellow);
  color: var(--vrto-ink);
}

.vrto-docs__tag--stale {
  border-color: var(--vrto-rose);
  color: var(--vrto-rose);
}

/* The sentence the judgement rests on. Quiet, but never hidden — a link whose
 * quote is not visible is a link nobody can refute. */
.vrto-docs__quote {
  margin: 6px 0 0;
  padding-left: 12px;
  border-left: 2px solid var(--vrto-line);
  font-size: 13px;
  color: var(--vrto-muted);
}

.vrto-docs__quote cite {
  display: block;
  margin-top: 4px;
  font-size: 12px;
  font-style: normal;
  opacity: 0.8;
}

.vrto-docs__history li {
  padding: 4px 0;
  font-size: 13px;
}

/* ---- comments ------------------------------------------------------------ */

.vrto-docs__comments li {
  padding: 12px 0;
  border-top: 1px solid var(--vrto-line);
}

.vrto-docs__commenthead {
  margin: 0;
  font-size: 13px;
}

.vrto-docs__commentbody {
  margin: 4px 0 0;
  font-size: 14px;
  white-space: pre-wrap;
}

.vrto-docs__compose {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  margin-top: 16px;
}

.vrto-docs__compose textarea {
  flex: 1;
  padding: 8px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  font: inherit;
  font-size: 14px;
  resize: vertical;
}

.vrto-docs__compose button {
  padding: 8px 16px;
  border: 0;
  border-radius: 6px;
  background: var(--vrto-ink);
  color: #fff;
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}

.vrto-docs__compose button:disabled {
  opacity: 0.4;
  cursor: default;
}

/* ---- threads: the same block on a section, an area, a rule and a link -----
 *
 * 🔴 The whole point of `CommentThread` is that these four read the same. A
 * reader who learns how a thread behaves on a section must not have to learn
 * it again on a rule, so nothing below is scoped to a parent block. What
 * changes between them is DENSITY, and only that: a thread nested inside a
 * rule or a link gets the tighter spacing further down. */

.vrto-docs__thread {
  margin-top: 8px;
}

/* The chip. A `<select>` for anyone who can move it, a `<span>` for anyone who
 * cannot — styled identically on purpose, because the state it shows is the
 * same fact either way and only the affordance differs. */
.vrto-docs__cstatus {
  margin-left: 8px;
  padding: 1px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  background: none;
  font: inherit;
  font-size: 11px;
  color: var(--vrto-muted);
  vertical-align: middle;
}

select.vrto-docs__cstatus {
  cursor: pointer;
}

select.vrto-docs__cstatus:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Open is the only one that shouts. `answered` and `resolved` are states
 * somebody already dealt with, and colouring all three turns the list into a
 * traffic light nobody can scan. */
.vrto-docs__cstatus--open {
  border-color: var(--vrto-accent);
  color: var(--vrto-accent);
}

.vrto-docs__cstatus--answered {
  border-color: var(--vrto-green);
  color: var(--vrto-ink);
}

/* Faded, not hidden. A resolved comment is the record of a question that got
 * an answer, and the answer is often the thing somebody comes back for. */
.vrto-docs__comment--resolved {
  opacity: 0.62;
}

/* One level. There is no rule for a second, because the service refuses to
 * write one — see `addComment`. */
.vrto-docs__replies {
  margin: 8px 0 0 20px;
  padding: 0 0 0 12px;
  border-left: 2px solid var(--vrto-line);
  list-style: none;
}

.vrto-docs__replies > li {
  border-top: 0;
  padding: 8px 0 0;
}

/* Resolved threads fold away by default and say how many. Same judgement the
 * spec and evidence blocks make: kept, not shown. */
.vrto-docs__resolved {
  margin-top: 8px;
}

.vrto-docs__resolved > summary {
  cursor: pointer;
  font-size: 12px;
  color: var(--vrto-muted);
}

/* "Reply" and "Cancel" — actions that must not read as heavily as Send. */
.vrto-docs__linkish {
  margin-top: 6px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12px;
  color: var(--vrto-muted);
  text-decoration: underline;
  cursor: pointer;
}

.vrto-docs__linkish:disabled {
  opacity: 0.4;
  cursor: default;
}

.vrto-docs__compose--reply {
  margin-top: 8px;
}

/* ---- a thread inside a rule or a link ------------------------------------
 * Tighter, and indented to the rule body's column so it lines up with "What we
 * built" rather than starting back at the rule id. */

.vrto-docs__rule > .vrto-docs__thread {
  margin-left: 72px;
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px dashed var(--vrto-line);
}

.vrto-docs__rule .vrto-docs__comments li,
.vrto-docs__linkthread .vrto-docs__comments li {
  padding: 8px 0;
}

.vrto-docs__linkthread > summary {
  margin-top: 8px;
  font-size: 12px;
  color: var(--vrto-muted);
  cursor: pointer;
}

/* ---- a rule's console-owned status ---------------------------------------
 *
 * 🔴 The line below the control shows BOTH statuses whenever they disagree —
 * what the register judged, and what somebody decided since. Neither is
 * decoration: the register's value is what the next `npm run docs:sync`
 * reasserts, so a reader who cannot see it cannot tell whether the override is
 * still needed. */

.vrto-docs__ruleset {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 12px 0 0 72px;
}

.vrto-docs__ruleset h4 {
  margin: 0;
}

.vrto-docs__ruleset select,
.vrto-docs__ruleset input {
  padding: 5px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  background: #fff;
  font: inherit;
  font-size: 13px;
}

.vrto-docs__ruleset input {
  flex: 1 1 220px;
  min-width: 160px;
}

.vrto-docs__ruleset button {
  padding: 5px 12px;
  border: 0;
  border-radius: 6px;
  background: var(--vrto-ink);
  color: #fff;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.vrto-docs__ruleset button:disabled {
  opacity: 0.35;
  cursor: default;
}

.vrto-docs__override {
  margin: 10px 0 0 72px;
  padding: 6px 10px;
  border-left: 3px solid var(--vrto-accent);
  background: #fff5f2;
  font-size: 13px;
}

.vrto-docs__overridenote {
  color: var(--vrto-muted);
}

.vrto-docs__tag--override {
  border-color: var(--vrto-accent);
  color: var(--vrto-accent);
}

.vrto-docs__notice,
.vrto-docs__error {
  margin: 16px 0 0;
  padding: 10px 12px;
  border-radius: 6px;
  background: #f4f5f7;
  font-size: 13px;
}

.vrto-docs__error {
  background: #fdeceb;
  color: #8a2a1c;
}

/* App / WebApp. Rendered whenever the surface changes, because both pages have
 * a section called `Onboarding & Auth` and the list would otherwise show the
 * same name twice with nothing separating them. */
.vrto-docs__surface {
  padding: 20px 8px 6px;
  border-bottom: 1px solid var(--vrto-line);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--vrto-muted);
}

.vrto-docs__list > .vrto-docs__surface:first-child {
  padding-top: 8px;
}

.vrto-docs__ratio {
  white-space: nowrap;
}

/* `tabular-nums` on the ratio keeps `9/9` and `11/14` from shifting the column
 * as the numbers change — but it also widens the SPACE glyph, which pulled
 * `not assessed` apart into `not  assessed`. Digits want it; the words do not. */
.vrto-docs__ratio .vrto-docs__muted {
  font-variant-numeric: normal;
}

/* ── Document Hub · the rule register ────────────────────────────────────
   Rules are the densest thing on this screen — a section can carry forty —
   so the whole block is built to be SCANNED closed and read one row at a
   time. Nothing here competes with the gap block above it for attention. */

.vrto-docs__rulegroup {
  border-left: 3px solid var(--vrto-line, #e5e7eb);
  margin: 12px 0;
  padding-left: 12px;
}
.vrto-docs__rulegroup > summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 14px;
  padding: 4px 0;
}

/* The four statuses that need somebody to do something, and only those four.
   A colour on every group would make the block a rainbow and tell a reader
   nothing about where to look first. */
.vrto-docs__rulegroup--built_differs_from_document {
  border-left-color: var(--vrto-orange, #ff623e);
}
.vrto-docs__rulegroup--not_built_blocked {
  border-left-color: var(--vrto-orange, #ff623e);
}
.vrto-docs__rulegroup--client_owed {
  border-left-color: var(--vrto-rose, #ff2e92);
}
.vrto-docs__rulegroup--built_awaiting_client_sign_off {
  border-left-color: var(--vrto-yellow, #ffea70);
}

.vrto-docs__rule {
  border-top: 1px solid var(--vrto-line, #e5e7eb);
  padding: 8px 0;
}
.vrto-docs__rule > summary {
  cursor: pointer;
  display: flex;
  gap: 10px;
  align-items: baseline;
  line-height: 1.45;
}
.vrto-docs__ruleid {
  color: var(--vrto-muted, #6b7280);
  font-size: 12px;
  /* Fixed width so forty ids form a column the eye can run down rather than
     a ragged edge that moves with each requirement's length. */
  flex: 0 0 62px;
  font-variant-numeric: tabular-nums;
}
.vrto-docs__rulereq {
  flex: 1 1 auto;
}

.vrto-docs__rule h4 {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--vrto-muted, #6b7280);
  margin: 12px 0 4px;
}
.vrto-docs__rule blockquote {
  margin: 0;
  padding-left: 12px;
  border-left: 2px solid var(--vrto-line, #e5e7eb);
  font-style: italic;
}
.vrto-docs__rule > div,
.vrto-docs__rulenext {
  /* Indented to the id column, so an opened row's body lines up under its
     requirement instead of under its id. */
  margin-left: 72px;
}
.vrto-docs__rulewatch {
  color: var(--vrto-deep, #003c5d);
}
.vrto-docs__rule p {
  margin: 4px 0;
}

/* The one number on the section header that means "act on this". */
.vrto-docs__decisions {
  color: var(--vrto-orange, #ff623e);
  font-weight: 600;
}

/* ── The areas, under the sections ───────────────────────────────────────
   Quieter than the section list on purpose: sections are what this page is
   about, areas are the way to the 412 rules that belong to no screen.
   (⚠️ That figure read 108 here until 2026-08-24 and was measured when the
   register was a third of its current size. Re-derive rather than quote.) */

.vrto-docs__areas {
  margin-top: 32px;
}
.vrto-docs__areas h2 {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--vrto-muted, #6b7280);
  margin-bottom: 4px;
}
.vrto-docs__areas ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.vrto-docs__areas button {
  display: grid;
  /* Counts right-aligned in fixed columns so the eye can run down them; the
     name takes whatever is left. The last column is the open-comment chip,
     which is empty on most rows — a fixed width keeps the "to decide" column
     from shifting between rows that have one and rows that do not. */
  grid-template-columns: 1fr 48px 110px 28px;
  gap: 12px;
  align-items: baseline;
  width: 100%;
  border: 0;
  background: none;
  border-top: 1px solid var(--vrto-line, #e5e7eb);
  padding: 9px 0;
  cursor: pointer;
  font: inherit;
  text-align: left;
}
.vrto-docs__areas button:hover {
  background: var(--vrto-hover, #f9fafb);
}
.vrto-docs__areaname {
  font-weight: 500;
}
.vrto-docs__areas .vrto-docs__muted,
.vrto-docs__areas .vrto-docs__decisions {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: 13px;
}

/* ---------------------------------------------------------------------------
 * The shared controls — `src/components/ui/`.
 *
 * 🔴 Every rule below leans on the `--vrto-*` tokens, and those are declared on
 * the PAGE ROOTS at the top of this file, not on `:root`, because this
 * stylesheet is injected into the sign-in page too. So these components only
 * look right inside `.vrto-dashboard` / `.vrto-operators` / `.vrto-support` /
 * `.vrto-docs`. When a fifth custom page arrives, its root joins that selector
 * list — do not "fix" an unstyled dialog by moving the tokens to `:root`.
 *
 * Scale, taken from the operators block above and not reinvented: 4px radius on
 * controls, 8px on surfaces, 12/13/15px type.
 * ------------------------------------------------------------------------- */

/* --- Select: a form field the operator picks and then submits ------------ */

/* 🔴 `appearance: none` is the whole point of this rule. Without it a `<select>`
   keeps the operating system's widget — grey gradient on macOS, a different
   grey on Windows — and no amount of border/background/radius below shows
   through. The Role picker looked native next to `.vrto-menu__trigger` for
   exactly that reason. The chevron has to come back by hand once it is off:
   it is a background image rather than `::after`, because a replaced element
   has no generated content. */
.vrto-select {
  appearance: none;
  padding: 8px 32px 8px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 4px;
  background-color: #fff;
  /* The same ▾ the menu trigger draws, in --vrto-ink. Inline so it survives
     `/admin-assets` — there is no image pipeline here. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='%23191a1d' d='M0 0h10L5 6z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  color: var(--vrto-ink);
  font-size: 13px;
  cursor: pointer;
}

.vrto-select:hover:not(:disabled) {
  border-color: var(--vrto-ink);
}

.vrto-select:disabled {
  opacity: 0.5;
  cursor: default;
}

/* --- MenuButton: a short list of actions --------------------------------- */

.vrto-menu {
  position: relative;
  display: inline-block;
}

.vrto-menu__trigger {
  padding: 4px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 4px;
  background: #fff;
  color: var(--vrto-ink);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

/* The chevron is CSS, not a text node: it is decoration, and putting it in the
   label would send it to the accessible name and to `aria-label` on the list. */
.vrto-menu__trigger::after {
  content: ' ▾';
}

.vrto-menu__trigger:hover:not(:disabled) {
  border-color: var(--vrto-ink);
}

.vrto-menu__trigger:disabled {
  opacity: 0.5;
  cursor: default;
}

.vrto-menu__trigger[aria-expanded='true'] {
  border-color: var(--vrto-ink);
  background: #f7f7f9;
}

.vrto-menu__list {
  position: absolute;
  z-index: 10;
  top: calc(100% + 4px);
  /* Right-aligned: the trigger sits in the row's Actions cell, hard against the
     right edge of the table, and a left-aligned list would run off it. */
  right: 0;
  min-width: 100%;
  margin: 0;
  padding: 4px;
  list-style: none;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 8px 24px rgb(25 26 29 / 12%);
}

.vrto-menu__item {
  display: block;
  width: 100%;
  padding: 6px 10px;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--vrto-ink);
  font-size: 12px;
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}

.vrto-menu__item:hover {
  background: #f7f7f9;
}

.vrto-menu__item--danger {
  color: var(--vrto-danger);
}

/* --- ConfirmDialog ------------------------------------------------------- */

.vrto-dialog__overlay {
  position: fixed;
  /* Above the AdminJS sidebar, which is the only thing on the page with its own
     stacking order. */
  z-index: 100;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgb(25 26 29 / 45%);
}

.vrto-dialog {
  width: 100%;
  max-width: 460px;
  padding: 24px;
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 24px 64px rgb(25 26 29 / 24%);
}

.vrto-dialog__title {
  margin: 0 0 12px;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.4;
}

.vrto-dialog__body p {
  max-width: 62ch;
  margin: 0 0 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--vrto-muted);
}

.vrto-dialog__actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 20px;
}

.vrto-dialog__cancel,
.vrto-dialog__confirm {
  padding: 9px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 4px;
  background: #fff;
  color: var(--vrto-ink);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

.vrto-dialog__cancel:hover {
  border-color: var(--vrto-ink);
}

.vrto-dialog__confirm {
  border-color: var(--vrto-ink);
  background: var(--vrto-ink);
  color: #fff;
}

/* Destructive, and it should not be possible to mistake it for the routine one
   at a glance. Initial focus goes to Cancel for these — see `useConfirm`. */
.vrto-dialog__confirm--danger {
  border-color: var(--vrto-danger);
  background: var(--vrto-danger);
}

.vrto-dialog__cancel:disabled,
.vrto-dialog__confirm:disabled {
  opacity: 0.5;
  cursor: default;
}

/* ---------------------------------------------------------------------------
 * Users → show → Set New Password.
 *
 * 🔴 Almost nothing here styles the drawer any more, and that is the point. The
 * component is built from AdminJS's own drawer parts — `DrawerContent` owns the
 * 48/32/24 padding, `H3` the title, `FormGroup`/`Label`/`Input` the fields,
 * `DrawerFooter` the buttons — so it matches the filter drawer by construction
 * instead of by a second set of numbers maintained here. The rules this block
 * used to hold (`h1` at 18px, `__form` with its own label/input/button
 * padding, radii and hover) were exactly that second set, and they were what
 * made the drawer read as a different product from every other drawer.
 *
 * `.vrto-support` survives for ONE reason: it is the `--vrto-*` token root, and
 * those tokens are declared on page roots rather than `:root` because this
 * stylesheet is injected into the sign-in page too. `ConfirmDialog`, the hint
 * and the notices below all read them.
 * ------------------------------------------------------------------------- */

/* Undoes the shared page-root rule at the top of this file — a 1180px max-width
 * and 32/24/64 padding are for a page, and this is a 500px drawer whose padding
 * belongs to `DrawerContent`. `flex: 1 1 auto` is load-bearing: the drawer is a
 * full-height flex column, and without it this form sizes to its content and
 * the footer sits under the last field instead of at the bottom of the panel. */
.vrto-support {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  max-width: none;
  padding: 0;
}

/* 14px, not 13px: `theme.fontSizes.default` is what the labels, inputs and
   buttons around it resolve to. No `62ch` cap either — it is wider than the
   drawer, so it never applied. */
.vrto-support__hint {
  margin: 0;
  color: var(--vrto-muted);
  font-size: 14px;
  line-height: 1.5;
}

/* The figure rows — `stateSummary` in `components/member-nutrition-reset.ts`,
 * rendered in the drawer AND, since the read moved onto the member page, in the
 * `nutritionState` panel on Users → show.
 *
 * 🔴 These had no rule at all until 2026-08-31, which is not a style nit: the
 * markup is `<div><span>label</span><strong>value</strong></div>`, so with no
 * rule the two run together as one line of text — "TDEE now (live)2380 kcal" —
 * and the panel that exists to let an operator compare two numbers rendered
 * them as prose. Space-between is the whole fix; the dotted leader is what
 * keeps a label tied to its value once the panel is page-width rather than
 * 500px of drawer. */
.vrto-support__state {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-width: 560px;
}

.vrto-support__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 0;
  border-bottom: 1px dotted #e3e3e8;
  font-size: 14px;
}

.vrto-support__row > span {
  color: var(--vrto-muted);
}

.vrto-support__row > strong {
  font-weight: 600;
  text-align: right;
}

.vrto-support__notice,
.vrto-support__error {
  margin: 16px 0 0;
  padding: 10px 12px;
  border-left: 3px solid var(--vrto-ink);
  background: #f7f7f9;
  font-size: 14px;
}

.vrto-support__error {
  border-left-color: var(--vrto-accent);
  background: #fff5f2;
}

/* ── Ingredient matching ────────────────────────────────────────────────────
 *
 * The queue that links the inherited ingredient library to FatSecret.
 *
 * 🔴 Every colour here is a token from the block at the top of this file, which
 * takes them from the Colour frame on the `📄 Read Me` page of the Figma file.
 * Nothing on this screen invents one — the note beside those tokens is the
 * licence, and it does not extend to new hues.
 *
 * The layout answers one question per card: is this the same food? So the
 * figures are the loudest thing on it and the candidate's NAME is deliberately
 * not — a plausible name is exactly what makes a wrong match look right.
 */
.vrto-match {
  max-width: 980px;
  margin: 0 auto;
  padding: 28px 24px 64px;
  color: var(--vrto-ink);
}

.vrto-match__head h1 {
  margin: 0 0 6px;
  font-size: 22px;
  letter-spacing: -0.01em;
}

.vrto-match__lede {
  margin: 0 0 20px;
  max-width: 62ch;
  color: var(--vrto-muted);
  font-size: 13px;
  line-height: 1.55;
}

/* The counts sit above the work, not inside it — they are the reason to keep
 * going, and they must not compete with a card for attention. */
.vrto-match__counts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 10px;
  margin-bottom: 26px;
}

.vrto-match__count {
  padding: 12px 14px;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  background: #fff;
}

.vrto-match__count-value {
  display: block;
  font-size: 21px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.vrto-match__count-label {
  display: block;
  margin-top: 3px;
  color: var(--vrto-muted);
  font-size: 11.5px;
}

/* Only the two counts that mean "there is work" carry a rule. Everything else
 * is context. */
.vrto-match__count--now {
  border-left: 3px solid var(--vrto-accent);
}

.vrto-match__count--done {
  border-left: 3px solid var(--vrto-green);
}

.vrto-match__section {
  margin-top: 34px;
}

.vrto-match__section h2 {
  margin: 0 0 4px;
  font-size: 15px;
  letter-spacing: -0.005em;
}

.vrto-match__cards,
.vrto-match__drifts {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: 14px;
}

.vrto-match__card {
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
  overflow: hidden;
}

.vrto-match__card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 18px 14px;
  border-bottom: 1px solid var(--vrto-line);
}

/* What WE hold. It leads, because the question is whether the candidate is the
 * same thing as this. */
.vrto-match__ours {
  margin: 0;
  font-size: 15.5px;
  font-weight: 620;
}

.vrto-match__ours-meta {
  margin: 3px 0 0;
  color: var(--vrto-muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* The blast radius. §3.7 — a wrong match leaves every one of these recipes
 * carrying wrong figures indefinitely. */
.vrto-match__uses {
  flex: none;
  padding: 4px 9px;
  border: 1px solid var(--vrto-line);
  border-radius: 999px;
  color: var(--vrto-muted);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.vrto-match__uses--many {
  border-color: var(--vrto-danger);
  color: var(--vrto-danger);
  font-weight: 600;
}

.vrto-match__candidates {
  margin: 0;
  padding: 0;
  list-style: none;
}

.vrto-match__candidate {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 8px 18px;
  padding: 13px 18px;
  border-bottom: 1px solid var(--vrto-line);
}

.vrto-match__candidate:last-child {
  border-bottom: 0;
}

/* An exact match is the only thing the machine would have applied on its own,
 * so it reads differently from the rest of the list. */
.vrto-match__candidate--exact {
  background: rgba(0, 208, 132, 0.06);
  box-shadow: inset 3px 0 0 var(--vrto-green);
}

.vrto-match__candidate-head {
  grid-column: 1 / -1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.vrto-match__candidate-name {
  margin: 0;
  font-size: 13.5px;
  font-weight: 560;
}

.vrto-match__candidate-brand {
  margin: 2px 0 0;
  color: var(--vrto-muted);
  font-size: 11.5px;
}

.vrto-match__score {
  flex: none;
  padding: 2px 8px;
  border-radius: 999px;
  background: #f3f4f7;
  color: var(--vrto-muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.vrto-match__score--exact {
  background: var(--vrto-green);
  /* Deep rather than white: white on this green is 1.7:1. */
  color: var(--vrto-deep);
  font-weight: 650;
}

.vrto-match__figures {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
}

.vrto-match__figure,
.vrto-match__delta {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.vrto-match__figure-value {
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.vrto-match__figure-value small {
  margin-left: 2px;
  color: var(--vrto-muted);
  font-size: 10.5px;
  font-weight: 400;
}

.vrto-match__figure-label {
  color: var(--vrto-muted);
  font-size: 10.5px;
  text-transform: lowercase;
}

/* How far the figures move. This is the number the operator is actually
 * judging, so it is separated from the raw values beside it. */
.vrto-match__delta {
  padding-left: 18px;
  border-left: 1px solid var(--vrto-line);
}

.vrto-match__delta--wide .vrto-match__figure-value {
  color: var(--vrto-danger);
}

.vrto-match__pick {
  padding: 7px 15px;
  border: 1px solid var(--vrto-ink);
  border-radius: 8px;
  background: var(--vrto-ink);
  color: #fff;
  font-size: 12.5px;
  font-weight: 560;
  cursor: pointer;
}

.vrto-match__pick:disabled {
  border-color: var(--vrto-line);
  background: #f3f4f7;
  color: var(--vrto-muted);
  cursor: not-allowed;
}

.vrto-match__card-foot {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 18px;
  background: #fafbfc;
  border-top: 1px solid var(--vrto-line);
}

.vrto-match__aside {
  padding: 5px 12px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
  font-size: 12px;
  cursor: pointer;
}

.vrto-match__hint,
.vrto-match__none,
.vrto-match__empty {
  color: var(--vrto-muted);
  font-size: 11.5px;
}

.vrto-match__none,
.vrto-match__empty {
  margin: 0;
  padding: 16px 18px;
  font-size: 12.5px;
}

/* ── Recipes that moved ── */
.vrto-match__drift {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 6px 18px;
  padding: 14px 18px;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}

.vrto-match__drift-head {
  grid-column: 1;
  display: flex;
  align-items: baseline;
  gap: 10px;
}

.vrto-match__drift-head h3 {
  margin: 0;
  font-size: 14px;
  font-weight: 580;
}

.vrto-match__drift-pct {
  font-size: 13px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--vrto-muted);
}

.vrto-match__drift-pct--wide {
  color: var(--vrto-danger);
}

.vrto-match__drift-figures {
  grid-column: 1;
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}

.vrto-match__was {
  color: var(--vrto-muted);
  text-decoration: line-through;
}

.vrto-match__arrow {
  color: var(--vrto-muted);
}

.vrto-match__now {
  font-weight: 620;
}

.vrto-match__drift-actions {
  grid-column: 2;
  grid-row: 1 / span 2;
  display: flex;
  gap: 8px;
}

.vrto-match__accept,
.vrto-match__reject {
  padding: 6px 13px;
  border-radius: 8px;
  font-size: 12px;
  cursor: pointer;
}

.vrto-match__accept {
  border: 1px solid var(--vrto-line);
  background: #fff;
}

.vrto-match__reject {
  border: 1px solid var(--vrto-danger);
  background: #fff;
  color: var(--vrto-danger);
  font-weight: 560;
}

.vrto-match__notice,
.vrto-match__error {
  margin: 0 0 16px;
  padding: 10px 14px;
  border-radius: 9px;
  font-size: 12.5px;
}

.vrto-match__notice {
  border: 1px solid var(--vrto-green);
  background: rgba(0, 208, 132, 0.08);
}

.vrto-match__error {
  border: 1px solid var(--vrto-danger);
  background: rgba(200, 52, 26, 0.06);
  color: var(--vrto-danger);
}

.vrto-match__loading,
.vrto-match__denied {
  color: var(--vrto-muted);
  font-size: 13px;
}

@media (max-width: 640px) {
  .vrto-match__candidate,
  .vrto-match__drift {
    grid-template-columns: 1fr;
  }

  .vrto-match__drift-actions {
    grid-column: 1;
    grid-row: auto;
  }
}

/* ── Recipe amounts ─────────────────────────────────────────────────────────
 * Where the unit mismatches the matching queue creates get fixed. A line that
 * cannot be read is the loudest thing here; everything else is context.
 * Tokens only — no new colours (see the note at the top of this file). */
.vrto-amt {
  max-width: 1080px;
  margin: 0 auto;
  padding: 28px 24px 64px;
  color: var(--vrto-ink);
}
.vrto-amt h1 {
  margin: 0 0 6px;
  font-size: 22px;
  letter-spacing: -0.01em;
}
.vrto-amt__lede {
  margin: 0 0 20px;
  max-width: 70ch;
  color: var(--vrto-muted);
  font-size: 13px;
  line-height: 1.55;
}
.vrto-amt__split {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: 20px;
  align-items: start;
}
.vrto-amt__list {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  overflow: hidden;
  background: #fff;
}
.vrto-amt__row + .vrto-amt__row {
  border-top: 1px solid var(--vrto-line);
}
.vrto-amt__row button {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 10px 12px;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.vrto-amt__row--open button {
  background: #f3f4f7;
  font-weight: 600;
}
.vrto-amt__row-name {
  font-size: 12.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vrto-amt__row-broken {
  flex: none;
  color: var(--vrto-danger);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.vrto-amt__detail {
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}
.vrto-amt__detail-head {
  padding: 16px 18px 12px;
  border-bottom: 1px solid var(--vrto-line);
}
.vrto-amt__detail-head h2 {
  margin: 0 0 4px;
  font-size: 16px;
}
.vrto-amt__totals {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  color: var(--vrto-muted);
}
.vrto-amt__arrow {
  color: var(--vrto-line);
}
.vrto-amt__derived {
  color: var(--vrto-ink);
  font-weight: 620;
}
.vrto-amt__no-total {
  color: var(--vrto-danger);
  font-weight: 600;
}
.vrto-amt__lines {
  margin: 0;
  padding: 0;
  list-style: none;
}
.vrto-amt__line {
  display: grid;
  grid-template-columns: 1.4fr 1fr 72px 90px 92px auto;
  gap: 10px;
  align-items: center;
  padding: 9px 18px;
  border-bottom: 1px solid var(--vrto-line);
  font-size: 12.5px;
}
.vrto-amt__line:last-child {
  border-bottom: 0;
}
/* The 58.7%. */
.vrto-amt__line--broken {
  background: rgba(200, 52, 26, 0.05);
  box-shadow: inset 3px 0 0 var(--vrto-danger);
}
.vrto-amt__ing {
  font-weight: 560;
}
.vrto-amt__stored,
.vrto-amt__cal {
  color: var(--vrto-muted);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}
.vrto-amt__line--broken .vrto-amt__cal {
  color: var(--vrto-danger);
  font-weight: 600;
}
.vrto-amt__qty,
.vrto-amt__unit {
  padding: 5px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  font: inherit;
  font-size: 12px;
}
.vrto-amt__qty {
  font-variant-numeric: tabular-nums;
}
.vrto-amt__save {
  padding: 5px 12px;
  border: 1px solid var(--vrto-ink);
  border-radius: 7px;
  background: var(--vrto-ink);
  color: #fff;
  font-size: 12px;
  cursor: pointer;
}
.vrto-amt__save:disabled {
  border-color: var(--vrto-line);
  background: #f3f4f7;
  color: var(--vrto-muted);
  cursor: not-allowed;
}
.vrto-amt__hint,
.vrto-amt__empty {
  padding: 12px 18px;
  margin: 0;
  color: var(--vrto-muted);
  font-size: 11.5px;
}
.vrto-amt__queue {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.vrto-amt__pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.vrto-amt__pager button {
  padding: 4px 9px;
  border: 1px solid var(--vrto-line);
  border-radius: 7px;
  background: #fff;
  font: inherit;
  font-size: 11.5px;
  color: var(--vrto-ink);
  cursor: pointer;
}
.vrto-amt__pager button:disabled {
  color: var(--vrto-muted);
  background: #f7f8fa;
  cursor: not-allowed;
}
.vrto-amt__pager-at {
  flex: 1 1 auto;
  text-align: center;
  color: var(--vrto-muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.vrto-amt__order {
  flex: 1 0 100%;
}
.vrto-amt__notice,
.vrto-amt__error {
  margin: 0 0 16px;
  padding: 10px 14px;
  border-radius: 9px;
  font-size: 12.5px;
}
.vrto-amt__notice {
  border: 1px solid var(--vrto-green);
  background: rgba(0, 208, 132, 0.08);
}
.vrto-amt__error {
  border: 1px solid var(--vrto-danger);
  background: rgba(200, 52, 26, 0.06);
  color: var(--vrto-danger);
}
@media (max-width: 820px) {
  .vrto-amt__split {
    grid-template-columns: 1fr;
  }
  .vrto-amt__line {
    grid-template-columns: 1fr 1fr;
  }
}

/* ---------------------------------------------------------------------------
 * The page header — LIST and SHOW (`src/components/list-header.ts`,
 * `show-header.ts`, `filter-panel.ts`).
 *
 * AdminJS's stock header is three stacked rows — breadcrumb, an `H2` naming the
 * action ("List", "Show") with an optional badge, a button row — and on a list
 * its filter is a drawer mounted as a flex SIBLING of the table, so opening it
 * narrows the columns. The override is one row, and the filter drops as a panel
 * between the row and the table: the same pixels, spent downward, none taken
 * from the data.
 *
 * 🔴 Two prefixes, and the split is deliberate. `.vrto-pagehead__*` is the row
 * both screens share — flex layout, rule, wrap breakpoint — declared once.
 * `.vrto-listhead__*` is only what a list has and a show does not: the filter
 * button and the panel under it. A rule that would apply to both goes in the
 * first; anything mentioning a filter goes in the second.
 *
 * Tokens only — no new colours (see the note at the top of this file). The one
 * rule here that reaches into a component this console does not own is the
 * field margin reset, and it is commented where it sits.
 * ------------------------------------------------------------------------- */

.vrto-pagehead {
  margin-bottom: 12px;
}
.vrto-pagehead__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 16px;
  padding: 14px 0 10px;
  border-bottom: 1px solid var(--vrto-line);
}
.vrto-pagehead__title {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1 1 auto;
  min-width: 0;
}
/* 🔴 `flex: 0 1 auto`, against AdminJS's own `flexGrow: 1` on `Breadcrumbs`.
   The stock breadcrumb is alone on its row, so it grows to fill it — which here
   would push the count badge (list) or the record crumb (show) to the far right
   of the screen, a screen's width from the name it belongs to. `__title` does
   the growing instead, so the two stay adjacent and the buttons stay right. */
.vrto-pagehead__title > [data-css$='breadcrumbs'] {
  flex: 0 1 auto;
  min-width: 0;
}
.vrto-pagehead__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

/* ── The show header's record crumb ───────────────────────────────────────
   The trailing breadcrumb `show-header.ts` appends, because AdminJS's own
   `Breadcrumbs` names the ACTION and never the record.

   🔴 The separator is here rather than in the component for the reason
   `ui/menu-button.ts` gives about the filter caret: the crumb's text is a
   record title, and a `/` concatenated onto one is a slash inside whatever a
   member happened to be called. AdminJS draws its own separators the same way,
   as a `::after` on `BreadcrumbLink`.

   ⚠️ It is a `::before` and not an `::after` on the preceding crumb, because
   the preceding crumb belongs to AdminJS's component: its last link matches
   `:last-child` inside that component's own Box and deliberately renders no
   separator. */
.vrto-pagehead__crumb {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 14px;
  line-height: 1.4;
  color: var(--vrto-ink);
}
.vrto-pagehead__crumb::before {
  content: '/';
  margin-right: 10px;
  color: var(--vrto-muted);
}

/* The show header carries a little more air below it than the list one, and
   the reason is what follows each. A list's next element is either the filter
   panel — which butts up against the row on purpose, sharing its border — or a
   table head that reads as a second rule. A show's next element is
   `.vrto-show`'s first band, and that band zeroes its own top margin
   (`.vrto-show > .vrto-show__band:first-child` below), so without this the
   first field label sits 12px under the rule. */
.vrto-showhead {
  margin-bottom: 20px;
}
/* The disclosure caret, in CSS rather than in the component for the reason
   `ui/menu-button.ts` gives: the label is a translated string, and a glyph
   concatenated onto one is a glyph inside whatever a translator writes. */
.vrto-listhead__filter::after {
  content: '▾';
  margin-left: 6px;
  font-size: 10px;
  line-height: 1;
}
.vrto-listhead__filter[aria-expanded='true']::after {
  content: '▴';
}
.vrto-listhead__panel {
  padding: 14px 16px 12px;
  border: 1px solid var(--vrto-line);
  border-top: 0;
  border-radius: 0 0 10px 10px;
  background: #fbfbfd;
}
/* `auto-fill` with a 220px floor, so App Settings' single filter and Recipes'
   nine both land on a grid rather than each needing a hand-set column count. */
.vrto-listhead__fields {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 4px 16px;
}
/* ⚠️ AdminJS's filter properties are a label and an input in a `Box` carrying
   its own bottom margin, sized for a drawer where each had a full row. In a
   grid that margin stacks on top of the grid's own gap. Keep this to the
   margin — it is the only rule in this block reaching inside the design
   system. */
.vrto-listhead__field > div {
  margin-bottom: 0;
}
.vrto-listhead__panel-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--vrto-line);
}
/* The sidebar's own breakpoint, and the same number for the same reason: below
   it the sidebar is an overlay and the content column is at its narrowest, so
   the buttons take their own line rather than crushing the breadcrumb. */
@media (max-width: 1323px) {
  .vrto-pagehead__title {
    flex: 1 1 100%;
  }
}

/* ── Ingredient matching · the run panel ──────────────────────────────────
   The controls that FEED the queue. Set apart from the review cards below
   with a filled ground, because starting a run costs FatSecret calls and
   minutes — it should not read like one more button among the per-row ones. */
.vrto-match__run {
  margin: 0 0 28px;
  padding: 20px 22px;
  background: var(--vrto-surface-2, #f4f6f5);
  border: 1px solid var(--vrto-rule, #dfe4e2);
  border-radius: 4px;
}
.vrto-match__run h2 {
  margin: 0 0 14px;
  font-size: 15px;
}
.vrto-match__run-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 16px;
}
.vrto-match__run-row + .vrto-match__run-row {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--vrto-rule, #dfe4e2);
}
.vrto-match__field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 150px;
  flex: 1 1 150px;
  max-width: 260px;
}
.vrto-match__field--check {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.vrto-match__field-label {
  font-size: 12px;
  font-weight: 600;
}
.vrto-match__field-input {
  padding: 7px 9px;
  border: 1px solid var(--vrto-rule, #c9d2cf);
  border-radius: 3px;
  font: inherit;
  font-variant-numeric: tabular-nums;
}
.vrto-match__field-input:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}
/* The hints carry the reasons — that confidence 1 means exact-only, that a
   refresh run never reaches past the first names. They are the documentation
   an operator actually reads, so they stay legible rather than decorative. */
.vrto-match__field-hint {
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--vrto-muted, #64716d);
  flex-basis: 100%;
}
.vrto-match__run-button {
  padding: 8px 16px;
  border: 1px solid var(--vrto-rule, #c9d2cf);
  border-radius: 3px;
  background: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.vrto-match__run-button--primary {
  background: var(--vrto-accent, #0f6e63);
  border-color: var(--vrto-accent, #0f6e63);
  color: #fff;
}
.vrto-match__run-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.vrto-match__run-note {
  margin: 14px 0 0;
  font-size: 12px;
  color: var(--vrto-muted, #64716d);
}

/* ── Ingredient matching · the status tabs ────────────────────────────────
   They exist so `Ask again` is reachable: it only ever rendered on pending
   rows, and the rows that need it are set-aside or no-candidate. */
.vrto-match__tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 16px;
}
.vrto-match__tab {
  padding: 6px 12px;
  border: 1px solid var(--vrto-rule, #c9d2cf);
  border-radius: 999px;
  background: #fff;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.vrto-match__tab--on {
  background: var(--vrto-accent, #0f6e63);
  border-color: var(--vrto-accent, #0f6e63);
  color: #fff;
  font-weight: 600;
}
.vrto-match__tab:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ---------------------------------------------------------------------------
 * Recipe coverage — `vrto-mr`
 *
 * 🔴 THE COMPONENT SHIPPED WITH NO STYLESHEET. `member-recipes.ts` renders a
 * horizontal bar per row — label, bar, count as three spans — and every one of
 * its seventeen class names was absent from this file, so the page rendered as
 * raw inline text: `Breakfast515`, `<10042`, `Vegan341`, label and figure
 * jammed together with the bar an empty span between them. Nothing was wrong
 * with the markup or the numbers; there was simply nothing to lay them out.
 *
 * Its two siblings were fine, which is why it went unnoticed — `vrto-match`
 * carries 87 rules here and `vrto-amt` 40.
 * ------------------------------------------------------------------------- */

.vrto-mr {
  max-width: 980px;
  margin: 0 auto;
  padding: 28px 24px 64px;
  color: var(--vrto-ink);
}

.vrto-mr__title {
  margin: 0 0 4px;
  font-size: 22px;
  letter-spacing: -0.01em;
}

/* The two tiers are different libraries with different rules, so the heading
 * has to separate them harder than a paragraph break. */
.vrto-mr__tier {
  margin: 34px 0 4px;
  padding-top: 18px;
  border-top: 1px solid var(--vrto-line);
  font-size: 15px;
  font-weight: 650;
}

.vrto-mr__tier:first-of-type {
  margin-top: 22px;
}

.vrto-mr__lede {
  margin: 0 0 20px;
  max-width: 62ch;
  color: var(--vrto-muted);
  font-size: 13px;
  line-height: 1.55;
}

.vrto-mr__groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 18px;
}

.vrto-mr__group {
  padding: 16px 18px 18px;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  background: #fff;
}

.vrto-mr__group-title {
  margin: 0 0 3px;
  font-size: 13px;
  font-weight: 650;
}

.vrto-mr__group-note {
  margin: 0 0 14px;
  color: var(--vrto-muted);
  font-size: 11.5px;
  line-height: 1.5;
}

.vrto-mr__rows {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 7px;
}

/* 🔴 The three-column grid IS the fix. As inline spans the label ran straight
 * into the count — which is what "Breakfast515" was — and the bar had no box
 * to fill. A fixed label column also left-aligns every figure down the card,
 * which is the whole reason to draw this as bars rather than a list. */
.vrto-mr__row {
  display: grid;
  grid-template-columns: 92px 1fr 44px;
  align-items: center;
  gap: 10px;
}

.vrto-mr__row-label {
  font-size: 12px;
  color: var(--vrto-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vrto-mr__row-bar {
  position: relative;
  height: 8px;
  border-radius: 999px;
  background: var(--vrto-hover, #eef1f4);
  overflow: hidden;
}

.vrto-mr__row-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--vrto-accent, #0f6e63);
  /* The component writes `width: 0%` for a zero count deliberately — "a bar you
   * can see is a count that exists" — so nothing here may give it a minimum. */
  min-width: 0;
}

.vrto-mr__row-count {
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.vrto-mr__empty,
.vrto-mr__loading,
.vrto-mr__denied {
  margin: 0;
  padding: 14px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  background: #fff;
  color: var(--vrto-muted);
  font-size: 13px;
}

.vrto-mr__denied {
  border-left: 3px solid var(--vrto-danger, #b4232a);
}

/* ---------------------------------------------------------------------------
 * Users → show — the member page's bands and its typographic hierarchy
 *
 * 🔴 This block exists because the page had none. AdminJS renders a show
 * property through `ValueGroup`, which draws the heading as
 * `<Label variant="light">`: 12px, weight 300, `#898a9a`, 4px below. The
 * caption underneath it — the `description` that `RecordTable` and
 * `DescribedValue` render — was `<Text variant="sm" color="grey60">`: 12px,
 * weight 400, `#898a9a`, 4px below. Same size, same colour, same spacing, and
 * the 100 units of weight separating them ran BACKWARDS, the heading being the
 * lighter of the two. A section and its own footnote were one object.
 *
 * Worse, the heading was the faintest thing in its own section: its table's
 * column headers are 12px bold `#191a1d` and its body cells 14px `#191a1d`, so
 * the label carried less weight than the data it labelled. `#898a9a` on white
 * is also 3.4:1, under AA.
 *
 * So the three levels are set here explicitly — band, heading, caption — and
 * the table head is demoted out of the way of the heading above it.
 *
 * 🔴 Scoped under `.vrto-show` throughout, and that is not decoration.
 * `adminjs_Label` and `adminjs_TableHead` are console-wide classes from the
 * design system's `cssClass()` — unscoped, these rules would restyle every
 * list, edit and filter screen in the panel.
 * ------------------------------------------------------------------------- */

/* 🔴 The specificity is deliberate. `.vrto-show label.adminjs_Label` is 0,2,1
 * against the styled-component's own 0,1,0, so it wins on specificity rather
 * than on order — styled-components injects its `<style>` at runtime and there
 * is no guarantee this sheet is parsed after it. Dropping the `label` element
 * or the wrapper class re-opens that race. */
.vrto-show label.adminjs_Label {
  font-size: 13px;
  font-weight: 700;
  color: var(--vrto-ink);
  margin-bottom: 6px;
}

/* The caption, now unmistakably subordinate to the heading: smaller, and
 * `--vrto-muted` (#5c5f66, 6.4:1) rather than the design system's `grey60`
 * (#898a9a, 3.4:1). The measure is capped because these run to three lines —
 * `diagnostics` is 240 characters — and a caption spanning 1180px scans as
 * body copy. */
.vrto-show .vrto-show__desc {
  margin: 0 0 10px;
  max-width: 74ch;
  font-size: 12px;
  line-height: 1.5;
  color: var(--vrto-muted);
}

/* The band. Uppercase and letter-spaced because it is the only thing on the
 * page treated that way, so it cannot be misread as one more section heading;
 * the rule above it is what actually does the grouping. */
.vrto-show .vrto-show__band {
  margin: 34px 0 16px;
  padding-top: 18px;
  border-top: 1px solid var(--vrto-line);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--vrto-muted);
}

/* The first band on the page opens it, so it takes no rule above it and no gap
 * before it — there is nothing there to be separated from.
 *
 * 🔴 The child combinator is load-bearing, and the DOM it depends on is not
 * obvious. Measured against `adminjs/lib/frontend/components/actions/utils/`
 * on 2026-09-01:
 *
 *   - `layout-element-parser` turns each `['@H5', {…}]` in `actions.show.layout`
 *     into its own layout element, and `layout-element-renderer` renders it as a
 *     DIRECT child of the `.vrto-show` Box. So the seven static bands match `>`.
 *   - a bare property name in that layout is wrapped in a `Box` of its own and
 *     then in `<Box flexGrow={1}>` before the component runs. So
 *     `supportHistory`'s band — the one drawn from inside the gate by
 *     `ui/section.ts` — sits two divs deep and does NOT match `>`. It keeps the
 *     rule and the gap above it, which is right: it opens a section in the
 *     middle of the page.
 *
 * ⚠️ This comment said that band was rendered INSIDE a `ValueGroup`, and it is
 * not — `RecordTableView` and `DescribedValueView` both render it as a sibling
 * BEFORE the `ValueGroup`, and both say so in their own headers, because
 * `ValueGroup` draws its `<Label>` ahead of its children and a band passed as a
 * child lands below the heading it introduces. A
 * `.vrto-show .adminjs_ValueGroup .vrto-show__band` rule sat here restoring a
 * margin on the strength of that reading; it could never match, and it is
 * gone. */
.vrto-show > .vrto-show__band:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* One-line scalars two up rather than a full-width block each. `auto-fit` with
 * a floor, not a fixed two columns: the same rule then collapses to one column
 * on a narrow window without a media query, and widens to three on a large
 * one. */
.vrto-show .vrto-show__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 4px 32px;
}

.vrto-show .vrto-show__value {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--vrto-ink);
}

/* `access` and anything else built as newline-joined lines. Block spans rather
 * than `<br>`, because the value is a text node — no HTML goes into a property
 * value on this page. */
.vrto-show .vrto-show__lines {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--vrto-ink);
}

/* The empty-case copy. Muted, because "no operator has run a support action
 * against this member" is an answer but not a finding. */
.vrto-show .vrto-show__empty {
  margin: 0;
  max-width: 74ch;
  font-size: 13px;
  line-height: 1.5;
  color: var(--vrto-muted);
}

/* 🔴 Demoted, not restyled for taste. At the design system's 12px/700/#191a1d
 * these column headers were HEAVIER than the section heading above them, which
 * is the hierarchy inverted: the table shouted its own labels down. Now they
 * sit below both the heading and the body cells they head. */
.vrto-show .adminjs_TableHead td {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--vrto-muted);
}

/* ---------------------------------------------------------------------------
 * Table / list pills — AdminJS `Badge`
 *
 * Figma 1390:13107 (`pillButton` on `exerciseOptionStack`). The design
 * system's Badge is a grey40 fill with white text, or `outline` with
 * grey60 (#898a9a, 3.4:1) on a transparent fill. Neither is the product
 * pill: Cloud Dancer/60 (#e7e8ed at 0.6) over white, Obsidian Night
 * text, 12px/600, 6×12 padding, 24px ends.
 *
 * Boolean columns (`BooleanPropertyValue`) are the ones that show up in
 * every resource table — `isRequired`, `isRatified`, `isActive` — and they
 * always render this atom at `outline` `size="sm"`. The same rule covers
 * the count Badge in the list header, so a Yes in a cell and a "12" beside
 * the title are one object.
 *
 * 🔴 `#app span.adminjs_Badge` is 1,1,1 against the styled-component's
 * 0,1,0. Order cannot be the thing that wins: styled-components injects
 * at runtime. Dropping `span` or `#app` re-opens that race, and the
 * outline variant's `background: transparent` would punch a hole in the
 * wash.
 * ------------------------------------------------------------------------- */
#app span.adminjs_Badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 6px 12px;
  border: 0;
  border-radius: 24px;
  color: #191a1d;
  font-size: 12px;
  font-weight: 600;
  line-height: 18px;
  letter-spacing: -0.2px;
  vertical-align: middle;
  white-space: nowrap;
  background-color: transparent;
  background-image:
    linear-gradient(
      90deg,
      rgba(231, 232, 237, 0.6) 0%,
      rgba(231, 232, 237, 0.6) 100%
    ),
    linear-gradient(90deg, #fff 0%, #fff 100%);
}

/* ─────────────────────────────────────────────────────────────────────────
   Show page: the linked table cell, and the cover image.
   Both added 2026-09-02 with the programme screens' show pages.
   ───────────────────────────────────────────────────────────────────────── */

/* One cell per row on the variants table opens that variant.
 *
 * `--vrto-deep` and not `--vrto-accent`: the same call the token block above
 * spells out. The accent is 2.97:1 against white and this is 13px text.
 * `.vrto-docs__sub a` already uses `--vrto-deep` for exactly this.
 *
 * 🔴 Underlined at rest, not only on hover. A single link inside a table of
 * plain cells has no other cue that it is one — colour alone is the failure
 * mode WCAG 1.4.1 is about, and an operator scanning seven columns for the one
 * they can click should not have to hunt with the pointer. */
.vrto-show .vrto-show__link {
  color: var(--vrto-deep);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.vrto-show .vrto-show__link:hover {
  text-decoration-thickness: 2px;
}

/* The programme-variant cover.
 *
 * 🔴 Capped by HEIGHT, not width. These come in two aspect ratios — a landscape
 * `cover` and a `portrait_cover` — and a width cap would render the portrait
 * one two and a half times taller than the landscape one on the same page,
 * pushing everything below it off the screen. The height cap makes the two sit
 * as a pair.
 *
 * `width: auto` is explicit because `console.css` is loaded after the design
 * system, which sets `img { max-width: 100% }` globally. */
.vrto-show .vrto-show__cover {
  display: block;
  max-height: 200px;
  width: auto;
  max-width: 100%;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  /* The covers are photographic and several are near-white at the edges. The
   * border alone disappears on those; the tint behind a transparent PNG is
   * what stops one rendering as a floating cutout. */
  background: #f6f6f8;
}

/* ---------------------------------------------------------------------------
 * Cutover → Exercise videos — `vrto-cv`
 *
 * The repair screen for what the nightly Bunny reconciliation finds. Its
 * layout carries one argument: the four categories above the rule are WORK,
 * and everything below it is context. Anything that blurs that line — a shared
 * card style, one amber for both halves — turns a queue into a wall.
 *
 * 🔴 The root is named in the token block near the top of this file. It renders
 * its own `main`, so without that line every `var(--vrto-*)` below would
 * resolve to nothing. That has happened four times in this console already.
 * ------------------------------------------------------------------------- */

.vrto-cv {
  max-width: 980px;
  margin: 0 auto;
  padding: 28px 24px 64px;
  color: var(--vrto-ink);
}

.vrto-cv__title {
  margin: 0 0 4px;
  font-size: 22px;
  letter-spacing: -0.01em;
}

.vrto-cv__lede {
  margin: 0 0 14px;
  max-width: 62ch;
  color: var(--vrto-muted);
  font-size: 13px;
  line-height: 1.55;
}

/* The sync stamp reads as data, not prose — every count on the page is only as
 * true as this line. */
.vrto-cv__synced {
  margin: 0 0 20px;
  font-size: 12px;
  color: var(--vrto-muted);
  font-variant-numeric: tabular-nums;
}

/* 🔴 Never synced is not a quiet caption. With an empty video list every id
 * reads as "unknown", so the four counts below would be alarming and wrong. */
.vrto-cv__stale {
  margin: 0 0 20px;
  padding: 10px 12px;
  border-left: 3px solid var(--vrto-danger);
  background: #fdf2f0;
  font-size: 13px;
  line-height: 1.5;
}

.vrto-cv__verdict {
  margin: 0 0 26px;
  padding: 12px 14px;
  border: 1px solid var(--vrto-line);
  border-left: 3px solid var(--vrto-danger);
  border-radius: 8px;
  background: #fff;
  font-size: 14px;
  font-weight: 600;
}

.vrto-cv__verdict--clear {
  border-left-color: var(--vrto-green);
  color: var(--vrto-muted);
  font-weight: 500;
}

.vrto-cv__notice,
.vrto-cv__error {
  margin: 0 0 16px;
  padding: 10px 12px;
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.5;
}

.vrto-cv__notice {
  border-left: 3px solid var(--vrto-green);
  background: #eefaf4;
}

.vrto-cv__error {
  border-left: 3px solid var(--vrto-danger);
  background: #fdf2f0;
  color: var(--vrto-danger);
}

/* --- the four work categories ------------------------------------------- */

.vrto-cv__cat {
  margin: 0 0 22px;
  padding: 16px 18px 18px;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  background: #fff;
}

.vrto-cv__cat-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
}

.vrto-cv__cat-title {
  margin: 0;
  font-size: 15px;
  font-weight: 650;
}

.vrto-cv__count {
  min-width: 26px;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--vrto-danger);
  color: #fff;
  font-size: 12px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* --- the counts strip ----------------------------------------------------
 *
 * 🔴 Every category, including the ones at zero — and it is what makes it safe
 * for a category at zero to have NO section below it. Four blocks each saying
 * "nothing here" is the wall of amber this layout exists to avoid, arriving by
 * a different door; four numbers in a row is the same information in one line.
 *
 * Zero is drawn as calm rather than as another number, so an operator who
 * opens this and reads four greys has finished and can close it.
 * ------------------------------------------------------------------------- */

.vrto-cv__strip {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
  margin: 0 0 18px;
  padding: 0;
  list-style: none;
}

.vrto-cv__stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--vrto-line);
  border-top: 3px solid var(--vrto-danger);
  border-radius: 8px;
  background: #fff;
}

.vrto-cv__stat--zero {
  border-top-color: #dfe3ea;
  background: #fbfbfc;
}

.vrto-cv__stat-n {
  font-size: 22px;
  font-weight: 650;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--vrto-danger);
}

.vrto-cv__stat--zero .vrto-cv__stat-n {
  color: var(--vrto-muted);
}

.vrto-cv__stat-l {
  color: var(--vrto-muted);
  font-size: 11.5px;
  line-height: 1.35;
}

.vrto-cv__cat-note {
  margin: 6px 0 0;
  max-width: 68ch;
  color: var(--vrto-muted);
  font-size: 12.5px;
  line-height: 1.5;
}

.vrto-cv__rows {
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}

/* One row is one job: who is broken on the left, the control on the right. */
.vrto-cv__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 0;
  border-top: 1px solid var(--vrto-line);
}

.vrto-cv__row:first-child {
  border-top: none;
}

.vrto-cv__who {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  flex-wrap: wrap;
}

.vrto-cv__name {
  font-size: 14px;
  font-weight: 550;
}

.vrto-cv__id {
  color: var(--vrto-muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* The id an operator would otherwise have to read out of an email. */
.vrto-cv__guid {
  padding: 1px 6px;
  border-radius: 4px;
  background: #f4f5f8;
  color: var(--vrto-muted);
  font-size: 11.5px;
}

/* 🔴 A GRID with fixed tracks, not a flex row.
 *
 * As a flex row every control sat at a different x on every line: a `<select>`
 * sizes itself to its LONGEST OPTION, so the dropdown was 142px on one row and
 * 218px on the next, and the search box in front of it wandered 76px between
 * rows of the same list. Measured, not guessed — the page rendered and the
 * boxes were ragged.
 *
 * This is a queue somebody works down one line at a time, so the control has
 * to be in the same place on every line. Fixed tracks and `min-width: 0` on
 * the select, which is what stops its intrinsic width blowing the column. */
.vrto-cv__repair {
  display: grid;
  /* The last track is `max-content` above its floor: "7 more — type to search"
   * needs 132px and a 104px column CLIPPED it at the card edge — measured on
   * the rendered page. The row has ~265px of slack, so the name gives it up. */
  grid-template-columns: 148px 206px 62px minmax(140px, max-content);
  align-items: center;
  gap: 8px;
  flex: none;
}

.vrto-cv__repair .vrto-cv__search,
.vrto-cv__repair .vrto-cv__select {
  width: 100%;
  max-width: none;
  min-width: 0;
}

/* "No video matches …" stands where the dropdown would be, so the button and
 * the hint beside it do not jump left when a search misses. */
.vrto-cv__repair .vrto-cv__no-options {
  grid-column: 2;
  text-align: left;
}

/* 🔴 The box comes BEFORE the dropdown, in reading order and in tab order.
 * The dropdown is a shortlist — see `suggest` — and a control that narrows it
 * placed after it reads as a filter on what you already rejected. */
.vrto-cv__search {
  width: 150px;
  padding: 6px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  background: #fff;
  color: var(--vrto-ink);
  font-size: 13px;
}

.vrto-cv__select {
  max-width: 320px;
  padding: 6px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  background: #fff;
  color: var(--vrto-ink);
  font-size: 13px;
}

/* ⚠️ The shortlist admits it is one. Without this an operator who cannot see
 * the video they want concludes it is not in Bunny, and the next step from
 * that conclusion is asking someone to re-upload a video that already exists. */
.vrto-cv__hidden {
  color: var(--vrto-muted);
  font-size: 11.5px;
  white-space: nowrap;
  text-align: right;
}

.vrto-cv__search:focus-visible,
.vrto-cv__select:focus-visible,
.vrto-cv__link:focus-visible {
  outline: 2px solid var(--vrto-danger);
  outline-offset: 2px;
}

/* Every write re-reads the whole page from the server, so the numbers on
 * screen during one really are stale. Dimmed rather than replaced by a
 * spinner: the row being repaired should stay where the eye left it. */
.vrto-cv--busy {
  opacity: 0.6;
  pointer-events: none;
}

.vrto-cv__link {
  padding: 6px 14px;
  border: 1px solid var(--vrto-ink);
  border-radius: 6px;
  background: var(--vrto-ink);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

/* Disabled until a video is chosen — the button is the commit, and one that is
 * always live invites a click that does nothing. */
.vrto-cv__link:disabled {
  border-color: var(--vrto-line);
  background: #eef0f4;
  color: var(--vrto-muted);
  cursor: not-allowed;
}

.vrto-cv__no-options {
  color: var(--vrto-muted);
  font-size: 12.5px;
  text-align: right;
}

/* --- the row just repaired ------------------------------------------------
 *
 * 🔴 Green, and it is the only green on the page. Everything above it is a
 * problem; this is the one block that reports a success — and it still carries
 * a control, because the success is the thing nothing downstream can check.
 * Drawn as a result rather than as a fifth category.
 * ------------------------------------------------------------------------- */

.vrto-cv__linked {
  margin: 26px 0 0;
  padding: 14px 16px 6px;
  border: 1px solid var(--vrto-line);
  border-left: 3px solid var(--vrto-green);
  border-radius: 8px;
  background: #fbfdfc;
}

.vrto-cv__linked-title {
  margin: 0;
  font-size: 14px;
  font-weight: 650;
}

.vrto-cv__linked-note {
  margin: 6px 0 4px;
  max-width: 68ch;
  color: var(--vrto-muted);
  font-size: 12.5px;
  line-height: 1.5;
}

/* --- below the rule: true, and nobody's job tonight ---------------------- */

.vrto-cv__info {
  margin: 34px 0 0;
  padding-top: 20px;
  border-top: 1px solid var(--vrto-line);
}

.vrto-cv__info-title {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 650;
  color: var(--vrto-muted);
}

.vrto-cv__info-note {
  margin: 0 0 16px;
  max-width: 62ch;
  color: var(--vrto-muted);
  font-size: 12.5px;
  line-height: 1.55;
}

.vrto-cv__info-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 16px;
}

/* Deliberately flatter than a work card: no border-left accent, muted heading,
 * grey ground. It should read as a footnote, not as a fifth thing to do. */
.vrto-cv__info-card {
  padding: 14px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  background: #fbfbfd;
}

.vrto-cv__info-card h4 {
  margin: 0 0 6px;
  font-size: 13px;
  font-weight: 650;
}

.vrto-cv__info-card p {
  margin: 0;
  color: var(--vrto-muted);
  font-size: 12.5px;
  line-height: 1.55;
}

.vrto-cv__info-card ul {
  margin: 10px 0 0;
  padding-left: 18px;
  color: var(--vrto-muted);
  font-size: 12.5px;
  line-height: 1.7;
}

.vrto-cv__loading,
.vrto-cv__denied {
  max-width: 980px;
  margin: 0 auto;
  padding: 28px 24px;
  color: var(--vrto-muted);
  font-size: 13px;
}

@media (max-width: 720px) {
  .vrto-cv__strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .vrto-cv__row {
    align-items: stretch;
    flex-direction: column;
    gap: 8px;
  }

  /* Two rows of two: the pair that chooses a video, then the pair that
   * commits it. The tracks stay equal so the column edge holds down the list. */
  .vrto-cv__repair {
    grid-template-columns: 1fr auto;
  }

  .vrto-cv__repair .vrto-cv__search,
  .vrto-cv__repair .vrto-cv__select,
  .vrto-cv__repair .vrto-cv__no-options {
    grid-column: 1 / -1;
  }

  .vrto-cv__hidden {
    text-align: left;
  }

  .vrto-cv__no-options {
    text-align: left;
  }
}

