/**
 * Shared components.
 *
 * Two sources, kept apart on purpose:
 *
 *   PASIA, ported from _mockups/css/site.css. Alerts, forms, chips and
 *   filters, cards and the canonical helper card, the stat band, callouts, the
 *   placeholder badge, pagination and tables. This is the signed-off system.
 *
 *   STARTER, kept because the fourteen surviving starter sections still read
 *   these class names and Phase 1 has not yet decided which of them live on.
 *   They re-brand automatically through the alias layer at the end of
 *   tokens.css, so they follow the PASIA palette without being rewritten.
 *   Anything in that block whose section is dropped in Phase 1 goes with it.
 *
 * .btn, .eyebrow, .tag, .price, .container and .section are NOT here: they are
 * PASIA primitives and they live in tokens.css. Redeclaring one would let a
 * later stylesheet quietly win over the design system.
 *
 * Tokens only, no literals.
 *
 * ===========================================================================
 * THE MAP. What is in here, and who is allowed to reach for it.
 * ===========================================================================
 *
 * WHY THIS FILE AND NOT A SECTION STYLESHEET. inc/enqueue.php loads
 * assets/css/sections/<id>.css only on pages whose section stack contains that
 * section. This file and base.css are enqueued on every page. So:
 *
 *   - a rule ONE section uses  ->  assets/css/sections/<id>.css
 *   - a rule TWO OR MORE sections share  ->  here
 *   - a rule a Phase 4 template needs  ->  here, always, because those
 *     templates render no section stack at all and nothing enqueues anything
 *     for them
 *   - a rule for markup that lives in a shared parts/ partial  ->  here or
 *     base.css, beside the partial
 *   - a rule ported from _mockups/css/site.css, which is the mock-ups' own
 *     shared stylesheet  ->  here, whatever today's consumer count is. It was
 *     shared before this theme existed, and `.stats` is why this criterion is
 *     written down: one section uses it and no template does, which reads as a
 *     contradiction of the rule above until you know it came from site.css.
 *
 * `.pasia-accordion` NO LONGER HAS A PHASE 4 CONSUMER, and the change is a
 * correction rather than drift. Inventory §B.2 gives helper-category.html a
 * category FAQ accordion; there is no relation of any kind between a `faq` post
 * and a `helper_category` term, so taxonomy-helper_category.php renders no
 * accordion and emits no `FAQPage`. The component stays exactly where it is for
 * its two section consumers. See that template's header.
 *
 * Getting this wrong ships an unstyled region that no gate in AGENTS.md §7 can
 * catch, because each agent's own page renders perfectly. The worked case:
 * faq.html is page-head + faq-groups + cta-dark, so it contains no `faq`
 * section and faq.css is never enqueued on it. Put the accordion in faq.css and
 * the site's own FAQ page ships every <details> bare.
 * docs/pasia-section-inventory.md §0.
 *
 * IF YOU ARE A SECTION AGENT, THIS TABLE IS YOUR ANSWER. Use these classes.
 * Do not re-port them into your section stylesheet: two owners for one rule is
 * how the button system broke in Phase 0.
 *
 *   class                  | sections that use it        | Phase 4 templates
 *   -----------------------+-----------------------------+---------------------
 *   .alert-*               | (authored in PHP)           | taxonomy-helper_category,
 *                          |                             | single-helper, archive-vacancy
 *   .callout               | bluf                        | archive-helper, single-helper,
 *                          |                             | home, single, single-vacancy
 *   .card                  | many                        | many
 *   .chip / .chipset       | cta-dark, next-steps,       | archive-helper,
 *                          | cta-panel                   | taxonomy-helper_category, search
 *   .filter-bar            | -                           | archive-vacancy
 *   .filter-side           | -                           | archive-helper
 *   .hcard                 | helper-cards                | archive-helper,
 *                          |                             | taxonomy-helper_category,
 *                          |                             | single-helper
 *   .pager / .pagination   | -                           | archive-helper,
 *                          |                             | taxonomy-helper_category, home
 *   .ph-badge              | most                        | most
 *   .stats / .stat         | stats-card                  | -
 *   .table / .table-wrap   | data-table                  | single
 *   .pasia-accordion       | faq, faq-groups             | -
 *   .pasia-searchband      | -                           | archive-helper, search
 *   .pasia-cta-band        | cta-dark                    | archive-vacancy
 *   .pasia-cta-panel       | cta-panel                   | archive-helper,
 *                          |                             | taxonomy-helper_category,
 *                          |                             | single-helper, search
 *   .pasia-crosslinks      | next-steps                  | archive-vacancy, single-vacancy
 *   .pasia-media-row       | feature-row                 | archive-vacancy
 *   .pasia-form-card       | form-panel                  | single-vacancy
 *   .pasia-panel           | highlight-panel             | home
 *   .pasia-insight-card    | -                           | home, single
 *   .pasia-cat-card        | category-cards              | taxonomy-helper_category
 *   .pasia-icon-tile       | office-details, icon-cards, | -
 *                          | path-cards, service-blocks, |
 *                          | service-cards               |
 *   .pasia-ticklist        | feature-row, checklist-pair,| -
 *                          | process-flow,               |
 *                          | consultation-scheduler      |
 *
 * NOT HERE BECAUSE IT IS A PRIMITIVE. tokens.css, loaded first, owns .btn,
 * .eyebrow, .tag, .price, .container and .section. Two worth knowing:
 *   - the "From HK$X,XXX" price pill is `class="tag tag--new"`, already in
 *     tokens.css and byte-identical to index.html's `.c-path .from` down to the
 *     .42em/1em padding. Three consumers now use it rather than re-implementing
 *     it: `path-cards`, `service-blocks` and `helper-cards`. `path-cards` had
 *     hand-rolled a copy before this line existed, which was the cost of the
 *     map not saying so.
 *   - `.eyebrow` is already inline-flex and already has an `.on-dark` variant.
 *
 * Two shared partials own their own markup and have NO section stylesheet:
 *   parts/page-hero.php  + parts/page-hero-ridge.php  ->  rules in base.css
 *   parts/cta-panel.php                               ->  rules here
 * base.css also owns `.head`, `.head h2` and `.head h2 em`, which is the gold
 * italic clause every "wrap words in <em>" field instruction promises.
 *
 * WHAT IS DELIBERATELY NOT HERE, so nobody goes looking:
 *   - .table td.y / td.n / tr.from  ->  sections/data-table.css. One consumer.
 *     blog-post.html's .bp-table was checked line by line and uses .bp-flag
 *     pills instead: it contains no .y, no .n and no tr.from at all.
 *   - cta-dark's cross-link row (.lbl inline, under the panel, on the light
 *     ground) -> sections/cta-dark.css. One consumer: archive-vacancy's band
 *     carries no links, vacancies.html routes those to next-steps instead.
 *     .pasia-crosslinks below is the OTHER row, the standalone band.
 *   - the .container-form width wrapper -> sections/form-panel.css. One
 *     consumer: single-vacancy puts its form in .vd-layout, not a form column.
 *   - .bp-flag, .as-conf's dashed list, .in-media.duo  ->  single consumer
 *     each, or dropped by the inventory.
 *
 * KNOWN SHARED AND STILL UNOWNED. Not a decision, an open item, so that it
 * reads as undecided rather than as "not shared":
 *   - `.sf-inline`, the inline live-search band on find-a-helper.html,
 *     search-results.html and 404.html. It is NOT here on purpose. In the
 *     mock-ups it is five rules that reuse `.sf-modal`'s internals, and Phase
 *     0A rebuilt that whole family as `.pasia-search-*`, modal only, with no
 *     inline variant. Porting it means writing a second mode of an existing
 *     component whose markup contract is set by assets/js/search-overlay.js, so
 *     it belongs with the search family rather than in the shared-component
 *     sweep. It has no reachability problem either way: search-overlay.css is
 *     enqueued unconditionally on every page. Inventory §B.3 calling the .sf-*
 *     family "already built by Phase 0A" is stale and is what hid this.
 *
 * IF WHAT YOU NEED IS NOT ON THIS PAGE: check tokens.css for a primitive
 * first, then the "not here" list above. If it is genuinely shared by two or
 * more sections, or by any Phase 4 template, ESCALATE rather than re-port it
 * into your own stylesheet. A second copy under a second class name is not a
 * collision the gates can see, and by the time it is three copies it is
 * somebody's afternoon. .pasia-icon-tile below reached three before anyone
 * said anything.
 *
 * A GLOBAL RULE IS ONLY AS GOOD AS THE BOX IT LANDS IN. Two defects of this
 * shape have already been found here, each reported one instance at a time by a
 * section agent who could only see their own page, so the pattern is worth
 * stating once.
 *
 *   1. CONTRAST AGAINST A BARE TOKEN IS NOT A MEASUREMENT OF THE COMPONENT.
 *      Anything with a translucent fill composites against whatever it lands
 *      on, so its real contrast is a property of the band. `.ph-badge` failed
 *      on the dark bands at 4.20:1, the warm band at 4.38:1 and inside
 *      `.callout` at 4.39:1, and a fourth composition, a callout on a warm
 *      band, was sitting at 4.06:1 unreported. It now pins its own ground on
 *      both sides and inherits neither. The remaining translucent fills, if you
 *      are checking: `.callout` (--forest-tint-05), `.chip`, `.tag--active`.
 *
 *   2. A CLIPPING CONTAINER CAN ERASE THE FOCUS RING. tokens.css draws
 *      `outline: 3px` at `outline-offset: 2px` with a 6px halo, so a focusable
 *      needs 6px of slack to its nearest clipping ancestor's padding box.
 *      `.pasia-accordion details` had ZERO, because the summary fills the card
 *      exactly, so a closed <details> showed no focus state at all.
 *
 *      That sweep is done, do not repeat it. Every clipping container in this
 *      file was measured against every focusable it can hold. THE FIGURES BELOW
 *      ARE NOW THE NARROWEST OF 320 AND 1440, WHICH IS THE CORRECTION: the
 *      original pass ran at 1440 only, and one line of it was not merely
 *      incomplete but the wrong sign at 320.
 *        .pasia-accordion details   summary 0px    FIXED with a negative offset
 *        .pasia-accordion details   answer link    29px  ok
 *        .table-wrap                cell links     17 to 68px, and it scrolls
 *        .pasia-cta-panel           button   -31.3px at 320   THE STALE LINE
 *                                   It read "button, chip 60px ok", and 60px is
 *                                   this panel's padding at 1440, so what it
 *                                   recorded was the bound a child gets for
 *                                   free by staying inside the content box. The
 *                                   button did not stay inside it. Re-measured
 *                                   as the real distance from the focusable's
 *                                   right edge to the clip edge, across all
 *                                   five callers, worst case at each width:
 *                                     320    -31.3px   9.4px past the clip on
 *                                             /helper-pooling/, 31.3px past it
 *                                             on /?s=helper. Not slack at all,
 *                                             the button was being cut.
 *                                     375     22.1px on /?s=helper, clear but
 *                                             already the tightest of the five
 *                                     768    103.0px    1024  281.4px
 *                                     1440   476.4px
 *                                   FIXED at `.pasia-cta-panel .btn` below.
 *                                   Now: 320 32.0px, 375 32.0px, 768 103.0px,
 *                                   1024 281.4px, 1440 476.4px. The two narrow
 *                                   figures are the inline padding exactly,
 *                                   which is what a child inside the content
 *                                   box is supposed to get.
 *        .pasia-cta-panel           chip     42.8px at 320, 66.7 at 375,
 *                                   119.4 at 768, 321.4 at 1024, 437.8 at 1440
 *                                   ok. It was never the offender; the narrow
 *                                   figures moved because the padding did.
 *        .pasia-insight-card        stretched link 24px  ok
 *        .pasia-cat-card            .go link       32px  ok
 *      Ten focusables, two offenders, and both are fixed. If you add a
 *      focusable child to a component here, or set `overflow` on one, measure
 *      it AT 320 AS WELL: a container whose padding is a clamp has its narrowest
 *      content box at the narrowest viewport and its widest slack at 1440, so a
 *      1440-only reading of a clipping container reports the best case and
 *      calls it the case.
 *
 *   3. A TRACK FLOOR IS A BOX TOO, AND NO WIDTH SWEEP CAN SEE INSIDE IT. This
 *      is the one worth reading even if you skip the rest. `.stats` had a
 *      `minmax(170px, 1fr)` floor that was too tight for a --text-3xl numeral,
 *      so above three figures the numerals overlapped EACH OTHER inside the
 *      card. `document.scrollWidth` never moved, because a card does not
 *      scroll. Every overflow check in this build measures the document, so
 *      every one of them was blind to it: 89 widths were swept, then 801, and
 *      neither saw a thing. It was found by looking at a screenshot.
 *
 *      The probe that does see it measures each element against its OWN box,
 *      `scrollWidth > clientWidth`, on everything that is not itself a
 *      scroller. Run at 1440 / 1280 / 1024 / 860 / 768 / 620 / 480 / 375 / 320
 *      against every fixed-floor grid in this file:
 *
 *        .stats + .stat .n            the defect. Two parts, both now here:
 *                                     the floor caps the count, and
 *                                     overflow-wrap catches what a third of a
 *                                     narrow row still cannot hold.
 *        .grid-auto + .hcard          clean with realistic content
 *        .grid-3 + .pasia-insight-card   clean with realistic content
 *        .grid-2 + .pasia-cat-card    clean with realistic content
 *        .pasia-media-row + figure    spills 12 to 20px BY DESIGN, which is
 *                                     the offset gold keyline. It stays inside
 *                                     --gutter at every width including 320.
 *                                     Not a defect, do not report it again.
 *
 *      The three card grids only overlap when fed a single unbreakable token
 *      longer than the track. That WAS written here as a hypothetical, with
 *      the remedy named and deliberately withheld "because nothing measures as
 *      needing it". It measures now, and the justification is what kept the
 *      hole open, so it is replaced rather than left standing:
 *
 *        .pasia-insight-card h3   one 79-character unbreakable title runs
 *                                 743px inside a 230px box at 320, and 892px
 *                                 inside 291px at 1440. The card's own
 *                                 `overflow: hidden` truncates it mid-word.
 *                                 FIXED below with `overflow-wrap: break-word`,
 *                                 exactly the remedy this paragraph named.
 *
 *      `break-word` and not `anywhere`, and the card's `overflow: hidden` is
 *      why: a scroll container's automatic minimum size is zero, so the grid
 *      track never grew and `document.scrollWidth` never moved. Nothing needs
 *      min-content to change; the text only needs to be allowed to wrap. Where
 *      the track DOES grow, `anywhere` is the answer instead, and `.head h2`
 *      in base.css is the worked case.
 *
 *      A TITLE IS NOT A "REALISTIC NAME, REFERENCE OR TITLE" PROBLEM. This
 *      heading is a post title, which is client-controlled free text, and the
 *      failure is silent: the card looks fine, it is the last third of the
 *      headline that is missing. The two remaining grids in this list are
 *      still clean and still carry no rule.
 *
 *   4. A CORRECTLY SCROLLING CONTAINER CAN STILL WIDEN THE DOCUMENT, and the
 *      chain walk that normally finds an overflow cannot see why. A scroll
 *      container clips a descendant only when it is that descendant's
 *      CONTAINING BLOCK. An absolutely positioned box inside a `position:
 *      static` scroller resolves to the initial containing block instead, so it
 *      escapes the scroller and lands in the VIEWPORT's scrollable overflow.
 *
 *      Measured on /pricing/: `documentElement.scrollWidth` 478 while
 *      `body.scrollWidth`, `main`, the `.section`, the `.container` and the
 *      `.table-wrap` itself all read exactly the viewport width, and the
 *      scroller was doing its job at clientWidth 278 against scrollWidth 560.
 *      478 at a 320 viewport AND at a 375 viewport, unchanged, which is the
 *      diagnostic: a box positioned by the viewport moves with it and a box
 *      positioned by an escaped ancestor's layout does not.
 *
 *      The escapees were `.sr-only` spans, which are `position: absolute`.
 *      FIXED at `.table-wrap` below, which now establishes a containing block.
 *
 *      THE CENSUS, so nobody re-derives it. Every clipping container in the
 *      theme was probed on all 17 seeded pages, /insights/, both archives, a
 *      category, search and 404, for absolutely positioned descendants whose
 *      containing block sits outside them:
 *        .table-wrap                 the offender. Two consumers, /pricing/
 *                                    (4 escapees past the viewport) and
 *                                    /our-team/ (1, inside the viewport today
 *                                    and latent). Both fixed by the one rule.
 *        .hc-facetband .chipset      the theme's only other scroller. No
 *                                    absolutely positioned descendant at all,
 *                                    so nothing to escape. Its chips carry no
 *                                    `.sr-only`. If one is ever added, this is
 *                                    the shape to check, and `.hc-facetband`
 *                                    being `position: relative` does NOT help:
 *                                    the containing block has to be the
 *                                    scroller, not its parent.
 *        every `overflow: hidden` component   clean; none holds an absolutely
 *                                    positioned child today.
 */

/* ===========================================================================
 * PASIA: ALERTS
 * ======================================================================== */
.alert {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  border: 1px solid;
  border-radius: var(--r-md);
  padding: var(--s-3) var(--s-4);
  font-size: var(--text-sm);
}

.alert svg {
  width: 20px;
  height: 20px;
  flex: none;
  margin-top: 1px;
}

.alert-info    { background: var(--info-bg);    border-color: var(--info-border);    color: var(--info-ink); }
.alert-success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-ink); }
.alert-warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-ink); }
.alert-danger  { background: var(--danger-bg);  border-color: var(--danger-border);  color: var(--danger-ink); }

/* ===========================================================================
 * PASIA: FORMS
 * ======================================================================== */
.field {
  display: flex;
  flex-direction: column;
  gap: .4em;
  margin-bottom: var(--s-3);
}

/* THE FLUENT FORMS CONTROLS RIDE ON THESE SELECTORS. Every rule in this block
   that has a Fluent Forms equivalent names it here rather than restating the
   values under the plugin's class names, for the reason the file header gives:
   two owners for one rule is how the button system broke in Phase 0. The
   argument, the measurements and the rules that have NO theme equivalent are
   in the FLUENT FORMS block after the form card. */
.field > label,
.pasia-form-card .fluentform .ff-el-input--label label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
}

.field .req  { color: var(--danger); }
.field .hint { font-size: var(--text-xs); color: var(--ink-faint); }

.input,
.select,
.textarea,
.pasia-form-card .fluentform .ff-el-form-control {
  font-family: var(--sans);
  font-size: var(--text-base);
  color: var(--ink);
  background: var(--paper);
  border: 1.5px solid var(--input-border);
  border-radius: var(--r-md);
  padding: .72em .9em;
  width: 100%;
  transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}

.input::placeholder,
.textarea::placeholder,
.pasia-form-card .fluentform .ff-el-form-control::placeholder { color: var(--ink-faint); }

.input:focus,
.select:focus,
.textarea:focus,
.pasia-form-card .fluentform .ff-el-form-control:focus {
  outline: none;
  border-color: var(--input-border-focus);
  box-shadow: var(--input-ring);
}

.textarea,
.pasia-form-card .fluentform textarea.ff-el-form-control {
  min-height: 130px;
  resize: vertical;
}

/* THE ONE LITERAL COLOUR IN THIS FILE, and it is a language limit rather than
   an exemption. `var()` is not resolved inside a `url()` data URI, and a
   <select> cannot carry a pseudo-element, so the chevron cannot be a mask
   tinted by a token either. The literal is therefore unavoidable; what was
   avoidable is it not matching anything, so %235f6b66 is --ink-faint's value
   rather than the mock-up's unexplained #6f7b76. If --ink-faint ever moves,
   this has to move by hand. Nothing else in this file or base.css carries a
   literal colour, and nothing else should. */
.select,
.pasia-form-card .fluentform select.ff-el-form-control {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%235f6b66' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .9em center;
  padding-right: 2.4em;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-3);
}

@media (max-width: 620px) {
  .form-row { grid-template-columns: 1fr; }
}

.field.invalid .input,
.field.invalid .select,
.field.invalid .textarea { border-color: var(--danger); }

.field .err { font-size: var(--text-xs); color: var(--danger); }

.checkline {
  display: flex;
  align-items: flex-start;
  gap: .6em;
  font-size: var(--text-sm);
  color: var(--ink-soft);
}

.checkline input {
  width: 18px;
  height: 18px;
  margin-top: 2px;
  accent-color: var(--forest);
  flex: none;
}

/* ===========================================================================
 * PASIA: CHIPS, FILTERS, TABS
 * ======================================================================== */
.chipset {
  display: flex;
  flex-wrap: wrap;
  gap: .55em;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  font-family: var(--sans);
  font-size: var(--text-sm);
  font-weight: 600;
  padding: .5em 1em;
  border-radius: var(--r-pill);
  background: var(--paper);
  color: var(--ink-soft);
  border: 1.5px solid var(--line);
  cursor: pointer;
  transition: border-color var(--dur-fast) ease,
              background-color var(--dur-fast) ease,
              color var(--dur-fast) ease;
}

.chip:hover {
  border-color: var(--pine);
  color: var(--forest);
}

.chip[aria-pressed="true"],
.chip.active {
  background: var(--forest);
  border-color: var(--forest);
  color: var(--paper);
}

.chip .x { font-weight: 700; opacity: .7; }

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
  padding: var(--s-3);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-s);
}

.filter-bar .select {
  width: auto;
  min-width: 160px;
}

.filter-side { display: grid; gap: var(--s-4); }

/* A GROUP MAY BE A <fieldset>, which is why the border is zeroed and re-drawn
   rather than only set on the bottom edge. archive-helper.php groups its
   checkboxes in fieldsets so the legend names the group for a screen reader,
   and a fieldset arrives with a `groove` border and, more awkwardly, with an
   automatic `min-width: min-content` that no other element has: inside a grid
   track it refuses to shrink and pushes the whole sidebar wider than its
   column. Both are answered here, next to the rule they belong to, rather than
   in a later rule that would have to fight this one on specificity. */
.filter-side .fs-group {
  border: 0;
  border-bottom: 1px solid var(--line);
  padding-bottom: var(--s-4);
  min-width: 0;
}

.filter-side .fs-group:last-child { border-bottom: 0; }

/* The group label. Three elements, one treatment: `h4` is the starter's, `h3`
   is the level archive-helper.php's sidebar sits at under its own `h2`, and
   `legend` is what a fieldset names its group with. */
.filter-side h3,
.filter-side h4,
.filter-side legend {
  font-family: var(--sans);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--s-2);
}

.tabs {
  display: flex;
  gap: .3em;
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--s-5);
  flex-wrap: wrap;
}

.tab {
  padding: .7em 1.1em;
  font-weight: 600;
  font-size: var(--text-base);
  color: var(--ink-soft);
  border: 0;
  background: transparent;
  cursor: pointer;
  border-bottom: 3px solid transparent;
  margin-bottom: -1px;
}

.tab:hover { color: var(--forest); }

.tab[aria-selected="true"] {
  color: var(--forest);
  border-bottom-color: var(--gold);
}

/* ===========================================================================
 * PASIA: THE INLINE SEPARATOR
 * ===========================================================================
 * `<span class="sep" aria-hidden="true">·</span>` between facts that belong on
 * one line: "Domestic Helper · Filipino · 34", "Ref · PH-1042", a testimonial's
 * "district · date", a breadcrumb's "/". It is a real element the markup marks
 * aria-hidden rather than a pseudo-element, because some screen readers read
 * generated content aloud and the punctuation is not a fact.
 *
 * ONE OWNER, ONE VALUE. It had three: `.4em` scoped to `.helper-cards`, `.45em`
 * scoped to `.testimonials`, and, in base.css, no margin at all because
 * `.crumbs` is a flex row with its own `gap`. Five files emit the class and
 * three of them (parts/helper-card.php and the two hand-built helper templates)
 * matched none of the three rules, so their separators collapsed against the
 * text. A default here is the fix; the scoped copies are redundant against it.
 *
 * `.crumbs` is the one documented exception and it has to be, not as a
 * preference: base.css gives it `display: flex; gap: .5em`, and a margin inside
 * a gapped flex row ADDS to the gap rather than replacing it, so the crumb
 * separators would drift wider on every page that carries a hero.
 *
 * The value is em rather than a spacing token on purpose: it has to scale with
 * the text it sits in, and the same separator appears at `--text-sm` in a card
 * sub line and at `--text-xs` in a card footer. `--s-1` is .5rem and would be
 * the same 8px in both.
 *
 * IT IS NOW `--sep-gap`, WHICH IS THE STEP THAT WAS BEING DEFERRED. The note
 * here used to end "tokens.css has no em-relative gap token today and it is
 * outside this change's paths", which was true of that change and is not true
 * of this one. A bare `.45em` in this file is a hardcoded design value in a
 * stylesheet that is not tokens.css, which AGENTS.md §1.1 calls a defect
 * outright, and the reasoning above is exactly the "if the token you need does
 * not exist, add it" case. It is the first em-relative gap token in the file
 * and it is declared beside --gap-split, which is the same shape: a named gap
 * for one composition. `gap: .6em` on `.eyebrow` and `gap: .55em` on
 * `.chipset` are the same idiom and stay literal because they are IN
 * tokens.css, which is the one file allowed them.
 *
 * VERIFIED RATHER THAN ASSUMED, because the consolidation landed from another
 * agent and the exception is the fragile half. Measured on the rendered page:
 * `.crumbs .sep` computes `margin-inline: 0px` against a `.crumbs` column gap
 * of 7.392px, so the crumb separators sit on the flex gap alone and nothing
 * adds to it. Every `.sep` in the DOM on the seeded pages today is inside
 * `.crumbs`; the plain form is declared and correct but has no live consumer
 * yet, because the helper cards that emit it are not rendering their fact
 * lines on the four seeded helper records.
 * ======================================================================== */
.sep { margin-inline: var(--sep-gap); }

.crumbs .sep { margin-inline: 0; }

/* ===========================================================================
 * PASIA: CARDS, and the canonical helper card
 * ======================================================================== */
.card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-s);
}

.hcard {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--s-4);
  box-shadow: var(--shadow-s);
  display: flex;
  flex-direction: column;
  transition: border-color var(--dur-normal) ease,
              box-shadow var(--dur-normal) ease,
              transform var(--dur-normal) var(--ease-out-soft);
}

.hcard:hover {
  border-color: var(--gold);
  box-shadow: var(--shadow-m);
  transform: translateY(var(--lift-hover));
}

.hcard .top {
  display: flex;
  gap: var(--s-3);
  align-items: center;
  margin-bottom: var(--s-3);
}

.hcard .avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  flex: none;
  object-fit: cover;
  box-shadow: 0 0 0 2px var(--paper), 0 0 0 3.5px var(--gold-tint-20);
}

/* THE CARD NAME IS FLAT, DELIBERATELY OFF THE TYPE LADDER. The whole argument
   and the measurements live on --card-title-size in tokens.css; the short
   version is that every signed-off mock-up writes a bare 1.2rem here while
   using --text-lg a few rules away, so the flat value is the design rather than
   an omission. --text-lg measured 21.70px at 1440 against the design's 19.2px,
   13% over, and it drifted with the viewport where the design does not. Do not
   put it back on the ladder. */
.hcard .who h3 {
  font-family: var(--display);
  font-size: var(--card-title-size);
  font-weight: 600;
}

.hcard .who .sub {
  font-size: var(--text-sm);
  color: var(--ink-faint);
}

.hcard .chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--pill-row-gap);
  margin-bottom: var(--s-3);
}

/* THE FOOTER NEEDS A GAP AND A WRAP, and the wrap is what stops the `.price`
   chip painting its own underline in the wrong place.

   `justify-content: space-between` guarantees no MINIMUM separation. It only
   says that leftover space goes between the children, and when there is none
   the two sit flush. Measured on /helper-pooling/ at 320: `.ref` and `.price`
   were exactly 0.0px apart on the "Change employer" cards. That is the
   reviewer's "too close to each other" seen inside one card rather than
   between two, and no column count can fix it because it is a shrinking
   container, not a grid.

   THE UNDERLINE IS THE WORSE HALF AND IT IS A BUG, not a matter of taste.
   `.price` is a flex item, so it is BLOCKIFIED: whatever its text does, the
   element is one block box, and the `linear-gradient(transparent 62%,
   var(--price-bg) 62%)` that draws its underline paints ONCE across that whole
   box. On two lines the colour band starts at 62% of the two-line height, so
   line one gets no underline at all and the band lands across the middle of
   line two. Measured before this rule, same cards: `.price` 52.78px tall
   against a 26.39px line height, band starting 32.72px down, which is 6.3px
   into the second line.

   `box-decoration-break: clone` does NOT reach it, and this was tested rather
   than assumed: the property computed to `clone` and the box stayed one 52.78px
   block, because that property splits a background across INLINE fragments and
   a blockified flex item has exactly one fragment.

   BOTH NEW DECLARATIONS ARE LOAD-BEARING, measured one at a time at 320:

     nowrap alone   chip correct at 26.39px, but `.ref` collapses to two lines
                    to make room and the pair still touches at 0.0px
     wrap alone     chip still wraps to 52.78px, because it is the chip's own
                    text that wraps, not the row
     both + gap     chip 26.39px on one line, `.ref` back to one line, 12px of
                    row gap between them, nothing outside the card

   NOWRAP CANNOT PUSH THE CHIP OUT. `pasia_helper_route_choices()` is exactly
   two values, "Direct hire" and "Change employer". The longer measures 142.7px
   at a 16px root against the 230px content box of the narrowest card this grid
   can build (280px less 2 * --s-4 less the hairlines), so it wraps to its own
   row with 87px to spare. A translated label wider than that box would
   overflow, and the trilingual layer owns both strings, so this is the rule to
   look at if a Chinese route label ever runs wide. The fallback if it does is
   to tile the gradient per line on `.price` itself rather than to undo this:
   `background-repeat: repeat; background-size: 100% 1lh` resolves to the line
   box exactly, measured at 26.39px here.

   THIS IS NOT A 320-ONLY BUG, which is the half the columns fix left standing.
   The break happens on any card narrower than about 308px, and the helper
   ARCHIVE makes one at desktop widths: with the filter sidebar beside it,
   `.fh-grid` builds 281px to 308px cards from 1002px of viewport to about
   1075px, and the chip was two lines across that whole band. Measured at 1024:
   57.4px tall against a 28.7px line. That is a laptop, not a phone. */
.hcard .foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
  justify-content: space-between;
  border-top: 1px solid var(--line);
  padding-top: var(--s-3);
  margin-top: auto;
}

.hcard .foot .ref {
  font-size: var(--text-xs);
  color: var(--ink-faint);
}

/* The other half of the underline fix. See the block above `.hcard .foot`. */
.hcard .foot .price {
  white-space: nowrap;
}

.grid-auto {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--s-4);
}

.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-4); }
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-4); }

@media (max-width: 860px) { .grid-3 { grid-template-columns: 1fr 1fr; } }
@media (max-width: 620px) { .grid-3, .grid-2 { grid-template-columns: 1fr; } }

/* ===========================================================================
 * PASIA: STAT BAND, NOTE, CALLOUT, PLACEHOLDER BADGE
 * ======================================================================== */
/* THE TRACK FLOOR IS A THIRD OF THE ROW, NOT A PIXEL CONSTANT, and this is the
   one defect in this file that no width sweep could have found.

   `minmax(170px, 1fr)` is too tight for a numeral set at --text-3xl. Above
   three figures the numerals run into each other INSIDE the card, and the
   document's scroll width never changes, because a card does not scroll. Every
   overflow check in this build measures document.scrollWidth, so all of them
   are blind to it; the stats-card agent swept 89 widths and then 801 and saw
   nothing. It was found by looking at a screenshot.

   Measured per element rather than per document, on the mock-ups' own widest
   value, before the fix:

     three figures   860px  track 225  numeral 234   9px into its neighbour
                     768px  track 198  numeral 223  25px
                     480px  track 179  numeral 189  10px
     six figures    1440px  track 185  numeral 198  13px
                    1024px  track 200  numeral 221  21px

   One correction to the report that raised this: the 1440px case is the SIX
   figure layout, not the three. At 1440 three figures get 325px tracks and do
   not collide. Three figures fail at 860, 768 and 480, which is the narrower
   half of the range, so a fix validated only at 1440 and 1024 would have missed
   them entirely.

   Three across IS the design, in both mock-ups and in the field instruction, so
   a floor of a third of the row says what the design says and needs no measured
   constant. A flat 240px floor was tried and rejected: it fixes the wide end and
   silently drops three figures to two columns at 768, because --text-3xl is
   fluid and the numeral is only 190px wide there. Sizing the floor off
   --text-3xl with a multiplier held everywhere but with about 1px of headroom at
   768, so it breaks the next time anyone touches the type scale.

   `min(100%, …)` wraps the whole thing because a minmax floor larger than the
   container overflows it rather than shrinking, which is the classic way this
   pattern breaks at 320px.

   Authored here rather than as a section override. `.stats` has one consumer
   today, but it came from _mockups/css/site.css, which is the second criterion
   in the map above, and a section-scoped override of a shared component is the
   duplication this file exists to prevent. sections/stats-card.css carries the
   same declaration at `.stats-card .stats` and can now drop it: it wins on
   specificity so nothing breaks either way, and the values are identical. */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, max(170px, (100% - 2 * var(--s-4)) / 3)), 1fr));
  gap: var(--s-4);
}

.stat { text-align: center; }

/* `display: block` is STRUCTURAL, not decoration, and it was the other half of
   the same gap. `.n` and `.l` are both spans, so with `.n` left inline the
   figure and its label run together on one line and the component does not
   render as designed at all. Both mock-up wrappers declare it, and so did the
   only section using it, which is why the omission here was invisible: the
   component was being completed by its consumer. A second consumer would have
   got a broken one.

   `overflow-wrap` is the other half of the track-floor fix above. The floor
   caps the column count at three, which fixes four figures and up, but it
   cannot make a numeral fit a third of a 723px row: measured, three figures
   still ran 9px into a neighbour at 860, 25px at 768 and 10px at 480. This
   catches that residue. Breaking a figure across two lines is ugly and it is
   the lesser evil, because running it through the figure beside it is worse and
   is silent. `break-word` rather than `anywhere` so it only ever triggers when
   the value cannot fit on a line by itself. */
.stat .n {
  display: block;
  font-family: var(--display);
  font-size: var(--text-3xl);
  font-weight: 600;
  color: var(--forest);
  overflow-wrap: break-word;
}

.stat .n em { font-style: normal; color: var(--gold-deep); }

.stat .l {
  font-size: var(--text-sm);
  color: var(--ink-soft);
  margin-top: .2em;
}

.note { font-size: var(--text-sm); color: var(--ink-soft); }
.note strong { color: var(--forest); }

.callout {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  background: var(--forest-tint-05);
  border: 1px solid var(--line);
  border-left: 4px solid var(--gold);
  border-radius: var(--r-md);
  padding: var(--s-4);
}

.callout svg {
  width: 22px;
  height: 22px;
  color: var(--gold-deep);
  flex: none;
}

/* The client-to-supply badge. It exists to make a gap impossible to miss, so
   it is deliberately loud: no client data has arrived, and every unsupplied
   fact on this site wears one until it does. Do not restyle it quiet.

   IT PINS ITS OWN GROUND AND NEVER INHERITS ONE, and that is the whole design
   of this component rather than a detail. Its fill is translucent stripes, so
   for as long as the gaps between them showed the page through, its contrast
   was a property of whatever band it happened to land on. Measured across every
   ground it can reach today, --gold-deep on the worst-case stripe:

     inside a .card / --paper                 5.17:1  ok
     default band / --mist                    4.75:1  ok
     inside .alert-info                       4.59:1  ok
     inside .alert-warning                    4.84:1  ok
     inside .as-conf's gold-tint panel        4.72:1  ok
     inside .callout on a paper band          4.77:1  ok
     warm band / --surface-2                  4.38:1  FAILS
     inside .callout on a mist band           4.39:1  FAILS
     inside .callout on a warm band           4.06:1  FAILS

   Three of nine, and the set is open: thirty-four sections will add grounds
   nobody has measured. Three separate reports arrived one ground at a time,
   dark then warm then callout, each with a correct one-line patch, and a fourth
   was already sitting there unreported. Pinning the base makes all nine 5.17:1
   and closes the class rather than the instance.

   The light set is unenumerable and the dark set is closed and short, so the
   dark set is the one that gets enumerated. See the two rules below. */
.ph-badge {
  display: inline-flex;
  align-items: center;
  gap: .45em;
  font-family: var(--sans);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .04em;
  padding: .3em .7em;
  border-radius: var(--r-pill);
  background-color: var(--paper);
  background-image: repeating-linear-gradient(45deg, var(--gold-tint-10), var(--gold-tint-10) 7px, transparent 7px, transparent 14px);
  color: var(--gold-deep);
  border: 1px dashed var(--gold);
}

/* THE DIAMOND IS DECORATION AND IT WAS BEING READ ALOUD. Measured on the
   rendered contact page, Chromium's accessibility tree gave the badge the name
   "◆ Client to supply", so the mark is announced before every one of the badges
   on every page, and there are eleven on some of them. It is a shape that
   repeats the border and the stripes; it carries no information a screen reader
   user needs.

   `aria-hidden` is not available: a pseudo-element has no attributes, and the
   badge is drawn by pasia_the_placeholder() in PHP so a real
   `<span aria-hidden="true">` would mean changing every caller. The CSS answer
   is the alt-text syntax, `content: <image> / <string>`, where an empty string
   gives the generated content an empty accessible name while leaving it
   painted. Chrome 77+, Edge 79+, Safari 17.4+ and Firefox 133+ implement it.

   TWO DECLARATIONS, DELIBERATELY, AND THIS IS THE JUDGEMENT CALL. A browser
   that does not know the `/ <alt>` form treats the WHOLE declaration as invalid
   and drops it, which would erase the diamond rather than silence it, on a
   component whose own note two paragraphs up says "do not restyle it quiet".
   Declared twice, an older browser keeps the first and renders exactly what it
   renders today, mark included and announced; every browser that understands
   the second gets the mark without the announcement. Nothing regresses
   anywhere, which is the only reason to spend the extra line. */
.ph-badge::before { content: "◆"; font-size: .7em; }
.ph-badge::before { content: "◆" / ""; }

/* THE DARK GROUND. The one exception to the pinning above, and the only list in
   this component, because the dark set is closed and short where the light set
   is not. --gold-deep is 2.53:1 on deep forest, so the "no licence number yet"
   badge in the footer legal strip, which is on all 27 pages and is the single
   most important gap on the site, was the one place the badge could not be
   read at all. _mockups/css/site.css has no on-dark variant either.

   This pins too, and for the same reason the base does. The documented 7.20:1
   for --gold-bright is the BARE ground; through the badge's own --gold-tint-20
   stripe it is 5.05:1 over --deep-forest but only 4.20:1 over --forest, and
   `.pasia-cta-band` is a gradient from --forest while `.cta--dark` and
   `.stats-band` are flat --forest. Pinning to --deep-forest makes every member
   5.05:1, and on the surfaces that already were deep forest nothing changes.

   ON A DARK GROUND, ADD `on-dark` TO THE BADGE. Do not re-colour it from a
   section or template stylesheet. `.eyebrow.on-dark`, `.btn-ghost.on-dark` and
   `.crumbs.on-light` are the same convention and the testimonials section
   already uses this one correctly for its featured card. The trap is specific
   and blog.html walks into it: `.bl-news .ph-badge` sets `color` and
   `border-color` to --gold-bright and no background, which now leaves
   gold-bright on the pinned --paper at 1.82:1. Setting one half of the pair is
   worse than setting neither. Add the class instead, and add the ancestor here
   only for a shell surface where no author can reach the element. */
.site-foot .ph-badge,
.site-header .ph-badge,
.pg-hero .ph-badge,
.cta--dark .ph-badge,
.stats-band .ph-badge,
.pasia-cta-band .ph-badge,
.pasia-cta-panel .ph-badge,
.ph-badge.on-dark {
  color: var(--gold-bright);
  border-color: var(--gold-bright);
  background-color: var(--deep-forest);
  background-image: repeating-linear-gradient(45deg, var(--gold-tint-20), var(--gold-tint-20) 7px, transparent 7px, transparent 14px);
}

/* ===========================================================================
 * PASIA: PAGINATION
 * ======================================================================== */
.pager {
  display: flex;
  /* Wraps, like .pagination .nav-links below. Phase 4 hand-builds the archive
     pagination on .pager and a ten-page range overflows 320px without this. */
  flex-wrap: wrap;
  gap: .35em;
  justify-content: center;
  align-items: center;
  margin-top: var(--s-6);
}

.pager a,
.pager span {
  min-width: 42px;
  height: 42px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 .6em;
  border-radius: var(--r-md);
  border: 1.5px solid var(--line);
  color: var(--ink-soft);
  font-weight: 600;
  font-size: var(--text-sm);
  background: var(--paper);
}

.pager a:hover {
  border-color: var(--pine);
  color: var(--forest);
}

.pager [aria-current="page"] {
  background: var(--forest);
  border-color: var(--forest);
  color: var(--paper);
}

.pager .disabled { opacity: .4; pointer-events: none; }

/* WordPress prints the_posts_pagination() as .nav-links inside .pagination.
   Mapped onto .pager so core pagination and hand-built pagination cannot look
   like two different sites. */
.pagination .nav-links {
  display: flex;
  gap: .35em;
  justify-content: center;
  align-items: center;
  margin-top: var(--s-6);
  flex-wrap: wrap;
}

.pagination .page-numbers {
  min-width: 42px;
  height: 42px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 .6em;
  border-radius: var(--r-md);
  border: 1.5px solid var(--line);
  color: var(--ink-soft);
  font-weight: 600;
  font-size: var(--text-sm);
  background: var(--paper);
}

.pagination a.page-numbers:hover {
  border-color: var(--pine);
  color: var(--forest);
}

.pagination .page-numbers.current {
  background: var(--forest);
  border-color: var(--forest);
  color: var(--paper);
}

/* ===========================================================================
 * PASIA: TABLE
 * ======================================================================== */
/* `position: relative` IS THE LOAD-BEARING DECLARATION HERE, and it is not
   cosmetic. A scroll container only clips a descendant whose CONTAINING BLOCK
   is inside it. `.table-wrap` was `position: static`, so every absolutely
   positioned descendant resolved its containing block to the initial
   containing block instead, escaped this scroller entirely, and joined the
   VIEWPORT's scrollable overflow region.

   `.sr-only` in base.css is `position: absolute`, and `sections/data-table.php`
   puts one in a cell whenever a ticked cell also carries wording ("Included")
   or a cell is empty ("Not included"). Inside a 560px table those spans sit at
   the table's own x, not the viewport's. Measured on /pricing/:

     documentElement.scrollWidth  478 against a 320 viewport   (+158)
     documentElement.scrollWidth  478 against a 375 viewport   (+103)
     body.scrollWidth             320 and 375, exactly correct
     .table-wrap                  clientWidth 278, scrollWidth 560, scrolling

   The 478 IS THE SAME NUMBER AT BOTH WIDTHS, which is the tell: the escapee's
   position comes from the table's layout and not from the viewport, so no
   ancestor of the table ever measured wrong. Every one of them, up to and
   including `body`, read exactly the viewport width. Walking the chain finds
   nothing; the box was never in the chain. The rightmost escapee's border box
   ended at 477.7px and the document rounded up to 478.

   Verified rather than reasoned: `.table-wrap { position: relative }` takes the
   document to 320 and 375 with `.table-wrap` still at clientWidth 278 and
   scrollWidth 560, so the scroller keeps scrolling. `clip-path: inset(50%)` on
   `.sr-only` left it at 478 and `contain: strict` left it at 478, because
   neither changes which box is the containing block. Only this does.

   BOTH CONSUMERS OF THIS COMPONENT CARRY THE SHAPE, not only the one that
   showed a symptom. /our-team/ has the same escaping `caption.sr-only`; it
   lands at x=21 because that table has no ticked cells today, so it measures
   clean and is one ticked cell away from not being. Fixing the scroller rather
   than the page closes both, and closes single.php's `.bp-table` before it is
   written.

   NOT A REGRESSION FROM THE SHARED-LAYER BATCH, checked rather than assumed:
   this rule, `.table` below it and `.sr-only` in base.css are byte-identical
   from 171c1db through HEAD. The defect arrived with the section, because the
   section's accessibility work is what first put an absolutely positioned box
   inside this scroller. `_mockups/pricing.html` has no `.sr-only` anywhere in
   its table, so the design phase's clean 320px sweep was honest about the
   markup it measured. */
.table-wrap {
  position: relative;
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--paper);
  box-shadow: var(--shadow-s);
}

/* A scroll container is keyboard-focusable, so this one takes focus, and its
   resting `box-shadow` above was silently eating the focus halo. tokens.css:301
   puts the 6px --gold-tint-20 ring on `box-shadow` at `:focus-visible`, which is
   one class of specificity, exactly the same as `.table-wrap`, and this file
   loads later. The 3px outline still painted so focus stayed visible, but this
   was the only component in the theme with a weaker ring than everywhere else.
   Composing both shadows restores it without losing the card edge. */
.table-wrap:focus-visible {
  box-shadow: var(--shadow-s), 0 0 0 6px var(--gold-tint-20);
}

.table {
  width: 100%;
  border-collapse: collapse;
  min-width: 560px;
}

.table th,
.table td {
  text-align: left;
  padding: .9em 1.1em;
  border-bottom: 1px solid var(--line);
  font-size: var(--text-sm);
}

.table thead th {
  background: var(--forest);
  color: var(--paper);
  font-weight: 600;
  letter-spacing: .02em;
}

.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover { background: var(--forest-tint-05); }

/* Two cell treatments that two consumers share, so they sit beside .table
   rather than in a section stylesheet: the `data-table` section and
   single.php's `.bp-table`, which renders no section stack.

   The row-header column, and only the part all three agree on. pricing.html
   and blog-post.html both set weight 600 and --ink, which is what is here;
   blog-post.html additionally shades it --forest-tint-05 and fixes it at
   34% width, and our-team.html sets 700 in --sans and forest AND shades it.
   So the shading is two of three and the width one of three, and both are
   inside one section rather than across two, which makes them the `data-table`
   agent's call to resolve rather than a third global rule.

   The `sub` line. `data-table` registers a `sub` field on every column and
   blog-post.html marks up the same thing, so both need it. It sits on the
   forest thead, where --on-dark-strong measures 8.4:1. The full on-dark ladder
   is in the header of assets/css/sections/cta-dark.css. */
.table tbody th[scope="row"] {
  font-weight: 600;
  color: var(--ink);
}

.table thead th .sub {
  display: block;
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--text-xs);
  color: var(--on-dark-strong);
  margin-top: 2px;
}

/* ===========================================================================
 * PASIA: THE ACCORDION
 * ===========================================================================
 * THREE consumers, and that is why it is here rather than in faq.css: the
 * `faq` section, the `faq-groups` section, and taxonomy-helper_category.php.
 *
 * faq.html is page-head + faq-groups + cta-dark. It contains no `faq` section,
 * so assets/css/sections/faq.css is never enqueued on it. This component in
 * faq.css would ship the site's own FAQ page with every <details> unstyled: no
 * card, no plus-to-cross marker, no open-state gold border. Neither agent could
 * catch it alone, because each one's page looks right.
 *
 * Native <details>. No JavaScript, now or later: it works with scripting off,
 * which is the pattern AGENTS.md §1.5 names. Anything added is enhancement.
 *
 * Markup contract:
 *   <div class="pasia-accordion">
 *     <details>
 *       <summary>Question</summary>
 *       <div class="a"><p>Answer</p><span class="qnote">…</span></div>
 *     </details>
 *   </div>
 *
 * Ported from index.html `.c-faq`, which faq.html `.fq-acc` and
 * helper-category.html `.hc-acc` copy byte for byte. `faq.css` keeps only its
 * footer-link row; `faq-groups.css` keeps only the two-column layout, the
 * sticky jump nav and the group headings.
 * ======================================================================== */
.pasia-accordion details {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-s);
  margin-bottom: var(--s-2);
  overflow: hidden;
  transition: border-color var(--dur-normal) ease;
}

.pasia-accordion details[open] { border-color: var(--gold); }

.pasia-accordion summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-4);
  cursor: pointer;
  list-style: none;
  font-family: var(--display);
  font-weight: 600;
  font-size: var(--text-lg);
  color: var(--ink);
  transition: color var(--dur-fast) ease;
}

.pasia-accordion summary::-webkit-details-marker { display: none; }
.pasia-accordion summary:hover { color: var(--pine); }

/* THE FOCUS RING GOES INSIDE, because the summary fills the card exactly.
   Measured at 1440: the summary's border box and the details' padding box are
   the same rectangle to the pixel, and `overflow: hidden` above clips at the
   padding box. The global indicator is `outline: 3px` at `outline-offset: 2px`
   with a 6px halo, so on a CLOSED <details> every pixel of it fell outside the
   clip and nothing rendered at all; on an open one only the bottom edge
   survived. WCAG 2.4.7, and it hit `faq`, `faq-groups` and Phase 4's
   taxonomy-helper_category.php equally, which is why it is fixed here once
   rather than in two section stylesheets and not at all in the template.

   A negative offset draws the ring inside the clip. It lands in the summary's
   own --s-4 padding, so it never sits over the text or the marker, and the
   radius is restated so it follows the card's corners rather than the global
   rule's 2px. What is lost is the outer halo, which is decoration: the 3px
   --gold-deep outline measures 5.72:1 on --paper, well past the 3:1 that
   SC 1.4.11 asks of an indicator.

   `overflow: clip` with `overflow-clip-margin: 6px` was the alternative and it
   is rejected on its failure mode rather than on taste: `overflow-clip-margin`
   has materially thinner support than `overflow: clip` itself, and a browser
   that takes the first and drops the second clips at exactly the padding box
   again, which is the original bug with no warning. Removing `overflow: hidden`
   was rejected too: nothing needs the corner clip TODAY, but the first
   consumer to add `summary:hover { background }` would square the card. */
.pasia-accordion summary:focus-visible {
  outline-offset: -4px;
  border-radius: var(--r-md);
}

/* The plus rotates to a cross on open. A pseudo-element, so it is never
   announced and the summary's own text carries the whole meaning. */
.pasia-accordion summary::after {
  content: "+";
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-family: var(--sans);
  font-weight: 600;
  background: var(--gold-tint-10);
  color: var(--gold-deep);
  transition: transform var(--dur-normal) var(--ease-out-soft);
}

.pasia-accordion details[open] summary::after { transform: rotate(45deg); }

.pasia-accordion .a {
  padding: 0 var(--s-4) var(--s-4);
  color: var(--ink-soft);
  font-size: var(--text-base);
  max-width: 62ch;
}

/* faq.html calls this `.fq-note` and helper-category.html `.hc-faqnote`. Same
   rule, one name. */
.pasia-accordion .a .qnote {
  display: block;
  margin-top: var(--s-2);
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--ink-faint);
}

.pasia-accordion .a .ph-badge { margin-top: var(--s-2); }

/* ===========================================================================
 * PASIA: THE CTA BAND
 * ===========================================================================
 * Two consumers: the `cta-dark` section, which covers nine regions, and
 * archive-vacancy.php's `.vc-cta`, which renders no section stack.
 *
 * Ported from index.html `.c-cta` and RE-EXPRESSED IN TOKENS. Five of the nine
 * hand-pasted copies use #fff and rgba(255,255,255,.78) literally, three use
 * --paper and --on-dark-strong, one is a hybrid. The tokenised form wins even
 * though index.html is the signed-off base, because --on-dark-strong is .82
 * against the literal's .78, a difference nobody will see, and the literal form
 * is a hard rule 1 breach the moment it lands in a theme stylesheet.
 * Inventory §C.2.
 *
 * The measures are standardised too: six values across nine copies is not six
 * typographic decisions. 24ch heading, 48ch body.
 *
 * The cross-link row that sits UNDER this band on six pages is NOT here. It has
 * one consumer and lives in sections/cta-dark.css. See the map at the top.
 *
 * Markup contract, and this is the highest-traffic component in the file, so
 * read it rather than inferring it from the rule bodies below:
 *   <div class="pasia-cta-band">
 *     <span class="eyebrow on-dark">…</span>          (optional)
 *     <h2 id="$uid-heading">Words <em>in gold italic</em></h2>
 *     <p>…</p>                                        (optional; see .row)
 *     <div class="row"><a class="btn btn-gold">…</a>
 *                     <a class="btn btn-ghost on-dark">…</a></div>
 *     <p class="fine"><span class="ph-badge">…</span></p>   (optional)
 *   </div>
 * The buttons go in `.row`, not loose. `.fine` is the small placeholder line,
 * named `.fine` rather than `.note` because `.note` is a light-ground component
 * above whose `strong` is forest. Omitting `<p>` is a supported state and the
 * `.row` gap adjusts itself: nine regions use this and two of them have no body
 * copy at all.
 * ======================================================================== */
.pasia-cta-band {
  background: linear-gradient(150deg, var(--forest), var(--deep-forest));
  border-radius: var(--r-lg);
  padding: clamp(2.5rem, 6vw, 4.5rem);
  text-align: center;
  color: var(--paper);
  box-shadow: var(--shadow-lift);
}

.pasia-cta-band h2 {
  color: var(--paper);
  font-size: var(--text-2xl);
  max-width: 24ch;
  margin-inline: auto;
}

.pasia-cta-band h2 em {
  font-style: italic;
  color: var(--gold-bright);
  font-weight: 500;
}

.pasia-cta-band p {
  color: var(--on-dark-strong);
  font-size: var(--text-lg);
  margin: var(--s-3) auto var(--s-5);
  max-width: 48ch;
}

.pasia-cta-band .row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  justify-content: center;
}

/* index.html's paragraph convention puts the gap on the paragraph, so the
   button row needs a top margin in exactly one case: when there is no body
   copy at all, which is about.html and vacancies.html. Derived from the
   content, not from a field. */
.pasia-cta-band h2 + .row { margin-top: var(--s-5); }

/* The small placeholder line under the buttons. vacancies.html calls it
   `.fine` and directors-message.html `.note`; `.fine` wins because `.note` is
   already a light-ground component above, whose `strong` is forest and would
   vanish here. --on-dark measures 6.0:1 on --forest, so it clears AA even at
   --text-xs. Never a tint token here: AGENTS.md §1.7. */
/* The margin is set on all four sides, not just the top. `.fine` is a <p>, so
   it was inheriting the --s-5 bottom margin from the paragraph rule above and
   hanging a band's worth of space under the last line. */
.pasia-cta-band .fine {
  margin: var(--s-3) auto 0;
  font-size: var(--text-xs);
  color: var(--on-dark);
}

/* ===========================================================================
 * PASIA: THE CTA PANEL, the markup for which is parts/cta-panel.php
 * ===========================================================================
 * FIVE consumers and four of them are hand-built templates:
 * archive-helper.php, taxonomy-helper_category.php, single-helper.php and
 * search.php, plus the `cta-panel` section, which serves one page.
 *
 * So there is no assets/css/sections/cta-panel.css, exactly as page-head has
 * none. A section stylesheet would never be enqueued on four of the five and
 * the panel would render as unstyled markup there.
 *
 * Not a tint of .pasia-cta-band, which is why both exist: flat --deep-forest
 * against a gradient, --shadow-green against --shadow-lift, left-aligned
 * against centred, and the cross-links sit INSIDE the panel as outlined chips
 * on a hairline rule rather than underneath it on the light ground.
 * Inventory §C.4.
 *
 * Canonical: find-a-helper.html `.fh-cta`, and the measures below are its
 * 24ch/54ch rather than the 26ch/58ch that `.hc-cta` and `.srp-cta` share. That
 * is the opposite of the majority rule applied to `.pasia-cta-band` above, and
 * deliberately so: there the inventory resolves nine hand-pasted copies by
 * standardising (§C.2), here it names one page canonical and calls the rest
 * mirrors of it (§C.4). Two of the three headed panels do use 26ch/58ch, so the
 * count is being overruled by the naming, not by an oversight.
 *
 * Markup contract:
 *   <section class="section">
 *     <div class="container">
 *       <div class="pasia-cta-panel reveal">
 *         <h2 id="$uid-heading">…</h2>          (optional; omit and the <p>
 *                                                becomes the message and grows)
 *         <p>…</p>
 *         <div class="btns"><a class="btn btn-gold">…</a>
 *                          <a class="btn btn-ghost on-dark">…</a></div>
 *         <nav class="xlinks" aria-label="…"><a class="chip">…</a></nav>
 *       </div>
 *     </div>
 *   </section>
 * parts/cta-panel.php emits all of it. Call the partial rather than hand-write
 * this: it also handles the button classes by position and renders nothing when
 * there is nothing to say.
 * ======================================================================== */
/* `overflow-wrap: anywhere` ON THE CONTAINER, not on each of its children.
   Everything inside this panel is client free text, the heading, the message,
   the button labels and the cross-link chips, and the panel clips, so every
   one of them fails silently. Declared once here and inherited: one
   66-character unbreakable token cut the `h2` 851px at 320 and 954px at 1440,
   and the `p` 453px at 320 and 130px at 1440, with `document.scrollWidth`
   unmoved in all four. The panel is a block inside `.container`, so changing
   its intrinsic minimum changes no used width anywhere; the only thing this
   declaration can do is stop the box cutting its own contents. */
.pasia-cta-panel {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: var(--deep-forest);
  color: var(--paper);
  border-radius: var(--r-lg);
  /* Block padding is the design's clamp untouched. The INLINE floor drops from
     2.25rem to --s-5, and only below a 640px viewport, where 5vw is under 2rem
     and the clamp was frozen at its floor: at 720px and up the two expressions
     are identical to the pixel. The reason is in the block above `.btns` and it
     is the finding itself, the design's own primary label measures 208.9px and
     the padding left it a 208.0px box. --s-5 is the space scale's value below
     2.25rem, which is off the scale. */
  padding: clamp(2.25rem, 5vw, 3.75rem) clamp(var(--s-5), 5vw, 3.75rem);
  box-shadow: var(--shadow-green);
  overflow-wrap: anywhere;
}

/* The gold glow bleeding off the top-right corner. Decorative, behind the
   content, and clipped by the panel's own overflow. */
.pasia-cta-panel::after {
  content: "";
  position: absolute;
  right: -70px;
  top: -70px;
  z-index: -1;
  width: 260px;
  height: 260px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--gold-tint-20), transparent 70%);
}

.pasia-cta-panel h2 {
  color: var(--paper);
  font-size: var(--text-2xl);
  max-width: 24ch;
}

.pasia-cta-panel p {
  color: var(--on-dark-strong);
  max-width: 54ch;
  margin-top: var(--s-2);
}

/* helper-pooling.html's panel has no heading, so its prompt is the whole
   message and is set larger. Derived from the content, not a field. */
.pasia-cta-panel > p:first-child {
  font-size: var(--text-lg);
  margin-top: 0;
}

/* THE PANEL WAS CLIPPING ITS OWN PRIMARY BUTTON AT 320, AND THIS IS THE FIX.
 * What used to stand here was a note saying the headroom was tight but not a
 * defect. It was a defect, on two live pages with today's content, and the note
 * is replaced rather than left standing because its own reasoning is what kept
 * it open.
 *
 * At a 320px viewport the padding clamp bottoms out at 2.25rem, 36px a side,
 * and 5vw never reaches that until roughly a 720px viewport, so the clamp is a
 * constant on every phone. Measured, panel border box against button border
 * box, before:
 *
 *   /helper-pooling/  320   panel right 300.0   a.btn.btn-gold right 309.4
 *                           "Ask about pooled helpers"   CUT BY 9.4px
 *   /?s=helper        320   panel right 280.0   a.btn.btn-gold right 311.3
 *                           "Book a consultation"        CUT BY 31.3px
 *                           and the glyph run itself ends 3.1px past the clip,
 *                           so the trailing arrow and the right pill cap go
 *
 * After, same measurement: 268.0 against a panel right of 300.0 and 248.0
 * against 280.0, so 32.0px inside the clip on both, which is the inline padding
 * exactly. `div.btns` self-overflow goes 45 -> 0 and 67 -> 0 at 320 and 14 -> 0
 * at 375, and the three pages that carried the 1px case, /find-a-helper/,
 * /find-a-helper/childcare/ and /insights/, go 1 -> 0 with them.
 *
 * BOTH HALVES ARE HERE, AND NEITHER WORKS ALONE. This was measured, not
 * assumed, so the reasoning is worth keeping.
 *
 * PADDING ALONE CANNOT FIX IT. The gold button's single line is 253.4px inside
 * a 280px panel and 235.3px inside a 240px one, so fitting it would need the
 * padding down to 13px a side on one page and 2px on the other. And the label
 * is a client field, so any number chosen today is beaten by a longer label
 * tomorrow. Wrapping is the only bound that holds by construction: a button can
 * never be wider than the box it is in, at any viewport, for any label.
 *
 * WRAPPING ALONE OVER-CORRECTS. With `white-space: normal` and the 2.25rem
 * inline floor, a button wrapped whenever its label exceeded the CONTENT box,
 * and at 320 that box was 208.0px against a "Book a consultation" of 208.9px.
 * So five buttons that were not clipped, on /find-a-helper/, /find-a-helper/
 * childcare/ and /insights/ at 320 and on /?s=helper at 375, went to two lines
 * to recover under a pixel, while the panel's 36px of padding sat there unused.
 * That is a visible change to a signed-off layout in exchange for nothing, and
 * a screenshot is what showed it: the arithmetic said "0.9px overflow" and the
 * page said "the button is now twice as tall".
 *
 * SO THE INLINE PADDING FLOOR MOVES, deliberately, and it is a design decision
 * taken on the measurement rather than a bug fix smuggled in. --s-5 is 2rem, the
 * space scale's step below the 2.25rem that was there and is not on the scale.
 * It applies only under a 640px viewport, because 5vw passes 2rem there and the
 * clamp stops being frozen; from 720px up nothing changes at all. It buys the
 * 320px panel a 216px content box, which holds the design's own primary label
 * with 7px to spare, and it leaves the block padding alone.
 *
 * `white-space: normal` SCOPED TO THIS PANEL, not lifted off `.btn` globally.
 * `nowrap` is a real property of the button system in tokens.css, and the
 * per-element sweep of all 23 URLs at 320/375/768/1024/1440 found exactly one
 * `.btn` clipped anywhere in the theme, this one. The defect is a property of
 * this container, a 36px-a-side padding floor inside a box that gets down to
 * 240px, so the rule belongs to the container.
 *
 * The `overflow-wrap: anywhere` on the panel above is the other half: with
 * `nowrap` gone it becomes effective on the label, so the button's intrinsic
 * minimum is no longer its longest word and a one-word label cannot clip
 * either. Two lines set at `line-height: 1` are tight, and deliberately not
 * loosened here: a line-height on the panel's buttons would change the height
 * of every single-line button on all five callers at every viewport, which is
 * a design change, where wrapping only ever appears in place of a truncation.
 *
 * IT IS NOT THE `div.btns` EXCEPTION. That one rests on `.btns` computing
 * `overflow: visible` with nothing clipped, and `.btns` is visible; its parent
 * is not, which is where the 9.4 and the 31.3 came from. */
.pasia-cta-panel .btns {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-4);
}

.pasia-cta-panel .btn {
  white-space: normal;
}

.pasia-cta-panel .xlinks {
  display: flex;
  flex-wrap: wrap;
  gap: .55em;
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--on-dark-line);
}

/* The chip is a light-ground component above, so it is re-grounded here rather
   than duplicated. --on-dark-strong measures 10.16:1 on --deep-forest and the
   --on-dark-faint border 4.71:1, both clear of AA and of SC 1.4.11's 3:1 for a
   control boundary. Never a tint token: --brand-tint composites to 1.00:1 on
   forest. AGENTS.md §1.7. */
.pasia-cta-panel .xlinks .chip {
  background: transparent;
  border-color: var(--on-dark-faint);
  color: var(--on-dark-strong);
}

.pasia-cta-panel .xlinks .chip:hover {
  border-color: var(--gold-bright);
  color: var(--gold-bright);
}

/* ===========================================================================
 * PASIA: THE CROSS-LINKS BAND
 * ===========================================================================
 * The standalone closing row: a small label over a centred set of chips, on
 * its own band. Three consumers: the `next-steps` section, archive-vacancy.php
 * `.vc-links` and single-vacancy.php `.vd-links`.
 *
 * Canonical: book-consultation.html `.bc-links`. The label is a plain
 * `.eyebrow`, which tokens.css already styles and which is inline-flex, so the
 * text-align below centres it without a second rule.
 *
 * This is NOT the row that sits inside a `cta-dark` section, which puts its
 * label inline and lives in sections/cta-dark.css. Both forms are in the
 * signed-off design and the rule that separates them is in inventory §C.3:
 * links inside the band's own section are a field on the band, a links band of
 * its own is `next-steps`.
 *
 * `next-steps` also carries an optional button row above the links. That has
 * one consumer and stays in sections/next-steps.css.
 * ======================================================================== */
.pasia-crosslinks { text-align: center; }

.pasia-crosslinks .chipset {
  justify-content: center;
  margin-top: var(--s-3);
}

/* ===========================================================================
 * PASIA: THE COPY AND PHOTOGRAPH ROW
 * ===========================================================================
 * Two consumers: the `feature-row` section and archive-vacancy.php's
 * `.vc-intro`.
 *
 * Three ratios exist in the mock-ups for one component (1.15fr .85fr on about,
 * 1.05fr .95fr on aftersales, 1.5fr 1fr on vacancies) and that is drift, not
 * three decisions. About's ratio and its framed photograph win, being the
 * richer treatment and the one in the signed-off design. Recorded delta:
 * aftersales' two photographs and vacancies' one gain the gold keyline frame.
 *
 * Which side the photograph sits on is DERIVED: the renderer adds `flip` to
 * every second consecutive row, which reproduces aftersales.html without a
 * layout field.
 *
 * Markup contract:
 *   <div class="pasia-media-row">
 *     <div class="pasia-media-copy">…</div>
 *     <figure class="pasia-media-figure"><img><figcaption>…</figcaption></figure>
 *   </div>
 * ======================================================================== */
.pasia-media-row {
  display: grid;
  grid-template-columns: 1.15fr .85fr;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}

/* BOTH TRACKS GET AN EXPLICIT ZERO MINIMUM, AND IT BELONGS TO THE GRID.
   A `1.15fr .85fr` track is `minmax(auto, …)`, and that `auto` floor is the
   min-content width of the GRID ITEM, not of anything nested inside it. So a
   pasted URL in the copy, or an unbroken caption, sets the floor and the row
   pushes the whole page wider than the viewport. `min-width` on an inner
   element cannot reach it; it has to be on the item.

   THIS LIVED IN sections/feature-row.css AND WAS UNREACHABLE BY THE OTHER
   CONSUMER. home.php renders `.pasia-media-row` for its lead story and never
   loads a section stylesheet at all, because it renders no section stack, so
   the floor simply did not exist there. Measured on /insights/, the only live
   `.pasia-media-row` on the site, one 79-character unbreakable token in the
   lead story's headline and its figcaption, as horizontal page scroll:

     320   696px      1024   147px
     375   648px      1100    88px
     768   324px      1200    10px
     880   259px      1440     0px

   The band was reported as 861 to 1200 and it is wider than that: below 861
   the row is a single column, which changes why it overflows but not whether
   it does. Real content is at 0px across the whole range, before and after,
   so nothing here was visible today. The scoped copy in feature-row.css has
   been deleted with this landing, as its own comment asked.

   `min-width` ALONE MADE IT WORSE, WHICH IS WHY `overflow-wrap` IS HERE TOO.
   Measured, not reasoned: with only the floor moved, 880 went from 259px of
   page scroll to 689px and 1100 from 88px to 625px. The floor lets the TRACK
   shrink; it does nothing about the token, which then hangs out of a much
   narrower column than before. feature-row.css pairs the two for exactly this
   reason and says so, and only `anywhere` is taken into account when
   min-content is computed, so it is the half that makes the floor mean
   anything. Shipping the floor by itself would have traded one overflow for a
   bigger one. With both, every width above reads 0px.

   ON THE TWO SHARED CHILDREN, WHICH IS AS FAR AS IT GOES. `.pasia-media-copy`
   and `.pasia-media-figure` are this component's own class names and they are
   the grid items, so the pair belongs together on them. `.feature-row-body`,
   `.feature-row-points` and `.feature-row-caption` keep their own
   `overflow-wrap` in sections/feature-row.css: those are that section's names,
   and what a consumer allows to break inside its own column stays the
   consumer's decision. Inherited `anywhere` is a no-op for ordinary copy; it
   only ever acts on a word that cannot fit a line by itself. */
.pasia-media-row > .pasia-media-copy,
.pasia-media-row > .pasia-media-figure {
  min-width: 0;
  overflow-wrap: anywhere;
}

.pasia-media-row.flip > .pasia-media-copy { order: 2; }
.pasia-media-row.flip > .pasia-media-figure { order: 1; }

.pasia-media-figure {
  position: relative;
  margin: 0;
}

/* The editorial offset frame: a gold keyline behind, the photograph in front,
   pulled down and left so the two do not agree. */
.pasia-media-figure::before {
  content: "";
  position: absolute;
  inset: clamp(1rem, 3vw, 1.75rem)
         calc(-1 * clamp(.75rem, 2vw, 1.25rem))
         calc(-1 * clamp(.75rem, 2vw, 1.25rem))
         clamp(1rem, 3vw, 1.75rem);
  border: 1.5px solid var(--gold);
  border-radius: var(--r-lg);
  z-index: 0;
}

.pasia-media-figure img {
  position: relative;
  z-index: 1;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-m);
}

.pasia-media-figure figcaption {
  position: relative;
  z-index: 1;
  margin-top: var(--s-3);
  display: flex;
  flex-wrap: wrap;
  gap: .6em;
  align-items: center;
  font-size: var(--text-sm);
  color: var(--ink-faint);
}

@media (max-width: 860px) {
  .pasia-media-row { grid-template-columns: 1fr; }
  .pasia-media-figure img { aspect-ratio: 16 / 9; }
  /* Stacked, the copy always leads. A flipped row that put the photograph
     above the heading would read as a caption for the section below it. */
  .pasia-media-row.flip > .pasia-media-copy { order: 1; }
  .pasia-media-row.flip > .pasia-media-figure { order: 2; }
}

/* ===========================================================================
 * PASIA: THE FORM CARD
 * ===========================================================================
 * Two consumers: the `form-panel` section (contact.html and
 * book-consultation.html) and single-vacancy.php's `.vd-form`, which renders
 * no section stack.
 *
 * The controls themselves are the FORMS block near the top of this file. This
 * is only the card around them.
 *
 * The success alert in the mock-ups is a STATIC PREVIEW of the post-submit
 * state, labelled as such. Never render it as though a message had been sent.
 *
 * Two recorded deltas, both from contact.html, which is the outlier of the
 * three: its `.checkline` margin is --s-3 where book-consultation.html and
 * vacancy-detail.html both use --s-4, and it alone adds
 * `.ct-form .btn { margin-top: var(--s-2) }`. Two of three win on both, so
 * contact's consent checkbox gains a little room and its submit button loses a
 * little.
 *
 * Markup contract:
 *   <form class="pasia-form-card">
 *     <div class="field">…</div>              (.field/.input/.select/.textarea
 *     <div class="form-row">…</div>            and .form-row are the FORMS block
 *     <label class="checkline">…</label>       near the top of this file)
 *     <button class="btn btn-primary">…</button>
 *     <p class="recipient">…</p>
 *   </form>
 * The width column is NOT here: contact.html and book-consultation.html wrap
 * this in --container-form, single-vacancy does not, so that lives in
 * sections/form-panel.css.
 * ======================================================================== */
/* `overflow-wrap` HERE, BECAUSE ONE OF THE TWO CONSUMERS HAD NO COVER AT ALL.
   sections/form-panel.css sets `overflow-wrap: anywhere` on `.form-panel` and
   says in its own comment that the card is components.css's to close and that
   single-vacancy.php is the consumer it can never reach. Measured, and it is
   not a small-screen nicety: on single-vacancy.php's `.vd-form` at a 320
   viewport the card computes `overflow-wrap: normal`, and

     a 40-character real email address   overflows `.recipient` by 18px,
                                         visibly, not clipped
     a 96-character unbroken token       document.scrollWidth 501 against a
                                         320 viewport, 181px of page scroll

   THE 96-CHARACTER CASE IS SYNTHETIC AND THE FIX IS STILL WORTH IT, which is
   the honest way round: no real address is 96 characters. The 40-character one
   is real and already overflows, the card wraps a live form whose every label
   and hint is client-typed, and `anywhere` on a container that holds nothing
   but text costs nothing when no token is long enough to need it. The other
   consumer has had exactly this declaration for as long as it has existed, so
   this makes the two agree rather than introducing anything.

   `anywhere` and not `break-word`, matching form-panel.css: `.recipient` below
   is a flex row, so its children are flex items whose automatic minimum is
   their own min-content width, and only `anywhere` is taken into account when
   min-content is computed. That is also what makes `min-width: 0` on the row
   unnecessary: with the text allowed to break, the items have nothing left to
   refuse to shrink below. Measured after: the same two probes report 0px of
   overflow on `.recipient` and 0px on the document. */
.pasia-form-card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  box-shadow: var(--shadow-s);
  overflow-wrap: anywhere;
}

.pasia-form-card .checkline { margin-block: var(--s-4); }

.pasia-form-card .btn,
.pasia-form-card .fluentform .ff-btn.ff-btn-submit {
  width: 100%;
  justify-content: center;
}

/* Where submissions go. Flex rather than plain centring because contact.html
   puts a [Client to supply] badge on this line beside the text. */
.pasia-form-card .recipient {
  margin-top: var(--s-3);
  display: flex;
  flex-wrap: wrap;
  gap: .5em;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.pasia-form-card .alert-success { margin-top: var(--s-4); }

/* --- the theme's own forms (inc/forms/) ------------------------------------
   Shared by form-panel and single-vacancy, so global. The honeypot is moved
   off-screen rather than display:none, because some bots skip fields that are
   not rendered and fill the ones that are. It is also aria-hidden and
   tabindex="-1", so no person ever meets it. */
.pasia-form-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.pasia-form-required-note {
  font-size: var(--text-xs);
  color: var(--ink-faint);
  margin-bottom: var(--s-3);
}

/* The error summary and the expired-form notice, above the fields. */
.pasia-form-summary {
  margin-bottom: var(--s-4);
}

.pasia-form-summary > div { min-width: 0; }

.pasia-form-summary-list {
  margin: var(--s-2) 0 0;
  padding-left: 1.2em;
  display: grid;
  gap: var(--s-1);
}

/* Underlined, not coloured: the alert already sets the text colour, so
   colour alone could not tell a link from the sentence around it. */
.pasia-form-summary a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.pasia-form-file { padding-block: .55em; }

.pasia-form-card .alert-success .tag-line {
  display: block;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  margin-bottom: .35em;
  opacity: .75;
}

@media (max-width: 620px) {
  .pasia-form-card { padding: var(--s-4); }
}

/* ===========================================================================
 * PASIA: FLUENT FORMS, mapped onto the theme's own form components
 * ===========================================================================
 * WHAT THIS CLOSES. AGENTS.md §1.6 says there is one button system, and it is
 * bypassed the moment a client pastes a shortcode into the `form-panel`
 * section's `shortcode` field. Fluent Forms emits its own markup under its own
 * class names, and until this block existed no rule in the theme touched any of
 * them. Measured on a live form rendered inside `.pasia-form-card`:
 *
 *   .ff-btn.ff-btn-submit   rgb(26,126,251), radius 7px, weight 500,
 *                           padding 8px/20px, label "Submit Form"
 *                           against .btn.btn-primary: --forest, --r-pill,
 *                           weight 600, padding 17.1px/30.6px
 *   .ff-el-form-control     colour rgb(96,98,102), border rgb(218,219,221),
 *                           radius 7px, font -apple-system at the browser's
 *                           own 13.33px
 *                           against .input: --ink on --paper, --input-border,
 *                           --r-md, --sans at --text-base
 *   .ff-el-input--label     the theme's --ink by inheritance only, at the
 *                           body size and weight 400
 *                           against .field > label: --text-sm, weight 600
 *   .ff-el-is-required      the asterisk is rgb(245,108,108), about 3.0:1 on
 *                           paper, against --danger's 5.9:1
 *
 * A bright blue submit button in a forest-and-gold card is the visible half.
 * The asterisk failing AA is the half nobody would have reported.
 *
 * WHY THE MAPPING IS NOT IN THIS BLOCK. Almost every rule above has a theme
 * component that already owns it, so the plugin's selector was added to THAT
 * rule rather than restated here:
 *
 *   .ff-btn.ff-btn-submit          -> tokens.css, .btn / .btn-primary
 *   .ff-el-form-control            -> this file, .input / .select / .textarea,
 *                                     including :focus, ::placeholder, the
 *                                     textarea's height and the select chevron
 *   .ff-el-input--label label      -> this file, .field > label
 *   .ff-el-is-required's asterisk  -> the --fluentform-danger remap below,
 *                                     because the plugin's own selector for it
 *                                     outranks anything scoped to the card
 *   the submit button's full width -> this file, .pasia-form-card .btn
 *
 * Restating them would have put a second `.btn-primary` in the theme under a
 * plugin's class name, which is the exact Phase 0 collision §1.6 was written
 * after. On these selectors instead, a change to --btn-radius or --input-border
 * reaches the plugin's form for free, and there is nothing to keep in step.
 *
 * ONLY WHAT HAS NO THEME EQUIVALENT IS BELOW: Fluent Forms' own block spacing,
 * retuned to the theme's rhythm.
 *
 * SCOPED TO `.pasia-form-card`, NOT TO `.form-panel-live`. The live wrapper is
 * `<div class="pasia-form-card form-panel-live">`, so the card is the outer of
 * the two and it is the shared component: single-vacancy.php renders the same
 * card as `.vd-form`. Scoping to the card means a shortcode reaching either
 * consumer is styled, and a form pasted somewhere the theme does not own is
 * left alone. `.fluentform` is in each selector so this outranks the plugin's
 * own stylesheet whichever of the two the browser loads last.
 *
 * IT IS NOT IN sections/form-panel.css, and that file says so itself: a
 * section stylesheet is enqueued only on pages whose stack contains that
 * section, and single-vacancy.php renders no stack at all.
 * ======================================================================== */

/* FLUENT FORMS HAS ITS OWN DESIGN TOKENS, AND RE-POINTING THEM IS THE WHOLE
   TRICK. The plugin declares `--fluentform-primary`, `--fluentform-secondary`,
   `--fluentform-danger`, `--fluentform-border-color` and
   `--fluentform-border-radius` on `:root` and paints from them. A custom
   property inherits, so redeclaring the five inside the card beats `:root` for
   the whole subtree and there is no specificity contest at all.
 *
 * IT IS ALSO THE ONLY THING THAT WINS. The required asterisk was mapped first
 * as a plain rule and lost, silently: the plugin's own selector is
 * `.fluentform .ff-el-input--label.ff-el-is-required.asterisk-right label:after`
 * at (0,4,2), against (0,3,2) for anything scoped to the card. Chasing it with
 * more classes would have worked once and taught nobody anything. Re-pointing
 * the token fixes that mark and every other thing the plugin paints from the
 * same palette, enumerated or not, which is the difference between closing the
 * class and closing the instance. The asterisk was rgb(245,108,108), about
 * 3.0:1 on paper; --danger is 5.9:1, so this is an AA fix as well as a brand
 * one.
 *
 * The explicit rules elsewhere still earn their place: the plugin has one
 * radius token for both its inputs and its button, where the theme has --r-md
 * for a control and --r-pill for a button, and no token of the plugin's carries
 * a font family, a size or a padding. */
.pasia-form-card .fluentform {
  --fluentform-primary: var(--forest);
  --fluentform-secondary: var(--ink-soft);
  --fluentform-danger: var(--danger);
  --fluentform-border-color: var(--input-border);
  --fluentform-border-radius: var(--r-md);
}

/* Fluent Forms ships 20px under every field group. `.field` uses --s-3. */
.pasia-form-card .fluentform .ff-el-group { margin-bottom: var(--s-3); }

/* `.field` sets `gap: .4em` between its label and its control; the plugin uses
   a flat 5px, which is tighter than the theme at every type size. */
.pasia-form-card .fluentform .ff-el-input--label { margin-bottom: .4em; }

/* The submit button's own group is the last thing in the form, so its group
   margin is a trailing gap inside the card rather than a step between fields.
   `.recipient` supplies the space under it when there is one. */
.pasia-form-card .fluentform .ff_submit_btn_wrapper { margin-bottom: 0; }

/* ===========================================================================
 * PASIA: THE EDITORIAL PANEL
 * ===========================================================================
 * Two consumers: the `highlight-panel` section (aftersales.html `.as-mid`) and
 * home.php's `.bl-standard`, which renders no section stack. Identical apart
 * from the closing note, which is this component's `note` field.
 *
 * Not `.callout`, and the separation is argued in the inventory's
 * highlight-panel entry rather than assumed: this carries a section-scale
 * heading, an eyebrow, no icon and --shadow-s elevation, and it stands alone in
 * its band. A callout carries an icon, body-scale type and leans against the
 * region below it. One is a region, the other annotates one.
 *
 * Markup contract:
 *   <div class="pasia-panel">
 *     <span class="eyebrow">…</span>
 *     <h2 id="$uid-heading">…</h2>
 *     <p>…</p>
 *     <p class="note">…</p>                        (optional closing line)
 *     <span class="ph-badge">…</span>              (optional)
 *   </div>
 * No icon. If you are reaching for one, the component you want is `.callout`.
 * ======================================================================== */
.pasia-panel {
  background: var(--paper);
  border: 1px solid var(--line);
  border-left: 4px solid var(--gold);
  border-radius: var(--r-lg);
  padding: clamp(1.5rem, 4vw, 2.5rem);
  box-shadow: var(--shadow-s);
}

.pasia-panel h2 {
  font-size: var(--text-2xl);
  margin: var(--s-3) 0;
}

.pasia-panel p {
  color: var(--ink-soft);
  font-size: var(--text-lg);
}

.pasia-panel .note { margin-top: var(--s-4); }

/* Ported from aftersales.html `.as-mid .ph-badge`, where the welfare check-in
   schedule is a visible placeholder. Reported as having no reachable consumer,
   because `highlight-panel`'s `note` field is typed `text` and a badge cannot
   survive in it. Kept, for two reasons: the section side is widening the field,
   and a badge can also arrive from the template through
   pasia_the_placeholder() rather than from a field at all, which is how
   office-details and licence-card do it. If BOTH of those turn out false, this
   line is dead and should go with the section that proves it. */
.pasia-panel .ph-badge { margin-top: var(--s-3); }

/* ===========================================================================
 * PASIA: THE INSIGHT CARD
 * ===========================================================================
 * Shared by home.php (`.bl-card`) and single.php (`.bp-card`). NEITHER RENDERS
 * A SECTION STACK, so there is no section stylesheet that could ever reach
 * this and no section that owns it. It is here or it is duplicated twice.
 *
 * The whole card is clickable through a stretched link on the headline, so the
 * focus ring stays on real text rather than on the card edge, and the link text
 * is the article title rather than "read more".
 *
 * Recorded delta: the mock-ups set the headline at a flat 1.25rem. That is a
 * hard rule 1 breach here, and --text-lg (18 to 22px, centred on 20) is the
 * honest mapping. Same for the category badge's 14px offset, which takes --s-2.
 *
 * Markup contract. The stretched link means the ORDER matters: the <a> must be
 * inside the heading, and nothing else in the card may be a link, or it will be
 * covered by the ::after overlay.
 *   <article class="pasia-insight-card">
 *     <div class="media">
 *       <span class="cat">Category</span>
 *       <img … width height>
 *     </div>
 *     <div class="body">
 *       <h3><a href="…">The article title, which is the link text</a></h3>
 *       <p class="stand">…</p>
 *       <p class="meta"><span>…</span><span class="dot">·</span><span>…</span></p>
 *     </div>
 *   </article>
 * ======================================================================== */
.pasia-insight-card {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-s);
  transition: border-color var(--dur-normal) ease;
}

.pasia-insight-card .media {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
}

.pasia-insight-card .media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out-soft);
}

.pasia-insight-card .cat {
  position: absolute;
  top: var(--s-2);
  left: var(--s-2);
  z-index: 1;
  display: inline-flex;
  align-items: center;
  font-family: var(--sans);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  padding: .38em .85em;
  border-radius: var(--r-pill);
  background: var(--paper);
  color: var(--forest);
  box-shadow: var(--shadow-s);
}

.pasia-insight-card .body {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-4);
  flex: 1;
}

/* `overflow-wrap` because the title is client-controlled free text and the card
   clips. Measured, one 79-character unbreakable token: 743px inside a 230px
   box at 320, 892px inside 291px at 1440. See the third numbered note in the
   file header, which is where the decision not to apply this used to live. */
.pasia-insight-card h3 {
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
  overflow-wrap: break-word;
}

.pasia-insight-card h3 a {
  color: var(--ink);
  transition: color var(--dur-fast) ease;
}

/* Stretched link: the card is the hit area, the headline is the accessible
   name and keeps the focus ring. */
.pasia-insight-card h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
}

.pasia-insight-card .stand {
  color: var(--ink-soft);
  font-size: var(--text-sm);
}

.pasia-insight-card .meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5em .8em;
  margin-top: auto;
  padding-top: var(--s-2);
  font-size: var(--text-xs);
  color: var(--ink-faint);
}

.pasia-insight-card .meta .dot { color: var(--line); }

/* A designed hover: the image scales, the headline shifts gold, the hairline
   turns. No shadow bump, deliberately, so a grid of these does not jump. */
.pasia-insight-card:hover { border-color: var(--gold); }
.pasia-insight-card:hover .media img { transform: scale(1.06); }
.pasia-insight-card:hover h3 a { color: var(--gold-deep); }

/* ===========================================================================
 * PASIA: THE CATEGORY CARD
 * ===========================================================================
 * Two consumers: the `category-cards` section (industries.html `.in-card`) and
 * taxonomy-helper_category.php's `.hc-relcard`, which renders no section stack.
 * The related-category card is the same card without the "Suits:" line.
 *
 * `scroll-margin-top` clears the sticky header, so it is
 * calc(var(--header-h) + var(--s-4)) and never the 96px the mock-up hardcodes:
 * the token for the header's own height, plus a gutter so a deep-linked card
 * lands under the header rather than flush against it. AGENTS.md §6.
 *
 * industries.html's Household card uses a two-photograph `.in-media.duo` split.
 * That is not ported and it is not a gap: the card covered two helper_category
 * terms at once in a mock-up that predates the taxonomy, the terms are separate
 * now and each carries its own photograph.
 *
 * Markup contract:
 *   <article class="pasia-cat-card">
 *     <div class="media"><img></div>
 *     <div class="body">
 *       <h2|h3>…</h2>  <p>…</p>
 *       <p class="suits"><strong>Suits:</strong> …</p>   (optional)
 *       <a class="go">…</a>
 *     </div>
 *   </article>
 * ======================================================================== */
.pasia-cat-card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-s);
  display: flex;
  flex-direction: column;
  scroll-margin-top: calc(var(--header-h) + var(--s-4));
  transition: transform var(--dur-normal) var(--ease-out-soft),
              border-color var(--dur-normal) ease,
              box-shadow var(--dur-normal) ease;
}

.pasia-cat-card:hover {
  transform: translateY(-5px);
  border-color: var(--gold);
  box-shadow: var(--shadow-m);
}

.pasia-cat-card .media {
  aspect-ratio: 3 / 2;
  overflow: hidden;
}

.pasia-cat-card .media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pasia-cat-card .body {
  padding: var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  flex-grow: 1;
}

/* The heading level is derived from the page: a card is h2 when it is the
   page's own content and h3 under a section heading. Both are styled. */
.pasia-cat-card .body h2,
.pasia-cat-card .body h3 { font-size: var(--text-xl); }

.pasia-cat-card .body > p { color: var(--ink-soft); }

.pasia-cat-card .suits {
  font-size: var(--text-sm);
  color: var(--ink-soft);
  border-top: 1px solid var(--line);
  padding-top: var(--s-3);
  margin-top: auto;
}

.pasia-cat-card .suits strong {
  color: var(--gold-deep);
  font-weight: 700;
}

.pasia-cat-card .go {
  display: inline-flex;
  align-items: center;
  gap: .45em;
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--pine);
  /* Bottom-aligned when there is no "Suits:" line to do it, which is the
     related-category card on taxonomy-helper_category.php. */
  margin-top: auto;
  transition: gap var(--dur-fast) ease, color var(--dur-fast) ease;
}

.pasia-cat-card .suits ~ .go { margin-top: var(--s-1); }

.pasia-cat-card .go:hover,
.pasia-cat-card:hover .go {
  gap: .8em;
  color: var(--gold-deep);
}

/* ===========================================================================
 * PASIA: RICH TEXT OUTPUT
 * ===========================================================================
 * EVERY LIST IN EVERY RICH-TEXT FIELD RENDERED WITHOUT MARKERS. base.css resets
 * `ul, ol { list-style: none }` globally, which is right for the theme's own
 * chrome lists and wrong for anything a client types. Only `.page-plain-body`
 * restored it, so a pasted policy list came out as indented sentences with no
 * bullets and no numbers, and nothing errored.
 *
 * That lands hardest exactly where it is least acceptable. The three legal
 * pages are placeholders whose whole content is a pasted policy, and a numbered
 * clause list rendering unnumbered is a legal document losing its clause
 * numbers. It is the first thing the client will paste.
 *
 * Two corrections to the report that raised this. SIX sections take a wysiwyg,
 * not five: `feature-row` was missing from the list, and `checklist-pair` is
 * not affected at all, because it has no wysiwyg field and no wp_kses_post()
 * call anywhere in it. Its lists are repeaters of `text`.
 *
 * Nested levels are declared too, and that is the point rather than polish: a
 * policy with numbered sub-clauses needs 1. / a. / i. to stay readable as a
 * legal instrument.
 *
 * `padding-left` is restated because base.css's reset zeroes padding on
 * everything, and a list with no padding hangs its markers outside its own box.
 * ------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
 * PART ONE: THE PROSE RHYTHM. Moved here from base.css, where it was declared
 * on `.page-plain-body` alone.
 *
 * `.pasia-rte` was the canonical rich-text class and carried NO prose treatment
 * at all, only the list markers below. tokens.css resets `* { margin: 0 }` for
 * the whole theme, so a rich-text field in a wrapper that is nothing but
 * `.pasia-rte` had no block spacing whatsoever. Measured on
 * single-vacancy.php's "About the role", which is exactly that wrapper: every
 * gap between consecutive blocks was 0px, paragraph against paragraph against
 * heading, for the whole description. It is the same defect single.php's header
 * records for a post body, and single.php answered it by wearing
 * `.page-plain-body` as a second class and reporting the fix upward. This is
 * that fix.
 *
 * ADDED TO THE SELECTOR, NOT MOVED ONTO IT. page.php, singular.php and
 * index.php all emit `.page-plain-body` with no canonical class beside it, so
 * moving the rules would have stripped the prose treatment off the block-editor
 * page template, which is the one surface it was written for.
 *
 * `> * + *` RATHER THAN THE FLEX COLUMN IT REPLACES, and this is the part worth
 * reading. base.css had `display: flex; flex-direction: column; gap: var(--s-3)`
 * here. A gap ADDS to a child's own margin, and two wrappers that now match
 * this rule already declare their own step:
 *
 *   .feature-row-body > * + *   margin-top: var(--s-3)   sections/feature-row.css
 *   .hc-prose p + p             margin-top: var(--s-4)   this file, below
 *
 * As a flex gap the two stack: measured, the feature-row body went from 16px
 * between blocks to 32px and the category intro from 24px to 40px. As a margin
 * they COMPOSE instead, because a later or more specific rule replaces the step
 * rather than adding to it, and both of those rules already win. `> * + *` is
 * also the idiom three section stylesheets already use for this exact job, so
 * the shared rule now speaks the same language as the sections it generalises.
 *
 * It also retires a cost that was recorded twice, in base.css and in
 * single.php: a flex column ignores `float`, so an `alignleft` or `alignright`
 * image out of the editor rendered full width. With margins it floats again.
 *
 * WHAT IT COSTS, measured rather than assumed: a heading's own `margin-top` no
 * longer stacks on top of a gap, so on single.php the step above an `h2` goes
 * from 48px to 32px and above an `h3` from 40px to 24px. Those are the authored
 * values; the larger numbers were a gap and a margin adding up, which nobody
 * chose.
 *
 * `:not(.btn)` ON THE LINK RULE, because `pasia_kses_rte()` keeps `class` and a
 * client can therefore paste `<a class="btn btn-primary">` into a wysiwyg. This
 * rule is (0,1,1) and `.btn-primary` is (0,1,0), so without the guard a shared
 * prose rule would quietly repaint a button, which is the shape of the Phase 0
 * collision AGENTS.md §1.6 exists to stop. The guard was free; finding it later
 * would not have been.
 * ------------------------------------------------------------------------- */
.page-plain-body,
.pasia-rte {
  max-width: 72ch;
}

.page-plain-body > * + *,
.pasia-rte > * + * { margin-top: var(--s-3); }

.page-plain-body h2,
.pasia-rte h2 { font-size: var(--text-2xl); margin-top: var(--s-5); }

.page-plain-body h3,
.pasia-rte h3 { font-size: var(--text-xl); margin-top: var(--s-4); }

/* An in-page anchor must clear the sticky header, or the heading you clicked
   lands underneath it. Measured before this rule: a fragment jump put the
   target at top 29.58px with the header bottom at 73px, so the heading was
   completely hidden. Same calc the rest of the theme already uses.

   Deliberately broad rather than scoped to single.php's table of contents:
   any block-editor anchor on page.php, singular.php, index.php or the
   taxonomy prose lands in one of these two wrappers, and every one of them
   has the same sticky header above it. Scoping it to the one caller that
   reported the bug would leave the other four to find it again. */
:is(.page-plain-body, .pasia-rte) :is(h2, h3, h4)[id] {
  scroll-margin-top: calc(var(--header-h) + var(--s-4));
}

.page-plain-body a:not(.btn),
.pasia-rte a:not(.btn) {
  color: var(--pine);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.page-plain-body a:not(.btn):hover,
.pasia-rte a:not(.btn):hover { color: var(--forest); }

/* ---------------------------------------------------------------------------
 * PART TWO: THE LIST MARKERS.
 * ------------------------------------------------------------------------ */

/* `.pasia-rte` IS THE CANONICAL CLASS. Put it on the element that wraps a
   pasia_kses_rte() call and nothing else is needed.

   The names after it are a MIGRATION SHIM, not the contract. They are the
   wrappers the wysiwyg sections emit today, listed so the legal pages are
   correct right now rather than after six agents have each edited a file, which
   matters because Phase 3 is seeding those pages as this lands. Each section
   should add `pasia-rte` to its wrapper when it is next open, and drop its name
   from here; `process-flow` can also delete the local copy it already wrote at
   sections/process-flow.css:125. When the list is empty, delete it.
   `.page-plain-body` is in it because it is the block-editor page template and
   the same rules were duplicated for it in base.css; that duplicate is now
   gone. It stays on the list even though it is named in PART ONE above: the
   two halves answer different questions, and page.php still emits it alone.

   `feature-row-body` HAS COME OFF, which is the shim working as designed rather
   than a change of mind. sections/feature-row.php now renders
   `class="feature-row-body pasia-rte"`, so the canonical selector reaches it
   and its name here was doing nothing. Each of the remaining names was checked
   the same way rather than taken on trust, by grepping the section that emits
   it:

     prose-body             sections/prose.php:142           no pasia-rte  KEEP
     bluf-text              sections/bluf.php:229            no pasia-rte  KEEP
     directors-letter-body  sections/directors-letter.php:255 no pasia-rte KEEP
     highlight-panel-body   sections/highlight-panel.php:124 no pasia-rte  KEEP
     pf-body                sections/process-flow.php:247    no pasia-rte  KEEP
     page-plain-body        page.php:34, singular.php:30,
                            index.php:27                     no pasia-rte  KEEP

   Flat selectors, and the list repeated on each rule rather than CSS nesting.
   Nesting would read better and it is a newer dependency than anything else in
   this file, which is not a trade worth making on the one stylesheet every page
   loads. The repetition is also mildly annoying, which is the right incentive
   for a block that is supposed to be deleted. */
:is(.pasia-rte, .page-plain-body, .prose-body, .bluf-text, .directors-letter-body, .highlight-panel-body, .pf-body) :is(ul, ol) { padding-left: var(--s-4); }

:is(.pasia-rte, .page-plain-body, .prose-body, .bluf-text, .directors-letter-body, .highlight-panel-body, .pf-body) ul { list-style: disc; }
:is(.pasia-rte, .page-plain-body, .prose-body, .bluf-text, .directors-letter-body, .highlight-panel-body, .pf-body) ol { list-style: decimal; }

:is(.pasia-rte, .page-plain-body, .prose-body, .bluf-text, .directors-letter-body, .highlight-panel-body, .pf-body) ul ul { list-style: circle; }
:is(.pasia-rte, .page-plain-body, .prose-body, .bluf-text, .directors-letter-body, .highlight-panel-body, .pf-body) ul ul ul { list-style: square; }
:is(.pasia-rte, .page-plain-body, .prose-body, .bluf-text, .directors-letter-body, .highlight-panel-body, .pf-body) ol ol { list-style: lower-alpha; }
:is(.pasia-rte, .page-plain-body, .prose-body, .bluf-text, .directors-letter-body, .highlight-panel-body, .pf-body) ol ol ol { list-style: lower-roman; }

:is(.pasia-rte, .page-plain-body, .prose-body, .bluf-text, .directors-letter-body, .highlight-panel-body, .pf-body) li { margin-bottom: var(--s-1); }
:is(.pasia-rte, .page-plain-body, .prose-body, .bluf-text, .directors-letter-body, .highlight-panel-body, .pf-body) :is(li > ul, li > ol) { margin-top: var(--s-1); }

/* ===========================================================================
 * PASIA: THE ICON TILE
 * ===========================================================================
 * A rounded square on --forest-tint-10 with a --forest glyph. Three sections:
 * `office-details` (contact.html `.ct-row .ico`), `icon-cards`
 * (accreditations.html `.ac-duty .ico`) and `path-cards` (index.html
 * `.c-path .ico`).
 *
 * Added in the audit round, and it was late. Three agents had already written
 * three private copies under three class names, which is the shape of the
 * `.btn-primary` collision Phase 0A had to unpick, forming rather than
 * inherited. The map above is what should have prevented it.
 *
 * The three differ only in size, 42px, 46px and 52px with 21px, 24px and 27px
 * glyphs, and that is real rather than drift: the tile tracks the density of
 * the card it sits in. So the size is a component-local custom property with
 * the middle value as its default, and a consumer that differs sets one line
 * rather than re-declaring the treatment. This is an author-side variable, not
 * a client-facing switch: nothing in the picker reaches it, so AGENTS.md §1.2
 * is untouched.
 *
 *   .path-card-ico   { --pasia-tile: 52px; --pasia-tile-glyph: 27px; }
 *   .pasia-office-row .ico { --pasia-tile: 42px; --pasia-tile-glyph: 21px; }
 *
 * The glyph is --forest on the tint, which measures 9.72:1 over --paper,
 * 8.88:1 over --mist and 8.15:1 over --surface-2, so it clears AA on every band
 * a card can sit on. The width and height on the svg beat pasia_icon()'s own
 * 18px presentation attributes, so an allow-list icon and a hand-drawn one land
 * at the same size.
 *
 * Markup contract:
 *   <span class="pasia-icon-tile"><svg …></svg></span>
 * ======================================================================== */
.pasia-icon-tile {
  --pasia-tile: 46px;
  --pasia-tile-glyph: 24px;
  width: var(--pasia-tile);
  height: var(--pasia-tile);
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--r-md);
  background: var(--forest-tint-10);
  color: var(--forest);
}

.pasia-icon-tile svg {
  width: var(--pasia-tile-glyph);
  height: var(--pasia-tile-glyph);
}

/* ===========================================================================
 * PASIA: THE TICKED LIST
 * ===========================================================================
 * Three sections use it and none of them can see another's stylesheet:
 * `feature-row` (about.html `.ab-checklist`), `checklist-pair` (aftersales.html
 * `.as-cov`) and `process-flow` (helper-pooling.html `.pl-list`).
 *
 * FOUR consumers, not three. The audit round found `consultation-scheduler` had
 * organically become one as well, which is the argument for this component
 * rather than against it.
 *
 * Near-identical in all four, and the differences are all drift: the gap is
 * .6em, .65em or .7em; aftersales.html:72 also zeroes the margin, which is a
 * consequence of it sitting inside a card rather than under a heading; and two
 * of the three omit `font-size` and inherit --text-base anyway. About's .65em
 * and an explicit --text-base win, About being the canonical page for
 * `feature-row`. A consumer that genuinely needs the margin zeroed sets that
 * one line in its own stylesheet.
 *
 * The tick is a pseudo-element, so it is decoration rather than content and the
 * item's own text carries the meaning.
 * ======================================================================== */
.pasia-ticklist {
  list-style: none;
  margin: var(--s-4) 0 0;
  display: grid;
  gap: .65em;
}

.pasia-ticklist li {
  position: relative;
  padding-left: 1.7em;
  color: var(--ink-soft);
  font-size: var(--text-base);
}

.pasia-ticklist li::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--gold-deep);
  font-weight: 700;
}

/* ===========================================================================
 * PASIA: THE HELPER SURFACES
 * ===========================================================================
 * archive-helper.php (`.fh-`), taxonomy-helper_category.php (`.hc-`) and
 * single-helper.php (`.hp-`), ported from find-a-helper.html,
 * helper-category.html and helper-profile.html.
 *
 * WHY ALL THREE NAMESPACES ARE IN components.css AND NOT IN THREE STYLESHEETS.
 * None of the three templates renders a section stack, so
 * pasia_preload_section_styles() enqueues nothing for them and a
 * per-template stylesheet would never be loaded at all. Inventory §0 and the
 * map at the top of this file: "a rule a Phase 4 template needs -> here,
 * always". The three pages are also one composition, not three: the same card,
 * the same chips, the same search band and the same closing panel.
 *
 * ALREADY OWNED ABOVE, AND NOT RE-PORTED HERE. The mock-ups re-declare all of
 * these per page; two owners for one rule is how the button system broke in
 * Phase 0.
 *
 *   .hcard          the card itself, its avatar, chips and footer. `.fh-grid`,
 *                   `.hc-grid` and `.hp-similar` only lay them out.
 *   .callout        `.fh-bluf` and `.hc-bluf` are the shared callout; only the
 *                   margin is below.
 *   .filter-side    the sidebar's groups and labels, including the fieldset
 *                   handling archive-helper.php needs.
 *   .pager          pagination on both list pages.
 *   .pasia-accordion, .pasia-cat-card, .pasia-ticklist, .pasia-cta-panel
 *                   `.hc-acc`, `.hc-relcard`, `.hc-duties` and every closing
 *                   panel are these components under the mock-ups' names.
 *   .alert-*        `.hc-medical` and the profile's PDPO note.
 *   .chip .tag .btn .eyebrow  primitives, tokens.css.
 *
 * `.hc-relcard` HAS NO RULES AT ALL and that is correct: it is
 * `.pasia-cat-card` without the "Suits:" line, which that component already
 * handles with `margin-top: auto` on `.go`. The class stays on the markup so
 * the mock-up and the template still diff cleanly.
 *
 * CONTRAST, MEASURED AS COMPOSITED rather than against a bare token, because
 * anything with a translucent fill composites against whatever it lands on.
 * The dark ground on all three pages is the hero, which paints --deep-forest
 * under a --hero-overlay to --hero-wash-deep wash:
 *
 *   --on-dark-strong on --deep-forest      10.16:1   `.hc-herochips` label,
 *                                                    the hero notes
 *   --gold-bright    on --deep-forest       7.20:1   `.hp-facts dt`
 *   --paper          on --deep-forest      14.48:1   `.hp-facts dd`
 *   --on-dark-faint  border on deep forest   4.71:1   the hero chip's edge,
 *                                                    past SC 1.4.11's 3:1
 *   --ink-faint      on --paper              5.55:1   `.filter-side` labels,
 *                                                    `.hp-aside .ref`
 *   --ink-soft       on --paper              8.18:1   `.hp-sec p`
 *   --ink-soft       on --mist               7.45:1   every `.note`
 *   --gold-deep      on --mist               5.21:1   `.hc-steps` numerals
 *   --forest         on --forest-tint-10 over --paper 10.64:1  a ticked facet
 *
 * NEVER a tint token as a text colour: --brand-tint composites to 1.00:1 on
 * forest. AGENTS.md §1.7.
 *
 * CLIPPING CONTAINERS, and there are two, both deliberate and both measured.
 * The global indicator is `outline: 3px` at `outline-offset: 2px` with a 6px
 * halo, so a focusable needs 6px of slack to its nearest clipping ancestor's
 * padding box.
 *
 *   .hc-facetband .chipset   scrolls sideways. 6px of block padding, which is
 *                            exactly the halo, plus `scroll-padding-inline` so
 *                            a chip scrolled into view by the keyboard is not
 *                            flush against the edge.
 *   .fh-filter               scrolls down only when it is taller than the
 *                            viewport, which is what stops a long sidebar
 *                            pinning its own Apply button off screen. Its
 *                            --s-4 padding is 24px, four times the halo, and
 *                            `scroll-padding-block` keeps a chip off the edge.
 *
 * Nothing else here sets `overflow`. `.pasia-cta-panel` does, and the 60px of
 * slack the sweep above used to claim for it was a 1440-only figure: at 320 it
 * was cutting its own primary button by 9.4px on /helper-pooling/ and 31.3px on
 * /?s=helper. Fixed at `.pasia-cta-panel .btn`, and the corrected census is in
 * the file header under point 2.
 * ======================================================================== */

/* --- the inline search band, floated over the hero ------------------------
   SHARED, and the shape is a requirement rather than a flourish. The live
   suggestion drop is absolutely positioned and `.pg-hero` sets
   `overflow: hidden` for its ridge SVG, so a band nested inside the hero has
   its suggestions clipped to nothing. Both mock-ups that float a band over a
   hero put it in the NEXT section and pull it up, and search-overlay.css says
   so above `.pasia-search-drop`.

   `--s-6` rather than the mock-up's `clamp(2.2rem, 5vw, 3.2rem)`: 3rem sits
   inside that range and is the token. The hero's own bottom padding is at
   least 3rem, so the band never rides up over the lede.

   search.php's `.srp-searchband` is the same wrapper under another name, and
   for a while it carried no rule at all, so its band did not lift. That is
   fixed: search.php now pairs the two the way archive-helper.php pairs
   `pasia-searchband fh-searchband`, so this rule has two consumers. Keep the
   pairing rather than writing a second copy under the `srp-` name. */
.pasia-searchband {
  position: relative;
  z-index: var(--z-raised);
  margin-top: calc(-1 * var(--s-6));
}

.pasia-searchband .pasia-search-inline { max-width: var(--container-narrow); }

/* The band and the facet band both hang off the bottom of the hero, so the
   section that carries them starts flush. */
.section.fh-top { padding-top: 0; }
.section.hc-top { padding-block: 0; }

/* The closing panels sit directly under the content they close.

   `.srp-ctasec` WAS MISSING FROM THIS GROUP AND ITS CONSUMER WAS NOT. Four
   templates pass a `section_class` to parts/cta-panel.php for this pattern,
   archive-helper.php:1003, taxonomy-helper_category.php:985,
   single-helper.php:825 and search.php:595, and only three of the four were
   named here. `_mockups/search-results.html:129` writes the same
   `padding-top: 0` the other three get, so the search page's closing panel was
   carrying a full --section-y of top padding that no mock-up asks for. It is a
   different namespace from the other three, which is how it fell out. */
.section.fh-ctasec,
.section.hc-ctasec,
.section.hp-ctasec,
.section.srp-ctasec { padding-top: 0; }

/* The editor-only banner on a hidden profile. Neither list page carries a hero
   note: an empty roster is reported once, in the results section, rather than
   badged twice on one screen. */
.hp-hero-badge {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5em;
  margin-top: var(--s-4);
  color: var(--on-dark-strong);
  font-size: var(--text-sm);
}

.fh-bluf,
.hc-bluf { margin-top: var(--s-5); }

/* --- find a helper: the sidebar and results ------------------------------- */

/* `minmax(0, 1fr)` on the results track, never a bare `1fr`. A `1fr` track's
   automatic minimum is the MIN-CONTENT size of the grid item, so a card grid
   or a long reference inside it widens the track rather than wrapping, and
   `min-width: 0` on some inner child does not reach it. */
.fh-layout {
  display: grid;
  grid-template-columns: 290px minmax(0, 1fr);
  gap: var(--s-5);
  align-items: start;
  margin-top: var(--s-5);
}

/* NOT STICKY, and that is a measured departure from the signed-off page rather
   than an omission. find-a-helper.html sets `position: sticky; top: 92px` on
   this panel. The panel holds six groups built from the real taxonomy, which is
   six categories, three nationalities, seven skills, six languages and two
   fields: about 1000px tall, taller than any laptop viewport. A sticky element
   taller than the viewport never scrolls, so its foot never comes into view,
   and the foot of this one is the Apply button.

   The usual remedy, `max-height: calc(100vh - …)` with `overflow-y: auto`, was
   built and then rejected on the screenshot: it slices the card through the
   middle of a chip row with no scrollbar and no other affordance, so it reads
   as broken rather than as scrollable, and it makes a clipping container out of
   a panel that is almost entirely focusable. Scrolling with the page costs a
   nicety and keeps every control reachable.

   If it comes back, it needs a visible scroll affordance and 6px of ring slack
   at both ends, and it should only engage where the panel actually fits. */
.fh-filter { padding: var(--s-4); }

.fh-filter > h2 {
  font-size: var(--text-xl);
  margin-bottom: var(--s-3);
}

.fs-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-4);
}

.fs-actions .btn { font-size: var(--text-sm); }

/* A FACET IS A REAL CHECKBOX INSIDE THE PILL, visible rather than hidden away.
   The state is therefore carried by the control itself and not by colour alone
   (SC 1.4.1), the browser's own focus ring lands on a real focusable, and the
   whole thing filters with JavaScript off because it is a form. */
.chip.fh-facet {
  gap: .5em;
  font-weight: 600;
}

.fh-facet input {
  width: 1em;
  height: 1em;
  flex: none;
  accent-color: var(--forest);
  cursor: pointer;
}

/* The pill answers focus as well as the box inside it, so the target reads as
   one control. */
.fh-facet:focus-within {
  border-color: var(--gold-deep);
  box-shadow: 0 0 0 3px var(--gold-tint-20);
}

/* The ticked treatment is a TINT and not the solid forest `.chip.active`
   carries, because a forest ground would swallow the forest accent colour of
   the checkbox that is doing the actual telling. Its own rule rather than an
   addition to the `.chip[aria-pressed]` selector list: `:has()` invalidates the
   whole list on a browser that does not support it, and the checkbox has to go
   on carrying the state there. */
.chip.fh-facet:has(input:checked) {
  background: var(--forest-tint-10);
  border-color: var(--forest);
  color: var(--forest);
}

.fh-reshead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-1) var(--s-3);
  margin-bottom: var(--s-4);
}

.fh-reshead h2 { font-size: var(--text-xl); }

.fh-pdpo,
.hc-pdpo {
  margin-top: var(--s-4);
  max-width: 72ch;
}

.fh-pageinfo {
  text-align: center;
  margin-top: var(--s-2);
}

.hc-shownote { margin-top: var(--s-4); }

@media (max-width: 860px) {
  .fh-layout { grid-template-columns: minmax(0, 1fr); }

}

/* --- the card's own button, NOW HERE, UNSCOPED ----------------------------
   MOVED IN from assets/css/sections/helper-cards.css, where it was scoped to
   `.helper-cards` and therefore reached one of its four consumers.

   The mock-ups declare it four times, identically, and three of the four are
   Phase 4 templates that render NO section stack, so nothing ever enqueued a
   section stylesheet for them:

     helper-pooling.html:89   `.pl-pool .hcard .btn`    -> helper-cards section
     find-a-helper.html:84    `.fh-grid .hcard .btn`    -> archive-helper.php
     helper-category.html:139 `.hc-grid .hcard .btn`    -> taxonomy-helper_category.php
     helper-profile.html:108  `.hp-similar .hcard .btn` -> single-helper.php

   parts/helper-card.php emits the markup for all four. While the rule was
   scoped, those three templates shipped the profile button with no width, no
   centring and no `--text-sm`, which is exactly the §F8 defect where every
   agent's own page renders perfectly. Unscoped is the answer, not a fourth
   copy under a fourth selector.

   NOT REPRODUCED: the mock-up also writes `padding: .8em 1.2em` here.
   `--btn-padding-block` and `--btn-padding-inline` in tokens.css are already in
   em, so dropping the button to `--text-sm` shrinks its padding in proportion
   and reaches the same button without minting a second, untokened button size.
   The residual difference is about two pixels of block padding.

   `margin-top: auto` pins the button to the foot of the card, so a row of cards
   with different numbers of chips still lines its buttons up. When the card has
   a `.foot`, that already carries the auto above, and a second one would split
   the free space between the two, so the button takes a fixed gap instead. */
.hcard .btn {
  margin-top: auto;
  width: 100%;
  justify-content: center;
  font-size: var(--text-sm);
}

.hcard .foot ~ .btn { margin-top: var(--s-3); }

/* --- helper category: the facet switcher ---------------------------------- */

/* The page's navigation spine. It scrolls sideways rather than wrapping, which
   makes it a clipping container, so the chipset carries 6px of block padding:
   exactly the focus halo, measured rather than guessed. */
.hc-facetband {
  position: relative;
  z-index: var(--z-raised);
  margin-top: calc(-1 * var(--s-6));
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-lift);
  padding: var(--s-1) var(--s-2);
}

/* 6px on every side, not the mock-up's `6px 2px`. Measured at 1440: with 2px of
   inline padding the FIRST chip's focus ring had 2px of slack to the scroll
   box and lost 4px of its halo, while every other chip had 6px. Even a
   clipping container that scrolls has to hold the ring of the child sitting
   against its edge. */
.hc-facetband .chipset {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  padding: 6px;
  margin-block: -4px;
  scroll-padding-inline: var(--s-2);
}

.hc-facetband .chipset::-webkit-scrollbar { display: none; }

.hc-facetband .chip { flex: none; }

/* The current facet: filled forest with a gold keyline, so it is unmistakably
   current rather than merely darker. */
.hc-facetband .chip[aria-current="page"] {
  background: var(--forest);
  border-color: var(--gold);
  color: var(--paper);
  box-shadow: 0 0 0 3px var(--gold-tint-20);
}

.hc-facetband .chip[aria-current="page"]:hover { color: var(--paper); }

/* Informational chips on the dark hero. `cursor: default` because they are not
   controls: `.chip` sets `cursor: pointer` for the ones that are. */
.hc-herochips {
  display: flex;
  flex-wrap: wrap;
  gap: .55em;
  margin-top: var(--s-4);
}

.hc-herochips .chip {
  background: transparent;
  border-color: var(--on-dark-faint);
  color: var(--on-dark-strong);
  cursor: default;
}

.hc-herochips .chip strong { color: var(--gold-bright); }

/* --- helper category: the grid and the writing ---------------------------- */

/* Four across is the signed-off design here, against `.grid-auto`'s auto-fill
   on the archive. Both are `minmax(0, …)` so a track can shrink below its
   content rather than widening the grid. */
/* A 280px FLOOR, not the mock-up's four fixed columns, and the arithmetic is
   why. `.container` is 1200px wide and gives 1072px of track at 1440. Four
   cards at the `.hcard` floor of 280px plus three --s-4 gaps need 1192px, so a
   fourth column can only exist by squeezing the card to 250px, which is under
   the width the component was measured at.

   What that costs is not theoretical and no width sweep can see it: at 250px
   the card's `.foot` runs out of room, the route chip wraps under the reference
   and its underline gradient lands across the line above. Found with the
   per-element `scrollWidth > clientWidth` probe and then on the screenshot,
   which is the same class of defect as `.stats` overlapping its own numerals.

   So this is `.grid-auto`'s rule under this page's name: three across at 1440,
   two at 1024, one at 620, and every card at or above the width it was drawn
   for. Recorded as a departure from helper-category.html's four across. */
.hc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--s-4);
}

.hc-prose { max-width: 66ch; }

.hc-prose p { color: var(--ink-soft); }

.hc-prose p + p { margin-top: var(--s-4); }

/* The opening paragraph carries the weight, which is a treatment derived from
   position rather than anything the client picks. */
.hc-prose p:first-of-type {
  font-size: var(--text-lg);
  color: var(--ink);
}

.hc-duties-h {
  font-size: var(--text-xl);
  margin: var(--s-5) 0 var(--s-4);
}

/* `.pasia-ticklist` above draws the gold tick and the spacing; this adds the
   two columns and nothing else. pasia_icons() has no `check`, and the component
   means none is needed. */
.hc-duties {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-3) var(--s-4);
}

.hc-medical { margin-top: var(--s-5); }

/* The numbered advice list, which the field's own instruction calls the part of
   the page most likely to be quoted back by an answer engine. A real <ol>, so
   the order survives with the stylesheet off; the counter only styles it. */
.hc-steps {
  counter-reset: hcstep;
  display: grid;
  gap: var(--s-4);
  max-width: 68ch;
}

.hc-steps li {
  counter-increment: hcstep;
  display: grid;
  grid-template-columns: 3.4rem minmax(0, 1fr);
  gap: var(--s-3);
  padding-top: var(--s-4);
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
}

.hc-steps li::before {
  content: counter(hcstep, decimal-leading-zero);
  font-family: var(--display);
  font-style: italic;
  font-weight: 600;
  font-size: var(--text-2xl);
  line-height: 1;
  color: var(--gold-deep);
}

.hc-steps li strong {
  display: block;
  font-family: var(--display);
  font-size: var(--text-lg);
  font-weight: 600;
  margin-bottom: .2em;
}

/* `.hc-grid` needs no breakpoints of its own: `auto-fill` with a 280px floor
   steps itself down and can never produce a track under the width the card was
   measured at. Only the duties list, which is a fixed two columns, does. */
@media (max-width: 620px) {
  .hc-duties { grid-template-columns: minmax(0, 1fr); }
}

/* --- one helper: the hero ------------------------------------------------- */

/* The portrait column exists only when there is a portrait, and there is one
   only when written consent is on file. `.has-portrait` is set by the template
   from pasia_helper_photo(), so a helper without consent gets a single column
   rather than a two-column grid with a hole in it. */
.hp-hero-grid.has-portrait {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(220px, 26vw, 300px);
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: center;
}

.hp-hero-copy { min-width: 0; }

.hp-tags {
  display: flex;
  flex-wrap: wrap;
  gap: .5em;
  margin-top: var(--s-3);
}

.hp-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) 0;
  margin-top: var(--s-5);
}

.hp-facts > div {
  padding-right: var(--s-4);
  margin-right: var(--s-4);
  border-right: 1px solid var(--on-dark-line);
}

.hp-facts > div:last-child {
  border-right: 0;
  margin-right: 0;
  padding-right: 0;
}

.hp-facts dt {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gold-bright);
}

.hp-facts dd {
  margin-top: .15em;
  font-family: var(--display);
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--paper);
}

.hp-hero-btns {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-5);
}

.hp-photo img {
  width: 100%;
  border-radius: var(--r-lg);
  box-shadow: 0 0 0 3px var(--on-dark-border),
              0 0 0 4.5px var(--gold-tint-20),
              var(--shadow-lift);
}

/* --- one helper: the body ------------------------------------------------- */

.hp-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 330px;
  gap: var(--s-5);
  align-items: start;
  margin-top: var(--s-5);
}

.hp-maincol {
  display: grid;
  gap: var(--s-4);
  min-width: 0;
}

.hp-sec { padding: var(--s-5); }

.hp-sec h2 {
  font-size: var(--text-xl);
  margin-bottom: var(--s-3);
}

.hp-sec h3 {
  font-family: var(--sans);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: var(--s-4) 0 var(--s-2);
}

.hp-sec h3:first-of-type { margin-top: 0; }

.hp-sec p {
  color: var(--ink-soft);
  max-width: 62ch;
}

/* Employment history, one row per placement, which is what the repeater's own
   instruction promises the client the page will do with them. */
.hp-history {
  display: grid;
  gap: var(--s-3);
  margin-top: var(--s-3);
}

.hp-history li {
  border-top: 1px solid var(--line-soft);
  padding-top: var(--s-3);
}

.hp-history li:first-child {
  border-top: 0;
  padding-top: 0;
}

.hp-history-h {
  display: block;
  font-weight: 600;
  color: var(--ink);
}

.hp-history-when {
  display: block;
  font-size: var(--text-sm);
  color: var(--ink-faint);
}

.hp-history p { margin-top: var(--s-1); }

.hp-kv {
  display: grid;
  gap: var(--s-3);
}

.hp-kv li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-3);
  border-bottom: 1px solid var(--line);
  padding-bottom: var(--s-3);
}

.hp-kv li:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

.hp-kv .k {
  flex: none;
  min-width: 190px;
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--ink);
}

/* The action rail. Sticky with no max-height, unlike `.fh-filter`: it holds a
   reference, two buttons, a note and three links, so it cannot outgrow a
   viewport and does not need to become a scroller to stay reachable. */
.hp-aside {
  padding: var(--s-5);
  position: sticky;
  top: calc(var(--header-h) + var(--s-3));
}

.hp-aside .ref {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--s-3);
}

.hp-aside .btn {
  width: 100%;
  justify-content: center;
}

.hp-aside .btn + .btn { margin-top: var(--s-2); }

.hp-aside .note { margin-top: var(--s-3); }

.hp-aside .xlinks {
  display: grid;
  gap: .5em;
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--line);
}

.hp-aside .xlinks a {
  font-size: var(--text-sm);
  font-weight: 600;
}

.hp-similar { margin-top: var(--s-7); }

@media (max-width: 860px) {
  .hp-hero-grid.has-portrait { grid-template-columns: minmax(0, 1fr); }

  .hp-photo { max-width: 260px; }

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

  .hp-aside { position: static; }
}

/* ===========================================================================
 * STARTER: kept for the fourteen surviving sections
 * ===========================================================================
 * These re-brand through the alias layer in tokens.css rather than being
 * rewritten, which is the point of inverting the usual fork advice. Phase 1
 * decides which of the sections stay; anything whose only consumer is dropped
 * goes with it. */

/* --- buttons ---------------------------------------------------------------
 * There are NO .btn-primary / .btn-ghost / .btn-outline rules here any more.
 *
 * The starter shipped its own set at exactly the same specificity as PASIA's
 * in tokens.css, and components.css loads after tokens.css (inc/enqueue.php),
 * so the starter's gold .btn-primary silently beat the signed-off forest one
 * on every starter section. Two systems answering to one class name is not
 * something a later phase would have caught: it renders, it just renders the
 * wrong brand.
 *
 * Resolved by deleting the duplicates and moving the markup onto PASIA's
 * system: `btn btn-primary`, `btn btn-ghost`, `btn btn-gold`, plus
 * `.btn-ghost.on-dark` on a dark band. `.btn-outline` is gone entirely, it was
 * the starter's name for what PASIA calls `.btn-ghost`.
 * ------------------------------------------------------------------------- */

.btn .icon { width: 1.1em; height: 1.1em; }

.btn-sm {
  padding: var(--space-2xs) var(--space-xs);
  font-size: var(--text-small);
}



.btn-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-2);
}

@media (max-width: 560px) {
  .btn-row { flex-direction: column; align-items: stretch; }

  .btn-row .btn { justify-content: center; }
}

/* --- section head --------------------------------------------------------- */
.section-head {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  margin-bottom: var(--s-6);
}

.section-title-group {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.section-title-group .eyebrow { line-height: 1; }

.section-head h2 {
  font-size: var(--text-h2);
  max-width: 64ch;
}

.section-head h2 em {
  font-style: italic;
  font-weight: 500;
  color: var(--gold-deep);
}

.section-lead,
.lead {
  font-size: var(--text-lead);
  color: var(--ink-soft);
  max-width: 60ch;
  line-height: var(--leading-body);
}

/* --- wordmark ------------------------------------------------------------- */
.wordmark {
  display: inline-flex;
  align-items: center;
}

.wordmark img {
  height: 40px;
  width: auto;
  display: block;
}

/* --- breadcrumb, starter flavour ------------------------------------------ */
.breadcrumb {
  font-size: var(--text-sm);
  color: var(--ink-faint);
}

.breadcrumb a { color: var(--pine); }
.breadcrumb a:hover { color: var(--forest); }

/* --- icons -----------------------------------------------------------------
 * Inline SVG, from pasia_icon(). There is no icon font: see inc/enqueue.php.
 * Sized in em so an icon tracks the text it sits beside rather than needing a
 * px override per context. */
.icon {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  display: inline-block;
  vertical-align: -0.15em;
}

.stars {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  color: var(--gold);
}

/* --- page-load animations ------------------------------------------------- */
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes scaleIn {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}

.load-fade-up  { opacity: 0; animation: fadeUp 0.7s var(--ease-out) forwards; }
.load-fade-in  { opacity: 0; animation: fadeIn 0.6s ease forwards; }
.load-scale-in { opacity: 0; animation: scaleIn 0.8s var(--ease-out) forwards; }

.load-d1 { animation-delay: 0.1s; }
.load-d2 { animation-delay: 0.2s; }
.load-d3 { animation-delay: 0.35s; }
.load-d4 { animation-delay: 0.5s; }
.load-d5 { animation-delay: 0.65s; }

/* Same reasoning as .reveal in base.css: these start at opacity 0 and only the
   animation lifts them, so a browser that has animations turned off must not
   be left with an invisible page. */
@media (scripting: none) {
  .load-fade-up,
  .load-fade-in,
  .load-scale-in { opacity: 1; animation: none; }
}

@media (prefers-reduced-motion: reduce) {
  .load-fade-up,
  .load-fade-in,
  .load-scale-in {
    opacity: 1;
    transform: none;
    animation: none;
  }
}

/* ---------------------------------------------------------------------------
   Helper directory filtering, busy state.

   assets/js/helper-filter.js sets aria-busy and .is-filtering on the
   [data-pasia-filter] region while a request is in flight. This is the whole
   visual cost of the enhancement: the list dims slightly and stops taking
   clicks, so a visitor cannot queue a second filter on a list that is already
   being replaced.

   Nothing here is required for the page to work, and with the script absent
   neither class is ever set, so none of it applies. Tokens only, and opacity
   is the one property animated because it is compositor-friendly.
   --------------------------------------------------------------------------- */
.fh-layout,
[data-pasia-filter] {
  position: relative;
}

[data-pasia-filter].is-filtering {
  opacity: .55;
  pointer-events: none;
  transition: opacity var(--duration-fast, 150ms) ease-out;
}

/* The facet controls stay legible while the grid behind them dims, because the
   sidebar is what the visitor is looking at when they tick a box. */
[data-pasia-filter].is-filtering .fh-filter {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  [data-pasia-filter].is-filtering {
    transition: none;
  }
}

/* ===========================================================================
 * PASIA: THE VACANCY ARCHIVE SURFACE
 * ===========================================================================
 * archive-vacancy.php (`.vc-`), ported from _mockups/vacancies.html.
 *
 * WHY IT ARRIVED LATE, WHICH IS THE USEFUL PART. The mock-up keeps its `vc-`
 * rules in a page-level <style> block, not in _mockups/css/site.css, so they
 * were never in the file Phase 4 ported from. Three of its regions found a
 * home regardless, because a shared component already painted them and the
 * template was pointed at it: `.vc-intro` at `.pasia-media-row`, `.vc-cta` at
 * `.pasia-cta-band`, `.vc-links` at `.pasia-crosslinks`. The four with no
 * shared counterpart, the filter label, the job grid, the job card and the
 * placeholder note, had nowhere to be rehomed TO and were dropped. The
 * template renders no section stack, so there was no third place for them
 * either. The map at the top of this file already names the rule they fell
 * through: "a rule a Phase 4 template needs -> here, always".
 *
 * IT SHIPPED BECAUSE NO GATE IN AGENTS.md §7 CAN SEE AN UNSTYLED REGION. The
 * PHP parses, the field keys verify, the scoper suite is untouched and the URL
 * returns 200 with every element in the right order and the right language.
 * It was found by a reviewer looking at the rendered page: job cards with
 * their text flush against the card border, and the grid sitting hard against
 * the filter bar. The sibling gap is still open and is worse, because it is a
 * whole page layout rather than padding: single-vacancy.php's `.vd-` namespace
 * has no rules of any kind either, so `.vd-layout` renders as one stacked
 * column instead of the aside-and-main split vacancy-detail.html specifies.
 *
 * ALREADY OWNED ABOVE, AND DELIBERATELY NOT RE-PORTED HERE:
 *   .card           the shell. It carries no padding on purpose and every
 *                   consumer sets its own, the way `.fh-filter` does.
 *                   `.vc-job` below IS that setting, and its absence is the
 *                   whole of "all cards show no spacing".
 *   .grid-3         the three tracks and the 860/620 collapse. Its
 *                   `gap: var(--s-4)` was present, correct and never the
 *                   defect: the space the review asked for is INSIDE a card,
 *                   and ABOVE the grid, not between two cards.
 *   .filter-bar     the bar itself. Only its `.lab` is below.
 *   .tag            tokens.css. Only the row that wraps them is below.
 *   .alert-warning  the empty-state note. Only its measure is below.
 *   .head           `.vc-intro` renders as a plain `.head`, whose
 *                   `margin-bottom: var(--s-6)` in base.css is already the
 *                   mock-up's gap between the intro and the filter bar.
 *   .pasia-cta-band .pasia-crosslinks .pagination   the rest of the page.
 *
 * CONTRAST, MEASURED rather than estimated, on --paper unless stated:
 *   --on-dark-strong composited on --deep-forest  10.20:1  .vc-hero-badge
 *   --ink-faint                                    5.55:1  .vc-filters .lab
 *   --ink-soft                                     8.18:1  .vc-job > p
 *   --pine                                         8.51:1  .vc-job .go
 *   --gold-deep                                    5.72:1  .vc-job .go:hover
 *   --ink-soft on --surface-2                      6.81:1  a .tag in the card
 * ======================================================================== */

/* THE PAGE'S OWN CTA LINKS TO `#roles` AND THE HEADER IS STICKY, so without
   this the "See open roles" button scrolls "Open roles" underneath the header
   and lands the visitor on the filter bar. Same calc as `.pasia-insight-card`
   and the prose-heading rule above, and `var(--header-h)` rather than a
   literal, per AGENTS.md §6.

   Scoped to this section deliberately. Every `.section[id]` in the theme has
   the same gap and a blanket `[id]` rule would reach far past this page; that
   survey belongs to whoever can check all 27 pages, not to a spacing fix. */
.vc-roles { scroll-margin-top: calc(var(--header-h) + var(--s-4)); }

/* The hero badge, in parts/page-hero.php's `after` slot on a forest band. It
   renders only while no vacancy is published, so it is the one region on this
   page a reviewer looking at seeded staging could not have seen. */
.vc-hero-badge {
  margin-top: var(--s-4);
  font-size: var(--text-sm);
  color: var(--on-dark-strong);
}

.vc-hero-badge .ph-badge { margin-right: .5em; }

/* The filter bar's micro-label. Same idiom as `.filter-side h3 / h4 / legend`
   above and `.eyebrow` in tokens.css, but it sits inline in a flex row rather
   than above a group, so it takes an inline margin and no block one. */
.vc-filters .lab {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-right: .4em;
}

/* THE SECOND HALF OF THE REVIEW FINDING, "the grid has no space between filter
   bar". `.grid-3` sets the gap BETWEEN cards; nothing set the gap ABOVE the
   grid, so the roles sat flush under the bar.

   The two empty states need the same space and did not get it from the
   mock-up, which has no empty state at all: `vacancies.html` filters invented
   data in the browser and always has three cards. `.vc-empty` is this
   template's own class for the "no roles match those filters" branch, whose
   two paragraphs would otherwise sit on the bar AND on each other, since
   base.css sets no default margin on `p`. */
.vc-jobs,
.vc-empty {
  margin-top: var(--s-5);
}

.vc-empty + .vc-empty { margin-top: var(--s-3); }

/* THE FIRST HALF OF THE REVIEW FINDING: the card's own padding. Flex column
   plus `height: 100%` is not decoration either, it is what lets `.go` below
   pin itself to the bottom of every card whatever the excerpt length. */
.vc-job {
  padding: var(--s-5);
  display: flex;
  flex-direction: column;
  height: 100%;
  transition: border-color var(--dur-normal) ease,
              box-shadow var(--dur-normal) ease,
              transform var(--dur-normal) var(--ease-out-soft);
}

.vc-job:hover {
  border-color: var(--gold);
  box-shadow: var(--shadow-m);
  transform: translateY(var(--lift-hover));
}

.vc-job h3 { font-size: var(--text-xl); }

.vc-job .tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--pill-row-gap);
  margin: var(--s-3) 0;
}

.vc-job > p {
  color: var(--ink-soft);
  font-size: var(--text-base);
  margin-bottom: var(--s-4);
}

.vc-job .go {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  margin-top: auto;
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--pine);
  transition: gap var(--dur-fast) ease, color var(--dur-fast) ease;
}

/* `:focus-visible` is in the group and the mock-up has no equivalent, because
   the mock-up's affordance is hover-only and a keyboard user reaching this
   link gets the global ring and nothing else. The ring is the requirement and
   it already passes; this is the same colour answer given to the same gesture,
   which is what `.icon-card` above does for the same reason. */
.vc-job .go:hover,
.vc-job .go:focus-visible,
.vc-job:hover .go {
  gap: .8em;
  color: var(--gold-deep);
}

/* The empty-state note. Narrower than the grid it stands in for: one sentence
   set to the full --container-width measure reads as a banner rather than as a
   note about what is missing. */
.vc-note {
  max-width: var(--container-narrow);
  margin: var(--s-6) auto 0;
}

/* ===========================================================================
 * PASIA: THE VACANCY DETAIL SURFACE
 * ===========================================================================
 * single-vacancy.php (`.vd-`), ported from _mockups/vacancy-detail.html. The
 * sibling the region above named as still open, and it arrived the same way and
 * for the same reason: vacancy-detail.html keeps its `vd-` rules in a
 * page-level <style> block, not in _mockups/css/site.css, so Phase 4 ported a
 * file that never held them. The template renders no section stack, so there is
 * no third place for them either. Here, always.
 *
 * MEASURED BEFORE PORTING, AND THE COUNT IN THE REPORT THAT RAISED IT WAS LOW.
 * Every stylesheet this page loads (tokens, base, components, layout,
 * search-overlay) was stripped of comments and searched. `.vd-` matched nothing
 * in any of them. It appears in this theme's CSS only inside prose, which is
 * exactly why grepping the namespace looks like an answer and is not.
 *
 * TWELVE classes were unstyled, not ten. The two the report missed are
 * `.vd-meta` and `.vd-glance-facts`, and they were missed because neither ever
 * appears in a `class` attribute: the template builds one facts list twice
 * through a closure and passes the class name as an argument, so the grep that
 * finds the other ten cannot see them. Both reach the DOM.
 *
 * ALREADY OWNED ABOVE, AND DELIBERATELY NOT RE-PORTED HERE. The mock-up
 * re-declares all four per page; two owners for one rule is how the button
 * system broke in Phase 0.
 *
 *   .pasia-ticklist   `.vd-list` in full. The mock-up's `.vd-list` block is
 *                     this component down to the 1.7em indent, the tick and
 *                     --gold-deep. Recorded deltas, both the component's: its
 *                     gap is .65em against .7em, and it carries a leading
 *                     --s-4 the mock-up sets to 0. `padding: 0` and `margin: 0`
 *                     are in neither, because base.css already resets padding
 *                     and tokens.css already resets margin, on everything.
 *   .pasia-crosslinks `.vd-links` in full, and declaration for declaration:
 *                     `text-align: center` plus the chipset's
 *                     `justify-content: center` and `margin-top: var(--s-3)`.
 *                     sections/next-steps.css says the archive's `.vc-links`
 *                     and this band are one component and it is right. Nothing
 *                     is below for it.
 *   .pasia-form-card  every declaration of the mock-up's `.vd-form` block
 *                     except its top margin: the ground, the hairline, the
 *                     radius, the --s-5 padding, the shadow, `.checkline`'s
 *                     --s-4 block margin, the full-width button, the success
 *                     alert's margin and its `.tag-line`. Its `.recipient` rule
 *                     also lands where the mock-up's `.vd-recipient` cannot:
 *                     the template writes `note recipient`, so the mock-up's
 *                     own selector matches nothing on this page.
 *   .card             the shell. No padding, on purpose, every consumer sets
 *                     its own, and `.vd-glance` below is this page's.
 *   .alert .note .field .input .textarea .checkline .chipset .chip .btn
 *                     the form controls and the primitives, unchanged.
 *
 * MOCK-UP RULES WITH NO CONSUMER IN THIS TEMPLATE, so they are not ported:
 *   .vd-main .callout      vacancy-detail.html puts a callout under "What we
 *                          offer", which this template does not render at all
 *                          and says why in its own header: build spec 3.3 gives
 *                          `vacancy` no field behind it.
 *   .vd-form .btn          the application card is the inert preview, so it
 *                          ships no submit button. `.pasia-form-card .btn`
 *                          already covers the day it does.
 *   .vd-form .vd-recipient superseded, see `.pasia-form-card .recipient` above.
 *
 * THREE REGIONS BELOW CANNOT BE REACHED ON SEEDED DATA, which is worth knowing
 * before anyone concludes from a browser that this file is finished. All six
 * demo vacancies carry `status = open` and an `apply_url` of
 * `mailto:placeholder@example.com`, and none carries a `closing_date`. So
 * `.vd-hero-closed` needs a closed or filled role, `.vd-form` and `.vd-file`
 * need a role with the apply URL cleared, and the "Applications close" fact row
 * needs a closing date. Set them on a draft vacancy to review any of the three.
 *
 * CONTRAST, MEASURED rather than estimated. The dark ground is the page hero,
 * --deep-forest under a --hero-overlay to --hero-wash-deep wash, and every
 * translucent value is composited before it is measured rather than read off
 * the token:
 *
 *   --on-dark-strong on --deep-forest      10.16:1  .vd-meta
 *   --paper          on --deep-forest      14.48:1  .vd-meta strong
 *   --gold-bright    on --deep-forest       7.20:1  .vd-hero-closed, and the
 *                                                   separator, which is
 *                                                   decorative anyway
 *   --ink-soft       on --paper             8.18:1  .vd-glance-facts li
 *   --ink            on --paper            16.28:1  .vd-glance h2, li strong
 *   --ink-soft       on --mist              7.45:1  .vd-main prose
 *   --forest         on --mist             10.57:1  the CV button's label
 *   --forest         on --forest-tint-05 over --paper
 *                                          10.64:1  the same on hover
 *
 * Every one clears AA at body size, so nothing needed correcting. Two figures
 * in the helper region above were wrong and have been corrected to agree with
 * this calculator and with the archive region: --ink-soft on --paper is 8.18:1
 * and --paper on --deep-forest is 14.48:1. Both overstated a ratio that was
 * comfortably past AA either way, so nothing rendered was ever at risk.
 * ======================================================================== */

/* --- the hero: the placeholder badge and the facts line ------------------- */

/* The paragraph holds the badge and nothing else, so `.ph-badge.on-dark` sets
   its own size and its own gold. Only the space above it is this page's to set,
   which is why the archive's two extra declarations are not copied down. */
.vd-hero-badge { margin-top: var(--s-4); }

/* The facts line, the first of two renderings of one list. A row with a dot
   between items rather than a column, which is all that separates it from
   `.vd-glance-facts` below; the data behind them is identical. */
.vd-meta {
  display: flex;
  flex-wrap: wrap;
  gap: .5em 0;
  margin-top: var(--s-4);
  font-size: var(--text-sm);
  color: var(--on-dark-strong);
}

.vd-meta li {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sep-gap);
}

/* THE SEPARATOR IS HIDDEN FROM THE ACCESSIBILITY TREE, and the two rules are
   one idiom rather than a duplicate. `.crumbs` solves the same problem with a
   real `span` carrying `aria-hidden`, which a pseudo-element cannot have, so
   this uses the alternative-text form of `content` the way `.ph-badge::before`
   above already does: the first rule is what a browser without the slash syntax
   keeps, the second replaces it wherever the syntax parses. Without it a screen
   reader announces the dot between every fact. */
.vd-meta li + li::before {
  content: "·";
  margin-inline: .9em;
  color: var(--gold-bright);
  font-weight: 700;
}

.vd-meta li + li::before { content: "·" / ""; }

.vd-meta strong {
  color: var(--paper);
  font-weight: 600;
}

/* The closed and filled notice. THE MOCK-UP HAS NO COUNTERPART FOR THIS ONE:
   vacancy-detail.html draws a single open role and never draws this state, so
   there is nothing signed off to copy and the template named the class itself.
   Left unstyled it would inherit the hero's --paper at body size and sit flush
   against the facts line, because tokens.css resets margin on everything, so
   "no rule" is not a neutral outcome here.

   The colour is not a new decision either. AGENTS.md 1.7 closes the set of text
   colours permitted on a forest band to --paper, --on-dark-strong, --on-dark
   and --gold-bright, and this sentence is the one piece of news in the hero,
   which is the job --gold-bright already does for `.pg-hero h1 em` and for the
   `.ph-badge.on-dark` sitting a line above it. 7.20:1. */
.vd-hero-closed {
  margin-top: var(--s-4);
  color: var(--gold-bright);
  font-weight: 600;
}

/* --- the split ----------------------------------------------------------- */

/* THE GLANCE CARD IS FIRST IN SOURCE AND SECOND ON SCREEN, which is the whole
   reason `order` appears here at all. On a phone the card is the useful thing
   first, so it leads the document; from 861 up it belongs in the narrower
   right-hand track, so `.vd-main` is pulled ahead of it and the media query
   puts it back. Source order stays the reading order in both.

   --gap-split rather than the mock-up's `clamp(2rem, 5vw, 4rem)`, which is that
   token's value exactly. tokens.css added it because three copies of this one
   literal is what AGENTS.md 1.1 calls a missing token, and this would have been
   the fourth. */
.vd-layout {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: var(--gap-split);
  align-items: start;
}

/* `min-width: 0` ON BOTH TRACKS, where the mock-up guards only one. An `fr`
   track is `minmax(auto, …)` and that `auto` floor is the min-content width of
   the grid ITEM, so either column can push the document wider than the viewport
   on its own. `.pasia-media-row` above carries the measurement and the argument
   in full. The aside is the track the mock-up leaves open and it is the one
   holding client-typed district and employment-type values inside a flex row,
   whose items refuse to shrink below their own min-content. One declaration,
   and it closes the pair rather than half of it. */
.vd-main,
.vd-aside { min-width: 0; }

.vd-main { order: -1; }

/* The same sticky idiom as `.hp-aside`, the equivalent rail on the helper
   profile, and `var(--header-h)` rather than the mock-up's literal per
   AGENTS.md 6. --header-h plus --s-4 is 96px, which is the mock-up's own value
   exactly rather than an approximation of it. */
.vd-aside {
  position: sticky;
  top: calc(var(--header-h) + var(--s-4));
}

@media (max-width: 860px) {
  .vd-layout { grid-template-columns: 1fr; }
  .vd-main { order: 0; }
  .vd-aside { position: static; }
}

/* --- the glance card ----------------------------------------------------- */

/* `.card` IS A BARE SHELL AND THIS IS THE PADDING IT DOES NOT CARRY. Without it
   the heading, the facts and the button all sit against the card's own
   hairline, which is the defect `.vc-job` answers one region above and this is
   the same answer. Do not move it onto `.card`: the mock-up's `.card` has no
   padding either, and every other consumer already sets its own. */
.vd-glance { padding: var(--s-5); }

/* --text-lg, and it is the mock-up's own token rather than a choice made here.
   --card-title-size is deliberately not reused: tokens.css says so in that
   token's docblock, it names one card, and this is a panel title beside no
   avatar and inside no pixel-floored track. */
.vd-glance h2 {
  font-size: var(--text-lg);
  margin-bottom: var(--s-3);
}

/* The second rendering of the same facts, stacked rather than in a row. The
   trailing margin is the gap to whatever follows, which is the apply button on
   an open role and the honest note on a closed one. `list-style`, `padding` and
   the two zero margins in the mock-up's shorthand are all reset globally
   already, so only the third value of it survives the port. */
.vd-glance-facts {
  display: grid;
  gap: .7em;
  margin-bottom: var(--s-4);
}

.vd-glance-facts li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sep-gap);
  font-size: var(--text-sm);
  color: var(--ink-soft);
}

.vd-glance-facts li strong {
  color: var(--ink);
  font-weight: 600;
}

.vd-glance .btn {
  width: 100%;
  justify-content: center;
}

/* --- the main column ----------------------------------------------------- */

/* THE CHILD COMBINATORS ARE THE DELTA, and they are load-bearing rather than
   tidiness. The mock-up writes `.vd-main section + section`, `.vd-main h2` and
   `.vd-main p` as descendant selectors because on that page the main column
   holds nothing but its own prose. It does not hold nothing but its own prose
   here:

     .vd-main h2  would tie at (0,1,1) with `.pasia-rte h2` above and, loading
                  later, add a bottom margin to every heading in a
                  client-written description.
     .vd-main p   would beat the colour `.alert-info` and `.alert-success` set
                  on their own container, because a rule on the element itself
                  wins against an inherited value at any specificity. This page
                  renders up to three alerts inside `.vd-main` where the mock-up
                  renders one, and all three would lose their tone.

   So each is bound to the section's own children, which is exactly the set the
   mock-up's rule paints on the mock-up's markup. */
.vd-main > section + section { margin-top: var(--s-6); }

.vd-main > section > h2 {
  font-size: var(--text-2xl);
  margin-bottom: var(--s-3);
}

/* The second selector is the other half of the same delta, in the opposite
   direction. "About the role" is a bare paragraph in the mock-up and is wrapped
   in `.pasia-rte` here, one level deeper than a child combinator reaches, so it
   is named rather than dropped. */
.vd-main > section > p,
.vd-main .pasia-rte p { color: var(--ink-soft); }

/* THE `#apply` ANCHOR HAS TO CLEAR THE STICKY HEADER. layout.css pins
   `.site-header` and tokens.css sets `scroll-behavior: smooth`, so the glance
   card's "How to apply" button scrolled its own target underneath the header
   and landed the visitor mid-form. Same calc as `.vc-roles`, `.hp-aside` and
   the prose-heading rule above, and `var(--header-h)` rather than a literal per
   AGENTS.md 6.

   On the class, not on the id, matching `.vc-roles`: the archive carries
   `class="section vc-roles" id="roles"` for exactly this, no id selector exists
   anywhere in this theme's CSS, and a blanket `[id]` rule would reach every
   `.section[id]` on all 27 pages. That survey belongs to whoever can check
   them, not to this fix. */
.vd-apply { scroll-margin-top: calc(var(--header-h) + var(--s-4)); }

/* --- the application card ------------------------------------------------ */

/* The one declaration `.pasia-form-card` does not carry, and it has to be here
   rather than there: the card follows a lead-in paragraph on this page and is
   the first thing in its column on the other two, and the global margin reset
   means no default absorbs the difference. */
.vd-form { margin-top: var(--s-4); }

/* THE CV CONTROL'S OWN BUTTON, which no other rule in this theme touches: a
   file input is the only control that ships a browser-drawn button inside
   itself, and left alone it renders in the user agent's system font and system
   chrome inside a card that is otherwise entirely --sans and --r-pill. Nothing
   here is reachable on seeded data; see the header.

   The 1.5px border is a component dimension with no token behind it, the same
   practice tokens.css follows for `.eyebrow::before` and components.css for the
   60px avatar on `.hcard`. --line rather than a token of its own is deliberate:
   it is the border of the `.input` this button sits inside, and the two should
   never be able to disagree. */
.vd-file::file-selector-button {
  border: 1.5px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--mist);
  color: var(--forest);
  font-family: var(--sans);
  font-size: var(--text-sm);
  font-weight: 600;
  padding: .45em 1em;
  margin-right: .9em;
  cursor: pointer;
  transition: border-color var(--dur-fast) ease,
              background-color var(--dur-fast) ease;
}

.vd-file::file-selector-button:hover {
  border-color: var(--pine);
  background: var(--forest-tint-05);
}
