/* ⚠ COPY of apps/forms-web/public/forms.css — kept here so the Studio builder's preview iframe
 * renders the real vf-* form theme in fixture mode (PreviewPane's formsCssUrl). This is a verbatim
 * duplicate; if the canonical forms-web theme changes, re-copy it. (A shared build step is the
 * eventual fix — copy is the pragmatic MVP. See the project-form-builder-status memory.) */
:root {
  --vf-color-primary: oklch(0.55 0.22 264);
  --vf-color-surface: oklch(0.99 0 0);
  --vf-color-text: oklch(0.205 0 0);
  --vf-color-muted: oklch(0.55 0 0);
  --vf-color-danger: oklch(0.52 0.2 25);
  --vf-color-success: oklch(0.6 0.15 150);
  --vf-color-warning: oklch(0.65 0.16 75);
  --vf-font-family: system-ui, sans-serif;
  --vf-font-size-base: 1rem;
  --vf-line-height-base: 1.5;
  --vf-radius-sm: 0.25rem;
  --vf-radius-md: 0.5rem;
  --vf-radius-lg: 0.75rem;
  --vf-spacing-unit: 0.25rem;
  --vf-border-width: 1px;
  --vf-shadow-md: 0 1px 3px oklch(0 0 0 / 0.1);
}

/* Dark colour tokens. Applied automatically when the OS prefers dark (unless a
 * scheme is forced to light), AND on demand via :root[data-vf-scheme="dark"] for
 * the styleguide's Light/Dark/Auto toggle. The two blocks MUST carry identical
 * colour values — drift-guarded by test/forms-css.test.ts. shadow_md is
 * scheme-independent (single :root value above) so the Style axis can override
 * it via a plain :root without losing to the forced-scheme selector. */
@media (prefers-color-scheme: dark) {
  :root:not([data-vf-scheme="light"]) {
    --vf-color-primary: oklch(0.72 0.16 264);
    --vf-color-surface: oklch(0.18 0 0);
    --vf-color-text: oklch(0.97 0 0);
    --vf-color-muted: oklch(0.72 0 0);
    --vf-color-danger: oklch(0.72 0.18 25);
    --vf-color-success: oklch(0.74 0.14 150);
    --vf-color-warning: oklch(0.8 0.15 75);
  }
}

:root[data-vf-scheme="dark"] {
  --vf-color-primary: oklch(0.72 0.16 264);
  --vf-color-surface: oklch(0.18 0 0);
  --vf-color-text: oklch(0.97 0 0);
  --vf-color-muted: oklch(0.72 0 0);
  --vf-color-danger: oklch(0.72 0.18 25);
  --vf-color-success: oklch(0.74 0.14 150);
  --vf-color-warning: oklch(0.8 0.15 75);
}

body {
  font-family: var(--vf-font-family);
  font-size: var(--vf-font-size-base);
  line-height: var(--vf-line-height-base);
  color: var(--vf-color-text);
  background: var(--vf-color-surface);
  max-width: 40rem;
  margin: 2rem auto;
  padding: 0 1rem;
}

/* Anchors take the AA-tuned --vf-color-primary instead of the UA-default browser blue, which fails
   WCAG AA contrast on the dark surface (1.99:1). `:where()` gives it specificity ZERO on purpose:
   origin still beats the UA sheet, but ANY author rule wins — which matters because this stylesheet
   is vendored into a document (the studio preview iframe) rather than owning it. See the same rule,
   with the full rationale, in apps/forms-web/public/forms.css. */
:where(a) {
  color: var(--vf-color-primary);
}

/* The focus-ring default, same argument one criterion over: this used to be an ENUMERATED selector
   list here, so anything focusable outside the list fell back to the UA ring, and a new control
   silently landed outside it. As a default, a new control is covered the moment it exists. */
:where(:focus-visible) {
  outline: 2px solid var(--vf-color-primary);
  outline-offset: 2px;
}

/* Conditional logic + SSR start-hidden rely on the `hidden` attribute. Author rules like
   `.vf-field { display: flex }` below override the UA `[hidden] { display: none }` by
   specificity, so `visible_if`-hidden fields would stay visible. This reset wins by
   `!important` regardless of source order (incl. layered override themes). */
[hidden] {
  display: none !important;
}

.vf-field {
  margin-bottom: calc(var(--vf-spacing-unit) * 6);
  display: flex;
  flex-direction: column;
  gap: var(--vf-spacing-unit);
}

.vf-label {
  /* 14px on the 16px base — labels step below the input text (the verified reference band);
     inputs stay at 1em/16px (the iOS no-zoom floor). em (not rem) so an imported
     font_size_base override scales the whole hierarchy. */
  font-size: 0.875em;
  font-weight: 600;
  color: var(--vf-color-text);
}

.vf-field input,
.vf-field textarea,
.vf-field select {
  /* border-box so padding is INSIDE min-height (the control is exactly min-height tall, not
     min-height + padding) and the field never overflows its grid cell. */
  box-sizing: border-box;
  padding: calc(var(--vf-spacing-unit) * 2) calc(var(--vf-spacing-unit) * 3);
  font: inherit;
  color: var(--vf-color-text);
  background: var(--vf-color-surface);
  border: var(--vf-border-width) solid var(--vf-color-muted);
  border-radius: var(--vf-radius-md);
  min-height: calc(var(--vf-spacing-unit) * 10);
}

.vf-help {
  /* 13px on the 16px base — help steps below the 14px label. */
  font-size: 0.8125em;
  color: var(--vf-color-muted);
}

.vf-field--unsupported {
  color: var(--vf-color-danger);
}

.vf-field-error {
  color: var(--vf-color-danger);
  font-size: 0.8125em;
}

.vf-required {
  color: var(--vf-color-danger);
}

/* Composite fields (name/address/composite) render as a <fieldset> + visible <legend> group
   label, with the scalar sub-fields laid out in a responsive grid (.vf-composite-grid). The
   fieldset's UA chrome (border + notched box) is reset so the group reads as a labelled row of
   fields. Each sub-input keeps its own top-aligned <label> and programmatic association; the
   <legend> names the group for assistive tech AND is shown on screen. A sub-field shares a row only
   when it opts in to a fractional width via data-vf-col="num/den" (set inline as --vf-col); absent =
   its own full-width line. The row collapses to one column on narrow viewports (mobile reflow / 200%
   zoom safe). */
.vf-composite {
  display: block;
  border: 0;
  padding: 0;
  margin-inline: 0;
  min-inline-size: 0;
}
.vf-legend {
  font-weight: 600;
  color: var(--vf-color-text);
  margin-bottom: var(--vf-spacing-unit);
  /* UA gives <legend> ~2px horizontal padding; zero it so the group label aligns flush-left
     with the fields/labels below (the fieldset chrome reset above doesn't cover the legend). */
  padding-inline: 0;
}
/* A field row: fields flow and wrap; each opts into a fractional width via --vf-col (set inline by
   the renderer from data-vf-col="num/den"); absent = full row. A field with data-vf-break starts a
   new row even if it would fit. Mobile-first: single column until 30rem. The SAME contract powers
   three contexts: a composite's sub-field grid, a repeating-group row, AND the top-level form body
   (.vf-form-row — renderFormFields wraps the whole flow), so form-level multi-column needs no new
   rules — a top-level field with data-vf-col shares a row with its siblings. */
.vf-composite-grid,
.vf-rg-row,
.vf-form-row {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--vf-spacing-unit) * 4);
}
/* The top-level form body keeps its 1.5rem vertical field rhythm (column-gap stays the tight 1rem
   shared row gap for side-by-side columns). Composite sub-fields and repeating-group rows are tight
   intra-group rows and keep the shared 1rem gap in both axes — so this override is form-body only. */
.vf-form-row {
  row-gap: calc(var(--vf-spacing-unit) * 6);
}
.vf-composite-grid > .vf-field,
.vf-rg-row > .vf-field,
.vf-form-row > .vf-field {
  flex: 1 1 100%;
  /* Let a sized child actually shrink to its flex-basis. Without this, min-width:auto floors each
     field at its input's intrinsic width, so e.g. three thirds overflow by a hair and the last wraps. */
  min-width: 0;
}
@media (min-width: 30rem) {
  .vf-composite-grid > .vf-field[data-vf-col],
  .vf-rg-row > .vf-field[data-vf-col],
  .vf-form-row > .vf-field[data-vf-col] {
    /* subtract the gap share so N siblings actually fit on one row */
    flex: 0 1 calc(var(--vf-col) - var(--vf-spacing-unit) * 4);
  }
}
.vf-composite-grid > .vf-field[data-vf-break],
.vf-rg-row > .vf-field[data-vf-break],
.vf-form-row > .vf-field[data-vf-break] {
  flex-basis: 100%;
}
.vf-composite-grid > .vf-field,
.vf-rg-row > .vf-field,
.vf-form-row > .vf-field {
  margin-bottom: 0; /* the row gap owns within-row spacing */
}
/* Sub-field labels are quiet captions SUBORDINATE to the group <legend>: lighter weight + muted +
   slightly smaller, so "First name"/"City" don't compete with the group label. Mirrors the
   reference's sub-label hierarchy (weight + colour do the work, not just size). Kept muted (not the
   full --vf-color-muted-only trick): --vf-color-muted clears WCAG AA (≥4.5:1) on the surface in both
   schemes at this size, and the visible meaning is reinforced by the input's own programmatic label. */
.vf-composite-grid .vf-label {
  font-size: 0.8125em; /* 13px — one step below the 14px field labels; size reinforces the weight+colour hierarchy */
  font-weight: 400;
  color: var(--vf-color-muted);
}

/* A `daterange` renders as <fieldset class="vf-field vf-daterange"> + <legend class="vf-legend"> +
   two <div class="vf-daterange-part"> (each a <label for> + an <input type=date>). It reuses the
   composite substrate rather than inventing one: the fieldset resets the UA chrome exactly like
   .vf-composite, and .vf-daterange-parts is the same wrapping fractional flex row, so the two ends
   sit side by side on a wide viewport and stack below 30rem (mobile reflow / 200% zoom safe). The
   two ends are equal halves — a date range has no primary end — so they are sized here rather than
   through data-vf-col, which is an AUTHOR affordance and a daterange has no sub-fields to author.
   "Start"/"End" are the same subordinate captions a composite's sub-labels are, for the same reason:
   they must not compete with the <legend> that carries the question. No new colour is introduced. */
.vf-daterange {
  display: block;
  border: 0;
  padding: 0;
  margin-inline: 0;
  min-inline-size: 0;
}
.vf-daterange-parts {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--vf-spacing-unit) * 4);
}
.vf-daterange-parts > .vf-daterange-part {
  flex: 1 1 100%;
  min-width: 0;
}
@media (min-width: 30rem) {
  .vf-daterange-parts > .vf-daterange-part {
    flex: 1 1 calc(50% - var(--vf-spacing-unit) * 4);
  }
}
.vf-daterange-parts .vf-label {
  font-size: 0.8125em;
  font-weight: 400;
  color: var(--vf-color-muted);
}

/* Repeating groups (<fieldset class="vf-field vf-rg"> + <legend class="vf-legend">). The group
   and each row reset the UA fieldset chrome (like .vf-composite). A row is the same wrapping
   fractional flex row as a composite, so a row field opts into a width via data-vf-col (rules above);
   default = full-width line. The Remove control sits on its own full-width line at the row foot. */
.vf-rg {
  display: block;
  border: 0;
  padding: 0;
  margin-inline: 0;
  min-inline-size: 0;
}
.vf-rg-rows {
  display: flex;
  flex-direction: column;
  row-gap: calc(var(--vf-spacing-unit) * 4);
}
.vf-rg-row {
  border: 0;
  margin: 0;
  min-inline-size: 0;
  padding: calc(var(--vf-spacing-unit) * 3);
  border-radius: var(--vf-radius-md);
  background: var(--vf-color-surface);
  box-shadow: inset 0 0 0 var(--vf-border-width) var(--vf-color-muted);
}
/* forced-colors / High Contrast Mode drops box-shadow → give the row a real border the OS
   promotes to a system colour, so rows stay visually separated (a11y: WCAG 1.4.11-adjacent). */
@media (forced-colors: active) {
  .vf-rg-row {
    border: var(--vf-border-width) solid;
  }
}
/* The Remove button trails the row's fields, content-sized and pushed to the right edge. With the
   default full-width fields it lands on its own line; with fractional fields it right-aligns in the
   last line's free space — the flex equivalent of the old grid-column:1/-1; justify-self:end. */
.vf-rg-row > .vf-rg-remove {
  flex: 0 0 auto;
  margin-left: auto;
  align-self: center; /* if it shares the last line with a taller field, don't stretch it */
}
/* Add/Remove are secondary actions, not the form's submit primary: outline, token-derived. */
.vf-rg-add,
.vf-rg-remove {
  color: var(--vf-color-primary);
  background: var(--vf-color-surface);
  border: var(--vf-border-width) solid var(--vf-color-muted);
}
.vf-rg-add {
  margin-top: calc(var(--vf-spacing-unit) * 2);
}
.vf-rg-remove {
  color: var(--vf-color-danger);
}

/* Repeating-group TABLE layout (presentation.layout:"table") — the invoice / line-items renderer.
   The group is <fieldset class="vf-field vf-rg vf-rg--table"> + <legend>, holding
   <table class="vf-rg-table"> with a <th scope=col> per row field + an Actions column; each row is a
   <tr data-vf-rg-row>, each control in a <td> wrapping the same .vf-field markup stacked uses. On wide
   viewports the column <th> is the visual header and the per-cell <label> is clipped (present for AT).
   Below the breakpoint the table reflows to one card per row: thead hidden, tr/td become blocks, and
   each per-cell <label> is un-clipped so it becomes the on-screen "Field:" label — the same element
   that is the control's accessible name (no data-label/::before duplication, no drift). */
.vf-rg-table {
  width: 100%;
  border-collapse: collapse;
  /* fixed layout: the table fits its container (the ≤40rem form column) and distributes columns,
     instead of expanding to the inputs' intrinsic widths and overflowing the form. */
  table-layout: fixed;
}
.vf-rg-table th {
  text-align: start;
  font-size: 0.875em;
  font-weight: 600;
  color: var(--vf-color-text);
  padding: calc(var(--vf-spacing-unit) * 2);
  border-bottom: var(--vf-border-width) solid var(--vf-color-muted);
}
.vf-rg-table td {
  padding: calc(var(--vf-spacing-unit) * 2);
  vertical-align: top;
}
.vf-rg-table .vf-field {
  margin-bottom: 0; /* the cell padding owns spacing; no stacked-field bottom margin inside a cell */
}
/* fixed layout distributes columns; let each control shrink to its cell (overriding the input's
   intrinsic min-width) so it never forces the column wider than its share. */
.vf-rg-table td .vf-field input,
.vf-rg-table td .vf-field select,
.vf-rg-table td .vf-field textarea {
  min-width: 0;
}
/* the Actions column is just the × button — pin it narrow so the data columns get the room, and
   compact the button's wide (Add-row-sized) padding down to a square tap target. */
.vf-rg-table th:last-child {
  width: calc(var(--vf-spacing-unit) * 16);
}
.vf-rg-table .vf-rg-actions .vf-rg-remove {
  padding-inline: calc(var(--vf-spacing-unit) * 3); /* compact vs Add-row, but keep a comfortable tap target */
}
/* Wide: the column <th> names each control visually, so clip the per-cell <label> (AT only). Uses the
   same clip technique as .vf-sr-only, scoped to table cells (the markup keeps a plain .vf-label so the
   renderer is not forked). It is un-clipped in the card media query below. */
.vf-rg-table td .vf-label {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.vf-rg-table .vf-rg-actions {
  text-align: end;
  white-space: nowrap;
}

/* <tfoot> totals (presentation.footer) — welded TOP-LEVEL fields, one <tr> per total: a
   <th scope=row> holding the real <label> (visible row header AND the control's programmatic
   name) spanning all but the last data column, then the value cell under the final column — the
   invoice look. Right-aligned per design §6.1. A real border-top separates totals from the row
   body (survives forced-colors, unlike box-shadow). tfoot row headers drop the thead underline. */
.vf-rg-table tfoot {
  border-top: var(--vf-border-width) solid var(--vf-color-muted);
}
.vf-rg-table tfoot th {
  border-bottom: 0;
}
/* scoped under .vf-rg-table so the end-alignment beats the base `.vf-rg-table th` start rule */
.vf-rg-table .vf-rg-foot-label {
  text-align: end;
}
.vf-rg-table .vf-rg-foot-value {
  text-align: end;
  font-variant-numeric: tabular-nums; /* totals digits align down the column */
}
/* A footer row is a mirror container ([data-field-id]): visible_if hides it via the `hidden`
   attribute. The card-collapse `tr { display:… }` rules below would override the UA's [hidden]
   display:none and re-reveal it — pin [hidden] above any display the layout sets. */
.vf-rg-table tr[hidden] {
  display: none;
}

/* Card collapse — below the breakpoint each row becomes a card and each cell stacks with its (now
   visible) label. Reflow-safe to 320px / 200% zoom (WCAG 1.4.10); no horizontal-scroll trap. */
@media (max-width: 40rem) {
  .vf-rg-table thead {
    display: none; /* the per-cell <label> carries the name in card mode, so headers are removed */
  }
  .vf-rg-table,
  .vf-rg-table tbody,
  .vf-rg-table tr,
  .vf-rg-table td {
    display: block;
    width: auto;
  }
  .vf-rg-table tr {
    /* each row = a card, matching the Slice A .vf-rg-row chrome (token-derived). */
    padding: calc(var(--vf-spacing-unit) * 3);
    margin-bottom: calc(var(--vf-spacing-unit) * 4);
    border-radius: var(--vf-radius-md);
    background: var(--vf-color-surface);
    box-shadow: inset 0 0 0 var(--vf-border-width) var(--vf-color-muted);
  }
  .vf-rg-table td {
    padding: 0;
    margin-bottom: calc(var(--vf-spacing-unit) * 3);
  }
  .vf-rg-table td .vf-label {
    /* un-clip → the per-cell label becomes the visible "Field:" label in card mode. */
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    clip: auto;
    overflow: visible;
    white-space: normal;
  }
  .vf-rg-table .vf-rg-actions {
    text-align: start;
    margin-bottom: 0;
  }
  /* tfoot totals collapse to one summary line per total — label left, value right — inside the
     same card chrome the body rows get (the `tr` card rule above also matches tfoot rows). The
     empty actions placeholder cell collapses to nothing in the flex row. */
  .vf-rg-table tfoot {
    display: block;
  }
  .vf-rg-table tfoot tr {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: calc(var(--vf-spacing-unit) * 2);
  }
  .vf-rg-table tfoot th {
    padding: 0;
  }
  .vf-rg-table tfoot td {
    margin-bottom: 0;
  }
}

/* forced-colors / High Contrast Mode drops box-shadow → give the CARDS a real border the OS promotes
   to a system colour (matches the Slice A .vf-rg-row fallback). Card mode only, which is where the row
   is bounded by an inset shadow and nothing else.
   ⚠ A `.vf-rg-table th { border-bottom }` rule lived here and was DELETED 2026-07-27 as dead: the base
   `th` rule already declares a border-bottom, and forced colors overrides a border's COLOUR but not its
   style or width, so the boundary survived without help. It could not be falsified — deleting it
   changed no rendering — and an unfalsifiable rule reads as coverage it does not provide. `tfoot th`
   out-specified it anyway (`border-bottom: 0`), so it never applied there either. */
@media (forced-colors: active) {
  .vf-rg-table tr {
    border: var(--vf-border-width) solid;
  }
}

.vf-error-summary {
  border: calc(var(--vf-border-width) * 2) solid var(--vf-color-danger);
  border-radius: var(--vf-radius-md);
  padding: calc(var(--vf-spacing-unit) * 3) calc(var(--vf-spacing-unit) * 4);
  margin-bottom: calc(var(--vf-spacing-unit) * 6);
}

/* The summary's heading is an <h3> in the per-instance VfForm (page h1 → form-title h2 →
   summary h3); kept level-agnostic so a future heading-level change can't silently unstyle it. */
.vf-error-summary h2,
.vf-error-summary h3 {
  margin: 0 0 calc(var(--vf-spacing-unit) * 2);
  font-size: 1em;
}

.vf-error-summary ul {
  margin: 0;
  padding-left: 1.25rem;
}

.vf-error-summary a {
  color: var(--vf-color-danger);
}

button {
  padding: calc(var(--vf-spacing-unit) * 2.4) calc(var(--vf-spacing-unit) * 4.8);
  min-height: calc(var(--vf-spacing-unit) * 10);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  color: var(--vf-color-surface);
  background: var(--vf-color-primary);
  border: none;
  border-radius: var(--vf-radius-sm);
}

/* forced-colors / High Contrast Mode: give back the boundary of every button whose affordance is
   carried by `background` ALONE. See the identical rule in apps/forms-web/public/forms.css for the
   full reasoning — HCM overrides `background-color`/`color` but not `border-style`, so `border: none`
   above leaves a button rendering as bare bold text (a 1.4.11 failure, no secondary cue).
   ⚠ THIS FILE IS A DRIFTED SECOND COPY of the forms-web stylesheet (different content hash, and a
   1155-line subset of its 1751). Fixing only the forms-web original leaves the studio preview surface
   broken, which is why this rule is duplicated rather than shared. The datecell/richtext overrides
   that accompany it there have no counterpart here because this copy carries no rules for those
   widgets at all. */
@media (forced-colors: active) {
  button:not(.vf-datecell):not(.vf-review-edit) {
    border: var(--vf-border-width) solid ButtonText;
  }
}

.vf-notice {
  padding: calc(var(--vf-spacing-unit) * 4);
  background: var(--vf-color-surface);
  border-left: calc(var(--vf-border-width) * 3) solid var(--vf-color-warning);
  border-radius: var(--vf-radius-lg);
  box-shadow: var(--vf-shadow-md);
}

.vf-confirmation {
  border-left-color: var(--vf-color-success);
}

/* Empty-state chrome suppression for the ALWAYS-PRESENT live regions.
   A live region only announces mutations it is in the accessibility tree to observe, so the
   confirmation and the multipage nav error ship rendered-and-empty instead of `hidden` (see
   `render-confirmation.ts` and `render-multipage.ts`). That makes the empty state visible, so it
   must cost nothing: `.vf-notice` otherwise draws padding, a surface fill, a 3x accent border and a
   shadow on every form that has not been submitted yet.
   ⚠ Suppress the CHROME, never the BOX. `display: none` or `visibility: hidden` here would take the
   region back out of the accessibility tree and recreate the exact write-into-an-unobserved-region
   bug in CSS — silently, and one layer further from where anyone would look for it. `:empty` is the
   right hook precisely because the chrome returns the instant content is written. */
.vf-confirmation:empty {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}

.vf-mp-error:empty {
  margin: 0;
}

/* Editable entry fields (workflow human-task write-back: the step's edit_entry.fields). These are
   the ENTRY's own fields the assignee may change — visually distinguished from the read-only view
   projections and the step-private inputs (view.highlight_writable). The distinction is carried by
   the visible <legend> ("Editable details") AND a structural accent (left border + faint tint), NOT
   by colour alone — assistive tech reads the group label, sighted users get both (a11y-first). The
   fieldset chrome is reset to the app look (like .vf-composite) before the accent is layered on. */
.vf-edit-entry {
  display: block;
  border: 0;
  min-inline-size: 0;
  margin-block: calc(var(--vf-spacing-unit) * 4) 0;
  padding: calc(var(--vf-spacing-unit) * 3) calc(var(--vf-spacing-unit) * 4);
  border-inline-start: calc(var(--vf-border-width) * 3) solid var(--vf-color-primary);
  border-radius: var(--vf-radius-md);
  /* Faint primary-tinted surface — low alpha so field controls keep their own contrast. */
  background: color-mix(in oklch, var(--vf-color-primary) 6%, var(--vf-color-surface));
}
.vf-edit-entry > .vf-legend {
  /* The group label carries the meaning; make it read as a section heading, not a field label. */
  font-size: 0.9375em;
  color: var(--vf-color-text);
}
/* Each writable field's wrapper. No extra chrome by default (the fieldset already frames the group);
   the hook exists for per-field affordances and as a stable mount target. The last field clears its
   bottom margin so the fieldset padding owns the trailing gap. */
.vf-edit-entry > .vf-writable:last-child > .vf-field {
  margin-bottom: 0;
}

/* (The field/button focus ring used to live here. It is the `:where(:focus-visible)` default at the
   top of this sheet now — same declarations, no list to fall outside of.) */

/* Save & continue later (save-and-resume) */
.vf-save {
  margin-top: 1rem;
}
.vf-save-trigger {
  background: none;
  border: 0;
  padding: 0;
  min-height: 0; /* shed the global button min-height so this reads as an inline link */
  color: var(--vf-color-primary);
  text-decoration: underline;
  cursor: pointer;
  font: inherit;
}
.vf-save-panel {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: end;
  margin-top: 0.75rem;
}
.vf-save-panel input {
  flex: 1 1 16rem;
}
.vf-save-status:not(:empty) {
  margin-top: 0.5rem;
  font-size: 0.9em;
}

/* Visually-hidden utility (the standard clip technique for accessible hidden content: present for
   screen readers, invisible on screen, NOT display:none). Global (not page-scoped) so it reaches
   elements rendered inside child components — e.g. the conversational live region
   (.vf-live.vf-sr-only). The composite group label (.vf-legend) is NO LONGER in this group: it is
   now shown on screen as a visible heading above its row (see the composite section above). */
.vf-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Conversational (one-field-per-screen) channel. */
.vf-conversation {
  margin: 0 auto;
}

.vf-progress {
  height: 0.375rem;
  background: var(--vf-color-muted);
  border-radius: 999px;
  overflow: hidden;
  margin-bottom: 2rem;
}
.vf-progress-bar {
  height: 100%;
  width: 0%;
  background: var(--vf-color-primary);
  transition: width 240ms ease;
}

.vf-screen {
  padding: 1rem 0;
}

/* Big, prominent question per screen — the screen's own (top-level) field label. Child-scoped to
   `.vf-screen >` so it does NOT hit a composite's sub-field labels (First/Last name etc.), which
   keep their smaller caption size, nor the save-panel's own .vf-label. A composite screen's big
   question is its group label (the now-visible <legend>) — styled by the rule just below — while
   the sub-fields keep the standard label hierarchy. rem (not em) is deliberate here: the prompt is
   a fixed enlargement, independent of the field type scale. */
.vf-conversation .vf-screen > .vf-field > .vf-label {
  display: block;
  font-size: 1.5rem;
  line-height: 1.3;
  margin-bottom: 1rem;
  font-weight: 600;
}
/* A composite screen's big question is its (now-visible) group legend. Mirror the single-field
   label sizing so the group label reads as the prompt; the sub-field labels keep their smaller
   caption size. */
.vf-conversation .vf-screen > .vf-composite > .vf-legend {
  font-size: 1.5rem;
  line-height: 1.3;
  margin-bottom: 1rem;
}
.vf-conversation .vf-field input,
.vf-conversation .vf-field select,
.vf-conversation .vf-field textarea {
  font-size: var(--vf-font-size-base); /* lock to base — conversational controls are not enlarged */
}

.vf-review-title {
  font-size: 1.25rem;
}
.vf-review-list dt {
  font-weight: 600;
  margin-top: 0.75rem;
}
.vf-review-list dd {
  margin: 0 0 0.25rem 0;
}

/* The review screen's edit affordance. It is a real <button> — it performs an in-page action, not a
   navigation — but styled as a link so the dt/dd review list still reads as prose rather than as a
   column of chunky primary buttons. It was an `<a href="#">`, which promises a destination it does
   not have.
   Excluded from the forced-colors `button` border rule above ON PURPOSE: its affordance is carried by
   an UNDERLINE, and text-decoration is not overridden by HCM — so it stays perceivable as a control
   (1.4.11) without boxing every answer in the list. */
.vf-review-edit {
  padding: 0;
  min-height: 0;
  font: inherit;
  color: var(--vf-color-primary);
  background: none;
  border: 0;
  border-radius: 0;
  text-align: left;
  text-decoration: underline;
  cursor: pointer;
}

.vf-nav {
  display: flex;
  gap: 0.75rem;
  margin-top: 1.5rem;
}
.vf-nav .vf-next,
.vf-nav .vf-submit {
  margin-left: auto;
}

/* Enter transition for screens. Screens hide via the `hidden` attribute (+ the global
   [hidden]{display:none !important} reset), so only the visible state is styled — no plain
   `display:` rule on .vf-screen that would fight the reset. Reduced-motion safe. */
@media (prefers-reduced-motion: no-preference) {
  .vf-screen:not([hidden]) {
    animation: vf-screen-in 240ms ease both;
  }
  @keyframes vf-screen-in {
    from {
      opacity: 0;
      transform: translateY(0.75rem);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
}

/* Multi-file field: the island appends one <li> per selected file (name, state, remove control,
   hidden indexed carrier). A rejected file marks its row state so it reads in the field's error
   colour (the same --vf-color-danger the field error text uses). */
.vf-file-list {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
}
.vf-file-list li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0;
}
.vf-file-list li[data-vf-file-state="rejected"] {
  color: var(--vf-color-danger);
}

/* ── Data-view grid (R1) — additive on the frozen vf-* token contract (spec P2) ──────────────
   Every rule derives colour from the existing --vf-color-* tokens (no new token, no hex), so
   the Style × Palette × Scheme axes flow through automatically — the dark @media / forced-dark
   blocks above re-point the same tokens and the grid follows with zero per-scheme duplication.
   Tables get real <table> semantics (caption + scoped headers); cards are list semantics. */

.vf-view {
  color: var(--vf-color-text);
  font-family: var(--vf-font-family);
  font-size: var(--vf-font-size-base);
  line-height: var(--vf-line-height-base);
}

/* Constrained header/footer markdown zones (P13 Custom Content equivalent). */
.vf-view-header,
.vf-view-footer {
  margin-block: calc(var(--vf-spacing-unit) * 4);
}
.vf-view-header > :first-child,
.vf-view-footer > :first-child {
  margin-block-start: 0;
}

/* ── Table layout (GV Table parity) ── */
.vf-view-table {
  width: 100%;
  border-collapse: collapse;
}
.vf-view-table caption {
  /* a11y: the caption is PRESENT (names the table) and positioned start, not removed. */
  text-align: start;
  font-weight: 600;
  padding-block-end: calc(var(--vf-spacing-unit) * 2);
}
.vf-view-table th {
  text-align: start;
  font-weight: 600;
  padding: calc(var(--vf-spacing-unit) * 2) calc(var(--vf-spacing-unit) * 3);
  border-bottom: var(--vf-border-width) solid var(--vf-color-muted);
}
.vf-view-table td {
  padding: calc(var(--vf-spacing-unit) * 2) calc(var(--vf-spacing-unit) * 3);
  border-bottom: var(--vf-border-width) solid
    color-mix(in oklch, var(--vf-color-muted) 30%, transparent);
  vertical-align: top;
}
/* zebra striping derives from the surface token (subtle, scheme-following) */
.vf-view-table tbody tr:nth-child(even) {
  background: color-mix(in oklch, var(--vf-color-muted) 8%, transparent);
}

/* ── Cards layout (GV List parity) — list semantics ── */
.vf-view-cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: calc(var(--vf-spacing-unit) * 3);
}
.vf-view-card {
  border: var(--vf-border-width) solid var(--vf-color-muted);
  border-radius: var(--vf-radius-md);
  padding: calc(var(--vf-spacing-unit) * 4);
  box-shadow: var(--vf-shadow-md);
}
.vf-view-card-row {
  display: flex;
  gap: calc(var(--vf-spacing-unit) * 3);
  padding-block: calc(var(--vf-spacing-unit));
}
.vf-view-card-label {
  flex: 0 0 8rem;
  font-weight: 600;
  color: var(--vf-color-muted);
}
.vf-view-card-value {
  min-width: 0;
}

/* ── Cells ── */
.vf-cell {
  min-width: 0;
}
/* (The data/markdown anchor colour used to live here as an enumerated list. It is the `:where(a)`
   default at the top of this sheet now — anything focusable or linkable is covered the moment it
   exists, rather than when someone remembers to extend a list.) */
.vf-cell--currency,
.vf-cell--number,
.vf-cell--percent {
  text-align: end;
  font-variant-numeric: tabular-nums;
}
.vf-cell-image {
  max-height: calc(var(--vf-spacing-unit) * 12);
  width: auto;
  border-radius: var(--vf-radius-sm);
  vertical-align: middle;
}
.vf-cell-files {
  list-style: none;
  margin: 0;
  padding: 0;
}
.vf-cell-files li {
  padding-block: calc(var(--vf-spacing-unit) / 2);
}
/* progress: a token-driven meter; the value width is set inline (data, not style) */
.vf-cell-progress {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--vf-spacing-unit) * 2);
}
.vf-cell-progress-track {
  flex: 1;
  min-width: 4rem;
  height: calc(var(--vf-spacing-unit) * 2);
  border-radius: var(--vf-radius-sm);
  background: color-mix(in oklch, var(--vf-color-muted) 25%, transparent);
  overflow: hidden;
}
.vf-cell-progress-fill {
  display: block;
  height: 100%;
  background: var(--vf-color-primary);
}

/* ── Tags / badges — FIXED tone allowlist (renderer hands an arbitrary tone; the template only
   emits vf-tag--<tone> for a tone in THIS set, else it falls back to neutral — free-form author
   tones can never reach the class attribute). Every tone maps to an existing colour token. ── */
.vf-tag {
  display: inline-block;
  padding: 0 calc(var(--vf-spacing-unit) * 2);
  border-radius: var(--vf-radius-sm);
  font-size: 0.875em;
  line-height: 1.6;
  color: var(--vf-color-surface);
}
.vf-tag--neutral,
.vf-tag--gray {
  background: var(--vf-color-muted);
}
.vf-tag--primary,
.vf-tag--blue {
  background: var(--vf-color-primary);
}
.vf-tag--success,
.vf-tag--green {
  background: var(--vf-color-success);
  /* Success is a LIGHT hue in both schemes (oklch L 0.6 light / 0.74 dark) — white surface text on it
     fails WCAG AA (3.6:1). Dark ink reads strongly on green in BOTH schemes, so override the
     surface-text default here (mirrors .vf-tag--warning's fix; data-view grid axe pass). */
  color: oklch(0.22 0 0);
}
.vf-tag--warning,
.vf-tag--amber {
  background: var(--vf-color-warning);
  /* Amber is a LIGHT hue in both schemes (oklch L 0.65 light / 0.8 dark) — white surface text on it
     fails WCAG AA (3.23:1). Dark ink reads strongly on amber in BOTH schemes, so override the
     surface-text default here (a11y: data-view grid axe pass). */
  color: oklch(0.22 0 0);
}
.vf-tag--danger,
.vf-tag--red {
  background: var(--vf-color-danger);
}

/* ── Pager (Pagination Info + Page Links widgets, P13) ── */
.vf-view-pager {
  display: flex;
  align-items: center;
  gap: calc(var(--vf-spacing-unit) * 4);
  margin-block-start: calc(var(--vf-spacing-unit) * 4);
}
.vf-view-count {
  color: var(--vf-color-muted);
}

/* ── Empty state ── */
.vf-view-empty {
  color: var(--vf-color-muted);
  padding: calc(var(--vf-spacing-unit) * 6);
  text-align: center;
}

/* Collapse the card label/value rows to stacked on narrow viewports. */
@media (max-width: 40rem) {
  .vf-view-card-row {
    flex-direction: column;
    gap: calc(var(--vf-spacing-unit));
  }
  .vf-view-card-label {
    flex-basis: auto;
  }
}

/* ── Single-entry detail page (slice D) ──
   Additive on the frozen vf-* token contract — a label/value description list of the entry's
   sections. Reuses the .vf-cell / .vf-tag / .vf-cell-* rules above for cell rendering; only the
   page chrome (.vf-entry-*) is new. No new tokens, no colour literals. */
.vf-entry {
  color: var(--vf-color-text);
  font-family: var(--vf-font-family);
  font-size: var(--vf-font-size-base);
  line-height: var(--vf-line-height-base);
  max-width: 48rem;
  margin-inline: auto;
}
.vf-entry-header {
  margin-block-end: calc(var(--vf-spacing-unit) * 2);
}
.vf-entry-back {
  color: var(--vf-color-primary);
  text-decoration: none;
}
.vf-entry-back:hover {
  text-decoration: underline;
}
.vf-entry-title {
  font-size: 1.5em;
  font-weight: 600;
  margin-block: calc(var(--vf-spacing-unit) * 2);
}
.vf-entry-section {
  margin-block: calc(var(--vf-spacing-unit) * 5);
}
.vf-entry-section-title {
  font-size: 1.05em;
  font-weight: 600;
  color: var(--vf-color-muted);
  margin-block-end: calc(var(--vf-spacing-unit) * 2);
}
.vf-entry-fields {
  margin: 0;
  display: grid;
  gap: calc(var(--vf-spacing-unit) * 2);
}
.vf-entry-field {
  display: flex;
  gap: calc(var(--vf-spacing-unit) * 4);
  align-items: baseline;
}
.vf-entry-field-label {
  flex: 0 0 12rem;
  font-weight: 600;
  color: var(--vf-color-muted);
  margin: 0;
}
.vf-entry-field-value {
  margin: 0;
  min-width: 0;
}
@media (max-width: 40rem) {
  .vf-entry-field {
    flex-direction: column;
    gap: calc(var(--vf-spacing-unit));
  }
  .vf-entry-field-label {
    flex-basis: auto;
  }
}

/* ── slice S — search / filter / sort controls ──────────────────────────────────────────────
   The control chrome that sits ABOVE the grid (P13: search + filters on top, pager below). Same
   additive posture as R1: every colour derives from the frozen --vf-color-* tokens (no new token,
   no hex except the amber-on-active ink override, which mirrors .vf-tag--warning's AA fix), so the
   Style × Palette × Scheme axes flow through unchanged. Selectors + active-state attributes match
   VfView.astro AND paint-controls.ts (the island repaints the SAME classes/attrs): chips/match use
   single-select aria-current links: active is [aria-current="true"] (chips, the alpha strip AND the
   match toggle all share the pattern); sortable headers carry aria-sort on the <th> with the
   <a data-vf-sort> inside. */

.vf-view-controls {
  display: flex;
  flex-direction: column;
  gap: calc(var(--vf-spacing-unit) * 3);
  margin-block-end: calc(var(--vf-spacing-unit) * 4);
}

/* ── Search box (inline input + submit) ── */
.vf-view-search,
.vf-view-filter-text {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: calc(var(--vf-spacing-unit) * 2);
}
.vf-view-search-label,
.vf-view-filter-label {
  display: flex;
  flex-direction: column;
  gap: var(--vf-spacing-unit);
  flex: 1 1 12rem;
  min-width: 0;
}
.vf-view-search-label-text,
.vf-view-filter-label-text {
  font-size: 0.875em;
  font-weight: 600;
  color: var(--vf-color-text);
}
/* The view-control inputs live OUTSIDE .vf-field, so the form control look is restated here
   (1em/16px keeps the iOS no-zoom floor; border-box keeps padding inside the height). */
.vf-view-search input[type="search"],
.vf-view-filter-text input {
  box-sizing: border-box;
  width: 100%;
  padding: calc(var(--vf-spacing-unit) * 2) calc(var(--vf-spacing-unit) * 3);
  font: inherit;
  color: var(--vf-color-text);
  background: var(--vf-color-surface);
  border: var(--vf-border-width) solid var(--vf-color-muted);
  border-radius: var(--vf-radius-sm);
}
/* Search/filter submit: secondary-action look (token-derived outline), same as .vf-rg-add. */
.vf-view-search-submit,
.vf-view-filter-submit {
  flex: 0 0 auto;
  padding: calc(var(--vf-spacing-unit) * 2) calc(var(--vf-spacing-unit) * 3);
  font: inherit;
  color: var(--vf-color-primary);
  background: var(--vf-color-surface);
  border: var(--vf-border-width) solid var(--vf-color-muted);
  border-radius: var(--vf-radius-sm);
  cursor: pointer;
}

/* ── Quick filters (groups of chips / inline text filters) ── */
.vf-view-filters {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--vf-spacing-unit) * 4);
}
.vf-view-filter-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--vf-spacing-unit) * 2);
}
.vf-view-filter-group-label {
  font-size: 0.875em;
  font-weight: 600;
  color: var(--vf-color-muted);
}

/* Filter chip: a pill anchor. Active is [aria-current="true"] (set by both SSR and the island). */
.vf-filter-chip {
  display: inline-block;
  padding: calc(var(--vf-spacing-unit) / 2) calc(var(--vf-spacing-unit) * 3);
  font-size: 0.875em;
  line-height: 1.6;
  color: var(--vf-color-text);
  text-decoration: none;
  background: var(--vf-color-surface);
  border: var(--vf-border-width) solid var(--vf-color-muted);
  border-radius: 999px;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}
.vf-filter-chip:hover {
  background: color-mix(in oklch, var(--vf-color-muted) 12%, transparent);
}
.vf-filter-chip[aria-current="true"] {
  /* surface-on-primary mirrors .vf-tag--primary (proven AA in both schemes). */
  color: var(--vf-color-surface);
  background: var(--vf-color-primary);
  border-color: var(--vf-color-primary);
}

/* ── Alpha strip (A–Z first-letter nav) ── */
.vf-view-alpha {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vf-spacing-unit);
}
.vf-alpha-letter {
  display: inline-block;
  min-width: calc(var(--vf-spacing-unit) * 7);
  padding: calc(var(--vf-spacing-unit) / 2) var(--vf-spacing-unit);
  font-size: 0.875em;
  text-align: center;
  color: var(--vf-color-primary);
  text-decoration: none;
  border-radius: var(--vf-radius-sm);
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}
.vf-alpha-letter:hover {
  background: color-mix(in oklch, var(--vf-color-muted) 12%, transparent);
}
.vf-alpha-letter[aria-current="true"] {
  color: var(--vf-color-surface);
  background: var(--vf-color-primary);
}

/* ── Sortable table headers ── The <th> carries aria-sort; the <a data-vf-sort> is the affordance.
   The direction glyph is DECORATIVE (aria-sort on the <th> is the semantics). aria-sort="none" is a
   sortable-but-unsorted header — leave it glyphless (an optional neutral hint only on hover/focus). */
.vf-view-table th a[data-vf-sort] {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
.vf-view-table th a[data-vf-sort]:hover,
.vf-view-table th a[data-vf-sort]:focus-visible {
  text-decoration: underline;
}
.vf-view-table th[aria-sort="ascending"] a[data-vf-sort]::after {
  content: " ▲";
}
.vf-view-table th[aria-sort="descending"] a[data-vf-sort]::after {
  content: " ▼";
}

/* ── Match toggle (Any / All) — small segmented look ── Active is [aria-current="true"]. */
.vf-view-match {
  display: inline-flex;
  border: var(--vf-border-width) solid var(--vf-color-muted);
  border-radius: var(--vf-radius-sm);
  overflow: hidden;
  align-self: start;
}
.vf-view-match-option {
  padding: calc(var(--vf-spacing-unit) / 2) calc(var(--vf-spacing-unit) * 3);
  font-size: 0.875em;
  color: var(--vf-color-primary);
  text-decoration: none;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}
.vf-view-match-option + .vf-view-match-option {
  border-inline-start: var(--vf-border-width) solid var(--vf-color-muted);
}
.vf-view-match-option:hover {
  background: color-mix(in oklch, var(--vf-color-muted) 12%, transparent);
}
.vf-view-match-option[aria-current="true"] {
  color: var(--vf-color-surface);
  background: var(--vf-color-primary);
}

/* forced-colors: these three carry "which one is active" by background + colour alone, so HCM renders
   the active chip/letter/option IDENTICALLY to the inactive ones — the selection disappears while
   everything stays perfectly legible. Found by the source guard, NOT by the manual pass: that pass
   recorded "filter chips keep visible boundaries and legible text", which is true and is a different
   question from "can you tell which chip is on". `Highlight`/`HighlightText` is the system selection
   pair and HCM preserves it. MUST stay after the base rules: same specificity, so order decides. */
@media (forced-colors: active) {
  .vf-filter-chip[aria-current="true"],
  .vf-alpha-letter[aria-current="true"],
  .vf-view-match-option[aria-current="true"] {
    background-color: Highlight;
    color: HighlightText;
    border-color: Highlight;
  }
}

/* (The nine data-view control focus rings used to be enumerated here. The `:where(:focus-visible)`
   default at the top of this sheet covers them, and every control the list never knew about.) */

/* Reduced-motion: drop the subtle chip/letter/match transitions. */
@media (prefers-reduced-motion: reduce) {
  .vf-filter-chip,
  .vf-alpha-letter,
  .vf-view-match-option {
    transition: none;
  }
}

/* Narrow viewports: let every control row wrap rather than overflow. */
@media (max-width: 40rem) {
  .vf-view-search,
  .vf-view-filter-text {
    align-items: stretch;
  }
  .vf-view-search-submit,
  .vf-view-filter-submit {
    flex: 1 1 auto;
  }
}
