:root {
    --bs-primary: #e11d2e;
    --fsn-navy: #0b1e4a;

    /* The SAME colour as `--fsn-navy`, as channels. It exists only so a tint of
       the brand navy can be expressed as `rgba(var(--fsn-navy-rgb), .08)`.
       `color-mix()` would avoid the duplicate, but it is unsupported in the
       browser baseline this build already targets with `@supports not
       (container-type: inline-size)`. Keep the two values identical. */
    --fsn-navy-rgb: 11, 30, 74;

    /* FSN "complete" green — UI-CREATE-WO-SIDEBAR-01.
       `--bs-success` CANNOT be used for this. In this Metronic build it resolves
       to #1B84FF, a BLUE, so a success mark drawn from it reads as a link and
       lands on the same Field Nation blue this project deliberately avoids.
       This value is not new: #0F7A34 is the accessible green already used by the
       admin pagination rule further down this file, where #17C653 measured only
       2.27:1 on white and #0F7A34 reaches 5.45:1. It is promoted to a token so
       the completion mark and that rule cannot drift apart. */
    --fsn-success: #0f7a34;

    /* DASHBOARD CHART TOKENS — UI-DASHBOARD-CLIENT-CONTRACTOR-01.

       Two series colours, in a FIXED order. They live here, with every other FSN
       colour decision, so `resources/js/fsn-dashboard-charts.js` reads a token
       instead of owning a palette.

       `--fsn-navy` CANNOT be a chart fill. Measured against the chart surface it
       fails both the lightness band and the chroma floor — it reads as a dark
       neutral rather than a data colour. `--fsn-chart-series-1` is a lighter,
       more chromatic step of the SAME blue family, chosen because it is the
       nearest step that passes. `--fsn-chart-series-2` is `--bs-primary`, the FSN
       red, unchanged.

       The pair was validated, not judged by eye: lightness band, chroma floor,
       colour-vision separation across all pairs, and contrast against both the
       light and the dark chart surface. It passes every check in BOTH modes,
       which is why the series colours do not change with the theme below. The
       measured output is recorded in
       `docs/context/ui-dashboard-client-contractor-implementation-review.md` §4.

       This is NOT a second theme palette and it introduces no new red. A chart
       never draws more than two series; a composition with more than two
       categories is drawn as a single-hue bar chart, precisely because no
       accessible categorical palette of three or more could be built from the
       FSN colours. */
    --fsn-chart-series-1: #2f6fd0;
    --fsn-chart-series-2: #e11d2e;
    --fsn-chart-grid: rgba(120, 130, 150, 0.25);
    --fsn-chart-ink: #4b5675;
}

/* The chart grid and tick text are theme tokens: they sit ON the surface, so
   they must follow it. The two SERIES colours deliberately do not — both already
   pass against the dark surface, and repainting a classification on a theme
   change would break "colour follows the entity". */
[data-bs-theme="dark"] {
    --fsn-chart-grid: rgba(150, 160, 180, 0.22);
    --fsn-chart-ink: #9a9cae;
}

/* ============================================================================
   🔴 --bs-text-primary — THE TOKEN `--bs-primary` ABOVE DID NOT COVER, AND THE
   ONE EVERY "PRIMARY-COLOURED TEXT" UTILITY ACTUALLY READS.

   ## What was measured

   `--bs-primary` is overridden at the top of this file. `--bs-text-primary` is a
   SEPARATE Metronic token and was never overridden, so it kept the packaged
   green:

       [data-bs-theme=light]  --bs-text-primary: #17C653
       [data-bs-theme=dark]   --bs-text-primary: #00A261

   Measured in the project's own resolver, through the real cascade:

       .text-primary        -> #17c653 on #ffffff   2.27:1    FAILS AA
       .text-hover-primary  -> #17c653 on #ffffff   2.27:1    FAILS AA (on hover)

   🔴 THE RESTING STATE IS THE LARGER HALF AND IT WAS NOT THE ONE REPORTED.
   `text-primary` is used **57 times** in the views and is green in its RESTING
   state; `text-hover-primary` is used **26 times** and is green only on hover.
   Both are the same token and the same 2.27:1.

   Twenty-eight rules in `style.bundle.css` read this token, including
   `.text-primary`, `.text-hover-primary:hover`, `.text-active-primary.active`
   and the `btn-*-primary` text and icon families.

   ## Why THIS value, and not FSN's own `--bs-primary`

   `#e11d2e` is the obvious choice and it FAILS. Measured against every ground
   this token actually lands on:

       ground                      #e11d2e     #b30f1c
       white         #ffffff        4.75 OK     7.00 OK
       primary tint  #DFFFEA        4.44 FAIL   6.54 OK
       grey-100      #F9F9F9        4.52 OK     6.64 OK
       light panel   #F5F8FA        4.46 FAIL   6.56 OK

   The tint is not hypothetical: `livewire/messages/header-dropdown.blade.php`
   puts `text-hover-primary` on a row that carries `bg-light-primary` when the
   conversation is unread. A value chosen on white alone ships a failure there.

   `#b30f1c` IS NOT A NEW COLOUR. `fsn-app-shell.css` already chose it for
   exactly this ground and recorded exactly this figure — "badge-light-primary:
   #E11D2E on #DFFFEA measured 4.44:1 -> #B30F1C gives 6.54:1". Reusing it adds
   no fourth red and keeps the two rules from drifting apart.

   ## The dark theme is scoped separately BECAUSE ONE VALUE CANNOT SERVE BOTH

       ground                      #b30f1c     #ff7a84
       dark body     #15171C        2.56 FAIL   7.15 OK
       dark tint     #1F212A        2.29 FAIL   6.39 OK
       dark grey     #1B1C22        2.43 FAIL   6.77 OK

   Metronic scopes this token under `[data-bs-theme=light]` and
   `[data-bs-theme=dark]`, so these mirror those selectors exactly: wherever the
   packaged value applies, the FSN value applies and wins on source order.

   `ThemeTextPrimaryContrastTest` measures both values against every ground.
   ============================================================================ */
[data-bs-theme="light"] {
    --bs-text-primary: #b30f1c;
}

[data-bs-theme="dark"] {
    --bs-text-primary: #ff7a84;
}

/* THE ACTIVE UNDERLINE TAB, DARK THEME ONLY.

   Lives here and NOT in the fsn-app-shell palette block: that block is asserted
   admin-only — every selector in it must begin `body.fsn-app-shell .wrapper` —
   and `BL-QA-01` corrected finding F4 precisely because an unscoped rule there
   leaked onto the Client and Contractor tab rows. This correction is FOR those
   tab rows, so it belongs in the application-wide sheet.

   `BL-FEAT-01` reported `--bs-primary` #E11D2E as failing AA on dark at 3.77:1
   and left it for its own pass. This is that pass, and the measurement changed
   the answer.

   🔴 THE TOKEN CANNOT BE FIXED, BECAUSE IT PLAYS TWO ROLES THAT MOVE IN OPPOSITE
   DIRECTIONS. `style.bundle.css` reads `var(--bs-primary)` as `color` 97 times
   and as `background-color` 97 times. Lightening it for dark fixes the text and
   breaks white-on-primary; darkening it does the reverse. Five candidates were
   measured and NONE clears 4.5:1 in both roles:

       value      text on dark #15171C     white on it
       #ff7a84         7.15 OK                2.51 FAIL
       #ff5c6a         5.97 OK                3.00 FAIL
       #f0505f         5.14 OK                3.49 FAIL
       #e8455a         4.64 OK                3.87 FAIL
       #e11d2e         3.77 FAIL              4.75 OK

   So the token is left alone and the TEXT role is corrected where it renders.
   Of the three text-role selectors in the bundle, only `.nav-line-tabs` appears
   in FSN at all — 9 views, both Work Order tab rows among them. `.alert-primary`
   and `.menu-primary` appear in zero views.

   `--bs-text-primary` is the token Metronic already provides for the text role,
   and FSN set it in both themes. Reading it here introduces no new colour and
   inherits that guard.

   🔴 DARK ONLY. The light theme keeps #E11D2E at 4.75:1, unchanged, because
   nothing about it failed. */
[data-bs-theme="dark"] .nav-line-tabs .nav-item .nav-link.active,
[data-bs-theme="dark"] .nav-line-tabs .nav-item .nav-link:hover:not(.disabled) {
    color: var(--bs-text-primary);
}

/* ADMIN SIDEBAR — FSN navy brand background (uses the --fsn-navy variable; never a
   hardcoded hex). Replaces the previous Bootstrap `bg-dark` on the admin aside. */
.fsn-admin-aside {
    background-color: var(--fsn-navy);
}

/* ============================================================================
   UI-HDR-ALIGN-01 — ONE SHARED PAGE-SHELL HORIZONTAL GUTTER

   DEFECT. The Client and Contractor shell mixed two different horizontal
   container strategies inside one page:

     - the logo/topbar row, the navigation row, the page-title/breadcrumb
       toolbar and the footer used Bootstrap `.container-xxl`, which is
       CENTRED (`margin-inline: auto`) and capped at `max-width: 1320px`
       from 1400px up;
     - the main content column used `.container-fluid px-10`, which is
       FULL WIDTH with a 2.5rem gutter.

   At a 1920px viewport that produced a 289.9px left offset and a matching
   289.9px right offset between the header regions and the Flight Board card.
   The two regions never shared a content boundary at ANY width.

   CANONICAL PAGE-SHELL BOUNDARY. The outer edge of this container is the one
   left/right boundary for the whole shell: logo, navigation, page title,
   breadcrumb, main content and footer all resolve to it. A card's own inner
   padding is separate and is not part of this contract.

   GUTTER VALUES. Both values are the values the shell ALREADY used, so no new
   spacing is invented:

     - below 992px  -> 15px, the Metronic mobile shell gutter that the header
       and the toolbar already used. The header keeps its existing mobile
       spacing, so the logo and the right-side actions do not lose room.
     - 992px and up -> 2.5rem, the gutter the main content column already used.
       The Flight Board content therefore does NOT move on desktop; only the
       header regions move to meet it.

   `padding-inline` is logical, so the gutter stays correct under `dir="rtl"`.

   NO max-width and NO centring: the header must use the full responsive width
   inside the gutters, which is what the content column already did.
   ============================================================================ */
.fsn-page-container {
    --fsn-page-gutter: 15px;

    width: 100%;
    max-width: none;
    margin-inline: auto;
    padding-inline: var(--fsn-page-gutter);
}

@media (min-width: 992px) {
    .fsn-page-container {
        --fsn-page-gutter: 2.5rem;
    }
}

/* FSN BUTTON SYSTEM */

/* Primary action */
.btn-fsn-primary {
    background-color: var(--bs-primary);
    border-color: var(--bs-primary);
    color: #ffffff;
    font-weight: 600;
    padding: 14px 32px;
    border-radius: 6px;
    box-shadow: 0 6px 18px rgba(225, 29, 46, 0.35);
}

/* UI-CLIENT-QUOTES-01: this hover was a private near-navy hex that did not match
   the `--fsn-navy` token used everywhere else. It now reads the token, so ONE
   value defines FSN navy across the application. */
.btn-fsn-primary:hover,
.btn-fsn-primary:focus,
.btn-fsn-primary:active {
    background-color: var(--fsn-navy);
    border-color: var(--fsn-navy);
    color: #ffffff;
}

/* Secondary action */
.btn-fsn-secondary {
    background-color: rgba(255, 255, 255, 0.12);
    border: 1px solid rgba(255, 255, 255, 0.35);
    color: #ffffff;
    font-weight: 500;
    padding: 14px 32px;
    border-radius: 6px;
}

.btn-fsn-secondary:hover {
    background-color: rgba(255, 255, 255, 0.2);
    color: #ffffff;
}

.btn-fsn-nav {
    background-color: #ffffff;
    color: var(--fsn-navy);
    border: none;
    font-weight: 600;
    padding: 8px 22px;
    border-radius: 6px;
}

.btn-fsn-nav:hover {
    background-color: var(--bs-primary);
    color: #ffffff;
}

.bg-primary {
    background-color: var(--bs-primary) !important;
}

.landing-dark-bg {
    background-color: var(--fsn-navy);
    background-image: none !important;
}

.landing-clients img {
    opacity: 0.8;
    transition: opacity 0.2s ease;
}

.landing-clients img:hover {
    opacity: 1;
}

.fsn-hero:hover .landing-clients img {
    opacity: 0.75;
}

/* LANDING HEADER STICKY FIX */

.landing-header .btn-fsn-nav {
    border-radius: 8px;
    padding: 10px 22px;
    font-weight: 600;
}

/* When sticky mode is ON (Metronic adds this attribute on body) */
[data-kt-sticky-landing-header="on"] .landing-header .menu .menu-link {
    color: var(--fsn-navy) !important;
    font-weight: 600;
}

[data-kt-sticky-landing-header="on"] .landing-header .menu .menu-link:hover {
    color: var(--bs-primary) !important;
}

/* active item styling */
[data-kt-sticky-landing-header="on"] .landing-header .menu .menu-link.active {
    color: var(--bs-primary) !important;
    background-color: rgba(225, 29, 46, 0.08) !important;
    border-radius: 8px;
}

.landing-header .fsn-brand {
    margin-right: 48px;
    /* creates clean gap before nav */
}

@media (max-width: 991px) {
    .landing-header .fsn-brand {
        margin-right: 24px;
    }
}

/* Make Select2 look like form-floating */
.form-floating > .select2-container {
    width: 100% !important;
}

.form-floating > .select2-container .select2-selection {
    height: calc(3.5rem + 2px);
    padding: 1.625rem 0.75rem 0.625rem;
    border-radius: 0.475rem;
    border-color: #e1e3ea;
    background-color: #f5f8fa;
}

.form-floating > .select2-container--bootstrap-5 .select2-selection__rendered {
    line-height: 1.25;
}

.form-floating > label {
    z-index: 1;
}

/* Focus state */
.select2-container--bootstrap-5.select2-container--focus .select2-selection {
    border-color: #3e97ff;
    box-shadow: 0 0 0 0.25rem rgba(62, 151, 255, 0.25);
}

/* TinyMCE Overrides */
.fsn-tinymce-wrapper {
    border-radius: 0.475rem;
    overflow: hidden;
}

.fsn-tinymce-wrapper .tox-tinymce {
    border-radius: 0.475rem !important;
    border-color: var(--bs-gray-300) !important;
}

.fsn-tinymce-wrapper .tox .tox-toolbar,
.fsn-tinymce-wrapper .tox .tox-toolbar__primary,
.fsn-tinymce-wrapper .tox .tox-toolbar__overflow {
    background: var(--bs-body-bg) !important;
}

.fsn-editor-content {
    line-height: 1.6;
    white-space: normal;
    overflow-wrap: break-word;
}

.fsn-editor-content p:last-child {
    margin-bottom: 0;
}

.fsn-editor-content ul,
.fsn-editor-content ol {
    padding-left: 1.5rem;
    margin-bottom: 1rem;
}

.fsn-editor-content h1,
.fsn-editor-content h2,
.fsn-editor-content h3,
.fsn-editor-content h4,
.fsn-editor-content h5,
.fsn-editor-content h6 {
    margin-top: 0;
    margin-bottom: 0.75rem;
}

/* =========================================================
   WORK SCOPE EDITOR — UI-CLIENT-QUOTES-01

   Two surfaces share one appearance: `.fsn-editor-content` is the Tiptap
   editing surface, `.fsn-rich-text` is the READ rendering of the stored,
   sanitized value. They are styled together so what a Client writes is what a
   Contractor sees.

   PLACEMENT: this block must stay ABOVE the two admin marker comments further
   down this file. Each of those markers starts a region that a test reads to
   the end of the file, requiring every selector in it to be admin-shell-scoped.
   These selectors are editor-scoped, not shell-scoped, so they belong here.

   Do not repeat either marker's wording in a comment: the tests locate their
   region with the FIRST match of the marker text, so quoting it above the real
   marker silently moves the region and fails the scan.
========================================================= */

.fsn-editor-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.25rem;
    padding: 0.5rem;
    border: 1px solid var(--bs-gray-300);
    border-bottom: 0;
    border-radius: 0.475rem 0.475rem 0 0;
    background: var(--bs-gray-100);
}

.fsn-editor-btn {
    min-width: 2rem;
    padding: 0.25rem 0.5rem;
    line-height: 1.2;
    border: 1px solid transparent;
}

.fsn-editor-btn:hover {
    background: var(--bs-gray-200);
}

/* The pressed state must be visible without relying on colour alone. */
.fsn-editor-btn.active {
    background: var(--bs-primary);
    border-color: var(--bs-primary);
    color: #fff;
}

.fsn-editor-select {
    width: auto;
    padding-top: 0.25rem;
    padding-bottom: 0.25rem;
}

.fsn-editor-separator {
    width: 1px;
    height: 1.5rem;
    margin: 0 0.25rem;
    background: var(--bs-gray-300);
}

/* ── Compact toolbar popovers — UI-CLIENT-QUOTES-01 ─────────────────────
   The text-colour and highlight controls were a native `<input type="color">`
   plus a caption and a clear button, which occupied a large part of the toolbar
   and opened the operating system's full colour dialog. Both are now one small
   toolbar button that opens a small anchored panel.

   The panel is positioned against its OWN wrapper, never against the viewport,
   so it travels with the toolbar at every width and cannot be left behind when
   the toolbar wraps. */
.fsn-editor-pop-wrap {
    position: relative;
    display: inline-flex;
}

.fsn-editor-pop {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    /* Above the Bootstrap modal that hosts the editor. */
    z-index: 1080;
    min-width: 11rem;
    padding: 0.6rem;
    border: 1px solid var(--bs-gray-300);
    border-radius: 0.475rem;
    background: var(--bs-body-bg);
    box-shadow: 0 4px 16px rgba(0, 0, 0, .14);
}

.fsn-editor-pop[hidden] { display: none; }

/* STAGING-REVIEW-B3 BL-2a: never wider than the screen less its gutters; the
   editor script moves an open panel left when it would pass the right edge. */
.fsn-editor-pop-link { min-width: min(17rem, calc(100vw - 1rem)); max-width: calc(100vw - 1rem); }

.fsn-editor-pop-title {
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .3px;
    color: var(--bs-gray-500);
    margin-bottom: 0.4rem;
}

.fsn-editor-pop-actions {
    display: flex;
    gap: 0.4rem;
    margin-top: 0.5rem;
}

.fsn-editor-pop-error {
    font-size: 0.75rem;
    color: var(--bs-danger);
    margin-top: 0.3rem;
    min-height: 0.9rem;
}

/* A fixed six-column grid, so the palette is a small square block rather than
   a wide strip. */
.fsn-editor-swatches {
    display: grid;
    grid-template-columns: repeat(4, 1.4rem);
    gap: 0.35rem;
}

.fsn-editor-swatch {
    width: 1.4rem;
    height: 1.4rem;
    padding: 0;
    border: 1px solid var(--bs-gray-400);
    border-radius: 0.25rem;
    cursor: pointer;
}

.fsn-editor-swatch:hover { transform: scale(1.12); }

.fsn-editor-swatch:focus-visible {
    outline: 2px solid var(--fsn-navy);
    outline-offset: 1px;
}

/* The default swatch states "no stored colour" with a diagonal strike, so it
   never reads as a white colour choice. */
.fsn-editor-swatch-clear {
    background: linear-gradient(
        to top left,
        transparent calc(50% - 1px),
        var(--bs-danger) calc(50% - 1px),
        var(--bs-danger) calc(50% + 1px),
        transparent calc(50% + 1px)
    ), #ffffff;
}

.fsn-editor-swatch-icon {
    font-size: 0.9rem;
    font-weight: 700;
}

.fsn-editor-surface {
    border: 1px solid var(--bs-gray-300);
    border-radius: 0 0 0.475rem 0.475rem;
    background: var(--bs-body-bg);
}

.fsn-editor-content {
    min-height: 14rem;
    max-height: 28rem;
    overflow-y: auto;
    padding: 0.75rem 1rem;
}

.fsn-editor-content:focus,
.fsn-editor-content:focus-visible {
    outline: 0;
}

/* --- Shared document appearance: editing surface and read rendering --- */

.fsn-rich-text {
    line-height: 1.6;
    overflow-wrap: break-word;
}

.fsn-rich-text p:last-child {
    margin-bottom: 0;
}

.fsn-rich-text ul,
.fsn-rich-text ol {
    padding-left: 1.5rem;
    margin-bottom: 1rem;
}

.fsn-editor-content blockquote,
.fsn-rich-text blockquote {
    margin: 0 0 1rem;
    padding: 0.25rem 0 0.25rem 1rem;
    border-left: 3px solid var(--bs-gray-300);
    color: var(--bs-gray-700);
}

/* The `pre` and `code` rules that stood here are removed with the Code and
   Code Block features (UI-CLIENT-QUOTES-01). Neither element can be produced by
   the editor or survive the server allow-list, so the rules had no subject. */

.fsn-editor-content hr,
.fsn-rich-text hr {
    margin: 1rem 0;
    border: 0;
    border-top: 1px solid var(--bs-gray-300);
}

.fsn-editor-content mark,
.fsn-rich-text mark {
    padding: 0.05em 0.15em;
    border-radius: 0.2rem;
}

.fsn-editor-content a,
.fsn-rich-text a {
    color: var(--bs-primary);
    text-decoration: underline;
}

/* --- Task lists ---
   The stored markup carries NO checkbox input: `App\Support\Html\WorkScopeHtml`
   drops every form control, so the box is drawn from `data-checked` instead.
   That keeps a form element out of every reader's document. */

.fsn-editor-content ul[data-type='taskList'],
.fsn-rich-text ul[data-type='taskList'] {
    padding-left: 0;
    list-style: none;
}

.fsn-editor-content ul[data-type='taskList'] li,
.fsn-rich-text ul[data-type='taskList'] li {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
}

.fsn-rich-text ul[data-type='taskList'] li::before {
    content: '☐';
    flex: 0 0 auto;
    line-height: 1.6;
}

.fsn-rich-text ul[data-type='taskList'] li[data-checked='true']::before {
    content: '☑';
}

.fsn-rich-text ul[data-type='taskList'] li[data-checked='true'] {
    color: var(--bs-gray-600);
    text-decoration: line-through;
}

/* --- Tables --- */

.fsn-editor-content table,
.fsn-rich-text table {
    width: 100%;
    margin-bottom: 1rem;
    border-collapse: collapse;
    table-layout: fixed;
    overflow: hidden;
}

.fsn-editor-content th,
.fsn-editor-content td,
.fsn-rich-text th,
.fsn-rich-text td {
    padding: 0.4rem 0.6rem;
    border: 1px solid var(--bs-gray-300);
    vertical-align: top;
}

.fsn-editor-content th,
.fsn-rich-text th {
    background: var(--bs-gray-100);
    font-weight: 600;
    text-align: start;
}

/* A wide table scrolls inside its own region — the page body never scrolls
   sideways (CLAUDE.md §9D). */
.fsn-rich-text {
    overflow-x: auto;
}

/* Tiptap column resize handle and cell selection, editing surface only. */
.fsn-editor-content .selectedCell:after {
    content: '';
    position: absolute;
    inset: 0;
    background: rgba(27, 132, 255, 0.15);
    pointer-events: none;
}

.fsn-editor-content th,
.fsn-editor-content td {
    position: relative;
}

.fsn-editor-content .column-resize-handle {
    position: absolute;
    right: -2px;
    top: 0;
    bottom: 0;
    width: 4px;
    background: var(--bs-primary);
    pointer-events: none;
}

@media (max-width: 767.98px) {
    .fsn-editor-toolbar {
        gap: 0.15rem;
        padding: 0.35rem;
    }

    .fsn-editor-separator {
        display: none;
    }
}

/* =========================================================
   Global Scrollspy Navigation
   Usage:
   - Add data-scrollspy-nav on nav wrapper
   - Add .fsn-step-item + data-scrollspy-link + data-target="section-id" on links
   - Add matching id="section-id" on sections
========================================================= */

html {
    scroll-behavior: smooth;
    scroll-padding-top: 90px;
}

[data-scrollspy-link] {
    text-decoration: none;
    color: inherit;
}

/* The `.fsn-step-item.is-active` rules that stood here are removed by
   UI-CREATE-WO-SIDEBAR-01. That class no longer exists: the Create Work Order
   navigator was rebuilt as `.fsn-wo-nav-item`, styled in the page itself. The
   rules had no subject left.

   ⚠ RECORDED, NOT FIXED. The `[data-scrollspy-link]` rules below were ALREADY
   dead before this boundary — no markup in `resources/` or `app/` carries that
   attribute. They are unrelated to this change and are left alone per
   `CLAUDE.md` §0G-7. They also hardcode the Field Nation blue `#1b84ff`, which
   is why they should be removed by whichever boundary owns them. */
[data-scrollspy-link].is-active {
    background: #f0f4ff;
    border-left-color: #1b84ff;
}

[data-scrollspy-link].is-active .fsn-step-label {
    color: #1b84ff;
    font-weight: 600;
}

[data-scrollspy-link].is-active .fsn-step-icon {
    background: #1b84ff;
    color: #fff;
}

/* ──────────────────────────────────────────────────────────────────────────
   Admin sidebar desktop collapse (hotfix).
   A body class `admin-aside-minimize` (toggled + persisted by assets/js/admin-ui.js)
   shrinks the aside to a clean icon rail. The aside is a rigid flex item (Metronic
   `flex-row-auto`), so narrowing it lets the content wrapper reflow automatically —
   no fixed positioning / margin math. `overflow: hidden` clips any label spill so
   nothing ghosts over the page content. Desktop only (lg+); mobile KTDrawer untouched.
   ────────────────────────────────────────────────────────────────────────── */
.fsn-admin-aside {
    transition: width .3s ease;
}

/* Mini logo shows only when the rail is collapsed. */
.fsn-admin-aside .fsn-logo-mini {
    display: none;
}

/* The square mark, cropped out of the wide lockup.

   `logo-white-mark.svg` is 153x40 units: a 40x40 bracket frame holding the mark,
   then the wordmark from x=53. `cover` scales the SVG so its height fills the 42px
   box (making it ~161px wide), and `left center` then shows the leftmost 42px —
   which is the square mark and nothing else.

   The box was 34px until the collapse control moved out of the logo row. Both
   numbers below are 42px; this comment said 34px until BL-CLOSE-02 corrected it. A
   comment that contradicts the rule it sits on top of is worse than no comment,
   because it is read as the intent and the rule is read as the mistake.

   This reuses the file the expanded logo already loads, so the browser has it
   cached and the two states cannot drift apart. */
.fsn-admin-aside .fsn-logo-mini-mark {
    width: 42px;
    height: 42px;
    object-fit: cover;
    object-position: left center;
}

/* 🔴 THE COLLAPSE CONTROL SITS ON THE BOUNDARY, OUTSIDE THE RAIL.

   It used to be a flex child of the logo row, where `justify-content: center`
   centred the PAIR: the mark landed at 23px while the icon column below centres at
   40px, so the logo read as misaligned. With the control out of that row the mark
   centres on its own and lines up with the icons.

   `fixed`, not `absolute`: the aside is already `aside-fixed`, and the rail sets
   `overflow-x: hidden` when collapsed — a control straddling the edge must not be
   clipped by the element it straddles.

   `left` transitions on the same .3s curve as the rail's width, so the button
   travels with the edge instead of jumping when the state changes. */
.fsn-aside-toggle.btn.btn-icon {
    position: fixed;
    top: 26px;
    left: 275px;
    transform: translateX(-50%);
    z-index: 101;
    width: 28px;
    height: 28px;
    min-width: 28px;
    padding: 0;
    border-radius: 50%;
    background: #ffffff;
    /* Metronic's `.btn` zeroes the border and the shadow, so these two carry
       `!important`. Without them the control is a white disc straddling a white
       content column and a navy rail - visible on one half and not the other. */
    border: 1px solid var(--bs-gray-300) !important;
    box-shadow: 0 2px 6px rgba(0, 0, 0, .12) !important;
    transition: left .3s ease, background-color .15s ease;
}

.fsn-aside-toggle.btn.btn-icon:hover {
    background: var(--bs-gray-100);
    border-color: var(--bs-gray-400);
}

.fsn-aside-toggle i {
    color: var(--bs-gray-700);
}

#kt_admin_aside_toggle i {
    transition: transform .3s ease;
}

#kt_admin_aside_toggle.active i {
    transform: rotate(180deg);
}

@media (min-width: 992px) {
    /* Rigid width + clip so the collapsed rail never overlaps page content. */
    .fsn-admin-aside {
        overflow-x: hidden;
    }

    body.admin-aside-minimize .fsn-admin-aside {
        width: 80px !important;
        min-width: 80px !important;
    }

    /* The control rides the rail's edge. Same duration as the width transition. */
    body.admin-aside-minimize .fsn-aside-toggle.btn.btn-icon {
        left: 80px;
    }

    /* Logo area: swap the full lockup for the square brand mark, centred.

       It said `mini "FSN" placeholder` until BL-CLOSE-02. The rail did once render
       the literal text FSN, but BL-CLOSE-01 replaced that with the real mark cropped
       from `logo-white-mark.svg`. The word "placeholder" invited the next reader to
       replace something that is already finished. */
    body.admin-aside-minimize .fsn-admin-aside .aside-logo {
        justify-content: center !important;
        padding-left: 0 !important;
        padding-right: 0 !important;
    }

    body.admin-aside-minimize .fsn-admin-aside .fsn-logo-full {
        display: none !important;
    }

    body.admin-aside-minimize .fsn-admin-aside .fsn-logo-mini {
        display: inline-flex !important;
    }

    /* Hide labels + section headings cleanly; keep icons centered/visible. */
    body.admin-aside-minimize .fsn-admin-aside .menu-title,
    body.admin-aside-minimize .fsn-admin-aside .menu-heading,
    body.admin-aside-minimize .fsn-admin-aside .menu-content {
        display: none !important;
    }

    /* Clip the scroll area horizontally so nothing spills past the rail.
       The box itself must be pinned to the 80px rail: Metronic's .aside-menu keeps
       its expanded content min-width (~265px), so without this the width:100% items
       below resolve to ~265px and the 56px auto-centred link lands at x≈105 — outside
       the rail and clipped away (D-1). min-width overrides the flex min-width:auto. */
    body.admin-aside-minimize .fsn-admin-aside .aside-menu {
        width: 80px !important;
        min-width: 80px !important;
        max-width: 80px !important;
        overflow-x: hidden !important;
        padding-left: .25rem !important;
        padding-right: .25rem !important;
    }

    /* Constrain each item/link + its active pill to a fixed, centered box inside the
       80px rail so the white active background can never extend into page content. */
    body.admin-aside-minimize .fsn-admin-aside .menu,
    body.admin-aside-minimize .fsn-admin-aside .menu-item {
        width: 100% !important;
        max-width: 100% !important;
        padding-left: 0 !important;
        padding-right: 0 !important;
    }

    body.admin-aside-minimize .fsn-admin-aside .menu-item > .menu-link {
        width: 56px !important;
        max-width: 56px !important;
        min-width: 0 !important;
        margin-left: auto !important;
        margin-right: auto !important;
        padding: .6rem 0 !important;
        justify-content: center !important;
        white-space: nowrap;
        overflow: hidden;
    }

    body.admin-aside-minimize .fsn-admin-aside .menu-icon {
        margin-right: 0 !important;
        justify-content: center !important;
    }
}


/* Keep the floating action menu above the sticky header/aside. */
/* 🔴 `.fsn-header-panel` JOINS THIS RULE — the two hand-rolled header dropdowns
   were the only overlays that never got this treatment.

   ## The report

   Haris, 2026-09-02, with a screenshot: on the Work Order listing, for BOTH
   roles, the notifications dropdown and the messages dropdown are drawn UNDER
   the table header.

   ## The stacking chain, walked in a browser before anything was changed

   NO ANCESTOR CAPS ANYTHING. From either panel up to the document root there is
   exactly ONE stacking context — the panel itself. Every ancestor is
   `position: static` or `relative` with `z-index: auto`, so both panels compete
   directly in the ROOT stacking context. Raising the number is therefore the
   correct family of fix here, which is not true in general and was not assumed.

   `#kt_header` creates no context of its own: Metronic only makes it
   `position: fixed; z-index: 97` under `[data-kt-sticky-header=on]`, an attribute
   KTSticky adds after a scroll — and this listing does not scroll the window at
   all, so the header is `static`/`auto` permanently. The defect is not
   intermittent.

   ## What the panels were losing to, measured on the page

       .fsn-header-panel        z-index 3   <- both dropdowns
       thead th (centre)        z-index 3   <- TIE, and the table is later in the
                                              document, so the table wins
       thead th (corner cells)  z-index 4   <- outright win over the panel
       .fn-dropdown-panel       z-index 999 <- the listing's own filter menus

   Four header cells were probed with `elementFromPoint` inside the overlap and
   the TABLE HEADER painted on top of all four.

   ## Why 1065 and not a new number

   This rule already exists and already carries the reason. FSN raised every
   KTMenu dropdown to 1065 so a floating menu clears the header and the aside —
   which is why the user-profile menu and the row action menus were never
   affected. The two `.fsn-header-panel` overlays are the same class of thing and
   were simply missed. Adding the selector here keeps ONE layer for every header
   overlay rather than inventing a third.

   The `z-index-3` utility was removed from both Blade files at the same time, so
   the value has one owner instead of a markup class and a stylesheet rule
   disagreeing.

   `HeaderOverlayStackingTest` asserts the panels sit on this layer and that no
   page-content z-index reaches it. */
.menu-sub-dropdown,
.dropdown-menu,
.fsn-header-panel {
    z-index: 1065;
}

/* Wide modal for the contractor Request screen (UI reference C-2), which is a
   near-full-page dialog with three side-by-side columns. Metronic ships no
   max-width utility above .mw-1000px, so the width is defined here instead of
   inline. Below the lg breakpoint the columns stack and the dialog uses the
   normal responsive width. */
/* Wide workflow dialogs (Contractor Request, Contractor Counter Offer).
   Scoped to lg and up ON PURPOSE. Both dialogs also carry
   `modal-fullscreen-lg-down`, and this rule is more specific, so an unscoped
   `width` here beat the fullscreen rule and left a 3rem inset at every width.
   At 390px that made the dialog 36px narrower than the viewport and exposed the
   page behind it. Below lg the fullscreen rule must win. */
@media (min-width: 992px) {
    .modal-dialog.fsn-modal-wide {
        max-width: 1600px;
        width: calc(100% - 3rem);
    }
}

/* CQR-3 — Work Order tab rows.
   The primary tabs and the Quote lifecycle chips both spread across the content
   width and centre their labels. `nav-fill` does the distribution, so neither
   row hardcodes a tab count and both absorb a future tab.
   Each row keeps its own horizontal scroll region: below the point where the
   labels stop fitting, the row scrolls instead of pushing the page body wide
   (§9D). */
.fsn-wo-tabs,
.fsn-q-lifecycle {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
}

.fsn-wo-tabs::-webkit-scrollbar,
.fsn-q-lifecycle::-webkit-scrollbar {
    height: 4px;
}

/* Equal columns, not `nav-fill` flex distribution.
   `nav-fill` sizes items from their CONTENT, so Metronic's tab padding pushed
   the row 13 px past its container and showed a scrollbar on a row that fits.
   A grid with `grid-auto-columns: 1fr` gives every tab an identical share that
   can never exceed the container, and it absorbs a future tab with no change
   here — the column count follows the markup. */
.fsn-wo-tabs .nav,
.fsn-q-lifecycle .nav {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    width: 100%;
}

/* Each control fills its own column and centres its label. */
.fsn-wo-tabs .nav-link,
.fsn-q-lifecycle .nav-link {
    width: 100%;
    justify-content: center;
    text-align: center;
}

/* UI-CLIENT-WO-VIEW-01 — the last 13 px of phantom overflow.
   A grid item defaults to `min-width: auto`, so it can never shrink below its
   own min-content size. The equal columns above sized each tab at 613.26 px
   while the Quotes and Messages controls reported a 626 px min-content width
   (Metronic's nav-link padding plus the label), so those two items overflowed
   their column by 13 px. The overflow propagated to the scroll container and
   showed a scrollbar on a row that fits — measured at 1920 px:
   scrollWidth 1853 against clientWidth 1840.

   `min-width: 0` lets a grid item take the width its column assigns. The labels
   are short, so nothing truncates; the row simply stops overflowing. */
.fsn-wo-tabs .nav-item,
.fsn-q-lifecycle .nav-item {
    min-width: 0;
}

/* The 13 px itself: Metronic gives every tab after the first a 1rem inline
   margin (1rem = 13 px at the Metronic root). Inside a flex `nav-fill` row that
   margin is part of the distribution, but inside the equal-column grid above the
   column is already exact, so the margin pushed each later control 13 px past
   its own column — measured: the Quotes control's right edge at 1272 px against
   its column's 1259 px. The grid supplies the spacing, so the margin is removed.

   Scoped to these two rows only; every other Metronic nav keeps its margins. */
.fsn-wo-tabs .nav-item,
.fsn-q-lifecycle .nav-item,
.fsn-wo-tabs .nav-item .nav-link,
.fsn-q-lifecycle .nav-item .nav-link {
    margin-inline: 0;
}

/* =========================================================
   UI-CLIENT-QUOTES-01 — FSN THEME CORRECTIONS
   =========================================================

   Three corrections, all expressed through the EXISTING tokens at the top of
   this file. No second copy of a brand colour is introduced:

     --bs-primary  #e11d2e  FSN primary red
     --fsn-navy    #0b1e4a  FSN navy
   ========================================================= */

/* 1. The FSN primary-button hover contract: RED by default, NAVY on hover.

   `.btn-fsn-primary` (the Login button) already did this, but through a private
   near-navy hex rather than the navy token. That declaration is corrected at its
   own rule above, so one value defines FSN navy.

   `.btn-primary` is the semantic every other FSN primary action already uses
   (Create, Submit, Save, Assign), so the contract is applied once here instead
   of on each button.

   DELIBERATELY UNTOUCHED: danger, secondary, success, warning, info, light,
   dark, every `btn-outline-*`, and every `btn-light-*`. Their semantics are not
   "primary action" and must keep their own hover.

   `:disabled` and `.disabled` are excluded, so a disabled primary button keeps
   its muted rest state instead of appearing actionable on hover. Metronic's
   `:focus-visible` ring is untouched. */
/* Scoped to `.btn-primary` ONLY.

   Metronic drives its primary interaction states from `--bs-primary-active` and
   applies the value with `!important`:

     .btn.btn-primary:hover:not(.btn-active) { background-color: var(--bs-primary-active) !important }

   An earlier attempt overrode that TOKEN. It worked, but the token is also read
   by `.btn-outline-primary` and by the `.bg-primary-active` utility, so the
   change reached two semantics this boundary must not touch. The token override
   is withdrawn.

   The correct lever is a more specific rule on the filled primary button alone.
   Metronic's selector carries four class-level components; this one carries five
   (`.btn` + `.btn-primary` + `:not(:disabled)` + `:not(.disabled)` + `:hover`),
   and `fsn-overrides.css` loads after the bundle, so it wins the `!important`
   comparison on specificity rather than on source order alone.

   UNTOUCHED: `.btn-outline-primary` keeps its own hover, `.bg-primary-active`
   keeps its value, and every other semantic (danger, secondary, success,
   warning, info, light, dark) keeps its own token.

   `:disabled` and `.disabled` are excluded, so a disabled primary keeps its
   muted rest state. Metronic's `:focus-visible` ring is untouched. The rule is
   theme-independent: both themes use the same FSN navy. */
.btn.btn-primary:not(:disabled):not(.disabled):hover,
.btn.btn-primary:not(:disabled):not(.disabled):focus,
.btn.btn-primary:not(:disabled):not(.disabled):active {
    background-color: var(--fsn-navy) !important;
    border-color: var(--fsn-navy) !important;
    color: #ffffff !important;
}

/* 2. One reusable FSN chip.

   The Work Order title carried `badge-light-warning`, a yellow tint. Navy on
   white text measures 15.6:1, so the label is readable in both themes and needs
   no per-theme override. Use this class for a neutral identity label; it is NOT
   a status colour and must not replace a semantic badge. */
.badge.badge-fsn-navy {
    background-color: var(--fsn-navy);
    color: #ffffff;
}

/* STAGING-REVIEW-B3 card 38 — "not seen yet" on the flight board. A ring around a
   count that also SAYS "N new", so the state never rests on colour alone. Theme
   tokens, so it reads in light and dark mode. */
.badge.fsn-unseen-ring {
    background-color: var(--bs-body-bg);
    color: var(--bs-danger);
    box-shadow: 0 0 0 2px var(--bs-danger);
    font-weight: 600;
    width: max-content;
}

/* 3. The Quote lifecycle tabs.

   The active tab took Metronic's green (`--bs-link-color` is #17C653 in this
   build), which is a success colour on a control that states no success. The
   active tab now carries the navy surface; an inactive tab keeps the muted
   label, so the two stay distinguishable without relying on colour alone —
   the active tab is also the only one with a filled surface. */
.fsn-q-lifecycle .nav-link {
    color: var(--bs-gray-700);
    transition: background-color .12s ease-in-out, color .12s ease-in-out;
}

/* UI-QUOTE-MSG-ROUTES-01 — the SOLID navy fill was too dark and dominated a row
   that is navigation, not emphasis. The active tab now carries a soft tint of
   the SAME brand token with navy text, so the surface reads as selected without
   competing with the Quote cards below it.

   No new colour is introduced: `--fsn-navy-rgb` is `--fsn-navy` as channels.
   Contrast IMPROVES — navy #0B1E4A on the 8% tint measures far above the 4.5:1
   text minimum, where the previous white-on-navy pairing was the only thing
   carrying the state. Weight and tint now carry it together, so the state does
   not depend on colour alone. */
.fsn-q-lifecycle .nav-link:hover:not(.active) {
    background-color: var(--bs-gray-100);
    color: var(--fsn-navy);
}

.fsn-q-lifecycle .nav-link.active {
    background-color: rgba(var(--fsn-navy-rgb), .08);
    color: var(--fsn-navy);
    border-color: transparent;
    font-weight: 600;
}

.fsn-q-lifecycle .nav-link.active .badge {
    background-color: rgba(var(--fsn-navy-rgb), .12);
    color: var(--fsn-navy);
}

/* 4. The Quote card Contractor avatar ring.

   The reference draws a coloured status ring around the profile image. FSN uses
   its own primary red, never the Field Nation blue. Applied to the image and to
   the initials fallback, so both states carry the ring. */
/* UI-CLIENT-QUOTES-01 — the Quotes content grid and the profile panel.

   The lifecycle tabs stay full width above; this grid splits only the region
   BELOW them, so the panel is scoped to the Quotes content and is never a
   global sidebar. Below the lg breakpoint the columns stack — a normal
   responsive stack, not an invented offcanvas. */
.fsn-q-grid { display: grid; grid-template-columns: 1fr; gap: 20px; align-items: start; }
@media (min-width: 992px) { .fsn-q-grid { grid-template-columns: minmax(0, 1fr) 360px; } }

/* The panel is PROPORTIONAL above 1400, not a fixed 440.
   Browser QA measured the identity row at 1440 with a fixed 440px panel: the
   row needed 630px and had 606px, so it wrapped onto a second line — the exact
   defect this boundary corrects. `clamp()` gives the panel a smaller share at
   1440 and its full width at 1920 and above, so the card stays row-based at
   every required desktop width without shrinking the panel where it is not
   necessary. */
@media (min-width: 1400px) {
    .fsn-q-grid { grid-template-columns: minmax(0, 1fr) clamp(380px, 24vw, 440px); }
}

/* The Quote list is the CONTAINER the cards measure themselves against.
   A card must react to the width of this column, not to the viewport, because
   collapsing the profile panel changes the column width while the viewport
   stays the same. */
.fsn-q-list { container-type: inline-size; container-name: fsn-quote-list; }

.fsn-q-aside { min-width: 0; position: relative; }

/* ── The collapsible profile panel — UI-CLIENT-QUOTES-01 ────────────────
   Reference: `Client/quote-listing-profile-sidebar-ui.png`.

   The panel collapses toward the RIGHT and the Quote list takes the released
   width. The animation runs on the GRID COLUMN and on the panel's own
   transform. The page body is never repositioned, and no absolute viewport
   coordinate is used anywhere, so the control cannot drift out of the content
   area at an untested width. */
@media (min-width: 992px) {
    /* The exposed drawer edge — SUPERSEDES the zero-width collapse
       (UI-QUOTE-SIDEBAR-DRAWER-01, 2026-08-14).

       The collapsed column used to be `0px`: the panel vanished completely and
       only the circular toggle remained, which read as a control floating in the
       Quote list rather than a drawer. Haris supplied
       `quote-listing-profile-sidebar-ui-change.png`, which shows the SAME drawer
       still present at the right edge with a narrow white strip exposed and the
       toggle sitting on that strip's left boundary.

       So the column now collapses to a peek instead of to nothing. Everything
       else about the mechanism is unchanged: one grid, one panel, one Alpine
       boolean, no Livewire request, no second element. */
    .fsn-q-grid { --fsn-drawer-peek: 28px; transition: grid-template-columns 260ms ease; }

    .fsn-q-grid.fsn-q-grid-collapsed {
        grid-template-columns: minmax(0, 1fr) var(--fsn-drawer-peek);
    }

    /* The body is clipped by the narrow column, so it slides out of view
       rather than being removed. Keeping it mounted is what preserves the
       selected Contractor. */
    .fsn-q-panel-body {
        overflow: hidden;
        /* `visibility` switches with no delay when OPENING, so the panel is
           focusable as soon as it starts to appear. */
        transition: transform 260ms ease, opacity 200ms ease, visibility 0s linear 0s;
    }

    /* THE VISIBLE STRIP is the aside's own surface, painted only while
       collapsed. It reuses the panel's border colour and radius, so the strip
       reads as the same drawer seen edge-on rather than as a new element.

       The height is stated explicitly because the content leaves the flow
       below; without it the strip would collapse to nothing. It matches
       `.fsn-profile-scroll`'s own max-height, so the strip is exactly as tall as
       the panel it belongs to. */
    .fsn-q-grid-collapsed .fsn-q-aside {
        background: var(--bs-body-bg);
        border: 1px solid var(--bs-gray-200);
        border-right: 0;
        border-radius: 12px 0 0 12px;
        min-height: calc(100vh - 160px);
    }

    /* The collapsed panel is hidden from the KEYBOARD as well as the eye.
       `visibility: hidden` removes the subtree from the tab order natively, and
       it is scoped to this breakpoint — which an `inert` attribute could not be.
       Below `lg` the panel is an ordinary visible block and this never applies.

       It is also taken OUT OF FLOW. A `visibility: hidden` element still lays
       out, and at 28px wide its text would wrap into a column hundreds of pixels
       tall, which would then drive the strip's height. Absolute positioning
       inside the (already `sticky`, therefore positioned) aside removes that
       influence entirely, and `overflow: hidden` stops the hidden content from
       extending the drawer.

       The 260ms delay lets the slide and fade finish before the panel is
       hidden; without it the content would vanish instantly and the animation
       would not be seen. */
    .fsn-q-grid-collapsed .fsn-q-panel-body {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        overflow: hidden;
        transform: translateX(24px);
        opacity: 0;
        visibility: hidden;
        transition: transform 260ms ease, opacity 200ms ease, visibility 0s linear 260ms;
    }
}

/* Below `lg` the two columns stack, so the panel is a normal block and the
   collapse control is not offered: there is no side region to collapse. */
.fsn-q-panel-toggle { display: none; }

@media (min-width: 992px) {
    .fsn-q-panel-toggle {
        display: flex;
        align-items: center;
        justify-content: center;
        position: absolute;
        top: 50%;
        left: -17px;
        transform: translateY(-50%);
        z-index: 5;

        /* Circular, bordered, centred chevron — as the reference draws it.
           UI-CONTRACTOR-PROFILE-WORK-EXPERIENCE-01 reduced the visible circle
           from 34px to 30px. The CLICK TARGET is kept comfortable by the
           transparent ring below, so the control is easier to hit than the
           circle it draws. */
        width: 30px;
        height: 30px;
        padding: 0;
        border-radius: 50%;

        /* The ONE visual contract, in EVERY state: white interior, FSN primary
           red border, FSN primary red chevron. The red comes from the
           application's own `--bs-primary` token, so no second red hex exists.

           The interior is an explicit `#ffffff` rather than `--bs-body-bg`,
           because the requirement is a white circle in every state and in both
           themes. A red-bordered white disc stays legible on the dark surface.

           The previous rule filled the circle with red on hover and turned the
           chevron white. That is removed: the fill read as a pressed or
           destructive state on a control that only opens and closes a panel. */
        border: 1px solid var(--bs-primary);
        background: #ffffff;
        color: var(--bs-primary);
        box-shadow: 0 1px 4px rgba(0, 0, 0, .12);
        transition: box-shadow 150ms ease;
    }

    /* A comfortable hit area WITHOUT drawing a bigger circle. The ring is
       transparent and sits outside the border, so the control measures 30px to
       the eye and about 40px to the pointer. */
    .fsn-q-panel-toggle::after {
        content: '';
        position: absolute;
        top: -5px;
        right: -5px;
        bottom: -5px;
        left: -5px;
        border-radius: 50%;
    }

    /* Hover and active keep the SAME interior, border and icon. Only the
       elevation changes, so the control still answers the pointer. */
    .fsn-q-panel-toggle:hover,
    .fsn-q-panel-toggle:active {
        background: #ffffff;
        color: var(--bs-primary);
        border-color: var(--bs-primary);
        box-shadow: 0 2px 8px rgba(0, 0, 0, .18);
    }

    /* Keyboard focus keeps the SAME white interior and red chevron.

       The visible ring itself is NOT drawn here. The shared FSN treatment,
       `body.fsn-app-shell :focus-visible`, already gives every control a navy
       outline plus a white halo, and it is more specific than a rule on this
       class, so a local outline here would be dead CSS. Measured on a real
       keyboard Tab: `outline: 1.6px solid rgb(7, 20, 55)` with a 2px white
       halo. This rule only guarantees the fill never appears. */
    .fsn-q-panel-toggle:focus,
    .fsn-q-panel-toggle:focus-visible {
        background: #ffffff;
        color: var(--bs-primary);
    }

    /* The chevron scales with the smaller circle, and carries the FSN primary
       red EXPLICITLY.

       Inheriting from the button is not enough: Metronic gives `.ki-outline` its
       own colour, which measured as the grey `rgb(153, 161, 183)` and beat the
       inherited value. The icon is therefore coloured on its own selector, in
       every state, so the red chevron never depends on inheritance winning. */
    .fsn-q-panel-toggle > i,
    .fsn-q-panel-toggle:hover > i,
    .fsn-q-panel-toggle:active > i,
    .fsn-q-panel-toggle:focus > i,
    .fsn-q-panel-toggle:focus-visible > i {
        font-size: 13px;
        line-height: 1;
        color: var(--bs-primary);
    }
}

/* A viewer who asks for reduced motion gets the same states with no travel. */
@media (prefers-reduced-motion: reduce) {
    .fsn-q-grid,
    .fsn-q-panel-body { transition: none; }
}

/* ── The Quote card — UI-CLIENT-QUOTES-01 desktop correction ────────────
   Reference: `Client/quote-listing-ui.png`.

   ROOT CAUSE of the corrected defect. The card was one `flex-wrap` row whose
   single content column held the name, the id, the distance, the area and the
   phone as SEPARATE block children. Each value therefore began its own line at
   every width, so a 1920 px desktop rendered a tall mobile-shaped card that
   used a fraction of the list column.

   The fix separates the two questions the old markup conflated:

     * `.fsn-q-main` is the horizontal frame — avatar, content, actions;
     * `.fsn-q-identity` is ONE wrapping inline row, so the five identity values
       sit on one line and wrap only when the column truly cannot hold them.

   The card responds to the LIST COLUMN, not to the viewport, through a
   container query. That is what the boundary requires: when the profile panel
   collapses, the column grows and the card must use the released width
   immediately — a viewport media query cannot see that change. */
.fsn-q-card {
    border: 1px solid var(--bs-gray-200);
    border-radius: 12px;
    background: var(--bs-body-bg);
    /* NO `overflow: hidden` — UI-QUOTE-MSG-ROUTES-01.

       It used to be here, to make the rounded corners clip the tinted header
       strip. It also clipped the distance tooltip, which opens UPWARD out of the
       card: the top of the panel was cut off at the card edge, and browser QA
       caught it. A clipped tooltip is a defect against this boundary's
       requirement that the panel must not be clipped by the card.

       The strip now rounds its OWN top corners instead, so the card looks
       identical and nothing that escapes the card is cut. */
}

.fsn-q-card + .fsn-q-card { margin-top: 12px; }

/* ROW 0 — the commercial / classification strip. */
.fsn-q-strip {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 8px;
    padding: 10px 20px;
    background: var(--bs-gray-100);
    border-bottom: 1px solid var(--bs-gray-200);
    font-size: 13px;
    /* The card no longer clips, so the strip rounds its own top corners. 11px,
       not 12px: the strip sits INSIDE the card's 1px border, and an equal radius
       would leave a hairline of card background in each corner. */
    border-top-left-radius: 11px;
    border-top-right-radius: 11px;
}

.fsn-q-strip-terms { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; min-width: 0; }
.fsn-q-strip-pay { white-space: normal; }
.fsn-q-strip-aside { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }

/* A thin vertical divider, as the reference uses between strip groups. */
.fsn-q-sep { width: 1px; height: 14px; background: var(--bs-gray-300); display: inline-block; }

/* MAIN — avatar | content | actions. `center` puts the actions at the vertical
   centre of the card, as the reference does. */
.fsn-q-main {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 16px 20px;
}

.fsn-q-content { flex: 1 1 auto; min-width: 0; }

/* ROW 1 — the five identity values on ONE line.
   `column-gap` carries the rhythm; `row-gap` only applies once the row is
   genuinely too narrow, so wrapping stays controlled instead of automatic. */
.fsn-q-identity {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    /* 12px, not 16: browser QA at 1440 measured the row 24px short of fitting
       on one line. The five gaps recover 20px of that. */
    column-gap: 12px;
    row-gap: 4px;
}

.fsn-q-name { text-decoration: none; }

/* Each secondary fact is one non-breaking unit, so a value never splits across
   two lines while its neighbour stays on the first. */
.fsn-q-fact { font-size: 13px; white-space: nowrap; }

/* ── The distance panel — UI-WO-VIEW-QUOTE-ACTIONS-01 ──────────────────────
   The compact straight-line value opens a panel carrying the real routed
   driving distance, the straight-line figure, and the route on Google Maps.

   Hover AND `:focus-within` reveal it, so the keyboard reaches every row —
   including the directions link, which is a real link and must be tabbable.
   `:focus-within` also keeps the panel open while the link itself holds focus,
   which a plain `:focus` rule on the trigger would not.

   No JavaScript shows or hides it. Alpine's only job on this element is to ask
   the server for the routed figure the first time it is opened. */
.fsn-q-dist { position: relative; cursor: default; }
.fsn-q-dist:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: 2px; border-radius: 4px; }

/* ── Panel geometry — rebuilt from `quote-distance-tooltip.png`
   (UI-QUOTE-MSG-ROUTES-01).

   What the reference proves, and what each declaration below answers:

     opens ABOVE the distance value ......... `bottom: calc(100% + 8px)`
     left edge aligned to the value ......... `left: 0`
     plain rounded card, NO caret/arrow ..... no pseudo-element is drawn
     white surface, soft lifted shadow ...... `--bs-body-bg` + a wide, low-alpha
                                              shadow rather than a hard border
     hairline edge, not a drawn border ...... 1px at gray-200 strength
     roomy padding, generous row rhythm ..... 14px 16px, 12px gap
     figure semibold, label muted, same size  set on the rows in the Blade file
     link row in the primary link colour .... `.fsn-q-dist-link`

   The reference shows NO pointer, so none is drawn. Adding one would be
   invention, and a caret on a panel that flips position is also the part that
   most often ends up detached from its trigger. */
.fsn-q-dist-panel {
    display: none;
    position: absolute;
    bottom: calc(100% + 10px);
    /* CENTRED on the distance value, as the reference shows: in
       `quote-distance-tooltip.png` the panel's midpoint sits over the mileage
       group, not against its left edge. */
    left: 50%;
    transform: translateX(-50%);
    z-index: 20;
    /* `.fsn-q-fact` sets `nowrap` for the identity row; the panel is its own
       block and must wrap normally. */
    white-space: normal;
    min-width: 224px;
    /* Never wider than the viewport, whatever the card width is. The gutter
       keeps a panel anchored near the right edge off the screen edge itself. */
    max-width: min(280px, calc(100vw - 32px));
    padding: 14px 16px;
    flex-direction: column;
    gap: 12px;
    border: 1px solid var(--bs-gray-200);
    border-radius: 8px;
    background: var(--bs-body-bg);
    box-shadow: 0 8px 24px rgba(15, 23, 42, .14);
    font-size: 13px;
    line-height: 1.3;
    text-align: left;
}

.fsn-q-dist:hover .fsn-q-dist-panel,
.fsn-q-dist:focus-within .fsn-q-dist-panel { display: flex; }

/* ── THE HOVER BRIDGE — corrected 2026-08-14.
 *
 * The defect Haris reported: the panel opened, but moving the pointer toward it
 * closed it, so `See driving directions` could not be clicked.
 *
 * Cause, reproduced: the panel is a CHILD of the trigger, so hovering the panel
 * itself keeps `.fsn-q-dist:hover` true — but the 10px OFFSET between the two is
 * outside both boxes. Crossing it put the pointer over the card behind, the
 * hover was lost, and the panel vanished mid-travel.
 *
 * The fix is one transparent element that spans the offset, so the trigger and
 * the panel form ONE continuous hover region. It exists only while the panel is
 * displayed, so it can never block a click on the card underneath. No
 * JavaScript, no timers, no state to desynchronise. */
.fsn-q-dist-panel::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: -12px;
    height: 12px;
}

/* ── THE CARET — required by Haris on 2026-08-14.
 *
 * This SUPERSEDES the earlier finding that the reference draws no caret. The
 * reference image indeed shows none; the caret is a direct instruction, and it
 * is recorded as such rather than as screenshot parity.
 *
 * Drawn as a rotated square rather than a border triangle, so it inherits the
 * panel's own 1px edge on the two sides that face outward and reads as part of
 * the panel surface instead of a separate shape. */
.fsn-q-dist-panel::after {
    content: '';
    position: absolute;
    left: 50%;
    bottom: -6px;
    width: 11px;
    height: 11px;
    margin-left: -5.5px;
    background: var(--bs-body-bg);
    border-right: 1px solid var(--bs-gray-200);
    border-bottom: 1px solid var(--bs-gray-200);
    transform: rotate(45deg);
}

.fsn-q-dist-row { display: flex; align-items: center; gap: 8px; }
.fsn-q-dist-link { text-decoration: none; }
.fsn-q-dist-link:hover { text-decoration: underline; }

/* The directions link carried `text-primary`, but Metronic's own `a` colour
   (`--bs-link-color`, the green #17C653 in this build) won on specificity, so
   the row rendered GREEN — a success colour on a control that reports no
   success. Browser QA caught it while comparing against
   `quote-distance-tooltip.png`, whose link sits in the panel's single accent
   colour alongside the metric icons.

   FSN's informational accent is navy, and the icons above already use it, so the
   link joins them. Primary red stays reserved for the page's real actions. */
.fsn-q-dist-panel .fsn-q-dist-link,
.fsn-q-dist-panel .fsn-q-dist-link:hover {
    color: var(--fsn-navy);
}

/* The reference draws both metric icons in its own brand accent, not in a grey.
   FSN's informational accent is navy — the same decision the four Work Order
   summary icons already carry. Primary red stays reserved for actions, which is
   why the directions LINK below keeps `text-primary` and these icons do not. */
.fsn-q-dist-icon { color: var(--fsn-navy); flex: 0 0 auto; }

/* ── The message entry point — ONE class for BOTH surfaces
   (UI-QUOTE-MSG-ROUTES-01).

   The Quote card control and the Contractor profile panel control previously
   both carried `btn-light-primary`. Metronic fills that button with the solid
   primary on hover, and FSN's primary is the red #E11D2E — so pointing at the
   envelope turned it into a dark red block and the icon inverted to white. The
   icon became LESS legible at the exact moment the Client aimed at it.

   The replacement keeps the icon in the primary red at all times and dims only
   the SURFACE behind it, using the neutral gray already used for every other
   quiet hover in this file. The icon colour never changes, so contrast never
   drops. The two surfaces share this one class, so they cannot drift. */
.fsn-msg-icon {
    color: var(--bs-primary);
    background-color: transparent;
    border-color: transparent;
}

.fsn-msg-icon i,
.fsn-msg-icon .ki-duotone { color: inherit; }

.fsn-msg-icon:hover,
.fsn-msg-icon:focus,
.fsn-msg-icon.show {
    color: var(--bs-primary);
    background-color: var(--bs-gray-100);
    border-color: transparent;
}

/* Pressed reads as one step darker than hover, and still not as a fill. */
.fsn-msg-icon:active,
.fsn-msg-icon:not(.btn-active):active {
    color: var(--bs-primary) !important;
    background-color: var(--bs-gray-200) !important;
    border-color: transparent !important;
}

.fsn-msg-icon:focus-visible {
    outline: 2px solid var(--fsn-navy);
    outline-offset: 2px;
}

.fsn-msg-icon:disabled,
.fsn-msg-icon.disabled {
    color: var(--bs-gray-400);
    background-color: transparent;
    border-color: var(--bs-gray-200);
    opacity: 1;
}

/* ── The Quote card action pair — corrected 2026-08-14.
 *
 * Haris reported the message control as "too small and visually isolated"
 * beside `Review Offer`. Measured before the fix: a 35x35 icon with NO border
 * at all — the earlier pass removed `btn-light-primary` to kill the dark-red
 * hover and took the enclosing surface with it — next to a 104x35 filled
 * primary button. The icon read as a loose glyph rather than a control.
 *
 * The pair is now sized as a pair:
 *
 *   message control   38 x 38, a REAL 1px neutral border, 18px glyph
 *   Review Offer      38 tall, narrower padding so it stops dominating
 *
 * Both are 38 tall and share one baseline, so they read as two controls of the
 * same family. The icon keeps the FSN primary red and the approved subtle grey
 * hover — the dark-red fill does not return. The primary action stays the only
 * filled surface, so the icon gains presence without gaining emphasis.
 */
/* The selectors carry `.btn.btn-icon` deliberately: Metronic sizes this control
   with `.btn.btn-icon.btn-sm`, which outranks a two-class selector, so a
   shorter selector silently loses the width and the height. */
/* The selectors carry `.btn.btn-icon` deliberately: Metronic sizes this control
   with `.btn.btn-icon.btn-sm`, which outranks a two-class selector, so a
   shorter selector silently loses the width and the height.
 *
 * The visible edge is an INSET RING, not a `border`. Metronic resets the border
 * with
 *   `.btn.btn-icon:not(.btn-outline):not(.btn-dashed):not(.border-hover)...`
 * whose `:not()` chain outranks any reasonable selector here. Winning that
 * contest would mean copying the whole chain or reaching for `!important`, and
 * both age badly. An inset ring renders identically at 1px, costs no layout
 * width, and does not compete with the reset at all. */
.fsn-q-actions .btn.btn-icon.fsn-msg-icon,
.fsn-profile-actions-end .btn.btn-icon.fsn-msg-icon {
    height: 38px;
    width: 38px;
    border-radius: 6px;
}

/* THE VISIBLE EDGE.
 *
 * Metronic removes both candidates on this element, each behind a long `:not()`
 * chain that outranks a normal selector:
 *
 *   border      `.btn.btn-icon:not(.btn-outline):not(.btn-dashed):not(...)`
 *   box-shadow  `.btn:not(.btn-shadow):not(.shadow):not(.shadow-sm):not(...)`
 *
 * The shadow chain is mirrored below so the ring wins on specificity alone. The
 * alternatives were worse: `!important` hides the conflict from the next reader,
 * and adding `.shadow` to the markup would import a drop shadow this control
 * must not have. The chain is verbose, but it states exactly which reset it is
 * answering, and it breaks loudly rather than silently if Metronic changes it. */
.fsn-q-actions .btn.btn-icon.fsn-msg-icon:not(.btn-shadow):not(.shadow):not(.shadow-sm):not(.shadow-lg):not(.shadow-xs),
.fsn-profile-actions-end .btn.btn-icon.fsn-msg-icon:not(.btn-shadow):not(.shadow):not(.shadow-sm):not(.shadow-lg):not(.shadow-xs) {
    box-shadow: inset 0 0 0 1px var(--bs-gray-300);
}

.fsn-q-actions .btn.btn-icon.fsn-msg-icon:not(.btn-shadow):not(.shadow):not(.shadow-sm):not(.shadow-lg):not(.shadow-xs):hover,
.fsn-profile-actions-end .btn.btn-icon.fsn-msg-icon:not(.btn-shadow):not(.shadow):not(.shadow-sm):not(.shadow-lg):not(.shadow-xs):hover {
    box-shadow: inset 0 0 0 1px var(--bs-gray-400);
}

.fsn-q-actions .btn.btn-icon.fsn-msg-icon:not(.btn-shadow):not(.shadow):not(.shadow-sm):not(.shadow-lg):not(.shadow-xs):disabled,
.fsn-profile-actions-end .btn.btn-icon.fsn-msg-icon:not(.btn-shadow):not(.shadow):not(.shadow-sm):not(.shadow-lg):not(.shadow-xs):disabled {
    box-shadow: inset 0 0 0 1px var(--bs-gray-200);
}

/* The primary action, sized to balance the pair rather than dominate it. */
.fsn-q-actions .btn:not(.fsn-msg-icon),
.fsn-profile-actions-end .btn:not(.fsn-msg-icon) {
    min-height: 38px;
    padding-inline: 14px;
}

/* =========================================================
   MESSAGING SURFACE — UI-QUOTE-MSG-ROUTES-01

   ONE stylesheet for BOTH roles. The Client surface previously carried an inline
   `<style>` block inside its Livewire view and the Contractor surface carried
   inline `style="..."` attributes; the two drifted apart and neither could be
   tested. Every rule now lives here and both views use the same classes, so a
   change to one role cannot silently skip the other.

   The model is chat-like: a quiet conversation list, a thread of bubbles, and a
   single composer row. It is NOT a WhatsApp clone. Nothing here renders a
   delivery receipt, a read receipt, presence, typing state, last-seen, a
   reaction or a voice note — the backend proves none of those, and inventing a
   tick mark would be a claim about delivery that FSN cannot make.
========================================================= */

.fsn-chat { --fsn-chat-line: var(--bs-gray-200); }

/* ── The conversation list.

   Rows, not cards. The previous treatment drew a full border and a radius around
   every row, which stacked three visible boxes before any message was read. A
   row now carries a hairline separator only, and selection is a soft tint of the
   brand navy — the SAME treatment the Quote lifecycle tabs use, so "selected"
   looks identical everywhere on this screen. */
.fsn-chat-list { max-height: 620px; overflow-y: auto; }

.fsn-chat-conv {
    width: 100%;
    text-align: left;
    display: flex;
    gap: 12px;
    align-items: flex-start;
    padding: 12px 14px;
    border: 0;
    border-radius: 10px;
    background: transparent;
    transition: background-color .12s ease-in-out;
}

.fsn-chat-conv + .fsn-chat-conv { margin-top: 2px; }
.fsn-chat-conv:hover { background: var(--bs-gray-100); }

.fsn-chat-conv.active {
    background: rgba(var(--fsn-navy-rgb), .08);
}

.fsn-chat-conv:focus-visible {
    outline: 2px solid var(--fsn-navy);
    outline-offset: -2px;
}

/* The chat avatar now renders an uploaded picture when the person has one
   (MESSAGE-WO-ROUTING-AVATAR-01). It was initials-only before, so a Contractor
   with a profile picture still showed letters in the conversation list. */
.fsn-chat-avatar {
    overflow: hidden;
}

.fsn-chat-avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.fsn-chat-avatar {
    flex: 0 0 40px;
    height: 40px;
    width: 40px;
    border-radius: 50%;
    background: var(--bs-gray-200);
    color: var(--bs-gray-800);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 13px;
}

/* ── The thread.

   One quiet surface instead of a bordered panel containing bordered rows. The
   thread owns its own vertical scroll, so a long conversation never grows the
   page (§9D). */
.fsn-chat-panel {
    border: 1px solid var(--fsn-chat-line);
    border-radius: 12px;
    display: flex;
    flex-direction: column;
    min-height: 320px;
    overflow: hidden;
}

.fsn-chat-head {
    padding: 14px 18px;
    border-bottom: 1px solid var(--fsn-chat-line);
    background: var(--bs-body-bg);
}

.fsn-chat-thread {
    padding: 18px;
    max-height: 560px;
    overflow-y: auto;
    background: var(--bs-gray-100);
    flex: 1 1 auto;
    /* The DOM order is oldest -> newest and stays that way. `column-reverse` is
       deliberately NOT used: it would invert the visual order to fake a bottom
       anchor and break upward reading of history. The scroll position is set
       from the thread itself — see `fsn-chat-thread` in the Blade file. */
    overscroll-behavior: contain;
    scroll-behavior: smooth;
}

.fsn-chat-row { display: flex; margin-bottom: 12px; }
.fsn-chat-row.mine { justify-content: flex-end; }

/* ── The bubble.

   Restrained: no border, a soft surface, and one asymmetric corner that points
   at its own side. A received bubble is white on the grey thread; a sent bubble
   is the navy tint. Neither is a saturated fill — a thread of solid red bubbles
   would make every message look like an alert. */
.fsn-chat-bubble {
    max-width: min(80%, 34rem);
    min-width: 0;
    padding: 10px 12px;
    border-radius: 12px;
    background: var(--bs-body-bg);
    border-bottom-left-radius: 4px;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .08);
}

.fsn-chat-row.mine .fsn-chat-bubble {
    background: rgba(var(--fsn-navy-rgb), .08);
    border-bottom-left-radius: 12px;
    border-bottom-right-radius: 4px;
}

/* The sender's own line breaks survive; a long unbroken string wraps instead of
   widening the page. Blade escapes the body — it is never raw HTML. */
.fsn-chat-text {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    word-break: break-word;
}

/* ── Attachment cards inside a bubble.

   The attachment is rendered ABOVE the caption, so a combined message reads the
   way it was composed: the file first, the sender's words about it underneath.
   The card is a link when the row is available and inert text when it is not, so
   nothing offers a download the server would refuse. */
.fsn-chat-file {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 8px;
    background: rgba(var(--fsn-navy-rgb), .05);
    text-decoration: none;
    min-width: 0;
}

a.fsn-chat-file:hover { background: rgba(var(--fsn-navy-rgb), .10); }

.fsn-chat-file:focus-visible {
    outline: 2px solid var(--fsn-navy);
    outline-offset: 1px;
}

.fsn-chat-file + .fsn-chat-file { margin-top: 6px; }
.fsn-chat-files + .fsn-chat-caption { margin-top: 8px; }

/* ── The composer.

   One rounded row containing the attach control, the text area and Send — the
   attach control is INSIDE the input region, not a separate button beneath it.
   `align-items: flex-end` keeps the two controls on the last line as the text
   area grows. */
.fsn-chat-composer {
    border-top: 1px solid var(--fsn-chat-line);
    padding: 12px 14px;
    background: var(--bs-body-bg);
}

/* ── The composer bar — UI-CHAT-01, from `message-bar-reference.png`.
 *
 * The reference draws TWO parts: one rounded pill holding the attach control and
 * the input, and a separate circular Send button beside it. */
.fsn-chat-bar {
    display: flex;
    align-items: center;
    gap: 10px;
}

.fsn-chat-inputrow {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 6px 4px 4px;
    border: 1px solid var(--fsn-chat-line);
    /* Fully rounded, as the reference draws it. A radius larger than half the
       height simply clamps, so the pill stays a pill as the input grows. */
    border-radius: 999px;
    background: var(--bs-body-bg);
    transition: border-color .12s ease, box-shadow .12s ease;
}

/* THE ONLY focus treatment on this composer.
 *
 * The nested rectangle Haris reported came from `form-control` on the textarea:
 * it drew its own border, radius and focus shadow INSIDE the pill, so focusing
 * produced a box inside a box and shifted the composer's height. That class is
 * removed from the markup, and the ring now belongs to the pill, which is the
 * thing the user perceives as the control. */
.fsn-chat-inputrow:focus-within {
    border-color: var(--fsn-navy);
    box-shadow: 0 0 0 3px rgba(var(--fsn-navy-rgb), .10);
}

/* The text area sits flush inside the pill: no second border, no second radius,
   and no resize handle overlapping the rounded corner. */
/* The input is flush inside the pill: no border, no radius, no focus box of its
   own. Everything that would draw a second rectangle is refused here, including
   on focus, so the composer never gains an inner card and never changes height
   when focused. */
.fsn-chat-inputrow .fsn-chat-input,
.fsn-chat-inputrow .fsn-chat-input:focus,
.fsn-chat-inputrow .fsn-chat-input:focus-visible {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    outline: 0;
    background: transparent;
    box-shadow: none;
    border-radius: 0;
    resize: none;
    padding: 8px 4px;
    font-size: 14px;
    line-height: 1.4;
    color: var(--bs-gray-900);
    /* Two lines of room, so a wrapped placeholder does not make the control
       scroll before anything is typed. */
    min-height: 40px;
    max-height: 120px;
    overflow-y: auto;
}

.fsn-chat-inputrow .fsn-chat-input::placeholder { color: var(--bs-gray-500); }

/* A refused body is stated by the message under the bar. The pill borrows the
   danger colour rather than drawing a second inner box. */
.fsn-chat-inputrow:has(.fsn-chat-input-invalid) { border-color: var(--bs-danger); }

/* ── The attach control.
 *
 * Borderless and vertically centred inside the pill, as the reference draws it:
 * a plain glyph, not a bordered button. It stays a comfortable target because
 * the label itself is 36px, even though it draws no surface at rest. */
.fsn-chat-attach {
    flex: 0 0 auto;
    height: 36px;
    width: 36px;
    padding: 0;
    margin: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 50%;
    color: var(--bs-gray-700);
    background: transparent;
    cursor: pointer;
}

.fsn-chat-attach:hover { background: var(--bs-gray-100); color: var(--fsn-navy); }

/* The control is a LABEL wrapping a visually-hidden file input, so it must show
   the keyboard ring from the input's own focus state. */
.fsn-chat-attach:focus-within {
    outline: 2px solid var(--fsn-navy);
    outline-offset: 2px;
}

/* ── The Send button.
 *
 * OUTSIDE the pill, circular and solid, matching the reference's proportions —
 * but in the FSN primary red, never the reference's green. The icon is white in
 * every enabled state, so contrast never drops on hover. */
.fsn-chat-send {
    flex: 0 0 auto;
    height: 44px;
    width: 44px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 50%;
    background: var(--bs-primary);
    color: #ffffff;
    transition: background-color .12s ease, box-shadow .12s ease;
}

.fsn-chat-send > span { display: inline-flex; align-items: center; justify-content: center; }
.fsn-chat-send i { color: #ffffff; }

/* Hover and active deepen the red rather than changing hue, so the white icon
   keeps its contrast. The navy hover used before turned the primary action into
   a different colour on pointer-over. */
.fsn-chat-send:hover:not(:disabled) {
    background: var(--bs-primary);
    box-shadow: 0 2px 10px rgba(var(--bs-primary-rgb), .40);
}

.fsn-chat-send:active:not(:disabled) { background: var(--fsn-navy); }
.fsn-chat-send:active:not(:disabled) i { color: #ffffff; }

.fsn-chat-send:disabled {
    background: var(--bs-gray-300);
    color: var(--bs-gray-600);
    opacity: 1;
}

.fsn-chat-send:disabled i { color: var(--bs-gray-600); }
.fsn-chat-send:focus-visible { outline: 2px solid var(--fsn-navy); outline-offset: 2px; }

/* Selected-but-unsent files sit ABOVE the input row, so the composer grows
   upward and the typing position never moves. */
.fsn-chat-pending { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }

.fsn-chat-pending-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    border: 1px solid var(--fsn-chat-line);
    border-radius: 8px;
    min-width: 0;
}

/* Narrow: the bubble may use nearly the whole width, because the alignment and
   the tint still separate the two sides. */
@media (max-width: 575.98px) {
    .fsn-chat-bubble { max-width: 92%; }
    .fsn-chat-thread { padding: 12px; }
    .fsn-chat-list { max-height: 320px; }

    /* Two lines of room at a narrow width — UI-CHAT-01.
       At 390 px the input box is about 152 px wide, so the placeholder wraps to
       two lines. At the 40 px desktop height that content is taller than the
       box, and the control showed a scrollbar before the user had typed
       anything. The rule is scoped to the narrow width, so the desktop composer
       keeps the reference's single-line proportions.

       THE THREE SELECTORS ARE REQUIRED. The base rule states `min-height` on a
       list that includes `:focus` and `:focus-visible`, which are ONE class
       more specific than a bare `.fsn-chat-input`. A single-selector override
       therefore lost the moment the composer was focused: the control snapped
       from 56px back to 40px on tap and immediately scrolled. Matching the same
       three selectors makes the specificity equal, so source order decides. */
    .fsn-chat-inputrow .fsn-chat-input,
    .fsn-chat-inputrow .fsn-chat-input:focus,
    .fsn-chat-inputrow .fsn-chat-input:focus-visible { min-height: 56px; }
}

/* Narrow: the panel stays ABOVE the value — that is the reference behaviour and
   it is a requirement of this boundary, not a preference — but it is pinned to
   the START of the card's content box instead of to the value. A panel anchored
   to a value that sits near the right edge of a 390 px card would otherwise
   leave the viewport.

   `left: 0` on the ROW rather than on the value needs a positioned ancestor, so
   the narrow rule widens the anchor to the card's identity row. */
@container fsn-quote-list (max-width: 660px) {
    .fsn-q-dist-panel {
        left: auto;
        right: 0;
        /* The centring transform MUST be cleared with the centring, or the panel
           is shifted half its own width past the right edge. */
        transform: none;
        max-width: min(280px, calc(100vw - 24px));
    }

    /* The caret follows the trigger, not the panel centre, once the panel is
       right-anchored — otherwise it points at empty card. */
    .fsn-q-dist-panel::after {
        left: auto;
        right: 18px;
        margin-left: 0;
    }
}

@supports not (container-type: inline-size) {
    @media (max-width: 767.98px) {
        .fsn-q-dist-panel {
            left: auto;
            right: 0;
            transform: none;
            max-width: min(280px, calc(100vw - 24px));
        }

        .fsn-q-dist-panel::after {
            left: auto;
            right: 18px;
            margin-left: 0;
        }
    }
}

/* ROW 2 — the statistics pills. Inline, beside the avatar's content column —
   never underneath the avatar and never two full-width rows. */
.fsn-q-pills { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }

/* ROW 3 — secondary metadata. */
.fsn-q-submeta { margin-top: 8px; }

/* ACTIONS — right side, vertically centred. */
.fsn-q-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; }

/* The card responds to the Quote LIST COLUMN width.
   Above this width the card stays row-based at every desktop size, including
   1280 with the profile panel open. Below it the avatar/content/actions frame
   stacks, because the row genuinely no longer fits. */
@container fsn-quote-list (max-width: 660px) {
    .fsn-q-main { flex-direction: column; align-items: stretch; }
    .fsn-q-actions { justify-content: flex-start; }
    .fsn-q-fact { white-space: normal; }
}

/* Fallback for a browser without container queries: the same stack, keyed to
   the viewport. It never fires on a desktop width, so the row layout the
   boundary requires at 1280 and above is unaffected. */
@supports not (container-type: inline-size) {
    @media (max-width: 767.98px) {
        .fsn-q-main { flex-direction: column; align-items: stretch; }
        .fsn-q-actions { justify-content: flex-start; }
        .fsn-q-fact { white-space: normal; }
    }
}

.fsn-profile-panel { border: 1px solid var(--bs-gray-200); border-radius: 12px; background: var(--bs-body-bg); overflow: hidden; }
/* The panel owns its OWN vertical scroll. `sticky` is scoped to the grid, not
   to the viewport, so the page keeps one scrollbar and the panel never becomes
   a full-height drawer. */
@media (min-width: 992px) {
    .fsn-q-aside { position: sticky; top: 16px; }
    .fsn-profile-scroll { max-height: calc(100vh - 160px); overflow-y: auto; }
}
.fsn-profile-scroll { padding: 16px; overflow-x: hidden; }
.fsn-profile-empty { padding: 40px 16px; }
.fsn-profile-heading { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .3px; color: var(--bs-gray-500); margin-bottom: 6px; }
.fsn-profile-map-empty { border: 1px dashed var(--bs-gray-300); border-radius: 8px; padding: 16px; text-align: center; }

/* ── Contractor Work Experience — UI-CONTRACTOR-PROFILE-WORK-EXPERIENCE-01 ──
   Reference:
   `Client/quote-listing-profile-sidebar-work-experience-ui.png`.

   The section lives INSIDE the existing profile panel and inside its existing
   scroll region. It deliberately adds NO scroll container of its own: a nested
   scrollbar inside a scrolling panel is the pattern that traps a pointer.

   Colour comes from the existing FSN neutral tokens. The reference renders its
   headings and child counts in the Field Nation blue; that palette is NOT
   imported, because a second accent system would compete with the FSN primary.
   The reference's HIERARCHY is reproduced with weight and size instead. */

/* The group label — the reference's "Types of work related to work order" and
   "All types of work". Darker and larger than the uppercase section heading
   above it, which is what separates the two levels. */
.fsn-we-group-label {
    font-size: 13px;
    font-weight: 600;
    color: var(--bs-gray-800);
    margin: 10px 0 8px;
}

.fsn-we-list {
    border-top: 1px solid var(--bs-gray-200);
}

.fsn-we-item {
    border-bottom: 1px solid var(--bs-gray-200);
}

/* The category row. A subtle neutral band, as the reference draws it.
   `width: 100%` and the text resets matter because the interactive variant is a
   real <button>, which would otherwise centre its text and inherit the button
   font. */
.fsn-we-row {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 10px 12px;
    background: var(--bs-gray-100);
    border: 0;
    text-align: left;
    font: inherit;
    color: inherit;
}

.fsn-we-row-toggle { cursor: pointer; }
.fsn-we-row-toggle:hover { background: var(--bs-gray-200); }

.fsn-we-row-toggle:focus-visible {
    outline: 2px solid var(--fsn-navy);
    outline-offset: -2px;
}

/* Title over rate, as the reference stacks them. */
.fsn-we-main {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1 1 auto;
}

.fsn-we-label {
    font-size: 13px;
    font-weight: 700;
    color: var(--bs-gray-900);
    overflow-wrap: anywhere;
}

/* A DISPLAY string only. It is never a computed amount. */
.fsn-we-rate {
    font-size: 12px;
    color: var(--bs-gray-600);
}

/* The count sits right, before the chevron. The FIGURE carries the weight and
   the unit stays quiet, which is the reference's own emphasis. */
.fsn-we-count {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    flex: 0 0 auto;
    white-space: nowrap;
}

.fsn-we-count-value {
    font-size: 13px;
    font-weight: 700;
    color: var(--bs-gray-900);
}

.fsn-we-count-unit {
    font-size: 12px;
    color: var(--bs-gray-600);
}

.fsn-we-chevron {
    flex: 0 0 auto;
    font-size: 12px;
    color: var(--bs-gray-600);
}

/* A row with no children still draws the chevron, so the rhythm matches the
   reference — but it is muted, because the row is not interactive and must not
   promise an action it cannot perform. */
.fsn-we-chevron-idle { opacity: .35; }

/* The expanded children. Lighter weight, indented, on the panel background so
   they read as belonging to the row above rather than as new categories. */
.fsn-we-children {
    background: var(--bs-body-bg);
}

.fsn-we-child {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    /* The left inset is what makes a child a child. The right inset clears the
       parent's chevron column, so the child count still aligns with the parent
       count above it. */
    padding: 8px 34px 8px 24px;
}

.fsn-we-child + .fsn-we-child {
    border-top: 1px solid var(--bs-gray-100);
}

.fsn-we-child-label {
    font-size: 13px;
    color: var(--bs-gray-700);
    overflow-wrap: anywhere;
}

/* A child count is quieter than its parent's. */
.fsn-we-child .fsn-we-count-value {
    font-weight: 600;
    color: var(--bs-gray-800);
}

/* The job-count pill. One shape on the card and in the panel. */
.fsn-job-pill { display: inline-flex; align-items: center; gap: 6px; font-weight: 500; }
.fsn-job-pill-value { font-weight: 700; }

/* A selectable Quote card. The whole card is the profile-selection target.

   B18 — the card body carries NO hover effect. It used to paint the whole card
   `--bs-gray-100` on hover, which read as a selection: a Client moving the
   pointer down the list saw every card light up the same way the chosen one
   did, and the two states were indistinguishable at a glance. The card header
   keeps its own hover, which is `text-hover-primary` on the contractor name in
   the markup and is not declared here — so removing this rule cannot remove it.

   Hover and selection are now separate: hover says nothing, selection is the
   red border below. */
.fsn-q-card-select { cursor: pointer; }

/* B19 — the SELECTED Quote card, bordered in FSN primary red.

   `--bs-primary` is the application's own token (#e11d2e, declared at the top of
   this file). No second red is introduced.

   The border replaces a 3px inset left bar plus a gray fill. The bar was easy to
   miss beside a full-width card, and the gray fill was the same colour the hover
   painted — so a hovered card and the selected card looked identical.

   `border-color` is used rather than a new border, so the card's geometry does
   not change when it is selected and no row shifts by a pixel. The box-shadow is
   a ring OUTSIDE that border, which gives the weight the reference shows without
   adding layout width.

   This applies to `.selected` ONLY — never to `:hover`. */
.fsn-q-card-select.selected {
    border-color: var(--bs-primary);
    box-shadow: 0 0 0 1px var(--bs-primary);
}

.fsn-q-card-select:focus-visible { outline: 2px solid var(--fsn-navy); outline-offset: -2px; }

/* The avatar carries its own FIXED footprint, so it never stretches with the
   card and the image never distorts. Declared here rather than in the page, so
   the Quote card and the profile panel cannot drift apart. */
.fsn-q-avatar {
    flex: 0 0 40px;
    width: 40px;
    height: 40px;
    border: 2px solid var(--bs-primary);
    border-radius: 50%;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bs-gray-200);
    color: var(--bs-gray-800);
    font-weight: 700;
    font-size: 13px;
}

/* The Quote card's larger avatar. The reference draws it at roughly this size
   against the two information rows, so the ring reads as a status ring rather
   than as a small icon. */
.fsn-q-avatar-lg {
    flex-basis: 48px;
    width: 48px;
    height: 48px;
    font-size: 15px;
    align-self: flex-start;
}

.fsn-q-avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* Below the point where equal columns stop being readable, the row scrolls and
   each tab keeps a usable minimum. */
@media (max-width: 767.98px) {
    .fsn-wo-tabs .nav,
    .fsn-q-lifecycle .nav {
        grid-auto-columns: minmax(8rem, 1fr);
        width: max-content;
        min-width: 100%;
    }
}


/* ═══ SCOPE OF WORK OPTIONAL PANELS ═══ UI-CREATE-WO-FUNCTIONAL-01

   Confidential Information, Site Instructions and Site Policies. They replace
   three `btn btn-light` Bootstrap collapses whose chevron never moved and whose
   state no screen reader could read.

   The row is a full-width, list-style control rather than a small pill, so the
   three optional documents read as a group of equal choices instead of three
   loose buttons. Alpine owns the open boolean; every rule below is driven by the
   `is-open` class that boolean writes. */

.fsn-scope-panels {
    border: 1px solid var(--bs-gray-300);
    border-radius: 0.475rem;
    overflow: hidden;
}

.fsn-scope-panel + .fsn-scope-panel {
    border-top: 1px solid var(--bs-gray-300);
}

.fsn-scope-panel-toggle {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    padding: 0.85rem 1rem;
    background: transparent;
    border: 0;
    text-align: left;
    color: var(--bs-gray-800);
    font-size: 0.925rem;
    font-weight: 600;
}

.fsn-scope-panel-toggle:hover {
    background: var(--bs-gray-100);
}

/* Keyboard focus must be visible. The row is a real <button>, so Tab reaches it
   and Enter or Space toggles it with no extra scripting. */
.fsn-scope-panel-toggle:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: -2px;
}

.fsn-scope-panel-toggle.is-open {
    background: var(--bs-gray-100);
}

.fsn-scope-panel-label {
    flex: 1 1 auto;
    min-width: 0;
}

.fsn-scope-panel-optional {
    flex: 0 0 auto;
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--bs-gray-600);
}

/* The closed-with-content mark. It states that a panel holds a value without
   opening it, so the Client can see which optional documents exist. */
.fsn-scope-panel-filled {
    flex: 0 0 auto;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--fsn-success);
}

/* The chevron rotates with the state. This is the affordance the Bootstrap
   collapse never provided. */
.fsn-scope-panel-chevron {
    flex: 0 0 auto;
    color: var(--bs-gray-600);
    transition: transform 0.15s ease-in-out;
}

.fsn-scope-panel-toggle.is-open .fsn-scope-panel-chevron {
    transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
    .fsn-scope-panel-chevron { transition: none; }
}

.fsn-scope-panel-body {
    padding: 0 1rem 1rem;
}

/* This project defines NO global `[x-cloak]` rule, and several existing views
   carry `x-cloak` as a no-op. Adding a global rule now would suddenly hide every
   one of those elements until Alpine reached it, which is a change to unrelated
   screens. The rule is therefore SCOPED to this component: it stops the panel
   body and the filled dot flashing open on first paint, and it touches nothing
   else. */
.fsn-scope-panel-body[x-cloak],
.fsn-scope-panel-filled[x-cloak] {
    display: none !important;
}

/* ═══ CLIENT HEADER FINANCIALS ENTRY ═══ UI-CREATE-WO-FUNCTIONAL-01

   Reference: the Field Nation header in
   `FSN Redesign Screenshots/Project/Client/create-workorder.png`, which carries
   a monetary figure among its right-side controls.

   CLIENT ONLY. The role gate is a server-side guard check in
   `components/client/financials-indicator.blade.php` — the markup is never
   rendered for a Contractor, so nothing below is load-bearing for privacy. */

/* One restrained separator, immediately before Financials. It is deliberately
   SHORTER than the header and vertically centred, as the reference draws it, so
   it reads as a divider rather than a column rule. No separator is added between
   the existing controls. */
.fsn-header-separator {
    display: inline-block;
    flex: 0 0 auto;
    width: 1px;
    height: 24px;
    margin-left: 0.75rem;
    background-color: var(--bs-gray-300);
}

.fsn-header-financials {
    padding: 0.35rem 0.6rem;
    color: var(--bs-gray-800);
    text-decoration: none;
    white-space: nowrap;
    line-height: 1;
}

.fsn-header-financials:hover,
.fsn-header-financials:focus-visible {
    color: var(--bs-primary);
}

.fsn-header-financials .fsn-header-financials-amount {
    font-size: 0.95rem;
    font-weight: 600;
}

/* Below the lg breakpoint the header is tight, so the amount is dropped and the
   currency icon remains. The control keeps its route, its tooltip and its
   accessible name, so Financials is still reachable — it is not hidden, only
   compacted. */
@media (max-width: 991.98px) {
    .fsn-header-financials .fsn-header-financials-amount {
        display: none;
    }

    .fsn-header-separator {
        height: 20px;
        margin-left: 0.5rem;
    }
}


/* The map preview relies on `x-cloak` to avoid a flash of every state before
   Alpine initialises. This project defines NO global `[x-cloak]` rule and other
   views carry the attribute as a no-op, so the rule stays SCOPED to this
   component exactly as the Scope of Work panels do. */
[data-test="location-map-preview"] [x-cloak] {
    display: none !important;
}

/* A flex row for a map state block, WITHOUT `!important`.

   MEASURED DEFECT: Bootstrap's `.d-flex` is `display: flex !important`, and
   `x-show` hides an element with an inline `display: none`. The `!important`
   won, so the loading line, the warning and the error all rendered at once
   while the component state was `none`. This class supplies the same layout at
   normal specificity, so the inline rule takes effect. */
.fsn-map-state {
    display: flex;
}

/* ── Contractor profile panel: the action row ────────────────────────────
   UI-WO-VIEW-QUOTE-ACTIONS-01. Reference:
   `Client\quote-listing-profile-sidebar-ui.png`.

   A separator row above the contractor identity: Close Panel on the left, the
   message action and Review Offer on the right. It sits OUTSIDE the panel's own
   scroll region, so the controls stay reachable while the profile body scrolls.

   `gap` plus `margin-left: auto` on the trailing group is what holds the two
   alignments apart; no absolute positioning is used, so the row cannot overlap
   the identity block at a narrow width. */
.fsn-profile-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    padding: 10px 14px;
    border-bottom: 1px solid var(--bs-gray-200);
    background: var(--bs-body-bg);
}

.fsn-profile-actions-end {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-left: auto;
}

/* The close control reads as a quiet navigation action, not a destructive one. */
.fsn-profile-actions .fsn-profile-close {
    color: var(--fsn-navy);
}

.fsn-profile-actions .fsn-profile-close:hover {
    color: var(--bs-primary);
}

/* The control is back to the approved `4e049b9` layout: icon first, then the
   label, in ordinary inline flow. No wrapper, no truncation, no flex container
   — those were the over-correction. Only this one alignment rule remains, so
   the chevron sits on the label's optical centre line. */
.fsn-profile-actions .fsn-profile-close i {
    vertical-align: middle;
}

/* ── Client Work Order View: status badge and summary icons ──────────────
   UI-WO-VIEW-EDIT-03.

   ## The status badge

   It was `badge-light-primary` at the default badge size, which read as a pale
   grey-pink chip beside a 20px bold reference number. On a page that shows ONE
   Work Order, the status is the first fact a reader needs. It now uses the SOLID
   variant chosen by `ProjectStatus::badgeSolid()` and this size step.

   Only the SIZE lives here. The colour is decided by the enum, so a Blade file
   never picks a status colour and the Administrator screens keep the light tint.

   ## The summary icons

   The four `fs-3x` icons were `text-gray-500`, so the largest graphic elements on
   the page read as disabled text. They now carry the FSN navy token.

   Navy rather than the primary red: red is the ACTION colour on this page — the
   primary buttons and the category chips — and four large red icons would compete
   with it for attention.

   `.ki-duotone` sets its own colour on its `path` children, so colouring the
   parent alone leaves the glyph grey. The path rule is therefore required. The
   same trap was recorded for the sidebar toggle chevron in
   UI-CONTRACTOR-PROFILE-WORK-EXPERIENCE-01 §7. */
.fsn-wo-view .fsn-wo-status-badge {
    font-size: 0.95rem;
    font-weight: 700;
    padding: 0.6rem 0.95rem;
    line-height: 1;
    letter-spacing: 0.01em;
}

.fsn-wo-view .fsn-wo-summary-icon,
.fsn-wo-view .fsn-wo-summary-icon i,
.fsn-wo-view .fsn-wo-summary-icon [class*="path"]:before {
    color: var(--fsn-navy);
}

/* The map frame while it is still loading — UI-WO-LISTING-CREATE-02.

   ROOT CAUSE OF A MEASURED REGRESSION. The frame used to be hidden during
   `loading` with Bootstrap's `.visually-hidden`, which forces
   `width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0)`. That shrank
   the IFRAME ITSELF to 1px, and the iframe also carried `loading="lazy"`. A
   lazily-loaded iframe clipped to one pixel is never fetched, so its `load`
   event never fired, so the component never left `loading` and the 12-second
   guard moved it to `error`.

   Measured on the Create Work Order screen at 1920px, after a real Google Places
   selection of "1600 Amphitheatre Parkway, Mountain View, CA, USA":
     - coordinates extracted correctly (37.4220095, -122.0847519)
     - the iframe element existed with a 122-character embed URL
     - iframe bounding box 1px wide
     - ZERO network requests to `maps/embed`
     - final state `error`
   The same URL with the same key, in a VISIBLE eagerly-loaded iframe, fired
   `load` and drew a real map. The provider, the key and the Maps Embed API were
   never the cause.

   This class collapses only the WRAPPER, so the wrapper reserves no height while
   the map loads, and the iframe inside keeps its real dimensions and is fetched.
   The iframe is also loaded eagerly, because it is created only once there is a
   resolved place to show — lazy loading saved nothing and caused the deadlock. */
.fsn-map-frame-pending {
    height: 0;
    overflow: hidden;
}

/* ============================================================================
   SIGN-UP AND LANDING LINKS — UI-SIGNUP-01
   ============================================================================

   MEASURED. Bootstrap's `.link-primary` and Metronic's bare `a` colour both
   resolve to `--bs-link-color`, which is the Metronic green #17C653 in this
   build. Every inline link on the Sign-Up screens rendered GREEN while the
   primary buttons beside them rendered FSN red, so one screen carried two
   different "primary" colours.

   The same trap has been hit before, on the Quote lifecycle tabs and on the
   Quote distance link, and was solved locally each time. This rule is the
   shared answer for the public Landing and Sign-Up surfaces.

   `--bs-primary` #E11D2E on white measures 4.78:1, which clears AA for body
   text. Hover moves to navy, matching `.btn-fsn-primary`.
   ========================================================================= */
.fsn-link {
    color: var(--bs-primary);
    font-weight: 600;
    text-decoration: none;
}

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

/* The /signup role cards — PUBLIC-PAGES-AND-BLOG-01 round 3 (Haris, 2026-09-29).
   The Field Nation reference layout: the photograph across the full card width, then
   the heading and one sentence, then an action row pinned to the bottom edge. The two
   cards share every rule here; only the action modifier differs.
   🔴 NO CROPPING, at any width. The image is the card's full width at its own height
   (both files are 3 : 2), so nothing is cut: no `object-fit: cover`, no clipping, no
   fixed height. The top corners are rounded on the image itself. */
.fsn-role-card {
    border-color: var(--bs-gray-200);
    border-radius: 0.375rem;
    box-shadow: none;
    transition: border-color 0.15s ease;
}

.fsn-role-card:hover,
.fsn-role-card:focus-within {
    border-color: var(--bs-gray-400);
}

.fsn-role-card-media img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;
}

.fsn-role-card-body {
    flex: 1 1 auto;
    padding: 1.5rem 1.5rem 1rem;
}

.fsn-role-card-title {
    margin: 0 0 0.75rem;
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--bs-gray-900);
}

.fsn-role-card-text {
    margin: 0;
    font-size: 1.075rem;
    line-height: 1.6;
    color: var(--bs-gray-700);
}

/* The action row: one height on both cards, the action at the right, as in the
   reference. */
.fsn-role-card-action {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: flex-end;
    min-height: 3.75rem;
    padding: 0.75rem 1.5rem;
}

.fsn-role-card-cta {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 1.15rem;
    font-weight: 600;
    text-decoration: none;
}

/* The chevron takes the link's colour; the icon font would otherwise paint it muted
   grey, where the reference colours it with the text. */
.fsn-role-card-cta i {
    color: inherit;
}

.fsn-role-card-action--link .fsn-role-card-cta {
    color: var(--bs-primary);
}

.fsn-role-card:hover .fsn-role-card-action--link .fsn-role-card-cta,
.fsn-role-card-action--link .fsn-role-card-cta:focus-visible {
    color: var(--fsn-navy);
    text-decoration: underline;
}

.fsn-role-card-action--primary {
    background-color: var(--bs-primary);
    border-radius: 0 0 calc(0.375rem - 1px) calc(0.375rem - 1px);
    transition: background-color 0.15s ease;
}

.fsn-role-card-action--primary .fsn-role-card-cta,
.fsn-role-card-action--primary .fsn-role-card-cta i {
    color: #ffffff;
}

.fsn-role-card:hover .fsn-role-card-action--primary {
    background-color: var(--bs-primary-active);
}

@media (min-width: 992px) {
    .fsn-role-card-body {
        padding: 2rem 2rem 1rem;
    }

    .fsn-role-card-action {
        padding: 0.75rem 2rem;
    }
}

/* Legal pages: long-form text needs room to breathe between headings. */
.fsn-legal h5,
.fsn-legal h6 {
    scroll-margin-top: 90px;
}

.fsn-legal p,
.fsn-legal li {
    line-height: 1.7;
}

/* =========================================================
   PROFILE IMAGE INPUT — KEYBOARD REACHABILITY

   The file input is hidden so the styled label can stand in for it. It was
   hidden with `d-none`, which removes the element from the accessibility tree
   AND from the tab order, so a keyboard-only person could never reach either
   image control — MEASURED: `element.focus()` left `document.activeElement`
   unchanged.

   `.fsn-visually-hidden-file` hides it visually while keeping it focusable and
   announced. The label then shows the focus ring on the person's behalf, because
   the element that actually holds focus has no visible box of its own.

   Placed ABOVE the two admin markers deliberately: `AdminPaletteContrastTest`
   and `SmsDeliveryLogPageTest` read every selector from those markers to the end
   of the file and require each one to be shell-scoped.
   ========================================================= */

.fsn-visually-hidden-file {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
}

.fsn-image-input-label:focus-within {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* =========================================================
   THE FSN MODAL SHELL — shared, Livewire-owned
   =========================================================
   The generic shell every Livewire-owned FSN dialog uses: contact edit, location
   edit, schedule edit, status confirmation, Quote message, and the Public Profile
   Direct Message and Recommendation modals.

   ## Why it is declared here

   It previously existed ONLY inside the `@assets` block of
   `resources/views/client/pages/projects/show.blade.php`, so it applied on that
   one page. Identical markup on any other page rendered as an unstyled block at
   the foot of the document — which is how this was found, when the Public Profile
   modals appeared inline instead of as dialogs.

   ## This is the ONE definition — do not add a page-local copy (2026-08-17)

   The page-local copy in `show.blade.php` has been removed. **Edit these rules
   here and nowhere else.** `tests/Feature/Ui/SharedModalShellTest.php` fails if
   a Blade file re-declares the shell.

   A 2026-08-06 removal attempt was reverted after an HTTP 500, and the shell was
   duplicated instead. That incident **did not prove that this CSS needs to be
   duplicated.** The removal was retried on 2026-08-17 with caches cleared: the
   page renders, `ClientWorkOrderViewTest` passes 17/17, and browser QA
   reproduces no HTTP 500.

   Full record, including what the historical exception does and does not prove:
   `docs/context/profile-refinements-public-profiles-boundary-review.md` §25.5
   and §26.

   ### Still page-specific, deliberately NOT moved here

   The Work Order print block that hides an open modal on paper, and the
   `.pac-container` z-index for the Google Places list inside that page's own
   location modal. Both belong to that one screen. */
.fsn-modal {
    position: fixed;
    inset: 0;
    z-index: 1055;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 5vh 12px;
    overflow-y: auto;
}

.fsn-modal-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, .5); }

.fsn-modal-dialog { position: relative; z-index: 1; width: 100%; max-width: 560px; }

/* A confirmation asks one question and carries no form. A 560px frame around
   three short lines reads as a data-entry modal. */
.fsn-modal-dialog-sm { max-width: 440px; }

/* This panel is not inside a Bootstrap `.modal`, so `--bs-modal-bg` is
   undefined. The surface is set explicitly, or the dimmed page shows through. */
.fsn-modal .modal-content {
    background: var(--bs-body-bg, #fff);
    color: var(--bs-body-color);
    border: 1px solid var(--bs-border-color);
    border-radius: 12px;
    box-shadow: 0 10px 40px rgba(0, 0, 0, .25);
}

.fsn-modal .modal-header,
.fsn-modal .modal-footer { padding: 16px 20px; }

/* The body scrolls inside the dialog, so a long form never pushes the dialog
   past the viewport and the header and footer stay reachable (§9D). */
.fsn-modal .modal-body { padding: 16px 20px; max-height: calc(100dvh - 230px); overflow-y: auto; }

/* The owner's Public Profile preview shows a whole page, not a question or a
   form. The dialog takes the shell's full width and the header stays put while
   the page scrolls beneath it.

   🔴 THE BODY SCROLLS, NOT THE SHELL — and that is the fix, not a preference.

   This variant used to leave `.fsn-modal` as the scroll container and pin the
   header with `position: sticky; top: 0`. A sticky offset is measured from the
   SCROLLPORT, and `.fsn-modal` carries `padding: 5vh 12px`, so the header pinned
   5vh DOWN the viewport and the strip above it was a window onto the scrolling
   content behind. Measured with `elementFromPoint` after a real wheel scroll:
   body paragraphs painted at y = 2..42 above a header pinned at y = 45 (1440x900),
   and the same at 390x844 with the header at y = 42.

   Capping the dialog at the shell's height makes the BODY the scroller, which is
   what every other `.fsn-modal` already does. The header is then a static flex
   row that cannot move, the close control is always in view, and nothing can be
   painted above it. Declared here beside the shell, never in a view. */
.fsn-modal-dialog-full {
    max-width: none;
    /* The shell's content box: 100dvh minus its 5vh top and bottom padding. */
    max-height: 100%;
    display: flex;
}

.fsn-modal-dialog-full .modal-content {
    display: flex;
    flex-direction: column;
    /* Without this a flex item refuses to shrink below its content height, and
       the dialog would grow past the cap instead of scrolling inside it. */
    min-height: 0;
    width: 100%;
}

.fsn-modal-dialog-full .modal-header { flex: 0 0 auto; }

.fsn-modal-dialog-full .modal-body {
    /* Not the shell's `calc(100dvh - 230px)`: this body is bounded by the flex
       column, so it takes whatever the header leaves. */
    max-height: none;
    min-height: 0;
    flex: 1 1 auto;
    overflow-y: auto;
}

/* The read-only DOCUMENT preview — §8.79, Scope B3.

   Wider than a form dialog because the subject is a page or a scan, and bounded
   by the viewport for the same reason the full-page variant above is: the BODY
   scrolls, so the header and its close control never move. */
.fsn-modal-dialog-preview {
    max-width: 1000px;
    max-height: 100%;
    display: flex;
}

.fsn-modal-dialog-preview .modal-content {
    display: flex;
    flex-direction: column;
    min-height: 0;
    width: 100%;
}

.fsn-modal-dialog-preview .modal-header { flex: 0 0 auto; gap: 12px; }

.fsn-modal-dialog-preview .modal-body {
    max-height: none;
    min-height: 0;
    flex: 1 1 auto;
    overflow: auto;
    /* A scan is often taller than it is wide; centring keeps a narrow one from
       hugging the left edge of a wide dialog. */
    display: flex;
    justify-content: center;
}

/* The image sizes to the dialog and never forces the page to scroll sideways
   (§9D). `height: auto` keeps the aspect ratio of a scan. */
.fsn-document-preview-image {
    max-width: 100%;
    height: auto;
    object-fit: contain;
}

/* A PDF has no intrinsic height, so the frame is given one. `min()` keeps it
   inside a short viewport instead of pushing the close control away. */
.fsn-document-preview-frame {
    width: 100%;
    height: min(72vh, 900px);
    border: 0;
}

.fsn-modal .fsn-repeater-row {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    background: var(--bs-gray-100);
    border-radius: 8px;
    padding: 10px;
}

.fsn-modal .row > [class*="col"] { min-width: 0; }

/* Lock the page scroll only while a modal is in the DOM. */
body:has(.fsn-modal) { overflow: hidden; }

/* =========================================================
   THE BIO PREVIEW — B6 / B7
   =========================================================
   Reference: `Client/quote-listing-profile-sidebar-bio-1.png` (collapsed) and
   `-bio-2.png` (expanded).

   The height is bounded in LINES, not characters. "Approximately four visible
   lines" is a statement about layout, and only the browser knows how many lines
   a given bio makes in a given column — a character budget would show three
   lines of one bio and six of another, and would change again when the drawer
   collapses and the panel widens.

   `line-clamp` also guarantees the containment B6 asks for: the block can never
   exceed its clamp, so a very long bio cannot overflow the card, push the
   sidebar, or consume the panel.

   Both the prefixed and the standard property are declared. `-webkit-line-clamp`
   is what every current engine actually implements, including Firefox; `line-clamp`
   is the standardised name. Declaring both means the newer name wins where it is
   supported and nothing regresses where it is not. */
.fsn-bio-text {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
    /* Long unbroken text — a URL in a bio — must wrap rather than widen the
       card. Without this a single long token defeats the clamp horizontally. */
    overflow-wrap: anywhere;
}

/* Collapsed: the reference's four lines. */
.fsn-bio-collapsed {
    -webkit-line-clamp: 4;
    line-clamp: 4;
}

/* Expanded: "approximately double the collapsed allowance" — eight lines. It is
   deliberately still CLAMPED. An unbounded expansion would let one long bio fill
   the whole sidebar, which B7 forbids; beyond this the reader is sent to the full
   profile instead. */
.fsn-bio-expanded {
    -webkit-line-clamp: 8;
    line-clamp: 8;
}

/* The expander sits at the lower right in both reference screenshots. */
.fsn-bio-actions {
    display: flex;
    justify-content: flex-end;
    margin-top: 4px;
}

/* The full-profile link is inline prose weight, not a button: the reference
   draws it as a quiet link following the text. */
.fsn-bio-full {
    display: inline-block;
    margin-top: 4px;
}

/* =========================================================
   THE PUBLIC PROFILE — B9 through B14
   =========================================================
   Reference: `FSN Redesign Screenshots/Profile/contractor-public-profile.png`.

   The reference is a two-column page under a full-width identity header. The
   columns stack on a narrow viewport; the grid does that by itself, so no
   width-specific rule is needed and nothing is hidden at any width (§9D). */
.fsn-pp-grid {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: 20px;
    align-items: start;
}

@media (max-width: 991.98px) {
    .fsn-pp-grid { grid-template-columns: minmax(0, 1fr); }
}

/* The identity header. `min-width: 0` on the text column is what stops a long
   display name from forcing the header wider than its container. */
.fsn-pp-head {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    flex-wrap: wrap;
}

.fsn-pp-avatar {
    flex: 0 0 80px;
    width: 80px;
    height: 80px;
    border-radius: 50%;
    overflow: hidden;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--bs-gray-200);
    color: var(--bs-gray-700);
    font-weight: 700;
    font-size: 26px;
}

.fsn-pp-avatar img { width: 100%; height: 100%; object-fit: cover; }

/* The tab row scrolls horizontally on a narrow viewport rather than wrapping
   into an unreadable stack — the same treatment the Quote lifecycle row uses.
   The PAGE never scrolls sideways; only this row does (§9D). */
.fsn-pp-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

.fsn-pp-tabs .nav { flex-wrap: nowrap; }
.fsn-pp-tabs .nav-link { white-space: nowrap; }

/* =========================================================
   UI-DASHBOARD-CLIENT-CONTRACTOR-01 — Client and Contractor dashboard layout

   Every rule below is scoped to a `fsn-` class this boundary introduced, so none
   of them can reach an existing page.

   POSITION IS DELIBERATE. This block sits BEFORE the two protected Administrator
   sections that follow — the accessibility block and the palette-contrast block.
   Both are guarded by tests that read from their own section marker to the END OF
   THE FILE and require every selector after it to be admin-scoped, so a rule
   appended after them is treated as part of them. Appending this block at the end
   of the file was tried first and was caught by exactly that guard.

   For the same reason this comment does not spell either section marker out. The
   guards locate their block with a plain string search, so repeating a marker
   here would move the block start to this comment and pull these rules into the
   Administrator contract they are deliberately outside of.

   NO FOCUS RULE IS DECLARED HERE, DELIBERATELY. The shared shell ring
   (`body.fsn-app-shell :focus-visible`, further down this file) is a universal
   descendant selector, so it already covers the KPI tile and the data-table
   summary. Two scoped rules were written first and then removed: measured in the
   browser, the shared rule won at equal specificity and painted the ring anyway,
   so they were a second focus contract that changed nothing.

   The chart PLOT is the only element that needs CSS at all: Chart.js draws into a
   canvas whose height must come from the page, because `maintainAspectRatio` is
   off. Everything else on the two dashboards is Metronic card, grid and utility
   markup.
   ========================================================= */

/* The plot box. `position: relative` is required by Chart.js's responsive
   resizing, which measures the canvas's offset parent. The height arrives as an
   inline `--fsn-chart-height` from the card, so one class serves a 260px
   supporting chart and a 340px dominant one. */
.fsn-chart-canvas {
    position: relative;
    width: 100%;
    height: var(--fsn-chart-height, 300px);
}

/* A canvas is replaced-content and does not shrink below its intrinsic width on
   its own. Without this a re-sized chart can hold the card open and push the page
   into a horizontal scroll at 390px. */
.fsn-chart-canvas > canvas {
    display: block;
    max-width: 100%;
}

/* The accessible data table under each chart. It is a `<details>`, so it costs no
   space until a reader opens it, and the marker stays a real, focusable control. */
.fsn-chart-table > summary {
    cursor: pointer;
    list-style-position: outside;
}

/* A very wide data table (365 daily rows is not possible — the buckets are
   capped — but a long Category list is) scrolls INSIDE the card. The page body
   never scrolls sideways. */
.fsn-chart-table .table-responsive {
    max-height: 320px;
    overflow-y: auto;
}

/* The KPI tile is a link. Metronic's card gives it the surface; these two rules
   give it the affordance and the focus ring the rest of the shell uses. */
.fsn-stat-tile {
    transition: box-shadow 0.15s ease-in-out;
}

/* ═══ THE CONTRACTOR MONEY BAND ═══ BL-PAY-12

   Haris, 2026-09-07: the payout card was "showing in between of the graphs which
   is not looking good". It was a lone col-xl-4 in its own row between the two
   chart bands, so two thirds of that row was empty above 1200px.

   It is now a titled section at the top of the dashboard, mirroring the Client
   balance widget. These rules give the money cells a surface that reads as a
   related group rather than as three unrelated cards, WITHOUT inventing a new
   component: they are ordinary Metronic cards with a border treatment. */
.fsn-payout-cell {
    border: 1px solid var(--bs-gray-200);
    transition: box-shadow 0.15s ease-in-out, border-color 0.15s ease-in-out;
}

.fsn-payout-cell:hover {
    border-color: var(--bs-gray-300);
    box-shadow: var(--bs-box-shadow-sm);
}

/* The money cell is the one he opened the page for, so it carries a quiet accent
   on its leading edge. A colour, not a size: making it taller than its
   neighbours would break the row. */
.fsn-payout-cell--money {
    border-inline-start: 3px solid var(--bs-success);
}

.fsn-payout-cell--prompt {
    border-inline-start: 3px solid var(--bs-primary);
}

/* 🔴 THE FIGURE MUST NOT WRAP AT 390px. A money figure broken across two lines is
   unreadable, and `$400.00` beside a 45px symbol plus a 1rem gap is close.

   MEASURED, because two things here are not what they look like:

     1. Metronic declares `.fs-2hx` as `calc(1.375rem + 1.5vw) !important`, so the
        `!important` below is REQUIRED, not decorative. Without it this rule loses.
     2. Metronic sets the ROOT font size to 12px below the large breakpoint, so
        `2rem` here resolves to 24px and NOT to 32px. That was verified in the
        browser at 390px rather than assumed: computed 24px, no wrap, no
        horizontal overflow, `bodyScrollWidth` 375 against a 390 viewport. */
@media (max-width: 575.98px) {
    .fsn-payout-band .fs-2hx {
        font-size: 2rem !important;
    }
}

/* The date-window control keeps its three options on one line at every width.
   `flex-wrap` on the group would break the shared border radius, so the group
   scrolls instead — the page body still does not. */
.fsn-dashboard-range {
    max-width: 100%;
}

/* At 390px the dashboard heading, its lead line and the window control stack.
   The control keeps a full-width row of its own so all three options stay
   reachable without a horizontal scroll. */
@media (max-width: 575.98px) {
    .fsn-dashboard-range {
        width: 100%;
    }

    .fsn-dashboard-range > .btn {
        flex: 1 1 0;
    }
}

/* ═══ HEADER DROPDOWN PANELS ═══ MESSAGE-WO-ROUTING-01

   A header dropdown is anchored to the RIGHT EDGE OF ITS TRIGGER (`end-0`).
   That is correct while the panel is narrower than the space to the left of the
   trigger, and wrong as soon as it is not.

   Measured at 390 px: the Message trigger's right edge sits at x=282 and the
   panel is 359 px wide, so its left edge landed at x=-77. The page body did not
   scroll — the panel is absolutely positioned and simply clipped — but the
   unread dot, the avatar and the start of every counterpart's name were off
   screen and unreachable. §9D forbids clipping a required control at a supported
   width.

   Below the small breakpoint the panel stops hanging off the trigger and spans
   the viewport with a margin instead. `position: fixed` takes it out of the
   trigger's coordinate space entirely, and `top` clears the mobile header, whose
   measured height is 65 px.

   This applies to BOTH header panels. The notification bell has the same anchor
   and the same exposure; it escaped notice only because it is narrower. */
.fsn-header-panel {
    position: absolute;
    inset-inline-end: 0;
}

@media (max-width: 575.98px) {
    .fsn-header-panel {
        position: fixed;
        inset-inline: 0.5rem;
        top: 65px;
        width: auto !important;
        min-width: 0 !important;
        max-width: none !important;
    }
}

/* ═══ HEADER MESSAGE DROPDOWN ═══ MESSAGE-WO-ROUTING-01

   The message preview is clamped to two lines, as the reference screenshot
   draws it. `-webkit-line-clamp` is the only cross-browser way to clamp by LINE
   rather than by character count, and clamping by characters would cut a short
   wide message and a long narrow one at the same place regardless of the width
   actually available. A fixed max-height keeps the row uniform where the
   property is unsupported. */
.fsn-msg-preview {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    max-height: 2.8em;
    line-height: 1.4;
}

/* ──────────────────────────────────────────────────────────────────────────
   🔴 PLACEMENT MATTERS HERE, AND A TEST ENFORCES IT.

   The admin-accessibility block further down this file is scoped to the admin
   shell, and
   `SmsDeliveryLogPageTest::test_the_accessibility_rules_are_scoped_to_the_admin_shell`
   asserts that EVERY selector from that block's marker TO THE END OF THIS FILE
   is admin-scoped. That is deliberate: it makes a rule which forgets the scope
   fail a test instead of silently restyling the Client and Contractor shells.

   The consequence is that new, unscoped CSS must go ABOVE that block, never
   appended to the end of the file. The home-page rules below were appended to
   the end first, and that test caught them.

   🔴 This comment must NOT repeat the block's marker text verbatim. The test
   locates the block with `strpos`, which finds the FIRST occurrence — so a
   comment quoting the marker becomes the block, and every rule after it is
   dragged into the admin-scope check. That happened here too, on the first
   attempt at this very note.
   ────────────────────────────────────────────────────────────────────────── */

/* ══════════════════════════════════════════════════════════════════════════
   HOME PAGE — the reference build (BL-HOMEPAGE-02)

   Reference: `FSN Redesign Screenshots/Landing/landing-page.png` (1920x6385)
   and `landing-page-header.png` (1901x113).

   Every rule below belongs to the public Landing surface. Nothing here is
   loaded conditionally, because `fsn-overrides.css` is already global; the
   selectors are namespaced with `fsn-` so they cannot reach another screen.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── The sticky header (BL-HOMEPAGE-03, Scope D) ─────────────────────────
   BOTH bars stay visible while scrolling. Metronic's own sticky fixed only the
   MAIN bar to top:0, which slid the utility bar away underneath it - the exact
   defect Scope D forbids - so its mechanism is retired in the partial and this
   wrapper owns positioning instead.

   The heights are TOKENS because three separate things must agree on them: the
   fixed wrapper, the space `.fsn-hero` reserves beneath it, and the
   scroll-margin every in-page anchor needs. Three hard-coded copies would drift,
   and the symptom would be an anchor landing under the header. */
:root {
    --fsn-utility-bar-h: 2.5rem;
    --fsn-main-header-h: 5rem;
    --fsn-header-h: calc(var(--fsn-utility-bar-h) + var(--fsn-main-header-h));
}

.fsn-header-fixed {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    /* Above the coverage map's overlay controls (z-index 2) and above Google's
       own map furniture, which is what sits highest on this page. */
    z-index: 1030;
    background: var(--fsn-navy);
}

/* The header is out of flow, so the band it used to sit inside reserves its
   height. `.fsn-hero` is the right owner: all SEVEN screens that render the
   header render it inside one. */
.fsn-hero {
    padding-top: var(--fsn-header-h);
}

/* 🔴 An in-page anchor must land with its target VISIBLE, not underneath the
   fixed header. `scroll-margin-top` is the property that does this without any
   script and without a spacer element. It is applied to the anchor TARGETS
   themselves, so every current and future section id is covered by one rule.

   🔴 SCOPED TO `body.app-landing`. A bare `[id]` selector would have applied
   this offset to every element carrying an id in the ENTIRE application - admin,
   client and contractor screens included - none of which have this header and
   none of which asked for a scroll offset. The landing body class is set by
   `landing/layout.blade.php` and `landing/signup/layout.blade.php`, which are
   the two layouts behind all seven screens that render the header. */
body.app-landing [id] {
    scroll-margin-top: calc(var(--fsn-header-h) + 1rem);
}

/* ── Utility bar (Scope B, made prominent in Scope C) ────────────────────
   The thin tier ABOVE the main header bar. In the reference it sits on white
   with grey text and a hairline rule beneath. FSN renders the header inside a
   navy `.fsn-hero`, so the same hierarchy is expressed in light-on-dark: the
   bar is a lift of the navy, and the rule beneath it separates the two tiers
   exactly as the reference does. */
.fsn-utility-bar {
    position: relative;
    z-index: 2;
    height: var(--fsn-utility-bar-h);
    display: flex;
    align-items: center;
    /* A lift of the navy rather than a new colour, so the bar reads as part of
       the header and not as a stripe pasted on top of it. It is now OPAQUE by
       inheritance from the fixed wrapper, because it sits over page content. */
    background: rgba(255, 255, 255, 0.06);
    border-bottom: 1px solid rgba(255, 255, 255, 0.14);
    /* Scope C: increased from 0.8125rem. The reference's two links read at
       roughly the nav's own size, not as fine print. */
    font-size: 0.9375rem;
}

.fsn-utility-bar > .container {
    width: 100%;
}

.fsn-utility-link {
    display: inline-block;
    padding: 0.15rem 0.4rem;
    /* Scope C: was rgba(255,255,255,.85). Full white, and semibold, so `Login`
       carries the weight the reference gives it. MEASURED against --fsn-navy:
       21.0:1, far above the 4.5:1 floor. */
    color: #fff;
    font-weight: 600;
    background: none;
    border: 0;
    text-decoration: none;
    line-height: 1.4;
}

.fsn-utility-link:hover,
.fsn-utility-link:focus-visible {
    color: #fff;
    text-decoration: underline;
}

/* The Help-centre placeholder. It is a DISABLED button, so the browser already
   blocks the pointer and removes it from the tab order. This rule only stops it
   looking like a live control: no underline on hover, and the `not-allowed`
   cursor states the same thing the `disabled` attribute tells assistive
   technology. Opacity is kept at .65 rather than lower so the label stays
   legible against the navy — a placeholder must be READABLE to be understood as
   a placeholder. Measured 4.6:1 against `--fsn-navy`. */
/* The Help-centre placeholder.
   🔴 IT IS DELIBERATELY STILL SECONDARY. Scope C asks for both links to be more
   prominent, and this one is - raised from .65 to .80 alpha and given the same
   larger size. It is NOT raised to parity with `Login`, because it is not a
   working control: the visual difference is an honest signal that carries the
   same message as the `disabled` attribute and the visually-hidden "not yet
   available". Making it identical would remove that signal from a sighted
   reader while the semantics stayed put. MEASURED against --fsn-navy: 13.4:1.
   The `disabled` attribute already blocks the pointer and the keyboard; this
   rule only stops it LOOKING live. */
.fsn-utility-link-muted,
.fsn-utility-link-muted:hover {
    color: rgba(255, 255, 255, 0.80);
    font-weight: 500;
    cursor: not-allowed;
    text-decoration: none;
}

.fsn-utility-divider {
    color: rgba(255, 255, 255, 0.45);
    padding: 0 0.4rem;
}

/* The main bar's height is a token too, so the wrapper, the reserved space and
   the anchor offset cannot disagree with it. Metronic's own 100px rule assumed
   its sticky mechanism, which this boundary retired. */
.landing-header {
    height: var(--fsn-main-header-h);
}

/* ── Coverage map (Scopes H and I) ───────────────────────────────────────
   Reference: `landing-page.png` y830-1250. Filters overlay the map top-left,
   the legend card sits bottom-right, and the map fills the band. */
.fsn-coverage {
    padding: 3rem 0;
}

.fsn-coverage-frame {
    position: relative;
    height: 26rem;
    border-radius: 0.625rem;
    overflow: hidden;
    /* The ground the map draws onto. It is also what a visitor sees if the Maps
       API never loads, so it must not read as a broken element. */
    background: var(--bs-gray-200);
}

.fsn-coverage-canvas {
    width: 100%;
    height: 100%;
}

.fsn-coverage-filters {
    position: absolute;
    top: 0.75rem;
    left: 0.75rem;
    z-index: 2;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    /* Bounded so a long region name cannot push the selects across the map. */
    max-width: calc(100% - 1.5rem);
}

.fsn-coverage-filters .form-select {
    min-width: 11rem;
    max-width: 15rem;
    background-color: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
}

.fsn-coverage-legend {
    position: absolute;
    right: 0.75rem;
    bottom: 0.75rem;
    z-index: 2;
    background: #fff;
    border-radius: 0.5rem;
    padding: 0.625rem 0.875rem;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
    max-width: calc(100% - 1.5rem);
    /* A legend with four steps and long numbers must scroll inside ITSELF
       rather than widen the page. §9D: the body never scrolls horizontally. */
    overflow-x: auto;
}

.fsn-coverage-legend-step {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    padding-right: 0.75rem;
    white-space: nowrap;
}

.fsn-coverage-swatch {
    display: inline-block;
    width: 0.9rem;
    height: 0.9rem;
    border-radius: 0.15rem;
}

/* Four steps of ONE hue. A sequential quantity needs a sequential ramp: a
   multi-hue legend would imply the four bands are different kinds of thing
   rather than more and less of the same thing. The ramp is a tint scale of
   `--bs-primary`, which is the colour the map circles are drawn in. */
/* 🔴 THE LEGEND MUST SHOW THE COLOURS THE MAP ACTUALLY PAINTS - BL-HOMEPAGE-08.
   These were FSN red at four opacities while the map filled with the reference's
   orange ramp, so the legend explained a scale that was not on screen. A legend
   that disagrees with its map is worse than none: it is confidently wrong.

   The four swatches are the same interpolation the map uses, sampled at a quarter,
   a half, three quarters and the full range, between rgb(255,197,136) and
   rgb(235,85,0) - the reference values measured on 2026-08-30. */
.fsn-coverage-band-1 { background: rgb(250, 169, 102); }
.fsn-coverage-band-2 { background: rgb(245, 141, 68); }
.fsn-coverage-band-3 { background: rgb(240, 113, 34); }
.fsn-coverage-band-4 { background: rgb(235, 85, 0); }

/* The InfoWindow carries ONLY the number, as the reference does. */
/* 🔴 THE COUNT NO LONGER HAS A LOOK OF ITS OWN, AND THAT IS THE POINT.

   `.fsn-coverage-info` styled the number inside a `google.maps.InfoWindow`. Measured
   at 1440: a 60x80 white bubble around 38x22 of content, a 25x12 pointer tail, and a
   `.gm-ui-hover-effect` close control 48x48 and VISIBLE - a control more than twice
   the size of the number beside it. Haris: too large, and wrong.

   The count is now shown by `bootstrap.Tooltip`, the same component the six
   `data-bs-toggle="tooltip"` call sites elsewhere in FSN use. Its appearance comes
   from Metronic's own `.tooltip-inner`, so there is nothing to declare here and
   nothing to keep in step. Measured: 42x39 against the 60x80 it replaces.

   What remains is the ANCHOR - an invisible point Popper positions against. */
.fsn-coverage-tip {
    position: absolute;
    width: 0;
    height: 0;
    /* It is a positioning device, never a target. Without this it would sit over the
       map and swallow the click that reveals the next state. */
    pointer-events: none;
}

/* The textual equivalent. It is bounded and scrolls INSIDE itself, so a long
   region list never lengthens the page without limit. */
.fsn-coverage-table {
    max-height: 18rem;
    overflow-y: auto;
    overflow-x: auto;
}

/* ── The header at phone width ───────────────────────────────────────────
   🔴 TWO STACKED FIXED BARS ON A PHONE CAN EAT A SMALL SCREEN. The tokens are
   reduced here so the combined height stays a modest share of the viewport, and
   the measured figure is reported in the boundary review rather than assumed.
   Neither bar is hidden or collapsed - Haris asked for both to stay visible. */
@media (max-width: 991.98px) {
    :root {
        --fsn-utility-bar-h: 2.25rem;
        --fsn-main-header-h: 3.75rem;
    }

    .fsn-utility-bar {
        font-size: 0.875rem;
    }
}

@media (max-width: 575.98px) {
    /* At 390px two selects side by side leave neither usable, and an overlay
       covering half the map hides the thing being filtered. They stack, and the
       frame grows to keep the map area worth looking at. */
    .fsn-coverage-frame {
        height: 30rem;
    }

    /* An absolutely positioned box shrinks to fit its content, so `max-width`
       alone left the stacked selects at their content width (MEASURED at 390px:
       171px of an available 327px). Pinning BOTH edges gives the column a real
       width, so each select spans the frame and is comfortable to tap. */
    .fsn-coverage-filters {
        flex-direction: column;
        left: 0.75rem;
        right: 0.75rem;
        max-width: none;
    }

    .fsn-coverage-filters .form-select {
        min-width: 0;
        max-width: none;
        width: 100%;
    }

    /* Below the legend's natural width the card would overlap the filters. It
       moves to the full width of the frame's foot instead. */
    .fsn-coverage-legend {
        left: 0.75rem;
        right: 0.75rem;
    }
}

/* ── Brand logos (BL-HOMEPAGE-04, Scope C) ──────────────────────────────
   CONFIRMED FROM USER 2026-08-29: both marks read as small against the rest
   of the interface.

   🔴 HEIGHT ONLY. `width` is left at its `auto` default so the browser derives
   it from the intrinsic size, which is what keeps the mark from being stretched.
   A width here — even a "matching" one — would be a second source of truth for
   one shape, and the two would drift the moment the asset changed.

   🔴 MEASURED BEFORE CHANGING. `assets/images/logo-white.png` is a 1600x549
   RASTER; there is no FSN vector anywhere in the repository. Enlarging a raster
   past its intrinsic width blurs it, so the targets were checked against the
   source first:

     header desktop  44px tall -> 128.2px wide -> needs 385px at 3x DPR (4.2x margin)
     header mobile   30px tall ->  87.4px wide -> needs 262px at 3x DPR (6.1x margin)
     footer          48px tall -> 139.9px wide -> needs 420px at 3x DPR (3.8x margin)

   The source carries 1600px, so every target is clean. Its ceiling is a 533px
   rendered width at 3x. If a vector is ever supplied it should replace the PNG,
   and these rules stay as they are. */
.fsn-brand-logo {
    height: 44px;
    width: auto;
}

/* The footer mark is LARGER than the header's, deliberately. The header sits in
   a sticky bar whose height it has to live inside; the footer has no such
   constraint, and a footer logo is the page's closing signature. */
.fsn-footer-logo {
    height: 48px;
    width: auto;
}

@media (max-width: 991.98px) {
    /* The header bar is shorter at phone and tablet widths, so the mark inside it
       comes down with it rather than crowding the bar. The footer is unaffected —
       it has room. */
    .fsn-brand-logo {
        height: 30px;
    }
}
/* ── Hero animation (BL-HOMEPAGE-03, Scope B) ────────────────────────────
   FSN's own supplied artwork, 1200 x 750. */

/* 🔴 THE RATIO IS RESERVED BEFORE ANYTHING LOADS.
   `aspect-ratio: 1.6` is the MEASURED ratio of the artwork (1200/750 = 1.6000
   exactly), so the box occupies its final height at first paint and the arriving
   animation shifts nothing below it. Layout shift on a marketing hero is a real
   defect, and it is the whole reason this is CSS rather than a size the player
   sets on a canvas it creates later. */
.fsn-hero-animation {
    position: relative;
    width: 100%;
    aspect-ratio: 1.6;
    /* `overflow` still clips, so the SVG can never spill outside its reserved
       box. The radius only shapes that clip; nothing is painted here. */
    border-radius: 0.625rem;
    overflow: hidden;

    /* 🔴 NO BORDER AND NO BACKGROUND. CONFIRMED FROM USER 2026-08-29: the frame
       around the artwork read as a prominent box against the navy hero.
       It was also wrong on its own terms — Haris supplied the animation to be
       shown on a TRANSPARENT background, and a container that paints a ground
       behind it is not transparent. The artwork now sits directly on the hero.

       The ground the loading and failure states need has moved onto
       `.fsn-hero-animation-static`, which is the only thing that needs
       something to sit on. */
    background: transparent;
}

/* A browser with no `aspect-ratio` support still reserves the space, via the
   padding ratio. 750/1200 = 62.5%. Without this the fallback is a zero-height
   box, and the shift comes back on exactly the older browsers least able to
   absorb it. */
@supports not (aspect-ratio: 1.6) {
    .fsn-hero-animation {
        height: 0;
        padding-top: 62.5%;
    }
}

.fsn-hero-animation-canvas,
.fsn-hero-animation-static {
    position: absolute;
    inset: 0;
}

.fsn-hero-animation-canvas > svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* The designed static state. Visible from first paint, hidden only once a frame
   has actually been drawn - so every failure mode ends here rather than on an
   empty box or a broken-asset icon. */
.fsn-hero-animation-static {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
    text-align: center;
    /* The ground lives HERE, not on the container. It is what stops the
       reserved box being a hole in the hero while the artwork loads, and what
       gives the failure state something to sit on — without framing the
       animation once it arrives. */
    background: rgba(255, 255, 255, 0.06);
    border-radius: 0.625rem;
}

.fsn-hero-animation-static-inner {
    max-width: 22rem;
}

.fsn-hero-animation-mark {
    height: 2rem;
    width: auto;
    opacity: 0.9;
    margin-bottom: 0.85rem;
}

.fsn-hero-animation-caption {
    color: rgba(255, 255, 255, 0.85);
    font-size: 0.9375rem;
    line-height: 1.5;
}
/* ── Contact form (Scope F) ──────────────────────────────────────────────── */
.fsn-contact {
    padding: 3rem 0;
    background: var(--bs-gray-100);
}

/* The honeypot.
   Positioned off-screen rather than `display:none`. A field that is display:none
   is the first thing a competent bot skips, while an off-screen field looks like
   a real one to a parser and is invisible to a person. It also carries
   `aria-hidden` and `tabindex="-1"` in the markup, so assistive technology and
   the keyboard both pass over it. */
.fsn-honeypot {
    position: absolute;
    left: -9999px;
    top: auto;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* ── Customer logo strip (home section 4) — PUBLIC-PAGES-AND-BLOG-01 B1 ──
   Field Nation's treatment: one row of logos, no caption, a line of figures below.

   🔴 ONE OPTICAL SIZE FROM FOUR DIFFERENT FILES. A wide wordmark and a square tile
   at the same height do not look the same size: the tile carries far more ink. So
   each logo carries a treatment class, and each class sets a height that gives the
   marks a similar visual weight, all centred on one line.

   The Xero file is a wide wordmark in the middle of a square canvas. `object-fit:
   cover` on a wide box shows the wordmark and crops only the empty canvas, so the
   supplied file is used unchanged. */
.fsn-logo-strip {
    /* Haris, 2026-09-29: the logos span the whole width of the section rather than
       sitting as a compact group. Four equal columns across the container, each
       logo centred in its own column. */
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    align-items: center;
    gap: 2rem;
    padding: 0;
}

.fsn-logo-strip-item {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 5.5rem;
}

.fsn-logo {
    display: block;
    width: auto;
    max-width: 100%;
}

.fsn-logo-wordmark {
    height: 3.25rem;
}

.fsn-logo-padded-wordmark {
    height: 3.25rem;
    aspect-ratio: 200 / 56;
    object-fit: cover;
}

.fsn-logo-tile {
    height: 5rem;
    border-radius: 0.5rem;
}

/* #4b5675 on white measures 7.0:1, the same muted colour as `.fsn-article-excerpt`. */
.fsn-logo-strip-figures {
    margin-top: 1.75rem;
    text-align: center;
    font-size: 1.075rem;
    font-weight: 600;
    color: #4b5675;
}

.fsn-logo-strip-separator {
    margin: 0 0.75rem;
}

@media (max-width: 767.98px) {
    /* Two columns on a phone or a small tablet, so each logo keeps a readable size. */
    .fsn-logo-strip {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 1.5rem 1rem;
    }

    .fsn-logo-strip-item {
        height: 4rem;
    }

    .fsn-logo-wordmark,
    .fsn-logo-padded-wordmark {
        height: 2.5rem;
    }

    .fsn-logo-tile {
        height: 3.75rem;
    }
}

@media (max-width: 575.98px) {
    .fsn-logo-strip-separator {
        display: block;
        height: 0;
        overflow: hidden;
        margin: 0;
    }

    .fsn-logo-strip-figures span:not(.fsn-logo-strip-separator) {
        display: block;
    }
}

/* ── Testimonials and awards (home sections 8 and 9) — B2 and B3 ─────────
   Rendered only when an approved record exists; see the view. */
/* The About page photograph — round 2, B4. Shown whole: the width follows the
   column and the height follows the photograph, so nothing is cut. */
.fsn-about-image {
    display: block;
    width: 100%;
    height: auto;
    border-radius: 0.75rem;
}

/* The "does not check" mark: the FSN navy, not a warning colour. It is a plain fact,
   not an alarm. */
.fsn-about-limit-mark {
    color: var(--fsn-navy);
}

.fsn-testimonial-mark {
    font-size: 3rem;
    line-height: 1;
    font-weight: 700;
    color: #b30f1c;
}

.fsn-testimonial blockquote {
    margin: 0;
}

/* Round 4 — the optional photo or logo at the top of a testimonial card (Field Nation's
   order). It may be a company mark or a person's photograph, so the whole image is
   shown inside a fixed-height box: never cropped, never stretched. */
.fsn-testimonial-image {
    display: block;
    align-self: flex-start;
    width: auto;
    /* A fixed height reserves the space before the file arrives, so the card does not
       jump inside the carousel (review finding 8). */
    height: 4rem;
    max-width: 10rem;
    object-fit: contain;
}

/* The LinkedIn link: navy text, underlined, with a visible focus ring from the shared
   public focus rule. */
.fsn-testimonial-linkedin {
    color: var(--fsn-navy);
    text-decoration: underline;
    text-underline-offset: 0.15em;
}

.fsn-testimonial-linkedin:hover {
    color: #b30f1c;
}

/* The testimonial carousel. Bootstrap draws its indicators white for a photograph
   behind them; here they sit on the light section ground, so they are drawn in the
   FSN navy, with the current slide at full strength. */
.fsn-testimonial-indicators [data-bs-slide-to] {
    width: 0.625rem;
    height: 0.625rem;
    margin: 0 0.3rem;
    border: 0;
    border-radius: 50%;
    background-color: var(--fsn-navy);
    opacity: 0.3;
}

.fsn-testimonial-indicators .active {
    opacity: 1;
}

.fsn-testimonial-control {
    flex-shrink: 0;
    background-color: #ffffff;
    border: 1px solid var(--bs-gray-300);
}

/* Slides of different lengths must not make the section jump as it moves. */
.fsn-testimonial-carousel .fsn-testimonial {
    min-height: 18rem;
}

@media (max-width: 575.98px) {
    .fsn-testimonial-carousel .fsn-testimonial {
        min-height: 25rem;
    }
}

.fsn-award-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 4rem;
    height: 4rem;
    border-radius: 50%;
    background: rgba(var(--fsn-navy-rgb), 0.08);
    color: var(--fsn-navy);
}

/* =========================================================
   LIGHT FOOTER — the reference treatment (BL-HOMEPAGE-05)
   =========================================================

   CONFIRMED FROM USER 2026-08-30: Haris approved the reference's light footer.

   WHY A CLASS AND NOT A BACKGROUND SWAP. Everything sitting on the footer had
   been chosen for a NAVY ground: a white logo, gray-600 body text, gray-500
   headings, and a dark dashed separator. On a light ground the logo becomes
   invisible and the rest becomes illegible. The ground and its contents change
   together, which is why this is one named block rather than one property.

   MEASURED, NOT JUDGED. Every ratio below is computed against the #F9F9F9
   ground and asserted by `FooterLightTreatmentTest`:

     gray-800 #252F4A  column headings          12.59:1   AA, AAA
     gray-700 #4B5675  links, body, copyright    6.91:1   AA, AAA
     primary  #e11d2e  link hover                4.52:1   AA (thin margin - see below)

   gray-600 #78829D was REJECTED at 3.64:1. It is the colour the footer used on
   navy, where it measured 4.22:1 - which FAILS AA (4.5:1) and had failed since
   the footer was written. The recolour fixes a real accessibility defect; it is
   not only a visual change.

   🔴 THE HOVER MARGIN IS THIN AND THAT IS DELIBERATE. #e11d2e on #F9F9F9 is
   4.52:1 against a 4.5:1 requirement - a 0.02 margin. The brand red is the
   established FSN interaction colour and changing it is a branding decision
   nobody asked for, so it stays. The fragility is converted into a guarded
   property instead: the test recomputes this ratio, so any future change to the
   ground or to --bs-primary that drops it below AA fails the suite.

   🔴 WHY #F9F9F9 AND A BORDER, WHEN THE REFERENCE IS PURE WHITE WITH NEITHER.
   This is a DELIBERATE, EVIDENCE-BASED DEVIATION, and the evidence is that this
   footer is a SHARED PARTIAL on SEVEN screens (CLAUDE.md 6D-1):

     /                        navy .fsn-hero closing band above  ->  contrast 15.36:1
     /legal/privacy-policy    WHITE page content above
     /legal/client-terms      WHITE page content above
     /legal/contractor-terms  WHITE page content above
     /signup                  WHITE page content above
     /signup/client           WHITE page content above
     /signup/contractor       WHITE page content above

   The reference's footer can be pure white because Field Nation always places a
   saturated orange band directly above it. FSN does not: on SIX of the seven
   screens the content above is white. A pure-white footer would separate on Home
   and MERGE on the other six - moving the exact defect Haris reported rather
   than fixing it. #F9F9F9 against #ffffff is only 1.05:1, so the tint alone does
   not do the work either. The 1px gray-300 top border is the mechanism that
   separates the footer from white content, and it is what makes the light
   treatment safe on all seven screens.

   🔴 NO MARGIN, ANYWHERE IN THIS BLOCK. Separation above a footer is the classic
   way to leave a strip of page background BELOW it on a short page. The
   separation here is colour and a border - both of which are painted INSIDE the
   footer box - plus existing padding. `FooterLightTreatmentTest` asserts no
   margin is declared, and browser QA proves the footer still reaches the
   document bottom on the shortest screen at a viewport taller than the page.

   🔴 READ THAT LAST CLAIM PRECISELY (clarified 2026-08-30 by BL-HOMEPAGE-06).
   The footer did reach the DOCUMENT bottom, and still does. What it did not do
   was reach the VIEWPORT bottom, because the document itself stopped short —
   595.32px short on `/signup/client` at 1440x2400. The sentence above is true
   and was never evidence about the strip. The block directly above this one
   fixes it and records the measurement.
   ========================================================= */

/* =========================================================
   THE SHORT-PAGE STRIP — BL-HOMEPAGE-06
   =========================================================

   🔴 A DEFECT THAT WAS INVISIBLE, NOT ABSENT, AND THAT IS WHY IT SURVIVED.

   On a page shorter than the viewport, the footer stopped where its content
   stopped and page background was painted BELOW it. Measured on
   `/signup/client` at 1440x2400: the footer's bottom edge sat at 1804.68px in a
   2400px viewport, leaving a 595.32px strip.

   It is OLD. `BL-HOMEPAGE-05` proved it pre-existing by neutralising every
   property that boundary added and re-measuring: 595.32px with the change,
   596.12px without — the light footer contributed 0.80px of 595. What the light
   footer DID do is hide it. Against the old navy footer that strip was 16.17:1
   and obvious; against #F9F9F9 it is 1.05:1 and effectively unseeable. An
   invisible defect is the one that survives, so it is fixed here rather than
   left for someone to notice again.

   🔴 THE CAUSE, AND WHY THE HALF THAT WAS ALREADY RIGHT DID NOT HELP.
   Metronic's `style.bundle.css` already sets `body { display: flex;
   flex-direction: column; }`. The column is there. What is missing is a reason
   for it to be TALLER than its content — `min-height` computed to `0px` — and
   anything to absorb the slack. Two declarations supply both.

   `100dvh` follows `100vh` on purpose. A browser that does not understand
   dynamic viewport units ignores the second declaration and keeps `100vh`; one
   that does uses `dvh`, which tracks a mobile browser's collapsing toolbar
   instead of overshooting by its height.

   🔴 SCOPED TO `body.app-landing`, WHICH IS EXACTLY THE FOOTER'S SEVEN SCREENS.
   Measured, not assumed: `/`, `/signup`, `/signup/client`, `/signup/contractor`
   and the three `/legal/*` documents carry `app-landing`. The four auth screens
   carry `fsn-app-shell`, have no landing footer, and are untouched by this rule.

   `FooterReachesViewportBottomTest` asserts the outcome at 1440 and 390.
   ========================================================= */

body.app-landing {
    min-height: 100vh;
    min-height: 100dvh;
}

/* The slack goes to the page content, so the footer is pushed to the bottom
   rather than stretched. `1 0 auto` grows and never shrinks below its content,
   so a page TALLER than the viewport is unaffected — it simply has no slack. */
body.app-landing > main {
    flex: 1 0 auto;
}

.fsn-footer-light {
    background-color: var(--bs-gray-100);
    border-top: 1px solid var(--bs-gray-300);
}

/* Column headings: bold and darkest, matching the reference's weight contrast. */
.fsn-footer-light .fsn-footer-heading {
    color: var(--bs-gray-800);
}

/* Body copy and the copyright line. */
.fsn-footer-light .fsn-footer-text {
    color: var(--bs-gray-700);
}

/* 🔴 SPECIFICITY, deliberately (0,3,0). Metronic colours its menus through
   `.menu-gray-600 .menu-item > .menu-link`, which is also (0,3,0). Equal
   specificity is decided by order, and fsn-overrides.css loads AFTER
   style.bundle.css in base/app.blade.php, so these win. A lower-specificity
   selector would silently lose and leave gray-600 links at 3.64:1. */
.fsn-footer-light .menu-item > .menu-link {
    color: var(--bs-gray-700);
}

.fsn-footer-light .menu-item > .menu-link:hover,
.fsn-footer-light .menu-item > .menu-link:focus-visible {
    color: var(--bs-primary);
}

/* The in-footer divider. It was `landing-dark-separator`, a dashed #2c3f5b line
   built to read on navy; on a light ground it is far too heavy. */
.fsn-footer-separator {
    border-top: 1px dashed var(--bs-gray-300);
}

/* ══════════════════════════════════════════════════════════════════════════
   THE PUBLIC BLOG — BL-BLOG-01

   Reference: `metronic_html_v8.3.2_demo11/demo11/pages/blog/home.html`
   ("Latest Articles, News & Updates", line 4010) and `pages/blog/post.html`
   (the content region at line 4000).

   PUBLIC-PAGES-AND-BLOG-01 (2026-09-29): the posts are rows in the database and
   carry real photographs — licensed repository files for the first posts, and
   Administrator uploads after that. The CSS media band that stood in for a
   photograph is retired. The reference's YouTube embed and stock portraits are
   still not used: the embed is a third-party origin, and a byline stays a
   monogram rather than a face.
   ══════════════════════════════════════════════════════════════════════════ */

/* The lead image and the in-body images — PUBLIC-PAGES-AND-BLOG-01 C3.4.
   The CSS media band that stood here was retired when the posts gained real,
   licensed photographs. Both images keep a 16:9 box so the text below does not
   jump while the file loads. */
.fsn-article-lead,
.fsn-article-body-image {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    background-color: var(--bs-gray-100);
}

/* The byline mark. Two letters where the reference puts a photograph of a
   person. `symbol-label` supplies the circle; only the colours are set here. */
.fsn-article-monogram {
    background-color: var(--fsn-navy);
    color: #ffffff;
    letter-spacing: 0.02em;
}

/* ── 🔴 REDUCED MOTION, AND A DEFECT THIS BOUNDARY FOUND ─────────────────
   `html { scroll-behavior: smooth }` is set globally near the top of this file
   with NO `prefers-reduced-motion` guard. Every in-page jump in the whole
   application therefore animated, including for a viewer whose operating system
   asks it not to — a vestibular-accessibility defect, not a style preference.

   It is fixed here rather than at the original rule because the original is
   inside the scrollspy block and a reader there would not know why the guard
   exists. `landing-in-page-nav.js` reads the SAME media query, so the scripted
   scroll and the CSS agree instead of one overriding the other.

   `scroll-padding-top` is deliberately NOT reset: it is an offset, not motion,
   and removing it would land an anchor underneath the fixed header. */
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }
}

/* 🔴 The scripted in-page scroll moves FOCUS to the target section, because a
   native anchor jump does that for free and a script does not. The target is
   given `tabindex="-1"` at that moment, which makes it focusable and would
   otherwise draw a focus ring around a whole page section. The ring is removed
   only for that programmatic case; every real keyboard focus ring is untouched.

   `:focus-visible` is NOT used to express this. A `-1` target never matches it,
   so the rule would have no subject. */
body.app-landing [data-fsn-scroll-target]:focus {
    outline: none;
}

/* ── 🔴 THE MOBILE MENU WAS INVISIBLE — WHITE TEXT ON A WHITE DRAWER ─────
   Found by BL-NAV-01 while measuring the menu at 390 px. It PRE-DATES that
   boundary: measured at 16f63f8, with the boundary's changes stashed, FOUR of the
   five items rendered at contrast 1.00.

     Home                #e11d2e on #ffffff   4.75   readable (it carries .active)
     Coverage            #ffffff on #ffffff   1.00   INVISIBLE
     How it works        #ffffff on #ffffff   1.00   INVISIBLE
     Service categories  #ffffff on #ffffff   1.00   INVISIBLE
     Contact             #ffffff on #ffffff   1.00   INVISIBLE

   ROOT CAUSE. `#kt_landing_menu` carries `menu-title-white`, which is correct
   inside the navy `.landing-header`. Below `lg`, Metronic's drawer SWAPS the menu
   out of the header and into `<body>` -- `data-kt-swapper-parent="{default:
   '#kt_body', lg: '#kt_header_nav_wrapper'}"`. The white titles then sit on the
   white body, and nothing had measured the menu in its swapped position.

   THE FIX IS COLOUR ONLY. No layout change, no collapse pattern, no truncation --
   the drawer, its width, its items and their order are all untouched. The items
   that were already there simply become readable, and so does the one BL-NAV-01
   adds.

   SCOPED TO THE SWAPPED POSITION. `body > [data-kt-drawer-name="landing-menu"]`
   matches ONLY when the drawer has been moved to the body, which happens only
   below `lg`. The desktop header keeps its white titles: at >= 992 px the menu is
   back inside `.landing-header` and this selector cannot match it. */
body > [data-kt-drawer-name="landing-menu"] {
    background-color: var(--fsn-navy);
}

body > [data-kt-drawer-name="landing-menu"] .menu-link {
    color: #ffffff;
}

/* The current item keeps its emphasis, in a tint that reads on navy rather than
   the red that was chosen for a white ground.

   🔴 THE `.menu-item` STEP IS LOAD-BEARING, and it was added after measuring. A
   first draft wrote `… .menu-link.active` and the FSN red kept winning, leaving
   the current item at 3.40:1 on navy while its five siblings measured 16.17:1.
   Metronic's own active rule carries the extra class, so this one has to match it
   to take effect. Measured after the change: 12.23:1. */
body > [data-kt-drawer-name="landing-menu"] .menu-item .menu-link.active {
    color: #ffd6da;
}

/* ── 🔴 THE CATEGORY BADGE IS FSN'S OWN, AND CONTRAST IS WHY ─────────────
   The reference gives each card a light pill with coloured text, one colour per
   category. That TREATMENT is copied. Metronic's `badge-light-*` classes are NOT
   used to express it, and the reason was measured in the browser rather than
   assumed:

     badge-light-warning   1.58:1   unreadable
     badge-light-info      3.23:1   below AA for small text
     badge-light-primary   4.44:1   below AA, and it renders RED text on a GREEN
                                    ground because FSN's theme maps primary to red
                                    while that class keeps a green background

   Those classes are used elsewhere in the application and are NOT changed here —
   that would be an unrelated change to screens this boundary does not own. The
   blog defines its own tag chip instead, `.fsn-blog-tag`, on the navy-on-pale-blue
   pair the retired category badge measured as clearing AA.

   `SharedPartialContrastFloorTest` records the standard this follows. */
.fsn-blog-tag {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.35rem 0.75rem;
    border-radius: 0.375rem;
    font-size: 0.875rem;
    font-weight: 600;
    background-color: #e7edf9;
    color: #0b1e4a;
}

.fsn-blog-tag:hover,
.fsn-blog-tag:focus-visible {
    background-color: #d3ddf3;
    color: #0b1e4a;
}

/* The selected tag. White on the FSN navy. */
.fsn-blog-tag-active,
.fsn-blog-tag-active:hover,
.fsn-blog-tag-active:focus-visible {
    background-color: #0b1e4a;
    color: #ffffff;
}

.fsn-blog-tag-count {
    font-weight: 700;
    opacity: 0.8;
}

/* The Administrator blog screens: a list thumbnail and an upload preview. */
.fsn-admin-blog-thumb {
    width: 4.5rem;
    height: 2.5rem;
    object-fit: cover;
}

.fsn-admin-blog-preview {
    display: block;
    width: 100%;
    max-width: 22rem;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    background-color: var(--bs-gray-100);
}

/* Round 4 — a testimonial's photo or logo, shown whole on the form (a logo must not be
   cropped the way a blog photograph may be). */
.fsn-admin-testimonial-preview {
    display: block;
    max-width: 100%;
    max-height: 8rem;
    width: auto;
    height: auto;
    object-fit: contain;
    background-color: var(--bs-gray-100);
}

/* The blog tag field — PUBLIC-PAGES-AND-BLOG-01 round 3, D2 and D3.
   ONE control: the selected tags are pills inside the bordered box, beside the text
   box, and the Add button is the group's `input-group-text` addon. The ring is drawn
   around the whole box while any part of it has focus, so the field reads as one. */
.fsn-tag-box {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.375rem;
    min-height: calc(1.5em + 1.55rem + 2px);
    padding-top: 0.4rem;
    padding-bottom: 0.4rem;
    cursor: text;
}

.fsn-tag-box-input {
    flex: 1 1 9rem;
    min-width: 9rem;
    padding: 0.25rem 0;
    border: 0;
    outline: 0;
    background: transparent;
    color: inherit;
    font: inherit;
}

/* The group's corners, so its focus ring (in `fsn-app-shell.css`) follows them. */
.fsn-tag-control {
    border-radius: 0.475rem;
}

.fsn-tag-pill {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
    padding: 0.1rem 0.1rem 0.1rem 0.7rem;
    border: 1px solid rgba(11, 30, 74, 0.3);
    border-radius: 999px;
    background-color: #eef1f7;
    color: var(--fsn-navy);
    font-size: 0.95rem;
    font-weight: 600;
    line-height: 1.4;
}

/* 24px square at the admin root size: the WCAG 2.2 minimum target (2.5.8). */
.fsn-tag-pill-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.85rem;
    height: 1.85rem;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--fsn-navy);
}

.fsn-tag-pill-remove i {
    color: inherit;
}

.fsn-tag-pill-remove:hover {
    background-color: rgba(11, 30, 74, 0.12);
}

.fsn-tag-add {
    cursor: pointer;
    font-weight: 600;
    color: var(--fsn-navy);
    transition: background-color 0.15s ease;
}

.fsn-tag-add:hover {
    background-color: var(--bs-gray-200);
}

/* The tag suggestion list — round 4, item 2. A real listbox under the field, so a
   picked suggestion is a click the widget sees, not an `input` event it cannot tell
   apart from typing. `x-cloak` is scoped here: the project defines no global rule. */
.fsn-tag-suggestions[x-cloak] {
    display: none !important;
}

.fsn-tag-suggestions {
    position: absolute;
    z-index: 5;
    top: 100%;
    left: 0;
    right: 0;
    max-height: 15rem;
    overflow-y: auto;
    margin: 0.25rem 0 0;
    padding: 0.25rem 0;
    list-style: none;
    background-color: #ffffff;
    border: 1px solid var(--bs-gray-400);
    border-radius: 0.475rem;
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.08);
}

.fsn-tag-suggestion {
    padding: 0.5rem 0.875rem;
    cursor: pointer;
    color: var(--bs-gray-900);
}

.fsn-tag-suggestion:hover,
.fsn-tag-suggestion.active {
    background-color: rgba(11, 30, 74, 0.08);
    color: var(--fsn-navy);
}

/* The article body's rich text — PUBLIC-PAGES-AND-BLOG-01 round 3, D4.
   Only `WorkScopeHtml::render()` output reaches these elements. The rules follow the
   ARTICLE's own scale, not the compact Work Scope reader: an editor heading matches
   the separate heading block (Metronic fs-3 for h2, weight 600, the same spacing), and
   bold is 700 against the 500 body, so it stays visible. */
.fsn-article-body .fsn-rich-text p {
    margin: 0 0 1.25rem;
}

.fsn-article-body .fsn-rich-text > :last-child,
.fsn-article-body .fsn-rich-text li > p {
    margin-bottom: 0;
}

.fsn-article-body .fsn-rich-text h1,
.fsn-article-body .fsn-rich-text h2,
.fsn-article-body .fsn-rich-text h3,
.fsn-article-body .fsn-rich-text h4,
.fsn-article-body .fsn-rich-text h5,
.fsn-article-body .fsn-rich-text h6 {
    margin: 2.5rem 0 1.25rem;
    color: var(--bs-gray-900);
    font-weight: 600;
    line-height: 1.3;
}

.fsn-article-body .fsn-rich-text > :first-child {
    margin-top: 0;
}

/* The Metronic fs-2 to fs-6 scale, fluid below 1200px exactly as those utilities are. */
.fsn-article-body .fsn-rich-text h1 { font-size: calc(1.275rem + 0.3vw); }
.fsn-article-body .fsn-rich-text h2 { font-size: calc(1.26rem + 0.12vw); }
.fsn-article-body .fsn-rich-text h3 { font-size: 1.25rem; }
.fsn-article-body .fsn-rich-text h4 { font-size: 1.15rem; }
.fsn-article-body .fsn-rich-text h5,
.fsn-article-body .fsn-rich-text h6 { font-size: 1.075rem; }

@media (min-width: 1200px) {
    .fsn-article-body .fsn-rich-text h1 { font-size: 1.5rem; }
    .fsn-article-body .fsn-rich-text h2 { font-size: 1.35rem; }
}

.fsn-article-body .fsn-rich-text strong,
.fsn-article-body .fsn-rich-text b {
    font-weight: 700;
    color: var(--bs-gray-900);
}

.fsn-article-body .fsn-rich-text ul,
.fsn-article-body .fsn-rich-text ol {
    margin: 0 0 1.25rem;
    padding-left: 1.5rem;
}

.fsn-article-body .fsn-rich-text li {
    margin-bottom: 0.4rem;
}

/* A task list stores no checkbox (the allow-list drops `input`); the state is drawn
   from `data-checked`. */
.fsn-article-body .fsn-rich-text ul[data-type="taskList"] {
    padding-left: 0;
    list-style: none;
}

.fsn-article-body .fsn-rich-text li[data-checked] {
    display: flex;
    gap: 0.6rem;
}

.fsn-article-body .fsn-rich-text li[data-checked]::before {
    content: "\2610";
    flex: 0 0 auto;
}

.fsn-article-body .fsn-rich-text li[data-checked="true"]::before {
    content: "\2611";
}

.fsn-article-body .fsn-rich-text blockquote {
    margin: 0 0 1.25rem;
    padding-left: 1rem;
    border-left: 3px solid var(--bs-gray-300);
    color: #4b5675;
}

.fsn-article-body .fsn-rich-text a {
    color: var(--bs-primary);
    text-decoration: underline;
}

.fsn-article-body .fsn-rich-text a:hover,
.fsn-article-body .fsn-rich-text a:focus-visible {
    color: var(--fsn-navy);
}

/* A wide table scrolls inside the article; the page never scrolls sideways. */
.fsn-article-body .fsn-rich-text table {
    display: block;
    max-width: 100%;
    margin: 0 0 1.25rem;
    overflow-x: auto;
    border-collapse: collapse;
}

.fsn-article-body .fsn-rich-text th,
.fsn-article-body .fsn-rich-text td {
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--bs-gray-300);
    vertical-align: top;
}

.fsn-article-body .fsn-rich-text th {
    background-color: var(--bs-gray-100);
    color: var(--bs-gray-900);
    font-weight: 700;
}

.fsn-article-body .fsn-rich-text hr {
    margin: 1.5rem 0;
}

/* 🔴 The excerpt and the article body. `text-gray-600` measured 3.64:1 at 15px,
   which is below AA for normal text. This is a scoped replacement, not a change
   to the utility class, so no other screen moves. */
.fsn-article-excerpt {
    color: #4b5675;
}

/* 🔴 The post page's meta row. `text-gray-500` measured 2.58:1 at 13px on white,
   which fails AA badly for the two facts a reader most needs — the date and the
   category. Scoped, like the excerpt above, so no other screen moves. */
.fsn-article-meta,
.fsn-article-muted {
    color: #4b5675;
}

/* ══════════════════════════════════════════════════════════════════════════
   HOME — "HOW FSN WORKS" (BL-SHELL-01, scope D)

   🔴 THE NUMBERS WERE REMOVED, SO SOMETHING ELSE HAS TO CARRY THE SEQUENCE.
   The markup is an `<ol>`, which carries it semantically. This rail carries it
   visually: it is drawn from the list itself, so it cannot disagree with the
   markup order the way a hand-placed graphic could.
   ══════════════════════════════════════════════════════════════════════════ */
.fsn-step-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.fsn-step {
    position: relative;
    display: flex;
    gap: 1rem;
    padding-bottom: 1.75rem;
}

.fsn-step:last-child {
    padding-bottom: 0;
}

/* The rail. It starts below one mark and stops at the next, so the last step has
   no trailing line dangling into nothing. */
.fsn-step:not(:last-child)::before {
    content: "";
    position: absolute;
    left: 1.375rem;
    top: 2.75rem;
    bottom: 0.5rem;
    width: 2px;
    background: var(--bs-gray-300);
}

.fsn-step-mark {
    flex: 0 0 auto;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #e7edf9;
    color: var(--fsn-navy);
    position: relative;
    z-index: 1;
}

.fsn-step-mark i {
    color: var(--fsn-navy);
}

.fsn-step-body {
    padding-top: 0.25rem;
}

/* The two lanes are equal-weight, so their cards match height and their headers
   read as a pair rather than as a heading and a subheading. */
.fsn-lane-card .card-header {
    min-height: auto;
    padding-top: 1.25rem;
    padding-bottom: 1.25rem;
}

/* ── HOME — "BUILT AROUND THE WORK ORDER" (scope E) ──────────────────────
   Larger cards with an image. The image has an intrinsic 3:2 ratio and carries
   `width`/`height`, so the browser reserves its box before the file arrives and the
   text below it does not jump — a layout shift is a real defect, not a nicety. */
.fsn-benefit-card {
    overflow: hidden;
    min-height: 26rem;
}

/* THE BENEFIT CARDS CARRY LICENSED PHOTOGRAPHS (PUBLIC-PAGES-AND-BLOG-01, 2026-09-29).

   Both rules below are live, and they are not alternatives to each other any more:

     - `.fsn-benefit-image` carries the licensed photograph on the home page's three
       benefit cards;
     - `.fsn-benefit-emblem` is STILL USED, by the three pillars on `/about`, which
       never carried a photograph and were built on this treatment from the start.

   Deleting the emblem because the home page stopped using it would have taken
   `/about`'s pillars with it. Both keep the same 12.5rem panel height, so a card
   built on either has the same proportions. */
.fsn-benefit-image {
    width: 100%;
    height: 12.5rem;
    object-fit: cover;
    display: block;
}

/* Drawn entirely in CSS from `--fsn-navy`, FSN's own brand colour, with one glyph
   from Bootstrap Icons — MIT, vendored by FSN, digest-checked. It needs no licence
   question of its own, which is what made it the right treatment for `/about`. */
.fsn-benefit-emblem {
    width: 100%;
    height: 12.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    /* A brand gradient, not a flat block: a single colour at this size reads as a
       missing image rather than a deliberate one. */
    background: linear-gradient(135deg, var(--fsn-navy) 0%, #16336f 100%);
}

.fsn-benefit-emblem .bi {
    font-size: 4rem;
    line-height: 1;
    color: #ffffff;
}

.fsn-benefit-card .card-body {
    padding: 1.5rem;
}

/* ══════════════════════════════════════════════════════════════════════════
   HOME — SERVICE CATEGORIES (scope F) and ARTICLES (scope G)

   Reference: fieldnation.com, MEASURED in the browser on 2026-08-31. The category
   cards there carry a CSS `background-image` and zero `<img>` tags, which is why a
   text extraction reported no imagery. FSN uses a real `<img>` instead, so the image
   has intrinsic dimensions and the browser can reserve its box.
   ══════════════════════════════════════════════════════════════════════════ */

.fsn-category-card {
    position: relative;
    overflow: hidden;
    border-radius: 0.625rem;
    min-height: 15rem;
    display: block;
}

/* 🔴 THE CATEGORY PHOTOGRAPH IS BACK, and `.fsn-category-emblem` went with it.

   Unlike the benefit emblem above, nothing else in FSN rendered this one — it existed
   only to stand in for the category photograph, so restoring the photograph orphaned
   it outright. A rule no selector reaches is dead weight that reads as live code, and
   the change that created the orphan is the change that should clear it.

   The label and the hover overlay stack on top of this image exactly as they stacked
   on the emblem, and at the same 15rem height, so nothing above it moves. */
.fsn-category-image {
    width: 100%;
    height: 15rem;
    object-fit: cover;
    display: block;
}

/* The resting label. It sits on a dark scrim rather than directly on the photograph,
   because a white word on an unknown image has no measurable contrast at all. */
.fsn-category-label {
    position: absolute;
    inset: auto 0 0 0;
    padding: 1rem;
    color: #ffffff;
    /* 🔴 AN OPAQUE-ENOUGH background-COLOR, not only a gradient, and the reason is
       measurable. A `linear-gradient` is a background-IMAGE: `getComputedStyle`
       reports `background-color: rgba(0,0,0,0)` for it, so every contrast resolver —
       including this project's — walks past it and computes the white label against
       the page's white ground at 1.00:1. The browser paints it correctly and every
       automated check reports it as invisible.
       A colour is declared first so the ground is real to a resolver AND survives if
       the gradient does not render; the gradient sits on top for the soft edge.
       White on rgba(11,30,74,0.92) over any photograph stays at or above 13.6:1. */
    background-color: rgba(11, 30, 74, 0.92);
    background-image: linear-gradient(to top, rgba(11, 30, 74, 0.35), rgba(11, 30, 74, 0));
}

.fsn-category-label h3 {
    color: #ffffff;
}

/* 🔴 THE OVERLAY, AND WHY THIS EXACT COLOUR.
   The brief asks for the primary colour at partial opacity. FSN's `--bs-primary` is
   #e11d2e, and white on SOLID #e11d2e measures 4.76:1 — which sounds safe and is not,
   because the overlay is TRANSLUCENT. Composited over a light photograph it moves
   towards white and the ratio FALLS:

       #e11d2e at 0.90 over white  ->  #e43443  ->  4.32:1   FAILS AA
       #e11d2e at 0.94 over white  ->  #e32b3a  ->  4.50:1   exactly on the floor

   A value that lands exactly on the floor is not a passing value; it is a rounding
   error away from failing. The overlay therefore uses #b30f1c — the darker primary
   this stylesheet already uses for the corrected badge text — at 0.92:

       #b30f1c at 0.92 over white  ->  #b9222e  ->  6.34:1   PASSES with margin

   White is the worst case a photograph can present, so 6.34 is a floor and not an
   average. `CategoryOverlayContrastTest` composites this rule and asserts it. */
.fsn-category-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: rgba(179, 15, 28, 0.92);
    opacity: 0;
    transition: opacity 0.18s ease;
}

/* 🔴 THREE WAYS IN, BECAUSE HOVER IS ONLY ONE OF THEM.
   `:focus-within` is what makes the card keyboard-reachable: tabbing to the button
   inside opens the same state a mouse opens. Without it the control would be focusable
   and invisible, which is worse than not being focusable at all. */
.fsn-category-card:hover .fsn-category-overlay,
.fsn-category-card:focus-within .fsn-category-overlay {
    opacity: 1;
}

/* 🔴 A TOUCH DEVICE CANNOT HOVER. Without this the Learn More control would be
   unreachable on every phone and tablet — the card would look interactive and do
   nothing. On a pointer-less device the overlay is simply always open. */
@media (hover: none) {
    .fsn-category-overlay {
        opacity: 1;
        background-color: rgba(179, 15, 28, 0.86);
    }

    /* With the overlay always open the resting label would sit under it, so it is
       hidden and the overlay carries the name instead. */
    .fsn-category-label {
        display: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .fsn-category-overlay {
        transition: none;
    }
}

/* ── Articles (scope G) ─────────────────────────────────────────────────── */

.fsn-blog-card {
    display: flex;
    flex-direction: column;
    background: #ffffff;
    border: 1px solid var(--bs-gray-300);
    border-radius: 0.625rem;
    overflow: hidden;
}

.fsn-blog-image {
    width: 100%;
    height: 11rem;
    object-fit: cover;
    display: block;
}

.fsn-blog-body {
    padding: 1.5rem;
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
}

.fsn-blog-category {
    align-self: flex-start;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: #b30f1c;
}

/* 🔴 THE DESCRIPTION IS CLAMPED, AND THE TWO CASES BEHAVE DIFFERENTLY.

   SHORTER than the clamp: nothing happens. `-webkit-line-clamp` only truncates when
   the content exceeds the line count, so a one-line description renders as one line.
   The `min-height` is what keeps the cards level in that case — without it a short
   description would leave its card shorter than its neighbours and the row would look
   ragged.

   LONGER than the clamp: the text is cut at the fourth line and an ellipsis is added
   by the browser. The full text is still in the DOM, so a screen reader reads all of
   it; only the visual box is bounded.

   Four lines, not three, because at 390px the same sentence wraps to more lines than
   it does at 1440px, and a three-line clamp truncated descriptions on a phone that
   were complete on a desktop. */
.fsn-blog-excerpt {
    color: #4b5675;
    display: -webkit-box;
    -webkit-line-clamp: 4;
    line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
    min-height: 5.25rem;
}

/* ── SIGN-UP SIDE CARDS (scope H) ────────────────────────────────────────
   Reference: `Signup/client-signup-page.png`. Vertically dominant, moderate width,
   a colour ground per card, an icon above a subordinate label.

   🔴 THE CARD IS AN ANCHOR, so it needs a visible focus indicator of its own. Without
   one the card would be keyboard-operable and give no sign of where the keyboard is —
   the failure mode that makes a focusable control worse than an unfocusable one. */
.fsn-signup-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
    padding: 2rem 1.75rem;
    border-radius: 0.625rem;
    /* 🔴 VERTICALLY DOMINANT, MEASURED RATHER THAN INTENDED.
       A first attempt used `min-height: 17rem` in a `col-lg-4` and measured
       400 x 224 — WIDER than tall, the opposite of the reference. Two reasons, both
       easy to miss: Metronic's root font-size is 13px, not 16px, so 17rem is 221px
       and not 272px; and a third of a 1200px container is 400px, which no sensible
       height beats.
       The width is therefore capped as well as the height raised. Measured after:
       247 x 338, a ratio of 1.37 — taller than wide, at every width above `sm`. */
    /* 🔴 TALLER, to match the reference's proportions rather than approach them.
       Measured on fieldnation.com/get-started at 1440: 275x458 and 275x410, ratios
       1.67 and 1.49. FSN measured 247x338, a ratio of 1.37 — vertically dominant
       already, but less so than the thing it is modelled on. 30rem at Metronic's 13px
       root is 390px, which against the same 247px width gives 1.58 — inside the
       reference's own range. */
    min-height: 30rem;
    max-width: 19rem;
    width: 100%;
    text-decoration: none;
    margin-bottom: 1.5rem;
    /* The positioning context for the full-bleed mark, and the clip that keeps it
       inside the rounded corner. */
    position: relative;
    overflow: hidden;
    /* 🔴 NO `transform` HERE. See the hover rule below: the old hover moved the card,
       which moves the layout. Only colour and shadow transition now. */
    transition: box-shadow 0.15s ease, filter 0.15s ease;
}

.fsn-signup-card:last-child {
    margin-bottom: 0;
}

/* 🔴 A FULL-BLEED GRADIENT GROUND, NOT A FLAT FILL.

   CONFIRMED FROM USER (Haris, 2026-08-31): the cards were "solid and quite sharp in
   color with no background", and that is what had to change.

   They were `background-color: #b30f1c` and `background-color: var(--fsn-navy)` — two
   flat rectangles. The treatment is an `--fsn-navy` gradient carrying a large icon, the
   same system as `.fsn-benefit-emblem`, which `/about`'s three pillars also use.

   🔴 THE REFERENCE USES PHOTOGRAPHS AND THESE CARDS DO NOT. Measured on
   fieldnation.com/get-started at 1440: a right-hand column at x=1028 holding two tall
   images, 275x458 and 275x410. FSN reproduces the CARD ARCHITECTURE — two tall cards,
   moderate width, a large mark, a heading in white — on a gradient ground instead.

   🔴 THE REASON CHANGED ON 2026-09-01, AND THE OLD ONE MUST NOT BE LEFT STANDING.
   This comment used to end "because blocker 12 closed by removing photography and
   nothing may reintroduce it". That was true for one day. Haris reversed the removal
   decision on 2026-09-01: the Metronic demo imagery is back on the home page and the
   `/signup` role cards as TRACKED PLACEHOLDERS, and blocker 12 is OPEN again.

   These cards keep the gradient anyway — not because photography is forbidden, but
   because a gradient carrying the role's own icon reads better here than a stock
   photograph would, and because every placeholder FSN does publish is now enumerated
   in `App\Support\Content\PlaceholderImagery`. Adding one here means adding it there.

   The two cards keep DIFFERENT hues so they read as two choices rather than one
   repeated tile: the technician card leans on FSN red, the account card on FSN navy.
   Both are gradients, and both carry the same white type. */
.fsn-signup-card-primary {
    /* 🔴 THE LIGHTEST STOP IS #c01d29, NOT THE #d4212f A FIRST DRAFT USED.
       Measured: white on #d4212f is 5.17 at rest but 4.53 once hover's
       `brightness(1.08)` lifts it — above the 4.5 floor by 0.03, which is not a margin,
       it is a rounding error. #c01d29 measures 6.09 at rest and 5.40 under hover. The
       gradient still reads as a gradient; the worst pair on the card gained nearly a
       whole point. */
    background: linear-gradient(135deg, #8f0c16 0%, #b30f1c 55%, #c01d29 100%);
}

.fsn-signup-card-navy {
    background: linear-gradient(135deg, var(--fsn-navy) 0%, #16336f 55%, #1d4390 100%);
}

/* 🔴 THE FULL-BLEED MARK. A large glyph bled off the card's own edge, behind the
   content, so the ground reads as a treatment rather than a panel. It is decorative and
   is drawn from the icon font already loaded — no image, from any source.

   `overflow: hidden` on the card keeps it inside the rounded corner; `pointer-events:
   none` keeps it out of the way of the one control. */
.fsn-signup-card::after {
    content: "";
    position: absolute;
    right: -1.5rem;
    bottom: -2rem;
    width: 9rem;
    height: 9rem;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.08);
    pointer-events: none;
}

/* White on #b30f1c measures 8.02:1 and on the navy #0b1e4a 15.36:1. Both clear AA
   for normal text with margin, so the eyebrow and the lead are safe at any size. */
.fsn-signup-card,
.fsn-signup-card i,
.fsn-signup-card .fsn-signup-card-label,
.fsn-signup-card .fsn-signup-card-lead {
    color: #ffffff;
}

/* Every child sits above the full-bleed mark. Without this the glyph would paint over
   the label and the action. */
.fsn-signup-card > * {
    position: relative;
    z-index: 1;
}

.fsn-signup-card-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3.5rem;
    height: 3.5rem;
    border-radius: 0.5rem;
    background: rgba(255, 255, 255, 0.16);
    margin-bottom: 0.75rem;
}

/* Subordinate to the icon: smaller, tracked out, and beneath it. */
.fsn-signup-card-label {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    opacity: 0.85;
}

.fsn-signup-card-lead {
    font-size: 1.35rem;
    font-weight: 600;
    line-height: 1.3;
    margin-bottom: 1.25rem;
}

.fsn-signup-card-action {
    margin-top: auto;
    pointer-events: none;   /* the anchor is the control; this is a drawn affordance */
}

/* 🔴 A STATE CHANGE THAT DOES NOT MOVE THE LAYOUT.

   The old hover was `transform: translateY(-2px)`. A transform does not reflow the
   document, but it DOES move the card under the pointer, and on a card whose whole
   surface is the control that shifts the target out from under the cursor mid-click.
   The brief asked for a hover that reads as a state change WITHOUT moving the layout.

   `filter: brightness()` lifts the whole gradient by a measurable amount, and the
   shadow deepens. Nothing moves, and the contrast floor covers the lifted ground: at
   1.08 every gradient stop is lifted, and every lifted stop is measured against white
   in `SignupCardContrastTest`. The worst pair on either card is white on the red
   card's lightest stop: 6.09 at rest, 5.40 under hover. */
.fsn-signup-card:hover,
.fsn-signup-card:focus-visible {
    filter: brightness(1.08);
    box-shadow: 0 0.5rem 1.5rem rgba(11, 30, 74, 0.25);
}

/* 🔴 THE FOCUS RING IS SEPARATE FROM THE HOVER LIFT, and it already existed.
   `BL-SHELL-01` gave these cards a white outline over a navy halo. The `brightness`
   above is a wash, and it is the SAME wash hover uses, so it cannot be the focus
   indicator on its own — a keyboard user must be able to see which card they are on.
   This rule is left exactly as it was; only the hover state changed around it. */
.fsn-signup-card:focus-visible {
    outline: 3px solid #ffffff;
    outline-offset: 3px;
    box-shadow: 0 0 0 6px rgba(11, 30, 74, 0.55);
}

@media (prefers-reduced-motion: reduce) {
    /* The hover no longer uses a transform — it lifts the gradient with `filter` and
       moves nothing — so there is no motion left to suppress. The transition is still
       switched off, because a fade is motion to someone who asked for none. */
    .fsn-signup-card { transition: none; }
}

/* SELECT2 PLACEHOLDER — the one pair below AA on a public page.

   MEASURED IN A BROWSER on `/signup/contractor` and `/signup/client`, 2026-08-31:
   `.select2-selection__placeholder` renders `#6c757d` on the FSN control ground
   `#f5f8fa` = **4.40:1**, against a 4.5 floor for 13px/400 text. It is the text a
   visitor reads BEFORE choosing anything, on the two screens where they create an
   account, so it is the state the control spends most of its life in.

   `#626a72` measures **5.15:1** on `#f5f8fa` and 5.49:1 on white. It is a small step
   down from the vendor grey, so the placeholder still reads as secondary against the
   `#212529` of a real selection — a placeholder as dark as the value it stands in for
   would trade one defect for another.

   🔴 `!important` IS LOAD ORDER, NOT EMPHASIS. `base/app.blade.php` links the Select2
   theme AFTER this file, and the theme's own selector is four classes deep
   (`.select2-container--bootstrap-5 .select2-selection--single
   .select2-selection__rendered .select2-selection__placeholder`). A later rule of
   higher specificity cannot be beaten by an earlier one of any specificity, so the
   only alternatives were to reorder the links — which changes the cascade for every
   other Select2 rule — or this.

   NOT SCOPED TO THE PUBLIC PAGES. The same control renders the same failing pair on
   the Client and Contractor screens. Scoping the correction to the pages it was
   measured on would repeat `project-rules.md` §6D exactly: a fix proved in one
   context, left out of every other context that has the same defect. */
.select2-selection__placeholder {
    color: #626a72 !important;
}

/* SWEETALERT2 CLOSE BUTTON — found by the floor, NOT by the browser pass.

   The library paints the close glyph `#cccccc` on the dialog's white ground =
   **1.61:1**, against a 4.5 floor. `#707070` measures **4.95:1** and keeps the glyph
   clearly secondary to the title.

   🔴 WHY THE BROWSER PASS MISSED IT, AND WHY IT IS STILL WORTH FIXING.

   `Swal.fire` renders `.swal2-close` into every dialog and hides it unless
   `showCloseButton: true`. FSN's `footer_scripts.js` never sets that option, so the
   button is in the DOM of every dialog and is painted in none of them. A browser
   measurement walks visible text and skipped it; the cascade floor reads rules rather
   than pixels and reported it.

   That difference is the useful one. The button is one option away from being visible,
   the option would be set in a component with no reason to think about contrast, and a
   1.61:1 control is not something a later reader would look for. Correcting it now
   costs one rule and removes the trap. It changes nothing that renders today.

   `!important` IS LOAD ORDER: SweetAlert2 ships its CSS inside the JS bundle and
   injects a `<style>` into `<head>` at run time, so its rules always come after every
   linked stylesheet. There is no ordering this file can win by. */
.swal2-close {
    color: #707070 !important;
}

/* FSN HERO BACKGROUND
   =========================================================

   MOVED OUT OF `style.bundle.css`, WHERE IT DID NOT BELONG.

   These eleven declarations were FSN-authored CSS written at the end of
   Metronic's vendor bundle. A Metronic upgrade replaces that file wholesale, so
   the hero background would have disappeared from every landing screen with
   nothing in the repository to say why - the rule would be gone, not overridden,
   and no diff on an FSN file would show it.

   THE MOVE IS ORDER-AND-LOCATION ONLY, AND THAT WAS MEASURED, NOT ASSUMED.
   The declarations are byte-for-byte the ones that were in the bundle. The only
   sheet between the old position (the end of `style.bundle.css`) and this one is
   `fsn-overrides.css` itself, and every declaration in it that touches any of
   these eleven properties was enumerated: none has a selector that can match an
   element carrying `fsn-hero`. The `.fsn-hero-animation*` rules target nested
   children, and the only wildcard subject, `.fsn-signup-card > *`, matches spans
   inside a card and declares the same `position: relative` in any case.
   `fsn-app-shell.css` loads after this file and every selector in it is scoped to
   `body.fsn-app-shell` or `body.aside-enabled`, which a landing page never carries.

   `VendorStylesheetOwnershipTest` now fails if any FSN-authored rule is written
   inside a vendor file again. */

.fsn-hero {
    position: relative;
    background-color: var(--fsn-navy);
    background-image:
        linear-gradient(rgba(11, 30, 74, 0.78),
            rgba(11, 30, 74, 0.78)),
        url("/assets/images/hero-bg.jpg");
    background-size: cover;
    /* Haris, 2026-09-29: the subject must sit on the LEFT, behind the headline, not
       behind the animation on the right. `12%` keeps the worker's head in frame. */
    background-position: left 12%;
    background-repeat: no-repeat;
}

/* The HOME hero only — Haris, 2026-09-29: the overlay is lighter (0.78 -> 0.66) so the
   photograph shows through. Measured against the brightest pixel behind each line, the
   white headline and lead clear AA at every width (lowest 5.35:1). The other landing
   bands keep 0.78: their 75%-white eyebrows and standfirsts fall below AA at 0.66. */
.fsn-hero.fsn-hero--home {
    background-image:
        linear-gradient(rgba(11, 30, 74, 0.66),
            rgba(11, 30, 74, 0.66)),
        url("/assets/images/hero-bg.jpg");
}

/* The blog's "All blogs" link on the hero band: 75% white at rest (it measured below
   AA at 50%), full white on hover and keyboard focus. `opacity-75` is !important in
   Bootstrap, so this must be too. */
.fsn-hero-back-link:hover,
.fsn-hero-back-link:focus-visible {
    opacity: 1 !important;
}

.fsn-hero::after {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at center,
            rgba(255, 255, 255, 0.05),
            transparent 60%);
    pointer-events: none;
}

/* =========================================================
   THE SHARED STYLESHEET ENDS BELOW THIS NOTE — APPEND ABOVE IT
   =========================================================

   SHELL-SCOPED RULES NO LONGER LIVE IN THIS FILE.

   Every rule scoped to `body.fsn-app-shell` or `body.aside-enabled` moved to
   `fsn-app-shell.css`. `base/app.blade.php` loads that file immediately after this
   one, so the cascade is unchanged: the shell rules still resolve last.

   WHY THE MOVE HAPPENED. Two consecutive boundaries appended public rules to the end
   of this file and landed inside its trailing admin-scoped region, where three guards
   require every selector to be shell-scoped. A written rule failed twice. The
   structure now makes the mistake impossible instead of forbidden: the region is not
   in this file, so appending here cannot reach it.

   IF YOU APPEND A SHELL-SCOPED RULE HERE, IT FAILS LOUDLY.
   `StylesheetRegionSeparationTest` reads this file and asserts that no selector
   starts with `body.fsn-app-shell` or `body.aside-enabled`. It reads
   `fsn-app-shell.css` and asserts that every selector does. Neither file can hold the
   other file's rules, and neither failure is silent. */

/* ══════════════════════════════════════════════════════════════════════════
   THE TAB ROW — `BL-PAY-08`, and a defect that had been shipping unnoticed.

   🔴 FOUND BY BROWSER QA, NOT BY A TEST. The Contractor Payouts tab row rendered
   as three raw browser buttons — `border: 1.6px outset`, `padding: 1px 6px`,
   `font-weight: 400` — measured from `getComputedStyle` on the real host.

   THE CAUSE IS `§6D`: COPYING A PROVEN SHAPE DOES NOT COPY ITS CONTEXT. The
   `.fsn-fb-tabs` / `.fsn-fb-tab` markup was copied from the Client Financials
   page, which took it from the Flight Board. But the rules live inside
   `components/ui/flight-board.blade.php`, in a `<style>` block scoped to a
   `.fsn-flight-board` ancestor — and that block only ships on the two pages that
   actually render the Flight Board component.

   🔴 SO THE CLIENT FINANCIALS PAGE HAS BEEN RENDERING UNSTYLED TABS SINCE IT
   SHIPPED. It has no `.fsn-flight-board` ancestor either, and nothing loads that
   CSS for it. This is pre-existing and was NOT introduced by `BL-PAY-08`; the
   Payouts page reproduced it by inheriting the same markup.

   Fixing only the new page would have made the two sides look DIFFERENT, which
   is the opposite of what `C1` asked for. One rule set, in the sheet that already
   owns site-wide FSN classes, repairs both.

   The Flight Board's own scoped block has higher specificity
   (`.fsn-flight-board .fsn-fb-tab`), so those two pages are unchanged. This is
   the fallback for every OTHER page that uses the project's tab vocabulary. */

.fsn-fb-tabs {
    display: flex;
    flex-wrap: nowrap;
    /* Wide tab rows scroll INSIDE this region. The page body must never scroll
       horizontally (§9D). */
    overflow-x: auto;
    border: 1px solid var(--bs-border-color);
    border-radius: 0.475rem;
    background: var(--bs-body-bg);
}

.fsn-fb-tab {
    flex: 1 0 auto;
    min-width: 120px;
    padding: 0.75rem 1rem;
    text-align: center;
    white-space: nowrap;
    background: transparent;
    border: 0;
    border-right: 1px solid var(--bs-border-color);
    color: var(--bs-gray-700);
    font-weight: 600;
}

.fsn-fb-tab:last-child {
    border-right: 0;
}

.fsn-fb-tab:hover,
.fsn-fb-tab:focus-visible {
    color: var(--bs-primary);
}

.fsn-fb-tab.active {
    background: var(--bs-primary);
    color: #fff;
}

/* ── The keyboard focus ring on the PUBLIC pages — PUBLIC-PAGES-AND-BLOG-01 round 2, B6.
   Measured by tabbing every changed public page: the header "Get started" button,
   the hero and closing buttons, every contact-form input and every footer link drew
   NO focus indicator, because Metronic sets `outline: 0` and `fsn-app-shell.css`
   scopes its ring to `body.fsn-app-shell` on purpose.
   This is the same ring, with the same two colours, for `body.app-landing`.
   `:focus-visible` only, so a mouse click draws nothing.
   On a navy band (`.fsn-hero`, which also wraps the header and the closing band)
   the order is reversed: the white line carries the contrast and the navy halo
   separates it from a white button. */
body.app-landing {
    --fsn-focus-ring: #071437;
    --fsn-focus-halo: #ffffff;
}

body.app-landing :focus-visible,
body.app-landing .form-control:focus-visible,
body.app-landing .form-select:focus-visible,
body.app-landing .btn:focus-visible,
body.app-landing .menu-link:focus-visible,
body.app-landing .nav-link:focus-visible {
    outline: 2px solid var(--fsn-focus-ring) !important;
    outline-offset: 2px;
    box-shadow: 0 0 0 2px var(--fsn-focus-halo) !important;
}

body.app-landing .fsn-hero :focus-visible,
body.app-landing .fsn-utility-bar :focus-visible {
    outline: 2px solid var(--fsn-focus-halo) !important;
    outline-offset: 2px;
    box-shadow: 0 0 0 4px var(--fsn-focus-ring) !important;
}

/* ── The match score and its breakdown — STAGING-REVIEW-B4, card 20 (M-6), as
   changed by the addendum (Haris, 2026-10-21).

   The BADGE follows `Client/quote-listing-ui.png`: a filled number pill and a short
   word, small, under the avatar. FSN theme tokens only: no band colours, no
   Field Nation blue.

   The PANEL follows the same listing's hover panel (`quote-distance-tooltip.png`,
   `.fsn-q-dist-panel` above): above the score, centred, a soft lifted shadow, one
   fact per row, the caret Haris required on 2026-08-14 and the hover bridge. It
   shows on hover, on keyboard focus (`:focus-visible`, so a mouse click does not
   keep it open after the pointer leaves) and on a tap (`.is-open`, set by the
   component). `--fsn-match-shift` moves it, and the caret back, only as far as the
   screen or the scroll box edge requires; `.fsn-match--below` opens it below the
   score when there is no room above. */
.fsn-match {
    position: relative;
    display: inline-flex;
    cursor: default;
    border-radius: 999px;
}

.fsn-match:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: 2px; }

.fsn-match-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 1px 6px 1px 1px;
    border: 1px solid var(--bs-gray-300);
    border-radius: 999px;
    background: var(--bs-gray-100);
    color: var(--bs-gray-800);
    font-size: 11px;
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap;
}

/* The strongest element of the badge, as the reference's number pill. Gray-900 on
   the body colour reverses with the theme, so both themes keep a high contrast. */
.fsn-match-pill {
    padding: 0 6px;
    border-radius: 999px;
    background: var(--bs-gray-900);
    color: var(--bs-body-bg);
    font-weight: 700;
}

.fsn-match-panel {
    display: none;
    position: absolute;
    bottom: calc(100% + 10px);
    left: 50%;
    transform: translateX(calc(-50% + var(--fsn-match-shift, 0px)));
    z-index: 30;
    /* 300 px keeps each factor on one line, as the reference panel keeps each fact. */
    width: 300px;
    max-width: calc(100vw - 32px);
    padding: 14px 16px;
    flex-direction: column;
    gap: 10px;
    border: 1px solid var(--bs-gray-200);
    border-radius: 8px;
    background: var(--bs-body-bg);
    box-shadow: 0 8px 24px rgba(15, 23, 42, .14);
    font-size: 13px;
    font-weight: 400;
    line-height: 1.3;
    text-align: left;
    white-space: normal;
}

.fsn-match:hover .fsn-match-panel,
.fsn-match:focus-visible .fsn-match-panel,
.fsn-match.is-open .fsn-match-panel { display: flex; }

/* The hover bridge across the 10 px offset, as `.fsn-q-dist-panel::before`. */
.fsn-match-panel::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: -12px;
    height: 12px;
}

/* The caret points at the score, wherever the shift has moved the panel. */
.fsn-match-panel::after {
    content: '';
    position: absolute;
    left: calc(50% - var(--fsn-match-shift, 0px));
    bottom: -6px;
    width: 11px;
    height: 11px;
    margin-left: -5.5px;
    background: var(--bs-body-bg);
    border-right: 1px solid var(--bs-gray-200);
    border-bottom: 1px solid var(--bs-gray-200);
    transform: rotate(45deg);
}

.fsn-match--below .fsn-match-panel { bottom: auto; top: calc(100% + 10px); }
.fsn-match--below .fsn-match-panel::before { bottom: auto; top: -12px; }
.fsn-match--below .fsn-match-panel::after {
    bottom: auto;
    top: -6px;
    border: 0;
    border-left: 1px solid var(--bs-gray-200);
    border-top: 1px solid var(--bs-gray-200);
}

.fsn-match-row {
    display: grid;
    grid-template-columns: 18px 40px 1fr auto;
    align-items: baseline;
    column-gap: 8px;
}

.fsn-match-icon { color: var(--bs-primary); }
.fsn-match-weight { font-size: 12px; white-space: nowrap; }

/* The Applicants card's avatar column: the avatar, then the score under it, as the
   reference places it. The badge overlaps the bottom of the ring by a few pixels. */
.fsn-q-avatar-col {
    flex: 0 0 auto;
    /* The avatar's own top alignment moves to the column, so the card keeps its
       shape: top of the row on a desktop, the start of the stack on a phone. */
    align-self: flex-start;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.fsn-q-avatar-col .fsn-q-avatar-lg { align-self: center; }
.fsn-q-avatar-col .fsn-match { margin-top: -8px; z-index: 1; }

/* Inside a scroll box (the Invite / Route candidate list) the component fixes the
   panel to the screen and sets `left`, `top` and the caret position itself, so the
   list's own overflow can never clip the breakdown. */
.fsn-match-panel--fixed {
    position: fixed;
    bottom: auto;
    transform: none;
}

.fsn-match-panel--fixed::after { left: var(--fsn-match-caret, 50%); }

/* STAGING-REVIEW-B4 U-20-3 (CONFIRMED FROM USER, Haris, 2026-10-21): a contractor name in the
   Invite / Route row wraps only between words, never inside a word. */
.fsn-name-wrap {
    white-space: normal;
    word-break: normal;
    overflow-wrap: normal;
    hyphens: none;
}

/* STAGING-REVIEW-B4 C20-1: the band word before the percentage ("Good · 86% match"). */
.fsn-match-band { font-weight: 600; }
.fsn-match-sep { color: var(--bs-gray-500); }
.fsn-match-row--band { padding-bottom: 0.25rem; border-bottom: 1px solid var(--bs-gray-200); }
