/*
 Theme Name:   CharterSpace.IO
 Description:  Child theme for charterspace.io — wires SCF local-JSON sync to acf-json/,
               enqueues the parent Divi stylesheet, and hosts site-specific assets.
 Author:       CharterSpace.IO Platform Team
 Template:     Divi
 Version:      0.4.3.1
*/

/* =============================================================================
   CHARTERSPACE DESIGN SYSTEM — docs/23-divi-build-plan.md items 0.2, 0.3, 0.4
   =============================================================================

   WHAT THIS FILE IS
   -----------------
   Two things, in order:

     1. The token layer (`:root`). Every colour, size, space and radius the site
        uses, named once. Divi's Global Presets cannot read CSS custom
        properties, so the presets are configured by hand in the Builder UI
        against these same literal values — see "DIVI GLOBAL PRESETS" at the
        bottom of this block. The tokens are the source of truth; the presets
        are a hand-copy of them.

     2. Component CSS for the four `charterspace-core` shortcodes, which render
        complete, correct, entirely unstyled HTML
        (`mu-plugin/charterspace-core/includes/shortcodes.php`). Browse, City
        Page, Landing and RFQ all render the same card partial, so this is
        written once and consumed four times.

   Component CSS is scoped under a `cs-` class, so it cannot fight Divi's
   cascade outside its own blocks. Three things are deliberately global and are
   confined to the PAGE CANVAS section below: the page background, the body/
   heading text colour that has to change with it, and `.screen-reader-text`
   (a WordPress core convention the shortcodes emit and no other file defines).


   VISUAL DIRECTION — "bold corporate + sci-fi"
   --------------------------------------------
   docs/06-divi-templates.md Landing item 1 is the only visual direction that
   exists. Translated concretely:

   *Corporate* is the substrate. Deep neutral spine, one confident brand blue,
   generous whitespace, flat surfaces, no gradient washes. This is a page where
   someone commits six figures of cargo to a rocket; it has to read like a
   freight desk, not a game menu.

   *Sci-fi* is carried by three moves only, so it never becomes costume:

     a. THE DATA IS THE SCI-FI. Mass in kg, volume in m³, launch windows in
        UTC. Every numeric value renders in a monospace face with tabular
        figures (`--cs-font-mono`), so columns of specs align down the page and
        read as instrumentation. This is the single highest-leverage decision
        in the whole palette and it costs no webfont.
     b. MICRO-LABELS. Eyebrows, `<dt>` labels, badges and filter headings are
        uppercase, 11–12px, letterspaced. Terse system-label register.
     c. HAIRLINES. A 3px brand→cyan accent edge on the pieces that lead a
        journey step (the RFQ leg summary, a hovered result card). Thin, bright,
        structural — never decorative fill.

   RELATION TO LCARS. `assets/lcars/lcars.css` owns a real LCARS palette for
   the opt-in `/console` easter egg (docs/15), and docs/23 Q9 explicitly says
   that palette must not bleed into the rest of the site. The main site is
   *related* to it, not made of it: it borrows the dark instrument-panel
   surface and the saturated-accent-on-dark logic, and borrows none of the
   colours, the elbow geometry or the Antonio typeface. The one place actual
   LCARS colour appears is `.cs-lcars-stripe` — see Q9's answer at the bottom.

   WHY THIS NEUTRAL RAMP. `--cs-ink-*` is a cool slate ramp with published,
   even contrast steps rather than hand-mixed greys. Cool neutrals sit under a
   blue brand without muddying it, and an evenly-stepped ramp means "one step
   darker" is a safe edit for anyone who touches this later without re-running
   a contrast check.

   CONTRAST. Every foreground/background pair used for text in this file was
   computed and meets WCAG AA (4.5:1) at its rendered size; most exceed 6:1.
   Form-control borders meet 1.4.11's 3:1 non-text minimum via
   `--cs-border-strong`, and the outer edge of a surface — which is what tells
   a card apart from the canvas now that the fill cannot — is measured too, via
   `--cs-border-surface`. `--cs-border` is left for internal hairlines inside
   an already-bounded box and is never the sole indicator of anything.

   The whole set was recomputed when the page went dark — the earlier figures
   were all measured against an implicit white page and none of them survived
   the change. The ratios quoted beside the semantic tokens are the current
   ones, measured against `--cs-surface`; the canvas and the sunken surface are
   both darker, so every pair scores higher there than the figure shown.


   THE ONE RULE FOR EDITING COLOUR HERE
   -------------------------------------
   **On a near-black canvas, every cue has to ADD light. Anything that works by
   removing light has no range left.**

   Relative luminance compresses to almost nothing down here — `--cs-void-900`
   sits at 0.003 — so the intuitions that are correct on a white page invert.
   Two were acted on during this build before being measured, and both were the
   same mistake wearing different clothes:

     "A darker shadow reads harder on a dark page."  It does not. Deepened to
     rgba(0,0,0,0.75) it measured 1.040:1 against the canvas — *less* than the
     0.18 light-page original managed on white. There is nothing below the
     canvas to cast onto. See the note where the shadow tokens used to be.

     "A deeper canvas gives the surfaces above it more room."  It does not.
     The card fill is 1.19:1 against the canvas and cannot be rescued from
     either end. See the note above `--cs-border-surface`.

   Both are true of paint and false of light. Before adding any cue that works
   by darkening — a shadow, a deeper well, a recessed surface — compute the
   composited value against what it sits on. If the answer is under about 1.2:1
   it is decoration that will not survive a render, and the cue you actually
   want is a lighter border, a lit inset, or type.


   THE `md` BREAKPOINT — item 0.4
   -------------------------------
   docs/06 references `md` five times as the point where tables and card rows
   collapse to stacked label/value pairs (lines 233, 471, 881, 917, 988) and
   never defines it. **`md` = 768px.**

   Three reasons, in order of weight:

     1. It is Divi's own phone/tablet boundary. Divi 4 emits
        `@media (max-width: 767px)` for its Phone tab and
        `@media (max-width: 980px)` for Tablet. So 768px is not a *new*
        breakpoint — it is the name for a line Divi is already collapsing its
        own columns on. A table that stacks under `md` stacks in exactly the
        viewport range Divi is already stacking rows in, so the page collapses
        once instead of twice, 1px apart.
     2. 768px is what `md` means in every mainstream framework (Bootstrap,
        Tailwind, Foundation). docs/23 item 0.4's stated risk is "two builders
        will pick two values"; the value every builder already assumes is the
        one least likely to be picked differently.
     3. Divi's 980/981px line is `lg`, not `md` — it is the tablet/desktop
        boundary, and using it for `md` would mean two-column spec tables never
        appear on a tablet at all.

   Divi's 980px line is recorded here as `--cs-bp-lg: 981px` for the same
   collapse-once reason.

   All queries in this file are mobile-first `min-width`, per docs/06 line 5's
   "design the 480px breakpoint first, then scale up" mandate: the base styles
   ARE the 480px design, and 768/981 only add to them. Writing only `min-width`
   also means there is no fractional-pixel gap between a `max-width: 767px`
   rule and a `min-width: 768px` one.

   NOTE: CSS custom properties cannot be used inside a media query condition.
   `--cs-bp-*` below is documentation and is readable from JS; the literal
   pixel value must be repeated in each `@media`. If you change one, grep for
   the literal.


   DIVI GLOBAL PRESETS — item 0.3, configured by hand in the Builder
   -----------------------------------------------------------------
   These cannot be set from a file or WP-CLI without unsupported postmeta
   surgery. Open Divi → Theme Options / Theme Customizer and the Builder's
   preset system and enter these values, which are the same literals as the
   tokens below. Full spec with click paths is in the Phase 0 handoff; the
   short form:

     Body font          Inter / Open Sans fallback, 400, 16px, line-height 1.6,
                        colour #E2E8F0
     Headings font      Barlow Semi Condensed (or Inter), 700, colour #F8FAFC,
                        line-height 1.2, letter-spacing -0.01em
     H1 36px / H2 30px / H3 24px / H4 20px  (Divi Phone tab: 30 / 24 / 22 / 20 —
                        these are the two ends of the clamp() on
                        --cs-text-3xl / 2xl / xl / lg, so a heading rendered by
                        a shortcode and one rendered by Divi match at both ends)
     Link               #9CBCF8, hover #C7DAFC, underline on hover
     Button preset      bg #2E6BE6, text #FFFFFF, 16px/600, radius 3px,
                        padding 8px 24px with min-height 44px,
                        1px border #2E6BE6, hover bg+border #1450D2,
                        no Divi button icon
     Button secondary   transparent bg, text #5E8FF0, 1px border #2E6BE6,
                        hover bg #10224A, hover border+text #5E8FF0/#E8EFFC
     Section spacing    desktop 96px top/bottom, tablet 64px, phone 48px
     Row spacing        desktop 32px gutter, max width 1080px
     Light section      bg #F8FAFC, text #1E293B, headings #0B1220 — the
                        inverse carve-out, for a section that deliberately
                        breaks out of the canvas. There is none today.


   THE DIVI CUSTOMIZER IS NOT OPTIONAL HERE — and it is not this file
   ------------------------------------------------------------------
   Divi writes its Customizer colours to a cached stylesheet
   (`wp-content/et-cache/<n>/<n>/global/et-divi-customizer-global.min.css`)
   that is printed AFTER the child stylesheet. At equal specificity it wins.
   `et_divi[accent_color]` alone emits a bare `a { color: … }`, so a dark page
   with a light-page accent colour renders every link in `the_content()` at
   about 1.5:1 no matter what this file says. Raising the specificity here to
   out-shout it would mean an ID selector on `a`, which would then also
   out-shout any per-module link colour a builder sets later — a worse trade.

   The colour therefore lives in the Customizer option, and
   `wp-cli/08-theme-builder-chrome.php` sets it to `--cs-brand-300` (#9CBCF8)
   on every site and flushes the cache, so the value is still in version
   control and still applied by a script rather than by hand.


   `#left-area` BEATS EVERY CLASS RULE IN THIS FILE
   ------------------------------------------------
   Divi's dynamic CSS styles post content through ID selectors — `#left-area
   ul`, `#left-area ul li` and friends, specificity (1,0,1). Nothing
   class-only in this file can ever win against them, at any source order,
   because an ID outranks any number of classes. Any `<ul>`, `<ol>` or `<li>`
   this stylesheet draws inside `the_content()` is affected: on the portal
   pages `#left-area` is a real ancestor (`#post-N > #left-area >
   #content-area > #main-content`).

   Four rules hit this in one session — console nav, footer nav, and the quote
   queue's filter tabs twice — and each was half-fixed, because the fix was
   applied only to the property someone happened to notice. Two rules make the
   difference between fixing it once and fixing it four times:

     1. Counter a shorthand WITH a shorthand. Divi's declaration here was
        `padding: 0 0 23px 1em`; an unqualified `padding: 0` loses all four
        sides at once, and `padding-left: 0 !important` fixes one and leaves
        three. Same for `list-style` vs `list-style-type`/`-image`.
     2. Measure the MOBILE width, not the desktop one. The losing value is
        often em-based (`1em` here), so it resolves against the element's own
        font-size and the damage GROWS as text scales down the breakpoints —
        14px at 1440 became 16px at 390. A desktop measurement of an em-based
        override is a lower bound, not an upper one.

   Diagnosing it: this class reads exactly like "my stylesheet isn't loading",
   which sends people to the deploy pipeline instead of the cascade. The check
   that settles it in one step is to inject the candidate CSS LAST into the
   live page (Playwright `addStyleTag`, or devtools) — the most favourable
   source order possible. If the computed value is still unchanged, it is
   specificity, and the answer is `!important` on the shorthand plus a
   reinforcing rule on the child element. See `.cs-quote-queue__views ul`
   below for the worked example.
   ============================================================================= */

:root {
	/* -- Neutral spine ------------------------------------------------------
	   Cool slate. 900 is the dark instrument surface (hero, footer, app shell);
	   050/100 are the sunken surfaces that separate a panel from the page. */
	--cs-ink-900: #0B1220;
	--cs-ink-800: #131C2E;
	--cs-ink-700: #1E293B;
	--cs-ink-600: #475569;
	--cs-ink-500: #64748B;
	--cs-ink-400: #94A3B8;
	--cs-ink-300: #CBD5E1;
	--cs-ink-200: #E2E8F0;
	--cs-ink-100: #F1F5F9;
	--cs-ink-050: #F8FAFC;
	--cs-white: #FFFFFF;

	/* -- Deep space canvas --------------------------------------------------
	   The page is dark. This ramp is the substrate the whole site sits on and
	   is separate from `--cs-ink-*`, which stays the published neutral ramp:
	   the ink dark end (900 #0B1220) is a *section* colour chosen to sit on a
	   white page, and reusing it as both the canvas and the cards on it would
	   make every card boundary vanish.

	   Void 900 is the canvas. It is a very dark navy, not #000 — pure black
	   under a blue brand reads as an unstyled page or a rendering fault, and it
	   leaves nowhere to go for the sunken surfaces that have to sit *below* the
	   canvas visually. Each step up is a surface that can be stacked on the one
	   before it and still be told apart. */
	--cs-void-900: #060A13; /* page canvas */
	--cs-void-800: #0C1526; /* sunken — recessed panels, disabled cards */
	--cs-void-700: #141E33; /* the default card/panel surface */
	--cs-void-600: #1E2B47; /* raised panel */
	--cs-void-500: #2B3A57; /* decorative edge */
	--cs-void-400: #33456B; /* emphasis fill (the vehicle badge) */

	/* -- Brand --------------------------------------------------------------
	   A single deep azure, now with a light end. On a dark canvas the 600/700
	   steps are the *fills*, not the text: 600 on the canvas is 2.93:1, below
	   1.4.11's 3:1, so the fill is 500 and every brand-coloured glyph, link and
	   focus ring comes off the 200–400 steps instead. 900 is the tinted callout
	   background that 100 used to be on a white page. */
	--cs-brand-900: #10224A;
	--cs-brand-800: #072E7D;
	--cs-brand-700: #0B3FA8;
	--cs-brand-600: #1450D2;
	--cs-brand-500: #2E6BE6;
	--cs-brand-400: #5E8FF0;
	--cs-brand-300: #9CBCF8;
	--cs-brand-200: #C7DAFC;
	--cs-brand-100: #E8EFFC;

	/* -- Accent -------------------------------------------------------------
	   Instrument cyan, and deliberately only one step of it. Decorative and
	   structural only — the far end of an accent edge, a hairline, a focus ring
	   on `--cs-ink-900`. It is never text and never a fill, which is what keeps
	   the sci-fi register a detail rather than a costume. */
	--cs-accent-400: #22D3EE;

	/* -- Signal -------------------------------------------------------------
	   Status meaning. Colour is never the sole carrier: every status badge is
	   a CSS-drawn shape plus a text label plus this colour (docs/06 QA note).

	   Inverted for the dark canvas: the light tints these used to be (#E6F4EC
	   and friends) would read as five bright stickers stuck onto a dark card,
	   and the dark `*-text` values on them held contrast only because the badge
	   was a light island. Now the tint is a deep wash of the hue and the text is
	   the light end of it — same construction, same 4.5:1 obligation, opposite
	   polarity. Ratios are text-on-its-own-tint. */
	--cs-success-text: #6EE7A8; /* 9.99:1 on tint */
	--cs-success-tint: #0E2A1C;
	--cs-warning-text: #F5C177; /* 8.98:1 on tint */
	--cs-warning-tint: #33260C;
	--cs-info-text: #A6C4FA;    /* 8.79:1 on tint */
	--cs-info-tint: #12224A;
	--cs-danger-text: #FCA5A0;  /* 8.51:1 on tint */
	--cs-danger-tint: #3A1512;

	/* -- Outcome controls (.cs-button--accept / --reject) -------------------
	   A separate ramp from the `*-text`/`*-tint` badge pair above, because a
	   badge and a button have different jobs.

	   The tints cannot be a button fill at all — they measure ~1.1:1 against
	   any surface here, i.e. invisible as a shade. The `*-text` colours *can*
	   be, and clear 1.4.11 as their own boundary, but they are mint and salmon
	   rather than green and red; and `--cs-danger-text` is simultaneously the
	   colour of `.cs-quote-detail__field-error` and `.cs-quote-queue__notice
	   --error`, so a reject button built from it wears the error hue while
	   sitting inches from real error text.

	   These are saturated instead, with the **edge** carrying 1.4.11 and the
	   fill free to be the "shaded" look — the same division of labour
	   `.cs-button`'s own hover comment describes. `*-press` is the hover fill,
	   with the edge held constant so the boundary does not move on interaction.

	   **Ratios below are against `--cs-surface-sunken` (#0C1526)**, which is
	   `.cs-quote-detail__action`'s background — the panel these buttons are
	   actually inside, not the section around it. Getting that pairing wrong is
	   the recurring failure on this surface, three times now: brand-500 stayed
	   documented against the page canvas after being moved onto a panel;
	   these were briefly documented against `--cs-void-700`, then against
	   `--cs-surface-panel`, while the same commit that set the ramp also set
	   the panel background under it. When a change touches both a surface and
	   the things sitting on it, re-measure the pairs — do not carry the old
	   numbers forward. Measured at render, not derived from the palette.

	   Label ratios below name `#FFFFFF` rather than saying "white", for the
	   same reason. `--cs-on-brand` resolves to `--cs-white` = #FFFFFF, and the
	   figures were briefly recorded for #F8FAFC — a colour word survives a
	   token substitution without ever looking wrong, so a ratio quoted in prose
	   carries its literal here, at both ends of the pair. */
	--cs-accept-fill: #1F7A4D;
	--cs-accept-edge: #2E9E63;  /* 5.38:1 on #0C1526; #FFFFFF on fill 5.32:1 */
	--cs-accept-press: #1A6842; /* #FFFFFF on fill 6.77:1 */
	--cs-reject-fill: #B3261E;  /* 2.79:1 on #0C1526; 3.43:1 vs the error pink — see below */
	--cs-reject-edge: #E5534B;  /* 4.93:1 on #0C1526; #FFFFFF on fill 6.54:1; 1.94:1 vs the error pink */
	--cs-reject-press: #961F18; /* #FFFFFF on fill 8.38:1 */

	/* -- Semantic surfaces --------------------------------------------------
	   The semantic layer is where light and dark differ; almost every component
	   rule below reads these and nothing else, which is why the page could be
	   turned over without touching the components. */
	--cs-bg-page: var(--cs-void-900);
	--cs-surface: var(--cs-void-700);
	--cs-surface-sunken: var(--cs-void-800);
	--cs-surface-panel: var(--cs-void-600);
	--cs-surface-dark: var(--cs-ink-900);

	/* A brand-tinted callout, and the fill behind an emphasised badge. Both
	   were light tints with dark text; both are now the reverse. */
	--cs-surface-accent: var(--cs-brand-900);
	/* 13.47:1 against a flat --cs-surface-accent. That number still holds everywhere this token is
	   used over a flat fill — but NOT on the homepage CTA since ER-12 (2026-09-03), which puts a
	   rotating Earth behind it. There the ratio is not a property of this pair alone: it is
	   guaranteed by .cs-home-cta__copy::before's 0.86 scrim (home.css), which bounds the composite
	   against a pure-white worst case and so holds at any rotation angle. Measured floor there is
	   13.44:1 for this token and 8.35:1 for the smaller --cs-brand-200 legal line. Flagged by
	   Celeste-2 in ER-12 review: a documented ratio that quietly stops being true is worse than no
	   number, because the next reader reasons from it. */
	--cs-text-on-accent: var(--cs-brand-100);  /* 13.47:1 on a flat fill — see above */
	--cs-surface-emphasis: var(--cs-void-400);
	--cs-text-on-emphasis: var(--cs-ink-050);  /* 9.11:1 */

	/* Text on a brand-600/500 fill — the primary button and the current page
	   number. Named rather than `--cs-white` at each call site so that a future
	   lighter brand fill has one place to flip to a dark foreground. */
	--cs-on-brand: var(--cs-white);            /* 4.81:1 on --cs-brand-500 */

	/* Ratios are against `--cs-surface`; each is higher on the canvas and on
	   the sunken surface, which are both darker. */
	--cs-text: var(--cs-ink-200);       /* 13.48:1 */
	--cs-text-strong: var(--cs-ink-050);/* 15.89:1 */
	--cs-text-muted: var(--cs-ink-400); /*  6.48:1 */
	--cs-text-on-dark: var(--cs-ink-200);

	--cs-link: var(--cs-brand-300);       /* 8.68:1 */
	--cs-link-hover: var(--cs-brand-200); /* 11.77:1 */

	/* Three edge weights, not two, because the dark canvas took a job away from
	   the fill and gave it to the border.

	   On a white page a card was identified by being white. Here the card fill
	   is 1.19:1 against the canvas, and it cannot be rescued from either end:
	   lifting the surface far enough to reach 3:1 lands around #2B4270, where
	   `--cs-text-muted` on it drops to 3.87:1 and fails AA, and the canvas is
	   already at luminance 0.003 and cannot go down. The perceptual span of the
	   whole `--cs-void-*` ramp is only 2.08:1 — relative luminance compresses
	   hard near black, so evenly-spaced hex steps are not evenly-spaced
	   perceptual ones, and the even-ramp reasoning that holds for `--cs-ink-*`
	   on white does not survive the move.

	   So the OUTER EDGE of a surface is structural and is measured:
	   `--cs-border-surface` at 4.75:1 on the card, 5.66:1 on the canvas. An
	   internal hairline inside an already-bounded box does not have to be
	   found, so `--cs-border` stays quiet at 1.46:1 — a grid of 4.75:1 rules
	   inside a card would read as a table.

	   `--cs-border-strong` keeps its old job: control boundaries subject to
	   WCAG 1.4.11's 3:1. It and `--cs-border-surface` are the same value today
	   and are named apart because they answer to different rules — and the
	   asymmetry is the reason that pays off: the obvious softer value, #4E6088,
	   is 3.16:1 on the canvas but only 2.65:1 on a card fill. It would work for
	   the chrome edges and fail for the card edge, so the two tokens cannot be
	   softened together even though they read as one value today.

	   If a render says 4.75 is too loud at 480px, the fix is more likely to be
	   WHICH edges use the token than what it is worth. `.cs-leg-grid` is single
	   column at base, so a card is near full-bleed: a fully-enclosed box that is
	   nearly as wide as its container stops reading as an object on a page and
	   starts reading as a frame around it, and that would look wrong at any
	   contrast. The move there is a top-only rule below 768px — separation from
	   the neighbour above rather than enclosure — restoring the full box at
	   >=768px where cards sit in a grid with real canvas around them. Same
	   token, same value.

	   The likelier defect is the opposite one: a 1px hairline disappearing under
	   fractional DPR on a real phone. Check that before checking brightness. */
	--cs-border: var(--cs-void-500);
	--cs-border-strong: #7A8AA5;
	--cs-border-surface: #7A8AA5;

	/* -- Type scale ---------------------------------------------------------
	   ~1.25 ratio off a 16px base. The three display sizes use clamp() so the
	   480px design and the 1280px design are one declaration, not a Divi
	   responsive override per module. */
	--cs-font-mono: ui-monospace, "SFMono-Regular", "JetBrains Mono", "Roboto Mono", Menlo, Consolas, monospace;

	--cs-text-xs: 0.75rem;      /* 12px — micro-labels, badges, flags */
	--cs-text-sm: 0.875rem;     /* 14px — spec values, form labels, meta */
	--cs-text-base: 1rem;       /* 16px — body */
	--cs-text-md: 1.125rem;     /* 18px — lead paragraph */
	--cs-text-lg: 1.25rem;      /* 20px — card title, h4 */
	--cs-text-xl: clamp(1.375rem, 1.2rem + 0.8vw, 1.5rem);   /* h3 */
	--cs-text-2xl: clamp(1.5rem, 1.2rem + 1.4vw, 1.875rem);  /* h2 */
	--cs-text-3xl: clamp(1.875rem, 1.4rem + 2.2vw, 2.25rem); /* h1 */

	--cs-leading-tight: 1.2;
	--cs-leading-snug: 1.35;
	--cs-leading-normal: 1.6;

	--cs-tracking-label: 0.08em;  /* the uppercase micro-label register */
	--cs-tracking-tight: -0.01em; /* headings only */

	/* -- Spacing scale ------------------------------------------------------
	   4px base, doubling from sm upward. t-shirt names rather than numbers so
	   `--cs-space-sm` is never misread as "4px" the way `--cs-space-4` is. */
	--cs-space-3xs: 0.25rem;  /* 4  */
	--cs-space-2xs: 0.5rem;   /* 8  */
	--cs-space-xs: 0.75rem;   /* 12 */
	--cs-space-sm: 1rem;      /* 16 */
	--cs-space-md: 1.5rem;    /* 24 */
	--cs-space-lg: 2rem;      /* 32 */
	--cs-space-xl: 3rem;      /* 48 */
	--cs-space-2xl: 4rem;     /* 64 */
	--cs-space-3xl: 6rem;     /* 96 */

	/* -- Radius and motion --------------------------------------------------
	   Small radii on purpose. Rounded-pill everything reads consumer; 3–6px
	   reads instrument panel while staying short of LCARS' 90px elbows. */
	--cs-radius-sm: 3px;
	--cs-radius-md: 6px;

	/* NO SHADOW TOKENS, deliberately. There were two, deepened to
	   rgba(0,0,0,0.55/0.75) on the theory that a black shadow reads on a dark
	   page. Measured, they composite to 1.030:1 and 1.040:1 against the canvas
	   — below any perceptual threshold, and less than the 1.131/1.482 the
	   light-page originals managed. Darkening a shadow when the page goes dark
	   is intuitive and backwards: there is nothing below #060A13 to cast onto.

	   So elevation is spent as a mechanism here — fill cannot carry it (1.19:1)
	   and neither can shadow. `--cs-border-surface` carries the whole of it, and
	   hierarchy otherwise comes from the type scale, the mono/tabular numeric
	   treatment and spacing. That makes this a typographic visual system rather
	   than a material one, which is worth knowing before reaching for depth.

	   If a render says a second cue is wanted, the move on dark UI is a lit top
	   edge, not a drop shadow: `inset 0 1px 0 rgba(255,255,255,0.10)` measures
	   1.35:1 on the card surface (0.18 gives 1.78:1).

	   Apply it to `.cs-leg-card` and NOT to `.cs-leg-summary`. Both have a
	   `::before` owning the top 3px, but they differ: the card's is `opacity: 0`
	   at rest and fades in on hover, so an inset would show exactly when it is
	   doing the grouping work and be covered exactly when the hairline and the
	   brand-500 border have taken over. The summary's is permanent, so an inset
	   there is dead pixels — and it needs none, being a single bounded element
	   that already carries a lit 3px edge. Measuring 1.35:1 and then applying it
	   to both is the mistake this paragraph exists to prevent. */

	--cs-transition: 150ms ease;

	/* Minimum tap target, docs/06 line 970 and lines 471/534's 44px row rule. */
	--cs-tap-target: 2.75rem; /* 44px */

	/* -- Breakpoints (documentation — see the note above) -------------------
	   Three, not four: Divi's row container caps at 1080px, so there is no
	   viewport at which a fourth, wider step would change anything. */
	--cs-bp-sm: 480px;  /* the mandated design-first width; base styles ARE this */
	--cs-bp-md: 768px;  /* item 0.4's answer; Divi's phone/tablet boundary */
	--cs-bp-lg: 981px;  /* Divi's tablet/desktop boundary */

	/* -- LCARS, quarantined -------------------------------------------------
	   docs/23 Q9. The Landing hero's "LCARS accent stripe" is answered as a
	   purely decorative segmented bar carrying no text and therefore no
	   contrast obligation. These four values exist for `.cs-lcars-stripe` and
	   for nothing else on the public site; they mirror
	   `assets/lcars/lcars.css`, which owns the real palette for /console. */
	--cs-lcars-orange: #FF9C00;
	--cs-lcars-blue: #99CCFF;
	--cs-lcars-lilac: #CC99CC;
	--cs-lcars-red: #CC6666;
}

/* =============================================================================
   PAGE CANVAS — the only globally-scoped rules in this file
   The site is a marketplace for residual capacity on real launches, and it now
   looks like it: the page itself is deep space and every surface is a lit panel
   on top of it.

   Three Divi defaults have to be answered, not just `body`:

     `body { background-color:#fff; color:#666 }`   Divi's own base.
     `#main-content { background-color:#fff }`      An ID, so it beats a plain
                                                    `body` rule. Without this
                                                    line the canvas would only
                                                    show in the margins and
                                                    every page would still be a
                                                    white sheet.
     `h1,h2,h3,h4,h5,h6 { color:#333 }`             Near-black headings.

   `#page-container` and `#main-content` go transparent rather than dark so the
   canvas is painted exactly once and a section that wants its own background
   (the footer, a hero) still gets it.

   `!important`, added 2026-08-18: Divi's own parent stylesheet already carries
   `.page.et_pb_pagebuilder_layout #main-content{background-color:transparent}`
   — but that class pair only appears on a builder-edited singular `page`. A
   Theme Builder-driven CPT archive (Browse, City Index) gets body classes like
   `archive post-type-archive-empty_leg et-tb-has-template` instead, so Divi's
   specific rule never matches there and this file's plain `#main-content`
   selector is left tied at equal specificity with Divi's own
   `#main-content{background-color:#fff}` — decided by cascade order, which is
   exactly the kind of tie the accent_color fix elsewhere in this project had
   to stop relying on. `!important` makes the outcome the same regardless of
   which body classes a given template produces.

   Divi's accent/link colour is NOT settable from here — see the note in the
   header block above.
   ============================================================================= */

body {
	background-color: var(--cs-bg-page);
	color: var(--cs-text);
}

body #page-container,
#main-content {
	background-color: transparent !important;
}

/* Divi's builder stylesheet carries a fourth default the three above miss:
   `.et-db #et-boc .et-l .et_pb_section{background-color:#fff}`. A Theme Builder
   section that declares no background of its own therefore renders white, not
   transparent — which is why clearing an explicit `#FFFFFF` off a section in
   `wp-cli/12-theme-builder-bodies.php` left `/cities/` and `/availabilities/`
   exactly as white as before. Divi tags any section that *does* declare a
   background with `et_pb_with_background` and emits a per-section rule for it
   with `!important`, so excluding that class lets the canvas show through the
   undeclared sections without touching a single deliberate fill. */
.et-db #et-boc .et-l .et_pb_section:not(.et_pb_with_background) {
	background-color: transparent;
}

h1,
h2,
h3,
h4,
h5,
h6 {
	color: var(--cs-text-strong);
}

/* =============================================================================
   BASE — scoped resets only
   ============================================================================= */

/* WordPress core convention, emitted by render_filter_chips(). Defined here
   because nothing else in this repo does: without it every filter chip renders
   the literal text " — remove filter" inline. */
.screen-reader-text {
	position: absolute !important;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip: rect(0 0 0 0);
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* Every card, panel and control here sizes a padded, bordered box against a
   grid track. Divi does ship a global border-box reset, but depending on a
   parent theme's reset for correct layout means a Divi update can silently
   push cards past their container — so the components declare it themselves. */
.cs-availabilities,
.cs-availabilities *,
.cs-leg-card,
.cs-leg-card *,
.cs-leg-summary,
.cs-leg-summary *,
.cs-city-legs,
.cs-city-legs *,
.cs-city-index,
.cs-city-index *,
.cs-transport-request,
.cs-transport-request * {
	box-sizing: border-box;
}

/* Divi sets `p { padding-bottom: 1em }`, `h1–h6 { padding-bottom: 10px }` and
   list padding on content elements. Every shortcode here builds its own
   rhythm with grid `gap`, so those defaults are cancelled inside cs- blocks
   only — never globally. */
.cs-availabilities p,
.cs-availabilities h2,
.cs-availabilities h3,
.cs-leg-card p,
.cs-leg-card h3,
.cs-leg-summary p,
.cs-leg-summary h2,
.cs-city-legs p,
.cs-city-legs h2,
.cs-city-index p,
.cs-city-index h2,
.cs-empty-state p,
.cs-filters p,
.cs-filters h2,
.cs-results-header p {
	margin: 0;
	padding: 0;
}

.cs-availabilities dl,
.cs-availabilities dt,
.cs-availabilities dd,
.cs-leg-card dl,
.cs-leg-card dt,
.cs-leg-card dd,
.cs-leg-summary dl,
.cs-leg-summary dt,
.cs-leg-summary dd {
	margin: 0;
	padding: 0;
}

.cs-chips,
.cs-city-index__list {
	margin: 0;
	padding: 0;
	list-style: none;
}

/* =============================================================================
   PRIMITIVE — .cs-button
   Emitted by every shortcode: card CTA, filter submit, sort submit, empty
   state, alert CTA, "See all", "Browse availabilities".
   ============================================================================= */

.cs-button,
a.cs-button,
button.cs-button {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--cs-space-2xs);
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-2xs) var(--cs-space-md);
	border: 1px solid var(--cs-brand-500);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-brand-500);
	color: var(--cs-on-brand);
	font-size: var(--cs-text-base);
	font-weight: 600;
	line-height: var(--cs-leading-snug);
	text-align: center;
	text-decoration: none;
	cursor: pointer;
	transition: background-color var(--cs-transition), border-color var(--cs-transition), color var(--cs-transition);
}

.cs-button:hover,
.cs-button:focus,
a.cs-button:hover,
a.cs-button:focus {
	/* The fill darkens on hover and the border does not. On a dark page a
	   lighter fill would be the natural "lift", but nothing lighter than
	   `--cs-brand-500` still carries white text at 4.5:1 — so the state change
	   is a press, and the boundary that 1.4.11 measures stays the 500 border
	   (4.11:1 on the canvas) in both states rather than dropping to the 600
	   fill's 2.93:1. */
	background: var(--cs-brand-600);
	border-color: var(--cs-brand-500);
	color: var(--cs-on-brand);
	text-decoration: none;
}

.cs-button--secondary,
a.cs-button--secondary,
button.cs-button--secondary {
	background: transparent;
	color: var(--cs-brand-400);
}

.cs-button--secondary:hover,
.cs-button--secondary:focus,
a.cs-button--secondary:hover,
a.cs-button--secondary:focus {
	background: var(--cs-surface-accent);
	border-color: var(--cs-brand-400);
	color: var(--cs-text-on-accent);
}

/* Outcome-coloured submits — accept green, reject red (user instruction, 2026-08-19).

   The edge carries 1.4.11 and the fill carries the "shaded" look, which is the same division of
   labour `.cs-button:hover` below already documents. That matters here because the fills are
   deliberately saturated: against `.cs-quote-detail__action`'s `--cs-surface-sunken` they are 3.43:1
   and 2.79:1 on their own, so the reject fill alone would not clear 3:1 and a self-coloured border
   (border == fill) would leave it with no compliant boundary. The edges are 5.38:1 and 4.93:1, and
   the label — `--cs-on-brand`, which resolves to #FFFFFF — is 5.32:1 and 6.54:1 on the fills.

   Two rejected alternatives, both recorded because both look reasonable until measured:
   - The `*-tint` badge pair as a fill: ~1.1:1 against any surface here. Invisible as a shade;
     renders as an outline button with a coloured label, which is not what was asked for.
   - The `*-text` pair as a fill: passes contrast easily, but they are mint and salmon rather than
     green and red, and `--cs-danger-text` is *also* the colour of `.cs-quote-detail__field-error`
     and `.cs-quote-queue__notice--error`. That version shipped briefly and was then rendered with a
     decline validation error present: the error line and the button read as one continuous pink
     block — button-fill against error-text measured **1.00:1, identical** — and the button was the
     largest pink element in a panel where pink means "something is wrong". The accept panel was the
     control that proved it: pink error above a *green* button separates instantly. The current fill
     is 3.43:1 against that pink and the edge 1.94:1, so neither reads as error styling. The accept
     side never had the problem, because nothing else on this page is green.

   Both are filled, and reject is not de-emphasised to `--secondary`, because each is the sole submit
   of its own enclosed panel rather than two buttons competing in one row. De-emphasising one would
   produce a panel whose only action looks secondary. Hierarchy comes from order — accept first.

   Colour is never the only signal: `label` in CS_QUOTE_ACTIONS is mandatory text on both, and they
   sit in separately-headed panels whose headings name each `<form>` via `aria-labelledby`. */
.cs-button--accept,
button.cs-button--accept {
	border-color: var(--cs-accept-edge);
	background: var(--cs-accept-fill);
	color: var(--cs-on-brand);
}

.cs-button--accept:hover,
.cs-button--accept:focus,
button.cs-button--accept:hover,
button.cs-button--accept:focus {
	/* The fill darkens and the edge does not, exactly as `.cs-button:hover` does it, so the boundary
	   1.4.11 measures stays put across the state change instead of moving to the darker fill. */
	border-color: var(--cs-accept-edge);
	background: var(--cs-accept-press);
	color: var(--cs-on-brand);
}

.cs-button--reject,
button.cs-button--reject {
	border-color: var(--cs-reject-edge);
	background: var(--cs-reject-fill);
	color: var(--cs-on-brand);
}

.cs-button--reject:hover,
.cs-button--reject:focus,
button.cs-button--reject:hover,
button.cs-button--reject:focus {
	border-color: var(--cs-reject-edge);
	background: var(--cs-reject-press);
	color: var(--cs-on-brand);
}

/* One focus treatment for everything interactive inside a cs- block, so a
   keyboard user never loses their place between a Divi module and a
   shortcode-rendered one.

   `outline-offset` is not cosmetic here. The 2px gap shows the page or card
   behind the control, so the pair 1.4.11 measures is ring-against-background
   (8.68:1 on the card, 10.34:1 on the canvas, 9.53:1 on
   `--cs-surface-sunken` where the quote-detail action panels sit). Set to 0 the ring
   would sit directly on the button's own fill and be measured against that
   instead: 2.51:1 on brand-500, 2.78:1 on `--cs-accept-fill`, 3.41:1 on
   `--cs-reject-fill`. Two of those three fail, so the offset is what keeps
   this rule compliant rather than what makes it look tidy.

   Verified with real Tab presses, not element.focus() — the latter does not
   trigger :focus-visible, so a script using it captures a ring-less button and
   reports a false negative. */
.cs-button:focus-visible,
.cs-availabilities :focus-visible,
.cs-leg-summary :focus-visible,
.cs-city-legs :focus-visible,
.cs-city-index :focus-visible {
	outline: 3px solid var(--cs-link);
	outline-offset: 2px;
}

/* =============================================================================
   PRIMITIVE — .cs-badge
   Taxonomy badges (vehicle, destination_type) and the status badge.
   ============================================================================= */

/*
 * --cs-badge-fg/--cs-badge-bg consumed here, with fallbacks matching this rule's own prior
 * defaults, rather than only on .cs-badge--status: .cs-badge--flag-* and .cs-badge--qstatus-*
 * (style.css, further down) have set these custom properties without anything ever reading them
 * into background/color -- a pre-existing bug found while adding a new flag badge, not introduced
 * by it, and it silently strips colour-coding from every flag/qstatus badge on the site. A modifier
 * that sets background/color directly instead (.cs-badge--vehicle, .cs-badge--destination-type)
 * still wins the cascade: equal specificity, later in source order.
 */
.cs-badge {
	display: inline-flex;
	align-items: center;
	gap: var(--cs-space-3xs);
	padding: 0.1875rem var(--cs-space-2xs);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-badge-bg, transparent);
	color: var(--cs-badge-fg, var(--cs-text-muted));
	font-size: var(--cs-text-xs);
	font-weight: 600;
	line-height: var(--cs-leading-snug);
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

/* The card shows the vehicle and the destination type adjacent with no labels
   ("Falcon 9" next to "LEO"), so the two facets are given different weights —
   the vehicle is the concrete thing, the destination is the category.

   --cs-badge-accent is set inline per badge (vehicle_badge_accent(), shortcodes.php) so each
   vehicle gets its own colour rather than one flat fill for every vehicle on the site — the
   fallback keeps today's single-colour look for the rare case the property isn't set at all. */
.cs-badge--vehicle {
	border-color: var(--cs-border-strong);
	background: var(--cs-badge-accent, var(--cs-surface-emphasis));
	color: var(--cs-text-on-emphasis);
	margin-right: 2px;
}

/* vehicle_badge_link_markup() (2026-08-19 user request) -- [cs_leg_detail] only, the leg card and
   [cs_leg_summary] still render the plain, unlinked .cs-badge--vehicle above. An underline (this
   project's usual link signal, .cs-leg-detail__route above) would run across the pill's own border
   and read as a rendering bug rather than a link, so the affordance here is a border brightening on
   hover/focus instead -- still never colour alone, since cursor:pointer and the surrounding "click
   through to the vehicle" context both hold regardless of colour perception. */
.cs-leg-detail__vehicle-link {
	display: inline-flex;
	text-decoration: none;
	border-radius: var(--cs-radius-sm);
}

.cs-leg-detail__vehicle-link:hover .cs-badge--vehicle,
.cs-leg-detail__vehicle-link:focus-visible .cs-badge--vehicle {
	border-color: var(--cs-text-on-emphasis);
}

.cs-leg-detail__vehicle-link:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

.cs-badge--destination-type {
	border-color: var(--cs-border-strong);
	color: var(--cs-text-muted);
}

/* launch-sites.php's "must not miss" content (docs/35 §5) needs the same weight the warning-tinted
   status badges already carry, not the bare unstyled primitive this fell through to until now. */
.cs-badge--theoretical {
	background: var(--cs-warning-tint);
	color: var(--cs-warning-text);
}

/* ER-14: same warning-tint weight as .cs-badge--theoretical above, and deliberately not the danger
   tint the provider queue's own `.cs-badge--flag-itar` uses (provider-quote-queue.php) -- that badge
   asserts a confirmed self-certified answer, this one is a jurisdiction-level "may apply" advisory,
   a different severity register for a different, cargo-vs-location concept. */
.cs-badge--export-risk {
	background: var(--cs-warning-tint);
	color: var(--cs-warning-text);
}

/* ER-17: Tier 2's badge -- deliberately the lighter info tint, not the warning tint Tier 1 uses
   above, the same weight distinction .cs-badge--status-booked (info) already draws against
   .cs-badge--status-on_hold (warning) elsewhere in this file. Tier 2 is a lower-severity advisory
   than Tier 1, and the colour should say so at a glance without reading either badge's text. */
.cs-badge--export-notice {
	background: var(--cs-info-tint);
	color: var(--cs-info-text);
}

/* launch-sites.php's export_control_notice()/export_control_tier2_notice() and
   city-page-sections.php's shortcode_city_hero() all emit this -- same spacing-only role
   .cs-launch-site__theoretical plays below for its own notice, shared across every tier/surface
   since the paragraph itself is otherwise unstyled prose next to a badge. The tier2 modifier adds no
   rules of its own today; it exists so a future visual distinction between tiers has a hook that
   doesn't require touching the PHP again. */
.cs-location__export-notice {
	margin: var(--cs-space-sm) 0 0;
}

/* Status badges are icon + colour + text, never colour alone (docs/06 QA
   note). The icon is a CSS-drawn shape, verified distinct in greyscale, and it
   is redundant with the label rather than replacing it.

   This comment used to claim the shapes survive forced-colours mode as well.
   Nothing had ever rendered them there, and the claim is doubtful on its face:
   the shapes are `background: currentColor` plus `border: 2px solid
   currentColor`, and overriding exactly those two is what forced-colours does,
   so the filled and hollow variants may well collapse into each other. In that
   mode the always-present text label is the carrier, which satisfies "never
   colour alone" on its own. If someone renders this in Windows High Contrast
   and finds the shapes do hold, restore the claim then — with the render. */
.cs-badge--status {
	--cs-badge-fg: var(--cs-text-muted);
	--cs-badge-bg: var(--cs-surface-panel);

	background: var(--cs-badge-bg);
	color: var(--cs-badge-fg);
	/* Safe here and only here: the status labels are a closed set whose longest
	   member is "Closed for quotes" (17 characters) — the *derived* label
	   leg_status_badge() substitutes for `published` past its deadline/window
	   (shortcodes.php), one character longer than the six raw stored statuses'
	   own longest, "Open for quotes" (15). Taxonomy terms are author-supplied
	   and unbounded, so those badges are left free to wrap. */
	white-space: nowrap;
}

.cs-badge--status-published     { --cs-badge-fg: var(--cs-success-text); --cs-badge-bg: var(--cs-success-tint); }
.cs-badge--status-on_hold       { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
.cs-badge--status-booked        { --cs-badge-fg: var(--cs-info-text);    --cs-badge-bg: var(--cs-info-tint); }
.cs-badge--status-de_manifested { --cs-badge-fg: var(--cs-danger-text);  --cs-badge-bg: var(--cs-danger-tint); }
.cs-badge--status-expired       { --cs-badge-fg: var(--cs-text-muted);   --cs-badge-bg: var(--cs-surface-panel); }
/* Derived, not a stored status: a `published` leg past its deadline or window, which every gate
   already treats as closed while the hourly sweep catches up (docs/26 §26.2). Warning tint rather
   than the muted `expired` one — this leg has not flown, it has stopped taking requests. */
.cs-badge--status-closed        { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
.cs-badge--status-draft         { --cs-badge-fg: var(--cs-text-muted);   --cs-badge-bg: var(--cs-surface-sunken); }
/* Derived, not a stored status (leg_status_badge(), 2026-08-30): a `published` leg listed with
   zero total capacity from the start -- a real launch tracked for reference, never for sale.
   `info` tint rather than `warning`/`danger`: nothing failed or sold out, there was simply nothing
   to sell. */
.cs-badge--status-science       { --cs-badge-fg: var(--cs-info-text);    --cs-badge-bg: var(--cs-info-tint); }

/* /vehicles/<slug>/ hero (docs/41, docs/46 §46.3) — same primitive, different caller
   (vehicle_status_badge(), vehicle-pages.php). `retiring` takes `on_hold`'s tint: both mean "still
   bookable today, won't be for much longer". `retired` takes `expired`'s muted tint: both mean "the
   thing this describes is gone", not a fault condition worth `danger`. */
.cs-badge--status-retiring      { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
.cs-badge--status-retired       { --cs-badge-fg: var(--cs-text-muted);   --cs-badge-bg: var(--cs-surface-panel); }

/* [cs_launch_site_index] card badges row (docs/48 §48.3's `active`/`inactive` location status
   field). Only `inactive` ever renders -- mirrors vehicle_status_badge()'s "active is the assumed
   default, badge it only lands for the exception" rule (its own label map omits 'active' outright;
   render_launch_site_index_card() does the equivalent by only ever emitting this class for
   `inactive`) -- but the `active` colour/shape pair is defined too rather than left to fall through
   to the base `.cs-badge--status` grey, in case a future caller does render it explicitly. Takes
   `retired`'s muted tint/shape (both mean "not currently operating", not a fault condition). */
.cs-badge--status-active        { --cs-badge-fg: var(--cs-success-text); --cs-badge-bg: var(--cs-success-tint); }
.cs-badge--status-inactive      { --cs-badge-fg: var(--cs-text-muted);   --cs-badge-bg: var(--cs-surface-panel); }

.cs-badge__icon {
	flex: none;
	width: 0.625rem;
	height: 0.625rem;
	border: 2px solid currentColor;
	background: currentColor;
}

.cs-badge--status-published .cs-badge__icon     { border-radius: 50%; }
.cs-badge--status-on_hold .cs-badge__icon       { border-radius: 50%; background: transparent; }
.cs-badge--status-booked .cs-badge__icon        { border-radius: 1px; }
.cs-badge--status-expired .cs-badge__icon       { border-radius: 1px; background: transparent; }
.cs-badge--status-draft .cs-badge__icon         { border-radius: 50%; border-style: dashed; background: transparent; }
/* A hollow diamond, and the only shape here that is rotated without also being
   a bar. `closed` had no rule at all, so it fell through to the base filled
   square — which is `booked`'s shape, and the two `--cs-badge-fg` values sit
   1.07:1 apart in greyscale. That left the label as the sole distinction
   between "we sold this" and "this stopped taking requests", which is the exact
   failure the shapes exist to prevent. */
.cs-badge--status-closed .cs-badge__icon        { border-radius: 1px; background: transparent; transform: rotate(45deg); }
.cs-badge--status-de_manifested .cs-badge__icon {
	width: 0.75rem;
	height: 2px;
	border: 0;
	transform: rotate(-45deg);
}
/* Dotted hollow circle: distinct from `on_hold` (solid hollow circle) and `draft` (dashed hollow
   circle) -- the third variant on the same base shape rather than a fourth invented one. */
.cs-badge--status-science .cs-badge__icon       { border-radius: 50%; border-style: dotted; background: transparent; }

/* Reuse `on_hold`'s hollow circle and `expired`'s hollow square rather than inventing two more
   shapes: a leg status badge (a leg card lower on the same /vehicles/<slug>/ page, via
   [cs_vehicle_legs]) and this hero badge never claim to be the same status, so sharing a shape
   with the nearest semantic match is a feature, not a collision — the label text is still the
   primary carrier either way (see this block's own opening comment). */
.cs-badge--status-retiring .cs-badge__icon      { border-radius: 50%; background: transparent; }
.cs-badge--status-retired .cs-badge__icon       { border-radius: 1px; background: transparent; }

/* [cs_launch_site_index] card footer — reuse `published`'s filled circle and `retired`'s hollow
   square, same shape-sharing rationale as the retiring/retired pair above: a location's
   active/inactive is a different fact from a leg's or a vehicle's status, so a shared shape with
   the nearest semantic match is deliberate, not a collision. */
.cs-badge--status-active .cs-badge__icon        { border-radius: 50%; }
.cs-badge--status-inactive .cs-badge__icon      { border-radius: 1px; background: transparent; }

/* /status/ (platform-status-page.php) and Console's [cs_console_status] mirror — new modifiers
   rather than reusing e.g. .cs-badge--status-published: an empty_leg's "published" and a *site*
   being reachable are unrelated facts, and a shared class name would make a future grep for one
   silently catch the other. Same primitive, same never-colour-alone icon convention, deliberately. */
.cs-badge--status-up          { --cs-badge-fg: var(--cs-success-text); --cs-badge-bg: var(--cs-success-tint); }
.cs-badge--status-maintenance { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
.cs-badge--status-unreachable { --cs-badge-fg: var(--cs-danger-text);  --cs-badge-bg: var(--cs-danger-tint); }
.cs-badge--status-idle        { --cs-badge-fg: var(--cs-text-muted);   --cs-badge-bg: var(--cs-surface-panel); }
.cs-badge--status-running     { --cs-badge-fg: var(--cs-info-text);    --cs-badge-bg: var(--cs-info-tint); }
.cs-badge--status-slow        { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
.cs-badge--status-stalled     { --cs-badge-fg: var(--cs-danger-text);  --cs-badge-bg: var(--cs-danger-tint); }

.cs-badge--status-up .cs-badge__icon          { border-radius: 50%; }
.cs-badge--status-maintenance .cs-badge__icon { border-radius: 50%; background: transparent; }
.cs-badge--status-unreachable .cs-badge__icon { border-radius: 1px; }
.cs-badge--status-idle .cs-badge__icon        { border-radius: 1px; background: transparent; }
.cs-badge--status-running .cs-badge__icon     { border-radius: 50%; }
.cs-badge--status-slow .cs-badge__icon        { border-radius: 50%; background: transparent; }
/* Same rotated hollow diamond as .cs-badge--status-closed above — both mean "stopped part-way
   through, not a clean end state". */
.cs-badge--status-stalled .cs-badge__icon     { border-radius: 1px; background: transparent; transform: rotate(45deg); }

/* =============================================================================
   .cs-platform-status — /status/ (platform-status-page.php)
   Same shape as .cs-provider-page above: title, a facts <dl> grid, badge-carried rows. Reused
   rather than restyled from scratch for the same reason .cs-provider-page reused .cs-launch-site's.
   ============================================================================= */
.cs-platform-status {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-lg);
	color: var(--cs-text);
	line-height: var(--cs-leading-normal);
}

.cs-platform-status__title {
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-3xl);
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

.cs-platform-status__lede {
	margin: 0;
	color: var(--cs-text-muted);
}

.cs-platform-status__section h2 {
	margin: 0 0 var(--cs-space-sm);
	color: var(--cs-text-strong);
	font-size: var(--cs-text-2xl);
	font-weight: 700;
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

.cs-platform-status__note {
	margin: var(--cs-space-xs) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-platform-status__facts {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
	gap: var(--cs-space-2xs);
	margin: 0;
}

.cs-platform-status__fact {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--cs-space-sm);
	padding: var(--cs-space-xs);
	border: 1px solid var(--cs-border-surface);
	border-left: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
}

.cs-platform-status__fact dt {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

.cs-platform-status__fact dd {
	margin: 0;
}

/* Deploy-progress completion bar. Same track+fill mechanism as .cs-leg-detail__specs-bar-cell /
   .cs-leg-detail__capacity-bar-fill (isolation:isolate gives the track its own stacking context, so
   its background reliably paints "below" the z-index:-1 fill without a separate track element) --
   a distinct class prefix since this is deploy completion, not remaining cargo capacity, even
   though the visual language matches on purpose. Fill width is a percentage set inline per-render
   (render_platform_status_progress_bar()), not a CSS variable, since it's server-computed per
   request from a live network option, not a design-time constant. */
.cs-platform-status__progress {
	display: flex;
	align-items: center;
	gap: var(--cs-space-xs);
}

.cs-platform-status__progress-track {
	position: relative;
	isolation: isolate;
	flex: 1 1 auto;
	height: 0.5rem;
	border-radius: var(--cs-radius-sm);
	background: var(--cs-void-600);
	overflow: hidden;
}

.cs-platform-status__progress-fill {
	position: absolute;
	inset: 0;
	z-index: -1;
	display: block;
	background: var(--cs-brand-500);
	border-radius: inherit;
	transition: width 0.3s ease;
}

.cs-platform-status__progress-label {
	flex: 0 0 auto;
	font-size: var(--cs-text-sm);
	font-variant-numeric: tabular-nums;
}

.cs-platform-status__table {
	width: 100%;
	margin: var(--cs-space-sm) 0 0;
	border-collapse: collapse;
	font-size: var(--cs-text-sm);
}

.cs-platform-status__table th,
.cs-platform-status__table td {
	padding: var(--cs-space-2xs) var(--cs-space-xs);
	border-bottom: 1px solid var(--cs-border);
	text-align: left;
	vertical-align: top;
}

.cs-platform-status__table thead th {
	border-bottom-color: var(--cs-border-strong);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-platform-status__table-empty {
	color: var(--cs-text-muted);
}

/* ER-19 — the per-site build version, as subtext under the Status badge rather than a fifth
   column: sits in the same <td> as the badge, on its own line, deliberately quieter than the
   badge itself (this is a diagnostic detail, not a status signal). */
.cs-platform-status__version {
	display: block;
	margin-top: var(--cs-space-3xs);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-variant-numeric: tabular-nums;
}

/* Below Divi's Phone breakpoint the table becomes stacked cards rather than a
   horizontal scroller, same pattern as .cs-quote-queue__table/.cs-history-table
   (docs/06 QA2). Each cell keeps its column name via the `data-label` the
   renderer emits, so the reading order still matches the header order it
   replaced. */
@media (max-width: 767px) {
	.cs-platform-status__table thead {
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip: rect(0 0 0 0);
		clip-path: inset(50%);
		white-space: nowrap;
	}

	.cs-platform-status__table,
	.cs-platform-status__table tbody,
	.cs-platform-status__table tr,
	.cs-platform-status__table th,
	.cs-platform-status__table td {
		display: block;
		width: 100%;
	}

	.cs-platform-status__table tr {
		margin: 0 0 var(--cs-space-sm);
		padding: var(--cs-space-xs);
		border: 1px solid var(--cs-border-strong);
	}

	.cs-platform-status__table td,
	.cs-platform-status__table th {
		border-bottom: 0;
		padding: var(--cs-space-3xs) 0;
	}

	.cs-platform-status__table td::before {
		content: attr(data-label) ": ";
		color: var(--cs-text-muted);
		font-size: var(--cs-text-xs);
		text-transform: uppercase;
	}
}

.cs-platform-status__locked {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--cs-space-sm);
	flex-wrap: wrap;
	padding: var(--cs-space-sm);
	border: 1px dashed var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
}

/* ER-10 -- per-site 90-day uptime strip (render_uptime_graph(), includes/platform-uptime.php).
   Shared by the public /status/ page and Console's /status/ panel, so every colour below is a
   `--cs-*` token and nothing else: assets/lcars/lcars.css remaps those tokens under `.cs-lcars`
   and the strip re-skins itself there with no second rule set. One flex row of 90 equal cells,
   oldest on the left -- `flex: 1 1 0` + `min-width: 0` so the row always fits the container
   rather than overflowing at narrow widths; the 480px-first sizing is the base rule, wider
   screens only get taller bars and a wider gap. Outage cells carry a stripe pattern on top of
   the colour so up/down never differ by hue alone (the same "never colour alone" rule the badge
   set follows); every cell also carries its date + percentage as text (aria-label/title). */
.cs-uptime {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-sm);
}

.cs-uptime__empty {
	margin: 0;
	padding: var(--cs-space-sm);
	border: 1px dashed var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
}

.cs-uptime__site {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-3xs);
	padding: var(--cs-space-xs);
	border: 1px solid var(--cs-border-surface);
	border-left: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
}

.cs-uptime__head {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--cs-space-3xs) var(--cs-space-sm);
	font-size: var(--cs-text-sm);
}

.cs-uptime__name {
	color: var(--cs-text-strong);
	font-weight: 600;
	overflow-wrap: anywhere;
}

.cs-uptime__summary {
	color: var(--cs-text-muted);
	font-variant-numeric: tabular-nums;
}

.cs-uptime__summary strong {
	color: var(--cs-text-strong);
}

.cs-uptime__strip {
	display: flex;
	gap: 1px;
	height: 1.5rem;
	margin: var(--cs-space-3xs) 0 0;
}

.cs-uptime__day {
	flex: 1 1 0;
	min-width: 0;
	border-radius: 1px;
	background: var(--cs-success-text);
}

.cs-uptime__day--degraded {
	background: var(--cs-warning-text);
}

.cs-uptime__day--down {
	background-color: var(--cs-danger-text);
	background-image: repeating-linear-gradient(
		-45deg,
		transparent 0 2px,
		var(--cs-danger-tint) 2px 4px
	);
}

.cs-uptime__day--nodata {
	background: var(--cs-surface-panel);
	box-shadow: inset 0 0 0 1px var(--cs-border);
}

.cs-uptime__axis {
	display: flex;
	justify-content: space-between;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-uptime__legend {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-3xs) var(--cs-space-sm);
	margin: 0;
	padding: 0;
	list-style: none;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

.cs-uptime__legend li {
	display: inline-flex;
	align-items: center;
	gap: var(--cs-space-3xs);
	margin: 0;
	padding: 0;
	list-style: none;
}

.cs-uptime__swatch {
	display: inline-block;
	flex: 0 0 auto;
	width: 0.75rem;
	height: 0.75rem;
	border-radius: 1px;
}

.cs-uptime__note {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

@media (min-width: 768px) {
	.cs-uptime__strip {
		gap: 2px;
		height: 2rem;
	}

	.cs-uptime__day {
		border-radius: 2px;
	}
}

/* =============================================================================
   [cs_availabilities] — root
   ============================================================================= */

.cs-availabilities {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-lg);
	color: var(--cs-text);
	font-size: var(--cs-text-base);
	line-height: var(--cs-leading-normal);
}

/* Titles, routes, provider names and taxonomy terms are all provider- or
   author-supplied, so a single long unbroken token (a payload designation, a
   pasted URL) would otherwise push a card past its grid track. */
.cs-leg-card,
.cs-leg-summary,
.cs-city-legs,
.cs-city-index__item {
	overflow-wrap: break-word;
}

/* =============================================================================
   .cs-filters — the GET filter form
   A horizontal panel above the results rather than a sidebar: eleven fields in
   a narrow rail is unusable, and a panel lays out identically whether the
   caller asked for `layout="grid"` or `layout="compact"`.
   ============================================================================= */

.cs-filters {
	padding: var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-top: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
}

/* -----------------------------------------------------------------------------
   Full-bleed filter panel, Browse only.

   The panel spans the viewport while the cards keep the Theme Builder row's
   margins, so the two read as separate bands rather than one long column. Done
   here rather than by restructuring the template: the filter form and the
   results are siblings inside a single Code module, so there is no seam in the
   Divi tree to split them at.

   `margin-inline: calc(50% - 50vw)` walks the element out past
   .cs-availabilities, the column and the 92%/1200px row in one step. The
   padding then puts its *contents* back on the same left edge as the cards —
   4vw while the row is at 92%, switching to (100vw - 1200px)/2 once the row
   hits its max-width, which is exactly where `max()` swaps over.

   The `overflow-x: clip` on the section is load-bearing: `vw` units include the
   scrollbar's width, so on a desktop with a classic scrollbar the panel
   overhangs by half a scrollbar each side and mints a horizontal scrollbar of
   its own. `clip` rather than `hidden` because `hidden` would make the section a
   scroll container and silently break `position: sticky` on anything inside it.
   -------------------------------------------------------------------------- */

.cs-page--browse {
	overflow-x: clip;
}

.cs-page--browse .cs-filters {
	margin-inline: calc(50% - 50vw);
	padding-inline: max(4vw, calc(50vw - (1200px / 2)));
	border-right: 0;
	border-left: 0;
	border-radius: 0;
}

/* The fields grid moved off `.cs-filters` and onto its own element when the panel became a
   `<details>`: eleven single-column controls plus an 8rem multi-select is ~1000px of form above the
   first result card at 480px. The disclosure is server-rendered `open` when a filter is applied. */
.cs-filters__fields {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-sm) var(--cs-space-md);
	padding-top: var(--cs-space-sm);
}

/* The `<summary>` is the control that opens the panel, so it carries the 44px
   target the panel's inputs already have. The default marker is removed twice —
   `list-style` for the standard marker, and the WebKit pseudo-element below for
   the Safari versions that still ignore it. */
.cs-filters__heading {
	display: flex;
	align-items: center;
	gap: var(--cs-space-2xs);
	min-height: var(--cs-tap-target);
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-sm);
	font-weight: 700;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
	cursor: pointer;
	list-style: none;
}

.cs-filters__heading::-webkit-details-marker {
	display: none;
}

/* The `<h2>` inside the summary exists for the heading outline, not for a second type treatment. */
.cs-filters__heading h2 {
	margin: 0;
	color: inherit;
	font: inherit;
	letter-spacing: inherit;
	text-transform: inherit;
}

/* Drawn, not `content: "▾"` — a pseudo-element glyph is announced by some screen readers on top of
   the `<summary>`'s own expanded/collapsed state. */
.cs-filters__heading::before {
	content: "";
	flex: 0 0 auto;
	width: 0.5rem;
	height: 0.5rem;
	border-right: 2px solid var(--cs-brand-400);
	border-bottom: 2px solid var(--cs-brand-400);
	transform: rotate(-45deg);
	transition: transform var(--cs-transition);
}

.cs-filters__disclosure[open] > .cs-filters__heading::before {
	transform: rotate(45deg);
}

.cs-filters__disclosure[open] > .cs-filters__heading {
	padding-bottom: var(--cs-space-2xs);
	border-bottom: 1px solid var(--cs-border);
}

.cs-filters__field {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-3xs);
	min-width: 0;
}

.cs-filters__field label {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-filters__field input,
.cs-filters__field select {
	width: 100%;
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-2xs) var(--cs-space-xs);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
	color: var(--cs-text);
	font-family: inherit;
	font-size: var(--cs-text-base); /* 16px — anything smaller zooms iOS Safari on focus */
	line-height: var(--cs-leading-snug);
}

.cs-filters__field select[multiple] {
	min-height: 8rem;
	padding: var(--cs-space-3xs);
}

/* `<option>` height is UA-controlled and `min-height` is ignored on it, so the
   tap target is bought with line-height and padding — the only two properties
   browsers honour here. Sized to clear 44px rather than the ~40px this used to
   land on; four rows are visible (`size="4"`), so the box grows with them. */
.cs-filters__field select[multiple] option {
	padding: var(--cs-space-2xs) var(--cs-space-2xs);
	line-height: 2;
}

.cs-filters__field input:focus,
.cs-filters__field select:focus {
	border-color: var(--cs-brand-400);
}

/* "Volume is compared in m³. 1U = 0.001 m³." — sits with the volume fields, not
   under the submit button, so it is read before the value is typed. */
.cs-filters__note {
	grid-column: 1 / -1;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-filters__actions {
	grid-column: 1 / -1;
	padding-top: var(--cs-space-2xs);
	border-top: 1px solid var(--cs-border);
}

.cs-filters__actions .cs-button {
	width: 100%;
}

@media (min-width: 480px) {
	.cs-filters__fields {
		grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
		align-items: start;
	}

	/* The `size="4"` departure multi-select is roughly twice the height of every
	   other control, so without this it sets the height of its whole grid row
	   and leaves a band of dead space under the three fields beside it. Matched
	   by containment rather than by position: render_filter_form() skips a
	   taxonomy with no terms, so this field's index is not fixed. Browsers
	   without `:has()` simply get the taller row back. */
	.cs-filters__field:has(select[multiple]) {
		grid-row: span 2;
	}

	.cs-filters__actions .cs-button {
		width: auto;
		min-width: 12rem;
	}
}

/* =============================================================================
   .cs-results-header — count, sort control, applied-filter chips
   ============================================================================= */

.cs-results-header {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-sm);
	align-items: end;
}

.cs-results-header__count {
	color: var(--cs-text-strong);
	font-size: var(--cs-text-md);
	font-weight: 700;
}

.cs-results-header__sort {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-2xs);
}

.cs-results-header__sort label {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-results-header__sort select {
	flex: 1 1 12rem;
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-2xs) var(--cs-space-xs);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
	color: var(--cs-text);
	font-family: inherit;
	font-size: var(--cs-text-base);
}

.cs-results-header__sort .cs-button {
	padding-inline: var(--cs-space-sm);
}

@media (min-width: 768px) {
	.cs-results-header {
		grid-template-columns: 1fr auto;
	}

	.cs-results-header__sort {
		justify-content: flex-end;
		flex-wrap: nowrap;
	}

	.cs-results-header__sort select {
		flex: 0 1 auto;
	}

	.cs-results-header .cs-chips {
		grid-column: 1 / -1;
	}
}

/* =============================================================================
   .cs-chips — one removable chip per applied filter
   The × is drawn with gradients rather than pseudo-content: the anchor already
   carries a visually-hidden " — remove filter", and a `content: "×"` would be
   announced on top of it.
   ============================================================================= */

.cs-chips {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-2xs);
}

.cs-chips__chip a {
	display: inline-flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-3xs) 1.75rem var(--cs-space-3xs) var(--cs-space-2xs);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
	color: var(--cs-text);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-snug);
	text-decoration: none;
	background-image:
		linear-gradient(45deg, transparent calc(50% - 0.5px), currentColor calc(50% - 0.5px), currentColor calc(50% + 0.5px), transparent calc(50% + 0.5px)),
		linear-gradient(-45deg, transparent calc(50% - 0.5px), currentColor calc(50% - 0.5px), currentColor calc(50% + 0.5px), transparent calc(50% + 0.5px));
	background-size: 0.5rem 0.5rem;
	background-repeat: no-repeat;
	background-position: right 0.625rem center, right 0.625rem center;
	transition: border-color var(--cs-transition), color var(--cs-transition), background-color var(--cs-transition);
}

.cs-chips__chip a:hover,
.cs-chips__chip a:focus {
	border-color: var(--cs-danger-text);
	background-color: var(--cs-danger-tint);
	color: var(--cs-danger-text);
	text-decoration: none;
}

.cs-chips__clear a {
	display: inline-flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	padding-inline: var(--cs-space-2xs);
	color: var(--cs-link);
	font-size: var(--cs-text-sm);
	font-weight: 600;
	text-decoration: underline;
}

.cs-chips__clear a:hover,
.cs-chips__clear a:focus {
	color: var(--cs-link-hover);
}

/* =============================================================================
   .cs-leg-grid + .cs-leg-card — the one result card, rendered by
   render_leg_card() for Browse, City Page, Landing and RFQ alike.
   ============================================================================= */

.cs-leg-grid {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-md);
}

/* `auto-fit`, not `auto-fill`: the Landing strip is `limit="3"` and a city
   module often returns two, and auto-fill would hold an empty fourth track
   open, parking three cards against the left edge with a column of dead space
   beside them. auto-fit collapses the track only when it is empty across the
   whole grid, so a full page of twelve results is unaffected.

   Browse/search results, and City Page's nearby-legs module, cap each card's
   max at 30% rather than 1fr, so one or two results sit at their capped width
   with visible empty space beside them instead of stretching to fill the row
   -- a lone result should not read as "the whole shelf." City Page's default
   limit is 6, so it hits the same one-or-two-result case Browse does; the
   compact variant below is untouched because nothing currently renders it at
   a variable count. 30%, not 33.3333%: three tracks at exactly a third each
   plus the two gaps between them overflows 100%, which dropped the row to 2
   columns instead of 3 -- 30% leaves the gaps room. */
@media (min-width: 480px) {
	.cs-leg-grid {
		grid-template-columns: repeat(auto-fit, minmax(17rem, 30%));
	}

	.cs-city-legs .cs-leg-grid,
	.cs-vehicle-legs .cs-leg-grid {
		grid-template-columns: repeat(auto-fit, minmax(15rem, 30%));
	}

	.cs-availabilities--compact .cs-leg-grid {
		grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
	}
}

.cs-leg-card {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-2xs);
	padding: var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface);
	transition: border-color var(--cs-transition);
	position: relative;
	overflow: hidden;
}

/* The sci-fi hairline: a 3px brand→cyan edge that only appears on the card the
   user is actually on. Decorative and additive — hover/focus state is also
   carried by the border, which is the only other thing carrying it now that the
   shadows are gone. */
.cs-leg-card::before {
	content: "";
	position: absolute;
	inset: 0 0 auto 0;
	height: 3px;
	background: linear-gradient(90deg, var(--cs-brand-500), var(--cs-accent-400));
	opacity: 0;
	transition: opacity var(--cs-transition);
}

.cs-leg-card:hover,
.cs-leg-card:focus-within {
	border-color: var(--cs-brand-500);
}

.cs-leg-card:hover::before,
.cs-leg-card:focus-within::before {
	opacity: 1;
}

/* Clearfix: .cs-share floats right inside here, and without this the float would escape into
   whatever renders next (the badges row) instead of the head staying tall enough to contain it. */
.cs-leg-card__head {
	overflow: hidden;
}

.cs-leg-card__head .cs-share,
.cs-leg-card__head .cs-save-leg {
	float: right;
	margin-left: var(--cs-space-2xs);
}

.cs-leg-card__title {
	margin: 0;
	font-size: var(--cs-text-lg);
	font-weight: 700;
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

/* The title is the card's second tap target after the CTA and the one a scanning visitor reaches
   for, so the anchor — not the heading around it — carries the 44px height. */
.cs-leg-card__title a {
	display: flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-text-strong);
	text-decoration: none;
}

.cs-leg-card__title a:hover,
.cs-leg-card__title a:focus {
	color: var(--cs-link-hover);
	text-decoration: underline;
}

.cs-leg-card__badges,
.cs-leg-summary__badges {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-3xs);
}

.cs-leg-card__route {
	display: flex;
	align-items: center;
	gap: var(--cs-space-3xs);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-snug);
}

.cs-leg-card__rocket-icon {
	flex: none;
}

.cs-badge--legtype {
	--cs-badge-fg: var(--cs-info-text);
	--cs-badge-bg: var(--cs-info-tint);
	background: var(--cs-badge-bg);
	color: var(--cs-badge-fg);
	margin-right: 2px;
}

.cs-badge--legtype .cs-badge__icon {
	border-radius: 50%;
}

/* leg_type_badge()'s link to destination_body's public page (2026-08-20 user request: "Location
   Badges should be clickable to their location page in any view") — unlike .cs-leg-detail__vehicle-
   link this wraps a badge shared by the leg card, [cs_leg_summary] and [cs_leg_detail] alike, so the
   affordance is generic rather than scoped to one view. Same border-brightens-on-hover/focus
   reasoning as that link: an underline would run across the pill's own border and read as a
   rendering bug rather than a link. */
.cs-badge-link {
	display: inline-flex;
	text-decoration: none;
	border-radius: var(--cs-radius-sm);
}

.cs-badge-link:hover .cs-badge,
.cs-badge-link:focus-visible .cs-badge {
	border-color: var(--cs-text-on-emphasis, var(--cs-text-strong));
}

.cs-badge-link:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

/* Specs stack rather than sitting label-beside-value: a card is ~17rem wide at
   every breakpoint, so the two-column form docs/06 describes for full-width
   tables would wrap in the middle of "12 Mar 2026 14:30 UTC". */
.cs-leg-card__specs {
	display: grid;
	grid-template-columns: 1fr;
	gap: 0;
	margin-top: var(--cs-space-3xs);
}

.cs-leg-card__specs dt {
	margin-top: var(--cs-space-2xs);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-leg-card__specs dt:first-child {
	margin-top: 0;
}

.cs-leg-card__specs dd {
	color: var(--cs-text-strong);
	font-family: var(--cs-font-mono);
	font-size: var(--cs-text-sm);
	font-variant-numeric: tabular-nums;
	line-height: var(--cs-leading-snug);
}

.cs-leg-card__status {
	margin-top: var(--cs-space-3xs);
}

.cs-leg-card__price {
	padding-top: var(--cs-space-2xs);
	border-top: 1px solid var(--cs-border);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

/* `margin-top: auto` so the CTA sits on the same line across a row of cards
   whose titles and spec counts differ. */
.cs-leg-card__cta {
	margin-top: auto;
	padding-top: var(--cs-space-2xs);
}

.cs-leg-card__cta .cs-button {
	width: 100%;
}

/* Watch Launch -- shortcode_vehicle_legs()'s per-leg card only (render_leg_card()'s
   $show_watch_launch param), directly under the CTA it doesn't replace. Reuses .cs-button--secondary
   verbatim; the only rule here is the same full-width-in-a-card treatment .cs-leg-card__cta already
   gives its own button, so the two stack as one visual unit. */
.cs-leg-card__watch {
	padding-top: var(--cs-space-2xs);
}

.cs-leg-card__watch .cs-button {
	width: 100%;
}

/* De-emphasised, not dimmed: `opacity` would drag the title and the status
   label below 4.5:1. A sunken surface plus a muted title holds contrast
   (7.24:1) while clearly reading as secondary. */
.cs-leg-card--unavailable {
	background: var(--cs-surface-sunken);
	border-color: var(--cs-border-surface);
}

.cs-leg-card--unavailable .cs-leg-card__title a {
	color: var(--cs-text-muted);
}

.cs-leg-card--unavailable .cs-leg-card__cta .cs-button {
	background: transparent;
	border-color: var(--cs-border-strong);
	color: var(--cs-brand-400);
}

.cs-leg-card--unavailable .cs-leg-card__cta .cs-button:hover,
.cs-leg-card--unavailable .cs-leg-card__cta .cs-button:focus {
	background: var(--cs-surface-accent);
	border-color: var(--cs-brand-400);
}

/* `compact` shows the same content as every other card, just with tighter chrome, so a
   three-across Landing strip does not out-weigh the hero above it. */
.cs-leg-card--compact {
	padding: var(--cs-space-sm);
}

.cs-leg-card--compact .cs-leg-card__title {
	font-size: var(--cs-text-base);
}

/* =============================================================================
   .cs-pagination — paginate_links() output
   ============================================================================= */

.cs-pagination {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-3xs);
	align-items: center;
	justify-content: center;
	padding-top: var(--cs-space-sm);
	border-top: 1px solid var(--cs-border);
}

.cs-pagination .page-numbers {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: var(--cs-tap-target);
	min-height: var(--cs-tap-target);
	padding-inline: var(--cs-space-2xs);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
	color: var(--cs-link);
	font-size: var(--cs-text-sm);
	font-weight: 600;
	font-variant-numeric: tabular-nums;
	text-decoration: none;
	transition: background-color var(--cs-transition), border-color var(--cs-transition), color var(--cs-transition);
}

.cs-pagination a.page-numbers:hover,
.cs-pagination a.page-numbers:focus {
	border-color: var(--cs-brand-400);
	background: var(--cs-surface-accent);
	color: var(--cs-link-hover);
	text-decoration: none;
}

.cs-pagination .page-numbers.current {
	border-color: var(--cs-brand-500);
	background: var(--cs-brand-500);
	color: var(--cs-on-brand);
}

.cs-pagination .page-numbers.dots {
	border-color: transparent;
	background: transparent;
	color: var(--cs-text-muted);
	min-width: auto;
}

/* =============================================================================
   .cs-empty-state — "No availabilities match"
   ============================================================================= */

.cs-empty-state {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-md);
	padding: var(--cs-space-xl) var(--cs-space-md);
	border: 1px dashed var(--cs-border-strong);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
	text-align: center;
}

.cs-empty-state > p:first-child {
	color: var(--cs-text-strong);
	font-size: var(--cs-text-md);
	font-weight: 600;
	max-width: 42ch;
	margin-inline: auto;
}

.cs-empty-state__actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-2xs);
	justify-content: center;
}

/* =============================================================================
   .cs-not-found — 404 Theme Builder body (CR-14, both blog 1 and blog 2)
   Not a reuse of .cs-empty-state above: that component's dashed border and sunken
   fill read as "this list has nothing in it right now", a state nested inside a
   page that has other furniture around it. A 404 has no list and no furniture —
   it is the whole page — so it gets its own plain, centred treatment instead of
   dressing an entire page in what would read as leftover list chrome. `.cs-button`/
   `.cs-button--secondary` (the shared primitive above) still carry both CTAs, same
   as every other empty state on the site.
   ============================================================================= */

.cs-not-found {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--cs-space-md);
	padding: var(--cs-space-2xl) var(--cs-space-md);
	text-align: center;
}

.cs-not-found__code {
	margin: 0;
	color: var(--cs-brand-500);
	font-size: clamp(3rem, 2rem + 5vw, 5rem);
	font-weight: 700;
	line-height: 1;
}

.cs-not-found h1 {
	margin: 0;
}

.cs-not-found p {
	max-width: 48ch;
	color: var(--cs-text-muted);
}

/* =============================================================================
   [cs_leg_summary] — the RFQ page's context card
   The one surface that leads a journey step, so it is the one that carries the
   accent edge permanently rather than on hover.
   ============================================================================= */

.cs-leg-summary {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-2xs);
	padding: calc(var(--cs-space-md) + 3px) var(--cs-space-md) var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	overflow: hidden;
	background: var(--cs-surface);
	color: var(--cs-text);
	line-height: var(--cs-leading-normal);
}

/* Drawn as a pseudo-element rather than `border-image`: with a 1px border on
   the other three sides, `border-image-slice` paints all four edges and there
   is no width-per-side control that keeps the 1px neutral border intact. */
.cs-leg-summary::before {
	content: "";
	position: absolute;
	inset: 0 0 auto 0;
	height: 3px;
	background: linear-gradient(90deg, var(--cs-brand-500), var(--cs-accent-400));
}

@media (min-width: 768px) {
	.cs-leg-summary {
		padding: calc(var(--cs-space-lg) + 3px) var(--cs-space-lg) var(--cs-space-lg);
	}
}

.cs-leg-summary__eyebrow {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-leg-summary__title {
	margin: 0;
	font-size: var(--cs-text-xl);
	font-weight: 700;
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

.cs-leg-summary__title a {
	display: flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-text-strong);
	text-decoration: none;
}

.cs-leg-summary__title a:hover,
.cs-leg-summary__title a:focus {
	color: var(--cs-link-hover);
	text-decoration: underline;
}

.cs-leg-summary__route {
	display: flex;
	align-items: center;
	gap: var(--cs-space-3xs);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-base);
}

/* This is docs/06's `md` collapse point, on the one spec list wide enough to
   need it: stacked label-over-value below 768px, two columns above. */
.cs-leg-summary__specs {
	display: grid;
	grid-template-columns: 1fr;
	gap: 0;
	margin-top: var(--cs-space-2xs);
	padding-top: var(--cs-space-2xs);
	border-top: 1px solid var(--cs-border);
}

.cs-leg-summary__specs dt {
	margin-top: var(--cs-space-2xs);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-leg-summary__specs dt:first-child {
	margin-top: 0;
}

.cs-leg-summary__specs dd {
	color: var(--cs-text-strong);
	font-family: var(--cs-font-mono);
	font-size: var(--cs-text-sm);
	font-variant-numeric: tabular-nums;
}

@media (min-width: 768px) {
	.cs-leg-summary__specs {
		grid-template-columns: max-content 1fr;
		column-gap: var(--cs-space-md);
		row-gap: var(--cs-space-2xs);
		align-items: baseline;
	}

	.cs-leg-summary__specs dt,
	.cs-leg-summary__specs dt:first-child {
		margin-top: 0;
	}

	.cs-leg-summary__specs dd {
		font-size: var(--cs-text-base);
	}
}

/* Fail closed and explain the next step — this is the message that stops
   someone filling nineteen fields against capacity they cannot have. */
.cs-leg-summary__notice {
	margin-top: var(--cs-space-2xs);
	padding: var(--cs-space-xs) var(--cs-space-sm);
	border-left: 3px solid var(--cs-warning-text);
	background: var(--cs-warning-tint);
	color: var(--cs-warning-text);
	font-weight: 600;
}

.cs-leg-summary__actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-2xs);
	margin-top: var(--cs-space-2xs);
}

/* docs/29 §29.5.1: the RFQ form's 95-110% courtesy notice (assets/capacity-warning.js), styled to
   match .cs-leg-summary__notice above -- same "informational, not an error" register, since this
   never blocks submission the way that one's underlying condition can. Starts hidden; the script
   removes [hidden] when the live ratio is in band, never CSS alone (no-JS must show nothing, not a
   stale warning that can't be dismissed). */
.cs-capacity-warning {
	margin-top: var(--cs-space-2xs);
	padding: var(--cs-space-xs) var(--cs-space-sm);
	border-left: 3px solid var(--cs-warning-text);
	background: var(--cs-warning-tint);
	color: var(--cs-warning-text);
	font-weight: 600;
}

.cs-capacity-warning[hidden] {
	display: none;
}

/* prompts/Feature-Release-01 item 2, docs/43-rfq-draft-save.md: [cs_rfq_resume_notice]. Info
   register, not warning, for the ordinary "resuming your draft" case (qa-2 review) — resuming is a
   neutral, expected state, not a caution, unlike .cs-leg-summary__notice/.cs-capacity-warning above.
   The closed-leg variant borrows the warning tint instead: "the capacity you were quoting has
   closed" is closer in weight to those two notices than to a routine confirmation. */
.cs-rfq-draft-notice {
	margin: var(--cs-space-sm) 0;
	padding: var(--cs-space-xs) var(--cs-space-sm);
	border-left: 3px solid var(--cs-info-text);
	background: var(--cs-info-tint);
	color: var(--cs-info-text);
	font-weight: 600;
}

.cs-rfq-draft-notice--closed {
	border-left-color: var(--cs-warning-text);
	background: var(--cs-warning-tint);
	color: var(--cs-warning-text);
}

.cs-rfq-draft-notice a {
	color: inherit;
	text-decoration: underline;
}

/* [cs_rfq_save_draft_button], placed twice on the RFQ page (above and below the form). Script-only
   like .cs-save-leg — ships `hidden`, assets/rfq-draft.js reveals it. */
.cs-rfq-save-draft {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-2xs);
	margin: var(--cs-space-sm) 0;
}

.cs-rfq-save-draft__status {
	font-size: var(--cs-text-sm);
	color: var(--cs-text-muted);
}

.cs-rfq-save-draft__hint {
	flex-basis: 100%;
	margin: 0;
	font-size: var(--cs-text-sm);
	color: var(--cs-text-muted);
}

/* qa-2 real-render finding, 2026-08-20: the button ships `hidden`, but `.cs-button`
   (button.cs-button { display: inline-flex; ... }, above) is an author-stylesheet rule and beats
   the UA default `[hidden] { display: none }` regardless of specificity — the button rendered fully
   visible and clickable with no script, exactly the dead-control outcome the hidden-until-revealed
   design exists to prevent. Same shape this project has already hit and fixed for .cs-share,
   .cs-save-leg, .cs-leg-detail__route-visual and .cs-quote-detail__action (search `[hidden]` in this
   file) — every element a script can hide gets its own explicit rule rather than trusting the UA
   default to survive the cascade. Scoped to the attribute rather than to `.cs-rfq-save-draft` (that
   class is on the wrapping div, not the button) so this can't accidentally suppress a future,
   unrelated `.cs-button` that is legitimately `hidden` for some other reason. */
[data-cs-save-rfq-draft][hidden] {
	display: none;
}

/* [cs_saved_quote_drafts] — "Draft Quotes" tab of [cs_dashboard_tabs] (FR-22). Kept deliberately
   simple: no table, no pagination, matching its sibling [cs_saved_legs] immediately below. */
.cs-saved-quote-drafts {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-xs);
}

.cs-saved-quote-drafts__row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-2xs);
	padding: var(--cs-space-xs) var(--cs-space-sm);
	border: 1px solid var(--cs-border);
	border-radius: var(--cs-radius-sm);
}

.cs-saved-quote-drafts__title {
	font-weight: 600;
}

.cs-saved-quote-drafts__meta {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	margin-left: auto;
}

/* Pre-existing gap, fixed in passing (FR-22): shortcode_saved_quote_drafts()'s empty-state markup has
   used this class since docs/43 shipped it, with no rule defined for it until now — same shape as
   .cs-saved-legs__empty below. */
.cs-saved-quote-drafts__empty {
	padding: var(--cs-space-lg) var(--cs-space-sm);
	border: 1px dashed var(--cs-border-strong);
}

/* [cs_saved_legs] — "Saved Legs" tab of [cs_dashboard_tabs] (FR-22), saved-legs.php. Same list shape
   as .cs-saved-quote-drafts immediately above (deliberately -- one visual language for both "profile"
   tabs), with its own class family since the two are independent data models (docs/43 "Cross-feature
   note"). Each row embeds the ordinary .cs-save-leg button (render_save_leg_button()) as its own
   "un-save" control -- no second write mechanism -- so [data-cs-save-leg][hidden] (below,
   .cs-save-leg[hidden] rule) already covers this row's button too. */
.cs-saved-legs {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-xs);
}

.cs-saved-legs__row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-2xs);
	padding: var(--cs-space-xs) var(--cs-space-sm);
	border: 1px solid var(--cs-border);
	border-radius: var(--cs-radius-sm);
	transition: opacity 0.2s ease, max-height 0.2s ease, padding 0.2s ease, margin 0.2s ease;
	max-height: 10rem;
	overflow: hidden;
}

/* FR-23 qa-2 finding: save-leg.js's own confirmed-unsave removal (removeSavedLegRow()) adds this
   class before removing the row from the DOM on transitionend -- collapses height/padding as it
   fades so the remaining rows close the gap smoothly rather than leaving a blank slot. */
.cs-saved-legs__row--removing {
	opacity: 0;
	max-height: 0;
	padding-top: 0;
	padding-bottom: 0;
	margin: 0;
	border-width: 0;
}

@media (prefers-reduced-motion: reduce) {
	.cs-saved-legs__row {
		transition: none;
	}
}

.cs-saved-legs__title {
	font-weight: 600;
	margin-right: auto;
}

.cs-saved-legs__draft-note {
	flex-basis: 100%;
	margin: 0;
	font-size: var(--cs-text-sm);
	color: var(--cs-text-muted);
}

.cs-saved-legs__empty {
	padding: var(--cs-space-lg) var(--cs-space-sm);
	border: 1px dashed var(--cs-border-strong);
}

/* docs/43: a draft is not a status on the quote_request pipeline — deliberately its own modifier,
   never .cs-badge--qstatus-* (that family's colour language would visually imply otherwise; qa-2
   review). Muted/neutral, matching .cs-badge--theoretical's shape of setting background/color
   directly rather than through the --cs-badge-fg/--cs-badge-bg custom properties some other
   modifiers set without anything reading them (see the PRIMITIVE — .cs-badge comment above). */
.cs-badge--rfq-draft {
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
}

/* =============================================================================
   [cs_leg_detail] — the empty_leg single (docs/06 "Leg Detail", docs/29 §29.15
   step 10). Deliberately mirrors [cs_leg_summary]'s structure and class
   patterns immediately above rather than inventing a second visual language
   for what is, in both cases, "one leg's facts" -- consistency the doc's own
   "one vocabulary" note (shortcodes.php header) asks for.
   ============================================================================= */

.cs-leg-detail {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-2xs);
	max-width: 720px;
}

/* 2026-08-20 user request: "the content of the leg detail page should be wider in desktop view" --
   720px reads narrow once the specs table, route line and badge row all have room to breathe;
   widened only at Divi's own tablet/desktop boundary (--cs-bp-lg, the project's real breakpoint, not
   an invented one) so mobile is untouched. 960px, not full Divi row width: still comfortably inside
   a typical ~1080px row so this never has to fight the section's own padding. */
@media (min-width: 981px) {
	.cs-leg-detail {
		max-width: 960px;
	}
}

/* Title + Save/Share/Request-a-quote as one row (2026-08-20 QA revision) -- .cs-leg-detail__header
   wraps the H1 and .cs-leg-detail__top-actions below as row siblings. `float` is not how this is
   done: `.cs-leg-detail` is a flex column, and float has no effect on a flex item per spec (the
   "floated, next to the title" ask reads the same either way -- title takes the available width,
   actions sit at the row's end). `flex-wrap` rather than a fixed no-wrap row: the row's own content
   (a text-labelled Save button, a full "Request a quote" CTA) is wide enough that forcing it beside
   a long leg title on a narrow viewport would crush both -- wrapping to its own line under the
   title is the graceful fallback the mobile-first breakpoints elsewhere on this page already use. */
.cs-leg-detail__header {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-start;
	gap: var(--cs-space-sm);
}

/* Same three controls that used to be two separately-placed things (icon-actions near the bottom,
   the CTA further up still) -- see leg_detail_top_actions_markup()'s docblock for the full history.
   `margin-left: auto`, not the header's `justify-content: space-between`, is what actually pins this
   to the right -- `space-between` only distributes items *within one flex line*, so on a real, long
   leg title ("F9 Transporter-14 Residual Slot A") the row wraps onto its own line and `space-between`
   has nothing left to distribute there, leaving it flush left instead of right (caught live,
   2026-08-20). `margin-left: auto` pulls it to the line's own right edge regardless of which line it
   ends up on, wrapped or not. `flex-wrap` for the same narrow-viewport reason as the header above:
   three controls (an icon button, a labelled Save button, a full-width-looking CTA) do not all
   belong forced onto one line under ~360px. */
.cs-leg-detail__top-actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: flex-end;
	gap: var(--cs-space-2xs);
	margin-left: auto;
}

.cs-leg-detail__title {
	margin: 0;
	font-size: var(--cs-text-2xl, var(--cs-text-xl));
	font-weight: 700;
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

.cs-leg-detail__badges {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-3xs);
}

/* "Float some containers if they don't need full page width" (prompts/Leg-Detail-Enhancements) --
   the same root cause as .cs-share's earlier fix above: as direct children of .cs-leg-detail's flex
   column, these two short, single-line facts were stretching to the column's full width under
   flexbox's default align-items:stretch, despite neither needing more than its own content width.
   `float` itself has no effect here (flex items ignore it per spec) -- align-self:flex-start is the
   equivalent "don't claim the full row" for a flex child. */
.cs-leg-detail__route,
.cs-leg-detail__provider {
	display: flex;
	align-items: center;
	align-self: flex-start;
	gap: var(--cs-space-3xs);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-base);
}

/* render_watch_launch_button()'s output, same "don't claim the full row" fix as .cs-leg-detail__route
   immediately above -- this paragraph only ever wraps one inline-flex .cs-button, so it should hug
   that button's own width, not stretch under the flex column's default align-items:stretch. */
.cs-leg-detail__watch {
	align-self: flex-start;
}

/* leg_route_line_markup()'s link to the departure location's public page (prompts/Leg-Detail-
   Enhancements). Underlined by default, not just on hover/focus -- a link that only reveals itself as
   clickable on interaction fails this project's "never a signal on hover alone" pattern for the same
   reason colour-alone status signals do elsewhere in this file. */
.cs-leg-detail__route a {
	color: inherit;
	text-decoration: underline;
	text-decoration-color: var(--cs-border);
	text-underline-offset: 2px;
}

.cs-leg-detail__route a:hover,
.cs-leg-detail__route a:focus-visible {
	color: var(--cs-text-strong);
	text-decoration-color: currentColor;
}

/* The header "book by" countdown (leg_book_by_header_markup(), prompts/Leg-Detail-Enhancements) --
   decorative/aria-hidden reinforcement of the specs table's "Book by" row, so this is styled for
   visual urgency only, never the sole carrier of the fact (the text row is that). Colour escalates at
   7 days, weight at 3 -- matching leg_book_by_urgency_class()'s PHP tiers exactly; there is no third
   CSS state for the sub-day/sub-hour precision tiers because those only change the *text* the script
   renders, not this element's colour or weight. */
.cs-leg-detail__countdown {
	display: flex;
	align-items: center;
	align-self: flex-start;
	gap: var(--cs-space-3xs);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-base);
	font-variant-numeric: tabular-nums;
}

.cs-leg-detail__countdown-icon {
	flex: 0 0 auto;
}

.cs-leg-detail__countdown--urgent {
	color: var(--cs-danger-text);
}

.cs-leg-detail__countdown--critical {
	color: var(--cs-danger-text);
	font-weight: 700;
}

/* Same docs/06 collapse rule as .cs-leg-summary__specs -- stacked below the
   project's real ~767px breakpoint, two columns above it. */
.cs-leg-detail__specs {
	display: grid;
	grid-template-columns: 1fr;
	gap: 0;
	margin-top: var(--cs-space-xs);
	padding-top: var(--cs-space-xs);
	border-top: 1px solid var(--cs-border);
}

.cs-leg-detail__specs dt {
	margin-top: var(--cs-space-2xs);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-leg-detail__specs dt:first-child {
	margin-top: 0;
}

.cs-leg-detail__specs dd {
	color: var(--cs-text-strong);
	font-family: var(--cs-font-mono);
	font-size: var(--cs-text-sm);
	font-variant-numeric: tabular-nums;
}

@media (min-width: 768px) {
	.cs-leg-detail__specs {
		grid-template-columns: max-content 1fr;
		column-gap: var(--cs-space-md);
		row-gap: var(--cs-space-2xs);
		align-items: baseline;
	}

	.cs-leg-detail__specs dt,
	.cs-leg-detail__specs dt:first-child {
		margin-top: 0;
	}

	.cs-leg-detail__specs dd {
		font-size: var(--cs-text-base);
	}
}

.cs-leg-detail__envelope-notes {
	margin-top: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

/* The "Remaining capacity" gauge (leg_capacity_bar_markup()) -- Feature QA-04 item 1 (2026-08-23):
   "the bar graphs should be positioned z-index behind the data for the respective fields". Originally
   (2026-08-19) this was a separate track+fill block rendered below the whole specs `<dl>`; it now
   lives inside that one row's own `<dd>` (`.cs-leg-detail__specs-bar-cell`).

   Two-layer gauge (2026-08-24, replacing a single ambiguous-fill-direction bar -- see the PHP
   docblock): the cell itself paints first and IS the base/track layer -- `--cs-void-600`, always full
   width, representing TOTAL capacity. `.cs-leg-detail__capacity-bar-fill` paints on top of that (still
   behind the row's own text) and is the REMAINING-capacity layer -- width = remaining/total, so a
   fully-open leg covers the whole track and a sold-out leg shows bare track with no fill. No second
   DOM element needed for the track: `isolation: isolate` gives the cell its own stacking context, and
   a stacking context's own background paints before any of its positioned descendants regardless of
   z-index, so the void-600 cell background is reliably "below" the z-index:-1 fill without needing to
   be a sibling node -- the fill's `z-index: -1` still only ever means "behind this cell's own text",
   never "behind the page". Fill colour history (2026-08-24): started as `--cs-brand-500`, swapped to
   `--cs-info-tint` over an AA-margin concern (brand-500 measured ~4.6:1 against `--cs-text-strong`,
   thin over AA's 4.5:1 floor) -- but info-tint (#12224A) and the track's own `--cs-void-600` (#1E2B47)
   are both dark, low-saturation navy at nearly the same luminance, so the two layers were visually
   indistinguishable at a glance (user report, same day: bar reads as "all track, no fill" regardless
   of percentage). Tried `--cs-success-tint` next, then settled back on the original `--cs-brand-500`
   (#2E6BE6, user's explicit final call) -- saturated and clearly lighter than the void-600 track even
   though both are blue, and 4.6:1 still clears AA (just with less margin than the tints above; watch
   this if `--cs-text-strong` or `--cs-brand-500` ever move). Still a fixed colour regardless of
   percentage -- this is a magnitude, not a status badge.

   Reused as-is (2026-08-24, docs/29 §29.13 Q4) for the "Available volume" row via
   leg_volume_bar_markup() -- same two class names, same mechanism, no volume-specific CSS: the
   fill's meaning (width = remaining/total) is identical for both rows, so a second class would only
   be a second name to keep in sync with this one. */
/* Divi's own reset re-applies `dd { margin-left: 1.5em }` (a definition-list indent) to every
   plain `<dd>` in this row's `<dl>` -- our own rule never overrides it, so every sibling row's text
   sits 1.5em right of the grid column edge. The horizontal bleed below used to zero that out
   entirely (`margin-left: -padding-left` nets to 0), pulling THIS row's text 1.5em left of every
   other row's. `calc(1.5em - var(--cs-space-2xs))` restores that same 1.5em baseline on the left --
   keeping it in `em` (not a `--cs-space-*` rem token) so it tracks this element's own font-size the
   same way Divi's rule does at every breakpoint -- then subtracts the bleed padding so the net text
   position still matches siblings exactly. Right/vertical bleed is unaffected: Divi's reset never
   touches margin-right, and there's no inherited vertical margin to preserve. */
.cs-leg-detail__specs-bar-cell {
	position: relative;
	isolation: isolate;
	padding: 2px var(--cs-space-2xs);
	margin: -2px calc(-1 * var(--cs-space-2xs)) -2px calc(1.5em - var(--cs-space-2xs));
	border-radius: var(--cs-radius-sm);
	background: var(--cs-void-600);
}

.cs-leg-detail__capacity-bar-fill {
	position: absolute;
	inset: 0;
	z-index: -1;
	display: block;
	background: var(--cs-brand-500);
	border-radius: inherit;
}

.cs-leg-detail__specs-bar-text {
	position: relative;
	z-index: 1;
}

/* docs/06 §Leg Detail item 2's source/non-representation disclosure -- "small,
   but not styled as fine print": readable body text, not the footer's muted
   12px register (that decision covers the footer's one line only).
   Italicised per prompts/Leg-Detail-Enhancements (2026-08-19) -- style alone,
   not a second "not styled as fine print" contradiction: font-size and colour
   are unchanged, this is the one property that request actually asked for. */
.cs-leg-detail__disclosure {
	margin-top: var(--cs-space-sm);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-style: italic;
}

.cs-leg-detail__disclosure p {
	margin: 0 0 var(--cs-space-3xs);
}

/* Item 3's WebGL globe+arc (assets/route-visual.js, docs/06 §Leg Detail item 3). Ships `hidden` in
   markup -- the script only ever removes it after confirming a working WebGL context, so a browser
   that never runs the script, or fails to get a context, shows nothing here rather than an empty
   box. aspect-ratio (not a JS-computed height) sizes the reserved space before the canvas' drawing
   buffer exists, so there is no layout shift when the script reveals it. `[hidden]` rule mirrors
   .cs-share[hidden] above against the same Divi specificity risk.

   QA-02 items 5/6 reverse prompts/Leg-Detail-Enhancements' original "full width of page body" full-
   bleed treatment: "decrease the width of the canvas to match page width boundaries" + "it should be
   page centered". No more 100vw/negative-margin breakout (or route-visual.js's alignFullBleed(),
   which existed only to correct that trick's own off-centre-ancestor problem -- moot once the trick
   itself is gone) -- a plain block, unset width, matches .cs-leg-detail's own column exactly the way
   every sibling element in it already does, no special-casing needed to keep it centred with the rest
   of the page's content. */
.cs-leg-detail__route-visual {
	position: relative;
	aspect-ratio: 21 / 9;
	margin-top: var(--cs-space-xs);
	background: var(--cs-void-800);
	/* Feature-QA-01 item "the container should have a frame that matches the theme" -- same
	   border/radius pairing .cs-leg-card and .cs-filters use elsewhere on this site
	   (var(--cs-border-surface) + var(--cs-radius-md)), not a one-off value invented for this box. */
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	/* No overflow: hidden (deliberate, not dropped by accident): the launch-site bubble
	   (.cs-leg-detail__route-bubble below) floats above its marker point and would get clipped by it
	   whenever the marker sits near the top edge of the frame. The canvas's own WebGL drawing is
	   rounded to match via its own `border-radius: inherit` below instead. CR-28's country-label
	   escape-at-full-zoom fix clips at .cs-leg-detail__route-labels instead of here, for the same
	   reason -- see that rule's own comment. */
}

/* 4/3, not the full-bleed era's flatter 21/6 -- that ratio only made sense against a box spanning
   the full viewport; back at column width, 21/6 reads as a thin letterbox sliver, and 4/3 is what
   the pre-full-bleed 720px box used for exactly this reason. Mobile's 21/9 base above is untouched:
   it was never full-bleed-specific, it was always sized against column width. */
@media (min-width: 768px) {
	.cs-leg-detail__route-visual {
		aspect-ratio: 4 / 3;
	}
}

.cs-leg-detail__route-visual[hidden] {
	display: none;
}

/* CR-16 -- leg_route_visual()'s "unsited" case (a real destination_body with no coordinates and no
   station/orbit record to hang a floating waypoint off of): a plain caption, not a globe marker,
   since there is no direction/position data to plot one against. Anchored to a fixed corner
   (position: absolute against the container's own `position: relative` above) rather than
   JS-tracked like the bubbles -- it isn't following a projected 3D point, it doesn't need per-frame
   updates. Server-rendered (shortcodes.php), so it reveals for free with the rest of the container;
   no separate JS wiring. */
.cs-leg-detail__route-unsited {
	position: absolute;
	left: var(--cs-space-xs);
	bottom: var(--cs-space-xs);
	margin: 0;
	padding: var(--cs-space-3xs) var(--cs-space-xs);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-void-900);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

/* position: absolute + inset (not a normal-flow width:100%/height:100%) -- found by rendering, not by
   reading the spec: .cs-leg-detail__route-visual is a flex item (.cs-leg-detail is `display:flex;
   flex-direction:column`), and a percentage-height *replaced element* (this canvas) filling a flex
   item whose own height comes from `aspect-ratio` creates a circular size dependency that Chromium
   resolves by falling back to the canvas's intrinsic 300x150 (2:1) ratio instead -- the aspect-ratio
   this file sets above is silently ignored, and every downstream measurement (bar height, drag/zoom
   math, the bubble's projected position) would have been computed against the wrong canvas size.
   Taking the canvas out of flow with `position: absolute` breaks that circularity: its size is
   resolved against .cs-leg-detail__route-visual's own box (which does have `position: relative`,
   needed for the launch-site bubble's positioning anyway), not the other way around. */
.cs-leg-detail__route-canvas {
	display: block;
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	/* Rounds the WebGL drawing itself to match the container's frame above -- `inherit` (not a
	   restated var(--cs-radius-md)) so the two can never drift out of sync, and applied here rather
	   than `overflow: hidden` on the container, which would also clip the launch-site bubble. */
	border-radius: inherit;
}

/* The canvas becomes a real focus stop the moment route-visual.js's reveal() runs (see that file and
   leg_route_visual()'s docblock for why aria-hidden comes off the container at the same time) -- it
   needs its own visible focus ring like any other interactive control, not the invisible default an
   unstyled canvas:focus would get. */
.cs-leg-detail__route-canvas:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

/* The launch-site bubble (route-visual.js's buildBubble()) -- real, focusable HTML positioned over
   the canvas by JS every frame (a WebGL canvas cannot expose a link to assistive tech or SEO on its
   own), not a WebGL-drawn label. Centred on the projected marker point via the transform, so the JS
   only ever has to set `left`/`top` to the point itself.

   Feature-QA-01 "visually appealing ... include the rocket/vehicle badge too" -- flex row of icon +
   name + the same .cs-badge--vehicle pill used elsewhere on this page, with a soft accent glow (this
   is the one place on the page floating over a WebGL scene rather than flat content, so a little more
   presence than a plain bordered box reads intentional instead of merely "still there"). */
.cs-leg-detail__route-bubble {
	position: absolute;
	transform: translate(-50%, -140%);
	display: flex;
	align-items: center;
	gap: var(--cs-space-3xs);
	/* 2026-08-30 user rule: a little taller (was --cs-space-3xs vertical, --cs-space-xs's the
	   smallest step up on this scale) and text a little smaller than --cs-text-xs, this design
	   system's own floor size -- calc() off it rather than a bare magic number, since there's no
	   smaller named token to reach for. */
	padding: var(--cs-space-xs) var(--cs-space-2xs);
	border: 1px solid var(--cs-accent-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-void-900);
	color: var(--cs-accent-400);
	font-size: calc(var(--cs-text-xs) * 0.9);
	white-space: nowrap;
	pointer-events: auto;
	box-shadow: 0 0 12px 0 rgb(34 211 238 / 35%); /* --cs-accent-400 glow */
}

.cs-leg-detail__route-bubble-icon {
	flex: none;
}

.cs-leg-detail__route-bubble-name {
	text-decoration: underline;
}

/* Feature-QA-01 "waypoint in space for deployment" (route-visual.js's buildWaypointBubble()) -- same
   bubble shape as the origin's above, recoloured to --cs-brand-400/WAYPOINT_COLOR (the arc-stub's own
   colour) rather than --cs-accent-400, so this bubble visibly belongs to the line it terminates
   instead of reading as a second launch site. Underline only when it is actually a link -- a `<span>`
   (no public page for this destination) has nothing to underline toward.

   CR-16 fix (Owen Castellan, live render finding, 2026-08-25): overrides the base bubble's shared
   `transform: translate(-50%, -140%)`. That -140% assumes headroom above the anchor point for the
   bubble to float into -- true for the departure marker (near the equator/globe surface, comfortably
   inside the frame), but the waypoint's own screen position (waypointDir, further out along the same
   ray at ORBIT_WAYPOINT_ALT) sits close to the container's own top edge under perspective projection
   at the default camera distance, confirmed by live render (leg 111/117, ISS and Lunar Gateway --
   identical offset both times, not incidental). The same -140% there pushed almost the entire bubble
   above the container, overlapping page content. -50% halves the float distance, keeping the bubble
   comfortably inside the frame at the same camera default this was found at -- a CSS-only fix,
   deliberately not a camera/pitch change, since this is about where a fixed-size box floats relative
   to an anchor already close to the frame edge, not about moving the anchor itself. **Not yet
   re-verified live** -- needs the same render check before trusting the exact offset is enough. */
.cs-leg-detail__route-bubble--waypoint {
	border-color: var(--cs-brand-400);
	color: var(--cs-brand-400);
	box-shadow: 0 0 12px 0 rgb(94 143 240 / 35%); /* --cs-brand-400 glow */
	transform: translate(-50%, -50%);
}

a.cs-leg-detail__route-bubble--waypoint .cs-leg-detail__route-bubble-name {
	text-decoration: underline;
}

.cs-leg-detail__route-bubble[hidden] {
	display: none;
}

/* CR-28: the clipped layer country labels render into (route-visual.js's reveal()), sized and rounded
   exactly like the canvas below (`inset: 0`, `border-radius: inherit`) so the clip boundary is the
   same visual frame a visitor already sees. `overflow: hidden` here, deliberately NOT on
   .cs-leg-detail__route-visual itself above -- that container stays `overflow: visible` on purpose
   (its own comment explains why: the launch-site/waypoint bubbles anchor above their marker and
   would clip near the top edge otherwise). Only labels -- which are centred ON their point and can
   legitimately project past the frame at full zoom-in -- need clipping; the bubbles do not, and must
   stay outside this layer to keep that existing fix intact. `pointer-events: none` since nothing in
   this layer is ever a click target (restated from the label rule below for defense in depth, in case
   a future label-layer child forgets its own). */
.cs-leg-detail__route-labels {
	position: absolute;
	inset: 0;
	overflow: hidden;
	border-radius: inherit;
	pointer-events: none;
}

/* QA-02 country labels (route-visual.js's reveal()/updateCountryLabels()) -- centred on their own
   point (transform: translate(-50%,-50%), not the bubbles' -140% vertical offset above: those anchor
   a box *above* a marker point, this centres text *on* one), deliberately quieter than the origin/
   waypoint bubbles (no border/background/glow) so a page with several dozen of these doesn't compete
   with the two bubbles that are actually interactive. `pointer-events: none` -- purely informational,
   never a click target. */
.cs-leg-detail__route-country-label {
	position: absolute;
	transform: translate(-50%, -50%);
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	text-shadow: 0 1px 2px rgb(6 10 19 / 80%); /* --cs-void-900, so it reads over any texture colour underneath */
	white-space: nowrap;
	pointer-events: none;
}

.cs-leg-detail__route-country-label[hidden] {
	display: none;
}

/* The always-visible discoverability cue (route-visual.js's reveal()) -- a mouse user has no other
   way to learn the globe is draggable at all, and this is the plain-text form of the same instruction
   the canvas's aria-label carries for assistive tech. */
.cs-leg-detail__route-hint {
	margin: var(--cs-space-3xs) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

/* 2026-08-30 -- "Origin"/"Destination" camera-focus pills (route-visual.js's reveal()), a sibling
   of the hint above for the same aspect-ratio-sizing reason. Same accent colour as
   .cs-leg-detail__route-bubble (the launch-site tooltip) since both are "this is a real, clickable
   point on this globe" affordances -- a pill and a bubble reading as two different systems would be
   a false distinction. */
.cs-leg-detail__route-nav {
	display: flex;
	gap: var(--cs-space-2xs);
	margin: var(--cs-space-2xs) 0 0;
}

.cs-leg-detail__route-nav-pill {
	padding: var(--cs-space-3xs) var(--cs-space-sm);
	border: 1px solid var(--cs-accent-400);
	border-radius: 999px; /* pill shape -- no --cs-radius-full token exists on this scale */
	background: transparent;
	color: var(--cs-accent-400);
	font-size: var(--cs-text-xs);
	cursor: pointer;
}

.cs-leg-detail__route-nav-pill:hover,
.cs-leg-detail__route-nav-pill:focus-visible {
	background: var(--cs-accent-400);
	color: var(--cs-void-900);
}

/* 2026-08-30 user request: "add a 'Zoom: {zoom level}' label ... so we know where we are when
   zooming" (route-visual.js's reveal()/updateZoomLabel()). Corner overlay, same technique as
   .cs-leg-detail__route-unsited above (position: absolute against the container's own
   position: relative), but top-right rather than bottom-left so it never collides with that note
   or with the Origin/Destination pills and hint text, which are siblings BELOW the container, not
   inside it. `pointer-events: none` -- purely informational, same reasoning as the activate-hint
   and country labels above. */
.cs-leg-detail__route-zoom-label {
	position: absolute;
	top: var(--cs-space-xs);
	right: var(--cs-space-xs);
	margin: 0;
	padding: var(--cs-space-3xs) var(--cs-space-xs);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-void-900);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-family: var(--cs-font-mono);
	white-space: nowrap;
	pointer-events: none;
}

/* Feature-QA-01 item 3's "click to activate" overlay (route-visual.js's reveal()) -- centred over the
   canvas, not a sibling like the hint/flip controls below, so it reads as belonging to the globe
   itself. `pointer-events: none` so the click it is asking for lands on the canvas underneath it, not
   on this label; JS removes the element outright on activation rather than hiding it, so it never
   lingers as an invisible click target. */
.cs-leg-detail__route-activate-hint {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	margin: 0;
	padding: var(--cs-space-3xs) var(--cs-space-2xs);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-void-900);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	white-space: nowrap;
	pointer-events: none;
}

/* Feature-QA-01 item 1's "Flip vertical axis" checkbox (route-visual.js's buildFlipAxisControl()) --
   same muted register as .cs-leg-detail__route-hint immediately above, since this is a control about
   that same hint's instructions, not a new visual weight class of its own. */
.cs-leg-detail__route-flip {
	display: flex;
	align-items: center;
	gap: var(--cs-space-3xs);
	margin: var(--cs-space-3xs) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

/* =============================================================================
   ER-11 (2026-09-03) — the globe's auto-rotate toggle (route-visual.js's
   buildAutoRotateControl()). A sibling of the container like the hint/pills above it, for the same
   aspect-ratio-sizing reason those are.

   Deliberately NOT styled as a third .cs-leg-detail__route-nav-pill: the pills are momentary camera
   actions and this is persistent state, and this project has already learned that whitespace alone
   cannot separate two groups of buttons that look identical (33px of gap failed; a real border
   worked). Square corners (--cs-radius-sm, not the pills' 999px), the muted register of
   .cs-leg-detail__route-flip, and its own row are the boundary.

   Every colour is a var(--cs-*) lookup with no hardcoded hue anywhere, so the LCARS console skin's
   token remap re-skins this for free if the globe ever renders there (it does not today — the
   console's leg page is a card, not docs/06's Leg Detail).
   ============================================================================= */
.cs-leg-detail__route-motion {
	display: flex;
	margin: var(--cs-space-3xs) 0 0;
}

.cs-leg-detail__route-motion-toggle {
	display: inline-flex;
	align-items: center;
	gap: var(--cs-space-2xs);
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-3xs) var(--cs-space-xs);
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-sm);
	background: transparent;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	cursor: pointer;
	transition: color var(--cs-transition), border-color var(--cs-transition);
}

.cs-leg-detail__route-motion-toggle:hover,
.cs-leg-detail__route-motion-toggle:focus-visible {
	border-color: var(--cs-brand-400);
	color: var(--cs-text-strong);
}

.cs-leg-detail__route-motion-toggle:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

/* The visible On/Off chip. `aria-hidden` in the markup — it mirrors aria-pressed for sighted users
   rather than adding a second announcement. Text, never colour alone, is what carries the state;
   the border/weight change below is reinforcement on top of the word, not the signal itself. */
.cs-leg-detail__route-motion-state {
	padding: 0 var(--cs-space-3xs);
	border: 1px solid currentColor;
	border-radius: var(--cs-radius-sm);
	font-family: var(--cs-font-mono);
	font-size: var(--cs-text-xs);
	line-height: 1.6;
}

.cs-leg-detail__route-motion-toggle[aria-pressed="true"] {
	color: var(--cs-text-strong);
}

/* Item 4: always visible, non-collapsible, warning accent plus a visible
   "Important" heading -- text and colour together, matching this project's
   never-colour-alone rule for every other status signal. */
.cs-leg-detail__risk {
	margin-top: var(--cs-space-xs);
	padding: var(--cs-space-sm);
	border: 1px solid var(--cs-warning-text);
	border-left: 4px solid var(--cs-warning-text);
	border-radius: var(--cs-radius-md);
	background: var(--cs-warning-tint);
	color: var(--cs-warning-text);
}

.cs-leg-detail__risk-heading {
	margin: 0 0 var(--cs-space-3xs);
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: var(--cs-tracking-label);
}

.cs-leg-detail__risk p:last-child {
	margin-bottom: 0;
}

.cs-leg-detail__actions {
	margin-top: var(--cs-space-2xs);
}

/* Item 5's "dead CTA, not a live one that fails after 20 fields" state --
   visually a button, semantically inert (aria-disabled, no href). */
.cs-button--disabled {
	display: inline-block;
	padding: var(--cs-space-xs) var(--cs-space-md);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface);
	border: 1px solid var(--cs-border);
	color: var(--cs-text-muted);
	font-weight: 600;
	cursor: not-allowed;
}

.cs-leg-summary__more a {
	display: inline-flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-link);
	font-size: var(--cs-text-sm);
	font-weight: 600;
	text-decoration: underline;
}

.cs-leg-summary__more a:hover,
.cs-leg-summary__more a:focus {
	color: var(--cs-link-hover);
}

.cs-leg-summary--unavailable {
	background: var(--cs-surface-sunken);
}

.cs-leg-summary--unavailable::before {
	background: var(--cs-warning-text);
}

/* Neither an error nor an offer — a dead end with one way out. The accent edge
   goes neutral, because there is no journey step here to lead. */
.cs-leg-summary--not-found {
	gap: var(--cs-space-sm);
	background: var(--cs-surface-sunken);
	text-align: center;
	align-items: center;
}

.cs-leg-summary--not-found::before {
	background: var(--cs-border-strong);
}

.cs-leg-summary--not-found h2 {
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-xl);
	font-weight: 700;
	line-height: var(--cs-leading-tight);
}

.cs-leg-summary--not-found p {
	max-width: 46ch;
	color: var(--cs-text-muted);
}

/* =============================================================================
   [cs_city_legs] — nearby capacity on a City Page
   ============================================================================= */

.cs-city-legs {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-md);
	color: var(--cs-text);
	line-height: var(--cs-leading-normal);
}

.cs-city-legs__heading {
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-2xl);
	font-weight: 700;
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

.cs-city-legs__intro {
	max-width: 68ch;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-md);
}

/* Persistent, not empty-state-only — someone who saw three legs and rejected
   them is more invested than someone who saw none, so this reads as a panel
   rather than a footnote. */
.cs-city-legs__consolation {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	/* Sized to content, not to the section: a full-width tinted band holding one
	   left-aligned sentence and a button reads as a broken layout. */
	width: fit-content;
	max-width: 100%;
	gap: var(--cs-space-2xs) var(--cs-space-sm);
	padding: var(--cs-space-sm) var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-left: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-accent);
	color: var(--cs-text-on-accent);
}

.cs-city-legs__empty {
	max-width: 68ch;
	padding: var(--cs-space-md);
	border: 1px dashed var(--cs-border-strong);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
}

.cs-city-legs__see-all,
.cs-city-legs__actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-2xs);
}

/* =============================================================================
   [cs_city_index] — the /cities/ hub
   Up to 500 rows, so the list is multi-column from 480px up: a single column of
   150 city names is a scroll, not an index.
   ============================================================================= */

.cs-city-index {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-sm);
	color: var(--cs-text);
	line-height: var(--cs-leading-normal);
}

.cs-city-index__group {
	margin: 0;
	padding-bottom: var(--cs-space-3xs);
	border-bottom: 2px solid var(--cs-brand-400);
	color: var(--cs-text-strong);
	font-size: var(--cs-text-sm);
	font-weight: 700;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-city-index__group:not(:first-child) {
	margin-top: var(--cs-space-lg);
}

.cs-city-index__list {
	display: grid;
	grid-template-columns: 1fr;
	gap: 0 var(--cs-space-lg);
	margin-bottom: var(--cs-space-md);
}

@media (min-width: 480px) {
	.cs-city-index__list {
		grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
	}
}

/* 44px minimum row height — docs/06 lines 471 and 534: this page is almost
   entirely links, so the row IS the tap target. */
.cs-city-index__item {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0 var(--cs-space-2xs);
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-3xs) 0;
	border-bottom: 1px solid var(--cs-border);
}

/* The anchor, not the row, carries the 44px height (docs/06 lines 471, 534) —
   a tall row is no use if only the text is clickable. It is deliberately not
   allowed to grow: pushed to full width it shunts the editor-only "not
   indexed" flag to the far right of the column, away from the city it labels. */
.cs-city-index__item > a {
	display: flex;
	flex: 0 1 auto;
	align-items: center;
	min-height: var(--cs-tap-target);
	/* Height alone leaves a 44×44 rule half-met on the short names this index is
	   full of ("Ely", "Bonn"), where the text is barely 30px wide. */
	min-width: var(--cs-tap-target);
	color: var(--cs-link);
	font-weight: 600;
	text-decoration: none;
}

.cs-city-index__item > a:hover,
.cs-city-index__item > a:focus {
	color: var(--cs-link-hover);
	text-decoration: underline;
}

/* Forced onto its own line under the city name, which is where a subhead reads
   as a subhead rather than as a second link. */
.cs-city-index__subhead {
	flex: 1 0 100%;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-snug);
}

/* Editor-only, per city_index_rows()'s `edit_posts` peek. Deliberately loud —
   an editor scanning the hub needs to spot it, and no visitor ever sees it. */
.cs-city-index__flag {
	flex: none;
	padding: 0 var(--cs-space-3xs);
	border: 1px solid var(--cs-warning-text);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-warning-tint);
	color: var(--cs-warning-text);
	font-size: var(--cs-text-xs);
	font-weight: 700;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-city-index__empty {
	padding: var(--cs-space-lg);
	border: 1px dashed var(--cs-border-strong);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
	text-align: center;
}

/* =============================================================================
   CHROME — Theme Builder header/footer templates
   Styles the two header/footer templates built headlessly in `et_header_layout`
   / `et_footer_layout` posts: blog 1's marketing chrome and the app-shell bar
   shared by blog 1's /dashboard/ and blog 2's Default Website Template.

   Divi's own `.et_pb_*` selectors appear here only where a Theme Builder module
   is being reshaped — the marketing header's account slot and the footer's legal
   menu. Everything else targets the `.cs-` markup emitted by chrome.php.
   ============================================================================= */

/* -- Shared shell ------------------------------------------------------- */

.cs-chrome .et_pb_row {
	width: 92%;
	max-width: 1200px;
	padding-top: 0;
	padding-bottom: 0;
}

/* -- Marketing header (blog 1 default template) -------------------------- */

/* Divi stacks the 3/4 + 1/4 row below 981px, which would drop the account links
   onto a second line under the logo and hamburger. Below the md breakpoint the
   row becomes one flex line again so "Log in" stays reachable in the bar. */
@media (max-width: 980px) {
	.cs-chrome--marketing .et_pb_row {
		display: flex;
		flex-wrap: nowrap;
		align-items: center;
		gap: var(--cs-space-sm);
	}

	.cs-chrome--marketing .et_pb_column {
		width: auto;
		margin-bottom: 0;
	}

	.cs-chrome--marketing .et_pb_column:first-child {
		flex: 1 1 auto;
		min-width: var(--cs-tap-target);
	}

	/* Divi's own <=479px breakpoint (`.et_pb_column.et_pb_column_1_4 { width: 100% }`)
	   outranks the `width: auto` above (3 classes vs. 2) at real phone widths, giving
	   this column a flex-basis of the whole row. With flex-shrink: 0 that basis is never
	   given up, so the menu column above -- min-width'd to 0 -- gets crushed to 0px. That
	   0px column is the containing block for .et_mobile_menu's `position: absolute; width:
	   100%`, so the open dropdown inherits 0 width and its background box disappears too
	   (text still renders past the 0px box via overflow: visible, reading as unstyled). */
	.cs-chrome--marketing .et_pb_column:last-child {
		flex: 0 0 auto;
		width: auto !important;
	}
}

/* The desktop header's logo, sized in px rather than as a share of the bar (ER-20).

   Divi sizes it as `#logo { max-height: 51% }` -- 51% of a bar that is 80px at rest and 54px
   once `.et-fixed-header` applies -- and the Customizer's "Hide Logo Image" setting adds
   `.et_hide_primary_logo #main-header:not(.et-fixed-header) .logo_container { height: 0 }` on
   top of it. Measured on the test network at 1440px before this block: 0x0 at rest and 41x28
   scrolled, of which the mark's own ink is about 19px, because the source PNG carries 32% of
   its height as transparent padding. Below 981px Divi's mobile header rules bypass both, so
   the phone header (64x43) was already larger than the desktop one -- the defect was
   desktop-only, which is why this whole block is, too.

   A px height rather than a percentage: the two bar heights are a scroll state, not a design
   intent, and a mark legible at rest should not give up a quarter of itself for scrolling.

   The width cap is what makes this aspect-agnostic. It holds for the square mark and for
   ER-20's wide lockup (4.62:1) alike, so changing which asset the Customizer holds needs no
   CSS change here. `min(..., 60vw)` rather than a bare px because at the narrow end of this
   tier the cap has to be a share of the viewport or a wide lockup crowds the nav toggle. */
@media (min-width: 981px) {
	body.et_hide_primary_logo #main-header:not(.et-fixed-header) .logo_container {
		height: 100%;
		opacity: 1;
	}

	#main-header #logo {
		max-height: none;
		height: 44px;
		width: auto;
		max-width: min(340px, 60vw);
	}

	#main-header.et-fixed-header #logo {
		height: 36px;
	}
}

/* -- Native "Slide In" mobile menu: patching Divi's own incomplete behaviour -----------------
   docs/45 §45.5-§45.7: the mobile nav now uses Divi's native Header Format = Slide In (both sites),
   not a hand-built component -- this block is NOT that component (see docs/45 §45.2, removed here
   as dead code once the native mechanism was confirmed live). It patches specific, confirmed defects
   in Divi's own Slide In implementation: no scrim, no scroll-lock, keyboard focus free to escape into
   undimmed page content while the panel is open, two stray default elements (cart/search) that don't
   apply to this site, and a background colour that Divi's own "Header Background Color" Customizer
   option never reaches (that option's generated CSS targets `#main-header`/`.et_mobile_menu` only --
   confirmed by reading the generated et-divi-customizer-global.min.css directly -- never
   `.et_slide_in_menu_container`, the Slide In panel's own wrapper).

   Everything here keys off `body.et_pb_slide_menu_active`, the class Divi's own JS already toggles
   when the panel opens/closes -- no new JS. */

.et_slide_in_menu_container {
	background-color: var(--cs-ink-900) !important; /* #0B1220 -- matches primary_nav_bg, see docs/45 §45.7 */
}

.et_slide_in_menu_container .et-cart-info,
.et_slide_in_menu_container .et-search-form {
	display: none;
}

body.et_pb_slide_menu_active {
	overflow: hidden;
}

body.et_pb_slide_menu_active::before {
	content: "";
	position: fixed;
	inset: 0;
	background: rgba(6, 10, 19, 0.2); /* --cs-void-900 at 20% -- docs/45 §45.10, down from 70% per QA-03.1 */
	z-index: 999998; /* one below the panel's own Divi-core 999999, above everything else */
	/* #main-header (holding the hamburger/close toggle) doesn't set a z-index of its own, so at
	   999998 this pseudo-element paints above it and would otherwise intercept the click meant to
	   close the panel -- confirmed live (the click just stopped reaching the toggle). Safe to make
	   click-through: the content underneath is already visibility: hidden below, not merely covered,
	   so there's nothing this scrim still needs to catch clicks on. */
	pointer-events: none;
}

/* docs/45 §45.10 -- QA-03.1: the close [X] toggle (Divi's own `.mobile_menu_bar`, an icon-font glyph
   swapped to an X via `.et_toggle_slide_menu` when open) had no colour rule of its own here and was
   rendering unreadably dark against the now-dark `.et_slide_in_menu_container` panel behind it once
   open. `--cs-text-strong` is this codebase's existing highest-contrast-on-dark token (15.89:1),
   already used elsewhere for exactly this pairing -- not a new colour choice. */
body.et_pb_slide_menu_active .mobile_menu_bar {
	color: var(--cs-text-strong);

	/* BF-25: pin the [X] to the viewport while the panel is open, instead of letting it ride the
	   page. Divi's Slide In animates `right` on #page-container (an inline style, 291.9px at a
	   390px viewport), sliding the whole page -- #main-header included -- left by the panel's own
	   width. The toggle travels with the header, so the close control ends up outside the menu it
	   closes, to its left, over dimmed content: measured live at 39px left of the panel edge at
	   390px and 420px at 1920px, since the offset grows with the panel/container arithmetic.
	   `position: fixed` takes the toggle out of that shifted coordinate space entirely; the offsets
	   below are derived, not chosen. 40px is `.et_slide_menu_top`'s own padding -- the panel's
	   content gutter -- and 24px is the closed hamburger's existing y. Each loses 6px, half the
	   difference between the 32px glyph and the 44px tap target, so the glyph re-centres onto the
	   exact pixel it already occupied vertically and lands flush with the menu items' right edge
	   horizontally (verified at 390/480/768/1280/1920: glyph right edge 350/440/728/1240/1880,
	   equal to the items' own right edge at every width). The closed hamburger is untouched -- this
	   whole block only applies while `.et_pb_slide_menu_active` is on <body>. */
	position: fixed;
	top: 18px;
	right: 34px;
	display: flex;
	align-items: center;
	justify-content: center;
	width: var(--cs-tap-target);
	height: var(--cs-tap-target);
	padding: 0;
}

/* BF-25, and both halves are consequences of the rule above, not independent choices.

   z-index: the toggle now sits over the panel, and the panel is Divi-core `z-index: 999999` while
   #main-header is 99999. A child cannot escape its ancestor's stacking context, so raising the
   toggle alone would do nothing -- the header itself has to outrank the panel. Nothing else in the
   header can paint over the panel as a side effect: the header's right edge and the panel's left
   edge stay flush throughout the animation (both are driven off the same animated offset), and the
   only other thing left in the visible strip is the logo, off-screen at x=-250.

   background/box-shadow: taking the toggle out of flow drops the header's measured height from
   80px to 24px while open, which left an orphaned ~98x24 block of header background floating above
   the scrim in the strip beside the panel. That strip is the off-canvas remainder of a header whose
   contents are all off-screen, so it has nothing left to show -- dropping its background is a
   truer fix than propping the height back up with a hardcoded min-height, and it avoids touching a
   height Divi's own JS manages (docs/45 §45.8 collapsed the header to 0px doing exactly that).
   Verified across open/close/reopen: the header returns to 80px and the closed state is unchanged. */
body.et_pb_slide_menu_active #main-header {
	z-index: 1000000;
	background-color: transparent;
	box-shadow: none;
}

/* #et-boc has exactly three children: the panel, #main-header (left alone -- it stays visible so
   the close icon keeps working), and #et-main-area (every other page element). The cookie-consent
   banner and its "Manage Cookie Consent" floating widget are both separate top-level children of
   <body>, outside #page-container entirely -- the banner was the concrete case found live (undimmed,
   fully interactive, positioned right next to the open panel); the widget was found by tabbing
   through with this fix applied and landing on it anyway. */
body.et_pb_slide_menu_active #et-main-area,
body.et_pb_slide_menu_active #cmplz-cookiebanner-container,
body.et_pb_slide_menu_active #cmplz-manage-consent {
	visibility: hidden;
}

@media (prefers-reduced-motion: reduce) {
	.et_slide_in_menu_container {
		transition-duration: 0.01ms !important;
	}
}

/* BF-26 -- the nav toggle and the panel's own links had no focus style at all. Measured live
   2026-09-06 on test-01-provider: `outline: rgb(248,250,252) none 0px` and `box-shadow: none` on
   both, so once assets/nav-keyboard.js makes the toggle reachable a keyboard user would still have
   had nowhere visible to be. WCAG 2.4.7, and the half of "not keyboard accessible" that CSS owns.

   3px/`--cs-link` is this stylesheet's existing focus ring (`.cs-button:focus-visible` and the four
   listing components beside it), not a new treatment. `--cs-link` is `--cs-brand-300` at 8.68:1,
   which is the pairing already chosen for this exact dark background -- the panel and the header
   bar are both `--cs-ink-900`.

   :focus-visible rather than :focus, matching the same rules: a pointer press must not paint a
   ring, and BF-26's JS moves focus to the toggle whenever the panel opens, including on a mouse
   click. Without :focus-visible that behaviour would put a ring on screen for every mouse user. */
.et_toggle_slide_menu:focus-visible,
.et_slide_in_menu_container a:focus-visible {
	outline: 3px solid var(--cs-link);
	outline-offset: 2px;
}

.cs-chrome__account .et_pb_code_inner {
	display: flex;
	justify-content: flex-end;
}

/* -- App-shell bar (blog 2 default template, blog 1 /dashboard/) ---------- */

.cs-appbar {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-xs) var(--cs-space-md);
	min-height: var(--cs-tap-target);
}

.cs-appbar__brand {
	display: inline-flex;
	align-items: center;
	margin-right: auto;
	text-decoration: none;
}

/* The logo is whatever each site's Divi Customizer holds, at whatever aspect ratio. Capping the
   width as well as the height keeps a wide wordmark from pushing "Browse availabilities" and the
   account links off the 480px bar that this file designs first.

   No `min(…, 100%)` guard here, unlike `.cs-footer__logo`, and the reason is structural rather than
   "it happens to fit": Divi's default row is 80% wide, so `40vw` is 40/80 = exactly half the
   container at every viewport width. It cannot overflow. Above 768 the 220px cap sits inside a
   ~614px container at the tier's narrowest. `max-height` is in any case the binding constraint on
   a realistic logo — a 36px-tall image would have to be about 8.5:1 to reach the 307px `40vw`
   allows at 767px — so the width cap here is close to inert and a guard on it would be inert twice.

   One oddity, recorded so it is not rediscovered as a bug: the cap goes 307px at 767 to 220px at
   768, so the allowance *shrinks* as the viewport grows past the breakpoint. The height cap hides
   this for any normal logo; a very wide wordmark image is the one case where it would show as a
   size jump. */
.cs-appbar__logo {
	display: block;
	width: auto;
	max-width: 40vw;
	max-height: 2.25rem;
}

@media (min-width: 768px) {
	.cs-appbar__logo {
		max-width: 220px;
	}
}

.cs-appbar__wordmark {
	color: var(--cs-text-strong);
	font-size: var(--cs-text-lg);
	font-weight: 700;
	letter-spacing: var(--cs-tracking-tight);
}

.cs-appbar__browse {
	display: inline-flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-link);
	font-size: var(--cs-text-sm);
	font-weight: 600;
	text-decoration: none;
}

.cs-appbar__browse:hover,
.cs-appbar__browse:focus {
	color: var(--cs-link-hover);
	text-decoration: underline;
}

/* -- Account menu (both headers) ----------------------------------------- */

/* Both headers render inside a Divi Theme Builder header layout, which is exactly
   ".et-l--header ul" -- one of the group Divi's own dynamic CSS targets with
   `list-style-type: disc; padding: 0 0 23px 1em;`, at (0,1,1) specificity. A bare
   class selector here is (0,1,0) and loses no matter where it sits in source order
   -- same root cause already found and fixed on the footer nav, console nav and
   queue-queue tabs. `!important` on both the <ul> and the <li> (inheritance is not
   enforcement -- a separate Divi selector can re-add the marker on the list item
   itself), and `padding: 0 !important` to match Divi's shorthand rather than only
   countering one side of it. */
.cs-account-menu__list {
	display: flex;
	align-items: center;
	gap: var(--cs-space-sm);
	margin: 0;
	padding: 0 !important;
	list-style: none !important;
}

.cs-account-menu__item {
	margin: 0;
	list-style: none !important;
	list-style-type: none !important;
	list-style-image: none !important;
}

.cs-account-menu__link {
	display: inline-flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-link);
	font-size: var(--cs-text-sm);
	font-weight: 600;
	text-decoration: none;
	white-space: nowrap;
}

.cs-account-menu__link--muted {
	color: var(--cs-text-muted);
	font-weight: 400;
}

.cs-account-menu__link:hover,
.cs-account-menu__link:focus {
	color: var(--cs-link-hover);
	text-decoration: underline;
}

.cs-chrome :focus-visible,
.cs-appbar :focus-visible,
.cs-account-menu :focus-visible {
	outline: 3px solid var(--cs-link);
	outline-offset: 2px;
}

/* -- Footer ---------------------------------------------------------------
   `[cs_footer]` renders the whole footer body, identically on every site, so
   this styles one block rather than a per-site arrangement of Divi modules.
   The legal links used to be a Divi menu module, which below 981px hid the
   list behind a hamburger — terms, privacy and the prohibited-cargo policy are
   exactly the links that must not be one tap further away on a phone. As plain
   markup they simply stack.

   Layout is a brand block plus three link columns. It is one grid whose column
   template changes at each breakpoint rather than three separate arrangements,
   because the source order (brand, Platform, Company, Legal) is already the
   reading order at every width — nothing has to move, only the number of
   tracks changes:

     base        one column; everything stacks in source order
     480         two tracks — brand beside Platform, Company beside Legal
     768         three equal tracks, brand spanning them horizontally above
     981         `1.5fr` brand + three equal tracks — brand is 1.5/4.5 of the
                 width, i.e. the left third, links the right two-thirds

   The 480 tier exists for height, not for width. Eleven links at the 44px tap
   floor plus three headings, the brand block and the disclaimer stack to around
   865px in one column — well over a 667px phone viewport, where the previous
   footer was four lines. Two tracks takes roughly 400px of dead scroll out from
   under every page without weakening a target size.

   Nothing spans below 768. A block that spans a row it does not fill is the one
   failure this arrangement is prone to, so the span appears exactly where the
   brand is also laid out horizontally to fill it, and nowhere else.

   Every tier is `min-width`, per the mandate at the top of this file: a rule
   that should stop applying is reset in the next tier up (`grid-column: auto`)
   rather than fenced with a `max-width`, so there is no fractional-pixel gap
   between a `max-width: 767px` rule and a `min-width: 768px` one. The 480/768/981
   set is the documented one; this block deliberately adds no fourth value.

   `minmax(0, ...)` on every track, not a bare `1fr`: a grid item's default
   `min-width: auto` refuses to shrink below its longest unbreakable word, and
   "Prohibited Cargo Policy" in the narrowest tier would push the track wider
   than its share and overflow the row.

   The footer section keeps `--cs-surface-dark` (#0B1220), which is now only
   one step off the page canvas rather than a slab against a white page, so it
   carries a top edge to stay legible as a distinct region. */

.cs-chrome--footer {
	/* Space before the footer's own border-top, not inside it: without this the border sits flush
	   against whatever content the page happened to end on, which reads as a stray rule rather than
	   the start of a new region (user, 2026-08-19). */
	margin-top: 18px;
	border-top: 1px solid var(--cs-border-surface);
}

.cs-footer {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-lg);
	color: var(--cs-text-on-dark);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-normal);
}

.cs-footer p {
	margin: 0;
	padding: 0;
}

.cs-footer__grid {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	gap: var(--cs-space-lg);
}

/* Two tracks, and the brand takes one of them rather than spanning. Spanning
   here would put a narrow left-aligned stack across a full row and leave most
   of it empty — a grid that failed to fill rather than a deliberate band. The
   fix at 768 is to lay the brand out horizontally so it uses the width it
   claims, but that does not work at this tier: logo (up to 200px) + copyright
   (~240px at 12px) + badge (44px) + gaps is around 510px, so a horizontal brand
   would wrap or overflow at 480 exactly. Not spanning avoids the question. */
@media (min-width: 480px) {
	.cs-footer__grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: var(--cs-space-lg) var(--cs-space-md);
	}
}

@media (min-width: 768px) {
	.cs-footer__grid {
		/* 4 nav columns as of ER-08 (Platform/Company/Legal/Account) -- was repeat(3, ...) when
		   there were only 3; a stale count here doesn't error, it just wraps the 4th column onto
		   its own row using this tier's own track widths, which looks like a layout bug rather
		   than an intentional row. Bump this whenever a column is added or removed. */
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}

	/* Now the brand does span, because three link columns beside it would each
	   be too narrow — so it has to use the width it claims, hence horizontal.
	   Source order is untouched, so nothing moves for a screen reader. */
	.cs-footer__brand {
		grid-column: 1 / -1;
		flex-direction: row;
		align-items: center;
		gap: var(--cs-space-md);
	}

	.cs-footer__copyright {
		margin-inline-start: auto;
	}
}

@media (min-width: 981px) {
	.cs-footer__grid {
		/* 1 brand track + 4 nav columns as of ER-08 -- was repeat(3, ...) when Account didn't
		   exist. Without this bump the 4th nav column has no track of its own and wraps under
		   the brand's 1.5fr track instead of sitting level with the other three. */
		grid-template-columns: minmax(0, 1.5fr) repeat(4, minmax(0, 1fr));
		gap: var(--cs-space-md);
	}

	/* Back to a column: it has its own track here and no width to fill. */
	.cs-footer__brand {
		grid-column: auto;
		flex-direction: column;
		align-items: flex-start;
		gap: var(--cs-space-2xs);
	}

	.cs-footer__copyright {
		margin-inline-start: 0;
	}
}

/* -- Footer brand block (logo, copyright, agency badge) ------------------- */

.cs-footer__brand {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--cs-space-2xs);
}

.cs-footer__brand-link {
	display: inline-flex;
	align-items: center;
	max-width: 100%;
	text-decoration: none;
}

/* `min(200px, 100%)`, not a bare `200px`: from the 480 tier the brand lives in
   the narrowest grid track on the page, and the track is a share of the Divi
   row's content width — not of the viewport. Divi's default row is 80% wide, so
   a 480px phone gives roughly a 180px track and a bare 200px cap overflows it.
   That 80% is the factor to remember when sizing anything in this file against
   a viewport number: content width is 0.8 × viewport, and a grid track is a
   share of *that*.
   The `100%` term makes the cap track whatever the container actually turns out
   to be, which is the part of this that does not depend on knowing Divi's row
   settings. `.cs-footer__brand-link` above carries the same guard because the
   percentage resolves against it, and an anchor already too wide would make the
   image's 100% too wide with it.

   Taller than the app bar's 2.25rem cap: this one sits in a column of its own
   rather than in a bar that must also hold two links on a 480px screen. */
.cs-footer__logo {
	display: block;
	width: auto;
	max-width: min(200px, 100%);
	max-height: 3rem;
}

.cs-footer__wordmark {
	color: var(--cs-text-strong);
	font-size: var(--cs-text-lg);
	font-weight: 700;
	letter-spacing: var(--cs-tracking-tight);
}

/* The copyright is the quietest thing on the page and is still held to 4.5:1 —
   `--cs-text-muted` is 7.30:1 on this surface. */
.cs-footer__copyright {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

/* Two characters is a small target, so the 44px floor the rest of the footer's
   links use is kept here as whitespace around the text rather than as a drawn
   box — a pill this large next to a 12px copyright line would read as the
   loudest element in the footer, which is the opposite of the intent.

   Both dimensions, not just height: "8LM" at 12px bold with `tracking-label`
   is about 34px wide, so `min-height` alone gives a 44 × 34 target. That clears
   WCAG 2.5.8 AA (24 × 24) but not docs/06's own 44 × 44 rule, which is the one
   this file holds every other link in the footer to. */
.cs-footer__agency {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-height: var(--cs-tap-target);
	min-width: var(--cs-tap-target);
	color: var(--cs-link);
	font-size: var(--cs-text-xs);
	font-weight: 700;
	letter-spacing: var(--cs-tracking-label);
	text-decoration: underline;
}

.cs-footer__agency:hover,
.cs-footer__agency:focus {
	color: var(--cs-link-hover);
}

/* -- Footer link columns -------------------------------------------------- */

/* `--cs-text-sm`, matching the links it heads rather than the 12px badge/eyebrow
   register: those are labels, this is a heading with a list under it, and a
   heading set smaller than its own items is the wrong signal however much weight
   and colour say otherwise. Uppercase at 12px is also where letterform
   discrimination starts to suffer — no ascenders or descenders to key off. */
.cs-footer__nav-title {
	margin: 0 0 var(--cs-space-3xs);
	padding: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-sm);
	font-weight: 700;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-footer__nav-list {
	display: flex;
	flex-direction: column;
	margin: 0;
	padding: 0;
	list-style: none !important;
}

/* list-style on the <ul> alone is inherited, not enforced -- a more specific Divi core selector can
   still win on the <li> itself and put the bullet back (same root cause fixed on the console nav
   this session). Killing it explicitly here closes that regardless of cascade order. */
.cs-footer__nav-item {
	margin: 0;
	list-style: none !important;
	list-style-type: none !important;
	list-style-image: none !important;
}

.cs-footer__nav-link {
	display: inline-flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-link);
	font-weight: 600;
	text-decoration: none;
}

.cs-footer__nav-link:hover,
.cs-footer__nav-link:focus {
	color: var(--cs-link-hover);
	text-decoration: underline;
}

/* Below the whole layout: a qualification on the site rather than a member of
   any column.

   The 12px muted register here is a decision the site owner made explicitly, so
   do not "fix" it back without asking them. The argument against it is real and
   was put to them: this is the "a listing is not a guarantee of flight"
   sentence, docs/06:304 specifies the equivalent Leg Detail disclosure as
   "plain body text (small, but not styled as fine print)", and at this size and
   colour it renders at the same visual rank as the copyright boilerplate. They
   chose the smaller/secondary treatment anyway, and presentation of a
   non-required disclosure is theirs to choose. Contrast is not the issue —
   `--cs-text-muted` is 7.30:1 on this surface, well clear of 4.5.

   Note the asymmetry this creates: docs/06:304 still governs the Leg Detail
   disclosures, which are the ones shown at the point of enquiry. This footer
   line is a site-wide restatement, not a substitute for those, and nothing here
   licenses styling those as fine print too. */
/* Sitewide pre-launch notice (chrome.php's CS_FOOTER_ALPHA_NOTICE, CON-2). Deliberately NOT the
   muted fine-print treatment the capacity disclaimer below gets: this one replaces the visible
   [COUNSEL TO CONFIRM] / DRAFT markers that were removed from the legal pages, so demoting it to
   the least-noticeable line in the footer would undo the point of adding it. Full-strength text
   colour at the small step rather than the micro step, and it renders above the disclaimer. Both
   colours are token reads, so the LCARS remap on `.cs-lcars` re-skins it with everything else. */
.cs-footer__alpha-notice {
	max-width: 78ch;
	margin-bottom: 0.5em;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-sm);
}

.cs-footer__disclaimer {
	max-width: 78ch;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

/* Sitewide build-version subtext (build-version.php's cs_build_version_display()) — chrome, not a
   disclosure, so it gets no special legal-copy treatment: same muted colour as the disclaimer
   above but a hair smaller and with breathing room over it, deliberately the least prominent line
   in the footer. */
.cs-footer__build-version {
	margin-top: 0.25em;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	opacity: 0.75;
}

.cs-footer :focus-visible {
	outline: 3px solid var(--cs-link);
	outline-offset: 2px;
}

/* =============================================================================
   UTILITY — .cs-lcars-stripe (docs/23 Q9)
   The Landing hero's "LCARS accent stripe". Add this class in the Divi module's
   Advanced → CSS Class field on the Fullwidth Header. It draws a segmented bar
   along the element's bottom edge in the real LCARS palette.

   It is decorative: it carries no text, no state and no meaning, so §6's QA
   note about contrast-checking the LCARS palette before using it outside
   /console does not apply — nothing is ever read off it. This is the ONLY
   place `--cs-lcars-*` is used on the public site.
   ============================================================================= */

.cs-lcars-stripe {
	position: relative;
}

.cs-lcars-stripe::after {
	content: "";
	position: absolute;
	inset: auto 0 0 0;
	height: 6px;
	pointer-events: none;
	background:
		linear-gradient(90deg,
			var(--cs-lcars-orange) 0 22%,
			var(--cs-ink-900) 22% 23.5%,
			var(--cs-lcars-blue) 23.5% 48%,
			var(--cs-ink-900) 48% 49.5%,
			var(--cs-lcars-lilac) 49.5% 62%,
			var(--cs-ink-900) 62% 63.5%,
			var(--cs-lcars-red) 63.5% 71%,
			var(--cs-ink-900) 71% 72.5%,
			var(--cs-lcars-orange) 72.5% 100%
		);
}

/* =============================================================================
   CITY PAGE SECTIONS
   [cs_city_hero], [cs_city_sites], [cs_city_related], [cs_city_map].
   One template serves all 150 city pages, so nothing here may depend on a
   per-city override — every value is either a token or derived from content.
   ============================================================================= */

/* The hero is full-bleed for the same reason Browse's filter panel is: it is the
   band that establishes the page, and a boxed hero inside a 1200px row reads as
   a card rather than a header. Same `vw` breakout, same `overflow-x: clip`
   requirement on the section — see the note on `.cs-page--browse .cs-filters`. */
.cs-page--city {
	overflow-x: clip;
}

.cs-city-hero {
	margin-inline: calc(50% - 50vw);
	background-color: var(--cs-surface-dark);
	background-position: center;
	background-size: cover;
	background-repeat: no-repeat;
}

/* THE SCRIM. This is the whole of docs/06 §City Page item 1's "applied
   unconditionally": it is a rule on an element the shortcode always emits, not a
   setting anybody has to remember per city. The hero image is editor-supplied
   for each of 150 cities and nobody audits city #12, so the text has to stay
   legible over an image chosen without reference to it.

   A flat overlay would wash out the photograph, so it is a vertical gradient
   that is heaviest where the copy sits. Measured against `--cs-text-strong` on a
   mid-grey photo the bottom stop clears AA for large text; the `background-color`
   above is what carries it when the image is dark, missing or still loading. */
.cs-city-hero__scrim {
	padding: var(--cs-space-2xl) max(4vw, calc(50vw - (1200px / 2)));
	background-image: linear-gradient(
		180deg,
		rgba(6, 10, 19, 0.55) 0%,
		rgba(6, 10, 19, 0.78) 60%,
		rgba(6, 10, 19, 0.92) 100%
	);
}

.cs-city-hero--no-image .cs-city-hero__scrim {
	background-image: none;
}

.cs-city-hero__title {
	max-width: 22ch;
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-3xl);
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

.cs-city-hero__subhead {
	max-width: 60ch;
	margin: var(--cs-space-sm) 0 0;
	color: var(--cs-text);
	font-size: var(--cs-text-md);
	line-height: var(--cs-leading-snug);
}

/* Directly under the subhead, not in the footer of the page: arriving in the
   wrong city is the most common mismatch this page has, and the full
   related-cities list sits below an essay most of those visitors never reach. */
.cs-city-hero__elsewhere {
	margin: var(--cs-space-md) 0 0;
	font-size: var(--cs-text-sm);
}

.cs-city-hero__elsewhere a {
	display: inline-flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-text-strong);
}

/* -----------------------------------------------------------------------------
   Served launch sites — cards on desktop, stacked rows on mobile.
   -------------------------------------------------------------------------- */

.cs-city-sites__heading,
.cs-city-related__heading {
	margin: 0 0 var(--cs-space-sm);
	color: var(--cs-text-strong);
	font-size: var(--cs-text-2xl);
	line-height: var(--cs-leading-snug);
}

.cs-city-sites__list,
.cs-city-related__list {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-2xs);
	margin: 0;
	padding: 0;
	list-style: none;
}

.cs-city-sites__item {
	display: flex;
	flex-direction: column;
	justify-content: center;
	gap: var(--cs-space-3xs);
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-xs);
	border: 1px solid var(--cs-border-surface);
	border-left: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
}

/* The anchor carries its own 44px, not just the row: a tall row is no use if
   only the text is clickable. Same rule as `.cs-city-index__item > a`. */
.cs-city-sites__link {
	display: flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-text-strong);
	font-weight: 600;
}

.cs-city-sites__note {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-snug);
}

.cs-city-related__item {
	display: flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	border-bottom: 1px solid var(--cs-border);
}

.cs-city-related__item > a {
	display: flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	width: 100%;
}

/* -----------------------------------------------------------------------------
   Static map. `figure` rather than a bare `img` so the block has somewhere to
   grow a caption if the served-sites legend is ever wanted on screen.
   -------------------------------------------------------------------------- */

.cs-city-map {
	margin: 0;
}

.cs-city-map__image {
	display: block;
	width: 100%;
	height: auto;
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
}

/* CR-38's interactive map canvas -- a <div> the JS SDK draws into, not an <img>, so it has no
   intrinsic size the way .cs-city-map__image's 640x320 attributes give it; aspect-ratio reproduces
   the same 2:1 footprint (640/320) so the page doesn't reflow once assets/location-map.js paints in.
   Shares .cs-city-map__image's own border/radius treatment so the two read as the same component
   whether or not JS ran. */
.cs-city-map__canvas {
	aspect-ratio: 2 / 1;
	width: 100%;
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
}

.cs-city-map__canvas:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

/* Same discoverability-cue treatment as .cs-leg-detail__route-hint (route-visual.js) -- a plain
   caption below the map, not an overlay: unlike that globe (where activation gates BOTH drag and
   zoom, prominent enough to earn a centred overlay), this only gates scroll-zoom, dragging already
   works immediately -- a smaller affordance for a smaller ask. `[hidden]` is set by
   assets/location-map.js on activation, same attribute-toggle convention every other progressive-
   enhancement hint element in this project uses rather than a class. */
.cs-location-map__activate-hint {
	margin: var(--cs-space-3xs) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

/* Two columns for the site cards once there is room for two lines of
   `access_note` side by side. Below this they stack, which is also the reading
   order: nearest site first. */
@media (min-width: 48em) {
	.cs-city-sites__list {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

/* =============================================================================
   docs/41 — /vehicles/ index and /vehicles/<slug>/ detail

   The hero deliberately does NOT reuse .cs-city-hero--no-image's strip-the-scrim
   behaviour: that variant exists for a fixed, already-verified-dark system token
   (--cs-surface-dark) as its fallback fill. This page's fallback is a hand-picked
   per-vehicle badge_color hex with no independent luminance guarantee beyond the
   PHP-side CS_VEHICLE_BADGE_HERO_LUMINANCE_MAX ceiling, so the scrim gradient
   stays active over it as a second line of defense (docs/41 §41.4 item 1).
   ============================================================================= */

.cs-vehicle-hero {
	margin-inline: calc(50% - 50vw);
	background-position: center;
	background-size: cover;
	background-repeat: no-repeat;
}

.cs-vehicle-hero__scrim {
	position: relative;
	padding: var(--cs-space-2xl) max(4vw, calc(50vw - (1200px / 2)));
	background-image: linear-gradient(
		180deg,
		rgba(6, 10, 19, 0.55) 0%,
		rgba(6, 10, 19, 0.78) 60%,
		rgba(6, 10, 19, 0.92) 100%
	);
}

/* Wraps the `<h1>` and, when set, vehicle_status_badge()'s pill (qa-2 review, Feature-Release-03) —
   `flex-wrap` rather than a fixed row so the badge drops to its own line under the title on narrow
   viewports instead of squeezing either element (docs/38's mobile-first mandate; this hero has no
   fixed height to protect the way a card does). No-op when the badge is absent (`active` vehicles,
   the common case): a `<h1>` alone in a flex row lays out identically to a bare `<h1>`. */
.cs-vehicle-hero__title-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-sm);
}

.cs-vehicle-hero__title {
	max-width: 22ch;
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-3xl);
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

.cs-vehicle-hero__subhead {
	max-width: 60ch;
	margin: var(--cs-space-sm) 0 0;
	color: var(--cs-text);
	font-size: var(--cs-text-md);
	line-height: var(--cs-leading-snug);
}

.cs-vehicle-hero__elsewhere {
	margin: var(--cs-space-md) 0 0;
	font-size: var(--cs-text-sm);
}

.cs-vehicle-hero__elsewhere a {
	display: inline-flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-text-strong);
}

/* Bottom-corner micro-caption, independent of the h1/subhead's own contrast
   clearance from the scrim — qa-2/visual-engineer-qa review, docs/41 §41.4
   item 1. Absent for the three easter-egg vehicles (no photo, nothing to
   credit), so no reserved space here — it only occupies room when rendered. */
.cs-vehicle-hero__credit {
	margin: var(--cs-space-lg) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

.cs-vehicle-hero__credit a {
	color: inherit;
	text-decoration: underline;
}

/* -----------------------------------------------------------------------------
   Specs — a fact list, same dt/dd shape as .cs-city-sites' cards.
   -------------------------------------------------------------------------- */

.cs-vehicle-specs__heading {
	margin: 0 0 var(--cs-space-sm);
	color: var(--cs-text-strong);
	font-size: var(--cs-text-2xl);
	line-height: var(--cs-leading-snug);
}

.cs-vehicle-specs__list {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-2xs);
	margin: 0;
}

.cs-vehicle-specs__fact {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-3xs);
	padding: var(--cs-space-xs);
	border: 1px solid var(--cs-border-surface);
	border-left: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
}

.cs-vehicle-specs__fact dt {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

.cs-vehicle-specs__fact dd {
	margin: 0;
	color: var(--cs-text-strong);
}

.cs-vehicle-specs__fact a {
	color: var(--cs-link);
}

@media (min-width: 48em) {
	.cs-vehicle-specs__list {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

/* -----------------------------------------------------------------------------
   [cs_vehicle_legs] — open capacity on a vehicle detail page. Same shape as
   .cs-city-legs (heading/consolation/empty/see-all); .cs-leg-grid's 3-per-row
   rule is extended in the shared selector above, not duplicated here.
   -------------------------------------------------------------------------- */

.cs-vehicle-legs {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-md);
	color: var(--cs-text);
	line-height: var(--cs-leading-normal);
}

.cs-vehicle-legs__heading {
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-2xl);
	font-weight: 700;
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

.cs-vehicle-legs__consolation {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	width: fit-content;
	max-width: 100%;
	gap: var(--cs-space-2xs) var(--cs-space-sm);
	padding: var(--cs-space-sm) var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-left: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-accent);
	color: var(--cs-text-on-accent);
}

.cs-vehicle-legs__empty {
	max-width: 68ch;
	padding: var(--cs-space-md);
	border: 1px dashed var(--cs-border-strong);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
}

.cs-vehicle-legs__see-all {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-2xs);
}

/* -----------------------------------------------------------------------------
   [cs_vehicle_index] — the /vehicles/ hub. Same fixed-aspect-ratio-media +
   text-below-media card shape qa-2/visual-engineer-qa specified: a badge_color
   swatch is proven safe only as a small accent (.cs-badge--vehicle), not as a
   text backdrop, so text never sits on top of either the photo or the swatch
   (docs/41 §41.3).
   -------------------------------------------------------------------------- */

.cs-vehicle-index__list {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-md);
	margin: 0;
	padding: 0;
	list-style: none;
}

@media (min-width: 480px) {
	.cs-vehicle-index__list {
		grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
	}
}

@media (min-width: 60em) {
	.cs-vehicle-index__list {
		grid-template-columns: repeat(var(--cs-vehicle-index-columns, 3), minmax(0, 1fr));
	}
}

.cs-vehicle-index__item {
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface);
	overflow: hidden;
}

.cs-vehicle-index__link {
	display: flex;
	flex-direction: column;
	height: 100%;
	color: inherit;
	text-decoration: none;
}

/* Fixed aspect ratio, same container for a real photo or the badge_color
   fallback swatch — qa-2 review: mismatched photo heights break row alignment,
   and a flat-colour card next to a photo card reads as broken unless both
   share one shape. */
.cs-vehicle-index__media {
	display: block;
	aspect-ratio: 4 / 3;
	overflow: hidden;
	background: var(--cs-surface-sunken);
}

.cs-vehicle-index__media img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* Soft corner highlight, not a new asset or a text overlay: docs/41 §41.3's swatch review ("colour
   strip only, no text sits on it") stands, this just keeps the flat fill from reading as a missing
   image rather than a deliberate placeholder. */
.cs-vehicle-index__media--swatch {
	background:
		radial-gradient(120% 120% at 15% 15%, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0) 60%),
		var(--cs-vehicle-swatch, var(--cs-surface-sunken));
}

/* Fixed to exactly 2 lines regardless of actual name length ("F9" vs "Delta IV Heavy") so every
   card's text block is the same height whether or not a name wraps — the grid's own row-stretch
   only equalizes cards within one row, not across rows/pages (visual-engineer-qa, 2026-08-24). A
   name over 2 lines truncates with an ellipsis rather than growing the card. */
.cs-vehicle-index__name {
	display: -webkit-box;
	overflow: hidden;
	margin: var(--cs-space-sm) var(--cs-space-sm) 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-lg);
	font-weight: 700;
	line-height: var(--cs-leading-snug);
	-webkit-line-clamp: 2;
	line-clamp: 2;
	-webkit-box-orient: vertical;
	min-height: calc(var(--cs-text-lg) * var(--cs-leading-snug) * 2);
}

/* Always reserves one line of height, present or not (render_vehicle_index_card() now always emits
   this span — see vehicle-pages.php) — the three easter-egg vehicles have no manufacturer, and
   without a reserved slot their cards were one line shorter than every real vehicle's card. */
.cs-vehicle-index__manufacturer {
	min-height: calc(var(--cs-text-sm) * var(--cs-leading-snug));
	margin: var(--cs-space-3xs) var(--cs-space-sm) 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-snug);
}

.cs-vehicle-index__count {
	margin: var(--cs-space-2xs) var(--cs-space-sm) var(--cs-space-sm);
	color: var(--cs-text);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

.cs-vehicle-index__empty {
	max-width: 68ch;
	padding: var(--cs-space-lg);
	border: 1px dashed var(--cs-border-strong);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
	text-align: center;
}

/* =============================================================================
   docs/35 §7 step 8 — /launch-sites/<slug>/. docs/35 §4.1 originally shipped this
   without a hero image (no field existed and inventing one for 15 rows was §1.5's
   over-building mistake in another form); `location` now carries an optional
   featured image + photo_credit/photo_license postmeta, so the page opens with a
   `.cs-launch-site__hero` band — a photo when one is set, a flat token fill when
   it isn't. Full-bleed on the marketing page since CR-12 (2026-08-25), same
   breakout City Page/Vehicle Detail's heroes use; contained inside the console
   mirror (see that block below for why).
   ============================================================================= */

/* No max-width, unlike .cs-leg-detail's 720px reading column (visual QA asked to confirm this is
   deliberate). Leg Detail is one leg's prose facts; this page is mostly grids -- the 2-column facts
   grid, the auto-fill city/vehicle lists, the leg-card grid -- that want the full 1200px row, not a
   narrower reading measure. A sparse/theoretical row with none of those grids populated is handled
   separately: render_launch_site_empty_notice() (launch-sites.php) discloses the emptiness instead
   of leaving a wide blank canvas. */
.cs-launch-site {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-xl);
	color: var(--cs-text);
	line-height: var(--cs-leading-normal);
}

/* The hero band. Contained (bordered card, normal flow) by default — this markup renders verbatim
   inside the console mirror (console-details.php), and the console's LCARS grid (assets/lcars/
   lcars.css .cs-lcars-frame) is not a centered container: a 60px sidebar rail + 8px gap offset it, so
   the `margin-inline: calc(50% - 50vw)` breakout .cs-vehicle-hero/.cs-city-hero use would not reach
   the console's edge — it assumes a symmetrically-centered parent to cancel out to 0, and would
   instead land the hero's left edge inside the sidebar rail. Full-bleed for the marketing page is
   applied below, scoped to `.cs-page--launch-site` (CR-12), leaving this base rule as the console's
   unchanged rendering. */
.cs-launch-site__hero {
	overflow: hidden;
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	background-position: center;
	background-size: cover;
	background-repeat: no-repeat;
	background-color: var(--cs-surface-dark);
}

/* Full-bleed only on the marketing page (`.cs-page--launch-site`, wp-cli/12-theme-builder-bodies.php's
   launch_site_body()) — same breakout .cs-vehicle-hero/.cs-city-hero use. `overflow-x: clip` (not
   `hidden`) for the same reason `.cs-page--browse` uses it over `.cs-page--filters`: `vw` units
   include the scrollbar width and would otherwise mint a horizontal scrollbar of their own. The
   console mirror has no `.cs-page--launch-site` ancestor, so it never matches this rule and keeps the
   contained card above. */
.cs-page--launch-site {
	overflow-x: clip;
}

.cs-page--launch-site .cs-launch-site__hero {
	margin-inline: calc(50% - 50vw);
	border: 0;
	border-radius: 0;
}

.cs-page--launch-site .cs-launch-site__hero-scrim {
	padding-inline: max(4vw, calc(50vw - (1200px / 2)));
}

/* Same construction as .cs-vehicle-hero__scrim: a flat overlay would wash out a photo, so the gradient
   is heaviest where the copy sits. Stripped on the no-image variant below, same as
   .cs-city-hero--no-image — the --cs-surface-dark fill above already clears contrast on its own. */
.cs-launch-site__hero-scrim {
	padding: var(--cs-space-lg);
	background-image: linear-gradient(
		180deg,
		rgba(6, 10, 19, 0.55) 0%,
		rgba(6, 10, 19, 0.78) 60%,
		rgba(6, 10, 19, 0.92) 100%
	);
}

.cs-launch-site__hero--no-image .cs-launch-site__hero-scrim {
	background-image: none;
}

/* Wraps the <h1> and the kind badge — flex-wrap so the badge drops to its own line under a long title
   at narrow widths rather than squeezing either element, same pattern as
   .cs-vehicle-hero__title-row. */
.cs-launch-site__hero-title-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-sm);
}

.cs-launch-site__title {
	max-width: 26ch;
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-3xl);
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

/* .cs-badge / .cs-badge--theoretical are already styled (shared with leg cards)
   — this only spaces the row it sits in under the title/kind row. */
.cs-launch-site__theoretical {
	margin: var(--cs-space-sm) 0 0;
}

/* Bottom-corner micro-caption inside the scrim, same construction as .cs-vehicle-hero__credit.
   Absent for every row with no licensed photo — render_launch_site_header() only emits this when
   $has_photo is true, so no space is reserved for it here. */
.cs-launch-site__hero-credit {
	margin: var(--cs-space-lg) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

.cs-launch-site__hero-credit a {
	color: inherit;
	text-decoration: underline;
}

.cs-launch-site__facts {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-2xs);
	margin: 0;
}

.cs-launch-site__fact {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-3xs);
	padding: var(--cs-space-xs);
	border: 1px solid var(--cs-border-surface);
	border-left: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
}

.cs-launch-site__fact dt {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

.cs-launch-site__fact dd {
	margin: 0;
	color: var(--cs-text-strong);
}

@media (min-width: 48em) {
	.cs-launch-site__facts {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

/* The real map (render_launch_site_map(), Enhancement Request 01) — `figure` rather than a bare `img`,
   same construction as .cs-city-map, matching that block's own reasoning: somewhere for a caption to
   grow into later, not currently used. Inert in production today (docs/33's key is unset), so this
   rule has no visible effect until that key is set — render_launch_site_map_section() falls back to
   .cs-launch-site__map-placeholder below until then. */
.cs-launch-site__map {
	margin: 0;
}

.cs-launch-site__map-image {
	display: block;
	width: 100%;
	height: auto;
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
}

/* CR-38's interactive map canvas -- see .cs-city-map__canvas's own comment (that class's exact
   reasoning applies here unchanged, just a different figure). */
.cs-launch-site__map-canvas {
	aspect-ratio: 2 / 1;
	width: 100%;
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
}

.cs-launch-site__map-canvas:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

/* Disclosed placeholder, not a silent gap and not a live request against an
   unset API key (docs/35 §1.2/§4.5, docs/33) — a labelled dashed box, the same
   visual register as an empty state, sized to read as "a map goes here" rather
   than as a broken layout. */
.cs-launch-site__map-placeholder {
	display: flex;
	align-items: center;
	justify-content: center;
	min-height: 12rem;
	border: 1px dashed var(--cs-border-strong);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
}

.cs-launch-site__map-placeholder-text {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

/* The live-launch video section (render_launch_site_watch_section(), launch-sites.php). 16/9 is the
   standard embed ratio -- deliberately distinct from .cs-leg-detail's 21/9 WebGL globe canvas above
   and the 4/3 card-media boxes elsewhere in this file, since the shape itself differs, not just the
   content. No JS/timer here: the iframe is YouTube's own channel-level live embed
   (/embed/live_stream?channel=), which shows its own idle frame natively when the channel isn't live
   (prompts/Enhancement-Request-02.md Scope Section 3) -- this rule only reserves and frames the space. */
.cs-launch-site__watch-frame {
	position: relative;
	aspect-ratio: 16 / 9;
	overflow: hidden;
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-dark);
}

.cs-launch-site__watch-frame iframe {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	border: 0;
}

/* Same "disclosed placeholder" visual register as .cs-launch-site__map-placeholder immediately above
   -- a labelled dashed box, not a silent gap -- shown when no real upcoming leg makes a live-video box
   meaningful yet (Gate 3, render_launch_site_watch_section()). Border/fill tokens match verbatim
   rather than inventing a second placeholder style for the same page. */
.cs-launch-site__watch-placeholder {
	display: flex;
	align-items: center;
	justify-content: center;
	min-height: 12rem;
	padding: var(--cs-space-lg);
	border: 1px dashed var(--cs-border-strong);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
	text-align: center;
}

.cs-launch-site__watch-placeholder-text {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

.cs-launch-site__details h2,
.cs-launch-site__watch h2,
.cs-launch-site__social h2,
.cs-launch-site__cities h2,
.cs-launch-site__vehicles h2,
.cs-launch-site__legs h2 {
	margin: 0 0 var(--cs-space-sm);
	color: var(--cs-text-strong);
	font-size: var(--cs-text-2xl);
	font-weight: 700;
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

/* =============================================================================
   Launch Pad Social Media (render_launch_site_social_section(), launch-sites.php) and the Live Launch
   Feed/Social tab pair (render_launch_site_tablist()) wrapping both this and .cs-launch-site__watch
   when a site has both. Tab markup/behaviour mirrors .cs-quote-detail__tabs (style.css above) exactly
   -- same tokens, same 44px tap target, same underline-plus-colour selected state -- kept as its own
   rule block rather than a shared selector list since the two tab pairs live on different pages and
   .cs-quote-detail__tabs already documents its own reasoning independently (2026-08-19 note above).
   ============================================================================= */

.cs-launch-site__tabs-wrap {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-sm);
}

.cs-launch-site__tabs {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-2xs);
}

.cs-launch-site__tab {
	flex: 1 1 0;
	min-width: 10rem;
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-2xs) var(--cs-space-sm);
	border: 1px solid var(--cs-border-strong);
	border-bottom: 3px solid transparent;
	border-radius: var(--cs-radius-sm) var(--cs-radius-sm) 0 0;
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
	font-family: inherit;
	font-size: var(--cs-text-sm);
	font-weight: 600;
	line-height: var(--cs-leading-snug);
	cursor: pointer;
}

.cs-launch-site__tab[aria-selected="true"] {
	border-bottom-color: var(--cs-brand-400);
	background: var(--cs-surface-panel);
	color: var(--cs-text);
}

.cs-launch-site__tab:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

/* launch-site-tabs.js is the only thing that ever sets this -- restated against Divi's own dynamic CSS
   for the same reason .cs-quote-detail__action[hidden] is above. */
.cs-launch-site__social[hidden],
.cs-launch-site__watch[hidden] {
	display: none;
}

.cs-launch-site__social-embed {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-2xs);
	margin: 0 0 var(--cs-space-sm);
	padding: var(--cs-space-sm);
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface);
}

.cs-launch-site__social-embed:last-child {
	margin-bottom: 0;
}

.cs-launch-site__social-heading {
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-base);
	font-weight: 700;
}

/* The honest "no plugin hooked yet" fallback (render_launch_site_social_platform_slot()) -- plain
   text plus a real link, not styled as a broken widget. */
.cs-launch-site__social-fallback {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-launch-site__social-fallback a {
	color: inherit;
	text-decoration: underline;
}

.cs-launch-site__city-list,
.cs-launch-site__vehicle-list {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-2xs) var(--cs-space-lg);
	margin: 0;
	padding: 0;
	list-style: none;
}

@media (min-width: 480px) {
	.cs-launch-site__city-list,
	.cs-launch-site__vehicle-list {
		grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
	}
}

/* 44px minimum row height, docs/06's rule applied here too — this list is
   almost entirely links, so the row is the tap target. */
.cs-launch-site__city-list li,
.cs-launch-site__vehicle-list li {
	display: flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	border-bottom: 1px solid var(--cs-border);
}

.cs-launch-site__city-list li > a,
.cs-launch-site__vehicle-list li > a {
	display: flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	width: 100%;
	color: var(--cs-link);
	font-weight: 600;
	text-decoration: none;
}

.cs-launch-site__city-list li > a:hover,
.cs-launch-site__city-list li > a:focus,
.cs-launch-site__vehicle-list li > a:hover,
.cs-launch-site__vehicle-list li > a:focus {
	color: var(--cs-link-hover);
	text-decoration: underline;
}

.cs-launch-site__more-cities a {
	display: inline-flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-text-strong);
	font-weight: 600;
}

/* Shown only when every other section is empty (facts/map/cities/vehicles/legs) — the
   theoretical/fictional rows today. Muted body text, not the placeholder's uppercase/dashed
   treatment: this is prose closing out the page, not a box standing in for a future feature. */
.cs-launch-site__empty-notice {
	margin: 0;
	color: var(--cs-text-muted);
}

.cs-launch-site__legs--arrivals {
	margin-top: var(--cs-space-lg);
}

.cs-launch-site__see-all {
	margin-top: var(--cs-space-sm);
}

/* -----------------------------------------------------------------------------
   [cs_launch_site_index] — the /launch-sites/ hub (docs/48 §48.5 sequencing
   item 2). Originally a text-only card (docs/35 §4.1, before `location` carried
   a photo field); Enhancement Request 01 adds a fixed-aspect-ratio media box
   above the existing badges/name/operator/count spans, matching
   [cs_vehicle_index]'s own media-box shape. Those spans are untouched — their
   own reserved-height rules (below) still do the height-consistency job the
   2026-08-24 visual pass fixed; the media box's height is fixed by
   aspect-ratio, never by content, so it cannot reintroduce that bug.
   -------------------------------------------------------------------------- */

.cs-launch-site-index__list {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-md);
	margin: 0;
	padding: 0;
	list-style: none;
}

@media (min-width: 480px) {
	.cs-launch-site-index__list {
		grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
	}
}

@media (min-width: 60em) {
	.cs-launch-site-index__list {
		grid-template-columns: repeat(var(--cs-launch-site-index-columns, 3), minmax(0, 1fr));
	}
}

.cs-launch-site-index__item {
	overflow: hidden;
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface);
}

.cs-launch-site-index__link {
	display: flex;
	flex-direction: column;
	height: 100%;
	padding: var(--cs-space-sm);
	color: inherit;
	text-decoration: none;
}

/* Fixed aspect ratio, [cs_vehicle_index]'s own media-box reference (4/3) as the starting point —
   real photo or the flat --cs-surface-dark fallback below, same shape either way so a photo-less row
   never sits a different height than a photographed one (qa-2's mismatched-height finding on the
   vehicle index, docs/41 §41.3, applies identically here). Pulled out to the card's edges with
   negative margins rather than restructuring __link's own padding (which every other span below still
   relies on) — `overflow: hidden` on .cs-launch-site-index__item above clips it back to the card's
   rounded corners. This element's own `overflow: hidden` is load-bearing too, not decorative: __link
   is a flex column, so without it a portrait-cropped photo's flex min-height:auto (computed from the
   img's natural ratio, ignoring object-fit) overrides the aspect-ratio and balloons the card — same
   defect fixed on the vehicle index (visual-engineer-qa, 2026-08-25). */
.cs-launch-site-index__media {
	display: block;
	aspect-ratio: 4 / 3;
	overflow: hidden;
	margin: calc(-1 * var(--cs-space-sm)) calc(-1 * var(--cs-space-sm)) var(--cs-space-sm);
	background: var(--cs-surface-sunken);
}

.cs-launch-site-index__media img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* location has no per-row accent colour the way a vehicle term's badge_color does, so this is a flat
   fill rather than [cs_vehicle_index]'s swatch treatment — the same --cs-surface-dark token
   .cs-launch-site__hero--no-image uses for the identical "this row has no photo" state on the detail
   page's own hero, reused here for visual consistency between the two. */
.cs-launch-site-index__media--no-image {
	background: var(--cs-surface-dark);
}

/* Reserves 2 badge-line rows' worth of height, not 1: a theoretical/fictional card carries 2 badges
   (kind + "Theoretical") against a real pad's 1 (kind only), and at narrow/tablet widths the longer
   kind labels wrap that pair onto a second line while the shorter ones don't -- found in QA
   (visual-engineer-qa, 2026-08-24): "Lunar Gateway"/"Deep Space 9"/"Starbase 1" (kind "Theoretical
   Orbit / Body", 24 chars) wrapped to 2 lines at 390-834px while "Starship Landing Zone"/"Vulcan"
   (kind "Theoretical Pad", 16 chars) stayed on 1, a 28px per-card mismatch among cards that are
   otherwise the same shape. One badge's own rendered height (border + padding + one line of
   --cs-text-xs) computed explicitly, doubled, plus one row gap -- not a guess. */
.cs-launch-site-index__badges {
	display: flex;
	flex-wrap: wrap;
	align-content: flex-start;
	gap: var(--cs-space-3xs);
	min-height: calc(2 * (var(--cs-text-xs) * var(--cs-leading-snug) + 0.375rem + 2px) + var(--cs-space-3xs));
}

/* Fixed to exactly 2 lines regardless of actual name length ("ISS (LEO Orbit)" vs "Starship Landing
   Zone – Theoretical Mars Surface") so every card's text block is the same height whether or not a
   name wraps — same rule [cs_vehicle_index]'s .cs-vehicle-index__name uses (visual-engineer-qa,
   2026-08-24). A name over 2 lines truncates with an ellipsis rather than growing the card. */
.cs-launch-site-index__name {
	display: -webkit-box;
	overflow: hidden;
	margin: var(--cs-space-xs) 0 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-lg);
	font-weight: 700;
	line-height: var(--cs-leading-snug);
	-webkit-line-clamp: 2;
	line-clamp: 2;
	-webkit-box-orient: vertical;
	min-height: calc(var(--cs-text-lg) * var(--cs-leading-snug) * 2);
}

/* Fixed to exactly 2 lines both ways, not just a 1-line min-height reservation: `operator` is real
   free text and this project's own values for it get long -- "CNES (French space agency), operating
   Europe's spaceport on behalf of ESA; Arianespace flies from it" (Kourou, docs/48 §48.2) wraps to
   3+ lines unclamped. Found in QA (visual-engineer-qa, 2026-08-24): Cape Canaveral/Kourou/Wallops
   rendered visibly taller than their row neighbours with only a 1-line reservation, the same class
   of bug .cs-launch-site-index__name already guards against. Reserved even when empty
   (render_launch_site_index_card() always emits this span — launch-sites.php) — the theoretical/
   fictional rows have no operator, and without a reserved slot their cards would sit shorter than
   every real pad's card. */
.cs-launch-site-index__operator {
	display: -webkit-box;
	overflow: hidden;
	min-height: calc(var(--cs-text-sm) * var(--cs-leading-snug) * 2);
	margin: var(--cs-space-3xs) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-snug);
	-webkit-line-clamp: 2;
	line-clamp: 2;
	-webkit-box-orient: vertical;
}

.cs-launch-site-index__count {
	margin: var(--cs-space-2xs) 0 0;
	color: var(--cs-text);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

.cs-launch-site-index__empty {
	max-width: 68ch;
	padding: var(--cs-space-lg);
	border: 1px dashed var(--cs-border-strong);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
	text-align: center;
}

/* =============================================================================
   .cs-provider-page — /providers/<slug>/ (provider-pages.php)
   Same shape as .cs-launch-site above: title/badges header, an optional facts
   <dl>, a leg-card grid. Reused rather than restyled from scratch — both are
   single-record public reference pages built from the same render primitives
   (render_leg_card(), the .cs-badge family).
   ============================================================================= */
.cs-provider-page {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-lg);
	color: var(--cs-text);
	line-height: var(--cs-leading-normal);
}

.cs-provider-page__title {
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-3xl);
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

.cs-provider-page__kind,
.cs-provider-page__verified {
	margin: var(--cs-space-sm) 0 0;
}

.cs-provider-page__facts {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-2xs);
	margin: 0;
}

.cs-provider-page__fact {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-3xs);
	padding: var(--cs-space-xs);
	border: 1px solid var(--cs-border-surface);
	border-left: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
}

.cs-provider-page__fact dt {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

.cs-provider-page__fact dd {
	margin: 0;
	color: var(--cs-text-strong);
}

.cs-provider-page__legs h2 {
	margin: 0 0 var(--cs-space-sm);
	color: var(--cs-text-strong);
	font-size: var(--cs-text-2xl);
	font-weight: 700;
	line-height: var(--cs-leading-tight);
	letter-spacing: var(--cs-tracking-tight);
}

.cs-provider-page__empty-notice {
	margin: 0;
	color: var(--cs-text-muted);
}

/* provider-pages.php's "Platform-Verified Provider" trust badge and its honest not-verified
   counterpart — same plain background/color shape as .cs-badge--theoretical above, not the
   icon+status family: this is a static fact about an org, not a leg lifecycle state. */
.cs-badge--verified {
	background: var(--cs-success-tint);
	color: var(--cs-success-text);
}

.cs-badge--unverified {
	background: var(--cs-surface-panel);
	color: var(--cs-text-muted);
}

/* =============================================================================
   .cs-prose — vertical rhythm for post_content pages
   Under docs/23 Q0 the marketing pages are hand-written HTML in
   wp-cli/content/*.html rendered through the theme's entry-content, which sets
   no spacing of its own. Left alone the copy runs together at one line-height
   with no separation between a heading and the paragraph above it.

   Scoped to an opt-in wrapper rather than applied to .entry-content globally:
   the same class can be adopted by the other content files one at a time, and
   nothing changes on a page nobody has looked at yet.
   ============================================================================= */

.cs-prose > * + * {
	margin-top: var(--cs-space-md);
}

/* Headings own the space above them, not below — a heading belongs to the copy
   that follows it, so the gap before it has to be the larger of the two or the
   section reads as attached to the wrong block. */
.cs-prose > h2,
.cs-prose > h3 {
	margin-top: var(--cs-space-xl);
	margin-bottom: 0;
	color: var(--cs-text-strong);
	line-height: var(--cs-leading-snug);
}

.cs-prose > h2 {
	font-size: var(--cs-text-2xl);
}

.cs-prose > h3 {
	font-size: var(--cs-text-xl);
}

/* No max-width here by design (removed 2026-08-18, was 68ch) -- the user wants copy to flow to
   the same width as the page's content modules, not sit narrower than the container around it. */
.cs-prose > p,
.cs-prose > ul,
.cs-prose > ol {
	line-height: var(--cs-leading-normal);
}

.cs-prose li + li {
	margin-top: var(--cs-space-2xs);
}

/* =============================================================================
   .cs-accordion — <details> disclosures for long reference copy

   NOT Divi accordion modules, and that is a constraint rather than a taste call:
   Divi only registers its module shortcodes while the builder is active, so
   `[et_pb_accordion]` typed into post_content renders as the literal string
   "[et_pb_accordion]" on the page. Verified against production. `<details>` is
   the same mechanism .cs-filters already uses — no JavaScript, so the panel
   behaves identically before, during and after a script failure.
   ============================================================================= */

.cs-accordion {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-2xs);
}

.cs-accordion__item {
	border: 1px solid var(--cs-border-surface);
	border-left: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-sunken);
}

/* The <summary> is the control, so it carries the 44px target itself. The
   default disclosure triangle is removed twice — `list-style` for the standard
   marker and the WebKit pseudo-element for the Safari versions that ignore it —
   and replaced with a caret that rotates, matching .cs-filters__heading. */
.cs-accordion__summary {
	display: flex;
	align-items: center;
	gap: var(--cs-space-2xs);
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-2xs) var(--cs-space-sm);
	cursor: pointer;
	list-style: none;
}

.cs-accordion__summary::-webkit-details-marker {
	display: none;
}

.cs-accordion__summary::before {
	content: "";
	flex: 0 0 auto;
	width: 0.5rem;
	height: 0.5rem;
	border-right: 2px solid var(--cs-brand-400);
	border-bottom: 2px solid var(--cs-brand-400);
	transform: rotate(-45deg);
	transition: transform var(--cs-transition);
}

.cs-accordion__item[open] > .cs-accordion__summary::before {
	transform: rotate(45deg);
}

.cs-accordion__summary h3 {
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-base);
	font-weight: 700;
	line-height: var(--cs-leading-snug);
}

.cs-accordion__summary:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: -2px;
}

.cs-accordion__body {
	padding: 0 var(--cs-space-sm) var(--cs-space-sm);
}

.cs-accordion__body > * + * {
	margin-top: var(--cs-space-sm);
}

.cs-accordion__body p,
.cs-accordion__body ul {
	max-width: 68ch;
	line-height: var(--cs-leading-normal);
}

/* =============================================================================
   .cs-rfq-gate — the logged-out block that stands in for the RFQ form
   ============================================================================= */

.cs-rfq-gate {
	padding: var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-top: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
}

.cs-rfq-gate__heading {
	margin: 0 0 var(--cs-space-2xs);
	color: var(--cs-text-strong);
	font-size: var(--cs-text-xl);
	line-height: var(--cs-leading-snug);
}

.cs-rfq-gate__why {
	max-width: 68ch;
	margin: 0 0 var(--cs-space-md);
	line-height: var(--cs-leading-normal);
}

.cs-rfq-gate__actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-2xs);
	margin: 0;
}

/* =============================================================================
   WPForms — the RFQ form, Contact, and the portal's Add Empty form

   WPForms Lite's modern render engine emits its whole palette as custom
   properties on `:root` (`<style id="wpforms-modern-full-inline-css">`), and
   every rule in wpforms-full.min.css reads them back out. So the form is
   rethemed by redeclaring the properties rather than by overriding a hundred
   selectors — custom properties inherit, so a declaration on the container
   beats the `:root` one by proximity, not by specificity, and nothing here
   needs to out-specify the plugin.

   The defaults are built for a white page and were never touched:
   `--wpforms-label-color: rgba(0,0,0,0.85)` composites to 1.05:1 against
   --cs-bg-page and `--wpforms-label-sublabel-color: rgba(0,0,0,0.55)` to
   1.03:1, which is why every label and field description on
   /request-quote/<code>/ read as blank space.

   Scoped to .wpforms-container rather than to the RFQ form's own
   `.cs-rfq-form` class: Contact and Add Empty are the same plugin carrying the
   same defaults, and there is no reason for the three to disagree.

   Field colours are the pair .cs-filters__field already uses for the Browse
   filter inputs, so a text input looks the same wherever the site draws one.
   ============================================================================= */

.wpforms-container {
	--wpforms-label-color: var(--cs-text-strong);          /* 18.94:1 on --cs-bg-page */
	--wpforms-label-sublabel-color: var(--cs-text-muted);  /*  7.73:1 */
	--wpforms-label-error-color: var(--cs-danger-text);    /* 10.41:1 */

	--wpforms-field-text-color: var(--cs-text);            /* 13.49:1 on --cs-surface */
	--wpforms-field-background-color: var(--cs-surface);
	--wpforms-field-border-color: var(--cs-border-strong);
	/* The `<select>` caret is drawn as two linear-gradients off the "spare" colour, not a glyph,
	   so it goes invisible unless this tracks the border. */
	--wpforms-field-border-color-spare: var(--cs-border-strong);
	/* The choices.js dropdown panel — a floating layer over the page, so it needs a surface of its
	   own rather than inheriting the field's. */
	--wpforms-field-menu-color: var(--cs-surface);

	--wpforms-button-background-color: var(--cs-brand-500);
	--wpforms-button-border-color: var(--cs-brand-500);
	--wpforms-button-text-color: var(--cs-on-brand);
}

/* Validation messages are the one part of the engine that ignores its own variables and hard-codes
   #D63637, which is 4.19:1 on --cs-bg-page — under the 4.5:1 the rest of the palette holds, and
   this is the text that has to be read to recover from a rejected submission. divi-child's
   stylesheet is enqueued after wpforms-full.min.css, so matching the plugin's specificity is
   enough to win. */
.wpforms-container .wpforms-error-container,
.wpforms-container .wpforms-error-noscript,
.wpforms-container label.wpforms-error,
.wpforms-container em.wpforms-error {
	color: var(--cs-danger-text);
}

/* `opacity: 0.35` is how the engine says "read-only". On a white page that reads as greyed out; on
   this canvas it takes the locked leg reference — the one field that tells you which leg you are
   quoting on — down to roughly 2:1. Said with the sunken surface and muted text the rest of the
   site uses for the same idea instead. The `pointer-events: none` in the same plugin rule is left
   alone, so the field stays uneditable. `!important` only because the declaration it is undoing
   carries it. */
.wpforms-container .wpforms-field.wpforms-field-readonly input[type="text"],
.wpforms-container .wpforms-field.wpforms-field-readonly textarea,
.wpforms-container .wpforms-field.wpforms-field-readonly select {
	opacity: 1 !important;
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted); /* 7.12:1 on --cs-surface-sunken */
}

/* The inline confirmation — Contact and Add Empty use it; the RFQ redirects to /thank-you/ and
   renders .cs-quote-confirmation below instead. Hard-coded #e0ffc7 with #333333 text: legible in
   itself, but a light-green card dropped into a dark page. */
div.wpforms-container-full .wpforms-confirmation-container-full,
div.wpforms-container-full .wpforms-confirmation-container-full p {
	border-color: var(--cs-success-text);
	background: var(--cs-success-tint);
	color: var(--cs-success-text);
}

/* =============================================================================
   .cs-quote-confirmation — the Thank You reference block
   ============================================================================= */

.cs-quote-confirmation {
	padding: var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-top: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-sunken);
}

.cs-quote-confirmation__label {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

/* Mono and tabular so the reference is readable character by character — it is
   copied into emails and read aloud down a phone. */
.cs-quote-confirmation__reference {
	margin: var(--cs-space-3xs) 0 var(--cs-space-md);
	color: var(--cs-text-strong);
	font-family: var(--cs-font-mono);
	font-size: var(--cs-text-2xl);
	font-variant-numeric: tabular-nums;
}

.cs-quote-confirmation__fallback {
	max-width: 68ch;
	margin: 0 0 var(--cs-space-md);
}

.cs-quote-confirmation__history {
	margin: 0;
}

/* =============================================================================
   .cs-share — share control on leg cards and the leg summary

   Rendered `hidden` by PHP and revealed by assets/share.js, so a browser with no
   JavaScript shows nothing rather than a button whose two mechanisms
   (navigator.share, navigator.clipboard) are both script-only.
   ============================================================================= */

.cs-share {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	/* Icon-only, so it is square and sized by the tap target rather than by its
	   text — 44px is the floor, not the aspiration (§6 QA note). */
	min-height: var(--cs-tap-target);
	min-width: var(--cs-tap-target);
	margin-left: var(--cs-space-2xs);
	padding: 0;
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-sm);
	background: transparent;
	color: var(--cs-text-muted);
	cursor: pointer;
	transition: color var(--cs-transition), border-color var(--cs-transition);
}

/* currentColor on the SVG, so the glyph follows the button's own hover and focus
   colours instead of needing its own rules for each state. */
.cs-share__icon {
	display: block;
	flex: 0 0 auto;
}

.cs-share[hidden] {
	display: none;
}

.cs-share:hover,
.cs-share:focus-visible {
	border-color: var(--cs-brand-400);
	color: var(--cs-text-strong);
}

.cs-share:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

/* The confirmation is a live region, so it must stay in the accessibility tree —
   `display: none` would stop it being announced. Empty until the script fills
   it, so it occupies nothing until it says something. */
.cs-share__status {
	margin-left: var(--cs-space-2xs);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

/* =============================================================================
   .cs-save-leg — save-a-leg star toggle on leg cards and Leg Detail (prompts/Feature-Release-01
   item 3). Same chrome as .cs-share directly above (44px tap target, transparent, bordered) so the
   two read as one family of icon buttons rather than two different control styles on the same card.

   Rendered `hidden` by PHP and revealed by assets/save-leg.js — same reasoning as .cs-share: the
   toggle has no server-rendered fallback to do anything on click, so a browser that never runs the
   script shows no star rather than a dead one.
   ============================================================================= */

.cs-save-leg {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--cs-space-3xs);
	min-height: var(--cs-tap-target);
	min-width: var(--cs-tap-target);
	margin-left: var(--cs-space-2xs);
	padding: 0 var(--cs-space-2xs);
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-sm);
	background: transparent;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	cursor: pointer;
	transition: color var(--cs-transition), border-color var(--cs-transition);
}

/* Icon-only on the card: no gap/padding to spare beyond the icon's own box. */
.cs-save-leg:not(.cs-save-leg--detail) {
	padding: 0;
}

.cs-save-leg__icon {
	display: block;
	flex: 0 0 auto;
}

.cs-save-leg[hidden] {
	display: none;
}

.cs-save-leg:hover,
.cs-save-leg:focus-visible {
	border-color: var(--cs-brand-400);
	color: var(--cs-text-strong);
}

.cs-save-leg:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

/* Saved state fills the star and keeps that emphasis outside hover/focus — .cs-text-strong rather
   than a brand hue, so it reuses a contrast pairing already verified for this exact button chrome
   (the :hover/:focus-visible rules above) instead of introducing a new one. */
.cs-save-leg[aria-pressed="true"] {
	color: var(--cs-text-strong);
}

.cs-save-leg[aria-pressed="true"] .cs-save-leg__icon path {
	fill: currentColor;
}

.cs-save-leg[disabled] {
	cursor: default;
	opacity: 0.7;
}

/* =============================================================================
   TRANSPORT REQUEST  (docs/42 — [cs_transport_request])
   ============================================================================= */

/* Panel chrome added 2026-08-24 (visual-engineer-qa/qa-2, docs/44 §44.9.7) -- reuses
   .cs-quote-detail__section's own surface/border/radius/padding values (docs/38 §38.6) rather than
   inventing a second "detail card" look, so an add-on screen now sits on the same raised-panel
   register as the rest of the quote detail experience instead of floating as a bare, unstyled flex
   column directly on the page canvas -- the single biggest contributor to this flow reading as a
   bolted-on sub-app rather than a native part of booking. */
.cs-transport-request {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-md);
	/* Single-column form content, not a facts/grid page -- reads badly stretched past this, unlike
	   .cs-leg-detail's wider 720px cap for prose+specs. */
	max-width: 30rem;
	margin: var(--cs-space-md) 0;
	padding: var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-panel);
}

.cs-transport-request__eyebrow {
	margin: 0;
}

.cs-transport-request h2 {
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-xl);
	font-weight: 700;
	line-height: var(--cs-leading-tight);
}

.cs-transport-request__ref {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-transport-request__change-mode {
	margin: 0;
	font-size: var(--cs-text-sm);
}

.cs-transport-request__change-mode a {
	display: inline-flex;
	align-items: center;
	min-height: var(--cs-tap-target);
	color: var(--cs-text-strong);
}

/* Result/failure notices -- same border-left + tint pattern .cs-quote-queue__notice already uses,
   extended with a --success modifier that pattern doesn't have yet (docs/38 §38.7's PRG rule: a
   page-level summary notice; the border-left colour is reinforced by position directly under the
   step header, never relied on alone). */
/* --cs-surface-sunken, not --cs-surface-panel (2026-08-24 re-measure) -- the enclosing
   .cs-transport-request panel is itself --cs-surface-panel now, and this notice nesting on the exact
   same surface left only the 3px border-left carrying its boundary. Sunken steps down one tier, same
   "recession, not a second ring" reasoning .cs-quote-detail__action's own docblock already gives. */
.cs-transport-request__notice {
	margin: 0;
	padding: var(--cs-space-xs) var(--cs-space-sm);
	border-left: 3px solid var(--cs-border-strong);
	background: var(--cs-surface-sunken);
	color: var(--cs-text);
	font-size: var(--cs-text-sm);
}

.cs-transport-request__notice--error   { border-left-color: var(--cs-danger-text); }
.cs-transport-request__notice--info    { border-left-color: var(--cs-info-text); }
.cs-transport-request__notice--success { border-left-color: var(--cs-success-text); }

/* Every form in this flow is its own flex column so field/button spacing follows the same gap
   rhythm as the wrapper, rather than relying on default block margins. Sunken/bordered/padded
   2026-08-24, same treatment as .cs-quote-detail__action -- each form now reads as its own enclosed
   action panel one step recessed from the surrounding .cs-transport-request panel, rather than form
   fields sitting directly on the panel with no boundary of their own. */
.cs-transport-request form {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-sm);
	padding: var(--cs-space-sm);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-sunken);
}

/* Screen 1's radio group */
.cs-transport-request fieldset {
	margin: 0;
	padding: 0;
	border: 0;
}

.cs-transport-request legend {
	margin: 0 0 var(--cs-space-2xs);
	padding: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-md);
	font-weight: 700;
}

.cs-transport-request__radio {
	display: flex;
	align-items: center;
	gap: var(--cs-space-2xs);
	min-height: var(--cs-tap-target);
	color: var(--cs-text);
	cursor: pointer;
}

.cs-transport-request__radio input {
	width: 1.125rem;
	height: 1.125rem;
	flex: 0 0 auto;
	accent-color: var(--cs-brand-500);
}

/* Screen 2/4 fields (address inputs, the air-cargo note textarea) */
.cs-transport-request__field {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-3xs);
}

.cs-transport-request__field label {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

.cs-transport-request__field input,
.cs-transport-request__field textarea {
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-2xs) var(--cs-space-xs);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
	color: var(--cs-text-strong);
	font-family: inherit;
	font-size: var(--cs-text-base);
}

.cs-transport-request__field textarea {
	min-height: 6rem;
	resize: vertical;
}

.cs-transport-request__field input:focus-visible,
.cs-transport-request__field textarea:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
	border-color: var(--cs-brand-400);
}

.cs-transport-request__prefill-caption {
	margin: calc(-1 * var(--cs-space-2xs)) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

/* Screen 3's quote card -- the same left-accent-on-surface register as .cs-launch-site__fact,
   reused rather than inventing a third bordered-box style next to that one and
   .cs-quote-queue__notice's border-left. */
.cs-transport-request__quote-card {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-3xs);
	padding: var(--cs-space-sm);
	border: 1px solid var(--cs-border-surface);
	border-left: 3px solid var(--cs-brand-400);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
}

/* Left-accent reflects real admin-price state (docs/44 §44.9.2, qa-2 finding 2026-08-24) rather than
   staying a fixed --cs-brand-400 regardless of whether a price is an estimate, pending broker
   confirmation, or a real confirmed figure -- same tokens as .cs-badge--price-status-* below, so the
   card border and its own badge never disagree about what state this price is in. */
.cs-transport-request__quote-card--estimate  { border-left-color: var(--cs-info-text); }
.cs-transport-request__quote-card--pending   { border-left-color: var(--cs-warning-text); }
.cs-transport-request__quote-card--confirmed { border-left-color: var(--cs-success-text); }

.cs-transport-request__carrier {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-transport-request__price {
	margin: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-2xl);
	font-weight: 700;
	line-height: var(--cs-leading-tight);
}

/* Pairs the price with its .cs-badge--price-status-* badge (transport_price_line(), docs/44 §44.9.2,
   2026-08-24) -- wraps at narrow widths rather than truncating, same reasoning
   .cs-transport-request__heading-row already documents for its own title+badge pair. */
.cs-transport-request__price-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-2xs);
	margin: 0;
}

.cs-transport-request__price-row .cs-transport-request__price {
	margin: 0;
}

.cs-transport-request__transit {
	margin: 0;
	color: var(--cs-text);
	font-size: var(--cs-text-sm);
}

.cs-transport-request__expiry {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-transport-request__disclaimer {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

/* Groups insurance's two disclaimer paragraphs (Warp's own framing + the facilitation copy, both
   text unchanged, docs/44 §44.9.7) into one info-tinted card instead of two disconnected muted
   paragraphs -- same border-left + tint register .cs-transport-request__notice--info already uses,
   so "important context, read this" carries a consistent visual signal across the whole flow. */
.cs-transport-request__facilitation {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-xs);
	padding: var(--cs-space-sm);
	border-left: 3px solid var(--cs-info-text);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-sunken);
}

.cs-transport-request__facilitation .cs-transport-request__disclaimer {
	color: var(--cs-text);
}

.cs-transport-request__facilitation .cs-transport-request__disclaimer a {
	color: var(--cs-link);
}

/* h2 + status badge, side by side (visual-engineer-qa/qa-2, 2026-08-24, docs/44 §44.9.7) -- wraps
   rather than truncating at narrow widths, since both the title and the badge are short enough that
   a two-line stack still scans in order. */
.cs-transport-request__heading-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-2xs);
	margin: 0;
}

.cs-transport-request__heading-row h2 {
	margin: 0;
}

/* The receiving/return/insurance CTA cluster, grouped under one heading instead of three unrelated
   stacked buttons (2026-08-24) -- reads as "other things you can add to this shipment", the same
   kind of enclosed grouping .cs-quote-detail__action uses for related controls. */
.cs-transport-request__related {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-2xs);
	padding-top: var(--cs-space-xs);
	border-top: 1px solid var(--cs-border);
}

.cs-transport-request__related h3 {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-transport-request__related p {
	margin: 0;
}

/* Every button in this flow is full-width: the column is capped at 30rem already (a "wide" button
   at this width is not the same problem a full-page-width button would be), and it keeps the
   primary/secondary pair from ever being read as two options competing in one row -- the same
   "sole submit of its own enclosed form" reasoning docs/38 §38.6 applies to accept/reject. The
   attach form and the secondary "Try again"/"Try different pallets" link are already separate
   block-level elements (a <form>, then a <p>), so they stack at every width with no row layout to
   collapse at a breakpoint. */
.cs-transport-request .cs-button {
	width: 100%;
}

/* Add-on identity eyebrow badges (transport_addon_badge(), 2026-08-24, docs/44 §44.9.7) -- a "type"
   facet, not a severity/status one, so this follows .cs-badge--legtype's own precedent (style.css,
   PRIMITIVE — .cs-badge) rather than the status ramp: one consistent info tint across all three, with
   .cs-badge__icon's shape carrying the distinction between them, same "icon is redundant with the
   label, never a replacement for it" rule every badge in this codebase already follows. */
.cs-badge--addon {
	--cs-badge-fg: var(--cs-info-text);
	--cs-badge-bg: var(--cs-info-tint);
	background: var(--cs-badge-bg);
	color: var(--cs-badge-fg);
}

.cs-badge--addon-ground .cs-badge__icon    { border-radius: 50%; }
.cs-badge--addon-air .cs-badge__icon       { border-radius: 1px; }
.cs-badge--addon-insurance .cs-badge__icon { border-radius: 1px; transform: rotate(45deg); }

/* "Estimate" vs "Attached to request" (transport_quote_card_heading(), 2026-08-24) -- estimate
   reuses on_hold's hollow-circle shape (a state that hasn't landed yet), attached reuses published's
   filled circle (the completed/current state), same shape-sharing rationale
   .cs-badge--status-retiring/-retired already document above. */
.cs-badge--status-quoted        { --cs-badge-fg: var(--cs-info-text);    --cs-badge-bg: var(--cs-info-tint); }
.cs-badge--status-quoted .cs-badge__icon   { border-radius: 50%; background: transparent; }
.cs-badge--status-attached      { --cs-badge-fg: var(--cs-success-text); --cs-badge-bg: var(--cs-success-tint); }
.cs-badge--status-attached .cs-badge__icon { border-radius: 50%; }

/* Air cargo's "not yet available" state (2026-08-24) -- same warning tint .cs-badge--theoretical
   already uses for "must not miss, still provisional" content, dashed hollow circle borrowed from
   draft's own shape (an unstarted/future state, the same fact this badge is communicating). Replaces
   the bespoke .cs-transport-request__badge pill this used to be, per docs/38 §38.6's own instruction
   to reuse the shared .cs-badge primitive rather than a parallel one-off. */
.cs-badge--status-soon          { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
.cs-badge--status-soon .cs-badge__icon     { border-radius: 50%; border-style: dashed; background: transparent; }

/* transport_price_status_badge() (docs/44 §44.9.2/§44.9.10, 2026-08-24) -- a deliberately distinct
   modifier namespace from .cs-badge--status-attached/-quoted above (coordinated with
   visual-engineer-qa): that family answers "is a Warp quote on the record", this one answers "has an
   admin set a real customer-facing price". Shared across ground/air/insurance, closed 3-value set --
   ground/air only ever render -estimate or -confirmed (neither has an async step, §44.9.2), insurance
   is the one add-on that also reaches -pending. Icon shapes intentionally differ from every other
   badge family already using circle/square so this one reads as its own axis at a glance: hollow
   dashed circle (nothing priced yet, matches -soon/-quoted's own "not real yet" language), solid
   circle (an in-progress human step, matches -running's "active" language), filled square (settled/
   final, matches -attached/-published's "done" language). */
.cs-badge--price-status-estimate  { --cs-badge-fg: var(--cs-info-text);    --cs-badge-bg: var(--cs-info-tint); }
.cs-badge--price-status-pending   { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
.cs-badge--price-status-confirmed { --cs-badge-fg: var(--cs-success-text); --cs-badge-bg: var(--cs-success-tint); }

.cs-badge--price-status-estimate .cs-badge__icon  { border-radius: 50%; border-style: dashed; background: transparent; }
.cs-badge--price-status-pending .cs-badge__icon   { border-radius: 50%; }
.cs-badge--price-status-confirmed .cs-badge__icon { border-radius: 1px; }

/* =============================================================================
   PROVIDER QUOTE QUEUE  (docs/36 — [cs_provider_quote_queue])
   ============================================================================= */

.cs-quote-queue {
	margin: var(--cs-space-md) 0;
}

.cs-quote-queue__notice {
	margin: 0 0 var(--cs-space-sm);
	padding: var(--cs-space-xs) var(--cs-space-sm);
	border-left: 3px solid var(--cs-border-strong);
	background: var(--cs-surface-panel);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-quote-queue__notice--error { border-left-color: var(--cs-danger-text); }
.cs-quote-queue__notice--info  { border-left-color: var(--cs-info-text); }

/* Filter tabs. Real links in every state, so they stay usable without JS and
   keyboard-reachable in order; `aria-current` carries the active state and the
   underline is only its visual echo (docs/06 QA2 — never colour alone). */
/* !important + a reinforced <li> rule: Divi's own dynamic CSS matches this <ul> via #left-area ul
   (specificity 1,0,1, an ID beats any class-only selector regardless of source order) and puts the
   bullet back. Third occurrence of this exact bleed-through this session (console nav, footer nav,
   now this) -- same fix each time.

   `padding` carries !important for the same reason and needs it on the shorthand, not just the left:
   the declaration being overridden is `padding: 0 0 23px 1em`, so an unqualified `padding: 0` loses
   all four sides at once. Measured on prod before this: padding-left 14px at 1440 and 16px at 390 --
   it is 1em, so it resolves against the ul's own font-size and the misalignment is worse on mobile,
   not better -- which put the tab row 14px right of .cs-quote-queue__scope, __summary and __table,
   every one of which sits flush. The bottom 23px is the half that reads as a bug rather than as an
   indent: it pushes this ul's border-bottom away from the tabs, so the active tab's underline and
   the row rule end up separated by a band of dead space (68px of ul around a 44px tab row). */
.cs-quote-queue__views ul {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-3xs);
	margin: 0 0 var(--cs-space-sm);
	padding: 0 !important;
	list-style: none !important;
	border-bottom: 1px solid var(--cs-border);
}

.cs-quote-queue__views li {
	margin: 0;
	list-style: none !important;
	list-style-type: none !important;
	list-style-image: none !important;
}

.cs-quote-queue__views a {
	display: flex;
	align-items: center;
	/* 44px minimum tap target (docs/06 QA2). The padding, not a fixed height, so
	   a wrapped label grows the target rather than clipping it. */
	min-height: 44px;
	padding: var(--cs-space-2xs) var(--cs-space-sm);
	border-bottom: 2px solid transparent;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	font-weight: 600;
	text-decoration: none;
}

.cs-quote-queue__views a[aria-current="page"] {
	border-bottom-color: var(--cs-brand-500);
	color: var(--cs-text);
}

/* assets/quote-queue-tabs.js's own status line -- ships empty (0 height), and only ever gets text
   while an AJAX tab fetch is in flight. Visible, not sr-only, matching .cs-rfq-save-draft__status'
   own reasoning: a slow tab switch should say so for a sighted user too, not just announce it. */
.cs-quote-queue__tab-status {
	margin: 0 0 var(--cs-space-sm);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

/* The AJAX-swapped region (provider-quote-queue.php's render_queue_results_section()). Dimmed and
   inert only while assets/quote-queue-tabs.js has a fetch in flight -- a no-JS page load never sets
   aria-busy at all, so this rule has nothing to match on that path. The transition (qa-2 finding,
   2026-08-20) is what keeps a fast round trip from reading as a flicker rather than a loading cue. */
.cs-quote-queue__results {
	transition: opacity var(--cs-transition);
}

.cs-quote-queue__results[aria-busy="true"] {
	opacity: 0.5;
	pointer-events: none;
}

.cs-quote-queue__summary {
	margin: 0 0 var(--cs-space-sm);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-quote-queue__table {
	width: 100%;
	border-collapse: collapse;
	font-size: var(--cs-text-sm);
}

.cs-quote-queue__table th,
.cs-quote-queue__table td {
	padding: var(--cs-space-2xs) var(--cs-space-xs);
	border-bottom: 1px solid var(--cs-border);
	text-align: left;
	vertical-align: top;
}

/* Status sits immediately after Cargo flags with no gap between the cells (measured 0px), and both
   columns can carry the same warning-tinted badge -- amber pills read as one group regardless of
   the gap between them, verified by simulating a dual-use + safety flag row: three identical amber
   badges in a run were not separable without tracing the column header. Enclosure (a border) is a
   different channel from proximity and settles it where more whitespace could not -- below Divi's
   Phone breakpoint this is moot anyway, since data-label prefixes every cell with its column name.
   Unreachable in current data (no quote_request today declares a flag), so this is a legibility fix
   verified on its own merits, not yet confirmed against a real amber-on-amber row. */
.cs-quote-queue__table td[data-label="Status"] {
	border-left: 1px solid var(--cs-border-strong);
}

.cs-quote-queue__table thead th {
	border-bottom-color: var(--cs-border-strong);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

/* References and figures are compared down the column, so they need tabular
   alignment for the same reason the leg spec tables do. */
.cs-quote-queue__ref {
	font-family: var(--cs-font-mono);
	font-weight: 600;
	white-space: nowrap;
}

.cs-quote-queue__leg { font-weight: 600; }

.cs-quote-queue__leg-meta,
.cs-quote-queue__pressure,
.cs-quote-queue__waiting-abs {
	display: block;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

/* Oversubscription (docs/29 §29.4.3). Warning-tinted because it is a state the
   provider should act on, but paired with its full sentence — never a bare
   colour change on a number. */
.cs-quote-queue__pressure { color: var(--cs-warning-text); }

.cs-quote-queue__orphan {
	color: var(--cs-text-muted);
	font-style: italic;
}

.cs-quote-queue__waiting { font-weight: 600; }

.cs-quote-queue__flags {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-3xs);
}

.cs-quote-queue__flags--none {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
}

.cs-badge--flag-itar      { --cs-badge-fg: var(--cs-danger-text);  --cs-badge-bg: var(--cs-danger-tint); }
.cs-badge--flag-dual-use  { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
.cs-badge--flag-safety-declared { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
/* BF-19 — an unasked questionnaire is an open question, not a clearance, so it carries the same
   warning weight as a declared flag rather than the muted treatment of 'None declared'. */
.cs-badge--flag-not-asked { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }

.cs-badge--qstatus-submitted         { --cs-badge-fg: var(--cs-info-text);    --cs-badge-bg: var(--cs-info-tint); }
.cs-badge--qstatus-provider_notified { --cs-badge-fg: var(--cs-info-text);    --cs-badge-bg: var(--cs-info-tint); }
.cs-badge--qstatus-under_review      { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
.cs-badge--qstatus-quoted            { --cs-badge-fg: var(--cs-brand-300);    --cs-badge-bg: var(--cs-surface-panel); }
.cs-badge--qstatus-converted_to_booking { --cs-badge-fg: var(--cs-success-text); --cs-badge-bg: var(--cs-success-tint); }
.cs-badge--qstatus-declined          { --cs-badge-fg: var(--cs-text-muted);  --cs-badge-bg: var(--cs-surface-panel); }
.cs-badge--qstatus-cancelled         { --cs-badge-fg: var(--cs-text-muted);  --cs-badge-bg: var(--cs-surface-panel); }
.cs-badge--qstatus-expired           { --cs-badge-fg: var(--cs-text-muted);  --cs-badge-bg: var(--cs-surface-panel); }

/* .cs-status-tracker__step--qstatus-* — the terminal tracker step's colour (docs/40 §40.5,
   2026-08-23 redesign), applied only alongside .is-current on the tracker's own last step. The
   same status→tone mapping as .cs-badge--qstatus-* directly above, on the same tokens, so a
   cancelled/expired/booked quote reads the same tone here as it does in the queue list — but
   expressed as `color` rather than the badge's `--cs-badge-fg`/`--cs-badge-bg` pair, since a tracker
   step has no pill background to fill, only the icon (via currentColor, see .is-current .icon below)
   and label text. Three classes chained ((0,3,0) specificity) so this wins over the plain .is-current
   colour rule ((0,2,0)) below without !important. `--qstatus-declined` deliberately dropped from the
   `.is-current` pairing here (CR-03, 2026-08-25): `declined`'s terminal slot is never `.is-current`
   any more (quote_tracker_view() now leaves it `.is-upcoming`), so this combination can no longer
   occur for it — its tone now lives on `.cs-status-tracker__branch-label` below instead, on the
   branch node quote_tracker_view() returns for it. */
.cs-status-tracker__step.is-current.cs-status-tracker__step--qstatus-converted_to_booking { color: var(--cs-success-text); }
.cs-status-tracker__step.is-current.cs-status-tracker__step--qstatus-cancelled,
.cs-status-tracker__step.is-current.cs-status-tracker__step--qstatus-expired { color: var(--cs-text-muted); }

.cs-quote-queue__empty {
	padding: var(--cs-space-lg) var(--cs-space-sm);
	border: 1px dashed var(--cs-border-strong);
	text-align: center;
}

.cs-quote-queue__empty h4 { margin: 0 0 var(--cs-space-2xs); }

/* Below Divi's Phone breakpoint the table becomes stacked cards rather than a
   horizontal scroller (docs/06 QA2). Each cell keeps its column name via the
   `data-label` the renderer emits, so the reading order still matches the
   header order it replaced. */
@media (max-width: 767px) {
	.cs-quote-queue__table thead {
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip: rect(0 0 0 0);
		clip-path: inset(50%);
		white-space: nowrap;
	}

	.cs-quote-queue__table,
	.cs-quote-queue__table tbody,
	.cs-quote-queue__table tr,
	.cs-quote-queue__table th,
	.cs-quote-queue__table td {
		display: block;
		width: 100%;
	}

	.cs-quote-queue__table tr {
		margin: 0 0 var(--cs-space-sm);
		padding: var(--cs-space-xs);
		border: 1px solid var(--cs-border-strong);
	}

	.cs-quote-queue__table td,
	.cs-quote-queue__table th {
		border-bottom: 0;
		padding: var(--cs-space-3xs) 0;
	}

	.cs-quote-queue__table td::before {
		content: attr(data-label) ": ";
		color: var(--cs-text-muted);
		font-size: var(--cs-text-xs);
		text-transform: uppercase;
	}
}

/* =============================================================================
   .cs-quote-detail — the Provider Portal's one-quote-request detail view
   (provider-quote-queue.php's render_quote_detail() and everything it calls).
   Shares .cs-quote-queue's base rules (it carries both classes) — this block
   only adds what the detail-specific markup needs.
   ============================================================================= */

.cs-quote-detail__back {
	margin: 0 0 var(--cs-space-sm);
}

.cs-quote-detail__title {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--cs-space-xs);
	margin: 0 0 var(--cs-space-md);
	font-size: var(--cs-text-lg);
}

.cs-quote-detail__admin-link {
	margin: 0 0 var(--cs-space-sm);
	font-size: var(--cs-text-sm);
}

/* .cs-status-tracker — the per-record status tracker (docs/40 §40.5), rendered by
   render_status_tracker() at the top of render_quote_detail(). Vertical rail below 768px
   (`md`, style.css:120), horizontal row at 768px and up (docs/40 §40.5's "vertical-always" was
   reopened and superseded 2026-08-24, prompts/Change-Request-01.md — see the dated note in
   docs/40 §40.5/§40.5.1). The label that killed the first horizontal attempt, docs/39:240's
   "Accepted — awaiting payment," is handled by letting each step wrap its label onto 2 lines
   inside a flexible (never fixed-px) share of the row, not by truncating it or capping the
   column width — WPML (docs/11) translation-length headroom survives because the width is still
   content-driven, just distributed across a row instead of stacked. `--cs-*` tokens only, so
   Console's `lcars.css` remap (docs/38 §38.1) applies automatically with no separate admin
   version — the same component renders for the Provider and platform-admin personas, since both
   reach it through render_quote_detail() (docs/40 §40.6). box-sizing is scoped to border-box
   here (matching the file's existing `.cs-leg-card *`-style precedent) because the horizontal
   connector line below is positioned with percentages measured against each step's own
   border-box width — content-box padding would throw that math off. */
.cs-status-tracker,
.cs-status-tracker * {
	box-sizing: border-box;
}

.cs-status-tracker {
	list-style: none;
	margin: 0 0 var(--cs-space-md);
	padding: 0;
}

.cs-status-tracker__step {
	position: relative;
	display: flex;
	align-items: baseline;
	gap: var(--cs-space-xs);
	padding: 0 0 var(--cs-space-md) var(--cs-space-lg);
}

.cs-status-tracker__step:last-child {
	padding-bottom: 0;
}

/* The icon is a CSS-drawn filled/hollow circle, the same "verified distinct in greyscale"
   convention .cs-badge--status already uses (this file, §38.6) — filled for is-complete/
   is-current, hollow (border only) for is-upcoming. It is redundant with the text label and the
   screen-reader prefix render_status_tracker() emits, never the only signal (docs/06 QA2). */
.cs-status-tracker__icon {
	position: absolute;
	left: 0;
	top: 0.2em;
	width: 0.875rem;
	height: 0.875rem;
	border-radius: 50%;
	border: 2px solid var(--cs-text-muted);
	background: transparent;
}

/* The connector line: drawn on the icon's own ::after rather than a separate element, so its
   length is a fixed value inside a fixed-height row instead of something that has to be measured
   against a sibling's rendered height. */
.cs-status-tracker__step:not(:last-child) .cs-status-tracker__icon::after {
	content: "";
	position: absolute;
	top: 100%;
	left: calc(0.4375rem - 1px);
	width: 2px;
	height: calc(var(--cs-space-md) + 0.6em);
	background: var(--cs-border);
}

.cs-status-tracker__step.is-complete .cs-status-tracker__icon,
.cs-status-tracker__step.is-current .cs-status-tracker__icon {
	background: currentColor;
	border-color: currentColor;
}

.cs-status-tracker__step.is-complete:not(:last-child) .cs-status-tracker__icon::after {
	background: var(--cs-success-text);
}

.cs-status-tracker__step.is-complete { color: var(--cs-success-text); }
.cs-status-tracker__step.is-current  { color: var(--cs-brand-400); }
.cs-status-tracker__step.is-upcoming { color: var(--cs-text-muted); }
.cs-status-tracker__step.is-skipped  { color: var(--cs-text-muted); }

/* is-skipped (2026-08-23 redesign, docs/40 §40.5): a position the tracker cannot vouch for — may
   or may not have happened (quote_tracker_view()'s own docblock explains why one state covers both
   a genuine unknown and a structurally-impossible-but-moot position). Dashed icon border and an
   italic label are both new, non-colour signals distinguishing it from is-upcoming's plain hollow
   circle and upright label — docs/06 QA2, colour/icon is never the only signal, and this state in
   particular must never read as identical to a plain "hasn't happened yet" future step. */
.cs-status-tracker__step.is-skipped .cs-status-tracker__icon {
	border-style: dashed;
}

.cs-status-tracker__step.is-skipped .cs-status-tracker__label {
	font-style: italic;
}

.cs-status-tracker__label {
	color: var(--cs-text);
	font-size: var(--cs-text-sm);
}

.cs-status-tracker__step.is-current .cs-status-tracker__label {
	font-weight: 700;
}

.cs-status-tracker__step.is-upcoming .cs-status-tracker__label,
.cs-status-tracker__step.is-skipped .cs-status-tracker__label {
	color: var(--cs-text-muted);
}

/* =============================================================================
   .cs-status-tracker__branch — the declined-record branch node (Change-Request-03, 2026-08-25).
   Nests inside its anchor step's own `<li>` (render_status_tracker(), the step whose slug matches
   quote_tracker_view()'s `branch.anchor`) rather than sitting in the `<ol>` as a sibling, so it never
   needs its own coordinate system — every rule below positions it relative to that one `<li>`
   (already `position: relative`), mobile and desktop each solved for their own geometry rather than
   one flipped into the other (mockup: docs/visuals/status-tracker-declined-branch-2026-08-25).

   Vertical rail (mobile, default/no-media): the anchor `<li>` wraps onto a second row for the branch
   (flex-wrap + flex-basis:100% on the branch itself) rather than the branch being absolutely
   positioned, so it always reserves real space instead of risking an overlap with whatever the next
   trunk step's own content height turns out to be. The elbow connector is a small bordered box
   (border-left + border-bottom + a rounded corner) reaching from the trunk's own icon column down and
   right into the branch icon — dashed and never the trunk's --cs-border/--cs-success-text tones, so it
   reads as "a different kind of line" at a glance, not a continuation of the trunk's own descending
   connector (which keeps running the whole sequence, unbroken, past the branch point — see the
   --has-branch connector-height override below, the fix for the specific gap qa-2 caught: the base
   connector height above is tuned for a single-row step and falls short once this step's own
   padding-bottom grows for the branch row). */
.cs-status-tracker__step--has-branch {
	flex-wrap: wrap;
	padding-bottom: calc(var(--cs-space-md) + var(--cs-space-lg));
}

.cs-status-tracker__step--has-branch:not(:last-child) .cs-status-tracker__icon::after {
	height: calc(var(--cs-space-md) + 0.6em + var(--cs-space-lg) + var(--cs-space-sm) + 1.3rem);
}

.cs-status-tracker__branch {
	flex: 0 0 100%;
	display: flex;
	align-items: baseline;
	gap: var(--cs-space-xs);
	margin: var(--cs-space-sm) 0 0 0;
}

.cs-status-tracker__branch-connector {
	position: absolute;
	top: 1rem;
	left: calc(0.4375rem - 1px);
	width: 2.95rem;
	height: 1.85rem;
	border-left: 2px dashed var(--cs-text-muted);
	border-bottom: 2px dashed var(--cs-text-muted);
	border-bottom-left-radius: 10px;
}

/* Diamond, not a circle — the trunk's own vocabulary (filled/hollow/dashed circle) is reserved for
   trunk positions; the branch needs a shape difference too, not just a label, so a legend-less render
   still reads "this is a different kind of node" (docs/06 QA2: colour/icon never the only signal).
   Filled, since this is a reached, settled outcome, never hollow (hollow means "hasn't happened"). */
.cs-status-tracker__branch-icon {
	flex: none;
	width: 0.75rem;
	height: 0.75rem;
	margin-top: 0.25em;
	border: 2px solid var(--cs-text-muted);
	background: var(--cs-text-muted);
	transform: rotate(45deg);
}

/* Bold + the diamond shape + the dashed connector are the differentiators — deliberately never
   italic (qa-2 should-fix, 2026-08-25 review): italic is already this component's claimed signal for
   is-skipped ("cannot vouch for, may or may not have happened"), and Quoted/Under review/Pending
   decline all sit right next to the branch using exactly that style. Declined is not ambiguous, it is
   the record's confirmed actual status, so it must not borrow is-skipped's typeface. --cs-text-muted
   matches the already-shipped .cs-badge--qstatus-declined tone deliberately (qa-2 confirmed, same
   review) — declined is documented elsewhere as a neutral, not alarming, outcome. */
.cs-status-tracker__branch-label {
	font-size: var(--cs-text-sm);
	font-weight: 700;
	color: var(--cs-text-muted);
}

/* .cs-status-tracker__note (pending_decline's non-terminal side-state overlay) and
   .cs-status-tracker__info (the `quoted` dead-end's neutral explanation) are separate classes on
   purpose, not one block styled by a modifier flag: one means "this needs attention", the other
   means "here's what the current step means" — collapsing them would put a warning tone on a
   status that isn't actually a problem. Text-labelled prefix, never colour alone (docs/06 QA2) —
   the convention console.php's "Attention:" prefix already established, reused as a new,
   portable class rather than that file's Console-specific <dl> markup (docs/40 §40.5.1). */
.cs-status-tracker__note,
.cs-status-tracker__info {
	margin: 0 0 var(--cs-space-sm);
	padding: var(--cs-space-xs) var(--cs-space-sm);
	border-radius: var(--cs-radius-md);
	font-size: var(--cs-text-sm);
}

.cs-status-tracker__note {
	border-left: 3px solid var(--cs-warning-text);
	background: var(--cs-warning-tint);
	color: var(--cs-warning-text);
}

.cs-status-tracker__note-prefix {
	font-weight: 700;
}

.cs-status-tracker__info {
	border-left: 3px solid var(--cs-border-strong);
	background: var(--cs-surface-panel);
	color: var(--cs-text);
}

/* Horizontal row, 768px and up (docs/40 §40.5's 2026-08-24 update) — a real redesign of the rail,
   not a flex-direction flip: the icon moves from beside the label to above it (each step becomes
   its own column: icon, then label, centred), and the connector moves off the icon's own ::after
   (which only ever had to reach *down* to the next row) onto the `<li>` itself, because it now has
   to reach *sideways* to an icon centred inside a sibling of unpredictable, content-driven width.
   `flex: 1 1 0` + `min-width: 0` gives every step an equal, flexible share of the row (never a
   fixed-px column — WPML headroom, same reasoning as the vertical rail) and is also what makes the
   connector math below exact: with every step the same width, "the next icon" is always exactly
   one step-width away, so a line from 50% to -50% (this icon's centre to the next one's) lines up
   regardless of how many steps there are or how long any one label runs. */
@media (min-width: 768px) {
	.cs-status-tracker {
		display: flex;
		align-items: flex-start;
	}

	.cs-status-tracker__step {
		flex: 1 1 0;
		min-width: 0;
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 0;
		padding: 0 var(--cs-space-2xs);
		text-align: center;
	}

	.cs-status-tracker__step:last-child {
		padding-bottom: 0;
	}

	.cs-status-tracker__icon {
		position: static;
		margin: 0 0 var(--cs-space-3xs);
	}

	/* The vertical rail's downward connector no longer applies — replaced by the sideways one
	   below, scoped to the `<li>` so its width can be measured in percentages of a known,
	   equal-width box instead of the icon's own fixed 0.875rem. */
	.cs-status-tracker__step:not(:last-child) .cs-status-tracker__icon::after {
		content: none;
	}

	.cs-status-tracker__step:not(:last-child)::after {
		content: "";
		position: absolute;
		top: 0.4375rem;
		left: calc(50% + 0.4375rem);
		right: calc(-50% + 0.4375rem);
		height: 2px;
		background: var(--cs-border);
		transform: translateY(-1px);
	}

	.cs-status-tracker__step.is-complete:not(:last-child)::after {
		background: var(--cs-success-text);
	}

	.cs-status-tracker__label {
		max-width: 100%;
	}

	/* .cs-status-tracker__branch, horizontal row: solved for this geometry specifically, not the
	   vertical rail's elbow flipped 90° — the branch only ever has the one column to hang off, so it
	   drops straight down, centred under the anchor column, with no sideways math to resolve the way
	   the trunk's own icon-to-icon connector above needs. Absolutely positioned so it never distorts
	   the row's own flex sizing; the anchor step reserves the extra vertical space below the rail via
	   its own taller padding-bottom instead. */
	.cs-status-tracker__step--has-branch {
		padding-bottom: calc(var(--cs-space-xl) * 2.1);
	}

	.cs-status-tracker__branch {
		position: absolute;
		top: calc(var(--cs-space-xl) + 0.2em);
		left: 50%;
		transform: translateX(-50%);
		flex-direction: column;
		align-items: center;
		gap: var(--cs-space-3xs);
		width: max-content;
		max-width: 9rem;
		margin: 0;
	}

	.cs-status-tracker__branch-connector {
		top: -1.1rem;
		left: 50%;
		width: 2px;
		height: 1.1rem;
		border-left: 2px dashed var(--cs-text-muted);
		border-bottom: 0;
		border-radius: 0;
	}

	.cs-status-tracker__branch-icon {
		margin-top: 0;
	}

	.cs-status-tracker__branch-label {
		text-align: center;
	}
}

/* =============================================================================
   .cs-flow-steps — a static, illustrative pure-CSS step-flow diagram for marketing/explainer
   pages (ER-05: How It Works' booking flow, the asset-tracking page's certificate flow).

   Deliberately a new, separate component from .cs-status-tracker above rather than a reuse of it
   -- that component is bound to one specific record's live is-complete/is-current/is-upcoming
   state (docs/40 §40.5); this one has no "current step" concept at all, since it illustrates a
   general process for a page visitor, not one specific request's progress. Same underlying visual
   language on purpose (CSS-drawn circle icons, a connector line, vertical rail on mobile /
   horizontal row from 768px, this file's own real breakpoint) so it reads as part of the same
   design system rather than a third, unrelated visual style.
   ============================================================================= */

.cs-flow-steps,
.cs-flow-steps * {
	box-sizing: border-box;
}

.cs-flow-steps {
	list-style: none;
	margin: var(--cs-space-lg) 0;
	padding: 0;
}

.cs-flow-steps__step {
	position: relative;
	display: flex;
	align-items: flex-start;
	gap: var(--cs-space-sm);
	padding: 0 0 var(--cs-space-lg) var(--cs-space-xl);
}

.cs-flow-steps__step:last-child {
	padding-bottom: 0;
}

.cs-flow-steps__icon {
	position: absolute;
	left: 0;
	top: 0;
	width: 1.75rem;
	height: 1.75rem;
	border-radius: 50%;
	background: var(--cs-brand-500);
	color: var(--cs-on-brand);
	display: flex;
	align-items: center;
	justify-content: center;
	font-weight: 700;
	font-size: var(--cs-text-sm);
	flex-shrink: 0;
}

/* Same "draw the connector on the icon's own ::after" technique as .cs-status-tracker__icon
   above, for the same reason: its length is a fixed value inside a fixed-height row instead of
   something measured against a sibling's rendered height. */
.cs-flow-steps__step:not(:last-child) .cs-flow-steps__icon::after {
	content: "";
	position: absolute;
	top: 100%;
	left: calc(0.875rem - 1px);
	width: 2px;
	height: calc(var(--cs-space-lg) + 0.5rem);
	background: var(--cs-border);
}

.cs-flow-steps__body {
	padding-top: 0.25rem;
}

.cs-flow-steps__label {
	display: block;
	color: var(--cs-text-strong);
	font-weight: 700;
	font-size: var(--cs-text-base);
	margin: 0 0 var(--cs-space-3xs);
}

.cs-flow-steps__desc {
	display: block;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	margin: 0;
}

@media (min-width: 768px) {
	.cs-flow-steps {
		display: flex;
		align-items: flex-start;
	}

	.cs-flow-steps__step {
		flex: 1 1 0;
		min-width: 0;
		flex-direction: column;
		align-items: center;
		gap: var(--cs-space-2xs);
		padding: 0 var(--cs-space-xs);
		text-align: center;
	}

	.cs-flow-steps__icon {
		position: static;
		margin: 0 0 var(--cs-space-2xs);
	}

	.cs-flow-steps__step:not(:last-child) .cs-flow-steps__icon::after {
		content: none;
	}

	/* The vertical rail's downward connector no longer applies here either -- replaced by the
	   sideways one below, same construction as .cs-status-tracker's own horizontal connector. */
	.cs-flow-steps__step:not(:last-child)::after {
		content: "";
		position: absolute;
		top: 0.875rem;
		left: calc(50% + 0.875rem);
		right: calc(-50% + 0.875rem);
		height: 2px;
		background: var(--cs-border);
		transform: translateY(-1px);
	}

	.cs-flow-steps__body {
		padding-top: 0;
	}

	.cs-flow-steps__label,
	.cs-flow-steps__desc {
		max-width: 100%;
	}
}

.cs-quote-detail__section {
	margin: 0 0 var(--cs-space-sm);
	padding: var(--cs-space-sm);
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-panel);
}

/* WCAG 1.4.11: .cs-button's base border-color (--cs-brand-500) documents 4.11:1 against the page
   canvas, but this section's background is --cs-surface-panel, lighter than the canvas -- the pair
   the comment describes stopped being the pair rendered here the moment these panelled sections
   shipped (23b43bd), dropping the measured contrast to 2.92:1. --cs-brand-400 restores it to 4.47:1
   on the panel without touching the fill or the text, which both still meet 1.4.3/1.4.11 on their
   own terms. Found by measuring the rendered backdrop rather than trusting the token comment.
   Repeated on :hover/:focus because .cs-button's own hover rule resets border-color to brand-500
   unconditionally -- without this the fix would hold at rest and fail again on interaction.

   The selector is scoped to .cs-quote-detail__section, not .cs-quote-detail__action, so it also has
   to hold on whichever surface the non-outcome button actually sits on today: the cancel button
   (.cs-button--secondary) renders inside .cs-quote-detail__action, whose own background is
   --cs-surface-sunken, not --cs-surface-panel -- re-measured at 5.79:1 there. --cs-brand-400 is kept
   as the one value rather than split per surface because it already clears 3:1 on both (4.47 / 5.79),
   which is the property worth choosing for going forward: a colour that clears every surface the
   component might land on, not just the one it happens to be on when it is picked.

   The outcome buttons are excluded rather than left to fight this on specificity. This selector is
   0,2,0 and `.cs-button--accept` is 0,1,0, so without the :not() pair a green-filled button would
   render with a brand-blue edge — the rule would silently undo an edge colour that was itself chosen
   against the panel they sit on (`--cs-accept-edge` 5.38:1, `--cs-reject-edge` 4.93:1, both clear
   of 3:1 — see the token block for which surface those are measured against). */
.cs-quote-detail__section .cs-button:not(.cs-button--accept):not(.cs-button--reject),
.cs-quote-detail__section .cs-button:not(.cs-button--accept):not(.cs-button--reject):hover,
.cs-quote-detail__section .cs-button:not(.cs-button--accept):not(.cs-button--reject):focus {
	border-color: var(--cs-brand-400);
}

.cs-quote-detail__section h3 {
	margin: 0 0 var(--cs-space-xs);
	font-size: var(--cs-text-base);
	text-transform: uppercase;
	letter-spacing: var(--cs-tracking-label);
	color: var(--cs-text-strong);
}

.cs-quote-detail__list {
	margin: 0;
}

/* Label beside value above 768px (docs/06's collapse point, same as .cs-leg-summary__specs),
   stacked below it — a dense label/value list is the one layout that keeps this page scannable. */
.cs-quote-detail__row {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--cs-space-3xs) 0;
	padding: var(--cs-space-2xs) 0;
	border-bottom: 1px solid var(--cs-border);
}

.cs-quote-detail__row:last-child {
	border-bottom: 0;
}

.cs-quote-detail__row dt {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-quote-detail__row dd {
	margin: 0;
	color: var(--cs-text);
	font-size: var(--cs-text-sm);
}

@media (min-width: 768px) {
	.cs-quote-detail__row {
		grid-template-columns: 14rem 1fr;
		align-items: baseline;
		gap: 0 var(--cs-space-sm);
	}
}

.cs-quote-detail__meta {
	margin: var(--cs-space-2xs) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

/* detail_prose()'s own docblock states line breaks come from this rule, not from an HTML-emitting
   formatter — esc_html() is the only thing standing between customer-authored free text and this
   page, so wrapping is CSS-only rather than ever re-introducing a formatter that could emit markup. */
.cs-quote-detail__prose {
	white-space: pre-line;
}

.cs-quote-detail__empty {
	color: var(--cs-text-muted);
	font-style: italic;
}

/* A deliberate redaction, not a rendering failure: letter-spacing and the muted colour read as "this
   was withheld on purpose" rather than as broken output. The glyphs themselves are aria-hidden
   (provider-quote-queue.php's mask_contact_span()) — the accessible name is the adjacent
   screen-reader-text span, not this punctuation. */
.cs-quote-detail__masked {
	color: var(--cs-text-muted);
	letter-spacing: 0.15em;
}

.cs-quote-detail__field-error {
	margin: 0 0 var(--cs-space-2xs);
	padding-left: var(--cs-space-xs);
	border-left: 3px solid var(--cs-danger-text);
	color: var(--cs-danger-text);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

/* Each action is an enclosed panel, not two forms sharing a box.

   A border-top between them was enough while one side was a bare button. Both sides now carry fields
   — price + currency against reason + note — and at four controls proximity alone stops answering
   "which submit owns which field": the gap above a form's own button is necessarily smaller than the
   gap between the two forms only if that second gap is large enough to look like dead space. Full
   enclosure removes the judgement. Measured on this exact section: the span *inside* the decline form
   (first field to submit) is 199px while the gap *between* the two forms was 44-85px, so proximity
   argued for the wrong grouping at every achievable margin. Only enclosure fixed it.

   Sunken rather than borderless, so the nesting reads as intentional recession instead of a second
   ring drawn inside the section's own border: `--cs-surface-sunken` steps 1.30:1 down from the
   section's `--cs-surface-panel`, and the 1px `--cs-border-strong` edge is 5.22:1 against it.
   `--cs-surface` was the other candidate and is rejected because it is exactly the fill the inputs
   inside use — the panel and its own controls would have been the same colour (1.00:1) and the
   fields would have dissolved into their container. Confirmed at render: the panels read as recessed
   insets rather than as a second stack of raised boxes. Note the knock-on, spelled out on the input
   rule above — even at sunken the input fill is only 1.10:1 against this panel, so those borders are
   now carrying the field boundaries alone.

   Always full width now (2026-08-19): the accept/decline pair no longer sits side by side at any
   width -- see .cs-quote-detail__tabs below, which replaced that with a tabbed view the same day. A
   solitary cancel_quote panel, or an untabbed lone accept/decline, still renders exactly this rule.
   The `max-width: 28rem` that used to cap this panel independent of its container survived that
   change by accident -- it left the tabbed panel narrower than the tablist sitting directly above
   it, which is the "container" this panel is meant to fill (2026-08-19 correction). Width now
   simply matches whatever `.cs-quote-detail__tabs`/its parent section already constrain it to. */
.cs-quote-detail__action {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-2xs);
	width: 100%;
	margin: 0 0 var(--cs-space-md);
	padding: var(--cs-space-sm);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-sunken);
}

/* quote-tabs.js is the only thing that ever sets this -- see its own docblock and
   render_tablist()'s in provider-quote-queue.php. Restated against Divi's own dynamic CSS the same
   way .cs-share[hidden] and .cs-leg-detail__route-visual[hidden] already are elsewhere in this file:
   the UA default `[hidden] { display: none }` is a 0,1,0 rule and this project has already been
   burned once by Divi's ID-specificity selectors silently outranking a same-specificity child-theme
   one, so every element script can hide gets its own explicit, unmissable rule instead of trusting
   the UA default to survive the cascade untouched. */
.cs-quote-detail__action[hidden] {
	display: none;
}

/* WAI-ARIA APG "Tabs" tablist -- the row of two buttons render_tablist() emits ahead of the accept
   and decline panels. flex-wrap rather than a media query: "Accept and quote" / "Decline with a
   reason" are long enough that a narrow phone needs the label to wrap inside its own tab rather than
   forcing two tabs onto one cramped line, and flex-basis: 0 with flex-grow means both tabs still
   claim equal width above that width regardless of label length. */
.cs-quote-detail__tabs {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-2xs);
	margin: 0 0 var(--cs-space-xs);
}

/* 44px tap target per this project's standing rule, matching every other control on this panel.
   Selected state is colour plus a visible underline, never colour alone, so it still reads for a
   user who cannot distinguish the accent hue from the unselected grey. */
.cs-quote-detail__tab {
	flex: 1 1 0;
	min-width: 8rem;
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-2xs) var(--cs-space-xs);
	border: 1px solid var(--cs-border-strong);
	border-bottom: 3px solid transparent;
	border-radius: var(--cs-radius-sm) var(--cs-radius-sm) 0 0;
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
	font-family: inherit;
	font-size: var(--cs-text-sm);
	font-weight: 600;
	line-height: var(--cs-leading-snug);
	cursor: pointer;
}

.cs-quote-detail__tab[aria-selected="true"] {
	border-bottom-color: var(--cs-brand-400);
	background: var(--cs-surface-panel);
	color: var(--cs-text);
}

.cs-quote-detail__tab:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

.cs-quote-detail__action-heading {
	margin: 0 0 var(--cs-space-2xs);
	font-size: var(--cs-text-base);
	font-weight: 700;
}

.cs-quote-detail__action label {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-weight: 600;
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

/* Same treatment as .cs-filters__field's inputs (style.css above) — 44px tap target, 16px text so
   iOS Safari doesn't zoom on focus, dark-theme surface/border tokens instead of UA defaults.

   Bare `input`, not `input[type="number"]`: the price field is `type="text"` (see render_accept_form()
   for why), and an attribute-scoped selector here would silently drop every one of these declarations
   the moment the type changed — leaving a UA-default light input on a dark panel, below the tap
   target, at a font size that zooms iOS Safari on focus. That is the exact defect this block exists
   to prevent, and scoping it to a type is how it comes back.

   **The 1px border is load-bearing, not decoration.** Since the enclosing panel went sunken, the
   input's own `--cs-surface` fill is 1.10:1 against it — the fill separates these fields from their
   container almost not at all, and `--cs-border-strong` (5.22:1 on that panel) is carrying the
   boundary by itself. Softening or removing this border for a cleaner look would leave the controls
   with no visible edge at all. Same class of dependency as `outline-offset` below. */
.cs-quote-detail__action select,
.cs-quote-detail__action input,
.cs-quote-detail__action textarea {
	width: 100%;
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-2xs) var(--cs-space-xs);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
	color: var(--cs-text);
	font-family: inherit;
	font-size: var(--cs-text-base);
	line-height: var(--cs-leading-snug);
}

.cs-quote-detail__action textarea {
	min-height: calc(var(--cs-tap-target) * 2);
	resize: vertical;
}

.cs-quote-detail__action select:focus,
.cs-quote-detail__action input:focus,
.cs-quote-detail__action textarea:focus {
	border-color: var(--cs-brand-400);
}

/* Price and currency read as one value, so they sit on one line — but only where the line is wide
   enough that neither is squeezed below a usable tap target. `flex-wrap` plus a `min-width` basis on
   the price does that without a media query, so it also holds inside a narrow Divi column rather than
   only below a viewport breakpoint. */
.cs-quote-detail__price-row {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-xs);
}

.cs-quote-detail__field {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-2xs);
}

.cs-quote-detail__field--price {
	flex: 1 1 11rem;
}

.cs-quote-detail__field--currency {
	flex: 0 1 7rem;
}

/* Separated from the hint above it, so the submit reads as the end of this panel rather than as one
   more line of its explanatory copy. */
.cs-quote-detail__action .cs-button {
	margin-top: var(--cs-space-2xs);
}

/* Text plus colour, never colour alone — the badge system's standing rule applied to the one figure
   on this page a provider must not have to compute by hand (does this cargo fit). */
.cs-badge--flag-over { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }

/* =============================================================================
   .cs-dashboard / .cs-history-table — Requestor Portal (docs/44, docs/06 "Customer dashboard
   mechanism"). .cs-history-table's stacked-card breakpoint below is deliberately identical to
   .cs-quote-queue__table's above — the same rule (docs/06 QA2: too wide to scroll horizontally at
   480px, `data-label` carries the column name once stacked) applies to both, and a second,
   independently-drifting copy of it is how the two surfaces end up disagreeing about a breakpoint.
   ============================================================================= */

.cs-dashboard__tabs {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-sm);
	margin: 0 0 var(--cs-space-md);
	border-bottom: 1px solid var(--cs-border-strong);
}

.cs-dashboard__tab {
	padding: var(--cs-space-xs) var(--cs-space-2xs);
	color: var(--cs-text-muted);
	text-decoration: none;
	font-weight: 600;
	font-size: var(--cs-text-sm);
	border-bottom: 3px solid transparent;
}

/* The border-bottom carries "active" (a real presence/absence structural cue, not only a hue swap on
   the text colour) — not "bold weight + border" as this comment used to claim: font-weight: 600 is
   already on the base .cs-dashboard__tab rule above, applied uniformly whether active or not, so the
   active state adds no weight increase of its own (qa-2 review, 2026-08-20). Harmless today since the
   border alone already satisfies colour-never-alone, but worth being accurate so a future edit to the
   base rule's weight isn't made on the false assumption the active state has its own bold backing it
   up. The label text itself already carries "Coming soon" for the two deferred tabs, so that state
   never depends on colour perception either. */
.cs-dashboard__tab--active {
	color: var(--cs-text);
	border-bottom-color: var(--cs-brand-400);
}

.cs-dashboard__tab:hover,
.cs-dashboard__tab:focus-visible {
	color: var(--cs-text);
}

.cs-dashboard__panel {
	min-height: 14rem; /* docs/06: fixed height so switching tabs doesn't jump the layout. */
}

.cs-dashboard__empty {
	padding: var(--cs-space-lg) var(--cs-space-sm);
	border: 1px dashed var(--cs-border-strong);
	text-align: center;
}

.cs-dashboard__coming-soon-note {
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-dashboard__unavailable {
	color: var(--cs-text-muted);
}

.cs-dashboard__signin .cs-button {
	margin-top: var(--cs-space-xs);
}

.cs-history-table {
	width: 100%;
	border-collapse: collapse;
	font-size: var(--cs-text-sm);
}

.cs-history-table th,
.cs-history-table td {
	padding: var(--cs-space-2xs) var(--cs-space-xs);
	border-bottom: 1px solid var(--cs-border);
	text-align: left;
	vertical-align: top;
}

.cs-history-table thead th {
	border-bottom-color: var(--cs-border-strong);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	letter-spacing: var(--cs-tracking-label);
	text-transform: uppercase;
}

.cs-history-table__addon-link {
	font-size: var(--cs-text-xs);
}

.cs-badge--bstatus-pending_payment    { --cs-badge-fg: var(--cs-warning-text); --cs-badge-bg: var(--cs-warning-tint); }
/* funded/reserved (docs/46) — in-progress toward completion, info tint like confirmed rather than
   the terminal success tint certificate_minted alone carries; the text label is what actually
   distinguishes the three, per this block's own "never colour alone" rule. */
.cs-badge--bstatus-funded             { --cs-badge-fg: var(--cs-info-text);    --cs-badge-bg: var(--cs-info-tint); }
.cs-badge--bstatus-reserved           { --cs-badge-fg: var(--cs-info-text);    --cs-badge-bg: var(--cs-info-tint); }
.cs-badge--bstatus-confirmed          { --cs-badge-fg: var(--cs-info-text);    --cs-badge-bg: var(--cs-info-tint); }
.cs-badge--bstatus-certificate_minted { --cs-badge-fg: var(--cs-success-text); --cs-badge-bg: var(--cs-success-tint); }
.cs-badge--bstatus-cancelled          { --cs-badge-fg: var(--cs-text-muted);   --cs-badge-bg: var(--cs-surface-panel); }
.cs-badge--bstatus-de_manifested      { --cs-badge-fg: var(--cs-danger-text);  --cs-badge-bg: var(--cs-danger-tint); }

@media (max-width: 767px) {
	.cs-history-table thead {
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip: rect(0 0 0 0);
		clip-path: inset(50%);
		white-space: nowrap;
	}

	.cs-history-table,
	.cs-history-table tbody,
	.cs-history-table tr,
	.cs-history-table th,
	.cs-history-table td {
		display: block;
		width: 100%;
	}

	.cs-history-table tr {
		margin: 0 0 var(--cs-space-sm);
		padding: var(--cs-space-xs);
		border: 1px solid var(--cs-border-strong);
	}

	.cs-history-table td,
	.cs-history-table th {
		border-bottom: 0;
		padding: var(--cs-space-3xs) 0;
	}

	.cs-history-table td::before {
		content: attr(data-label) ": ";
		color: var(--cs-text-muted);
		font-size: var(--cs-text-xs);
		text-transform: uppercase;
	}
}

/* =============================================================================
   MOTION
   ============================================================================= */

@media (prefers-reduced-motion: reduce) {
	.cs-button,
	.cs-leg-card,
	.cs-leg-card::before,
	.cs-chips__chip a,
	.cs-pagination .page-numbers,
	.cs-accordion__summary::before,
	.cs-share {
		transition: none;
	}
}

/* =============================================================================
   .cs-signup — the requestor signup surface (FR-26)

   ONLY WHAT IS SPECIFIC TO THIS FORM. The shared layout — sections, fields,
   labels, hints, errors, the result banner and the error summary — lives in the
   `.cs-form` block at the end of this file (contract:
   docs/68-shared-form-ui-contract.md). The wrapper carries both classes, and
   everything here scopes from `.cs-signup` or its `--closed` modifier onto
   `.cs-form__*` elements rather than restating them.

   What is left is the closed state, which is the part that needed the design
   work: a disabled form looks exactly like a broken form unless something says
   otherwise, so the explanation is styled as a real notice with its own
   emphasis rather than left as body copy above a grey box, and the section gets
   an explicit "off" treatment instead of relying on the browser's default
   disabled rendering.
   ============================================================================= */

/* The pre-launch explanation. Warning tint rather than danger: nothing has gone
   wrong, the thing simply is not open yet. */
.cs-signup__notice {
	margin: 0 0 var(--cs-space-md);
	padding: var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-top: 3px solid var(--cs-warning-text);
	border-radius: var(--cs-radius-md);
	background: var(--cs-warning-tint);
}

/* qa-2 flagged that links inside this block inherit the parent theme's colour
   onto a --cs-warning-tint background it knows nothing about, and that this
   codebase treats text-on-tint as a MEASURED pair (--cs-info-text carries
   '8.79:1 on tint' as a comment) -- re-parenting voids the inherited ratio.
   The redesign made the two primary actions .cs-button, which carry their own
   colours, so the flagged case is gone. This declares a colour anyway for any
   bare link added here later: --cs-warning-text is the token already measured
   against this exact tint, so it inherits a known-good pair instead of an
   unknown one. Underlined because it is then the same colour as the surrounding
   notice text and must not rely on colour alone to read as a link. */
.cs-signup__notice a:not(.cs-button) {
	color: var(--cs-warning-text);
	text-decoration: underline;
}

.cs-signup__notice-heading {
	margin: 0 0 var(--cs-space-2xs);
	color: var(--cs-warning-text);
	font-size: var(--cs-text-xl);
	line-height: var(--cs-leading-snug);
}

.cs-signup__notice p {
	max-width: 68ch;
	margin: 0 0 var(--cs-space-2xs);
	line-height: var(--cs-leading-normal);
}

.cs-signup__notice p:last-child {
	margin-bottom: 0;
}

/* -----------------------------------------------------------------------------
   The closed state.

   The controls here are `readonly` + `aria-disabled`, NOT `disabled` -- see the
   note in signup_form_markup(). That choice is an accessibility one, but it has
   a styling consequence: the browser gives readonly inputs no visual treatment
   at all, so unlike a disabled form there is nothing to lean on and every part
   of "this is off" has to be drawn deliberately here.

   Which is the better position to be in. Grey is what broken looks like, so the
   message is carried by the preview note and the reason-labelled button in the
   markup, and these rules only support them -- a dashed edge that reads as
   provisional rather than failed, and a recessed surface.

   Opacity is deliberately NOT used on the fields. The whole point of rendering
   the form is that a visitor can read what will be asked of them, and dimming
   the labels to signal "off" would trade away the only reason the block exists.
   -------------------------------------------------------------------------- */

/* Follows the card. The dashed, recessed "off" treatment has to sit on whatever
   element carries the surface, and that is now the form rather than the section. */
.cs-signup--closed .cs-form__form {
	background: var(--cs-surface-sunken);
	border-style: dashed;
}

/* Provisional, not broken: the field keeps full-contrast text so it stays
   readable, and signals its state through the edge and the not-allowed cursor.
   Targets :disabled -- the closed state uses <fieldset disabled>, which
   disables every control inside it (see signup_form_markup()'s note on why the
   readonly variant was tried and reverted). */
.cs-signup--closed .cs-form__field input:disabled {
	border-style: dashed;
	background: transparent;
	cursor: not-allowed;
	/* Browsers dim disabled controls by default. Overridden back to full colour:
	   the entire reason for rendering this form is that a visitor can READ what
	   will be asked of them, and the "off" message is carried by the preview
	   note, the dashed edges and the reason-labelled button instead. qa-2
	   measured the result -- legend/label 17.44, input text 14.80, hint 7.12 --
	   all comfortably above 4.5. */
	-webkit-text-fill-color: var(--cs-text);
	color: var(--cs-text);
	opacity: 1;
}

.cs-signup__preview-note {
	margin: 0 0 var(--cs-space-xs);
	padding: var(--cs-space-3xs) var(--cs-space-xs);
	border-left: 3px solid var(--cs-warning-text);
	background: var(--cs-warning-tint);
	color: var(--cs-warning-text);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

.cs-signup__form-intro {
	max-width: 68ch;
	margin: 0 0 var(--cs-space-md);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-normal);
}

.cs-signup--closed .cs-form__submit .cs-button {
	cursor: not-allowed;
	background: transparent;
	border: 1px dashed var(--cs-border-strong);
	color: var(--cs-text-muted);
	box-shadow: none;
}

/* Divi's button rules use :hover to promise interactivity. Nothing here is
   interactive, so the hover affordance is removed rather than inherited -- a
   button that lifts on hover and then refuses the click is worse than one that
   never suggested it would work. */
.cs-signup--closed .cs-form__submit .cs-button:hover,
.cs-signup--closed .cs-form__submit .cs-button:focus {
	background: transparent;
	border-color: var(--cs-border-strong);
	color: var(--cs-text-muted);
	transform: none;
}

/* The one working action in the closed state, so it sits above the preview
   block rather than after it -- the keyboard path reaches something real before
   it reaches anything inert. */
.cs-signup__notice-actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--cs-space-2xs);
	margin: var(--cs-space-sm) 0 var(--cs-space-2xs);
}

.cs-signup__notice-aside {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

/* Wrong-door pointer for launch operators. Quiet on purpose: it matters to a
   minority of readers and must not compete with the primary action. */
.cs-signup__other-door {
	max-width: 68ch;
	margin: var(--cs-space-md) 0 0;
	padding-top: var(--cs-space-sm);
	border-top: 1px solid var(--cs-border-surface);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-normal);
}

/* Rowan measured this anchor at 2.11:1 in a harness loading this stylesheet alone, and was right
   not to claim it renders that way live. It does not: Divi's Customizer emits a bare `a { color }`
   printed after this file, and `wp-cli/08-theme-builder-chrome.php` sets it to `--cs-brand-300` on
   every site. 2.11:1 is the browser default #0000EE on `--cs-bg-page` — a real number for a page
   that never exists.

   Declared anyway, and the argument is asymmetry rather than contrast (his): the anchor a few rules
   above states its own colour, so leaving this one to a global reads as deliberate later, and it is
   one Customizer edit from being wrong. `--cs-link` IS `--cs-brand-300`, the same value Divi
   supplies, so this changes nothing on screen today and removes the dependency. 10.34:1 on
   `--cs-bg-page`.

   No ID needed: class+element is (0,1,1) and Divi's bare `a` is (0,0,1), so this wins on
   specificity rather than order. The warning at the top of this file is about a rule that MATCHES
   Divi's specificity, not one that exceeds it.

   Underlined for the same reason its sibling is — the surrounding text is `--cs-text-muted`, and a
   colour change alone must never be what marks a link (docs/06). */
.cs-signup__other-door a {
	color: var(--cs-link);
	text-decoration: underline;
}

@media (max-width: 480px) {
	.cs-signup__notice {
		padding: var(--cs-space-sm);
	}
}

/* =============================================================================
   .cs-add-empty — the gated Add Empty form in the Provider Portal (CR-56,
   mu-plugin/charterspace-core/includes/provider-add-empty.php)

   Nineteen fields is long enough that the layout has one job above all others:
   make it obvious where you are, what is still to do, and — when a submission
   comes back rejected — which field to fix. Everything below serves that.

   The control block deliberately duplicates `.cs-quote-detail__action`'s
   declarations rather than sharing a selector list with it. That block is
   scoped to a *sunken* panel and its own comment says the 1px border is
   load-bearing precisely because of that surface pairing; these fieldsets sit
   on `--cs-surface-panel` instead. Joining the two selector lists would make
   one contrast argument silently govern two different backgrounds, which is the
   failure `.cs-quote-detail__action`'s comment already warns about from the
   other direction. Two blocks, two stated surfaces.
   ============================================================================= */

.cs-add-empty {
	margin: var(--cs-space-md) 0;
}

/* Same border-left + tint pattern as .cs-quote-queue__notice, so a gate message
   on this surface reads as the same *kind* of thing it does on the sibling one. */
.cs-add-empty__notice {
	margin: 0 0 var(--cs-space-sm);
	padding: var(--cs-space-xs) var(--cs-space-sm);
	border-left: 3px solid var(--cs-border-strong);
	background: var(--cs-surface-panel);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-add-empty__notice--error { border-left-color: var(--cs-danger-text); }
.cs-add-empty__notice--info  { border-left-color: var(--cs-info-text); }

/* The outcome banner after a POST/redirect/GET. Heavier than a notice on
   purpose: it is answering "did that work", which is the only question the
   provider has on arrival. Text carries the outcome either way — the tint is
   never the only signal (the badge system's standing rule). */
.cs-add-empty__result {
	margin: 0 0 var(--cs-space-md);
	padding: var(--cs-space-sm) var(--cs-space-md);
	border-left: 4px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-panel);
}

.cs-add-empty__result p { margin: 0; }

.cs-add-empty__result--success {
	border-left-color: var(--cs-success-text);
	background: var(--cs-success-tint);
}

.cs-add-empty__result--error {
	border-left-color: var(--cs-danger-text);
	background: var(--cs-danger-tint);
}

/* tabindex="-1" makes this focusable so the redirect's #cs-add-empty-result
   fragment can land on it; the ring must therefore be visible, not suppressed
   as a "not really interactive" element. */
.cs-add-empty__result:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

/* Real <fieldset>/<legend>, enclosed rather than separated by whitespace alone.
   Same reasoning .cs-quote-detail__action's own comment sets out at length: past
   about four controls, proximity stops answering "which group does this field
   belong to" at any achievable margin, and only enclosure removes the judgement.
   That argument applies far more here — this form has four groups and nineteen
   controls. */
.cs-add-empty__section {
	margin: 0 0 var(--cs-space-md);
	padding: var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-panel);
}

.cs-add-empty__section legend {
	padding: 0 var(--cs-space-2xs);
	color: var(--cs-text-strong);
	font-size: var(--cs-text-lg);
	font-weight: 600;
}

.cs-add-empty__section-intro {
	margin: 0 0 var(--cs-space-md);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-add-empty__section--error {
	border-color: var(--cs-danger-text);
}

.cs-add-empty__field {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-3xs);
	margin-bottom: var(--cs-space-md);
}

.cs-add-empty__field:last-child { margin-bottom: 0; }

.cs-add-empty__field label {
	color: var(--cs-text-strong);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

/* Text, never an asterisk and never colour alone. Muted rather than accented so
   "(required)" does not out-shout the label it qualifies. */
.cs-add-empty__required {
	color: var(--cs-text-muted);
	font-weight: 400;
}

.cs-add-empty__hint {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

/* Rendered *above* its control in the markup, not below: on a form this long a
   message underneath is read only once focus has already moved past the field,
   which is the difference between "field three is wrong" and scrolling to find
   out which. The left border gives it an edge that survives at 480px where the
   colour alone would read as decoration. */
.cs-add-empty__field-error {
	margin: 0;
	padding-left: var(--cs-space-xs);
	border-left: 3px solid var(--cs-danger-text);
	color: var(--cs-danger-text);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

/* 44px tap target and 16px text (so iOS Safari does not zoom the page on focus),
   dark-theme tokens instead of UA defaults. Bare element selectors rather than
   `input[type="…"]`, for the reason .cs-quote-detail__action's comment gives:
   this form's datetime fields fall back to `type="text"` where the browser has
   no picker, and a type-scoped rule would drop every declaration below at
   exactly that moment — leaving a UA-default light input on a dark panel, below
   the tap target, at a size that zooms on focus.

   These sit on `--cs-surface-panel`, and `--cs-border-strong` is 5.22:1 against
   it, so the 1px border is what actually carries the control boundary (WCAG
   1.4.11's 3:1). Removing it for a flatter look leaves the fields with no
   visible edge. */
.cs-add-empty__field select,
.cs-add-empty__field input,
.cs-add-empty__field textarea {
	width: 100%;
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-2xs) var(--cs-space-xs);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
	color: var(--cs-text);
	font-family: inherit;
	font-size: var(--cs-text-base);
	line-height: var(--cs-leading-snug);
}

.cs-add-empty__field textarea {
	min-height: calc(var(--cs-tap-target) * 2);
	resize: vertical;
}

.cs-add-empty__field select:focus,
.cs-add-empty__field input:focus,
.cs-add-empty__field textarea:focus {
	border-color: var(--cs-brand-400);
}

/* aria-invalid is the accessible signal; this is its visual echo. Both, never
   one — a border colour alone fails 1.4.1, and an ARIA attribute alone is
   invisible to everyone not using AT. */
.cs-add-empty__field--error select,
.cs-add-empty__field--error input,
.cs-add-empty__field--error textarea {
	border-color: var(--cs-danger-text);
}

/* The one control that reads left-to-right: a checkbox above its own label would
   be a stray box. `align-items: center` rather than baseline so the box lines up
   with the first line of a label that wraps at 480px. */
.cs-add-empty__field--check {
	flex-direction: row;
	align-items: center;
	gap: var(--cs-space-2xs);
}

.cs-add-empty__field--check input {
	width: auto;
	min-height: auto;
	min-width: 1.25rem;
	height: 1.25rem;
	flex: 0 0 auto;
}

/* The label is the real activation target -- clicking it toggles the box -- so it, not the 20px
   input, is what has to meet the 44px standard. Measured at 43px before this padding (qa-2), 1px
   short: vertical padding rather than a min-height so the row still centres on the checkbox. */
.cs-add-empty__field--check label {
	padding: var(--cs-space-3xs) 0;
	font-weight: 400;
}

/* The disclaimer is content the provider is receiving, not a hint on a control —
   so it gets its own recessed block above the checkbox rather than being crammed
   into the field description the way WPForms Lite forced. */
.cs-add-empty__disclaimer {
	margin-bottom: var(--cs-space-sm);
	padding: var(--cs-space-sm);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-sunken);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-add-empty__disclaimer p:last-child { margin-bottom: 0; }

.cs-add-empty__obligations {
	margin: 0 0 var(--cs-space-sm);
	padding-left: var(--cs-space-md);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

/* Separated from the fieldsets above it so the submit reads as the end of the
   form rather than as one more field in the last group. */
.cs-add-empty__submit {
	margin-top: var(--cs-space-md);
}

.cs-add-empty__submit-hint {
	margin: var(--cs-space-2xs) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

/* At 480px the fieldset padding is a meaningful share of the line length, and
   the enclosure is still doing its job at half of it. Designed here first, per
   docs/06's mobile-first mandate — this is the narrowing, not an afterthought. */
@media (max-width: 480px) {
	.cs-add-empty__section {
		padding: var(--cs-space-sm);
	}
}

/* CR-58: app.charterspace.io's coming-soon page. Rendered by app-coming-soon.php outside Divi's
   own header/footer templates, so this page owns its own full-viewport layout rather than relying
   on a page-content wrapper Divi would otherwise provide. `.cs-footer` itself is untouched — it is
   the real shared component (shortcode_footer()), not a copy — so nothing here re-styles it. */
.cs-coming-soon-body {
	margin: 0;
	background-color: var(--cs-bg-page);
}

/* This page bypasses Divi's own page template entirely (deliberately -- see
   app-coming-soon.php's docblock), so it gets none of the 80%/1080px container Divi's row/section
   wrappers normally apply to page content (reference-divi-default-template-full-bleed.md).
   [cs_footer] itself carries no max-width of its own -- every other page on the network supplies
   one via that Divi wrapper -- so without this rule the footer runs the full viewport width here
   and nowhere else on the site, which is the fix for exactly that report. Reproduces the site's
   own convention rather than inventing a narrower one. */
.cs-coming-soon__footer-container {
	width: 80%;
	max-width: 1080px;
	margin: 0 auto;
}

.cs-coming-soon {
	min-height: 80vh;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--cs-space-xl) var(--cs-space-md);
	text-align: center;
}

.cs-coming-soon__hero {
	max-width: 32rem;
}

.cs-coming-soon__rocket {
	display: block;
	margin: 0 auto var(--cs-space-lg);
}

/* A slow drift rather than a loop that calls attention to itself — this page's whole point is to
   say "not yet", not to perform activity it cannot back up with anything to click. Respects
   prefers-reduced-motion, same posture as ER-12/ER-12b's backdrop animations. */
@media (prefers-reduced-motion: no-preference) {
	.cs-coming-soon__rocket {
		animation: cs-coming-soon-drift 6s ease-in-out infinite;
	}
}

@keyframes cs-coming-soon-drift {
	0%, 100% { transform: translateY(0); }
	50%      { transform: translateY(-8px); }
}

.cs-coming-soon__headline {
	margin: 0 0 var(--cs-space-2xs);
	font-size: var(--cs-text-3xl);
	color: var(--cs-text-strong);
}

.cs-coming-soon__tagline {
	margin: 0;
	font-size: var(--cs-text-lg);
	color: var(--cs-text-muted);
}

/* =============================================================================
   .cs-form — the shared long-form UI (FR-25 provider signup, now on the provider site's /signup/ per CR-72; FR-26 /signup/ on this site)

   Contract: docs/68-shared-form-ui-contract.md. There is one element vocabulary
   for both forms and no per-feature variant of it; markup that does not match
   §3 of that document is not styled by this block, deliberately.

   THE DECISION THIS BLOCK ENCODES: these are sectioned single-document forms,
   not wizards. Both post to admin-post.php and come back through a redirect
   carrying a server-held draft, so a stepper would have to restore *which step
   you were on* on top of the values it already restores — new state whose only
   failure mode is losing your place, which is worse than not stepping at all.
   At 9–13 fields there is nothing to buy with that risk. `docs/07` §7.1's
   three-page split answered a different problem: 29 fields, two file uploads
   and two eSignatures.

   THERE IS NO PROGRESS RAIL, AND ITS ABSENCE IS THE DESIGN. A bar implies
   gating — that section 3 is unreachable until 2 is complete. Nothing here is
   gated, so a bar would be a lie drawn in CSS. `.cs-form__step` ("Step 2 of 3",
   inside the legend) answers where-am-I and how-much-is-left without asserting
   a gate it cannot enforce.

   NO JAVASCRIPT. That is the mobile-performance story and the accessibility
   story at once: the first paint is the finished form, on a phone whose main
   thread is already busy with Divi. If a stepper is ever wanted it is a
   progressive-enhancement layer over exactly this markup, in `quote-tabs.js`'s
   shape — server ships every section un-hidden, script narrows — and the server
   has to tell it which step to reopen after a redirect.

   SURFACES ARE STATED, NOT INHERITED, per the standing note on
   `.cs-quote-detail__action`: every enclosure here sits on `--cs-surface-panel`
   and every control on `--cs-surface`, and no selector list in this block is
   shared with a block on a different surface. This supersedes the element rules
   that used to live in the `.cs-signup` and `.cs-application` blocks — both now
   carry only what is genuinely feature-specific — so it is a replacement for
   two systems rather than a third one alongside them.
   ============================================================================= */

/* Scoped to this block, never global — the same shape as `.cs-leg-card *` and
   `.cs-status-tracker *` above, and for a concrete reason rather than for
   consistency. Every control below is `width: 100%` with its own padding and a
   1px border, so under content-box each one renders 26px wider than the column
   that contains it and overflows the section's right padding. Measured at
   1280px before this rule existed: field 686px, input 712px, 26px past the
   section's content edge. This stylesheet does not assume a global reset
   anywhere else and must not start here. */
.cs-form,
.cs-form * {
	box-sizing: border-box;
}

.cs-form {
	/* min(), never a fixed px width: the Divi content column is ~0.8×viewport,
	   so a fixed width overflows it at exactly the sizes that matter least on
	   desktop and most on a phone. */
	max-width: min(46rem, 100%);
	margin: var(--cs-space-md) 0;
}

.cs-form__intro {
	max-width: 68ch;
	margin: 0 0 var(--cs-space-md);
	line-height: var(--cs-leading-normal);
}

/* -----------------------------------------------------------------------------
   The outcome of the previous submission, read back after the PRG redirect.

   `role="status"` in the markup, not `role="alert"` — the redirect has already
   moved focus to a fresh document, so this is new page content being announced
   rather than an interruption.

   Each tone declares its own `color`. The block this replaces set the tint and
   let the text colour inherit, which is the re-parenting hazard this stylesheet
   documents elsewhere: an inherited colour carries a ratio measured against a
   surface it knows nothing about. The three `--cs-*-text` tokens are the ones
   already measured against these exact tints (9.99, 8.79 and 8.51:1).
   -------------------------------------------------------------------------- */

.cs-form__result {
	margin: 0 0 var(--cs-space-md);
	padding: var(--cs-space-sm) var(--cs-space-md);
	border-left: 4px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface-panel);
}

.cs-form__result h2 {
	margin: 0 0 var(--cs-space-2xs);
	font-size: var(--cs-text-lg);
	line-height: var(--cs-leading-snug);
}

.cs-form__result p {
	max-width: 68ch;
	margin: 0;
	line-height: var(--cs-leading-normal);
}

.cs-form__result--success {
	border-left-color: var(--cs-success-text);
	background: var(--cs-success-tint);
	color: var(--cs-success-text);
}

.cs-form__result--pending {
	border-left-color: var(--cs-info-text);
	background: var(--cs-info-tint);
	color: var(--cs-info-text);
}

.cs-form__result--error {
	border-left-color: var(--cs-danger-text);
	background: var(--cs-danger-tint);
	color: var(--cs-danger-text);
}

/* tabindex="-1" so the redirect's `#cs-{scope}-result` fragment can focus it;
   the ring must stay visible rather than be suppressed as "not interactive". */
.cs-form__result:focus-visible {
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 2px;
}

/* -----------------------------------------------------------------------------
   The error summary.

   Rendered inside the result banner only when a submit came back with field
   errors, and it is what replaces a wizard's focus management: every entry is
   an anchor to its own control, so the path from "something is wrong" to "the
   wrong thing" is one Tab and one Enter rather than a scan of the whole form.
   -------------------------------------------------------------------------- */

.cs-form__errors {
	margin-top: var(--cs-space-sm);
}

/* Two classes (0,2,0) rather than one, so it outranks `.cs-form__result h2`
   (0,1,1) above — the summary heading is a step down from the banner's own
   heading, not a second one at the same weight. */
.cs-form__result .cs-form__errors-heading {
	margin: 0 0 var(--cs-space-2xs);
	color: var(--cs-danger-text);
	font-size: var(--cs-text-base);
	line-height: var(--cs-leading-snug);
}

/* Divi's dynamic CSS matches any <ul> inside the_content() via `#left-area ul`
   (specificity 1,0,1) and puts back a bullet and `padding: 0 0 23px 1em`. An ID
   outranks any class-only selector at any source order, so this needs
   !important — and on the SHORTHAND, because the declaration being overridden
   is a shorthand and `padding-left: 0` would fix one side and leave three.
   Fourth-plus occurrence of this same bleed-through in this stylesheet (console
   nav, footer nav, the quote-queue filter tabs); same fix each time. */
.cs-form__errors-list {
	margin: 0;
	padding: 0 !important;
	list-style: none !important;
}

.cs-form__errors-list li {
	margin: 0 0 var(--cs-space-3xs);
	line-height: var(--cs-leading-normal);
}

.cs-form__errors-list li:last-child {
	margin-bottom: 0;
}

/* Underlined, and the colour declared rather than inherited. These links sit on
   `--cs-danger-tint` in the same `--cs-danger-text` as the prose around them, so
   the underline is the only thing that distinguishes a link from a sentence —
   never colour alone (docs/06). Declaring the colour keeps the measured 8.51:1
   pairing instead of inheriting one measured against a different surface. */
.cs-form__errors-list a {
	color: var(--cs-danger-text);
	text-decoration: underline;
}

/* -----------------------------------------------------------------------------
   Sections
   -------------------------------------------------------------------------- */

/* THE FORM IS THE CARD, NOT THE SECTION (user, 2026-09-07: "the captcha should be
   within the same container, not floating outside. also, include the submit
   button inside the container too").

   The captcha and the submit are SIBLINGS of the fieldsets — verified in the
   generator rather than inferred from the render: signup-form.php emits
   honeypot, then a fieldset per section, then hcaptcha_widget_markup(), then the
   submit wrapper, all directly inside <form>. So while the card was the section,
   anything after the last fieldset sat on the page background. The user is
   describing exactly what the markup does.

   MOVING THE MARKUP WAS THE OTHER OPTION AND IS THE WRONG ONE, for two reasons
   that are not style preferences:

   - `<fieldset disabled>` is how the closed signup state works. A submit moved
     inside would be disabled by INHERITANCE, silently replacing the explicit
     `disabled` + `aria-describedby` pointing at the preview note — the closed
     button is labelled "Accounts open at launch" and that reasoning would go.
   - BF-22 requires the captcha's container and its script to arrive as one
     string with nothing inserted between them; a widget with no script mints no
     token, and on a fail-closed path that refuses every genuine submitter.
     Wrapping markup around that pair is exactly the edit that risks it.

   So the enclosure moves in CSS instead. Nothing about the markup changes, both
   forms are fixed by one rule, and neither hazard is touched. */
.cs-form__form {
	padding: var(--cs-space-md);
	border: 1px solid var(--cs-border-surface);
	border-radius: var(--cs-radius-md);
	background: var(--cs-surface-panel);
}

/* A fieldset carries browser default margin, padding and border, so all three
   are zeroed rather than left to be inherited-looking. */
.cs-form__section {
	margin: 0;
	padding: 0;
	border: 0;
	/* Explicit rather than merely absent. Dropping the declaration would leave the
	   section transparent in THIS stylesheet, but says nothing to any other rule
	   that sets one — and it made the preview harness unfaithful, where the old
	   and new sheets are both present and the section kept its old fill. Stating
	   `none` is the difference between "I did not set a background" and "this
	   element has no background", and only the second is checkable. */
	background: none;
}

/* Separation between sections is now an INTERNAL hairline rather than the edge
   of a box, which is precisely the job this stylesheet's token comment reserves
   `--cs-border` for — a quiet 1.46:1 rule inside an already-bounded container,
   as against `--cs-border-surface` at 4.75:1 for a surface's outer edge. On the
   one-section signup form this selector never matches; on the four-section
   provider application it does the work the four separate cards used to. */
.cs-form__section + .cs-form__section {
	margin-top: var(--cs-space-md);
	padding-top: var(--cs-space-md);
	border-top: 1px solid var(--cs-border);
}

/* The legend is the only heading a section gets, so it is sized as one rather
   than as an uppercase micro-label: it is what a reader navigates this form by,
   and a screen reader announces it on entering the group.

   `float: left` + `width: 100%` is not decoration. A <legend> is laid out in the
   fieldset's own top border, vertically centred on it — fine for one line, and
   broken the moment the step marker below makes it two: the border is then drawn
   straight through the section title, with the step marker stranded outside the
   box. Measured at 1280px: a 43.2px legend against a border at its midpoint.
   Floating takes it out of that slot and lays it out as an ordinary block at the
   top of the fieldset, which is what the two-line form needs. The `<legend>`
   element and its grouping semantics are untouched — floating changes where it
   is painted, not what it is. */
.cs-form__legend {
	float: left;
	width: 100%;
	margin: 0 0 var(--cs-space-2xs);
	padding: 0;
	color: var(--cs-text-strong);
	font-size: var(--cs-text-lg);
	font-weight: 600;
	line-height: var(--cs-leading-snug);
}

/* The float has to be cleared by whatever follows the legend — a section intro
   where there is one, otherwise the first field. */
.cs-form__legend + * {
	clear: left;
}

/* "Step 2 of 3" — the entire progress affordance, and deliberately plain text
   inside the legend rather than a graphic: it is announced together with the
   legend on group entry, it survives with images or CSS off, and it costs
   nothing on a phone. Its own line, so the section title still starts at the
   left edge. `--cs-text-muted` is 5.49:1 on `--cs-surface-panel` — measured on
   the panel, not on `--cs-surface` where the token's own comment quotes 6.48. */
.cs-form__step {
	display: block;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-xs);
	font-weight: 400;
	letter-spacing: 0.06em;
	text-transform: uppercase;
}

.cs-form__section-intro {
	max-width: 68ch;
	margin: 0 0 var(--cs-space-md);
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-normal);
}

/* -----------------------------------------------------------------------------
   Fields

   Order inside a field is fixed by the contract: label, error, control, hint.
   The error goes ABOVE the control because a message underneath is only read
   once focus has already moved past the field.
   -------------------------------------------------------------------------- */

.cs-form__field {
	display: flex;
	flex-direction: column;
	gap: var(--cs-space-3xs);
	margin: 0 0 var(--cs-space-md);
}

.cs-form__field:last-child {
	margin-bottom: 0;
}

.cs-form__field label {
	color: var(--cs-text-strong);
	font-size: var(--cs-text-sm);
	font-weight: 600;
}

/* Text, never an asterisk and never colour alone. Lighter weight than the label
   so the two still read as one phrase rather than as two competing ones. */
.cs-form__required {
	color: var(--cs-text-muted);
	font-weight: 400;
}

.cs-form__hint {
	max-width: 68ch;
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-normal);
}

.cs-form__field-error {
	margin: 0;
	padding-left: var(--cs-space-xs);
	border-left: 3px solid var(--cs-danger-text);
	color: var(--cs-danger-text);
	font-size: var(--cs-text-sm);
	font-weight: 600;
	line-height: var(--cs-leading-normal);
}

/* 44px tap target, a 16px floor so iOS Safari does not zoom the viewport on
   focus, platform tokens instead of UA defaults. Bare element selectors rather
   than `input[type="…"]`: these forms use text/email/url/tel and a type-scoped
   rule would silently drop every declaration the moment one changed.

   The 1px border carries the control boundary, measured against BOTH surfaces
   it has to be found against: `--cs-border-strong` is 4.03:1 on
   `--cs-surface-panel` (the section fill it is drawn on) and 4.75:1 on
   `--cs-surface` (its own fill), each above WCAG 1.4.11's 3:1. The FR-25 block
   this replaces claimed 5.22:1 for the panel pairing — 5.22 is the ratio
   against `--cs-surface-sunken`, which is not the surface these sections use.
   Re-measured rather than carried across. */
.cs-form__field select,
.cs-form__field input,
.cs-form__field textarea {
	width: 100%;
	min-height: var(--cs-tap-target);
	padding: var(--cs-space-2xs) var(--cs-space-xs);
	border: 1px solid var(--cs-border-strong);
	border-radius: var(--cs-radius-sm);
	background: var(--cs-surface);
	color: var(--cs-text);
	font-family: inherit;
	font-size: max(1rem, var(--cs-text-base));
	line-height: var(--cs-leading-snug);
}

.cs-form__field textarea {
	min-height: calc(var(--cs-tap-target) * 2);
	resize: vertical;
}

/* An outline as well as the border colour. A 1px border changing hue is a weak
   focus indicator on a dark surface and disappears entirely for anyone who has
   the border in high-contrast forced colours; the outline is the part that is
   actually found. `--cs-brand-400` is 4.47:1 on `--cs-surface-panel` and 5.27:1
   on `--cs-surface`, both clear of 1.4.11's 3:1. `:focus`, not `:focus-visible`
   — a pointer user clicking into a long form benefits from seeing where the
   caret landed just as much as a keyboard user does. */
.cs-form__field select:focus,
.cs-form__field input:focus,
.cs-form__field textarea:focus {
	border-color: var(--cs-brand-400);
	outline: 2px solid var(--cs-brand-400);
	outline-offset: 1px;
}

/* `aria-invalid` is the accessible signal; this is its visual echo. Both, never
   one alone — and the message above the control carries the words, so nothing
   here depends on noticing a colour. */
.cs-form__field--error select,
.cs-form__field--error input,
.cs-form__field--error textarea {
	border-color: var(--cs-danger-text);
}

/* The honeypot. Off-screen at natural size rather than `display: none` or a
   zero-height box — both are known bot tells, and some bots skip hidden inputs
   specifically, which would defeat the point. `aria-hidden` plus `inert` in the
   markup keeps it out of the accessibility tree and out of the tab order, so
   this rule only has to keep it out of the visual flow. */
.cs-form__hp {
	position: absolute;
	left: -9999px;
	width: 1px;
	height: 1px;
	overflow: hidden;
}

/* .cs-captcha — the native challenge wrapper (BF-22,
   mu-plugin/charterspace-core/includes/hcaptcha-verify.php). Shared rather than
   namespaced under .cs-application: the same helper renders this block on the
   requestor signup form, and a second copy under a second prefix would be two
   things to keep in step for no gain.

   The widget itself is a third-party iframe at a fixed size we do not control,
   so this styles only the space around it. `max-width: 100%` because at 480px
   that iframe (~303px) plus the fieldset padding is close enough to the
   viewport that any horizontal margin here would push it into an overflow
   scrollbar. */
.cs-captcha {
	margin-top: var(--cs-space-md);
	max-width: 100%;
	overflow-x: auto;
}

.cs-captcha__noscript {
	margin: 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
}

.cs-form__submit {
	margin-top: var(--cs-space-md);
}

.cs-form__submit-hint {
	max-width: 68ch;
	margin: var(--cs-space-2xs) 0 0;
	color: var(--cs-text-muted);
	font-size: var(--cs-text-sm);
	line-height: var(--cs-leading-normal);
}

/* Single column at every width, so there is no multi-column layout to unwind
   here and no address row to reflow — the only thing that changes is the
   enclosure padding, which at 480px is a meaningful share of the line length
   and still reads at half of it. Mobile-first per docs/06: the base rules above
   are the design and this is the one adjustment. */
@media (max-width: 480px) {
	.cs-form__form,
	.cs-form__result {
		padding: var(--cs-space-sm);
	}
}

/* -----------------------------------------------------------------------------
   BF-49 — the provider site's content is not as wide as the main site looks.

   User report: "the test-01-provider site is still framed, not full width like
   test-01-www". Measured, and the premise needs restating before the fix makes
   sense: BOTH sites have the identical wrapper chain — `.container` >
   `#content-area` > `#left-area` — and Divi caps `.container` at
   `width:80%; max-width:1080px` on both. The main site is not structurally
   wider. It LOOKS wider because its hero sections carry `.cs-home__bleed` and
   break out of that container; its body prose is framed exactly the same.

   So this is not "restore a frame-free layout the provider site lost". It is a
   deliberate widening of one site's content column, chosen over the full-bleed
   treatment (user ruling, 2026-09-08, picking wider content rather than heroes
   that escape the container).

   Why the provider site specifically: it is an operator console — quote tables,
   application forms, listings. 1080px is a prose measure, and this project caps
   prose at 68ch precisely because reading width and working width are different
   problems. Nothing here touches the main site, where 1080px over `.cs-home p`'s
   68ch cap is correct.

   Scoped on `.cs-site--provider`, the body class chrome.php now emits. NOT on a
   blog id: the provider site is blog 2 on production and blog 8 on the test
   network, so `.blog-2` would silently do nothing on test — see
   .claude/rules/site-terminology.md.
   -------------------------------------------------------------------------- */

.cs-site--provider #main-content .container {
	/* 90/1440 against Divi's 80/1080. At a 1440 viewport that is 1296px of
	   content against 1080 — the cap was what bound, not the percentage, so
	   raising only `width` would have changed nothing above 1350px. */
	width: 90%;
	max-width: 1440px;
}
