/*
 * 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` — is where the two colours come
 * from. 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  #ff2e92      Obsidian Night/100  #191a1d
 *   Electric Sky   #00a3e0      Smoke               #e7e8ed
 *
 * 🔴 **The selected row is a GREY FILL and an EDGE RAIL, not a pill.** Until
 * 2026-09-08 it was the Figma frame's own 40/60 wash laid across the whole
 * row — rose bleeding in from the left, obsidian holding the middle two
 * fifths, sky bleeding in from the right, white text on top. It was the
 * heaviest mark in the console and it repeated down a white panel.
 *
 * ⚠️ **A lighter version of that wash was built on 2026-09-01 and reverted the
 * same day, and this is NOT that.** Inverting the wash to a white base cost the
 * row its shape — the tinted ends over #fff left the middle looking like bare
 * panel — and the 1px ring added to get the shape back read as a form field.
 * The note left behind said "if it ever needs to sit lighter, move the ALPHA,
 * not the base", and that advice was about keeping the wash. This version does
 * not keep the wash: an OPAQUE #e7e8ed fill gives the row its shape with no
 * ring to draw, and the two brand colours move to a discrete 4px mark on the
 * row's leading edge, where they read as a mark rather than as a surface.
 *
 * ⚠️ The alphas differ per surface, and there is no single number to quote:
 *
 *   the selected rail   1.0  — it is the only thing carrying the brand colours
 *                              now that the wash is gone, and it is 4px wide
 *   the section rail    0.8  — a step below the selected one, so the two rails
 *                              rank even before the fill is taken into account
 *
 * Read the alpha off the rule, never off this block.
 *
 * 🔴 **Both rails are drawn by a pseudo-element the size of the WHOLE ROW, not
 * the size of the rail**, carrying the row's own 8px radius over a 4px
 * background strip anchored `left`. A 4px-wide box cannot hold an 8px corner:
 * its two left radii sum to 16px across a 4px width, so CSS scales both by
 * 4/16 and you get a 2px curve that cuts INSIDE the row's corner and leaves a
 * sliver. Letting a full-size element's radius clip its own background is what
 * makes the strip curve WITH the box. Do not "simplify" it to `width: 4px`.
 *
 * ## 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    smoke fill, ink text at 600, a full-strength rail
 *   the section it is in   ink text at 600, a 0.8 rail while it is collapsed
 *   everything else        grey60 icon, ink text at 400, flat #f4f5f7 hover
 *
 * ⚠️ The FILL is what separates the first two. They draw the same rail in the
 * same place now, and the only reason no row can carry both marks is that
 * `sidebar-nav.ts` keeps the two classes mutually exclusive.
 *
 * ⚠️ 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: #191a1d;
  font-weight: 600;
  background-color: #e7e8ed;
}

/* 🔴 `inset: 0` with an 8px radius over a 4px BACKGROUND, not a 4px box — the
   reason is in the block at the top of this section, and it is the whole point
   of the treatment. `pointer-events` is not optional either: a positioned
   pseudo-element paints above the row's in-flow content, so without it the
   leading 4px of every selected row swallows its own clicks. */
#app [data-css='sidebar'] .vrto-nav__item.is-selected::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 8px;
  background: linear-gradient(180deg, #ff2e92 0%, #00a3e0 100%) left / 4px 100%
    no-repeat;
  pointer-events: none;
}

/* 🔴 The icon only. The chevron keeps its base #898a8f because it is not the
   row's subject and stays quiet at every state. The icon cannot: #898a8f on
   #e7e8ed measures 2.7:1, under WCAG 1.4.11's 3:1 for a non-text glyph.
   `is-current-section` below inks its icon for the same reason and leaves its
   chevron alone the same way. */
#app [data-css='sidebar'] .vrto-nav__item.is-selected .vrto-nav__icon {
  color: #191a1d;
}

/* Hover must not lighten the selection. `.is-selected` and
   `.vrto-nav__item:hover` are a specificity tie — 1,3,0 each — that source
   order already resolves this way; this rule is what makes that independent of
   where either one sits in the file. */
#app [data-css='sidebar'] .vrto-nav__item.is-selected:hover {
  background-color: #e7e8ed;
}

/* ── 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 selected child draws this same rail
   one level down, 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.
   ⚠️ Same shape as the selected rail, one step down in alpha, and NO fill.
   The fill is what tells the two apart — see the block above. */
#app
  [data-css='sidebar']
  .vrto-nav__item.is-current-section:not(.is-open)::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 8px;
  background: linear-gradient(
      180deg,
      rgba(255, 46, 146, 0.8) 0%,
      rgba(0, 163, 224, 0.8) 100%
    )
    left / 4px 100% no-repeat;
  pointer-events: none;
}

/* ── 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;
}

/* 🔴 Flat, at every state. This line carried the rail's own rose→sky gradient
   until 2026-09-08, back when the selected row was a pill and this thread was
   the only thing pointing down at it. The selected row now draws a rail of its
   own 12px to the right of this line, and two vertical gradients that close
   together read as two selections — the rule the heading above already states.
   The guide's job is the indent, not the state. */
#app [data-css='sidebar'] .vrto-nav__list--sub::before {
  content: '';
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: 20px;
  width: 1px;
  background: #e7e8ed;
}

#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 declarations that make it a bar live here.
 *
 * ⚠️ The border colour below is still a LITERAL, and the reason has not
 * changed: the bar is a sibling of every page root rather than a descendant of
 * one. `.vrto-topbar` is now itself in the token list below — added for the
 * title, which needs `--vrto-ink` — so a `var()` here would resolve; the
 * literal stays only because changing a shipped colour is not what that edit
 * was. Anything NEW in this block should use the tokens. */
#app .vrto-topbar {
  height: 64px;
  border-bottom: 1px solid #e7e8ed;
  background: #fff;
  /* 🔴 Sticky, not fixed, and it works because of where AdminJS puts this
     element rather than because of anything here. `application.tsx` renders
     `Box data-css="app-content"` with `overflowY: auto` and the bar as its
     FIRST child, so the bar's nearest scrollport is the one the page scrolls
     in — `top: 0` pins it against that and nothing needs a height, an offset
     or a spacer. A `position: fixed` bar would need all three, and would have
     to know the sidebar's width not to cover it.

     The stacking order is picked against the three already in this file:
     ABOVE `.vrto-menu__list` (10) so a row's dropdown scrolls under the bar
     rather than over it; BELOW the sidebar (AdminJS's own `SIDEBAR_Z_INDEX`
     is 50) so the mobile drawer still covers the bar as it always has; and
     below `.vrto-dialog__overlay` (100), which has to cover everything.
     `.vrto-builder__toast` (20) is bottom-centre and never meets it. */
  position: sticky;
  top: 0;
  z-index: 30;
}
/* The page title, published by whatever is on screen (`page-title.ts`).
   An `h1` when a custom page published a string, a `div` around the
   breadcrumb when a list or a show published its trail.

   🔴 No `flex-grow`. `Version` beside it is the bar's spacer and the only
   thing right-aligning the account menu; a second growing item would split
   the free space and leave the menu mid-bar. `min-width: 0` is what lets this
   shrink below its content — a flex item's `min-width` is `auto`, so without
   it a long record title pushes the account menu off the right-hand edge
   rather than truncating, and the bar has no scrollbar to get it back. */
#app .vrto-topbar__title {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  margin: 0 16px 0 4px;
  color: var(--vrto-ink);
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.01em;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- 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-builder` is the TWELFTH. The Program builder shipped without this
 * line, and every panel border, table gridline and input box on the page
 * resolved to nothing — measured 2026-09-07. Same bug as `.vrto-show`.
 *
 * `.vrto-modq` (Moderation queue), `.vrto-desktop-required` (the "needs a
 * laptop" message) and `.vrto-phone-launcher` (the Dashboard's phone
 * variant) are the THIRTEENTH through FIFTEENTH, added together for the
 * phone-nav ticket. Named here from the day each shipped, on the strength of
 * this list's own history.
 *
 * 🔴 `.vrto-topbar` is the SIXTEENTH, and it is the one entry here that is
 * not a page root. Every other selector in this list is a `main` or a `div`
 * that a page component renders; the bar is AdminJS's own chrome, a SIBLING of
 * all of them, which is exactly why it could not borrow the tokens from a page
 * and had to be named. It joined the list when the page title moved into it —
 * `.vrto-topbar__title` reads `--vrto-ink`, and without this line the title
 * would render in AdminJS's own text colour rather than the console's.
 *
 * 🔴 `.vrto-cm` is the SEVENTEENTH — Cutover → Exercise merges. It shipped
 * with NO block in this file at all, so the page drew as an unstyled list:
 * browser-default table, no rule, no amber. Named here in the edit that gave
 * it one, because it renders its own root and every `var(--vrto-*)` in its
 * block below would otherwise resolve to nothing — the fifth time. */
.vrto-topbar,
.vrto-dashboard,
.vrto-operators,
.vrto-support,
.vrto-show,
.vrto-docs,
.vrto-match,
.vrto-amt,
.vrto-uc,
.vrto-mr,
.vrto-cv,
.vrto-ee,
.vrto-cm,
.vrto-mqa,
.vrto-listhead,
.vrto-pagehead,
.vrto-builder,
.vrto-modq,
.vrto-judge,
.vrto-desktop-required,
.vrto-phone-launcher {
  --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;
}

/* ⚠️ No `h1` rule here any more. Every page's heading is published to the
   top bar (`page-title.ts`) and styled once, by `#app .vrto-topbar__title`. */

/* The period selector, and the refresh warning that sits beside it. It used to
 * be the heading on the left and the selector on the right; the heading is in
 * the top bar now, so `space-between` would have parked the selector against
 * the LEFT edge — the one place it has never been. It stays where an operator
 * last saw it. */
.vrto-dashboard__header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: flex-end;
  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,
.vrto-builder button:focus-visible,
.vrto-builder input:focus-visible,
.vrto-builder select: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;
}

/* The sync stamp alone, now that the heading is in the top bar — right-aligned
 * for the reason `.vrto-dashboard__header` gives. */
.vrto-docs__head {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  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);
}

/* The status select alone, same reason. */
.vrto-docs__detailhead {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  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__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;
}

/* A block, not a two-column grid. The action moved into the head so that the
 * figure grid inside here is exactly as wide as the one on our row above —
 * see `vrto-match__candidate-actions`. */
.vrto-match__candidate {
  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 {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
}

.vrto-match__candidate-actions {
  display: flex;
  flex: none;
  align-items: center;
  gap: 10px;
}

.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;
}

/* 🔴 A GRID, not a flex wrap, and the SAME template on our row and on every
 * candidate's — that shared template is the whole point of this block. The
 * card asks "is this the same food?", which is answered by reading one column
 * downwards; under `flex-wrap: wrap` a candidate with a long serving unit
 * pushed its calories out of line with ours and the comparison broke silently
 * at exactly the widths where it mattered most. */
.vrto-match__figures {
  display: grid;
  grid-template-columns: minmax(84px, 1.3fr) repeat(4, minmax(56px, 1fr));
  gap: 10px 18px;
}

/* Our side. Same columns, tinted so the card reads as one row of "held"
 * against N rows of "offered" rather than as five similar-looking foods. */
.vrto-match__figures--ours {
  padding: 12px 18px 14px;
  border-bottom: 1px solid var(--vrto-line);
  background: #f7f8fa;
}

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

/* The change against our figure for the same field. Muted by default: on a
 * good match five of these read within a percent or two, and colouring them
 * would make the ordinary case the loudest thing on the screen. */
.vrto-match__figure-delta {
  margin-top: 2px;
  color: var(--vrto-muted);
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
}

.vrto-match__figure-delta--wide {
  color: var(--vrto-danger);
  font-weight: 650;
}

/* Which side a row is. Ours and theirs are both a food name with figures
 * under it, so the label is on the row itself. */
.vrto-match__from {
  margin-right: 7px;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--vrto-ink);
  color: #fff;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  vertical-align: 1px;
}

.vrto-match__from--ours {
  background: #e7e9ef;
  color: var(--vrto-ink);
}

.vrto-match__provenance {
  margin-left: 8px;
  padding-left: 8px;
  border-left: 1px solid var(--vrto-line);
}

.vrto-match__food-id {
  margin-left: 6px;
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

/* Says what the list below IS, once, above it. */
.vrto-match__candidates-head {
  padding: 9px 18px;
  border-bottom: 1px solid var(--vrto-line);
  background: #fbfbfc;
  color: var(--vrto-muted);
  font-size: 11px;
}

.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;
}

.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__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;
}
/*
 * The conversion control, on its own row.
 *
 * 🔴 `grid-column: 1 / -1`. `.vrto-amt__line` is a six-column grid and this is
 * a seventh child, so without it the control is squeezed into the gap after
 * Remove and the inputs collapse to nothing.
 *
 * It appears only on a broken line that HAS a unit — a null unit means the
 * quantity is already a count of servings, so there is nothing to convert.
 */
.vrto-amt__convert {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  padding-top: 8px;
  border-top: 1px dashed var(--vrto-line);
  font-size: 11.5px;
  color: var(--vrto-muted);
}
.vrto-amt__grams {
  width: 72px;
  padding: 5px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  font: inherit;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.vrto-amt__convert-save {
  padding: 5px 12px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  background: var(--vrto-surface);
  font: inherit;
  font-size: 11.5px;
  font-weight: 560;
  cursor: pointer;
}
.vrto-amt__convert-save:disabled {
  opacity: 0.45;
  cursor: default;
}
.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);
}
/* ---------------------------------------------------------------------------
 * Build a new recipe — the form on Recipe amounts, reached from the Recipes
 * list's "Build a new recipe" button.
 *
 * 🔴 There were NO `__new*` rules here at all until 2026-09-08, so the form
 * shipped in PR #90 as a column of bare browser inputs. That was survivable
 * with three fields; the tagging controls make it thirteen, and twelve
 * unlabelled boxes in a row is how somebody marks the wrong recipe vegan.
 * ------------------------------------------------------------------------- */

/* ⚠️ An ANCHOR since 2026-09-08, not a button. It used to open a collapsed
 * form on this page; it now links to `pages/recipe-new`, so middle-click and
 * open-in-new-tab work — the same reason `list-header.ts` builds an `href`.
 * That is why `display` and `text-decoration` are set, and why the hover rule
 * lost its `:not(:disabled)`: an `<a>` has no disabled state. */
.vrto-amt__new-open {
  display: inline-block;
  margin: 0 0 20px;
  padding: 9px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 9px;
  background: #fff;
  color: var(--vrto-ink);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
.vrto-amt__new-open:hover {
  border-color: var(--vrto-accent);
  color: var(--vrto-accent);
}
.vrto-amt__new {
  margin: 0 0 24px;
  padding: 18px 20px;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}
.vrto-amt__new-note,
.vrto-amt__new-hint {
  margin: 0 0 14px;
  max-width: 70ch;
  color: var(--vrto-muted);
  font-size: 12.5px;
  line-height: 1.55;
}
.vrto-amt__new-name,
.vrto-amt__new-serves,
.vrto-amt__new-serve-unit,
.vrto-amt__new-search {
  margin: 0 8px 10px 0;
  padding: 8px 11px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  font-size: 13px;
}
.vrto-amt__new-name {
  width: min(340px, 100%);
}
.vrto-amt__new-serves {
  width: 90px;
}
.vrto-amt__new-search {
  width: min(340px, 100%);
}
.vrto-amt__new-lines,
.vrto-amt__new-matches {
  margin: 0 0 12px;
  padding: 0;
  list-style: none;
}
.vrto-amt__new-line,
.vrto-amt__new-matches li {
  display: flex;
  gap: 10px;
  align-items: center;
  padding: 6px 0;
  border-bottom: 1px solid var(--vrto-line);
  font-size: 13px;
}
.vrto-amt__new-line input {
  width: 90px;
  padding: 5px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
}
.vrto-amt__new-line span,
.vrto-amt__new-matches li span {
  flex: 1;
}

/* The tagging groups. A `fieldset` because they are genuinely grouped
 * questions, and a `legend` is the one heading a screen reader reads as one. */
.vrto-amt__new-tags {
  margin: 0 0 16px;
  padding: 14px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
}
.vrto-amt__new-tags legend {
  padding: 0 6px;
  color: var(--vrto-muted);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.vrto-amt__new-meals {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 20px;
}
.vrto-amt__new-check {
  display: flex;
  gap: 7px;
  align-items: center;
  font-size: 13px;
}
/* 🔴 Every control keeps its own visible label. `auto-fit` rather than a fixed
 * count so six flags do not become a horizontal scroll on a laptop. */
.vrto-amt__new-flags {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 10px 16px;
}
.vrto-amt__new-flag,
.vrto-amt__new-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12.5px;
}
.vrto-amt__new-flag span,
.vrto-amt__new-field span {
  color: var(--vrto-muted);
}
.vrto-amt__new-flag select,
.vrto-amt__new-field select,
.vrto-amt__new-field input {
  padding: 7px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
  font-size: 13px;
}
.vrto-amt__new-field {
  margin: 0 0 10px;
  max-width: 340px;
}
.vrto-amt__new-actions {
  display: flex;
  gap: 10px;
  margin-top: 4px;
}
.vrto-amt__new-actions button {
  padding: 9px 18px;
  border: 1px solid var(--vrto-line);
  border-radius: 9px;
  background: #fff;
  font-size: 13px;
  cursor: pointer;
}
.vrto-amt__new-save {
  border-color: var(--vrto-accent) !important;
  background: var(--vrto-accent) !important;
  color: #fff;
  font-weight: 600;
}
.vrto-amt__new-actions button:disabled {
  opacity: 0.5;
  cursor: default;
}

@media (max-width: 820px) {
  .vrto-amt__split {
    grid-template-columns: 1fr;
  }
  .vrto-amt__line {
    grid-template-columns: 1fr 1fr;
  }
  .vrto-amt__new-flags {
    grid-template-columns: 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). Two
 * rules here reach into a component this console does not own: the field
 * margin reset, and the live-region clip on `.react-datepicker__aria-live`.
 * Both are commented where they sit.
 * ------------------------------------------------------------------------- */

.vrto-pagehead {
  margin-bottom: 12px;
}
/* 🔴 The row is gone on a screen that has no buttons, and so is its margin.
   `list-header.ts` and `show-header.ts` render no `__row` at all when there is
   nothing to put in it — the title moved to the top bar and the rule under the
   row would otherwise draw a bare line across the page. What is left here is
   an empty element, and 12px of air above a table for no reason is the last
   trace of that row. */
.vrto-pagehead:empty {
  margin-bottom: 0;
}
.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);
}
/* ⚠️ This renders inside the TOP BAR now, not on the page — `list-header.ts`
   and `show-header.ts` build it and publish it through `page-title.ts`. The
   selectors below still describe the same element, which is why they did not
   move; `flex: 1 1 auto` did have to go, because in the bar the growing item
   is `Version` and a second one un-right-aligns the account menu. */
.vrto-pagehead__title {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 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;
}
/*
 * 🔴 AdminJS's last crumb still has an `:after`. `breadcrumbs.tsx` only
 * blanks `content` on `:last-child` — it leaves `padding: 0 8px`
 * (`theme.space.default`) on the empty pseudo, so every trail ended with
 * 16px of air after the resource name. On a list that padded the count
 * badge; on a show it sat in front of the record slash and read as
 * `Education     / Why calories are King`. Kill the leftover padding.
 */
.vrto-pagehead__title > [data-css$='breadcrumbs'] > :last-child::after {
  content: none;
  padding: 0;
}
/* `margin-left: auto` because the buttons are the row's only occupant now. The
   title used to grow and push them right; with it gone they would sit against
   the left edge, under the sidebar toggle rather than over the table's last
   columns where every other screen puts them. */
/* 🔴 AdminJS's own header, on every action this override does not reshape
   (edit, new, delete). It draws a breadcrumb row of its own, and the trail is
   already in the top bar — `list-header.ts` publishes it for these screens too,
   so without this an edit page states where it is twice, one line apart.

   The `H2` beside it is deliberately left alone: "Edit" is that screen's own
   heading and is the one thing the trail does not say. Scoped to the wrapper
   `list-header.ts` adds rather than to `[data-css$='breadcrumbs']` at large,
   which would also take out the copy in the bar. */
#app .vrto-stockhead [data-css$='breadcrumbs'] {
  display: none;
}
.vrto-pagehead__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  margin-left: auto;
}

/* ── 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 blanks the slash there
   (and, until the rule above, left the padding behind). The 8px padding
   matches AdminJS's own `padding: 0 theme.space.default` so the record
   slash sits at the same rhythm as `Dashboard / Education`. */
.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: '/';
  padding: 0 8px;
  color: var(--vrto-muted);
}
/*
 * `__title` keeps `gap: 10px` for the list count badge. A show's crumb is
 * the next trail item, not a sibling widget, so that gap would sit in
 * front of the slash on top of the leftover `:after` this file already
 * kills — the same hole, half as wide.
 *
 * 🔴 A MODIFIER, and it used to be `.vrto-showhead .vrto-pagehead__title`.
 * The title is published to the top bar now and the show header is still on
 * the page, so the descendant selector stopped matching anything — which
 * fails as 10px of air in front of a record's slash, not as an error.
 * `show-header.ts` puts the class on.
 */
.vrto-pagehead__title--show {
  gap: 0;
}

/* 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);
}

/* 🔴 NOT scoped under `.vrto-listhead`. react-datepicker renders a live
   region that announces "Selected date: Tuesday, September 1st, 2026 at
   12:00 AM". `@adminjs/design-system`'s copied stylesheet predates that
   node (datepicker.styles.ts has no `.react-datepicker__aria-live`), so the
   sentence draws as a second line under every calendar — filters and edit
   forms alike. The clip is react-datepicker's own rule, verbatim; without
   `clip-path: circle(0)` the class is present and the text is visible.
   Unscoped because the input-container region sits in the field and the
   calendar's own region sits in a popper that is not under this header. */
.react-datepicker__aria-live {
  position: absolute;
  clip-path: circle(0);
  border: 0;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  width: 1px;
  white-space: nowrap;
}
/* 🔴 NOT the sidebar's 1324px breakpoint, and this rule was written at it for a
   reason that is the exact opposite of what happens.

   It read `max-width: 1323px`, justified as "below it the sidebar is an overlay
   and the content column is at its narrowest". The first half is right and the
   second half is backwards. `StyledSidebar.defaultProps` is
   `position: ['absolute','absolute','absolute','absolute','initial']`
   (`adminjs/.../app/sidebar/sidebar.js:39`) against breakpoints
   `['577px','769px','1024px','1324px']` — the last being 1024 + the 300px
   sidebar (`@adminjs/design-system/build/theme.js:262`). So AT and above 1324px
   the sidebar is in flow and eats 300px; BELOW it the sidebar is absolute and
   off-canvas at `left: -300px`, and the content Box — `flexGrow: 1` in the
   app's flex row (`application.js:61`) — takes the FULL viewport width.

   The content column is therefore at its WIDEST at 1323px and loses ~300px at
   1324px. The old rule fired precisely across the range with the most room to
   spend, which is why a laptop under 1324px — where the sidebar is a drawer and
   looks collapsed — dropped Filter and Create Program onto their own line with
   a screen's width of empty space beside the breadcrumb.

   ⚠️ The desktop collapse toggle (`components/sidebar-toggle.ts`,
   `html[data-vrto-sidebar='collapsed']`) is a separate mechanism scoped to
   `min-width: 1324px`, and it never interacted with this rule in either
   direction.

   820px is a width where the row genuinely cannot hold a breadcrumb and two
   button groups, and it is already a breakpoint in this file. Above it the row
   needs no help: `__row` is `flex-wrap: wrap` and `__title` is `min-width: 0`,
   so the actions take their own line if and only if they truly do not fit. */
@media (max-width: 820px) {
  .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` 83.
 *
 * ⚠️ That second figure was **40** until 2026-09-08, and it was right when it
 * was written: `vrto-amt` had the queue and the detail panel styled and its
 * `__new*` form — the recipe builder — styled by nothing at all, the same
 * failure this block is about, one component over and unnoticed for the same
 * reason. Re-measure rather than trusting either number:
 *
 *     grep -cE "^\s*\.vrto-amt" admin/public/console.css
 * ------------------------------------------------------------------------- */

.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);
}

/* A run of yes/no columns read across — `components/yes-no-table.ts`. The
 * wrapper scrolls rather than the page when seven columns meet a narrow
 * window. */
.vrto-show .vrto-yesno {
  overflow-x: auto;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
}

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

.vrto-show .vrto-yesno__table th,
.vrto-show .vrto-yesno__table td {
  padding: 10px 14px;
  text-align: center;
  white-space: nowrap;
  color: var(--vrto-ink);
}

.vrto-show .vrto-yesno__table thead th {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--vrto-muted);
  background: #f7f7f9;
  border-bottom: 1px solid var(--vrto-line);
}

.vrto-show .vrto-yesno__table tbody tr + tr th,
.vrto-show .vrto-yesno__table tbody tr + tr td {
  border-top: 1px solid var(--vrto-line);
}

.vrto-show .vrto-yesno__table tbody th {
  text-align: left;
  font-size: 12px;
  font-weight: 600;
  color: var(--vrto-muted);
}

.vrto-show .vrto-yesno__pill {
  display: inline-block;
  min-width: 44px;
  padding: 3px 10px;
  border-radius: 24px;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
}

/* Text colours are the darker shade of each hue so the glyph clears AA on its
 * own wash; the brand greens and yellows do not as ink. */
.vrto-show .vrto-yesno__pill--yes {
  background: #e3f8ef;
  color: #0a6b47;
}

.vrto-show .vrto-yesno__pill--no {
  background: #f1f1f4;
  color: var(--vrto-muted);
}

/* 🔴 Loud on purpose: an undecided flag drops the recipe from the pool of
 * everyone with that requirement. See `FLAG_NOTE` in `recipe.resource.ts`. */
.vrto-show .vrto-yesno__pill--undecided {
  background: #fff4d6;
  color: #7a5200;
  box-shadow: inset 0 0 0 1px #f0c85a;
}

/* ---------------------------------------------------------------------------
 * 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;
}

/* A stored image as a tile in a TABLE ROW — Education → list. See
 * `components/list-thumbnail.ts`.
 *
 * 🔴 The fill IS the empty state. A blank column renders no `img` and an image
 * that fails to load hides its own, so both show this tile rather than a
 * broken-image glyph — and an operator scanning the list can see at a glance
 * which published pieces reach the hub with no card picture.
 *
 * `flex: none` and the fixed box are what stop the column from reflowing row
 * to row as images arrive: the space is reserved before anything loads. */
.vrto-thumb {
  display: block;
  flex: none;
  overflow: hidden;
  width: 40px;
  height: 40px;
  border-radius: 4px;
  background: var(--vrto-line);
}
/* `width`/`height` are set on the element as well, for the reserved box before
 * the bytes arrive; these two are what crop a cover that is not square.
 *
 * `max-width` is explicit because `console.css` is loaded after the design
 * system, which sets `img { max-width: 100% }` globally — the same note
 * `.vrto-show__cover` above carries. */
.vrto-thumb__img {
  display: block;
  width: 100%;
  max-width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Education → list / show: the register id, or the work when it is missing.
 * See `components/library-id.ts`.
 *
 * 🔴 A set id is just the id — no pill. Twenty mapped rows in a column of
 * badges would make the empty ones disappear. The yellow fill is the same
 * token the Documents page uses for "someone still has to look".
 *
 * Tokens fall back: this cell lives in the table, which is not under a
 * `.vrto-show` / `.vrto-listhead` root, so the custom-property block above
 * does not apply. */
.vrto-libid {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: -0.2px;
  color: var(--vrto-ink, #191a1d);
  white-space: nowrap;
}
.vrto-libid--needs {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 24px;
  background: var(--vrto-yellow, #ffea70);
  color: var(--vrto-ink, #191a1d);
}

/* The media band on Exercises → show — the cover and the Bunny player, side by
 * side.
 *
 * 🔴 It is NOT the scalar `.vrto-show__grid`, and that is the whole point.
 * That grid is `repeat(auto-fit, minmax(260px, 1fr))`, which opens a second
 * track at ~552px of content width and a fourth at ~1104px. A 480px cap on a
 * grid ITEM can therefore only bind between 480px and 552px of content width —
 * everywhere else the track is already narrower than the cap, so the cap does
 * nothing and the player paints at a quarter of the band (~271px wide, ~152px
 * tall on a 1180px content area) with two empty tracks beside it. That was the
 * first version of this block, and its comment claimed the opposite.
 *
 * `max-content` tracks let the two items take the width their height caps ask
 * for, and `align-items: start` stops the shorter one stretching. The row gap
 * is 16px rather than the scalar grid's 4px because these wrap into a stack on
 * a narrow window, and two bordered boxes 4px apart read as one. */
.vrto-show .vrto-show__grid--media {
  grid-template-columns: repeat(auto-fit, minmax(320px, max-content));
  gap: 16px 32px;
  align-items: start;
}

/* The Bunny iframe.
 *
 * 🔴 Capped by HEIGHT, matching `.vrto-show__cover` above, and for the reason
 * that rule gives: the two sit in one row and a cap on a different axis leaves
 * them aligned on neither edge. 200px at 16/9 is a 356px frame beside a 200px
 * cover.
 *
 * 🔴 `box-sizing` is explicit. This stylesheet has NO global one — see the note
 * on `.vrto-builder input` below — so under `content-box` the 1px border paints
 * outside the box and `aspect-ratio` sizes the content rather than the frame. */
.vrto-show .vrto-show__video {
  display: block;
  box-sizing: border-box;
  height: 200px;
  width: auto;
  max-width: 100%;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  /* The player letterboxes its own poster, so the frame behind it is dark
   * rather than the cover's `#f6f6f8` tint. */
  background: #191a1d;
}

/* The synced title, as a caption under the frame. Sized and coloured like
 * `__desc` rather than `__value`: it names the Bunny row the player is loading,
 * and at 14px ink it read as one more field in a band of fields. */
.vrto-show .vrto-show__video-title {
  margin: 6px 0 0;
  max-width: 356px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--vrto-muted);
}

/* The repair link in an empty media property. `.vrto-show__empty` is
 * `margin: 0` and `__link` styles only colour and underline, so without this
 * the anchor sits flush on the sentence above it. */
.vrto-show .vrto-show__empty-action {
  display: inline-block;
  margin-top: 8px;
  font-size: 13px;
}

/* ---------------------------------------------------------------------------
 * Program builder — `vrto-builder`
 *
 * One page, three views. Tokens only — no new colours. `.vrto-builder` is a
 * token root (see the list at the top of this file). Do not qualify button
 * rules as `.vrto-builder__form button` — that beats `__btn--primary` and
 * paints Create draft white-on-white.
 *
 * ⚠️ This banner used to be a light divider nested among the badge rules
 * above, where every other block in this file opens with the dashed form.
 * Cosmetic, and it is how a 700-line block goes missing.
 * ------------------------------------------------------------------------- */
.vrto-builder {
  max-width: 1200px;
  margin: 0 auto;
  padding: 28px 24px 64px;
  color: var(--vrto-ink);
}
.vrto-builder--wide {
  max-width: 1320px;
}
.vrto-builder--busy {
  cursor: progress;
}
/* 🔴 `console.css` has NO global `box-sizing` — the one occurrence, line 159,
   is scoped to a sidebar nav button. So every `width: 100%` control here is
   `content-box` and paints 18px wider than its box (16px padding + 2px
   border). Measured 2026-09-07, before this rule: the entry grid's `Sets`
   track was declared 48px and painted 66px, so all four numeric fields
   overlapped 10px into the next track against an 8px gap; the open phase
   editor's six controls painted 20px past the phase cell, over the Monday
   gridline; and `__banner` crossed its card's right border by 19px. One
   declaration, four symptoms. Scoped to the builder rather than `*` so the
   eight other roots this stylesheet serves are untouched. */
.vrto-builder input,
.vrto-builder select,
.vrto-builder textarea,
.vrto-builder button,
.vrto-builder__banner {
  box-sizing: border-box;
}
.vrto-builder h2 {
  margin: 0 0 8px;
  font-size: 16px;
}
.vrto-builder__lede,
.vrto-builder__muted {
  color: var(--vrto-muted);
  font-size: 13px;
  line-height: 1.55;
}
/* 🔴 Colour and size only — `__muted` carries NO spacing, and the rule above
   used to. It is on inline spans inside flex and grid rows (the picker's
   `Add`, the outline's day counts, the variant meta), where a bottom margin
   makes it a block, so a bottom margin applies: measured 2026-09-07 it made every search-result row 52px tall
   for 36px of content, and it is what pushed the outline header's lifecycle
   pill onto a line of its own with a 17px gap above it. Spacing belongs to
   whatever contains the text — the two places that relied on it say so
   below. */
.vrto-builder__muted {
  margin: 0;
}
.vrto-builder__lede {
  margin: 0 0 16px;
}
/* 🔴 This carried `margin-bottom: 12px` to clear the `Add draft variant`
   button that used to sit under it. The button is in `__program-head` now and
   nothing follows this paragraph but `__form`, which brings its own 12px
   top — so the margin was pure trailing space inside the card. */
.vrto-builder__program > .vrto-builder__muted {
  margin-bottom: 0;
}
.vrto-builder__picker > .vrto-builder__muted {
  margin-top: 12px;
}
/* The picker's status line is always in the DOM — a live region has to exist
   before the text it announces — so an empty one must not leave a gap. */
.vrto-builder__picker > .vrto-builder__muted:empty {
  margin-top: 0;
}
.vrto-builder__crumb {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin: 0 0 16px;
  font-size: 13px;
}
.vrto-builder__crumb-link {
  border: 0;
  padding: 0;
  background: none;
  color: var(--vrto-deep);
  text-decoration: underline;
  cursor: pointer;
  font: inherit;
}
.vrto-builder__crumb-sep {
  color: var(--vrto-muted);
}
.vrto-builder__crumb-here {
  font-weight: 600;
}
/* `__error`, `__warn`, `__notice` and `__ok` were four rules here and they are
   gone: all four messages now use `__banner` with a tone modifier, which is
   what stopped a notice rendering in the same grey as a caption. Wave A of the
   styling plan deleted nine class names nobody had written a rule for; this is
   the opposite housekeeping, and `program-builder.test.ts` only checks one
   direction, so it has to be done by hand. */
.vrto-builder__empty {
  padding: 24px;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}
.vrto-builder__empty-title {
  margin: 0 0 6px;
  font-weight: 600;
}
.vrto-builder__empty-body {
  margin: 0;
  color: var(--vrto-muted);
  font-size: 13px;
}
.vrto-builder__skel {
  padding: 32px 16px;
  border: 1px dashed var(--vrto-line);
  border-radius: 12px;
  color: var(--vrto-muted);
  font-size: 13px;
}
.vrto-builder__catalogue,
.vrto-builder__outline,
.vrto-builder__day-detail {
  margin: 0;
}
.vrto-builder__panel,
.vrto-builder__program {
  margin: 0 0 16px;
  padding: 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}
.vrto-builder__programs,
.vrto-builder__entries,
.vrto-builder__matches {
  margin: 0;
  padding: 0;
  list-style: none;
}
/* The name on the left, `Add draft variant` hard against the right edge of the
   card. `space-between` rather than a `margin-left: auto` on the button so an
   empty-variant programme and a twelve-variant one put the control in the same
   place, and `flex-wrap` so a long programme name drops the button to its own
   line instead of squeezing it. */
.vrto-builder__program-head {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 12px;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}
.vrto-builder__program-head > strong {
  min-width: 0;
  overflow-wrap: anywhere;
}
/* The header is the one place a builder button sits on a row with text rather
   than in a row of its own controls, so it takes the compact size — otherwise
   it out-weighs the programme name it is beside. */
.vrto-builder__program-head-action {
  flex: 0 0 auto;
  padding: 6px 12px;
  font-size: 13px;
}
/* 🔴 Three FIXED tracks. The middle one was `auto`, so it sized to the pill's
   own text — measured 2026-09-07, `live` 35px / `draft` 43px / `retired` 53px,
   which walked the pill 9px and the meta 9px across three rows and defeated
   the whole point of a grid. `260px` holds the widest meta this page can
   produce ("4 days · gym · Intermediate" measures 165px at 13px) and is
   right-aligned by `__variant-meta`, so the column has a fixed edge to read
   down. `88px` is the widest pill plus room. */
.vrto-builder__variant {
  display: grid;
  /* `170px` for the version date — `3 Dec 2025 – 17 Jan 2026` is the widest
     label the lineage writes without a same-day collision suffix. */
  grid-template-columns: minmax(140px, 1fr) 88px 170px 260px;
  gap: 10px 16px;
  align-items: center;
  width: 100%;
  padding: 10px 8px;
  border: 0;
  border-bottom: 1px solid var(--vrto-line);
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.vrto-builder__variant:hover {
  background: #f6f6f8;
}
.vrto-builder__variant-meta {
  text-align: right;
}
/* The version date. It is what tells two rows of one name apart, so it wraps
   rather than truncating — an ellipsis would cut the `(v2)` a same-day
   collision appends, which is the part that disambiguates. */
.vrto-builder__variant-when {
  min-width: 0;
  font-size: 13px;
}
/* A version filed under the one it supersedes: one step, then two at most. */
.vrto-builder__variant-item--version {
  padding-left: 24px;
}
.vrto-builder__variant-item--deep {
  padding-left: 48px;
}
/* The consequence line under a row. The row gives up its bottom border to it,
   or the line would read as belonging to the row BELOW. */
.vrto-builder__variant:has(+ .vrto-builder__version-note) {
  border-bottom: 0;
}
.vrto-builder__version-note {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px;
  margin: 0;
  padding: 0 8px 10px;
  border-bottom: 1px solid var(--vrto-line);
  font-size: 13px;
}
/* Its own rule rather than a second declaration on the shared list reset.
   🔴 It read `margin: 0 0 12px`, to stop `Add draft variant` reading as one
   more variant row — that button is in `__program-head` now, so the gap has
   nothing to separate and only `__form` can follow this list, with its own
   12px top. Every row keeps its bottom border. */
.vrto-builder__variants {
  margin: 0;
  padding: 0;
  list-style: none;
}
.vrto-builder__state {
  display: inline-block;
  justify-self: start;
  font-size: 11px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  line-height: 1.4;
}
.vrto-builder__state--live {
  background: var(--vrto-green);
  color: var(--vrto-deep);
}
.vrto-builder__state--draft {
  background: var(--vrto-yellow);
  color: var(--vrto-deep);
}
.vrto-builder__state--retired {
  background: #f3f4f7;
  color: var(--vrto-muted);
}
/* 🔴 `background-color`, NOT `background`. The shorthand resets
   `background-image`, and this selector is element-qualified — 0-1-1 against
   `.vrto-select`'s 0-1-0 — so it won, and every dropdown on the page lost the
   chevron `.vrto-select` had just given it. Same shape as defect ② in
   `program-builder-design-history.md` Part 1: an element-qualified rule in this block
   silently beating a class it was supposed to cooperate with. */
.vrto-builder input,
.vrto-builder select {
  font: inherit;
  font-size: 13px;
  padding: 6px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  background-color: #fff;
  color: var(--vrto-ink);
}
/* Size only. The box, the chevron, the hover and the disabled state all belong
   to `.vrto-select` further up — the same division as
   `.vrto-operators__add .vrto-select`, and for the same reason: the second page
   to want a dropdown should not have to draw one again. What is here is the
   two numbers that make a select the same height as the input beside it.
   Measured before this: bare native selects at 32px against 30px inputs, which
   `align-items: end` then turned into labels on two baselines. */
.vrto-builder .vrto-select {
  padding: 6px 26px 6px 8px;
  border-radius: 6px;
  background-position: right 8px center;
  font-size: 13px;
}
.vrto-builder__entry-row .vrto-select {
  width: 100%;
}

/* 🔴 The builder was the only root in this stylesheet with no `:disabled`
   rule — `.vrto-select`, `select.vrto-docs__cstatus` and
   `.vrto-match__field-input` all have one. Measured 2026-09-07 on a live
   variant's day, where every control is disabled: inputs came back `#fff` /
   `#191a1d` / opacity 1, and the selects beside them came back at opacity 0.7
   from the UA — so read-only looked editable, and looked editable
   INCONSISTENTLY within one row. The explicit `opacity: 1` is what takes the
   UA's version off the selects. */
.vrto-builder input:disabled,
.vrto-builder select:disabled,
.vrto-builder textarea:disabled {
  /* `background-color`, for the reason the rule above gives: the shorthand
     resets `background-image` and takes `.vrto-select`'s chevron off every
     disabled dropdown. `.vrto-select:disabled` keeps its chevron, so these
     do too. */
  background-color: #f6f6f8;
  color: var(--vrto-muted);
  opacity: 1;
  cursor: default;
}
.vrto-builder input[type='number'] {
  font-variant-numeric: tabular-nums;
}
.vrto-builder__btn {
  padding: 6px 12px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
  color: var(--vrto-ink);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.vrto-builder__btn:disabled {
  background: #f3f4f7;
  color: var(--vrto-muted);
  border-color: var(--vrto-line);
  cursor: not-allowed;
}
.vrto-builder__btn--primary {
  background: var(--vrto-ink);
  color: #fff;
  border-color: var(--vrto-ink);
}
.vrto-builder__btn--primary:disabled {
  background: #f3f4f7;
  color: var(--vrto-muted);
  border-color: var(--vrto-line);
}
.vrto-builder__btn--danger {
  color: var(--vrto-danger);
  border-color: var(--vrto-danger);
}
.vrto-builder__btn--quiet {
  padding: 2px 6px;
  border-color: transparent;
  background: none;
  color: var(--vrto-muted);
}
/* Destructive, but not the loudest thing in a 180px cell. `--danger` keeps its
   border for the day header, where `Delete day` sits on a card beside a Save
   and needs to be unmistakable. */
.vrto-builder__btn--quiet.vrto-builder__btn--danger {
  border-color: transparent;
  color: var(--vrto-danger);
}
.vrto-builder__btn--half {
  min-width: 0;
}
.vrto-builder__form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 12px;
  align-items: end;
  margin: 12px 0 0;
}
.vrto-builder__form--inline {
  margin-top: 12px;
}
/* A 180px table cell, so the editor cannot be a row — but it does not have to
   be six full-width bars either, which is what `flex-direction: column` with
   `align-items: stretch` produced: an unlabelled name box, two anonymous
   number boxes reading `1` and `4`, and a 183px red `Delete phase` bar that
   was the loudest element on the page. Two tracks, the week pair side by side,
   Save and Cancel side by side, and the destructive action last and quiet. */
.vrto-builder__form--stack {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  align-items: end;
  margin: 8px 0 0;
}
.vrto-builder__form--stack > * {
  grid-column: 1 / -1;
}
.vrto-builder__form--stack > .vrto-builder__field--half,
.vrto-builder__form--stack > .vrto-builder__btn--half {
  grid-column: auto;
}
/* Content width at the left of their track, not stretched across it — a
   full-width `Delete phase` centres its own label and reads as a bar. */
.vrto-builder__form--stack > .vrto-builder__btn {
  justify-self: start;
}
/* Which phase the six controls belong to. The first field holds the name, and
   the name is what is being edited — so the cell had no fixed statement of
   what was open. */
.vrto-builder__phase-editing {
  color: var(--vrto-muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
/* Save and Cancel are a pair on one row, so they fill their tracks rather
   than sitting content-width at the left — `justify-self: start` above is for
   the single destructive button on a row of its own. */
.vrto-builder__form--stack > .vrto-builder__btn--half {
  justify-self: stretch;
}
.vrto-builder__field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
/* 🔴 The reserve is GONE, and so is the reason for it. This rule read
   `min-height: 52px` to hold the `Auto` badge's 18px on every entry-row field,
   badge or not, so an inferred row was not taller than a plain one — 30px
   control + 4px gap + 18px badge, reserved in all of them for the few that
   used it. The badge is a 16px mark BESIDE its control now, so it costs no
   height at all and the rows are level by construction rather than by
   reservation. Every entry-row field is a row, not a column. */
.vrto-builder__entry-row .vrto-builder__field,
.vrto-builder__entry-head .vrto-builder__field {
  flex-direction: row;
  align-items: center;
  min-height: 0;
}
.vrto-builder__entry-row .vrto-builder__field > .vrto-select {
  flex: 1 1 auto;
  min-width: 0;
}
.vrto-builder__field--half {
  min-width: 0;
}
.vrto-builder__label {
  font-size: 11px;
  font-weight: 600;
  color: var(--vrto-muted);
}
.vrto-builder__outline-head {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-start;
  justify-content: space-between;
  margin: 0 0 16px;
  padding: 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}
.vrto-builder__outline-head h2 {
  margin: 0;
}
/* 🔴 One gap, declared once. This stack used to get its rhythm from whatever
   margins its children happened to carry — measured 2026-09-07 the header read
   6px / 17px / 13px between four lines, and the 17px was `__muted`'s bottom
   margin pushing the lifecycle pill onto a line of its own. Wave B5 of the
   styling plan asked for the pill on the title row; this is where that
   happens. */
.vrto-builder__outline-copy {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
  flex: 1 1 280px;
}
.vrto-builder__outline-title {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.vrto-builder__outline-actions {
  display: flex;
  gap: 8px;
  align-items: center;
}
/* The chassis for every message on the page — the read-only notice, the
   publish blocker, an error and a notice. Before this, three of those four
   were bare coloured text at 13px with UA paragraph margins, and `notice` was
   `--vrto-muted`, so "Draft variant created." rendered in the same grey as
   every caption on the page. */
.vrto-builder__banner {
  margin: 0;
  padding: 8px 10px;
  border-radius: 8px;
  background: #f6f6f8;
  color: var(--vrto-muted);
  font-size: 13px;
}
/* 🔴 `flex: 1 0 100%` belongs to the read-only notice ONLY, which is a direct
   child of the header card's ROW flex and has to wrap to a line of its own.
   It was on the base class, and `__outline-copy` is a COLUMN flex — where
   `flex-basis` is read against the main axis, so `100%` meant 100% of the
   card's height. The blocker came out 1200px wide inside a 1288px content box
   for that reason. */
.vrto-builder__outline-head > .vrto-builder__banner {
  flex: 1 0 100%;
}
/* Tints of the two tokens rather than new hues — `--vrto-danger` and
   `--vrto-green` lightened over white, the same way the lifecycle pills above
   get their fills. Ink stays the full-strength token, so the ratio is the
   token's own against a near-white ground. */
.vrto-builder__banner--danger {
  background: #fbeeeb;
  color: var(--vrto-danger);
  font-weight: 600;
}
.vrto-builder__banner--ok {
  background: #eafaf3;
  color: var(--vrto-deep);
}
/* Loose in the page flow, above the first panel. */
.vrto-builder__banner--page {
  margin: 0 0 16px;
}
/* 🔴 The grid was the one surface on this page with square corners, and it is
   not a table that happens to sit in a card — it IS the card. `__panel`,
   `__empty`, `__picker` and `__outline-head` are all 12px on a 1px
   `--vrto-line`; the grid drew its own 1px box out of collapsed cell borders,
   which no radius can round.

   ⚠️ The radius has to live HERE and not on the table. `border-radius` does
   nothing to a `border-collapse: collapse` table — the outer edge is the cell
   borders, not a box — and a radius on a `separate` table clips nothing,
   because a table is not a scroll container. The wrapper is both: it carries
   the border and the radius, and `overflow-x: auto` makes it clip, which is
   what rounds the corners of the cells underneath. That is the same shape as
   `__matches` (border + radius + `overflow: hidden`) and the reason the day
   cards inside already look right against it. */
.vrto-builder__grid-wrap {
  overflow-x: auto;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}
/* `separate` with zero spacing so the wrapper's radius has cells to clip
   rather than a second border to fight. Every gridline below is drawn once,
   on the right and bottom of each cell, and the wrapper draws the outside —
   so the doubled 2px edge `collapse` was hiding does not come back. */
.vrto-builder__grid {
  width: 100%;
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 13px;
}
.vrto-builder__col-phase {
  width: 200px;
}
.vrto-builder__col-day {
  width: auto;
}
/* Left, to match the cells. The UA centres a `th`, so every one of the eight
   column headings floated over left-aligned content — `Phase` above
   `Foundation`, `Mon` above `Lower body`. */
.vrto-builder__grid th {
  text-align: left;
}
.vrto-builder__grid th,
.vrto-builder__grid td,
.vrto-builder__cell {
  border: 0;
  border-right: 1px solid var(--vrto-line);
  border-bottom: 1px solid var(--vrto-line);
  padding: 8px;
  vertical-align: top;
  background: #fff;
}
/* The wrapper owns the outside edge. Without these the right column and the
   last phase row paint a line 1px inside the rounded border, which reads as a
   seam at every corner. */
.vrto-builder__grid tr > :last-child {
  border-right: 0;
}
.vrto-builder__grid tbody tr:last-child > * {
  border-bottom: 0;
}
/* The column headings are a label row, not content — a wash and a heavier
   rule under them is what separates `Mon` from the first day card, which at
   13px regular on white it did not. `--vrto-wash` is the token the muted
   columns already use. */
.vrto-builder__grid thead th {
  padding: 10px 8px;
  background: var(--vrto-wash, #fafafa);
  border-bottom-color: #dfe1e6;
  color: var(--vrto-muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
/* The header's own corners. The wrapper clips them, but only while the table
   is exactly as wide as it — a scrolled grid is wider, and then the wash
   square-cuts against the rounded border. */
.vrto-builder__grid thead th:first-child {
  border-top-left-radius: 11px;
}
.vrto-builder__grid thead th:last-child {
  border-top-right-radius: 11px;
}
/* A weekday past what the variant declares in `days`. Presentation only — the
   column is fully authorable, and `daysPerWeekBlocker` is what actually refuses
   the shape at publish. Before this, the grid simply did not draw the column,
   which made a weekend unreachable on any variant that had never had one. */
.vrto-builder__col-extra,
.vrto-builder__cell--extra {
  background: var(--vrto-wash, #fafafa);
}
.vrto-builder__col-extra {
  opacity: 0.65;
}
/* The day card and its drag handle. A cell may hold more than one card: the
   unique key is `(phase, week_number, day_of_week)`, so a weekday can carry one
   day per week number.

   The SLOT is the card — border, radius and hover wash — so the grip and the
   name read as one object. The wash sat on the button alone before, which lit
   the name and left the grip outside it, and with no edge the card had no
   boundary against the cell. */
.vrto-builder__day-slot {
  display: flex;
  align-items: stretch;
  gap: 2px;
  cursor: grab;
  margin-bottom: 6px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
  transition:
    border-color 120ms ease,
    background-color 120ms ease;
}
.vrto-builder__day-slot:hover {
  border-color: #c9cbd2;
  background: #f6f6f8;
}
.vrto-builder__day-slot:last-child {
  margin-bottom: 0;
}
.vrto-builder__day-slot:active {
  cursor: grabbing;
}
/* Matches `__entry-row--dragging`, for the same reason and at the same value. */
.vrto-builder__day-slot--dragging {
  opacity: 0.5;
}
/* The grip, and it is the only thing on the card that says it moves. `cursor:
   grab` alone is a hover-only sign, which is no sign at all to someone who has
   not hovered — `__entry-grip` carries the same mark for the same reason. It
   is drawn beside the day name rather than under it because a cell is 204px
   and the name is what has to stay readable. */
.vrto-builder__day-grip {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  padding: 0 2px 0 6px;
  /* 🔴 Was `var(--vrto-line)` — the table border colour, 1.2:1 on white, so
     the one sign that the card moves was close to invisible. */
  color: var(--vrto-muted);
  font-size: 18px;
  line-height: 1;
  user-select: none;
}
.vrto-builder__day-slot:hover .vrto-builder__day-grip {
  color: var(--vrto-ink);
}
/* The phase column stays put when the seven weekday columns scroll. The BODY
   cells did and the heading did not, so a scrolled grid showed `Foundation`
   under `Tue`. Both are sticky now, and the header keeps the higher stacking
   order because it is the one that has to cross the body cells. */
.vrto-builder__phase {
  position: sticky;
  left: 0;
  z-index: 1;
  text-align: left;
  background: #fff;
}
.vrto-builder__grid thead th:first-child {
  position: sticky;
  left: 0;
  z-index: 2;
}
/* The phase cell, top to bottom: name, the week range as a chip, what that
   range comes to in weeks and days, and the way in. It was three anonymous
   lines — bold name, caption grey, a transparent `Edit` — with the week range
   reading in the same grey as the day counts in the cells beside it. */
.vrto-builder__phase-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  padding: 2px 0;
}
.vrto-builder__phase-name {
  font-size: 13px;
  line-height: 1.35;
}
/* A chip, because the weeks are the phase's identity and the grid's only
   statement of WHERE in the programme a row sits. Tinted from `--vrto-deep`
   the way the lifecycle pills are, so it reads as a value and not a caption. */
.vrto-builder__phase-weeks {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  background: #eef1f6;
  color: var(--vrto-deep);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.vrto-builder__phase-meta {
  color: var(--vrto-muted);
  font-size: 11px;
  line-height: 1.4;
}
/* 🔴 `Edit` used `__btn--quiet` — transparent border, transparent background,
   `--vrto-muted` — so the only route to renaming a phase, re-placing it in the
   programme, duplicating it or deleting it was indistinguishable from the
   caption above it. It is a real button now, still quiet enough not to
   compete with the day cards, and it grows a border on hover so the target is
   visible before the click rather than after it. */
.vrto-builder__phase-edit {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 2px;
  padding: 3px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  background: #fff;
  color: var(--vrto-muted);
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.4;
  cursor: pointer;
  transition:
    border-color 120ms ease,
    background-color 120ms ease,
    color 120ms ease;
}
.vrto-builder__phase-edit:hover:not(:disabled) {
  border-color: #c9cbd2;
  background: #f6f6f8;
  color: var(--vrto-ink);
}
.vrto-builder__phase-edit:disabled {
  color: var(--vrto-line);
  cursor: not-allowed;
}
.vrto-builder__phase-edit-icon {
  font-size: 12px;
  line-height: 1;
}
/* 🔴 The day header is a STACK, not the outline header's two columns. It read
   `flex: 3 1 480px; justify-content: flex-end` on the form, which put the day's
   title and the boxes that edit that same title into one row and let them
   compete for it — at 1200px the name box was 180px, the note 204px, and both
   were unlabelled. The card keeps `__outline-head`'s chassis (border, radius,
   padding) and overrides only the axis. */
.vrto-builder__day-head {
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
}
/* 🔴 Every flex value `__outline-head`'s children carry is a ROW-axis value,
   and this header is a COLUMN — where `flex-basis` is read against the HEIGHT.
   Both of them bite: `__outline-copy` is `flex: 1 1 280px`, which opened a
   280px void between the title and the form (measured here, 2026-09-15, before
   this rule existed), and `__banner`'s `flex: 1 0 100%` — the wrap-to-its-own-
   line rule for the read-only notice — would stretch it to the card's full
   height. Both are already on their own line here by construction, so neither
   needs a basis. This is the same trap the note on `__outline-head >
   __banner` records, reached from the other direction. */
.vrto-builder__day-head > * {
  flex: 0 0 auto;
}
/* The form is the second row, under a hairline, so the rule is what says
   "below here you are editing" rather than the boxes having to say it. */
.vrto-builder__day-head .vrto-builder__day-meta {
  margin: 0;
  padding-top: 12px;
  border-top: 1px solid var(--vrto-line);
  align-items: end;
}
/* A day-level coaching note in a 204px box: measured 2026-09-07, `clientWidth`
   202 against `scrollWidth` 242, so "Warm up on the bike for five minutes."
   was cut mid-word with no way to see the rest. It is the longest field in this
   form and it was the shortest. It takes the row's slack now — the name is a
   short label and the actions are content-width.

   ⚠️ `:has()` is the only use of it in this file. The control it needs is the
   LABEL that wraps the input — `Field` owns that element and takes no class of
   its own — and the fallback if a browser ever lacked it is both fields
   sharing the row evenly, which is the shape that shipped before this. */
.vrto-builder__day-meta .vrto-builder__field:has(input[name='notes']) {
  flex: 1 1 320px;
  min-width: 220px;
}
.vrto-builder__day-meta .vrto-builder__field:has(input[name='workout']) {
  flex: 0 1 220px;
  min-width: 160px;
}
.vrto-builder__day-meta .vrto-builder__field > input {
  width: 100%;
}
/* Both buttons at the end of the row, on the control baseline. The gap is what
   separates Save from Delete — they were adjacent and the same weight, so the
   destructive one read as an equal choice. */
.vrto-builder__day-meta-actions {
  display: flex;
  gap: 12px;
  align-items: center;
  margin-left: auto;
}
/* Which weekday, as a mark on the title line rather than a word in a
   `phase · monday` caption where it was lower case and last. */
.vrto-builder__day-pill {
  padding: 2px 8px;
  border-radius: 999px;
  background: #f0f1f4;
  color: var(--vrto-muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.vrto-builder__day,
.vrto-builder__add-day {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
  min-height: 56px;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* The card sits beside the grip now, so it takes the rest of the row rather
   than the whole of it. `min-width: 0` is what lets a long day name wrap
   instead of pushing the grip out of the cell. */
.vrto-builder__day {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  padding: 8px 10px 8px 4px;
  border-radius: 0;
}
.vrto-builder__add-day {
  align-items: center;
  justify-content: center;
  color: var(--vrto-muted);
  font-size: 18px;
  line-height: 1;
}
/* `__day` is not here: the slot carries its hover wash, and the button
   painting its own on top drew a second, inset rectangle inside the card. */
.vrto-builder__add-day:hover {
  background: #f6f6f8;
}
/* 🔴 This was `var(--vrto-muted)` at 12px, and `__muted` is the same
   `#5c5f66` at 13px — so a class named `--warn` rendered one pixel of type
   size away from the neutral beside it, and the single day blocking
   publication looked exactly like the three days that were fine. */
.vrto-builder__day-warn {
  color: var(--vrto-danger);
  font-size: 12px;
  font-weight: 600;
}
/* 🔴 Add a phase was four loose controls hanging under the grid with no card,
   no heading and no statement of what it does — every other authoring surface
   on this page (`__panel`, `__picker`, `__outline-head`, `__empty`) is a
   bordered 12px card and this one, the action a coach reaches for FIRST on an
   empty variant, was not. It reads as a footer of the grid rather than as a
   thing you do.

   The dashed edge is the one departure from `__panel`: this card is a
   placeholder for a row that does not exist yet, and it sits directly under a
   table of solid ones. */
.vrto-builder__add-phase {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
  align-items: flex-end;
  justify-content: space-between;
  margin: 16px 0 0;
  padding: 16px;
  border: 1px dashed #d4d6dc;
  border-radius: 12px;
  background: var(--vrto-wash, #fafafa);
}
.vrto-builder__add-phase-copy {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  flex: 1 1 260px;
}
/* `.vrto-builder h2` is declared and `h3` is not — the same reason
   `__dup-title` says its own size. */
.vrto-builder__add-phase-title {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
}
/* `__form`'s own `margin: 12px 0 0` is for a form that follows copy inside a
   panel; here the card's gap already spaces it, and the form is the right-hand
   column rather than a block under the text. */
.vrto-builder__add-phase-form {
  flex: 0 1 auto;
  margin: 0;
  justify-content: flex-end;
}
.vrto-builder__add-phase-form .vrto-builder__field {
  flex: 0 1 auto;
}
.vrto-builder__add-phase-form input[name='name'] {
  width: 200px;
}
/* A week number is one or two digits. At the shared control width the two of
   them were as wide as the phase name beside them, which said the wrong thing
   about what they hold. */
.vrto-builder__week-input {
  width: 84px;
}
.vrto-builder__picker {
  margin: 0 0 16px;
  padding: 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}
/* 🔴 The picker drawn INSIDE a series section, which is what `+ Add exercise`
   opens. It lives in `__entry-scroll`, a horizontal scroller whose rows are
   1176px wide, so it is pinned to the left edge the way `__series-head` is —
   a coach scrolled right still sees the box they are typing in. The width is
   capped rather than `100%`, which in that container would resolve to the
   grid's width and draw a 1176px search box. */
.vrto-builder__picker--inline {
  position: sticky;
  left: 0;
  width: min(100%, 760px);
  margin: 12px;
  background: #fbfbfd;
}
.vrto-builder__picker .vrto-builder__form input[name='exerciseSearch'] {
  flex: 1 1 280px;
  min-width: 220px;
}
.vrto-builder__matches {
  margin-top: 12px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  overflow: hidden;
}
/* The row holds the tile and the match button as SIBLINGS — the ▶ is a button
   and cannot nest inside one. The rule and the hover moved up to the item
   with it: `__match:last-child` matched every button, each being the only
   child of its `li`, so the rule between results never drew. */
/* 🔴 The row had NO vertical padding, so the 40px tile was 40 of its 41px and
   sat flush against both dividers — a stack of photographs with a name beside
   them rather than a list of names with a picture to tell two apart. The tile
   is inset now: the row keeps its 41px, and the picture gives up the 8px that
   makes it read as a thumbnail. */
.vrto-builder__match-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 3px 0 3px 12px;
  border-bottom: 1px solid var(--vrto-line);
  background: #fff;
}
.vrto-builder__match-item:last-child {
  border-bottom: 0;
}
.vrto-builder__match-item:hover {
  background: #f6f6f8;
}
.vrto-builder__match {
  display: flex;
  flex: 1 1 auto;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  min-width: 0;
  /* 7px, not 8: the row's own 3px puts the total back at the 41px the list
     has always been, with the tile no longer touching the rules. */
  padding: 7px 12px 7px 0;
  border: 0;
  background: transparent;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* It is INSIDE `__entries` and had no padding, so it sat flush to the panel
   edge and read as a loose strip above a card whose top edge you could not
   find. */
.vrto-builder__toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin: 0;
  padding: 10px 12px;
  border-bottom: 1px solid var(--vrto-line);
}
.vrto-builder__pick-count {
  margin-right: auto;
  color: var(--vrto-muted);
  font-size: 13px;
}
.vrto-builder__entries {
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
  overflow: hidden;
}
/* The grid's own scroller, so the toolbar above it stays put. `overflow:
   hidden` on the panel is for the 12px radius, and it CLIPPED: measured at
   1024px the tracks crushed instead — `ramp ascending` truncated mid-word,
   `Romanian Deadlift` wrapped to two lines and row 2's actions wrapped onto a
   second line while its neighbours' did not. The outline grid has had
   `__grid-wrap` for exactly this reason; this is its counterpart. */
.vrto-builder__entry-scroll {
  overflow-x: auto;
}
.vrto-builder__entry {
  padding: 0;
}
/* 🔴 The last track was `auto`, and that is the whole reason this grid did not
   line up. The actions cell holds `Save Remove Down` on the first row,
   `Save Remove Up Down` in the middle and `Save Remove Up` on the last — and
   NOTHING in the header. `auto` takes what it needs before the `fr` tracks
   divide the remainder, so the four rows resolved four different sets of
   tracks: measured 2026-09-07, the Exercise track came out 357 / 278 / 264 /
   285 and every heading from `Sets` rightward sat 71–208px away from the field
   it names. A fixed track, plus `EntryRow` rendering all four action slots
   whether or not they are usable, makes the `fr` distribution identical for
   every row and the header by construction. */
/* 🔴 TWELVE tracks. The list is the only place the row's geometry is stated,
   and four other numbers are DERIVED from it — the sticky offsets below, the
   two sub-lines' left padding, and this rule's own `min-width`. Change a track
   and all four move; `program-builder.test.ts` pins the arithmetic so they
   cannot be changed one at a time.

     12  padding
     28  grip          left 12
      8  gap
     28  select        left 48
      8  gap
     36  series        left 84
      8  gap
    194  exercise      left 128   ← minmax, the last of the sticky band;
                                   36 of it is the cover tile, 8 its gap
    ...  sets 44 · reps 212 · rest 56 · tempo 72 · weight 68
    ...  series type 144 · modifier 118 · actions 64

   min-width = 608 fixed + 88 gaps + 24 padding + 456 flexible minima = 1176.

   🔴 `tempo` is 72 and not 56, and the 16px is not spare. Measured in Chrome
   on the preview harness: at 56 a four-digit code CLIPPED — `clientWidth` 54
   against `scrollWidth` 63 — so `2020` rendered as `202` on every row, which
   is a different tempo rather than an ugly one. The number is four digits by
   definition and the track has to hold four digits.

   ⚠️ What moved, and why, since the eleven-track version:
   - `Reps` 96 → 148. It holds the MEASUREMENT now — a `repType` select stacked
     over the value — which is the field the whole grid existed to express and
     the one thing it could not say.
   - `Weight` is new at 64. It is a real column `prefillWeightOf` reads and it
     had no control anywhere in this console.
   - `Reps` 148 → 212. Every per-set control lives in it now — one tile per
     set, its rep target over its class chip — and 212 is four 48px tiles and
     their gaps, so the common 3–4 set row fits on one line.
   - `Classification` is `Series type` again at 144. The per-set chips moved
     into the Reps tiles, beside the rep target of the set they classify.
   - `actions` 142 → 64, by making Remove an icon and stacking the arrows. That
     is where most of the room for the two new columns came from.
   - `series` 52 → 36 and `sets` 52 → 44: both hold at most two characters.

   🔴 TWELVE tracks became ELEVEN at the coach's review. Recount both the
   header and the row in `program-builder-day.ts` against this list before
   changing it — the header is the only thing that can drift out of the grid,
   and `__sticky--3`'s offset below is `12 + 28 + 8 + 28 + 8 + <track 2>`.
   - `Weight` REMOVED. *"Never prescribed at this level."* The COLUMN survives
     and `prefillWeightOf` still reads it; nothing in this console writes it.
   - `series` 36 → 64. It holds two things now: the position IN the series
     (`A1`, not `A`) stacked over the select that moves the row between
     series. 36 fit one letter and nothing else.
   - `Series type` `minmax(144, 1.15fr)` → `minmax(104, 0.8fr)`. It is read-only
     text now, not a select — derived from the rep targets and no longer
     overridable — so it needs the width of its longest word (`ramp_descending`
     renders as `ramp descending`) and not of a dropdown.
   - `min-width` 1176 → 1088: 984px of tracks, 80px of gaps, 24px of padding. */
.vrto-builder__entry-head,
.vrto-builder__entry-row {
  display: grid;
  grid-template-columns:
    28px 28px 64px minmax(194px, 1.5fr) 44px 212px 56px 72px
    minmax(104px, 0.8fr) minmax(118px, 0.95fr) 64px;
  gap: 8px;
  align-items: center;
  padding: 8px 12px;
  min-width: 1088px;
  /* Named, so the sticky identity cells can `inherit` it and the rows do not
     scroll out from under them. `--picked` overrides it and they follow. */
  background: #fff;
}

/* 🔴 The identity band pins while the rest of the row scrolls.
   The grid is 1048px inside its own horizontal scroller, so at 1024 — and on
   any laptop once the sidebar is out — a coach adjusting a modifier had the
   exercise name off-screen to the left. That is the one piece of information
   that says WHICH prescription is being edited.
   The offsets are the track arithmetic above, and nothing else may set them. */
.vrto-builder__sticky {
  position: sticky;
  z-index: 1;
  background: inherit;
  /* 🔴 STRETCH. A grid item is sized to its content, and `align-items: center`
     on the row centres it — so measured in Chrome the pinned exercise cell was
     33px tall inside an 80px row, and its opaque fill covered only the middle
     band. The scrolled row showed through above and below the exercise name,
     which reads as a rendering fault rather than as a pinned column.
     Stretched, each cell owns its full height and centres its own content. */
  align-self: stretch;
  display: flex;
  align-items: center;
}
/* 🔴 The CELLS are opaque; the GAPS between them were not. Measured in Chrome
   on the preview harness with the panel narrowed and the grid scrolled 260px:
   every pinned cell painted correctly and the scrolled row showed THROUGH the
   three 8px column gaps and the grid's own 12px left padding — `Reps` and its
   caption rendered on top of `Series` and `Exercise`, which is worse than not
   pinning at all.
   Each pinned cell fills the gap to its right with a pseudo-element, and the
   first fills the container's padding to its left. `background: inherit` picks
   up the row's own fill, so `--picked` and the header's grey follow for free,
   and `top: 0; bottom: 0` keeps the fill inside the cell's own height — a
   `box-shadow` spread would have painted over the group's bottom border. */
.vrto-builder__sticky::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 100%;
  width: 8px;
  background: inherit;
}
.vrto-builder__sticky--0::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  right: 100%;
  width: 12px;
  background: inherit;
}
.vrto-builder__sticky--0 {
  left: 12px;
}
.vrto-builder__sticky--1 {
  left: 48px;
}
.vrto-builder__sticky--2 {
  left: 84px;
}
.vrto-builder__sticky--3 {
  left: 156px;
}
/* Above the row cells, which carry their own `z-index: 1` through the header's
   sticky context. */
.vrto-builder__entry-head .vrto-builder__sticky {
  z-index: 2;
}
.vrto-builder__entry-head {
  font-size: 11px;
  font-weight: 600;
  color: var(--vrto-muted);
  background: #f6f6f8;
  border-bottom: 1px solid var(--vrto-line);
  position: sticky;
  top: 0;
  z-index: 1;
}
.vrto-builder__entry-form {
  display: contents;
}
/* Selection had no row treatment at all — with two of three rows picked the
   only cue was the checkbox, while the toolbar above said "2 of 3 selected". */
.vrto-builder__entry-row--picked,
.vrto-builder__entry-row--picked + .vrto-builder__entry-note {
  background: #f6f6f8;
}
/* 🔴 64px, down from 142. `Remove ↑ ↓` spent a 142px track on three controls
   a coach touches rarely, beside a Reps cell that had 96px for the
   prescription itself — and that track is where most of the room for the
   measurement select and the Weight column came from.
   The kill is an icon and the two arrows stack: 28 + 6 + 28 = 62 in a 64px
   track. Fixed, for the reason the note above the tracks gives — an `auto`
   last track made every row resolve a different set of columns. */
.vrto-builder__entry-actions {
  display: flex;
  gap: 6px;
  align-items: flex-start;
  justify-content: flex-end;
}
.vrto-builder__entry-kill {
  width: 28px;
  padding: 4px 0;
  text-align: center;
  font-size: 15px;
  line-height: 1.1;
}
/* The arrows in one 28px column. ⚠️ Both are still always rendered and
   disabled at the ends rather than omitted — the track is fixed to match, and
   the two move together or the header stops naming the column under it. */
.vrto-builder__entry-move-stack {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
/* An arrow is narrower than the word and says the same thing; the labels live
   in `aria-label`. */
.vrto-builder__entry-move {
  width: 28px;
  padding: 0;
  text-align: center;
  font-size: 11px;
  line-height: 1.35;
}
.vrto-builder__entry-row input,
.vrto-builder__entry-row select {
  width: 100%;
  min-width: 0;
}
/* 🔴 No spin buttons in the grid. They occupy real width inside a 44–72px
   track — which is the width a four-digit tempo needs — and nobody steps a
   tempo or a rest one unit at a time. `columnKeyDown` deliberately leaves the
   BARE arrow keys to the number input, so the keyboard path they offer is
   unaffected; only the painted control goes.
   ⚠️ Both rules are needed: Firefox reads `appearance`, Chrome and Safari read
   the pseudo-element. */
.vrto-builder__entry-row input[type='number'] {
  appearance: textfield;
  -moz-appearance: textfield;
}
.vrto-builder__entry-row input[type='number']::-webkit-outer-spin-button,
.vrto-builder__entry-row input[type='number']::-webkit-inner-spin-button {
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
}
.vrto-builder__series {
  font-weight: 600;
  min-width: 2ch;
}
.vrto-builder__series-letter {
  align-self: stretch;
  display: flex;
  /* ⚠️ A COLUMN since the row gained a Series select: the position sits over
     the control that changes it. It was `align-items: center` on a row. */
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: 2px;
  min-width: 0;
  font-weight: 700;
  font-size: 13px;
}
/* 🔴 The superset bracket, drawn down the SERIES column rather than as an
   inset shadow on the group wrapper.
   Two marks were being drawn on the same three pixels of a row's left edge —
   the group's ink bracket and `--changed`'s yellow — so a changed row inside a
   superset showed one of them and lost the other, and which one you got
   depended on rule order. They are now in different places because they say
   different things: the bracket says "these rows run together", the yellow
   says "this row is unsaved".
   ⚠️ It also has to inherit the sticky band, which an absolutely positioned
   bracket on the wrapper could not: the letter it brackets pins on scroll and
   a bracket left behind at the wrapper's x would separate from it. */
.vrto-builder__series-letter--super {
  border-left: 2px solid var(--vrto-ink);
  padding-left: 6px;
}
.vrto-builder__ex {
  min-width: 0;
}
/* 🔴 Was a bare 10px word AFTER the control, on its own line — the console's
   only 10px type outside one other rule, and it read as an orphaned label
   rather than a property of the field. It then became a filled `Auto` pill,
   which said the right thing and cost 18px of height in every row on the page
   whether or not it was there.
   It is a 16px mark beside its control now: same message, same token, and the
   `title` carries the sentence the word used to approximate. Zero height cost,
   so nothing has to reserve for it. */
.vrto-builder__auto {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--vrto-yellow);
  color: var(--vrto-deep);
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  cursor: help;
}
/* 🔴 Was `flex-wrap` with `flex: 1 1 180px`, so the widths depended on how
   many fields the row disclosed: measured 2026-09-07 the series field — which
   holds two characters — came out 543px on the rows with two fields and 268px
   on the row with four. Explicit tracks, and each field placed by name, because
   the two rare fields render as `input[type=hidden]` when the modifier does not
   ask for them and a `display: none` grid child occupies no track.
   ⚠️ The series field itself is GONE — the letter is derived from the row's
   position and drawn read-only at the head of the row — so the first track went
   with it and the three that remain keep their names.
   The indent is not a magic number: it is this grid's own `12px` padding plus
   the grip, checkbox and series-letter tracks and their gaps, so the second
   line starts under the `Exercise` column. It has been `92px` (which missed
   the mark by 16px), `108px` before the drag handle took a track of its own,
   `144px` while the series track was 52px, `128px` while it was 36px, and
   `156px` since it became 64px to hold the position over the Series select.
   🔴 Five values in this comment have gone stale in turn — DERIVE it from the
   track list, which is what `program-builder.test.ts` does rather than quoting
   a number, and which is what caught the last one. */
/* ⚠️ A flex line with FIXED widths now, not three placed grid tracks. The
   tracks existed because the two rare fields once rendered as hidden inputs;
   they render as nothing now, and two empty 80px tracks pushed every coaching
   note 176px right of the Exercise column it is meant to start under. The
   fixed basis is what still stops a field coming out 543px on one row and
   268px on the next. */
.vrto-builder__entry-note {
  display: flex;
  gap: 8px;
  align-items: center;
  /* 🔴 12px of grid padding + the grip (28) + gap + the checkbox (28) + gap +
     the SERIES track + gap. It was 128 while the series track was 36; the
     track is 64 now (it holds the position over the select that moves the
     row), so this is 156. `program-builder.test.ts` derives the number from
     the track list rather than restating it, which is what caught the stale
     one — a note line indented to the wrong column says nothing about which
     row it belongs to. */
  padding: 0 12px 10px 156px;
  min-width: 1176px;
}
.vrto-builder__note-stages,
.vrto-builder__note-drop,
.vrto-builder__note-step-reps,
.vrto-builder__note-step-tempo {
  flex: 0 0 80px;
  min-width: 0;
}
/* Rest/step carries a unit in its placeholder, so it needs the extra width. */
.vrto-builder__note-step-rest {
  flex: 0 0 96px;
  min-width: 0;
}
.vrto-builder__note-text {
  flex: 1 1 auto;
  min-width: 0;
}
.vrto-builder__series-group {
  border-bottom: 1px solid var(--vrto-line);
}
/* The bracket belongs to a group of two or more. It was on the base class, so
   a lone `B` got the same 3px rule as the `A1`/`A2` pair and "grouped" looked
   identical to "not grouped" — which is the one thing §3.4 of the build plan
   calls the risky part. */
/* ⚠️ The class stays — it is what says a cluster IS a superset, and it carries
   `data-series` — but the DRAWING moved to `__series-letter--super`. See the
   note there for why two marks on one left edge was the defect. */
.vrto-builder__series-group--super {
  box-shadow: none;
}
.vrto-builder__series-group:last-child {
  border-bottom: 0;
}
/* 🔴 The A–E frame. *"Prefer to have A-F series sections there already, then a
   search or '+' to add exercise directly into the series position."*
   The head is a band ACROSS the section and deliberately not a row in the
   grid — it sits outside `__entry-head`'s twelve tracks, so it neither lines
   up with them nor has to be recounted when one moves.
   ⚠️ It does NOT scroll with the grid's horizontal scroller: `position:
   sticky; left: 0` pins it to the viewport's left edge inside `__entry-scroll`
   so a coach scrolled right still sees which series they are adding to. */
.vrto-builder__series-head {
  position: sticky;
  left: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 12px;
  background: #fbfbfd;
  border-bottom: 1px solid var(--vrto-line);
  font-size: 11px;
}
.vrto-builder__series-name {
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.vrto-builder__series-add {
  margin-left: auto;
  padding: 2px 8px;
  border: 1px dashed var(--vrto-line);
  border-radius: 6px;
  background: transparent;
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
}
.vrto-builder__series-add:not(:disabled):hover {
  border-style: solid;
  border-color: var(--vrto-ink);
}
.vrto-builder__series-add:disabled {
  cursor: default;
  opacity: 0.5;
}
/* An empty section is quieter than a filled one, and that is the whole of the
   difference now. It used to also mean "you cannot fill this" — every empty
   section but the next free letter was greyed and had no `+` — and since
   2026-09-16 every one of them takes an exercise, so the muting says
   "nothing here yet" and nothing about what is allowed. It keeps its height so
   the frame does not jump as the day fills. */
.vrto-builder__series-group--empty .vrto-builder__series-head {
  background: transparent;
  color: var(--vrto-muted);
}
/* ⚠️ `margin-left: auto` lives on `__series-add`, so this one sits beside it
   rather than claiming the gap for itself — two controls both pushing left
   would put the destructive one at the far edge on a section with no `+`. */
.vrto-builder__series-remove {
  padding: 2px 8px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: var(--vrto-muted);
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
}
.vrto-builder__series-remove:not(:disabled):hover {
  border-color: var(--vrto-danger);
  color: var(--vrto-danger);
}
.vrto-builder__series-remove:disabled {
  cursor: default;
  opacity: 0.5;
}
/* `+ Add series`, under the last section. Pinned left like `__series-head`
   for the same reason: on a day scrolled right it would otherwise sit off
   screen at the end of a 1040px grid. */
.vrto-builder__series-new {
  position: sticky;
  left: 0;
  display: flex;
  padding: 10px 12px;
}
/* The row's position IN its series — `A1`, not `A` — in the pinned identity
   band. ⚠️ `.vrto-builder__series-pick` sat under it and is GONE with the
   per-row Series select; this is the whole of track 2's ink now. */
.vrto-builder__series-pos {
  font-weight: 700;
  font-size: 13px;
  line-height: 1.2;
}
/* The duplicate panel — the day view's last block, and until 2026-09-15 the
   one block in it that was not a card. It was a `__form`: an unlabelled
   `Duplicate into` span, a select, seven native checkboxes and a button on one
   wrapping flex row, sitting loose under `__entries`' bottom border at the
   same indent. `.vrto-builder__pick` — `font-size: 12px; margin-right: 8px`,
   the whole of the old styling — is gone with it.

   ⚠️ It reuses `__picker`'s box deliberately: 1px line, 12px radius, 16px
   padding, white. The day view opens with the exercise picker in that box and
   closes with this one, so the entry table reads as the thing BETWEEN two
   tools rather than as a card with sediment underneath. */
.vrto-builder__dup {
  margin: 16px 0 0;
  padding: 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}
.vrto-builder__dup-head {
  margin-bottom: 14px;
}
/* `.vrto-builder h2` is declared; `h3` is not, so this says its own size
   rather than inheriting the browser's 1.17em bold. */
.vrto-builder__dup-title {
  margin: 0 0 4px;
  font-size: 14px;
  font-weight: 600;
}
.vrto-builder__dup-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
/* A `fieldset` and a `div` in the same track. The fieldset's UA border,
   padding and margin all have to go or the two groups do not align. */
.vrto-builder__dup-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
/* Both a `<legend>` and a `<label>` wear this — the weekday group is a
   fieldset and the phase select is not — so it declares `float`/`padding`,
   which a legend carries from the UA sheet and a label does not. */
.vrto-builder__dup-legend {
  float: none;
  padding: 0;
  color: var(--vrto-muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.vrto-builder__dup-phase {
  max-width: 320px;
}
/* `auto-fill` rather than seven fixed tracks: at the day view's width the week
   is one row, and inside a narrow window it wraps to two or three even rows
   instead of overflowing. */
.vrto-builder__dup-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 8px;
}
/* The chip IS the label, so the whole 36px box is the hit area — the old
   control gave a 13px checkbox and a word. The native box stays visible and
   focusable: hiding it would take `.vrto-builder input:focus-visible` (the
   console's one focus ring, line 1020) off the only control here a keyboard
   reaches. */
.vrto-builder__dup-day {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
  font-size: 13px;
  cursor: pointer;
}
.vrto-builder__dup-day:hover {
  background: #f6f6f8;
}
.vrto-builder__dup-day input {
  margin: 0;
  accent-color: var(--vrto-ink);
  cursor: inherit;
}
.vrto-builder__dup-day-name {
  font-weight: 600;
}
/* Ink, not accent: a picked chip is a selection, and the accent red is the
   page's destructive colour. Two declarations so the ring does not move the
   row — a border-WIDTH change would shift every chip beside it. */
.vrto-builder__dup-day--on {
  border-color: var(--vrto-ink);
  background: #f3f4f7;
  box-shadow: inset 0 0 0 1px var(--vrto-ink);
}
/* The one the old bar could only say with `disabled`. `has a day` is the
   whole reason the box is grey, and it costs a line of 11px type. */
.vrto-builder__dup-day--taken {
  background: #f6f6f8;
  color: var(--vrto-muted);
  cursor: not-allowed;
}
.vrto-builder__dup-day--taken:hover {
  background: #f6f6f8;
}
.vrto-builder__dup-day-note {
  margin-left: auto;
  font-size: 11px;
  color: var(--vrto-muted);
  white-space: nowrap;
}
.vrto-builder__dup-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 12px;
  padding-top: 12px;
  border-top: 1px solid var(--vrto-line);
}
/* A live region, so the count is announced as chips are picked. It is always
   in the DOM for that reason — see the picker's status line for the same
   rule. */
.vrto-builder__dup-status {
  margin: 0;
  min-width: 0;
  color: var(--vrto-muted);
  font-size: 13px;
}

/* The catalogue's bar: filter, count, and the disclosure that used to be a
   six-field form pinned open above the list. Creating a programme is rare;
   browsing to one is what a coach does every session. */
.vrto-builder__cat-bar {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 16px;
}
.vrto-builder__cat-search {
  flex: 1 1 260px;
  min-width: 0;
}
/* The line naming what a working copy is a copy of. `__crumb-link` sits
   inside the sentence, so the whole line has to share a baseline with a
   button rather than being a block with a control dropped into it. */
/* The version date beside the outline's lifecycle badge. */
.vrto-builder__outline-when {
  font-size: 13px;
}
.vrto-builder__cloned {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px;
  margin: 0;
}
/* A draft's copy line: publishing replaces the source for new members, so it
   is flagged. `--vrto-accent` as the border only — it fails contrast as text,
   see the token note — with the sentence kept in ink on a pale orange wash. */
.vrto-builder__cloned--flag {
  padding: 8px 12px;
  border: 1px solid var(--vrto-accent);
  border-left-width: 3px;
  border-radius: 6px;
  background: #fff4f0;
  color: var(--vrto-ink);
}
.vrto-builder__picker-more {
  padding-top: 8px;
}
/* 🔴 The attribute filters, on their own row under the search box — location
   among them, rather than alone beside the query box.
   *"Can we add additional filters for: equipment type, movement pattern,
   primary muscle group, low impact."* They wrap rather than scroll: on a
   narrow panel four selects on one line would each be too narrow to read the
   option they are showing. */
.vrto-builder__filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding-top: 8px;
}
.vrto-builder__filters .vrto-select {
  flex: 1 1 150px;
  min-width: 0;
  max-width: 220px;
}
.vrto-builder__filter-check {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  white-space: nowrap;
  cursor: pointer;
}
.vrto-builder__filter-clear {
  border: 0;
  background: transparent;
  padding: 0;
  font-size: 12px;
  font-weight: 600;
  text-decoration: underline;
  cursor: pointer;
  color: var(--vrto-muted);
}
.vrto-builder__filter-clear:hover {
  color: var(--vrto-ink);
}
/* Says where the next add LANDS, when the picker was opened from inside a
   series section rather than from the day. */
.vrto-builder__picker-scope {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 8px 0 0;
  padding: 6px 10px;
  border-radius: 6px;
  background: var(--vrto-yellow-soft, #fdf6e3);
  font-size: 12px;
  font-weight: 600;
}
/* A derived value the coach cannot change — the rep ladder. Deliberately not
   styled as a disabled input: a greyed select invites a click that does
   nothing, and this is text because it is a READING, not a field. */
.vrto-builder__derived {
  font-size: 12px;
  color: var(--vrto-muted);
  text-transform: capitalize;
}
/* A row whose local model differs from the row it was seeded from. Without it
   the only account of unsaved work is the Save button's count, and the point
   of the local model is that a coach can see WHICH rows are pending. Left
   edge, in the yellow the `Auto` badge already uses for "derived, not
   stored" — the superset bracket is the same inset in ink, and the two never
   apply to the same element (one is on the group, one on the row). */
.vrto-builder__entry-row--changed {
  box-shadow: inset 3px 0 0 var(--vrto-yellow);
}
.vrto-builder__entry-row--dragging {
  opacity: 0.5;
}
/* The drag handle, at the head of the row rather than the tail of the actions
   cell. `cursor: grab` is the only affordance a drag has, and on its own it is
   not a sign — a coach has to be hovering the right thing already to learn
   the row moves. 28px is the grid's first track, so the glyph sits in its own
   column above and below every other row's. */
.vrto-builder__entry-grip {
  width: 28px;
  padding: 6px 0;
  text-align: center;
  justify-content: center;
  cursor: grab;
}
.vrto-builder__entry-grip:active {
  cursor: grabbing;
}
/* 🔴 The Reps cell is a STACK — the measurement OVER the per-set tiles.
   `rep_type` decides what the numbers under it MEAN: `8-12` repetitions,
   `20` metres, `1:30` of work. It was never on screen. The type was inferred
   from whatever was typed by a classifier with no branch for `time` or
   `distance`, so those two shapes could not be authored, could not be
   corrected, and rendered as an EMPTY box that turned itself into `specific`
   on the first keystroke.
   Stacked rather than laid side by side because side by side wanted ~180px in
   a grid already wider than the panel it sits in, and because it is how the
   frame the client sent draws it. */
.vrto-builder__reps-cell {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.vrto-builder__reps-type {
  width: 100%;
  min-width: 0;
}
/* The value line: one input for the counted shapes, two and a colon for time,
   one and an `m` for distance. */
.vrto-builder__reps-value {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}
.vrto-builder__reps-value input {
  flex: 1 1 auto;
  min-width: 0;
}
/* The unit is a LABEL on the field, not a value in it — `20` and `m` are one
   prescription, and typing the `m` into the box is what `distance_meters`
   exists to stop. */
.vrto-builder__reps-unit {
  flex: 0 0 auto;
  color: var(--vrto-muted);
  font-size: 11px;
  font-weight: 600;
}

/* 🔴 One tile per set: the rep target over that set's class. A set was spread
   across three places before — the Reps box, a per-set line under the row and
   a chip in the Classification column — so reading set 2 meant finding it
   three times. 48px holds `8-12` and a `1 WU` chip. */
.vrto-builder__set-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 4px;
}
.vrto-builder__set-tile {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 48px;
}
.vrto-builder__set-tile input {
  width: 100%;
  min-width: 0;
  padding-left: 4px;
  padding-right: 4px;
  text-align: center;
}
/* A range set is two bounds, so its tile holds a min–max pair and is wide
   enough for two two-digit boxes and the dash — three fit on one line of the
   212px Reps track. */
.vrto-builder__set-tile--range {
  width: 64px;
}
.vrto-builder__range {
  display: flex;
  align-items: center;
  gap: 1px;
}
.vrto-builder__range input {
  flex: 1 1 0;
  padding-left: 2px;
  padding-right: 2px;
}
.vrto-builder__range input::placeholder {
  font-size: 10px;
}
.vrto-builder__range-dash {
  color: var(--vrto-muted);
}
.vrto-builder__set-clamp {
  color: var(--vrto-muted);
  font-size: 10px;
}
/* 🔴 The per-set class chip is a LABEL now. The coach's review — *"remove warm
   up, technique, working pills - all programmed sets are working"* — took the
   AUTHORING: it was a button cycling `working → technique → warmup`, and
   nothing here writes `set_class` any more.

   ⚠️ It still RENDERS, deliberately: 327 prescriptions carry a `technique`
   set and the review queue has nowhere else to see which. So there is no
   cursor, no hover border and no `:disabled` state — a span cannot be
   disabled, and a control affordance on something nothing happens to press is
   the lie the removal was about. It goes when that queue is empty and the
   class leaves the enum, not before. */
.vrto-builder__set-tile .vrto-builder__set-chip {
  justify-content: center;
}
.vrto-builder__set-chip {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 1px 5px;
  border: 1px solid transparent;
  border-radius: 4px;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.6;
}
.vrto-builder__set-chip--working {
  background: var(--vrto-ok-soft, #e6f4ea);
  color: var(--vrto-deep);
}
.vrto-builder__set-chip--technique {
  background: var(--vrto-yellow);
  color: var(--vrto-deep);
}
.vrto-builder__set-chip--warmup {
  background: #eceef2;
  color: var(--vrto-muted);
}
/* The set number, so a row of identical marks still says which set is which.
   ⚠️ This is the only place the number is drawn — the separate `__set-num`
   line under each rep box went at the same review, because left to right is
   already the set order. */
.vrto-builder__chip-num {
  font-weight: 600;
  opacity: 0.65;
}

/* The exercise cell: the name, and under it where the movement can be done.
   The picker has printed the locations since the primary key came off it; the
   row never did, so once an exercise was placed the fact that decides whether
   it belongs in a home variant left the screen. */
/* Holds the select checkbox, so the sticky cell can stretch without the
   checkbox stretching with it. */
.vrto-builder__pick-cell {
  justify-content: center;
}
/* ⚠️ `align-items: flex-start` undoes `__sticky`'s `center` on the CROSS axis.
   These two are flex COLUMNS, so `__sticky`'s `align-items: center` — which is
   vertical centring on the row's own flex cells — becomes HORIZONTAL centring
   here, and it centred every exercise name and column heading over its track.
   The audit of 2026-09-07 fixed exactly this on the outline grid's `th`. */
.vrto-builder__ex-cell {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* Centred in its own right, because the cell is stretched now — see
     `__sticky`. */
  justify-content: center;
  gap: 1px;
  min-width: 0;
}
/* On a ROW the cell holds the cover tile beside the name, so it runs across.
   The header's Exercise cell keeps the column above — it has no tile. */
.vrto-builder__entry-row > .vrto-builder__ex-cell {
  flex-direction: row;
  gap: 8px;
}
.vrto-builder__ex-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  min-width: 0;
}

/* The exercise's cover, in the picker and on the row — see
   `program-builder-media.ts`. The fill IS the placeholder: a blank `cover`
   renders no `img`, and a cover that fails to load hides its own, so both
   show this tile rather than a broken-image glyph. Not the app's placeholder
   art, which the API deliberately does not copy. */
.vrto-builder__thumb {
  position: relative;
  flex: none;
  display: block;
  overflow: hidden;
  border-radius: 4px;
  background: var(--vrto-line);
}
.vrto-builder__thumb--row {
  width: 36px;
  height: 36px;
}
/* ⚠️ Smaller than the 40 it was, and it LOSES nothing: every cover is a
   portrait still (measured in the picker: 770×1100, 1080×1920), so a square
   tile is already a centre crop and 32 crops exactly as much as 40 did. */
.vrto-builder__thumb--match {
  width: 32px;
  height: 32px;
  border-radius: 5px;
}
/* An exercise with no cover renders no `img`, and a flat fill at this size
   reads as a tile still loading. The ring says the tile is empty on purpose. */
.vrto-builder__thumb--match::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid rgb(25 26 29 / 8%);
  border-radius: inherit;
  pointer-events: none;
}
/* 18px on a 32px tile is half the picture. */
.vrto-builder .vrto-builder__thumb--match .vrto-builder__thumb-play {
  width: 15px;
  height: 15px;
  font-size: 7px;
}
.vrto-builder__thumb-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* A corner badge, not a scrim over the whole tile: the picture is what the
   coach is reading, and darkening all of it to say "has video" costs the one
   thing the tile is for. */
.vrto-builder .vrto-builder__thumb-play {
  position: absolute;
  right: 2px;
  bottom: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0 0 0 1px;
  border: 0;
  border-radius: 50%;
  background: rgb(25 26 29 / 72%);
  color: #fff;
  font-size: 8px;
  line-height: 1;
  cursor: pointer;
}
.vrto-builder .vrto-builder__thumb-play:hover {
  background: var(--vrto-ink);
}

/* The player. The shared dialog is 460px for a sentence and two buttons; a
   16:9 frame at that width is too small to judge a movement by. */
.vrto-dialog--media {
  max-width: 720px;
}
.vrto-dialog--media .vrto-show__video {
  display: block;
  box-sizing: border-box;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  border-radius: 6px;
  background: #191a1d;
}
.vrto-dialog--media .vrto-show__video-title {
  margin: 6px 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--vrto-muted);
}
.vrto-builder__ex-loc {
  color: var(--vrto-muted);
  font-size: 10px;
  line-height: 1.4;
}

/* A two-line column heading: the name, and the vocabulary the cell under it
   accepts. The headings were twelve bare words in one weight, so `Reps` said
   nothing about the fact that its cell can hold metres. */
.vrto-builder__col {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 1px;
  min-width: 0;
}
.vrto-builder__col-cap {
  font-size: 10px;
  font-weight: 500;
  line-height: 1.3;
  opacity: 0.75;
}
/* 🔴 Fixed to the VIEWPORT. A message about an action has to be near the
   action, and the entry grid is `min-width: 1040px` inside its own scroller
   on a page that scrolls too — so no position in the document flow is near
   every action. Bottom-centre is the one place that is near all of them.
   `z-index` clears the sticky entry header (`z-index: 1`) and stays under the
   confirm dialog, which has to be able to cover it. */
.vrto-builder__toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: min(560px, calc(100vw - 32px));
  padding: 10px 12px 10px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background-color: #fff;
  box-shadow: 0 6px 24px rgb(0 0 0 / 18%);
  font-size: 13px;
  line-height: 1.4;
}
.vrto-builder__toast--ok {
  border-left: 3px solid var(--vrto-ink);
}
.vrto-builder__toast--danger {
  border-left: 3px solid var(--vrto-danger);
  color: var(--vrto-danger);
  font-weight: 600;
}
.vrto-builder__toast-close {
  flex: 0 0 auto;
  padding: 0 4px;
  border: 0;
  background-color: transparent;
  color: var(--vrto-muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.vrto-builder__toast-close:hover {
  color: var(--vrto-ink);
}

/* ---------------------------------------------------------------------------
 * 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;
  /* Five categories since `Created, not live yet` (exercise editing). */
  grid-template-columns: repeat(5, 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;
  }
}

/* ---------------------------------------------------------------------------
 * Cutover → Exercise merges — `vrto-cm`
 *
 * The page answers a LOOKUP and a QUEUE out of one table, and the styling is
 * what keeps them apart: the table is quiet, and the only colour on it is the
 * amber a merge earns by leaving rows behind. A clean row is grey text; a row
 * that is not carries a left rule and a tinted band, so an operator scanning
 * two hundred rows is scanning for the one thing that is work.
 *
 * ⚠️ The counts strip is NOT the video page's strip. There, zero is calm and
 * non-zero is a job. Here three of the four counts are inventory and only
 * `merges that left rows behind` is ever work — so every tile is neutral by
 * default and the component passes `amber` for the one that can be a job.
 *
 * 🔴 The root is named in the token block near the top of this file. It
 * renders its own root element, so without that line every `var(--vrto-*)`
 * below would resolve to nothing.
 * ------------------------------------------------------------------------- */

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

/* --- the counts strip ---------------------------------------------------- */

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

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

/* The one tile that can be a job. It is a tile, not a shout: the number takes
 * the ink and the card comes forward, so the strip keeps one shape. */
.vrto-cm__count--amber {
  border-top-color: var(--vrto-yellow);
  background: #fff;
}

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

.vrto-cm__count--amber .vrto-cm__count-value {
  color: var(--vrto-ink);
}

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

/* 🔴 An interrupted run is the only state on this page that needs a developer,
 * so it takes the danger red the console reserves for text rather than the
 * amber a stranded row gets. Two ambers would make them one thing. */
.vrto-cm__warning {
  margin: 0 0 18px;
  padding: 10px 12px;
  border-left: 3px solid var(--vrto-danger);
  background: #fdf2f0;
  font-size: 13px;
  line-height: 1.5;
}

/* --- the controls -------------------------------------------------------- */

.vrto-cm__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 0 0 8px;
}

.vrto-cm__search {
  flex: 1 1 260px;
  min-width: 0;
  padding: 7px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  background: #fff;
  color: var(--vrto-ink);
  font-size: 13px;
}

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

.vrto-cm__only {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--vrto-muted);
  font-size: 12.5px;
  white-space: nowrap;
  cursor: pointer;
}

/* An anchor doing a button's job, so it is given a button's hit area and
 * border. The component says why it is an anchor: the imperative download
 * idiom needs `document`, which this workspace's tsconfig has no lib for. */
.vrto-cm__csv {
  margin-left: auto;
  padding: 7px 12px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  color: var(--vrto-ink);
  font-size: 12.5px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

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

.vrto-cm__search:focus-visible,
.vrto-cm__run:focus-visible,
.vrto-cm__only input:focus-visible,
.vrto-cm__csv:focus-visible {
  outline: 2px solid var(--vrto-ink);
  outline-offset: 2px;
}

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

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

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

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

/* Sticky, because the two name columns look alike and a long list scrolls the
 * only thing that says which is which off the top. */
.vrto-cm__table th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: #fff;
  border-bottom: 1px solid var(--vrto-ink);
  color: var(--vrto-muted);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
}

/* The whole row, because the thing being flagged is the MERGE rather than the
 * count — a tint on the last cell of a 1080px table sits off the edge of where
 * the eye is reading the names. */
.vrto-cm__row--amber td {
  background: #fdf6e3;
}

.vrto-cm__row--amber td:first-child {
  box-shadow: inset 3px 0 0 var(--vrto-yellow);
}

.vrto-cm__name {
  font-weight: 600;
  line-height: 1.35;
}

.vrto-cm__id {
  margin-top: 1px;
  color: var(--vrto-muted);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}

/* The arrow is punctuation between the two names, not a column of its own. */
.vrto-cm__arrow {
  width: 1px;
  padding-left: 0;
  padding-right: 0;
  color: var(--vrto-muted);
  text-align: center;
}

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

.vrto-cm__left {
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
}

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

/* The page is desktop-gated, so this is the narrow LAPTOP rather than a phone
 * — the four-track strip is the only thing that cannot hold its shape there. */
@media (max-width: 900px) {
  .vrto-cm__counts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .vrto-cm__csv {
    margin-left: 0;
  }
}

/* ─────────────────────────────────────────────────────────────────────────
 * Dashboard: worklists and the activity panel.
 *
 * 🔴 No token block of its own. Both sections render INSIDE `.vrto-dashboard`,
 * which is already in the page-root list above, so they inherit `--vrto-*`.
 * A section that grows its own `<main>` would need adding to that list — the
 * mistake the comment above that block records five times.
 * ───────────────────────────────────────────────────────────────────────── */

.vrto-worklists,
.vrto-activity {
  margin-top: 28px;
}

.vrto-worklists__title,
.vrto-activity__title {
  margin: 0 0 12px;
  font-size: 14px;
  font-weight: 600;
  color: var(--vrto-ink);
}

.vrto-worklists__list {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Wider than `.vrto-tile` because each carries its definition, and a queue
 * whose meaning has to be guessed is the thing these exist to stop. */
.vrto-worklist {
  display: flex;
  flex: 1 1 240px;
  flex-direction: column;
  gap: 4px;
  max-width: 340px;
  padding: 16px;
  border: 1px solid var(--vrto-line);
  border-left: 3px solid var(--vrto-accent);
  border-radius: 8px;
  background: #fff;
}

.vrto-worklist__value {
  font-size: 26px;
  font-weight: 600;
  line-height: 1.1;
  color: var(--vrto-ink);
}

.vrto-worklist__label {
  font-size: 13px;
  font-weight: 600;
  color: var(--vrto-ink);
}

.vrto-worklist__definition {
  font-size: 12px;
  line-height: 1.45;
  color: var(--vrto-muted);
}

.vrto-activity__plot {
  padding: 12px 8px 4px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
}

/* The sentence that says what each line counts. It is not decoration: three
 * nested lines are unreadable without knowing they are nested. */
.vrto-activity__definition {
  margin: 8px 0 0;
  max-width: 70ch;
  font-size: 12px;
  line-height: 1.5;
  color: var(--vrto-muted);
}

.vrto-activity__empty {
  padding: 24px;
  border: 1px dashed var(--vrto-line);
  border-radius: 8px;
  font-size: 13px;
  color: var(--vrto-muted);
}

/* 🔴 TOP-aligned, not centred. The Reps cell is the tallest thing on a row —
   a select over a wrap of set tiles — and centring every other control on it
   floated Sets, Rest and the selects halfway down, level with nothing. Top
   alignment puts every single-line control on the same line as the Reps
   select, which is the line a coach reads across. */
.vrto-builder__entry-row {
  align-items: start;
}
.vrto-builder__entry-row > .vrto-builder__sticky {
  align-items: flex-start;
  padding-top: 7px;
}
.vrto-builder__entry-row > .vrto-builder__ex-cell {
  justify-content: flex-start;
  padding-top: 4px;
}

/* ── The programme import screen ──────────────────────────────────────────
 *
 * The builder's fourth view (`program-builder-import.ts`), reached from the
 * Import spreadsheet button on the Programs and Program variants lists.
 *
 * 🔴 It renders inside `.vrto-builder`, which is where the `--vrto-*` tokens
 * are declared — see the token block at the top of this file, whose own note
 * records the omission biting three times. Nothing here may be used outside
 * that root.
 */
.vrto-import {
  margin: 0;
}
/* The picker, the Read button and Start again on one row, wrapping to their
   own lines on a narrow screen rather than squeezing the file input — which
   is the one control here whose width the browser decides, not this file. */
.vrto-import__pick {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 12px;
  align-items: center;
  margin: 12px 0 8px;
}
.vrto-import__pick > input[type='file'] {
  min-width: 0;
  max-width: 100%;
  font-size: 13px;
  color: var(--vrto-ink);
}
.vrto-import__summary {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 600;
  color: var(--vrto-ink);
}
.vrto-import__list {
  margin: 16px 0 0;
}
.vrto-import__list > h3 {
  margin: 0 0 2px;
  font-size: 14px;
}
.vrto-import__list > p {
  margin: 0 0 8px;
}
.vrto-import__rows {
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Each warning is a stack, not a table row: the label, the detail and the
   sheet/row provenance are three different lengths and a grid would leave the
   longest of them wrapping inside a narrow column. */
.vrto-import__row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 12px;
  border: 1px solid var(--vrto-line);
  border-left-width: 3px;
  border-radius: 8px;
  background: #fff;
}
.vrto-import__row + .vrto-import__row {
  margin-top: 6px;
}
.vrto-import__row--warn {
  border-left-color: var(--vrto-yellow);
}
/* `--vrto-danger`, not `--vrto-accent`: see the token note. These rows carry
   text an operator has to read and act on. */
.vrto-import__row--danger {
  border-left-color: var(--vrto-danger);
}
.vrto-import__row-label {
  font-size: 13px;
  font-weight: 600;
  overflow-wrap: anywhere;
}
.vrto-import__row-detail {
  font-size: 13px;
  color: var(--vrto-ink);
  overflow-wrap: anywhere;
}
.vrto-import__row-note {
  font-size: 12px;
  color: var(--vrto-muted);
  overflow-wrap: anywhere;
}
.vrto-import__tab {
  margin: 0 0 16px;
  padding: 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}
.vrto-import__tab > h3 {
  margin: 0 0 8px;
  font-size: 14px;
}
/* ⚠️ Its own scroll container. Seven columns do not fit a laptop beside the
   sidebar, and the builder's page root is not a scroller — the entry grid
   below it makes the same arrangement for the same reason. */
.vrto-import__tab {
  overflow-x: auto;
}
.vrto-import__table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
.vrto-import__table th {
  padding: 4px 8px;
  border-bottom: 1px solid var(--vrto-line);
  color: var(--vrto-muted);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}
.vrto-import__table td {
  padding: 4px 8px;
  border-bottom: 1px solid var(--vrto-line);
  vertical-align: top;
}
/* 🔴 A tint AND a coloured word, never colour alone — the unresolved rows are
   the ones an operator must find, and `Not resolved` is in the cell beside
   this for a reader who cannot see the tint. */
.vrto-import__tr--unresolved {
  background: #fdf3f1;
}
.vrto-import__unresolved {
  color: var(--vrto-danger);
  font-weight: 600;
}

/* --- the import's OVERVIEW: the builder's grid, with flags on the cards --- */

/* 🔴 The grid itself is `.vrto-builder__grid` — the outline's, not a copy. The
   two screens are the same object at two moments and an operator who learns
   one has learned the other, so what is here is only what a SESSION card has
   that a day card does not: a tone for its worst flag, and the chips. */
.vrto-import__slot {
  /* Not draggable, unlike the outline's day card — a tab's weekday is moved
     with the picker on the session's own screen, where the flags explaining
     why it is on that weekday are legible. `grab` here would promise a gesture
     that does nothing. */
  cursor: default;
}
.vrto-import__slot--warn {
  border-left: 3px solid var(--vrto-yellow);
}
.vrto-import__slot--danger {
  border-left: 3px solid var(--vrto-danger);
}
.vrto-import__slot--open {
  outline: 2px solid var(--vrto-ink);
  outline-offset: 1px;
}
.vrto-import__slot--excluded {
  opacity: 0.55;
}
.vrto-import__flags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 2px;
}
/* 🔴 A chip carries its own words, never colour alone — `3 to match` reads the
   same to someone who cannot see that it is red, and the `title` carries the
   whole sentence the session's own screen prints in full. */
.vrto-import__flag {
  padding: 1px 6px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  white-space: nowrap;
}
.vrto-import__flag--warn {
  background: #fdf6e3;
  color: #7a5b00;
}
.vrto-import__flag--danger {
  background: #fdf3f1;
  color: var(--vrto-danger);
}

/* One session's own screen — its head, and the table beneath it. */
.vrto-import__session-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px 16px;
  margin-bottom: 10px;
}
.vrto-import__session-head h2 {
  margin: 0 0 2px;
}
/* ⚠️ Its own scroll container, as the stacked tables were: seven columns do
   not fit a laptop beside the sidebar, and the builder's page root is not a
   scroller. */
.vrto-import__session {
  overflow-x: auto;
}
/* The match state and the button that changes it, on one line in the cell. */
.vrto-import__cell-match {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.vrto-import__cell-btn {
  padding: 0;
  border: 0;
  background: none;
  /* ⚠️ `--vrto-ink`, not `--vrto-accent`. The accent is #ff623e and the token
     block's own note says it is not the colour for something an operator has
     to act on — an orange underline in a table of prescriptions reads as a
     warning about the row rather than as the control that changes it. */
  color: var(--vrto-ink);
  font: inherit;
  font-size: 12px;
  text-decoration: underline;
  cursor: pointer;
}
.vrto-import__cell-btn:disabled {
  color: var(--vrto-muted);
  cursor: default;
}
/* A row the operator chose to leave out stays on screen, struck through — the
   silent drop is what this whole screen is a correction of. */
.vrto-import__tr--dropped {
  color: var(--vrto-muted);
}
.vrto-import__tr--dropped td:nth-child(2) {
  text-decoration: line-through;
}
/* 🔴 The matcher opens UNDER the row it answers, in a cell spanning the table,
   rather than in a panel elsewhere: the question is decided by reading the
   sets, reps and tempo beside the name. */
.vrto-import__match-cell {
  background: var(--vrto-wash, #fafafa);
}
.vrto-import__match {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.vrto-import__match-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.vrto-import__match-controls .vrto-select {
  max-width: 420px;
  width: 100%;
  flex: 1 1 260px;
}
.vrto-import__search {
  padding: 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
}

/* --- the import's WRITE half: the answers, and what still blocks it ------ */

/* The chooser that settles one unresolved name. It sits under the row's own
   detail and note rather than beside them, because the note is the list of
   sheet rows this one answer covers and reading it is what decides the
   answer. */
.vrto-import__row-choice {
  display: block;
  margin-top: 6px;
}
.vrto-import__row-choice .vrto-select {
  max-width: 420px;
  width: 100%;
}

/* 🔴 The include tick and the weekday picker, on the tab they belong to.
   `.vrto-builder input` gives every input a border and 6px of padding, which
   turns a checkbox into a bordered smudge — so the checkbox opts out of that
   rule here rather than the rule growing an exception nobody can see from the
   markup. */
.vrto-import__tab-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  margin: 0 0 10px;
}
.vrto-import__tab-include {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--vrto-ink);
}
.vrto-import__tab-include input[type='checkbox'] {
  padding: 0;
  border: 0;
  width: auto;
  accent-color: var(--vrto-ink);
}
/* An excluded tab stays on screen, dimmed. Removing it would leave an operator
   who unticked the wrong one with no way back to it and no statement that a
   sixth tab exists at all — which is the silent drop this whole screen is a
   correction of. */
.vrto-import__tab--excluded {
  opacity: 0.55;
}

/* Why the button is off. A list of sentences, not a tooltip: the operator has
   to be able to read all of them at once and go and fix them. */
.vrto-import__blockers {
  margin: 12px 0 0;
  padding: 10px 12px 10px 28px;
  border: 1px solid var(--vrto-line);
  border-left: 3px solid var(--vrto-danger);
  border-radius: 6px;
  background: #fdf3f1;
  font-size: 13px;
  color: var(--vrto-ink);
}
.vrto-import__blockers > li + li {
  margin-top: 4px;
}
.vrto-import__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  margin-top: 14px;
}

/* ---------------------------------------------------------------------------
 * Moderation queue (`src/components/moderation-queue.ts`).
 *
 * The phone job list's first entry, and the same component on desktop — no
 * separate mobile layout, cards at every width. `.vrto-modq` picks up the
 * shared tokens declared on every page root above.
 * ------------------------------------------------------------------------- */

.vrto-modq {
  max-width: 720px;
  padding: 32px 24px 64px;
}

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

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

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

/*
 * Retry, inside the error paragraph. 44px tall because it is the one control
 * on this screen a phone operator reaches for while something is already
 * wrong — the same minimum `.vrto-phone-launcher__item` uses.
 */
.vrto-modq__retry {
  display: block;
  min-height: 44px;
  margin-top: 8px;
  padding: 10px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
  color: var(--vrto-ink);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.vrto-modq__retry:disabled {
  color: var(--vrto-muted);
  cursor: default;
}

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

.vrto-modq__cards {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 20px;
}

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

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

.vrto-modq__kind {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--vrto-muted);
}

.vrto-modq__reports {
  padding: 2px 8px;
  border-radius: 10px;
  background: var(--vrto-ink);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
}

.vrto-modq__body {
  margin: 10px 0 0;
  font-size: 15px;
  line-height: 1.5;
  white-space: pre-wrap;
}

/* A link-styled button rather than an `<a>` — it toggles in place and
   changes nothing about the location bar. */
.vrto-modq__more {
  margin: 8px 0 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--vrto-deep, var(--vrto-ink));
  font-size: 13px;
  font-weight: 600;
  text-decoration: underline;
  cursor: pointer;
}

.vrto-modq__meta {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 12px;
  margin: 14px 0 0;
  font-size: 13px;
}

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

.vrto-modq__meta dd {
  margin: 0;
  overflow-wrap: anywhere;
}

/* Approve/Reject — large tap targets rather than a compact row, so a card is
   still workable one-thumbed on a phone. Desktop keeps the same buttons; a
   ten-column table is the thing this screen replaces, not a second size for
   these two. */
.vrto-modq__actions {
  display: flex;
  gap: 12px;
  margin-top: 16px;
}

.vrto-modq__approve,
.vrto-modq__reject {
  flex: 1 1 0;
  min-height: 44px;
  padding: 10px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}

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

.vrto-modq__reject {
  border-color: var(--vrto-danger);
  background: #fff;
  color: var(--vrto-danger);
}

.vrto-modq__approve:disabled,
.vrto-modq__reject:disabled {
  opacity: 0.5;
  cursor: default;
}

/* ---------------------------------------------------------------------------
 * "This screen needs a laptop" (`src/components/desktop-required.ts`).
 *
 * Decision 2 from the ticket: the route stays open, so this is what an
 * operator sees instead of a grid that needs width they do not have.
 * ------------------------------------------------------------------------- */

.vrto-desktop-required {
  max-width: 480px;
  padding: 64px 24px;
  text-align: center;
}

.vrto-desktop-required__body {
  margin: 12px 0 0;
  color: var(--vrto-muted);
  font-size: 14px;
  line-height: 1.5;
}

/* ---------------------------------------------------------------------------
 * The phone launcher (`src/components/dashboard.ts`'s `PhoneLauncherView`).
 *
 * What the Dashboard renders below the phone breakpoint instead of the tile
 * grid — a short list of the jobs this operator can actually open.
 * ------------------------------------------------------------------------- */

.vrto-phone-launcher {
  max-width: 480px;
  padding: 32px 24px 64px;
}

.vrto-phone-launcher__empty {
  color: var(--vrto-muted);
  font-size: 14px;
}

.vrto-phone-launcher__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.vrto-phone-launcher__item {
  display: block;
  min-height: 44px;
  padding: 12px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  color: var(--vrto-ink);
  font-size: 15px;
  font-weight: 600;
  text-decoration: none;
}

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

/* ---------------------------------------------------------------------------
 * The phone breakpoint itself — `auth/phone-pages.ts`'s `PHONE_MAX_WIDTH`.
 *
 * 🔴 820, reused from Recipe amounts' own breakpoint above rather than a
 * sixth number invented beside 640/720/768/820/1324. It is NOT AdminJS's
 * 1324px drawer/collapse split (`sidebar-toggle.ts`'s header) — this query
 * must never move that one, and that one must never move this one.
 * ------------------------------------------------------------------------- */

@media (max-width: 820px) {
  .vrto-modq__actions {
    flex-direction: column;
  }
}
/* ---------------------------------------------------------------------------
 * Unit conversions — Repair. The Recipe amounts backlog, one row per
 * (food, unit) decision, biggest win first. `components/unit-conversions.ts`.
 * ------------------------------------------------------------------------- */
.vrto-uc {
  max-width: 1280px;
  margin: 0 auto;
  padding: 28px 24px 64px;
  color: var(--vrto-ink);
}
.vrto-uc__lede {
  margin: 0 0 20px;
  max-width: 78ch;
  color: var(--vrto-muted);
  font-size: 13px;
  line-height: 1.55;
}
.vrto-uc__stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  margin: 0 0 16px;
}
.vrto-uc__stat {
  padding: 14px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  background: #fff;
}
.vrto-uc__stat-value {
  font-size: 24px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.vrto-uc__stat-label {
  margin-top: 2px;
  font-size: 12px;
}
.vrto-uc__stat-note {
  margin-top: 4px;
  color: var(--vrto-muted);
  font-size: 11px;
}
.vrto-uc__caveat {
  margin: 0 0 16px;
  color: var(--vrto-muted);
  font-size: 12px;
}
.vrto-uc__notice,
.vrto-uc__error {
  margin: 0 0 16px;
  padding: 10px 14px;
  border-radius: 9px;
  font-size: 12.5px;
}
.vrto-uc__notice {
  border: 1px solid var(--vrto-green);
  background: rgba(0, 208, 132, 0.08);
}
.vrto-uc__error {
  border: 1px solid var(--vrto-danger);
  background: rgba(200, 52, 26, 0.06);
  color: var(--vrto-danger);
}
.vrto-uc__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin: 0 0 12px;
  font-size: 12.5px;
}
.vrto-uc__scope {
  display: inline-flex;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  overflow: hidden;
}
.vrto-uc__scope button {
  padding: 6px 12px;
  border: 0;
  background: #fff;
  font: inherit;
  cursor: pointer;
}
.vrto-uc__scope button.is-on {
  background: var(--vrto-ink);
  color: #fff;
}
.vrto-uc__search {
  display: inline-flex;
  gap: 6px;
}
.vrto-uc__search input,
.vrto-uc__grams {
  padding: 5px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  font: inherit;
}
.vrto-uc__pager {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  color: var(--vrto-muted);
}
.vrto-uc__table {
  width: 100%;
  border-collapse: collapse;
  border: 1px solid var(--vrto-line);
  border-radius: 10px;
  background: #fff;
  font-size: 12.5px;
}
.vrto-uc__table th {
  padding: 9px 10px;
  border-bottom: 1px solid var(--vrto-line);
  color: var(--vrto-muted);
  font-size: 11px;
  font-weight: 600;
  text-align: left;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.vrto-uc__table td {
  padding: 9px 10px;
  border-top: 1px solid var(--vrto-line);
  vertical-align: top;
}
.vrto-uc__rank {
  color: var(--vrto-muted);
  font-variant-numeric: tabular-nums;
}
.vrto-uc__ing {
  font-weight: 600;
}
.vrto-uc__sub {
  margin-top: 2px;
  color: var(--vrto-muted);
  font-size: 11px;
}
.vrto-uc__unit {
  padding: 2px 7px;
  border-radius: 999px;
  background: #f3f4f7;
  font-weight: 600;
}
.vrto-uc__num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.vrto-uc__lines {
  color: var(--vrto-danger);
  font-weight: 700;
}
.vrto-uc__member,
.vrto-uc__sep,
.vrto-uc__none {
  color: var(--vrto-muted);
}
.vrto-uc__ref ul {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 11px;
}
.vrto-uc__answer form {
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.vrto-uc__grams {
  width: 64px;
}
.vrto-uc__save {
  padding: 5px 12px;
  border: 0;
  border-radius: 6px;
  background: var(--vrto-accent);
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.vrto-uc__save:disabled {
  opacity: 0.4;
  cursor: default;
}
.vrto-uc__empty {
  color: var(--vrto-muted);
}

/* Repair → Modifier QA (`components/modifier-qa.ts`) — one prescription per
 * card. Named in the token list at the top of this file, or every
 * `var(--vrto-*)` below resolves to nothing. */
.vrto-mqa {
  max-width: 1180px;
  margin: 0 auto;
  padding: 24px 24px 64px;
  color: var(--vrto-ink);
  font-size: 13px;
}
.vrto-mqa__warning {
  margin: 0 0 16px;
  padding: 10px 14px;
  border: 1px solid var(--vrto-danger);
  border-radius: 9px;
  background: rgba(200, 52, 26, 0.05);
  font-size: 12.5px;
  line-height: 1.5;
}
.vrto-mqa__tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 16px;
}
.vrto-mqa__tabs button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
  font: inherit;
  cursor: pointer;
}
.vrto-mqa__tabs button.is-on {
  border-color: var(--vrto-ink);
  background: var(--vrto-ink);
  color: #fff;
}
.vrto-mqa__count {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}
.vrto-mqa__notice,
.vrto-mqa__error {
  margin: 0 0 12px;
  padding: 9px 14px;
  border-radius: 9px;
  font-size: 12.5px;
}
.vrto-mqa__notice {
  border: 1px solid var(--vrto-green);
  background: rgba(0, 208, 132, 0.08);
}
.vrto-mqa__error {
  border: 1px solid var(--vrto-danger);
  color: var(--vrto-danger);
  background: rgba(200, 52, 26, 0.06);
}
.vrto-mqa__card {
  padding: 20px 22px;
  border: 1px solid var(--vrto-line);
  border-radius: 12px;
  background: #fff;
}
.vrto-mqa__head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
  margin: 0 0 16px;
}
.vrto-mqa__where {
  color: var(--vrto-muted);
  font-size: 12px;
}
.vrto-mqa__exercise {
  margin: 4px 0 0;
  font-size: 20px;
}
.vrto-mqa__nav {
  display: flex;
  align-items: center;
  gap: 10px;
  font-variant-numeric: tabular-nums;
}
.vrto-mqa__nav button,
.vrto-mqa__quick button {
  padding: 5px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 7px;
  background: #fff;
  font: inherit;
  cursor: pointer;
}
.vrto-mqa__quick button.is-on {
  border-color: var(--vrto-ink);
  background: var(--vrto-ink);
  color: #fff;
}
.vrto-mqa__grid {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  gap: 24px;
}
.vrto-mqa__field {
  margin: 0 0 12px;
}
.vrto-mqa__label {
  margin: 0 0 3px;
  color: var(--vrto-muted);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.vrto-mqa__notes {
  font-style: italic;
}
.vrto-mqa__none {
  color: var(--vrto-muted);
}
.vrto-mqa__sets {
  margin: 0;
  padding-left: 18px;
}
.vrto-mqa__proposal {
  margin: 4px 0 16px;
  padding: 10px 12px;
  border-left: 3px solid var(--vrto-accent);
  border-radius: 4px;
  background: #fafafb;
  line-height: 1.5;
}
.vrto-mqa__proposal--none {
  border-left-color: #d98a00;
  background: rgba(217, 138, 0, 0.07);
}
.vrto-mqa__drift {
  margin-top: 4px;
  font-size: 12px;
}
.vrto-mqa__answer {
  padding: 14px 0 0;
  border-top: 1px solid var(--vrto-line);
}
.vrto-mqa__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 0 0 12px;
}
.vrto-mqa__row label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.vrto-mqa__row .vrto-mqa__label {
  margin: 0;
}
.vrto-mqa__quick {
  display: inline-flex;
  gap: 4px;
}
.vrto-mqa__row select,
.vrto-mqa__num {
  padding: 5px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  font: inherit;
}
.vrto-mqa__num {
  width: 56px;
}
.vrto-mqa__num[aria-invalid='true'] {
  border-color: var(--vrto-danger);
}
.vrto-mqa__hint {
  color: var(--vrto-muted);
  font-size: 11.5px;
}
.vrto-mqa__hint--warn {
  color: var(--vrto-danger);
}
.vrto-mqa__twins {
  margin: 0 0 14px;
  padding: 10px 12px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
}
.vrto-mqa__twins legend {
  padding: 0 4px;
  font-weight: 600;
}
.vrto-mqa__twin {
  display: block;
  margin: 3px 0;
}
.vrto-mqa__actions {
  display: flex;
  align-items: center;
  gap: 12px;
}
.vrto-mqa__confirm {
  display: inline-block;
  padding: 8px 16px;
  border: 0;
  border-radius: 8px;
  background: var(--vrto-ink);
  color: #fff;
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
.vrto-mqa__confirm:disabled {
  opacity: 0.4;
  cursor: default;
}
.vrto-mqa__link {
  display: inline-block;
  margin-top: 10px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--vrto-ink);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.vrto-mqa__siblings,
.vrto-mqa__done {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}
.vrto-mqa__siblings td,
.vrto-mqa__done td,
.vrto-mqa__done th {
  padding: 5px 6px;
  border-bottom: 1px solid var(--vrto-line);
  text-align: left;
}
.vrto-mqa__siblings tr.is-this {
  background: rgba(255, 98, 62, 0.1);
  font-weight: 600;
}
.vrto-mqa__flag {
  color: #b36f00;
  font-size: 11px;
}
.vrto-mqa__done tr.is-amber {
  background: rgba(217, 138, 0, 0.08);
}
.vrto-mqa__done-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  margin: 0 0 14px;
}
.vrto-mqa__empty {
  padding: 24px 0;
  color: var(--vrto-muted);
}

/* ── Insights ─────────────────────────────────────────────────────────────
 *
 * 🔴 No token block of its own. The page root is `.vrto-dashboard.vrto-insights`
 * and the landing page's strip renders inside `.vrto-dashboard`, so every
 * `var(--vrto-*)` below resolves from the dashboard root — the failure the
 * token note at the top of this file costs out is a root that declares none.
 *
 * Colour never carries a value alone: every bar prints its number and every
 * shaded cohort cell prints its percentage.
 */
.vrto-insights__tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--vrto-line);
}

.vrto-insights__tab {
  padding: 8px 14px;
  border: 0;
  border-bottom: 3px solid transparent;
  background: none;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--vrto-muted);
  cursor: pointer;
}

.vrto-insights__tab:hover,
.vrto-insights__tab:focus-visible {
  color: var(--vrto-ink);
}

.vrto-insights__tab:focus-visible {
  outline: 2px solid var(--vrto-sky);
  outline-offset: -2px;
}

.vrto-insights__tab--active {
  border-bottom-color: var(--vrto-accent);
  color: var(--vrto-ink);
}

.vrto-insights__controls {
  justify-content: flex-start;
  align-items: center;
}

.vrto-insights__segment {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 13px;
}

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

.vrto-insights__segment .vrto-select {
  min-width: 180px;
  max-width: 360px;
}

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

.vrto-insights__age {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 12px;
  color: var(--vrto-muted);
}

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

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

.vrto-insight {
  margin-top: 28px;
  max-width: 1100px;
}

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

.vrto-insight__definition,
.vrto-insight__caveat,
.vrto-insight__note {
  margin: 0 0 8px;
  max-width: 72ch;
  font-size: 12px;
  line-height: 1.45;
  color: var(--vrto-muted);
}

.vrto-insight__caveat {
  padding-left: 8px;
  border-left: 3px solid var(--vrto-yellow);
}

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

.vrto-insight__figures {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 8px;
}

.vrto-insight__figure {
  display: flex;
  flex: 0 1 220px;
  flex-direction: column;
  gap: 4px;
  padding: 14px 16px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
}

.vrto-insight__value {
  font-size: 26px;
  font-weight: 700;
  line-height: 1.1;
}

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

.vrto-insight__figure .vrto-insight__note {
  margin: 0;
}

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

/* Polarity, declared by the figure — never inferred from the arrow. */
.vrto-insight__delta--good {
  color: #0a7d4f;
}

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

.vrto-insight__bars {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}

.vrto-insight__bar {
  display: grid;
  grid-template-columns: minmax(140px, 280px) minmax(80px, 1fr) auto;
  align-items: center;
  gap: 10px;
  font-size: 13px;
}

.vrto-insight__bar-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vrto-insight__bar-track {
  height: 10px;
  border-radius: 5px;
  background: var(--vrto-line);
  overflow: hidden;
}

.vrto-insight__bar-fill {
  display: block;
  height: 100%;
  background: var(--vrto-sky);
}

.vrto-insight__bar-value {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

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

.vrto-insight__table-wrap {
  margin-top: 8px;
  overflow-x: auto;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: #fff;
}

.vrto-insight__table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.vrto-insight__table th,
.vrto-insight__table td {
  padding: 6px 10px;
  border-bottom: 1px solid var(--vrto-line);
  text-align: right;
  white-space: nowrap;
}

.vrto-insight__table th[scope='row'],
.vrto-insight__table thead th:first-child {
  text-align: left;
}

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

.vrto-insight__table tbody tr:last-child > * {
  border-bottom: 0;
}

.vrto-insight__cohorts td.vrto-insight__cell {
  text-align: center;
  color: var(--vrto-ink);
}

.vrto-glossary {
  margin: 8px 0 0;
}

.vrto-glossary__term {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
  font-size: 13px;
  font-weight: 600;
}

.vrto-glossary__status,
.vrto-glossary__req {
  padding: 1px 6px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  background: var(--vrto-line);
  color: var(--vrto-ink);
}

.vrto-glossary__status--awaiting-decision {
  background: var(--vrto-yellow);
}

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

.vrto-glossary__detail p {
  margin: 0 0 4px;
  max-width: 80ch;
}

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

/* Wave 9 — the outline's Details panel. Closed, it is one line: the length the
   app will use and where it came from (§9.4). Open, the variant's form and the
   programme's sit side by side, because the difference between them — one has
   a draft stage, one does not — is the thing to read at a glance. */
.vrto-builder__meta-head {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  align-items: center;
}
.vrto-builder__meta-head > h3 {
  margin: 0;
}
.vrto-builder__meta-length {
  flex: 1 1 320px;
  margin: 0;
}
.vrto-builder__meta-forms {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 24px;
  margin-top: 12px;
}
.vrto-builder__meta-forms h4 {
  margin: 0;
}
.vrto-builder__meta-forms textarea {
  font: inherit;
}

/* ── Challenge judging ──────────────────────────────────────────────────────
 *
 * Challenges → Seasons → show → Judge (US-C05). A list on the left, one card
 * on the right. Every colour is a token from the block at the top of this file.
 *
 * 🔴 The photographs are the loudest thing on the card, and the two in a pair
 * are the SAME size and aspect: §6 judges "overall visible change", and a
 * finish photo drawn larger than its start photo reads as more change.
 */
.vrto-judge {
  padding: 24px 24px 64px;
  color: var(--vrto-ink);
}

.vrto-judge__lede,
.vrto-judge__hint {
  margin: 4px 0 0;
  color: var(--vrto-muted);
  font-size: 13px;
  line-height: 1.5;
}

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

.vrto-judge__layout {
  display: grid;
  grid-template-columns: minmax(240px, 320px) 1fr;
  gap: 24px;
  margin-top: 20px;
  align-items: start;
}

.vrto-judge__list {
  position: sticky;
  top: 16px;
  max-height: calc(100vh - 120px);
  overflow-y: auto;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  padding: 12px;
}

.vrto-judge__filters {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.vrto-judge__filters button {
  padding: 4px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 999px;
  background: #fff;
  color: var(--vrto-ink);
  font-size: 13px;
  cursor: pointer;
}

.vrto-judge__filters button[aria-pressed='true'] {
  border-color: var(--vrto-ink);
  background: var(--vrto-ink);
  color: #fff;
}

.vrto-judge__toggle {
  display: block;
  margin: 10px 0;
  color: var(--vrto-muted);
  font-size: 13px;
}

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

.vrto-judge__list li button {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 8px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--vrto-ink);
  text-align: left;
  cursor: pointer;
}

.vrto-judge__list li button:hover,
.vrto-judge__list li button[aria-current='true'] {
  background: #f7f7f9;
}

.vrto-judge__list li button[aria-current='true'] {
  box-shadow: inset 3px 0 0 var(--vrto-sky);
}

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

.vrto-judge__card h2 {
  margin: 0;
  font-size: 20px;
}

.vrto-judge__card h3 {
  margin: 24px 0 8px;
  font-size: 14px;
}

.vrto-judge__badges {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 8px 0 0;
}

.vrto-judge__badges span {
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 12px;
}

.vrto-judge__ok {
  background: #e6faf2;
  color: var(--vrto-deep);
}

.vrto-judge__no {
  background: #fff5f2;
  color: var(--vrto-danger);
}

.vrto-judge__award {
  background: var(--vrto-yellow);
  color: var(--vrto-ink);
}

.vrto-judge__pairs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 16px;
}

.vrto-judge__pair {
  margin: 0;
}

.vrto-judge__photos {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
}

.vrto-judge__photos img,
.vrto-judge__missing {
  width: 100%;
  aspect-ratio: 3 / 4;
  border-radius: 4px;
  object-fit: cover;
  background: #f7f7f9;
}

.vrto-judge__missing {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--vrto-muted);
  font-size: 12px;
}

.vrto-judge__pair figcaption {
  margin-top: 4px;
  color: var(--vrto-muted);
  font-size: 12px;
  text-transform: capitalize;
}

.vrto-judge__videos {
  display: flex;
  gap: 12px;
  margin-top: 8px;
  font-size: 13px;
}

.vrto-judge__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 24px;
}

.vrto-judge__row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 5px 0;
  border-bottom: 1px dotted #e3e3e8;
  font-size: 13px;
}

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

.vrto-judge__testimonial {
  margin: 0;
  font-size: 14px;
  line-height: 1.6;
  white-space: pre-wrap;
}

.vrto-judge__footer {
  margin-top: 32px;
  padding-top: 16px;
  border-top: 1px solid var(--vrto-line);
}

.vrto-judge__footer button {
  margin-right: 8px;
  padding: 8px 14px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  background: #f7f7f9;
  color: var(--vrto-muted);
  font-size: 13px;
}

.vrto-judge__footer button:disabled {
  cursor: not-allowed;
}

/* ---------------------------------------------------------------------------
 * Cutover → Exercise editing — `vrto-ee`
 *
 * `admin/docs/plans/exercise-catalogue-editing-plan.md` §8. Named in the token
 * block near the top of this file, in the same edit that created it.
 * ------------------------------------------------------------------------- */

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

.vrto-ee--busy {
  opacity: 0.7;
}

.vrto-ee__lede,
.vrto-ee__note,
.vrto-ee__facts {
  margin: 0 0 14px;
  max-width: 70ch;
  color: var(--vrto-muted);
  font-size: 13px;
  line-height: 1.55;
}

.vrto-ee__facts {
  font-variant-numeric: tabular-nums;
}

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

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

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

.vrto-ee__warn {
  border-left: 3px solid var(--vrto-accent);
  background: #fff6f2;
}

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

.vrto-ee__h {
  margin: 0 0 8px;
  font-size: 16px;
}

.vrto-ee__h3 {
  margin: 22px 0 6px;
  font-size: 14px;
}

.vrto-ee__row,
.vrto-ee__grid,
.vrto-ee__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 0 0 14px;
}

.vrto-ee__field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 180px;
  font-size: 12px;
  color: var(--vrto-muted);
}

.vrto-ee__field--wide {
  width: 100%;
  margin: 0 0 14px;
}

.vrto-ee__field input,
.vrto-ee__field select,
.vrto-ee__row > input,
.vrto-ee__row > select {
  padding: 6px 8px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  font-size: 13px;
  color: var(--vrto-ink);
}

.vrto-ee__lock {
  color: var(--vrto-accent);
  font-style: normal;
}

.vrto-ee__sites {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  margin: 0 0 14px;
  padding: 10px 12px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  font-size: 13px;
}

.vrto-ee__primary,
.vrto-ee__danger,
.vrto-ee__actions button {
  padding: 7px 14px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  background: #fff;
  font-size: 13px;
  cursor: pointer;
}

/* Two classes, so these beat `.vrto-ee__actions button` (a class and an
 * element) — at equal footing the white base rule painted Save white on white. */
.vrto-ee .vrto-ee__primary {
  border-color: var(--vrto-accent);
  background: var(--vrto-accent);
  color: #fff;
}

.vrto-ee .vrto-ee__danger {
  border-color: var(--vrto-danger);
  color: var(--vrto-danger);
}

.vrto-ee button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ── Recipe catalogue (`components/recipe-catalogue.ts`) ───────────────── */
.vrto-cat__counts,
.vrto-cat__filters,
.vrto-cat__bulk {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 12px 0;
}
.vrto-cat__count,
.vrto-cat__filters button,
.vrto-cat__bulk button {
  padding: 5px 12px;
  border: 1px solid var(--vrto-line);
  border-radius: 7px;
  background: #fff;
  color: var(--vrto-ink);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.vrto-cat__count--on {
  border-color: var(--vrto-ink);
  background: var(--vrto-ink);
  color: #fff;
}
.vrto-cat__filters button:disabled,
.vrto-cat__bulk button:disabled {
  color: var(--vrto-muted);
  cursor: not-allowed;
}
.vrto-cat__filters input[type='search'] {
  min-width: 220px;
  padding: 5px 9px;
  border: 1px solid var(--vrto-line);
  border-radius: 7px;
  font: inherit;
}
.vrto-cat__bulk {
  padding: 8px 10px;
  border: 1px solid var(--vrto-line);
  border-radius: 8px;
  background: var(--vrto-surface);
}
.vrto-cat__table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
.vrto-cat__table th,
.vrto-cat__table td {
  padding: 6px 8px;
  border-bottom: 1px solid var(--vrto-line);
  text-align: left;
  vertical-align: middle;
}
.vrto-cat__table th {
  color: var(--vrto-muted);
  font-weight: 600;
  font-size: 12px;
}
.vrto-cat__num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.vrto-cat__tick {
  text-align: center;
}
.vrto-cat__meals {
  color: var(--vrto-muted);
}
.vrto-cat__row--retired .vrto-cat__name a {
  color: var(--vrto-muted);
  text-decoration: line-through;
}
.vrto-cat__badge {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--vrto-danger);
  color: #fff;
  font-size: 11px;
}
.vrto-cat__badge--quiet {
  background: var(--vrto-line);
  color: var(--vrto-ink);
}
.vrto-amt__vendor {
  margin-top: 28px;
  padding-top: 16px;
  border-top: 1px solid var(--vrto-line);
}
.vrto-amt__vendor h3 {
  margin: 0 0 4px;
  font-size: 14px;
}
.vrto-cat__editor {
  margin: 16px 0;
  padding: 12px 14px;
  border: 1px solid var(--vrto-ink);
  border-radius: 10px;
  background: #fff;
}
.vrto-cat__editor input {
  max-width: 120px;
  padding: 4px 7px;
  border: 1px solid var(--vrto-line);
  border-radius: 6px;
  font: inherit;
}
.vrto-cat__editor input[type='search'] {
  max-width: none;
  min-width: 260px;
}
