/* ===========================================================================
   EasyQuoteNext -- one stylesheet for the whole application.

   SELF-HOSTED ONLY. No CDN, ever, and no web font. The legacy app pulls from
   six external hosts, several over plain http on an https page, and one page
   loads its stylesheet from w3schools.com -- every one of those is a failure
   point on a flaky link. A web font would be 30-100KB before a single pixel of
   content renders, so the type stack below is entirely local.

   No npm, no build step, no preprocessor: one file of custom properties, in the
   house style of QbAutoBilling.Web.

   Structure
     1  tokens
     2  reset and base
     3  layout: the full-viewport rule
     4  application shell (top bar, navigation)
     5  page furniture (headings, cards, panels)
     6  forms and controls
     7  buttons
     8  data grid
     9  pagination
    10  status pills (legacy traffic-light colours, meaning preserved)
    11  busy states
    12  login
    13  utilities and motion
   =========================================================================== */

/* ---------------------------------------------------------------------------
   1  TOKENS
   Colour discipline is 60 neutral / 30 ink / 10 brand, per MD Docs/Web Design
   Brief.md. Hierarchy comes from weight and size, not from extra colours.
   --------------------------------------------------------------------------- */
:root {
    /* Brand. #465c71 is EasyQuote's existing slate chrome, carried across so the
       new screens are recognisably the same product. --brand-deep is the one
       used behind white text: the mid slate fails WCAG AA at small sizes. */
    --brand:            #465c71;
    --brand-deep:       #33475a;
    --brand-tint:       #eef2f7;
    --brand-tint-strong:#dde5ee;

    /* Ink and neutrals. */
    --ink:              #1b2733;
    --ink-soft:         #47566a;
    --muted:            #6b7a8d;
    --bg:               #f4f6f9;
    --surface:          #ffffff;
    --border:           #e2e8f0;
    --border-strong:    #cbd5e1;

    /* Status. These three are LOAD-BEARING: the call-status traffic lights at
       EQnet/Main.Master:44-66 carry meaning to users and must not be restyled
       into something prettier. Green #459E00, black #000000, red #CC0000. */
    --good:             #459e00;
    --neutral:          #000000;
    --bad:              #cc0000;

    /* ------------------------------------------------------------------
       HOW TALL A DATA GRID MAY GET before it scrolls inside itself, which is
       what keeps its column headings on screen (see .grid-scroll).

       100vh less an allowance for the chrome above it -- the 56px title bar,
       the 34px menu strip, the page heading and the filter row. Deliberately
       ONE number rather than per-screen: a heading that sticks on one report
       and not another is worse than either.

       A short table is unaffected. This is a MAXIMUM, so a grid with six rows
       stays six rows tall and never shows a scrollbar.
       ------------------------------------------------------------------ */
    --grid-viewport:    calc(100vh - 15rem);

    /* ------------------------------------------------------------------
       THE STICKY CHROME'S HEIGHT, as ONE number.

       Two tiers, both sticky: .topbar at top:0, and .menustrip at top:56px
       with a 34px inner -- which is where 90px comes from, and it is the
       project's own figure already (rule 5.4d quotes the same number when
       it explains the page-level sticky heading it rejected).

       IT EXISTS SO THAT ANYTHING SCROLLED TO LANDS BELOW THE CHROME rather
       than underneath it. Used as scroll-margin-top, which both
       scrollIntoView() and a #fragment jump honour -- so one number serves
       the script and the browser, and they cannot drift apart.
       ------------------------------------------------------------------ */
    --eq-chrome-h:      90px;

    /* Tints for the status pills, so the three hues above stay exact while the
       pill background stays light enough for dark text. */
    --good-tint:        #e8f5e0;
    --bad-tint:         #fbe6e6;
    --warn:             #b26a00;
    --warn-tint:        #fdf0dd;
    --info-tint:        #e7eef7;

    /* Legacy error colours, reused for form validation so the two systems agree
       on what "wrong" looks like: error pink #ffb5b5, error text #990000. */
    --error-bg:         #ffb5b5;
    --error-ink:        #990000;

    /* Microsoft Excel's own brand green, on Tim's instruction (21 August 2026)
       that the Excel button carry the Excel colour. #217346 is the current Office
       value; the darker #1a5c38 is for hover.

       WHY A BRAND COLOUR IS ALLOWED HERE when nothing else in this application
       uses one: the green is not decoration, it is the affordance. The legacy
       screens signal "this downloads a spreadsheet" with the green Excel icon
       (images/Excel Icon.jpg, used on every report in EQNetAdmin), so a user of
       twenty years reads green-and-Excel as one thing. Reproducing the SIGNAL
       without reproducing a bitmap is the point -- see .btn--excel. */
    --excel:            #217346;
    --excel-hover:      #1a5c38;

    /* Type. One family, local only. Segoe UI Variable is present on Windows 11,
       which is what every user of this system runs. */
    --font: "Segoe UI Variable Text", "Segoe UI", -apple-system, BlinkMacSystemFont,
            system-ui, Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono: "Cascadia Mono", Consolas, "SF Mono", "Courier New", monospace;

    --fs-eyebrow: .75rem;
    --fs-small:   .8125rem;
    --fs-body:    .9375rem;
    --fs-lead:    1.0625rem;
    --fs-h3:      1.0625rem;
    --fs-h2:      1.25rem;
    --fs-h1:      1.5rem;

    /* Spacing, on an 8px scale. */
    --s1: .25rem;  --s2: .5rem;   --s3: .75rem;  --s4: 1rem;
    --s5: 1.5rem;  --s6: 2rem;    --s7: 3rem;    --s8: 4rem;

    --radius-sm:   6px;
    --radius:      10px;
    --radius-lg:   14px;
    --radius-pill: 999px;

    /* Flat surfaces, depth from tint rather than heavy shadow.

       --shadow-sm is a hairline that seats a small control on its background;
       --shadow-lg is the cast of something that OVERLAYS the page -- a menu, a
       popover, a dialog. Neither is the panel lift.

       THERE IS NO --shadow, AND THAT IS DELIBERATE (12 September 2026).
       It used to sit here, between the two, and it was a second opinion about
       the same job --eq-lift does: lifting a panel off the ground. Two tokens
       for one job is a standard waiting to drift, and it did -- --eq-lift was
       strengthened at Tim's request in August and --shadow was not, so every
       .card got deeper while every .grid-wrap stayed flat, for three weeks,
       until he spotted it on screen.

       A panel, a card, a grid: all lift with var(--eq-lift). Rule 5.4l. */
    --shadow-sm: 0 1px 2px rgba(16, 24, 40, .05);
    --shadow-lg: 0 4px 8px rgba(16, 24, 40, .06), 0 24px 48px -16px rgba(16, 24, 40, .18);

    --ring: 0 0 0 3px rgba(70, 92, 113, .22);

    /* ------------------------------------------------------------------
       THE LIFT -- the shadow that makes a panel float off the page.
       ------------------------------------------------------------------
       Tim, 26 August 2026: "one of the graphical design features i like is
       where there is a Shodow effect on the elements to make them look as if
       they are floating off the page."

       Recorded as a design rule in MD Docs/Web Design Brief.md 1a.21, and as
       ONE token here so every panel lifts by the same amount. Panels that
       disagree about their shadow read as being at different depths, which is
       the thing that makes a page look assembled rather than designed.

       Two layers, and both are doing a job: a 1px contact shadow that sits
       the panel ON the surface, and a wide soft one that gives it height. A
       single large blur alone reads as a smudge.

       Deliberately stronger than --shadow, which is the app's general-purpose
       one: these panels sit on the legacy's grey ground (--eq-ground #b6b7bc),
       and a shadow tuned for a near-white page disappears on it.

       NOT applied to a field box inside a panel (.eq-formbox) or to a table.
       Nested shadows cancel the effect -- if everything floats, nothing does.
       ------------------------------------------------------------------ */
    /* STRENGTHENED 27 August 2026 on Tim's instruction: "Can you make the shadow
       element more pronounced?"

       Three layers now, not two, and each is doing a different job -- which is why
       this reads as depth rather than as a grey smudge:

         1. a 1px contact shadow, so the panel sits ON the surface;
         2. a mid shadow at 6px, which is what the eye reads as HEIGHT;
         3. a wide, soft, far-offset one, which is the ambient cast.

       Turning one blur up instead would just darken the edge. The middle layer is
       the one that made the difference.

       Still tuned against the legacy's grey ground (--eq-ground #b6b7bc) and the
       page slab (--eq-page #f6f6f6), which are both mid-tones -- a shadow
       calibrated on white disappears on them. If the ground ever changes, re-check
       this. */
    --eq-lift: 0 1px 2px rgba(16, 24, 40, .14),
               0 6px 12px -4px rgba(16, 24, 40, .22),
               0 18px 40px -12px rgba(16, 24, 40, .38);

    /* -----------------------------------------------------------------------
       THE LEGACY PALETTE, LIFTED EXACTLY -- Tim, 25 August 2026:
       "I'm happy for you to continue to use the web design brief from a styling
       point of view, but please retain my color scheme."

       So the CRAFT comes from MD Docs/Web Design Brief.md (spacing scale, type
       hierarchy from weight not colour, restraint) and the COLOURS come from
       here. Every value below is copied from the legacy stylesheets, not
       sampled from a screenshot, so they are exact:

         EQnet/Styles/BaseStyle.css   the chrome, page, headings, links
         EQnet/Styles/GM.css          the "Ext" grid skin and the form box

       Do NOT "modernise" these into a tidier ramp. A user of twenty years reads
       this blue-on-slate as EasyQuote; that recognition is the requirement.
       ----------------------------------------------------------------------- */

    /* Chrome. Two tiers: a lighter blue title bar above a darker menu strip. */
    --eq-titlebar:      #4b6c9e;   /* BaseStyle .header                        */
    --eq-menustrip:     #3a4f63;   /* BaseStyle div.hideSkiplink               */
    --eq-menu:          #465c71;   /* BaseStyle div.menu ul li a               */
    --eq-menu-ink:      #dde4ec;
    --eq-menu-line:     #4e667d;
    --eq-menu-on:       #afd7fb;   /* selected tab, with BLACK text            */
    --eq-menu-hover:    #bfcbd6;   /* hover, with #465c71 text                 */

    /* The page slab: a light sheet with a slate hairline, on a grey ground. */
    --eq-page:          #f6f6f6;   /* BaseStyle .page                          */
    --eq-page-line:     #496077;
    --eq-ground:        #b6b7bc;   /* the body colour behind the slab          */

    /* Type. Headings are small-caps at a LIGHT weight -- unusual, and the most
       recognisable thing about the legacy screens. Body ink is a soft grey. */
    --eq-heading:       #666666;
    --eq-body-ink:      #696969;

    /* Links. A saturated blue, underlined -- the legacy's default anchor. */
    --eq-link:          #034af3;
    --eq-link-hover:    #1d60ff;

    /* The green forward arrow on every quote step, sampled from the legacy's
       Images/arrow_right_32.png. Tokens rather than literals inside the inline
       SVG, so the one path is reused by every step and a theme cannot end up
       drawing a green arrow on a green ground. */
    --eq-arrow:         #8dc63f;
    --eq-arrow-line:    #5a8f1e;

    /* The "Ext" grid skin from GM.css: a pale blue header over banded rows,
       every cell hairlined. This is what the stats table on the home page and
       every legacy GridView actually look like. */
    --eq-grid-head:     #afd7fb;
    --eq-grid-head-ink: #15428b;

    /* The gridline a RULED list draws, on every cell and around the table.
       GM.css .ExtGridRowStyle td and .ExtGridHeaderStyle both use it (:467,
       :498), so one token serves the header and the body as the legacy does. */
    /* THE LEGACY'S ACCORDION BARS, read from the jQuery UI theme the admin
       master page actually links (Scripts/jQuery/3.6.0/jquery-ui.min.css):
       .ui-state-default for a closed bar and .ui-state-active for the open one.
       Tim asked for this screen to wear the legacy's styling, 14 September 2026. */
    /* THE COSTING LIST'S TWO ROW COLOURS. The legacy sets
       Drawing.Color.LightYellow and Drawing.Color.LightPink on the row, which
       are exact named colours -- #FFFFE0 and #FFB6C1. Kept exactly, because a
       manager reads this screen by colour and has done for years. */
    --eq-row-yellow:    #ffffe0;
    --eq-row-red:       #ffb6c1;
    /* LightGreen, #90EE90 -- the third of the legacy's named colours. Franchise
       Leads paints all three CELL BY CELL where Costing paints whole rows, so
       the green was not needed until 15 September 2026. Same source, same
       exactness: a rep reads these screens by colour. */
    --eq-row-green:     #90ee90;

    --eq-acc-bar:       #d7ebf9;   /* closed bar                               */
    --eq-acc-bar-ink:   #2779aa;   /* closed bar text                          */
    --eq-acc-bar-edge:  #aed0ea;   /* closed bar border                        */
    --eq-acc-bar-on:    #3baae3;   /* the open bar -- solid, white text        */
    --eq-acc-bar-on-edge: #2694e8;

    --eq-grid-rule:     #99bbe8;
    --eq-grid-line:     #99bbe8;
    --eq-grid-row:      #e0eefb;   /* band 1; band 2 is plain white            */
    --eq-grid-ink:      #333333;

    /* The pale blue box a group of fields sits in (GM.css .SectionsDiv). */
    --eq-formbox:       #e0eefb;
    --eq-formbox-line:  #99bbe8;

    /* THE TAB STRIP -- jQuery UI's cupertino theme, which is what the legacy
       tabs actually are (cupertino/jquery-ui-1.10.3.custom.css:822-828 and
       :846-852). ONE blue at two strengths, exactly as cupertino does it with
       its 80% and 50% glass overlays; see the note above .eq-tabs for why this
       is expressed as tints of a single value rather than two loose hexes.
       Requested by Tim on 5 September 2026 in place of a flat grey strip. */
    --eq-tab-blue:      #3baae3;   /* cupertino's one tab colour              */
    --eq-tab-line:      #aed0ea;   /* its inactive border                     */
    --eq-tab-line-on:   #2694e8;   /* its active border                       */

    /* The two strengths, as literal hex.
       ====================================================================
       NO color-mix() HERE -- THE SAME RULING AS THE CLOSE BUTTON'S.
       ====================================================================
       color-mix(in srgb, var(--eq-tab-blue) 18%, #fff) is the tidier way to
       say this and it was the first version written. The house decision
       against it is already recorded on .eq-x below: it needs a browser
       this project cannot assume, because the target is low-spec machines,
       some of them old.

       It is worse in a custom property than in a normal declaration, which
       is worth adding to that note: an unsupported value does not fall back
       to the previous rule, it becomes invalid at computed-value time, so
       `background: var(--eq-tab-off)` resolves to NOTHING and the tab loses
       its colour entirely -- the exact thing this change exists to add.

       So the mix is done here, over #ffffff, and recorded so it can be
       redone: blue*p + 255*(1-p) per channel, from #3baae3.
         18% -> #DCF0FA     34% -> #BCE2F5     10% -> #EBF6FC
       Change --eq-tab-blue and these three must be recomputed with it. */
    --eq-tab-off:       #dcf0fa;   /* 18% of the blue                        */
    --eq-tab-hover:     #bce2f5;   /* 34%                                    */
    --eq-tab-on:        var(--eq-tab-blue);
    --eq-tab-strip:     #ebf6fc;   /* 10% -- the band behind the tabs        */

    /* Cupertino puts BLACK on the inactive tabs and WHITE on the active one.
       The black is softened to the Ext header ink, which is the blue this
       application already uses for text on a pale blue ground -- pure black on
       a tinted tab is heavier than any other label on these screens. */
    --eq-tab-off-ink:   #15428b;
    --eq-tab-on-ink:    #0d3a6b;
    --eq-tab-on-ink-solid: #ffffff;

    /* THE ACCORDION on Vehicle Details (AjaxControlToolkit Accordion, 550 uses
       across the estate). Sampled from Tim's screenshots of 26 August 2026.

       Its own tokens rather than reusing the grid palette, because the open pane
       is a saturated blue with white text that nothing else here uses -- the
       grid header (--eq-grid-head) is far paler and would leave the open pane
       indistinguishable from the closed ones, which is the one thing this
       control exists to communicate. */
    --eq-acc-open:      #45a3dd;   /* the OPEN pane's bar                       */
    --eq-acc-open-hi:   #7cc4ec;   /* its gradient top -- the legacy has one    */
    --eq-acc-open-ink:  #ffffff;
    --eq-acc-shut:      #eaf4fd;   /* a CLOSED pane's bar                       */
    --eq-acc-shut-hi:   #ffffff;
    --eq-acc-shut-ink:  #15428b;   /* the same ink as the grid header           */

    /* THE SETTINGS ACCORDION -- Settings > Dealership Details, Settings > Staff
       Administration. SEPARATE TOKENS FROM THE --eq-acc-* SET ABOVE, and the
       separation is the point: those were sampled from Tim's screenshots of the
       quote wizard's Vehicle Details pane, that screen is signed off, and
       retuning them here would restyle a screen nobody asked to change.

       THESE ARE MEASURED, not sampled from a picture, and they come from the
       legacy screen Tim was comparing against. EQNetAdmin/Main.Master:15 loads
       Scripts/jQuery/3.6.0/jquery-ui.min.css -- the "Redmond" theme -- and
       DealershipPref.aspx:169 builds the page with $("#DealerPrefAccordian")
       .accordion(), so every bar on it is drawn by .ui-state-default and
       .ui-state-active. The values below are that file's, verbatim.

       WHY THIS CHANGED, 11 September 2026. Tim, comparing the two screens:
       "your accordion levels are predominantly white on a white background
       with everything else white as well, so it doesn't stand out". He was
       right and the cause was exact -- a closed bar ran a gradient from
       #ffffff down to #eaf4fd on a white surface, so its top half WAS the
       page, and the body below it was #fff on #fff as well.

       Redmond never puts white on a bar. A closed one is a solid #d7ebf9
       inside a definite #aed0ea border, and the pane body is tinted too. The
       body tint is half the fix and is the half easily left out: without it an
       open pane's content still dissolves into the page under a coloured bar. */
    --eq-set-shut:       #d7ebf9;  /* .ui-state-default                         */
    --eq-set-shut-hi:    #e8f3fb;  /* its glass_80 sheen -- a TINT, never white */
    --eq-set-shut-edge:  #aed0ea;
    --eq-set-shut-ink:   #2779aa;
    --eq-set-hover:      #e4f1fb;  /* .ui-state-hover                           */
    --eq-set-hover-edge: #74b2e2;
    --eq-set-hover-ink:  #0070a3;
    --eq-set-open:       #3baae3;  /* .ui-state-active                          */
    --eq-set-open-hi:    #63bde9;
    --eq-set-open-edge:  #2694e8;
    --eq-set-open-ink:   #ffffff;
    --eq-set-body:       #f2f5f7;  /* .ui-widget-content                        */
    --eq-set-body-edge:  #dddddd;

    /* ------------------------------------------------------------------
       THE STAFF EDITOR'S GROUND AND ITS SECTION CARDS

       Tim, 11 September 2026, putting our Account Rights layer beside the
       legacy's: "white on white on white on white. vs legacy. which looks
       better?" The legacy, plainly -- and the reason is that the legacy
       gives a card something to sit ON, which ours never did.

       Measured from EQNetAdmin/StaffAdministration.aspx, not sampled from
       the screenshot:

         .PopUpbody on the RIGHTS layer  -> #e7e7e7  (:966, overridden)
         .PopUpbody default               -> #f2f2f2  (YUIAjaxModelPopupCSS)
         .TABCOVER_0 section card         -> #e5f3ff, 1px #000, radius 15px,
                                             20px padding, centred (:159-166)
         every table and <td>             -> solid 1px lightgray

       THE GROUND IS THE WHOLE TRICK. A blue card on a grey body reads as a
       card; the same card on a white body reads as nothing at all. Both
       tones are here because the two layers genuinely differ: Rights holds
       four cards and gets the darker #e7e7e7, Details holds one table and
       keeps the paler default.
       ------------------------------------------------------------------ */
    --eq-sunken:         #e7e7e7;  /* the Rights layer's ground               */
    --eq-panel-quiet:    #f2f2f2;  /* the Details layer's ground              */
    --eq-fieldset:       #e5f3ff;  /* .TABCOVER_0                             */
    --eq-fieldset-edge:  #000000;  /* the legacy really does use black        */
    --eq-rule:           #d3d3d3;  /* "lightgray", the table and cell border  */

    /* SELECTION. BaseStyle.css .GridSelectedRowStyle, verbatim:
           background-color: #a0dbf9;  font-weight: bold;  color: Navy;
       All three carry the signal together -- see the .is-selected rule for why
       the weight and the colour are not optional. */
    --eq-selected:      #a0dbf9;
    --eq-selected-ink:  #000080;

    /* The NOTE list's own highlight, and it is a DIFFERENT blue from the grid
       selection above on purpose: ClientNotes.ascx.vb:437 uses #99CCFF while
       BaseStyle.css's GridSelectedRowStyle uses #a0dbf9. Two lists, two blues,
       in the system being reproduced. Do not "tidy" them into one -- a rep
       comparing the two screens would see the note list change colour. */
    --eq-note-picked:   #99ccff;
}

/* ---------------------------------------------------------------------------
   2  RESET AND BASE
   --------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

/* ===========================================================================
   THE ROOT SCALE -- ONE KNOB FOR THE SIZE OF THE WHOLE INTERFACE

   Tim, 26 August 2026, with the two home pages side by side: "your app is still
   a lot larger than the legacy."

   Most of that difference is not padding, it is TYPE SIZE, and the figure is
   worth writing down: the legacy sets body { font-size: .80em }
   (EQnet/Styles/BaseStyle.css:8), which is 12.8px on a default 16px root. This
   app was running 15px. Every rem in this file -- every padding, gap, radius and
   heading -- is a multiple of the root, so a 15px root shrinks the entire
   interface by about 6% at once, proportionally, rather than making the text
   small inside boxes that stayed big.

   This is the knob. 16px is the browser default and what this app used to run.
   15px is here. 14px lands almost exactly on the legacy's density; 12.8px would
   match it to the pixel and is, in my view, too small to ask a rep on a laptop
   to read all day.

   NOT set on `body`, and that matters: rem resolves against the ROOT element, so
   a font-size on body would shrink the text and leave every rem-based dimension
   where it was -- which is the "small text in big boxes" failure this is meant
   to avoid.

   It respects the user's own browser text size, because it is set in px on a
   root that -webkit-text-size-adjust already pins: a rep who has scaled their
   browser up still gets the scaling, since browser zoom multiplies px too.
   =========================================================================== */
html {
    font-size: 15px;
    -webkit-text-size-adjust: 100%;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    font-family: var(--font);
    font-size: var(--fs-body);
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
}

a { color: var(--brand-deep); text-decoration: none; }
a:hover { text-decoration: underline; }

:focus-visible {
    outline: 2px solid var(--brand-deep);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* Keyboard users must be able to skip the navigation. */
.skip-link {
    position: absolute; left: -9999px; top: 0; z-index: 100;
    background: var(--surface); color: var(--ink);
    padding: var(--s3) var(--s4); border-radius: 0 0 var(--radius) 0;
    box-shadow: var(--eq-lift);
}
.skip-link:focus { left: 0; }

/* ---------------------------------------------------------------------------
   3  LAYOUT -- APPLICATION SCREENS USE THE WHOLE SCREEN

   Read outwards:
     92%                use nearly all of the viewport
     max(1200px, 92%)   1200px is a FLOOR, never a ceiling. On a 2560px monitor
                        this is 2355px; on 1920px it is 1766px.
     min(100%, ...)     but never wider than the viewport, so the PAGE never
                        scrolls sideways -- inner regions scroll on their own.

   The legacy app is pinned at a fixed 1340px (EQnet/Styles/BaseStyle.css:119),
   so a 2560px monitor wastes 1220px while the user scrolls past content that
   would have fitted. That is a defect to remove, not a behaviour to reproduce.
   --------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   THE APPLICATION SURFACE: 92% of the screen, floor 1200px, CEILING 2200px.

   THE CEILING IS A DELIBERATE AMENDMENT TO RULE 5.5, MADE BY TIM ON
   27 AUGUST 2026: "can we limit this width to 2200 px max width please".
   Do not "restore" max-width: none - read this first.

   Rule 5.5 as originally written said the 1200px was "a floor, not a ceiling"
   and the user-level standard says never to cap a working screen. That was
   aimed at the legacy's 1340px cap, which wasted 1220px on a 2560px monitor and
   made a rep scroll sideways past content that would have fitted. The rule was
   right about the problem and slightly wrong about the remedy: past roughly
   2200px a quote screen stops gaining anything, and a line of key-and-value
   that runs 2355px puts the label and its number so far apart that the eye
   loses the row. Tim is reading this on a 2560px screen and asked for the cap.

   So the floor and the ceiling now do different jobs, and both are needed:

     92%     use nearly all of an ordinary screen, with a gutter either side
     1200px  FLOOR - a 1280px laptop must not end up narrower than a marketing
             page, which is what 92% alone would do
     2200px  CEILING - beyond this, width stops helping and starts hurting
     100%    never wider than the viewport, so the PAGE never scrolls sideways
             on a phone; inner regions scroll on their own instead

   Worked through, so nobody has to re-derive it: 2560 -> 2200 (capped, was
   2355); 1920 -> 1766 (untouched, below the cap); 1280 -> 1200 (the floor
   lifts it); 400 -> 400 (the min clamps it). Only the widest screens change.

   This caps the CONTENT of the chrome as well, because .topbar__inner and
   .menustrip__inner are both .app-surface. That is intended: the coloured bars
   stay full-bleed while the menu items line up with the content beneath them.
   Capping only main would leave the menu strip visibly wider than the page.

   Data-dense grids may still go full-bleed with their own internal scroll -
   that part of rule 5.5 is unchanged, and .grid-scroll is where it happens.
   --------------------------------------------------------------------------- */
.app-surface {
    width: min(100%, max(1200px, 92%));
    max-width: 2200px;
    margin-inline: auto;
    padding-inline: var(--s4);
}

main.app-surface { padding-block: var(--s6) var(--s8); }

/* Widening the frame must widen the LAYOUT, never the line length. Only running
   prose keeps a reading measure. */
p, .lead, .note, .prose { max-width: 68ch; }

/* ---------------------------------------------------------------------------
   THE CENTRED CONTENT COLUMN -- and why this is not a return to the 1340px cap

   The legacy centres its content in narrow columns inside the page slab: the
   home page search is a 1000px fieldset (BaseStyle .Reso_Fieldset_1), the stats
   table is 700px (Default.aspx:666), the Prospecting and Stocklist panels are
   500px each. Tim's instruction of 25 August 2026 is to match that layout.

   That does NOT conflict with the full-screen rule, because the rule is about
   WORKING SCREENS -- boards, grids, lists, editors. A four-field search form
   stretched across 2355px is not more usable, it is less: the eye has to travel
   the width of the monitor between a label and its box.

   So the split is by CONTENT, not by page:
     .app-surface   the shell and every data grid  -> full width, unchanged
     .eq-column     forms, panels, summary tables  -> a centred measure

   Open Quotes and Used stock therefore stay full-bleed and keep the width win;
   the home page, login and the quote steps get a column. If a screen ever needs
   both, the grid simply sits outside .eq-column.
   --------------------------------------------------------------------------- */
.eq-column {
    width: min(100%, 1040px);
    margin-inline: auto;
}
.eq-column--narrow { width: min(100%, 720px); margin-inline: auto; }

/* THE CLIENT SEARCH COLUMN. 350px, and that figure is the legacy's own:
   Default.aspx:596 wraps the entire search block -- heading, instruction, the
   pale blue field box and the button -- in style="width:350px; margin:0 auto"
   inside a fieldset roughly a thousand pixels wide.

   Reproduced rather than widened because Tim's side-by-side screenshots
   (26 August 2026) made the cost of the wider version measurable: filling the
   card gave a 709px input for a surname and a card 521px tall against the
   legacy's 353px, which is why the legacy fits FOUR blocks in 888px of screen
   (search, the counts table, Prospecting, Stocklist) and this page fitted two.
   A short column of fields is also read straight down, where a 709px row has
   to be scanned across. */
.eq-searchcol { width: min(100%, 350px); margin-inline: auto; }

/* ---------------------------------------------------------------------------
   4  APPLICATION SHELL
   --------------------------------------------------------------------------- */
/* TWO TIERS, as the legacy has: a lighter blue title bar carrying the identity,
   over a darker strip carrying the tabs. Reproduced because it is doing real
   work -- the title bar answers "which dealership am I in and as whom", which on
   a system where one person holds three credential rows across several branches
   is the question a rep needs answered before they trust anything on screen. */
.topbar {
    position: sticky; top: 0; z-index: 20;
    background: var(--eq-titlebar);
    color: #fff;
    box-shadow: var(--shadow-sm);
}
.topbar__inner {
    display: flex; align-items: center; gap: var(--s5);
    min-height: 56px;
}

/* The dealership line, in the legacy's light small-caps. Font-variant rather
   than text-transform, so "FOR123" keeps its full-height capitals while the
   words around it get small ones -- which is exactly how the legacy renders it
   and why the branch code stands out without being bolded. */
.brand {
    display: inline-flex; align-items: baseline; gap: .4rem;
    color: #f9f9f9; white-space: nowrap;
    font-size: 1.3125rem; font-weight: 700; font-variant: small-caps;
    letter-spacing: .01em;
}
.brand:hover { text-decoration: none; }
.brand strong { font-weight: 700; }
.brand span   { font-weight: 700; }

/* The section badge -- "Sales" in green beside the logo in the screenshot. The
   green is the legacy's own #459E00, already a token here because the call
   status lights use it. It marks WHICH MODULE you are signed in to, and that
   matters: the same human can hold a Sales and an Admin session at once, in two
   tabs, and the only thing distinguishing them is this word. */
.topbar__section {
    font-size: 1.125rem; font-weight: 600; font-variant: small-caps;
    color: #8fe04a;
    letter-spacing: .02em;
}

/* The menu strip is a SECOND bar below the title bar. */
.menustrip {
    position: sticky; top: 56px; z-index: 19;
    background: var(--eq-menustrip);
}
.menustrip__inner { display: flex; align-items: stretch; gap: var(--s4); min-height: 34px; }

.nav { display: flex; align-items: stretch; gap: 2px; flex: 1 1 auto; flex-wrap: wrap; }
/* SQUARE TABS, not pills. The legacy menu item is a bordered rectangle
   (BaseStyle div.menu ul li a: background #465c71, 1px #4e667d border, 4px 20px
   padding) and the selected one inverts to pale blue on black. Rounded pills
   read as a different product. */
.nav a {
    display: flex; align-items: center;
    background: var(--eq-menu);
    border: 1px solid var(--eq-menu-line);
    color: var(--eq-menu-ink);
    padding: .25rem 1.1rem;
    font-size: var(--fs-body);
    line-height: 1.35;
    white-space: nowrap;
    transition: background-color .12s ease, color .12s ease;
}
.nav a:hover { background: var(--eq-menu-hover); color: var(--eq-menu); text-decoration: none; }
.nav a[aria-current="page"] { background: var(--eq-menu-on); color: #000; }

/* Submenus use a real <details>, so they work with no JavaScript at all. */
.nav details { position: relative; }
.nav details > summary {
    list-style: none; cursor: pointer;
    display: flex; align-items: center;
    background: var(--eq-menu);
    border: 1px solid var(--eq-menu-line);
    color: var(--eq-menu-ink);
    padding: .25rem 1.1rem; line-height: 1.35;
    white-space: nowrap; height: 100%;
}
.nav details > summary::-webkit-details-marker { display: none; }
.nav details > summary::after { content: " \25be"; opacity: .7; margin-left: .35rem; }
.nav details > summary:hover { background: var(--eq-menu-hover); color: var(--eq-menu); }
.nav details[open] > summary { background: var(--eq-menu-on); color: #000; }
.nav details > .submenu {
    position: absolute; left: 0; top: calc(100% + .35rem); z-index: 30;
    min-width: 15rem; padding: var(--s2);

    /* WHITE ON DARK, BECAUSE THAT IS WHAT THE LEGACY MENU IS.
       Tim, 13 September 2026, on the Reports menu: "Please notice white font on
       dark menu buttons."

       The top-level buttons already used --eq-menu / --eq-menu-ink; only the
       DROPDOWN PANEL was painted on --surface, so opening a menu went from dark
       slate to a white card and back. The legacy's is one continuous dark strip
       (BaseStyle div.menu ul li a, #465c71 on a #4e667d border) and a rep of
       twenty years reads the whole menu as one object. Same tokens as the
       summary above, so the two can never drift apart again. */
    background: var(--eq-menu); border: 1px solid var(--eq-menu-line);
    border-radius: var(--radius); box-shadow: var(--shadow-lg);
    display: flex; flex-direction: column; gap: 2px;

    /* A DROPDOWN MUST NOT GROW PAST THE SCREEN.

       The Settings menu reproduces the legacy's own list -- around twenty
       items, most of them not yet built -- and on a laptop it was TALLER THAN
       THE VIEWPORT with no way to reach the bottom. The items were rendered,
       correctly, and simply could not be got at: Tim could not reach Mail
       Status at all (11 September 2026).

       It fails silently and it gets worse with every screen added, so the cap
       is on the rule rather than on one menu.

       100vh minus the chrome minus a small gutter, so the panel always ends on
       screen whatever the window height. --eq-chrome-h is the title bar plus
       the menu strip, which is exactly what sits above it. */
    max-height: calc(100vh - var(--eq-chrome-h) - 2rem);
    overflow-y: auto;

    /* NOTE: overscroll-behavior is NOT set here. Rule 5.4k -- it may only ever
       be in effect on a box that can actually scroll, and app.js section 22
       adds .is-scrollable when this one overflows. Declaring it statically is
       what killed the mouse wheel over short lists twice in one day. */
}
.nav details > .submenu a {
    color: var(--eq-menu-ink); border-radius: var(--radius-sm); padding: .4rem .6rem;
}
/* Hover inverts to the same pale-blue-on-dark the legacy uses for a selected
   item, so hover and current read as one family. */
.nav details > .submenu a:hover { background: var(--eq-menu-hover); color: var(--eq-menu); }
.nav details > .submenu a[aria-current="page"] { background: var(--eq-menu-on); color: #000; }

/* On the dark panel --muted is unreadable, so a not-yet-built item is dimmed
   from the menu's OWN ink rather than from the page's. */
.nav details > .submenu .nav-soon { color: color-mix(in srgb, var(--eq-menu-ink) 55%, transparent); }

/* A legacy menu item whose replacement screen is not built yet. Shown, not
   hidden, so the remaining scope stays visible at a glance. */
.nav-soon {
    display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
    padding: .4rem .6rem; border-radius: var(--radius-sm);
    color: var(--muted); font-size: var(--fs-body); cursor: default;
}

.userchip {
    display: flex; align-items: center; gap: var(--s3);
    margin-left: auto; white-space: nowrap;
}
.userchip__who { display: flex; flex-direction: column; line-height: 1.25; text-align: right; }
.userchip__name { font-weight: 600; font-size: var(--fs-body); }
.userchip__org  { font-size: var(--fs-small); color: rgba(255, 255, 255, .68); }

@media (max-width: 900px) {
    .topbar__inner { flex-wrap: wrap; padding-block: var(--s2); }
    .userchip { order: 2; }
    .nav { order: 3; width: 100%; }
    .userchip__who { text-align: left; }
}

/* ---------------------------------------------------------------------------
   5  PAGE FURNITURE
   --------------------------------------------------------------------------- */
.pagehead {
    display: flex; align-items: flex-end; justify-content: space-between;
    gap: var(--s5); flex-wrap: wrap;
    margin-bottom: var(--s5);
}
.eyebrow {
    font-size: var(--fs-eyebrow); font-weight: 650;
    text-transform: uppercase; letter-spacing: .08em;
    color: var(--muted); margin: 0 0 var(--s1);
}
h1 { font-size: var(--fs-h1); font-weight: 650; letter-spacing: -.015em; margin: 0; }
h2 { font-size: var(--fs-h2); font-weight: 650; letter-spacing: -.01em; margin: var(--s6) 0 var(--s3); }
h3 { font-size: var(--fs-h3); font-weight: 650; margin: var(--s5) 0 var(--s2); }

.lead  { font-size: var(--fs-lead); color: var(--ink-soft); margin: var(--s2) 0 0; }
.note  { font-size: var(--fs-small); color: var(--muted); }
.mono  { font-family: var(--font-mono); font-size: .9em; }
.num   { font-variant-numeric: tabular-nums; }

code {
    background: var(--brand-tint); color: var(--ink-soft);
    padding: .1rem .35rem; border-radius: 4px;
    font-family: var(--font-mono); font-size: .875em;
}

/* ---------------------------------------------------------------------------
   .card IS NOW THE LEGACY WHITE PANEL, not a floating modern card.

   Tim, 26 August 2026, with the quote view and the legacy screen side by side:
   "All pages should copy the same styling of the legacy."

   It used to be a shadowed, 14px-radius sheet with 24px of padding and no
   border -- a different visual language from every screen already rebuilt in
   the legacy skin (.eq-card: white, hairline, 8px radius). Changing it here
   rather than swapping the class on each page means the screens still using it
   (Clients, Drafts, the used manual entry) pick the legacy look up without a
   markup change; the quote view goes further and uses .eq-card .eq-step, which
   is the panel the approved wizard steps use.

   The heading rule matters as much as the frame. A bare <h2> carries
   margin-top: var(--s6) -- 32px of air above the first line of every panel,
   which was the single largest piece of the whitespace Tim was pointing at.
   Inside a card it becomes the legacy's Head3: light small-caps grey, tight.
   (BaseStyle .Head3 12pt/200/#666666, font-variant small-caps.)
   --------------------------------------------------------------------------- */
.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: var(--s4);
    margin-block: var(--s3) var(--s4);
    /* The lift. Tim likes panels reading as if they float off the page -- see
       design brief 1a.21, and --eq-lift in section 1. */
    box-shadow: var(--eq-lift);
}
.card--flush { padding: 0; overflow: hidden; }
.card > :first-child { margin-top: 0; }
.card > h2 {
    font-size: var(--fs-h3);
    font-weight: 200;
    font-variant: small-caps;
    letter-spacing: 0;
    color: var(--eq-heading);
    margin: var(--s4) 0 var(--s2);
}

.panel {
    background: var(--surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    border-left: 3px solid var(--brand);
    padding: var(--s4) var(--s5);
    margin: var(--s4) 0;
}
.panel > :first-child { margin-top: 0; }
.panel > :last-child  { margin-bottom: 0; }
.panel--good { border-left-color: var(--good); }
.panel--bad  { border-left-color: var(--bad); }
.panel--warn { border-left-color: var(--warn); }

/* Stat tiles. A 3-up grid that collapses cleanly, per the design brief. */
.stats {
    display: grid; gap: var(--s4);
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
.stat {
    background: var(--surface); border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm); padding: var(--s4) var(--s5);
}
.stat__label {
    font-size: var(--fs-eyebrow); font-weight: 650; text-transform: uppercase;
    letter-spacing: .07em; color: var(--muted); margin: 0;
}
.stat__value {
    font-size: 1.75rem; font-weight: 650; letter-spacing: -.02em;
    font-variant-numeric: tabular-nums; margin: var(--s1) 0 0;
}
.stat__hint { font-size: var(--fs-small); color: var(--muted); margin: var(--s1) 0 0; max-width: none; }

/* THE dl.kv RULE THAT USED TO BE HERE IS DELETED, and this note replaces it so
   nobody reinstates it.

   It set grid-template-columns on the <dl> itself, for a list whose direct
   children were dt/dd. No screen has been built that way -- every one of them
   wraps each pair in .kv__row (see _Kv.cshtml) -- so the columns applied to the
   ROW WRAPPERS instead, and being more specific than the real .kv rule further
   down, it silently won. Two rules for one component, one of them describing
   markup that does not exist. The live definition is in the quote detail
   section. */

.empty {
    text-align: center; padding: var(--s8) var(--s5); color: var(--muted);
}
.empty h3 { margin: 0 0 var(--s2); color: var(--ink); }
.empty p  { margin: 0 auto; max-width: 46ch; }

/* ---------------------------------------------------------------------------
   6  FORMS AND CONTROLS
   --------------------------------------------------------------------------- */
label { font-size: var(--fs-small); font-weight: 600; color: var(--ink-soft); }

/* CONTROL HEIGHT IS DELIBERATELY TIGHT -- Tim, 25 August 2026:

     "One of the general themes that you have is to extend things vertically,
      which requires users to scroll to be able to interact with the page. Can
      you try and keep the height of elements the same as what I show you in my
      screenshots?"

   He is right, and it was a systematic fault rather than one page's: .45rem of
   padding on top of a 1.55 body line-height gives a 36px control where the
   legacy has 24px. Over a six-field form that is 72px of nothing, which is the
   difference between a form that fits and a form that scrolls.

   line-height is set EXPLICITLY here. Without it the control inherits body's
   1.55, which is right for paragraphs and wrong inside a box -- and it is the
   larger of the two contributions, so tightening only the padding barely moves
   the height.

   The floor is the pointing target, not the pixel count. This lands at about
   28px, which is comfortable for a mouse on the desktop PCs these users run.
   Do not tighten it further for a phone layout: touch wants 44px, so if this app
   ever gets a real mobile view these controls grow again at that breakpoint. */
input[type="text"], input[type="search"], input[type="password"],
input[type="date"], input[type="number"], input[type="tel"],
select, textarea {
    width: 100%;
    font: inherit; color: var(--ink);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: .22rem .45rem;
    line-height: 1.35;
    transition: border-color .12s ease, box-shadow .12s ease;
}
textarea { line-height: 1.45; }
input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: var(--ring);
}
input[aria-invalid="true"], select[aria-invalid="true"] { border-color: var(--bad); }
input:disabled, select:disabled { background: var(--bg); color: var(--muted); }

/* 2px, not .25rem. A label belongs to the box under it, and the gap only has to
   be big enough to separate them -- any more and it starts to look like a gap
   between fields, which is the next rule's job. */
.field { display: flex; flex-direction: column; gap: 2px; }
.field > label { line-height: 1.3; }
.field--grow { flex: 1 1 14rem; min-width: 0; }

/* The filter bar above a grid. Wraps rather than scrolls, so nothing is hidden
   on a narrow window. */
.filters {
    display: flex; align-items: flex-end; gap: var(--s3) var(--s4); flex-wrap: wrap;
    background: var(--surface); border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm); padding: var(--s3) var(--s4);
    margin-bottom: var(--s3);
}
.filters__actions { display: flex; gap: var(--s2); align-items: center; }

/* Segmented control, standing in for the legacy screen's three radio buttons.
   Still real radios underneath, so it works without JavaScript and reads
   correctly to a screen reader. */
.segmented { display: inline-flex; background: var(--bg); border-radius: var(--radius-pill); padding: 3px; }
.segmented input { position: absolute; opacity: 0; width: 0; height: 0; }
.segmented label {
    padding: .35rem .85rem; border-radius: var(--radius-pill);
    cursor: pointer; font-weight: 600; font-size: var(--fs-small);
    color: var(--ink-soft); white-space: nowrap;
    transition: background-color .12s ease, color .12s ease, box-shadow .12s ease;
}
.segmented input:checked + label {
    background: var(--surface); color: var(--ink); box-shadow: var(--shadow-sm);
}
.segmented input:focus-visible + label { box-shadow: var(--ring); }

/* Searchable drop-down (.ss-*), styled to match the native controls above so
   the enhancement is invisible. See wwwroot/js/searchable-select.js. */
.ss-wrap { position: relative; }
.ss-input { cursor: text; }
.ss-panel {
    position: absolute; z-index: 40; inset-inline: 0; top: calc(100% + 4px);
    max-height: 16rem; overflow-y: auto;
    background: var(--surface); border: 1px solid var(--border-strong);
    border-radius: var(--radius); box-shadow: var(--shadow-lg);
    padding: var(--s1);
}
.ss-row {
    padding: .35rem .55rem; border-radius: var(--radius-sm);
    cursor: pointer; font-size: var(--fs-body);
}
.ss-row:hover, .ss-row.ss-hi { background: var(--brand-tint); }
.ss-row.ss-current { font-weight: 650; }
.ss-none { padding: .45rem .55rem; color: var(--muted); font-size: var(--fs-small); }

/* ---------------------------------------------------------------------------
   7  BUTTONS -- one vocabulary, repeated
   --------------------------------------------------------------------------- */
.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: .45rem;
    font: inherit; font-weight: 600; font-size: var(--fs-body);
    padding: .45rem 1rem;
    border: 1px solid transparent; border-radius: var(--radius-pill);
    cursor: pointer; white-space: nowrap;
    transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
.btn--primary { background: var(--brand-deep); color: #fff; }
.btn--primary:hover { background: #26374a; text-decoration: none; }
.btn--secondary { background: var(--surface); color: var(--ink); border-color: var(--border-strong); }
.btn--secondary:hover { background: var(--brand-tint); text-decoration: none; }
.btn--ghost { background: transparent; color: var(--ink-soft); }
.btn--ghost:hover { background: var(--brand-tint); text-decoration: none; }

/* Excel green, for anything that downloads a workbook. See --excel for why a
   brand colour is used here and nowhere else. White text on #217346 measures
   5.82:1 and on the hover #1a5c38 measures 7.98:1, so both clear WCAG AA for
   normal text with no lightening needed. Computed, not estimated. */
.btn--excel { background: var(--excel); color: #fff; }
.btn--excel:hover { background: var(--excel-hover); color: #fff; text-decoration: none; }
.btn--excel:focus-visible { outline: 2px solid var(--excel-hover); outline-offset: 2px; }

/* The "X" mark, standing in for the Excel icon. A LETTER RATHER THAN AN IMAGE is
   deliberate: the legacy ships a 6 KB JPEG per report page for this, and on this
   project bytes on the wire are the acceptance criterion. It costs nothing, scales
   with the text, and cannot 404. aria-hidden in the markup, because a screen
   reader should hear "Excel" once, not "X Excel". */
.btn__icon {
    display: inline-grid; place-items: center;
    width: 1.05em; height: 1.05em;
    border-radius: 2px;
    background: rgba(255, 255, 255, .22);
    font-size: .8em; font-weight: 700; line-height: 1;
}
.btn--block { width: 100%; }
.btn:disabled, .btn[aria-disabled="true"] { opacity: .6; cursor: not-allowed; }

.btn-row { display: flex; gap: var(--s3); flex-wrap: wrap; align-items: center; }

/* ---------------------------------------------------------------------------
   8  DATA GRID
   Wide content scrolls inside its own container, never the page body.
   --------------------------------------------------------------------------- */
.grid-wrap {
    background: var(--surface);
    border-radius: var(--radius-lg);
    /* THE ONE LIFT. Not a grid-specific shadow -- see rule 5.4l.

       This used to be var(--shadow), and that is exactly how the standard
       drifted. On 27 August 2026 Tim asked for the shadow to be "more
       pronounced"; --eq-lift was strengthened from two layers to three and
       every .card in the application got deeper. .grid-wrap was never moved
       across, so cards kept floating and GRIDS QUIETLY STOPPED -- which is the
       regression Tim reported on 12 September, pointing at Staff Administration
       (a .card) against Mail Status (a grid) and asking why one popped and the
       other did not.

       Nobody wrote that bug. Two tokens for one job is enough on its own: the
       moment one is tuned, everything on the other silently diverges. So there
       is now ONE, and GridStandardsTests fails if a second appears. */
    box-shadow: var(--eq-lift);
    overflow: hidden;
}

/* ---------------------------------------------------------------------------
   ONE LIFT PER STACK. A raised surface inside a raised surface stays flat.

   --eq-lift's own note has said this since August -- "NOT applied to a field box
   inside a panel or to a table. Nested shadows cancel the effect: if everything
   floats, nothing does" -- and nothing enforced it, so Mail Status ended up with
   a shadowed grid inside a shadowed panel and read as one flat slab.

   The outermost panel is the one that meets the page ground, so it is the only
   one with any contrast to cast against. Everything nested inside it is already
   floating by virtue of being in it.

   .eq-card ADDED 30 September 2026. It lifts with --eq-lift exactly as .card
   does, and was missing here, so the OTP wizard's Cash Accessories grid cast a
   second shadow inside its own panel -- the defect this rule exists to prevent,
   on the screen Tim asked to be brought into line.
   --------------------------------------------------------------------------- */
.eq-fieldset .grid-wrap,
.eq-dialog .grid-wrap,
.card .grid-wrap,
.eq-card .grid-wrap {
    box-shadow: none;
}
/* ---------------------------------------------------------------------------
   THE GRID SCROLL BOX -- and it is what makes the column headings stick.

   Tim, 27 August 2026, as a system-wide ruling: "the heading remains static
   while the remaining content of the report scrolls underneath it so that the
   heading is always visible in the report."

   BOTH AXES, AND A BOUNDED HEIGHT. This used to be overflow-x: auto, which
   looks like "scroll sideways only" and is not a thing CSS can do -- overflow-x
   auto computes overflow-y to auto as well. So the box was already a scroll
   container, but with a height that grows to fit its content, so it never
   scrolled vertically. thead th has carried position: sticky; top: 0 all along
   and had nothing to stick to: it sat at the top of a box that never moved, and
   the PAGE scrolled it out of sight.

   Giving the box a viewport-bounded height is what turns the existing sticky
   rule on. The rows scroll under the heading, and the heading stays.

   max-height, not height: a short grid keeps its natural size and shows no
   scrollbar at all.

   THIS BOX DELIBERATELY DOES NOT CONTAIN ITS OVERSCROLL, and that is a reversal
   of what this comment used to say. It claimed `contain` stopped the page
   lurching at the last row; Tim used it and decided the other way on
   12 September 2026. The box is nearly viewport-tall, so stopping the wheel at
   its last row strands the totals row and the footer below the fold unless the
   reader moves the cursor off the table.
   --------------------------------------------------------------------------- */
.grid-scroll {
    overflow: auto;
    max-height: var(--grid-viewport);

    /* NO overscroll-behavior, in either direction, and BOTH omissions are
       deliberate.

       It must not be here STATICALLY, because a container that cannot scroll
       must never carry `contain` -- it swallows the wheel and the page behind
       it does not move either (rule 5.4k).

       And it is not on this box's .is-scrollable form either, which is where
       the dialogs and the menu get theirs. A grid CHAINS: at its last row the
       page takes over, with no need to move the cursor off the table. The whole
       argument is above the .eq-dialog__body.is-scrollable rule. */
}

table.grid { width: 100%; border-collapse: separate; border-spacing: 0; }

/* ---------------------------------------------------------------------------
   ROW HEIGHT. Vertical padding is 20% off what it was.

   Tim, 4 September 2026, comparing this grid against the legacy's side by side:
   "I do prefer the styling of the new one with the exception that I think the
   line heights are a bit too tall. Please make them about twenty percent
   narrower in height to allow more content to be visible on the page at once."

   .6rem -> .48rem, which is the 20% he asked for and no more. HORIZONTAL
   padding is untouched at .85rem: he asked about height, and squeezing the
   sides would run a client name into the column beside it.

   Why this is the right place and the only place: every grid family in the
   application inherits these cells (.grid--eq and the pickers set their own,
   tighter, values -- they were already dense). One number, whole application,
   and no screen can drift from it.

   THE 44px TOUCH FLOOR STILL APPLIES ON A PHONE. Section 30 sets its own
   padding for card mode, so this cannot shrink a touch target below the
   minimum rule 5.4e requires -- check that block before reducing this further.
   --------------------------------------------------------------------------- */
table.grid th, table.grid td {
    padding: .48rem .85rem;
    text-align: left;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}
/* THE STICKY HEADING. top: 0 is the top of .grid-scroll, not of the page --
   see that rule for why the box has to be height-bounded for this to do
   anything.

   z-index 2, above the rows and the sticky totals row, and BELOW the page
   chrome's 19 and 20 so a heading can never sit over the menu strip.

   The background must stay opaque or rows show through it while they pass
   underneath; every skin that restyles this header sets one. */
table.grid thead th {
    position: sticky; top: 0; z-index: 2;
    background: var(--brand-tint);
    color: var(--ink-soft);
    font-size: var(--fs-eyebrow); font-weight: 700;
    text-transform: uppercase; letter-spacing: .06em;
    white-space: nowrap;
    border-bottom: 1px solid var(--border-strong);
}
table.grid tbody tr:last-child td { border-bottom: 0; }

/* ---------------------------------------------------------------------------
   RULED -- the legacy's own gridlines on a list

   Tim, 11 September 2026: "copy the styling of the staff admin list page".
   Measured from EQNetAdmin/Styles/GM.css, which is what that GridView actually
   wears (StaffAdministration.aspx:276 sets HeaderStyle to ExtGridHeaderStyle):

       .ExtGridHeaderStyle      #15428b ink, 1px #99bbe8 border  (:462-476)
       .ExtGridRowStyle td      1px #99bbe8, 5px either side     (:495-500)

   The ROW BANDS were already the legacy's -- rule 5.4f took #e0eefb / white
   from the same stylesheet. What was missing is that the legacy rules EVERY
   cell, which is what turns a wide row of ticks and buttons into something the
   eye can follow across.

   A MODIFIER, not a change to table.grid. Fourteen screens use .grid and were
   built and reviewed without gridlines; restyling all of them off one screen's
   complaint would be a change nobody asked for. Rolling it out further is a
   decision, and Tim's to make.

   The header keeps its sticky position, z-index and opaque background from the
   base rule -- rule 5.4d depends on all three, and only the colours change. */
table.grid--ruled { border: 1px solid var(--eq-grid-rule); }

table.grid--ruled > thead > tr > th,
table.grid--ruled > tbody > tr > td {
    border: 1px solid var(--eq-grid-rule);
    padding-left: .5rem;
    padding-right: .5rem;
}

table.grid--ruled > thead > tr > th {
    background: var(--eq-grid-head);
    color: var(--eq-grid-head-ink);
}

/* The base rule strips the last row's bottom border to sit flush on the card.
   A ruled table draws its own box, so the line has to come back or the grid
   ends in mid air. */
table.grid--ruled > tbody > tr:last-child > td {
    border-bottom: 1px solid var(--eq-grid-rule);
}

/* ---------------------------------------------------------------------------
   ALTERNATING ROW BANDS -- light blue, then white.

   Tim's standing instruction, 4 September 2026, given as a system-wide standard
   for every list of information in the application:

     "This alternating [light] blue white ... to easily be able to follow a line
      horizontally while reading. Please also remember that clicking anywhere in
      the line should have the same hyperlink effect as clicking the trigger ...
      That is a standard for all reports. Both the colour scheme and the
      trigger."

   IT IS A READING AID, NOT DECORATION, which is why it lives on the base .grid
   and is not opted into per screen. Reading across nine columns at 1766px the
   eye loses the row, looks up, and takes a date off one client and a phone
   number off the one below.

   THE COLOURS ARE THE LEGACY'S OWN, not a new palette. GM.css sets
   ExtGridRowStyle to #e0eefb and ExtGridAltRowStyle to white
   (EQnet/Styles/GM.css:540-549), and that pair is what a rep of twenty years is
   reading today. Held in --eq-grid-row so the four grid families cannot drift:
   the legacy skin (.grid--eq, section 16), the small summary tables (.eq-table,
   section 15) and the pickers (.eq-pick) were ALL already banded with it. This
   family was the only one that was not, which is the whole defect.

   BAND 1 IS TINTED AND BAND 2 WHITE, so the first row is blue -- matching the
   legacy, where RowStyle paints the odd rows and AlternatingRowStyle the even
   ones. The other way round makes a single-row result look greyed out.

   > td, NOT THE ROW, and this is the part that would be "tidied" and break.
   A background on a <tr> paints UNDERNEATH its cells, so any rule that fills a
   cell hides it. Every state that has to beat a band therefore also paints
   > td -- hover here, .is-selected in section 16, .is-activated just below.
   Add a fifth state on the <tr> and it will silently do nothing.
   --------------------------------------------------------------------------- */
table.grid tbody tr:nth-child(odd)  > td { background: var(--eq-grid-row); }
table.grid tbody tr:nth-child(even) > td { background: #fff; }

/* Hover after the bands, at equal specificity, so it wins on both. */
table.grid tbody tr:hover > td { background: var(--brand-tint); }
table.grid td.num, table.grid th.num { text-align: right; font-variant-numeric: tabular-nums; }
table.grid td.nowrap { white-space: nowrap; }

/* Whole-row navigation on a grid whose rows open a record.
   Tim's instruction, 22 August 2026, and it reproduces the legacy: on
   EQnet/OpenQuotes.aspx every cell is its own LinkButton, so clicking anywhere
   on the line opens the quote.

   .is-row-linked IS ADDED BY JAVASCRIPT, NEVER BY THE MARKUP. If app.js is
   blocked -- routine on these connections -- the rows genuinely are not
   clickable, so they must not advertise that they are. The quote number stays a
   real link and keeps working regardless. Do not move this class into the Razor
   view to "save a line of script": a pointer cursor over dead space is worse
   than no cursor at all.

   Text stays selectable on purpose (no user-select: none) because reps copy
   client names and quote numbers out of this grid; app.js tells a drag from a
   click instead. */
table.grid.is-row-linked tbody tr { cursor: pointer; }

/* THE SAME AFFORDANCE ON EVERY GRID FAMILY, not only table.grid.

   Tim's 4 September ruling is "a standard for all reports", and the application
   has four table families for historical reasons -- .grid, .grid--eq (a modifier
   on it), .eq-table (the small legacy transcriptions, which is what the home
   page drill-down uses) and .eq-pick. The row-click JS has always been
   family-agnostic; it selects table[data-row-link]. Only the CURSOR was written
   against .grid, so an .eq-table row was clickable and never said so.

   Scoped to .is-row-linked alone, so it follows the attribute wherever it goes.
   Still added by app.js and never by the markup -- see the note above for why a
   pointer cursor must not appear when the script is blocked. */
.is-row-linked tbody tr { cursor: pointer; }

/* ---------------------------------------------------------------------------
   A GRID WHOSE ROWS DO NOT ALL GO SOMEWHERE.

   Reminders is the first: an overdue prospect opens its quote, a Courtesy Call
   has no destination built yet (Default.aspx.vb:851-860 lands it on the Notes
   tab, which needs a route that does not exist). Both kinds sit in one grid.

   So a view may mark the rows that ARE clickable with .is-row-target, and the
   pointer is then withheld from the rest. Opting IN per row rather than out,
   because a grid whose rows are uniformly clickable -- which is all the others
   -- must keep working with no markup change at all: hence the :not() form
   rather than requiring the class everywhere.

   THE CURSOR IS THE WHOLE POINT. app.js already refuses to activate a row with
   no target, so those rows were harmless; they simply LOOKED clickable and did
   nothing, which is the same lie a pointer tells when script is blocked.
   --------------------------------------------------------------------------- */
.is-row-linked:has(tbody tr.is-row-target) tbody tr:not(.is-row-target) {
    cursor: default;
}
.is-row-linked tbody tr.is-activated > td { background: var(--brand-tint-strong); }

/* The click acknowledgement. The quote page answers in about 21ms, so a spinner
   would flash and read as a glitch -- what the user needs is to see which line
   they hit, so the eye can find its place again if the navigation is slow.
   Repeated for :hover because the pointer is by definition still on the row.

   A colour change with no animation, so prefers-reduced-motion is deliberately
   not consulted: this is the feedback a reduced-motion user should still get. */
/* > td, because the rows are banded now and a <tr> background would paint
   under the band and never be seen. See the banding rule above.

   THE RING IS ASSEMBLED FROM PER-CELL INSETS, which is fiddlier than the single
   `inset 0 0 0 1px` on the <tr> it replaces, and unavoidable for the same reason
   the background is: a row-level shadow sits under the cells.

   So every cell draws the top and bottom edge, and only the end cells draw a
   side. Drawing the sides on all of them would rule every column boundary and
   turn the highlight into a grid; drawing top and bottom only on the ends would
   leave the middle of a nine-column row unlined, which is what the first attempt
   at this did. */
table.grid.is-row-linked tbody tr.is-activated > td,
table.grid.is-row-linked tbody tr.is-activated:hover > td {
    background: var(--brand-tint-strong);
    box-shadow:
        inset 0  1px 0 var(--border-strong),
        inset 0 -1px 0 var(--border-strong);
}
table.grid.is-row-linked tbody tr.is-activated > td:first-child {
    box-shadow:
        inset 0  1px 0 var(--border-strong),
        inset 0 -1px 0 var(--border-strong),
        inset 1px 0  0 var(--border-strong);
}
table.grid.is-row-linked tbody tr.is-activated > td:last-child {
    box-shadow:
        inset 0  1px 0 var(--border-strong),
        inset 0 -1px 0 var(--border-strong),
        inset -1px 0 0 var(--border-strong);
}

/* A SUBMIT BUTTON THAT READS AS THE ROW'S TEXT, not as a control.

   Tim's instruction, 25 August 2026: "we make any text in EasyQuote clickable. We
   don't have a separate select button." On grids whose row opens a URL that is a
   plain anchor. On the CLIENT grid it cannot be: a ClientID identifies a person, so
   it travels in a submit button's value and never in a URL or an access log (see
   rule 5.3 and the POST-only client search). This class is what lets that button
   carry the client's NAME and look like every other clickable name in the app.

   Not a variant of .btn on purpose -- it inherits nothing from the pill treatment,
   because a name in a grid must not look like a call to action. It is styled from
   the browser's button defaults up, since those are what have to be removed.

   Left-aligned and inline rather than block: the name sits beside its "also known
   as" and "marketing prospect" tags, so a full-width target would swallow them. The
   whole row is the large target, added by app.js. */
.textlink {
    display: inline; margin: 0; padding: 0;
    border: 0; background: none;
    font: inherit; color: var(--brand-deep); text-align: left;
    cursor: pointer;
}
.textlink:hover { text-decoration: underline; }
.textlink:focus-visible { outline: 2px solid var(--brand-deep); outline-offset: 2px; }

/* Disabled means the row genuinely cannot be chosen -- a draft whose payload can no
   longer be read. It must not look clickable, and app.js refuses to activate it. */
.textlink:disabled { color: var(--muted); cursor: default; text-decoration: none; }

/* Sortable column headers are real links, so sorting survives JavaScript being
   blocked and each sort order is a bookmarkable URL. The legacy grid sorts by
   posting the whole page back and re-reading a DataTable out of ViewState. */
table.grid thead a {
    color: inherit; display: inline-flex; align-items: center; gap: .3rem;
}
table.grid thead a::after { content: ""; width: .55em; opacity: .45; }
table.grid thead a[data-sort="asc"]::after  { content: "\25b2"; opacity: 1; }
table.grid thead a[data-sort="desc"]::after { content: "\25bc"; opacity: 1; }

.grid-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--s4); flex-wrap: wrap;
    padding: var(--s3) var(--s5);
    background: var(--surface);
    border-top: 1px solid var(--border);
    font-size: var(--fs-small); color: var(--muted);
}

/* ---------------------------------------------------------------------------
   9  PAGINATION
   --------------------------------------------------------------------------- */
.pager { display: flex; align-items: center; gap: var(--s1); }
.pager a, .pager span {
    min-width: 2rem; padding: .25rem .5rem;
    border-radius: var(--radius-sm); text-align: center;
    color: var(--ink-soft); font-variant-numeric: tabular-nums;
}
.pager a:hover { background: var(--brand-tint); text-decoration: none; }
.pager [aria-current="page"] { background: var(--brand-deep); color: #fff; font-weight: 600; }
.pager .is-disabled { opacity: .4; pointer-events: none; }

/* ---------------------------------------------------------------------------
   10  STATUS PILLS
   The hues are EasyQuote's existing call-status traffic lights and they carry
   meaning to users, so they are preserved exactly rather than restyled.
   --------------------------------------------------------------------------- */
.pill {
    display: inline-block; padding: .15rem .55rem;
    border-radius: var(--radius-pill);
    font-size: var(--fs-eyebrow); font-weight: 700;
    letter-spacing: .03em; white-space: nowrap;
}
.pill--open     { background: var(--info-tint);  color: var(--brand-deep); }
.pill--otp      { background: var(--warn-tint);  color: var(--warn); }
.pill--invoiced { background: var(--good-tint);  color: var(--good); }
.pill--convert  { background: var(--good-tint);  color: var(--good); }
.pill--rental   { background: var(--brand-tint-strong); color: var(--brand-deep); }

/* CONTACT PREFERENCES -- the four opt-out channels on the client profile.

   Two new pills. They reuse the existing good/error tints rather than
   introducing colours, so a rep reads "stop" here the same way they read it
   everywhere else in the application. */
.pill--ok   { background: var(--good-tint); color: var(--good); }
.pill--stop { background: var(--error-bg); color: var(--error-ink); }

/* Collapsed by default: four rows on an already dense screen, and the common
   case is that nothing is opted out. */
.eq-optouts { margin: var(--s3) 0; }
.eq-optouts > summary {
    cursor: pointer; font-weight: 600; letter-spacing: .02em;
    padding: .45rem .6rem; border-radius: var(--radius-sm);
    background: var(--eq-sunken); color: var(--ink);
}
.eq-optouts > summary:hover { background: var(--brand-tint); }
.eq-optouts__body { padding: var(--s2) 0; }

/* The grid is key-and-value detail, not a list of records -- so no row
   trigger, and it is named in GridStandardsTests' baseline with that reason. */
.eq-optouts__grid td { vertical-align: middle; }

/* Reason and button on one line, wrapping on a phone rather than being
   hidden -- rule 5.4e: nothing interactive is ever hidden, it is re-laid-out. */
.eq-optouts__form {
    display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap;
}
.eq-optouts__form input[type="text"] { min-width: 12rem; flex: 1 1 12rem; }

/* Secondary text inside a cell. Deliberately not a size on the element, so it
   scales with whatever the surrounding grid is using. */
.small { font-size: .85em; }
.pill--closed   { background: var(--bad-tint);   color: var(--bad); }
.pill--neutral  { background: var(--bg);         color: var(--neutral); }

.tag {
    display: inline-block; padding: .1rem .45rem;
    border-radius: var(--radius-sm); background: var(--bg);
    font-size: var(--fs-eyebrow); font-weight: 600; color: var(--muted);
}

/* ---------------------------------------------------------------------------
   CALL TRAFFIC LIGHTS -- the quote list's primary information design.

   These three hues are NOT a design choice and must not be adjusted. They are
   EasyQuote's existing call-status colours, fixed in the legacy stylesheet at
   Styles/BaseStyle.css:678-692 and repeated in Main.Master:44-56, and they carry
   meaning that reps rely on:

     black  #000000   recently contacted, or a callback is scheduled
     green  #459E00   contacted between the two thresholds
     red    #CC0000   NO FUTURE DATED REMINDER -- the quote is going cold

   The "red" wording is the legacy's own tooltip text
   (EQNetAdmin/OpenQuotes.aspx.vb:377-381), which is the only place in the whole
   system where the meaning of these colours is written down.

   The legacy applies the colour to the LINK inside every cell of the row. Here
   it is applied to the row, which is the same visual result while the cells are
   not yet links.

   Colour is NEVER the only carrier: every coloured row also gets a title
   attribute and screen-reader-only text, because red/green is precisely the
   distinction that colour-blind users cannot make. The legacy has no such
   fallback.
   --------------------------------------------------------------------------- */
table.grid tr.zone-black td { color: #000000; }
table.grid tr.zone-green td { color: #459e00; }
table.grid tr.zone-red   td { color: #cc0000; }

/* THE WHOLE LINE TAKES THE LINE'S COLOUR, CONTROLS INCLUDED -- Tim,
   15 September 2026:

     "the font color is determined solely by the due date ... we should not have
      red text for overdue then birthday reminders in black text ... the entire
      line entry is 100% governed by the due date."

   The rules above colour the CELL, and a <button> does not inherit colour from
   its parent while an <a> carries its own. So on an overdue line the plain text
   went red and the controls did not: a Birthday opens the notes tab through a
   .textlink button (--brand-deep, which reads as black) and a quote number is a
   link. The result was a red line with black and blue words in it, which is
   exactly the reading Tim is ruling out -- it looks like the TYPE is deciding
   the colour when only the date ever does.

   Declared for all three zones together so they cannot drift apart, and left as
   `inherit` rather than repeating the three hex values, so a change to a zone
   colour reaches its controls automatically.

   Specificity is what makes it work: (0,2,4) beats .eq-slab a at (0,1,1) and
   .textlink at (0,1,0), both of which would otherwise win by being declared
   later. */
table.grid tr.zone-black td a,
table.grid tr.zone-black td button,
table.grid tr.zone-black td .textlink,
table.grid tr.zone-green td a,
table.grid tr.zone-green td button,
table.grid tr.zone-green td .textlink,
table.grid tr.zone-red   td a,
table.grid tr.zone-red   td button,
table.grid tr.zone-red   td .textlink { color: inherit; }

/* A left edge as a second, non-colour-dependent cue: red is a solid bar, green
   a lighter one, black none. Position and weight differ, so the zones remain
   distinguishable in greyscale. */
table.grid tr.zone-red   td:first-child { box-shadow: inset 3px 0 0 #cc0000; }
table.grid tr.zone-green td:first-child { box-shadow: inset 3px 0 0 #459e00; }

/* The muted sub-line (last contact date) must not fight the zone colour, but it
   should still read as secondary -- so it inherits the hue at reduced opacity
   rather than reverting to grey. */
table.grid tr.zone-black .note,
table.grid tr.zone-green .note,
table.grid tr.zone-red   .note { color: inherit; opacity: .7; }

/* ---------------------------------------------------------------------------
   11  BUSY STATES
   Any action over ~300ms shows one immediately, before the work resolves, and
   clears it on success AND on error.

   The legacy app gets this exactly wrong: it defines Hideloadingimg() and never
   calls it from anywhere, so one click on "Notifications and Updates" leaves the
   whole application behind a permanent click-blocking overlay at z-index
   9999999999, and only F5 escapes.
   --------------------------------------------------------------------------- */
.spinner {
    display: inline-block; width: 1em; height: 1em; flex: none;
    vertical-align: -.125em;
    border: 2px solid currentColor; border-right-color: transparent;
    border-radius: 50%;
    animation: spin .6s linear infinite;
}

/* A button in flight: label swapped for a verb, control disabled so it cannot
   be double-submitted. Double-submit protection is effectively absent in the
   legacy app (~3-5% of 513 buttons) while waits run to 60-90 seconds. */
.btn[data-busy="true"] { pointer-events: none; opacity: .75; }

/* Region-level busy state for a grid reload. The rows stay visible and legible
   underneath, which is calmer than replacing them with a blank box. */
.is-loading { position: relative; }
.is-loading::after {
    content: ""; position: absolute; inset: 0;
    background: rgba(244, 246, 249, .55);
    backdrop-filter: saturate(.85);
}
.is-loading > .loading-bar {
    position: absolute; inset-inline: 0; top: 0; height: 3px; z-index: 2;
    background: linear-gradient(90deg, transparent, var(--brand), transparent);
    background-size: 40% 100%; background-repeat: no-repeat;
    animation: slide 1.1s ease-in-out infinite;
}

@keyframes spin  { to { transform: rotate(360deg); } }
@keyframes slide {
    0%   { background-position: -40% 0; }
    100% { background-position: 140% 0; }
}

/* ---------------------------------------------------------------------------
   12  LOGIN
   The one screen in the application that is NOT full-width: a single short
   form, where a wide frame would just stretch six fields across a 2560px
   monitor. Centred and capped on purpose.
   --------------------------------------------------------------------------- */
.auth {
    min-height: 100dvh;
    display: grid; place-items: center;
    padding: var(--s5);
    background:
        radial-gradient(60rem 40rem at 15% -10%, var(--brand-tint), transparent 60%),
        radial-gradient(50rem 34rem at 110% 110%, var(--brand-tint-strong), transparent 62%),
        var(--bg);
}
.auth__card {
    width: 100%; max-width: 26rem;
    background: var(--surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    padding: var(--s6);
}
.auth__brand {
    display: flex; align-items: baseline; gap: .35rem;
    font-size: 1.3rem; letter-spacing: -.015em; margin-bottom: var(--s2);
}
.auth__brand strong { font-weight: 650; color: var(--brand-deep); }
.auth__brand span   { font-weight: 400; color: var(--muted); }
/* .5rem between fields and .75rem above the first one. The legacy's are about
   8px and 10px; this is the same shape without being cramped. */
.auth__form { display: flex; flex-direction: column; gap: var(--s2); margin-top: var(--s3); }
.auth__foot { margin-top: var(--s3); font-size: var(--fs-small); color: var(--muted); }
.auth__foot p { max-width: none; }

.alert {
    display: flex; gap: var(--s3); align-items: flex-start;
    border-radius: var(--radius); padding: var(--s3) var(--s4);
    font-size: var(--fs-body);
}
.alert--error { background: var(--error-bg); color: var(--error-ink); font-weight: 600; }
.alert--info  { background: var(--info-tint); color: var(--brand-deep); }

/* A HIDDEN ALERT STAYS HIDDEN. The browser's own [hidden] rule is display:none
   at the lowest specificity there is, so the display:flex above beats it and an
   alert carrying the attribute renders anyway -- as an empty coloured bar. The
   supplier list's error box did exactly that, a pink strip under the grid,
   found by screenshot (23 September 2026). Same shape of answer as .ovl[hidden]
   and .eq-close__pane[hidden]: scoped to the component that sets display. */
.alert[hidden] { display: none; }

/* Something succeeded and the rep should notice. Uses --good, which is the legacy's own
   call-status green #459E00 -- so "this worked" is the same green throughout the app rather
   than a second one invented per screen. Deliberately NOT bold: a confirmation is read once
   and then ignored, unlike an error. */
.alert--ok    { background: var(--good-tint); color: var(--good); }
.alert p { margin: 0; max-width: none; }

/* The over-discount legend on the Admin quote list. Uses the legacy error pink
   (#ffb5b5) because that is the colour of the rows it explains -- a legend in a
   different shade from the thing it describes is worse than none. Legacy text
   and colour: EQNetAdmin/OpenQuotes.aspx.vb:673 and :386. */
.alert--overdiscount {
    background: var(--error-bg);
    color: var(--error-ink);
    font-weight: 600;
    text-align: center;
    border-radius: var(--radius);
    padding: var(--s2) var(--s4);
    margin: 0 0 var(--s4);
    max-width: none;
}

/* A checkbox with its label on one line, sized to sit level with the adjacent
   select controls in a filter bar rather than baseline-drifting above them. */
.checkline {
    display: inline-flex; align-items: center; gap: var(--s2);
    font-weight: 400; color: var(--ink);
    padding: .45rem 0;
    white-space: nowrap; cursor: pointer;
}
.checkline input[type="checkbox"] { width: auto; margin: 0; accent-color: var(--brand-deep); }

/* ---------------------------------------------------------------------------
   13  UTILITIES AND MOTION
   --------------------------------------------------------------------------- */
.row      { display: flex; gap: var(--s4); flex-wrap: wrap; align-items: center; }
.spacer   { flex: 1 1 auto; }
.stack    { display: flex; flex-direction: column; gap: var(--s4); }
.muted    { color: var(--muted); }
.right    { text-align: right; }
.visually-hidden {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

ul.checks { padding-left: 1.15rem; max-width: 78ch; }
ul.checks li { margin: var(--s1) 0; }

/* Motion rewards interaction; it never delays reading. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
    }
    /* A busy state must stay VISIBLE when motion is suppressed, otherwise
       reduced-motion users lose the feedback entirely -- and it must still read
       as WORKING. It used to stop here as a full grey ring, and a ring that never
       moves reads as stuck: Tim, 7 October 2026, on the Studio's Working panel
       (his browser asks for reduced motion -- Windows with animation effects off,
       as a Remote Desktop session commonly is): "Can we have a rotating circle
       next to the word working while it is processing the image?" So the spin is
       SOFTENED, not dropped (the house rule allows either): the same small ring,
       its gap kept so the turn shows, at a third of the speed -- nothing that
       sweeps across the screen -- and only the spinner; everything else above
       stays still. The class selector and !important are what lift it over the
       blanket rule above. */
    .spinner { animation-duration: 1.8s !important; animation-iteration-count: infinite !important; }
    .is-loading > .loading-bar { background: var(--brand); }
}

@media print {
    .topbar, .filters, .pager, .grid-foot, .btn { display: none !important; }
    body { background: #fff; }
    .grid-wrap, .card { box-shadow: none; }
}

/* ---------------------------------------------------------------------------
   DEALERSHIP OVERVIEW -- the management dashboard's summary grids.

   Twelve numeric columns per salesperson, two departments. The design problem
   here is density: a manager scans for the outliers, so the grid has to make a
   breached threshold findable without reading every cell.
   --------------------------------------------------------------------------- */

.dept { margin: 0 0 var(--s6); }

.dept__head {
    margin: 0 0 var(--s2);
    font-size: 1.15rem;
    color: var(--brand-deep);
}

.dept__meta {
    margin: 0 0 var(--s3);
    font-size: var(--fs-small);
    color: var(--muted);
    max-width: none;
}
.dept__sep { margin: 0 .5rem; opacity: .5; }

/* Every column but the first is a count. Right-aligned with tabular figures so
   the digits line up vertically and a long column can be scanned for magnitude
   without reading it -- which is the whole point of a dashboard. */
table.grid--numeric th,
table.grid--numeric td { text-align: right; font-variant-numeric: tabular-nums; }

table.grid--numeric .grid__name,
table.grid--numeric th.grid__name { text-align: left; white-space: nowrap; }

/* The header cells wrap; the legacy uses literal <br/> inside two of its header
   labels to force a break. Allowing a natural wrap in a fixed max-width keeps
   twelve headings legible without hard-coding where each one breaks. */
table.grid--numeric thead th { white-space: normal; max-width: 7.5rem; }

/* THE COLUMN GROUPS: Leads | Prospects and reminders | Management and delivery.
   The legacy separates these with three empty spacer columns whose contents are
   three non-breaking spaces on every row of every grid. A left border does the
   same job for nothing per row. */
table.grid--numeric .grid__group { border-left: 2px solid var(--border-strong); }

/* A DISABLED salesperson who still has open work. The legacy sets this cell's
   background to Drawing.Color.Pink and bolds the name (ReportDealershipOverview
   .aspx.vb:930). Only the NAME cell is marked, not the row -- a fully pink row
   reads as an error, and this is not an error, it is "somebody has to pick this
   person's work up". */
table.grid td.is-disabled-rep {
    background: var(--error-bg);
    font-weight: 650;
}

/* A BREACHED THRESHOLD: BRIGHT STATIC RED, never animated.
   ---------------------------------------------------------------------------
   TIM'S DECISION, 21 August 2026: "i dont like the flashing RED please make it
   a bright static red."

   The legacy gives these numbers CssClass "blink" and they genuinely flash
   (ReportDealershipOverview.aspx.vb sets it on every threshold breach). That
   WAS reproduced here, on the reasoning that a manager finds the flashing cell
   before reading anything. Tim has ruled against it, and he is right: on a
   dense grid where several cells can breach at once, the flashing competes with
   itself and with reading the numbers.

   DO NOT REINTRODUCE THE ANIMATION. This is a deliberate, requested departure
   from the legacy, not an oversight -- and it is also better for the low-spec
   machines this project exists to serve, since an infinite animation forces the
   compositor to keep repainting.

   #d40000 rather than the softer --error-ink used for form errors: this has to
   read as an alarm across a wide grid at a glance, which is exactly the job the
   blink was doing. Bold weight carries it for anyone who cannot rely on colour.
   --------------------------------------------------------------------------- */
.alertnum {
    color: #d40000;
    font-weight: 700;
}

/* ---------------------------------------------------------------------------
   AND IT HAS TO WIN ON A LINK, which is where every one of these actually is.

   THIS WAS A LIVE BUG and Tim found it: "you need to understand the logic based
   per number. So as an example, certain items turn red at certain values."

   The logic was right and the colour was not arriving. Every drill-down number is
   an <a>, and

       table.grid--numeric td a { color: inherit; }

   is (0,2,2) against .alertnum's (0,1,0), so the alert inherited the row's ink.
   font-weight was not in that rule, so the number came out BOLD AND UNDERLINED
   BUT BLACK -- which reads as "emphasised" rather than "over threshold", and is
   why the screen looked calm with eight breaches on it.

   Written for the shared .grid rather than only the overview: the same alert
   class is used by any grid with a threshold, and the same override would bite
   there. Cell-type qualified (td/th) so it also holds in a totals row.
   --------------------------------------------------------------------------- */
table.grid td a.alertnum,
table.grid th a.alertnum,
table.grid td a.alertnum:hover,
table.grid th a.alertnum:hover,

/* ---------------------------------------------------------------------------
   AND IT BROKE AGAIN, THE SAME WAY, FOR A DIFFERENT REASON. Fixed 21 Sep 2026.

   The override above is (0,2,2). So is `table.grid--numeric td a { color:
   inherit }` further down this file -- and on a TIE THE LATER RULE WINS. The
   alert red was written first, so it lost to a rule 80 lines below it, and
   every breached threshold rendered in the cell's ordinary ink again.

   Measured on FOR123 before the fix: the markup emitted class="alertnum" on
   three numbers -- 13 prospects without reminders (limit 2), 33 and 17 overdue
   reminders (limit 10) -- and all three rendered as ordinary links. Tim saw a
   screen with eight breaches and no red on it, and asked for the links to be
   made red, which is the symptom rather than the cause.

   SPECIFICITY, NOT ORDER, is the fix. Naming grid--numeric makes these (0,3,2)
   so they beat the inherit whatever order the file ends up in -- source order
   is what failed twice now, and a third editor moving a block would break it
   again. */
table.grid--numeric td a.alertnum,
table.grid--numeric th a.alertnum,
table.grid--numeric td a.alertnum:hover,
table.grid--numeric th a.alertnum:hover {
    color: #d40000;
    font-weight: 700;
}

/* Kept as a no-op class so the markup and the AlertBlinks() distinction the
   legacy makes (the Deliveries alert never blinked) stay expressible without a
   view change if Tim ever wants them differentiated again. */
.alertnum--blink { /* intentionally no animation -- see above */ }

/* THE TOTALS ROW STICKS TO THE BOTTOM, by the same argument as the heading.

   Not asked for, and offered rather than assumed: a dealership total that
   scrolls out of the bottom of a long grid has the identical problem Tim
   described at the top -- you are looking at rows and the number they add up to
   is somewhere off screen. One line, and it goes if he would rather it did not.

   z-index 1: under the heading, over the rows. */
table.grid tfoot th,
table.grid tfoot td {
    position: sticky;
    bottom: 0;
    z-index: 1;
}

/* The totals row. Quieter than the header so it reads as a summary of the
   column rather than another heading. */
table.grid tfoot th {
    background: var(--bg);
    border-top: 2px solid var(--border-strong);
    font-weight: 700;
    color: var(--ink);
    white-space: nowrap;
}

.tag--muted { background: var(--error-bg); color: var(--error-ink); }

/* Visually hidden but read aloud. Each grid carries a caption naming its
   department and branch, so a screen-reader user reaching the second table
   knows which one it is without counting headings. */
.sr-only {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Denial and warning are different things and should not look the same: a
   refused branch is a boundary working correctly, not a fault. */
.alert--deny { background: var(--error-bg); color: var(--error-ink); font-weight: 600; }
.alert--warn { background: var(--info-tint); color: var(--brand-deep); }

/* The reasons a vehicle is not ready to advertise, inside its alert.

   A LIST RATHER THAN THE LEGACY'S COMMA-SEPARATED STRING. The stored
   BrochureToolTip reads "Category, Price, Price, " -- repeated words and a
   trailing comma, because three separate price checks each append their own --
   and that is what this banner used to print. Each entry here is one thing a
   person can go and fix.

   Tight margins because it sits inside an alert that already has padding, and
   the disc sits inside the box rather than hanging outside it. */
.brochure-reasons {
    margin: .4rem 0 0;
    padding-left: 1.15rem;
}

.brochure-reasons li {
    margin: .15rem 0;
}

/* .splitlinks IS GONE, with the markup that used it. It rendered the overdue
   column as "11 + 9" -- my addition, not the legacy's, which shows one number and
   one link. Tim, 27 August 2026: "I do not need the breakdown". Removed rather
   than left behind, because dead CSS is read as live by the next person and
   invites the markup back. See _DealershipOverviewGrid.cshtml for the full note. */

/* Drill-down numbers must still read as numbers, so the link carries no
   underline until hovered -- the alert colour and weight are the signal, and an
   underline on every non-zero cell would make a dense grid unreadable. */
table.grid--numeric td a {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px dotted currentColor;
}
table.grid--numeric td a:hover,
table.grid--numeric td a:focus-visible {
    text-decoration: underline;
    border-bottom-color: transparent;
}

/* ==========================================================================
   LEAD STATUS REPORT
   ========================================================================== */

/* Month stepping. The legacy renders these as "< PREVIOUS" and " NEXT > " with
   fifteen &nbsp; between them; a flex gap does the same job without the bytes. */
.monthstep { display: flex; gap: var(--s2); align-items: center; margin: 0 0 var(--s3); }

/* --------------------------------------------------------------------------
   The call-activity traffic light on the contact-number cell.

   THESE FOUR COLOURS CARRY MEANING AND ARE NOT FREE TO CHANGE. They are the
   legacy's own values, from AdminOtherLeadStatusHistory.aspx.vb:2128-2152, and a
   dealership manager reads them to see whether leads are being worked:

     green  #A1FFA9  answered, 60 seconds or more -- a real conversation
     yellow #FFE599  answered, under 60 seconds
     orange #FFBF7F  not answered, or answered with no duration recorded
     pink   #FABABD  call data exists for the period but none for this lead

   An UNCOLOURED cell is a fifth state and means "no call data at all", which is
   different from "not called". Do not give it a default colour -- conflating
   unknown with not-called would tell a manager a rep ignored a lead when the
   truth is that nobody knows.

   Text stays near-black on all four rather than inheriting the theme's body
   colour, because these backgrounds are fixed light tints and a dark-theme
   foreground on them is unreadable. That is deliberate, not an oversight.
   -------------------------------------------------------------------------- */
.call { color: #111; }
.call--talk     { background: #A1FFA9; }
.call--brief    { background: #FFE599; }
.call--noanswer { background: #FFBF7F; }
.call--none     { background: #FABABD; }

/* The "(n)" affordance the legacy shows when more than one call matched. */
.callcount { font-size: var(--fs-eyebrow); opacity: .7; margin-left: .25rem; }

.legend { display: flex; flex-wrap: wrap; gap: var(--s3); margin: var(--s2) 0; }
.legend__item { display: inline-flex; align-items: center; gap: .4rem; font-size: var(--fs-eyebrow); }
.legend .call { border: 1px solid rgba(0,0,0,.25); padding: 0 .15rem; }

/* --------------------------------------------------------------------------
   The grid. Markers are single characters in ten narrow columns, so they are
   centred rather than right-aligned -- an 'X' right-aligned under a wide header
   like "Finance Declined" reads as belonging to the next column.
   -------------------------------------------------------------------------- */
table.grid--leads td.num, table.grid--leads th.num { text-align: center; }
table.grid--leads tfoot .foot-label { text-align: right; font-weight: 600; }
table.grid--leads tfoot td { font-weight: 650; font-variant-numeric: tabular-nums; }

/* A cell carrying hover text should say so, or nobody discovers it. The legacy
   prints a whole line of instruction above the grid instead. */
table.grid--leads td[title]:not([title=""]) { cursor: help; }

/* ---------------------------------------------------------------------------
   NARROWING THE REPORT -- Tim, 12 September 2026

     "Added date, we've got date and time. If you put date at the top and time
      below, that will make that column narrower ... the heading itself, new
      used both. The heading itself is taking up the space, not the data below
      it. So I think make the heading multiline as well ... so that we can try
      and not scroll horizontally."

   Two separate causes of width, and they need different answers.

   1. THE DATA. A date-and-time stamp on one line is twice the width of either
      half. Stacked, the column is as wide as "18/08/2026" and the row is no
      taller, because these rows already run to two lines for a wrapped client
      name.

   2. THE HEADING. In a table using AUTO layout -- which this is, and must stay,
      because the column widths should follow the data -- a column is at least
      as wide as its widest cell's max-content width. `white-space: normal` on
      its own therefore changes NOTHING here: with no width constraint the
      browser simply takes the heading's full width and never wraps. The
      max-width is what makes wrapping actually happen, and the pair has to be
      set together.

      Same pair, same reasoning, as table.grid--numeric above, which is the
      Dealership Overview and has worked this way since August. This follows it
      rather than inventing a second approach.

   7rem fits "Finance" and "Approved" on two lines and leaves the single-word
   headings alone. It is a CEILING on the heading, not on the column: a cell
   whose data is wider still widens the column, so nothing is truncated.
   --------------------------------------------------------------------------- */
table.grid--leads thead th { white-space: normal; max-width: 7rem; }

/* A stacked date cell. Each LINE stays unbroken -- wrapping inside "18/08/2026"
   would defeat the point -- while the two lines sit one above the other. */
table.grid td.dt { white-space: nowrap; }
table.grid td.dt .dt__date,
table.grid td.dt .dt__time { display: block; }

/* The time is the subordinate half: same size, quieter colour. Shrinking it as
   well was tried and read as damage rather than hierarchy on a dense grid. */
table.grid td.dt .dt__time { color: var(--ink-soft); }

/* --------------------------------------------------------------------------
   Advanced filter: the stock exclusion list.
   -------------------------------------------------------------------------- */
.adv { margin: var(--s3) 0; }
.adv > summary { cursor: pointer; font-weight: 600; }
.stocklist {
    list-style: none;
    margin: var(--s2) 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
    gap: var(--s1) var(--s3);
}
.stocklist label { display: flex; align-items: baseline; gap: .5rem; }

.note--scope { margin-top: var(--s3); max-width: 68ch; }

/* --------------------------------------------------------------------------
   The lead-notes overlay.

   Built by lead-notes.js, which is progressive enhancement over a real link --
   so none of this is required for the notes to be reachable.
   -------------------------------------------------------------------------- */
.ovl {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,.45);
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: var(--s4) var(--s3);
    z-index: 100;
    overflow-y: auto;
}
.ovl[hidden] { display: none; }
.ovl__panel {
    position: relative;
    background: var(--surface);
    color: var(--ink);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 1.5rem 3rem rgba(0,0,0,.3);
    width: min(100%, 46rem);
    padding: var(--s3);
}
.ovl__close {
    position: absolute;
    top: var(--s1);
    right: var(--s1);
    background: none;
    border: 0;
    font-size: 1.6rem;
    line-height: 1;
    color: var(--muted);
    cursor: pointer;
}
.ovl__close:hover { color: var(--ink); }
.ovl__body { margin-top: var(--s2); }
body.ovl-open { overflow: hidden; }

.notes__head { margin: 0 0 var(--s2); font-size: var(--fs-h3); }
.notes__text { white-space: normal; }

/* The busy line inside the overlay. A pulse rather than a spin keeps it cheap on
   a weak CPU, and prefers-reduced-motion drops it entirely while leaving the
   text -- the busy STATE must survive, only the animation goes. */
.is-busy { animation: eq-pulse 1.2s ease-in-out infinite; }
@keyframes eq-pulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) {
    .is-busy { animation: none; }
}

/* ---------------------------------------------------------------------------
   Export links on the Lead Status Report.

   The heading becomes a flex row so the download links sit at its right edge,
   away from the title. They are deliberately quiet (ghost buttons, small): an
   export is an occasional action, and the report itself is what the reader came
   for. In the legacy these are a green Excel icon per grid.
   --------------------------------------------------------------------------- */
.dept__head {
    display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
}

/* margin-left:auto rather than justify-content:space-between, so the total stays
   next to the heading it belongs to instead of drifting to the middle. */
.dept__export { margin-left: auto; display: flex; gap: var(--s2); }

.btn--sm {
    padding: .25rem .6rem;
    font-size: var(--fs-small);
}

.grid-foot--export { justify-content: flex-end; gap: var(--s3); }

/* A link mid-download is not clickable again, and must look it. */
.btn.is-busy { opacity: .7; cursor: progress; pointer-events: none; }

/* The spinner inherits currentColor, which on the green button is white -- so it
   is visible without a second rule. Stated because the obvious "fix" of giving
   the spinner a fixed dark colour would make it invisible here. */

.note--warn { color: var(--error-ink); font-weight: 600; }

@media print {
    /* An export link in a printed report is meaningless. */
    .dept__export, .grid-foot--export { display: none !important; }
}

/* ---------------------------------------------------------------------------
   Quote detail (Pass A). A read-only record, so the layout optimises for
   scanning rather than for filling in: label-value pairs in columns, and the
   grand total given its own line because it is the figure everyone looks for.
   --------------------------------------------------------------------------- */
.qd__head {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: var(--s4); flex-wrap: wrap;
    margin-bottom: var(--s2);
}

/* THE QUOTE TITLE IN THE LEGACY'S OWN VOICE: light small-caps grey, not a bold
   navy headline. (BaseStyle .Head1 16pt/200 small-caps; the quote screen's own
   header block is smaller still.) font-weight 200 on a heading looks like a
   mistake to anyone tidying up, which is exactly why it is commented -- it is
   the most recognisable thing about these screens. */
.qd__title {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 200;
    font-variant: small-caps;
    color: var(--eq-heading);
}
.qd__sub { margin: .1rem 0 0; color: var(--eq-body-ink); font-size: var(--fs-small); max-width: none; }
.qd__sub span { margin-right: .25rem; }

/* The quote number, first thing on the subtitle line and darker than the rest of
   it. Tim: "the quote no is important" -- it is the handle for the whole
   conversation, so it does not read as one more grey attribute. */
.qd__no { color: var(--ink); font-weight: 700; }

/* The accessories total, under the tables rather than inside one footer (the
   reason is at the call site in _QuoteDetail.cshtml). Right-aligned so the figure
   lands under the money column it totals -- left-aligned it reads as a caption
   rather than as arithmetic. */
.qd__linetotal {
    display: flex;
    justify-content: flex-end;
    align-items: baseline;
    gap: var(--s3);
    margin: var(--s2) 0 0;
    padding: .18rem .4rem;
    /* p carries a 68ch reading measure globally (design brief: widening the frame
       must never widen the line length). This is a figure in a column, not prose,
       and the cap stopped it half way across the table it totals. */
    max-width: none;
}
.qd__linetotal strong { font-variant-numeric: tabular-nums; }

/* A sub-heading inside a panel (Used vehicle, Totals, Program money, Comments).
   .eq-h3 carries the centred small-caps underline and sets margin: 0, so this
   only supplies the rhythm -- and sets it tight, for the reason in the .card
   note above. */
.qd__subhead { margin: var(--s3) 0 var(--s2); }

.pill--open   { background: var(--brand-tint); color: var(--brand-deep); }
.pill--closed { background: var(--warn-tint);  color: var(--warn); }

/* ---------------------------------------------------------------------------
   LABEL-AND-VALUE ROWS, IN THE LEGACY'S FRAMED BOX.

   Same shape as .eq-box / .eq-box__row on the financial step, which is the
   pattern Tim approved: a hairlined frame round the group, and rows ruled off
   from each other. The frame is information rather than decoration -- it says
   which figures belong together, which is why the financial step has three of
   them side by side instead of one long list.

   The rows are DENSE on purpose. The previous version floated the pairs in
   space at a .35rem gap with no frame, which read as a modern spec sheet and
   left the quote view visibly unlike every other screen in the app. It also
   cost about 12px a row, and a quote has around thirty rows.
   --------------------------------------------------------------------------- */
.kv {
    display: grid;
    margin: 0;
    border: 1px solid var(--eq-grid-line);
    /* ONE SHEET WITH THE CARD. Was var(--surface): a white panel on a blue
       card is exactly the layering Tim asked to be rid of. The frame stays,
       because it is what keeps three columns reading as three panels. */
    background: var(--eq-formbox);
}
/* ---------------------------------------------------------------------------
   THE LABEL COLUMN IS PALE BLUE, THE VALUE COLUMN IS WHITE.

   Tim, 21 September 2026, marking three areas on the vehicle editor: "Let's
   make area three white, area one blue and area two white" -- area one being
   the caption, area two the control, area three the card around them.

   The first attempt put the blue on the CARD and he rejected it, correctly:
   caption and control were still white against each other, and those two are
   the pair the eye travels between on every row. Colour the boundary that is
   actually being read, not the box around it.

   It is the legacy's look as well. GM.css pairs a pale blue label cell with a
   white value cell in its detail tables, so a rep of twenty years reads the
   left column as "what this is" without being told.

   PADDING MOVED FROM THE ROW TO THE CELLS, and it had to. With the padding on
   .kv__row the blue would have been inset by .4rem on the left and floated
   clear of the value cell by the grid gap -- a blue rectangle sitting inside
   the row rather than a column running down the panel. The gap goes for the
   same reason; the cells' own padding restores the same breathing space.
   --------------------------------------------------------------------------- */
.kv__row {
    display: grid;
    grid-template-columns: minmax(8rem, 13rem) 1fr;
    line-height: 1.35;
}
/* Ruled BETWEEN rows, not around them, so the frame stays a single box.

   THE SAME BLUE AS THE VERTICAL RULE, and the same as the panel's own frame.
   Tim, 21 September 2026: "make the horizontal devider lines the same colour
   as the vertical devider lines." It was var(--border), a grey left over from
   when the panel was white -- so the grid of a single blue panel was drawn in
   two colours, one of which no longer belonged to anything on the screen. */
.kv__row + .kv__row { border-top: 1px solid var(--eq-formbox-line); }
.kv dt {
    color: var(--eq-body-ink);
    font-size: var(--fs-small);
    background: var(--eq-formbox);
    border-right: 1px solid var(--eq-formbox-line);
    padding: .18rem .5rem;
    /* FLEX SO THE CAPTION CENTRES WITHOUT THE CELL SHRINKING. The cell must
       fill the row's full height or the blue stops being a column -- see
       .kv__row--edit, where centring the GRID ITEM left white above and below
       the caption on every tall row. Centring the TEXT inside a stretched cell
       gets the alignment without the gap. */
    display: flex;
    align-items: center;
}
/* No background of its own -- it takes the panel's blue. The only white in
   the row is the control inside it, which declares var(--surface) itself
   (see the base input rule): white now MEANS "this captures data". */
.kv dd { margin: 0; padding: .18rem .5rem; }

/* Three columns on a wide screen, collapsing to one. auto-fit rather than a fixed
   count so the financial block reflows instead of scrolling on a laptop. */
.kv-cols {
    display: grid; gap: var(--s3);
    /* 17rem, down from 19rem: the boxes now carry their own frame and sit
       tighter, so three fit on a 1280px column where before two did and the
       third dropped to a second row -- 60px of height for nothing. */
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
}

.kv--totals { max-width: 26rem; }

.card__sub { margin: var(--s4) 0 var(--s2); font-size: 1rem; color: var(--brand-deep); }
/* The count beside a panel heading ("4 lines", "1 vehicle").

   The three resets are not tidying: the heading it sits inside is .eq-h3, which
   is small-caps AND underlined, and both inherit. "4 LINES" underlined reads as
   a second heading rather than as a count, so the span opts out and stays
   ordinary sentence text. */
.card__meta {
    margin-left: .5rem;
    font-weight: 400;
    font-size: var(--fs-small);
    color: var(--muted);
    font-variant: normal;
    text-transform: none;
    /* display:inline-block is what actually removes the underline, and it is the
       only thing that does. An ancestor's text-decoration is DRAWN BY the
       ancestor and propagates through inline descendants -- `text-decoration:
       none` on a child cannot cancel it. An atomic inline-level box is not part
       of that propagation, so this is the fix, not a layout preference. */
    display: inline-block;
    text-decoration: none;
}

.qd__grand {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: var(--s4); max-width: 26rem;
    margin: var(--s2) 0 0; padding: .2rem .4rem;
    /* Ruled in the legacy's own ink rather than a pale grey, because this is the
       figure every reader is looking for. .eq-box__row--total uses the same. */
    border-top: 2px solid var(--ink);
    font-size: 1.05rem;
    font-weight: 700;
}

/* A corrupt amount is shown as stored, in brackets, and marked. It must not look
   like an ordinary figure -- 95 rows in this estate hold the literal "NaN", and a
   reader who mistakes one for a number has a wrong price. */
.is-corrupt { color: var(--error-ink); font-family: var(--font-mono, monospace); }

/* An optional extra is quoted but NOT counted in the total, so the row is muted
   to match the "optional, not in total" tag rather than reading as a live line. */
.row--optional td { opacity: .72; }

.tags { display: flex; gap: .3rem; flex-wrap: wrap; }
.tag {
    display: inline-block; padding: .05rem .4rem;
    border-radius: var(--radius-pill);
    background: var(--warn-tint); color: var(--warn);
    font-size: .72rem; white-space: nowrap;
}
.tag--quiet { background: var(--surface); color: var(--muted); border: 1px solid var(--border); }

/* A label that IS present, beside .tag--quiet ones that are not -- the vehicle
   Brochure Details extras list. --good is the legacy's own green (the call
   traffic light above), so "included" reads the way it reads everywhere else in
   the system rather than as a warning, which is what plain .tag would say. */
.tag--on { background: var(--good-tint); color: var(--good); border: 1px solid var(--good-tint); }

/* The same list as TICKS, once script has run -- the legacy's CheckBoxList,
   two columns for the standard labels and four for a dealership's own
   (VehicleStocklistVer3.aspx:9884, :9898). One wrapping grid gives both, and
   collapses to a single column on a phone, so nothing is hidden (rule 5.4e).
   There was no house tick-list to reuse: every other list of choices in this
   application is links or radios. */
.eq-extras-ticks {
    display: grid; gap: var(--s1) var(--s3);
    grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
    margin: var(--s2) 0;
}

/* THE hidden ATTRIBUTE DOES NOT HIDE THESE ON ITS OWN. The browser's [hidden]
   rule is the user-agent's, and ANY author display rule beats it whatever its
   specificity -- which is how .alert kept showing an empty red box (the rule at
   .alert[hidden]). The Brochure tab swaps the tags for the ticks, and reveals
   Reset Text, by setting and clearing hidden; each of those needs this line or
   the swap shows both, or neither. */
.eq-extras-ticks[hidden],
.tags[hidden],
.btn[hidden] { display: none; }

/* A BOX WITH A SMALL BUTTON BESIDE IT -- the Brochure tab's Youtube Link and
   its Reset Text, and Manage Extras under the tick-list. The edit row's value
   cell is a flex COLUMN (.kv__row--edit > dd), and a column stretches a bare
   button across the whole row -- which is how Reset Text first rendered, as a
   grey bar the width of the screen. One line instead, wrapping on a phone.

   The input rule is qualified to (0,3,1) so it beats .kv__row--edit
   input[type="text"] at (0,2,1) honestly (CLAUDE.md 5.4n); it keeps that rule's
   22rem cap and only lets the box share its line. */
.kv__inline { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); }
.kv__row--edit .kv__inline input[type="text"] { flex: 1 1 14rem; }
.kv__inline > .btn { flex: 0 0 auto; }

/* Something is WRONG rather than merely worth noting -- a photograph whose file
   cannot be reached at all. --bad is the legacy's own red from the call traffic
   lights, so it carries the same meaning it carries everywhere else. Plain .tag
   is already amber, which is the right weight for "no full-size version" and
   the wrong weight for "this image is gone". */
.tag--bad { background: var(--bad-tint); color: var(--bad); border: 1px solid var(--bad-tint); }

/* ---------------------------------------------------------------------------
   PICTURE GALLERY -- a used vehicle's photographs
   ---------------------------------------------------------------------------
   auto-fill with a minimum rather than a fixed column count, so the gallery
   uses whatever width rule 5.5 has given the page: five across at 1766px, two
   on a phone, without a single media query. The legacy floats fixed 290px tiles
   and wraps wherever it lands.

   The tile is 300x225 because that is the size of the file being fetched --
   ThumbImagePath. Sizing a box to the image, rather than fetching a 1024x768
   and letting CSS shrink it into 288x216 as the legacy does, is the whole point
   of dossier defect 72.
   --------------------------------------------------------------------------- */
.pics {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: .75rem;
    margin: .5rem 0 .75rem;
}

.pics__tile {
    margin: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    overflow: hidden;
}

/* ---------------------------------------------------------------------------
   REORDERING BY DRAG -- the affordance and the two feedback states.

   Tim, 7 September 2026: drag a tile onto another and everything shifts down.

   grab/grabbing is the whole affordance, so it must only appear on a tile that
   can actually be dragged: draggable is rendered false when the row has no file
   number, and a hand cursor over a tile that refuses to move is worse than an
   arrow. Hence the attribute selector rather than the class.
   --------------------------------------------------------------------------- */
.pics__tile[draggable="true"] { cursor: grab; }
.pics__tile[draggable="true"]:active { cursor: grabbing; }

/* The tile being carried. Kept faint rather than hidden, so the rep can still
   see WHICH photograph is in flight -- the drag image the browser paints is
   translucent and easy to lose over a busy gallery. */
.pics__tile.is-dragging { opacity: .4; }

/* The tile under the cursor: where it will land. A 2px inset ring rather than a
   border change, because a border would resize the tile and shove the rest of
   the grid sideways mid-drag, which makes the target move as you aim at it. */
.pics__tile.is-dropzone {
    box-shadow: inset 0 0 0 2px var(--brand);
    background: var(--brand-tint);
}

/* The up/down arrows. NOT a mobile fallback -- drag events do not fire on a
   touch screen and a drag is unreachable by keyboard, so these are the only
   path for a phone, a keyboard or a screen reader (rule 5.4e: a control a rep
   can operate is never hidden). They sit beside the position number because
   that is the value they change. */
.pics__move {
    display: inline-flex;
    gap: 1px;
}

.pics__movebtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.4rem;
    height: 1.4rem;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--ink-soft);
    font: inherit;
    font-size: .6rem;
    line-height: 1;
    cursor: pointer;
}

.pics__movebtn:hover {
    border-color: var(--brand);
    color: var(--brand);
}

/* 44px touch floor comes from the shared mobile band; nothing needed here. */

/* The save bar, revealed by app.js once the order actually differs. It is
   `hidden` in the markup and toggled with el.hidden, so with script blocked it
   never appears -- correct, because with script blocked nothing can be
   reordered either. */
/* THE REMOVE CONTROL -- a red rubbish bin. Tim asked for the bin on 7 September
   2026, having also found that the cross it replaced DISAPPEARED on hover.

   That bug is worth recording because the mechanism is silent: the hover rule
   said `background: var(--danger)` and `--danger` is not a token in this file.
   CSS drops an unresolvable var without a word, so the background stayed white
   and only `color: #fff` applied -- a white glyph on white. The real token is
   --bad (#cc0000). StylesheetTokenTests now catches this class of mistake; it
   did not before, because ".btn--danger:hover" read as a definition of
   "--danger" to its own regex.

   RED FROM THE START, not only on hover. The earlier version was deliberately
   quiet on the argument that a permanently red button on eighteen tiles turns
   the gallery into a warning -- but a delete control that does not look like one
   until you touch it is the worse trade, and the bin shape carries the meaning
   even at 14px. */
.pics__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.4rem;
    height: 1.4rem;
    margin-left: auto;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--bad);
    font: inherit;
    font-size: .85rem;
    line-height: 1;
    cursor: pointer;
}

/* Hover FILLS rather than recolours the glyph, so there is no state in which the
   icon and its background can be the same colour. That is the specific failure
   above, made structurally impossible rather than merely corrected. */
.pics__remove:hover,
.pics__remove:focus-visible {
    border-color: var(--bad);
    background: var(--bad);
    color: #fff;
}

/* The two glyphs. The bin normally; the undo arrow once the tile is marked, so
   the control that removed it is the control that puts it back. */
.pics__bin { display: block; }
.pics__unremove { display: none; }

.pics__tile.is-removed .pics__bin { display: none; }
.pics__tile.is-removed .pics__unremove { display: block; }

/* A tile MARKED for removal, which is not the same as a tile that is gone.
   It stays in place and stays legible -- that is the point of staging: the stock
   controller can see what they are about to lose and change their mind.

   The photograph is dimmed and desaturated rather than hidden, and the tile
   keeps its footprint, so nothing reflows when four are marked in a row. */
.pics__tile.is-removed {
    border-color: var(--bad);
    background: var(--bad-tint);
}

.pics__tile.is-removed img {
    opacity: .35;
    filter: grayscale(1);
}

/* Its own remove button becomes the undo, so it stays legible against the
   tinted tile rather than inheriting the quiet treatment above. */
/* On a marked tile the control is the UNDO, so it stops looking destructive --
   red on red would also be unreadable against the tinted tile. */
.pics__tile.is-removed .pics__remove {
    border-color: var(--bad);
    color: var(--bad);
    background: #fff;
}

/* The arrows go dead on a marked tile: it has no position to move to. Hidden
   rather than disabled would violate rule 5.4e for a control that is coming
   straight back the moment the removal is undone. */
.pics__tile.is-removed .pics__movebtn {
    opacity: .4;
    pointer-events: none;
}

/* The crop dialog's title row, with the close cross pushed to the right.
   Tim asked for a visible way out on 8 September 2026 -- Cancel and Escape both
   worked already, but neither was discoverable in a stage that fills the
   screen. */
.crop-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s3);
}

.crop-close {
    flex: 0 0 auto;
    width: 1.9rem;
    height: 1.9rem;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
}

.crop-close:hover,
.crop-close:focus-visible {
    border-color: var(--ink);
    background: var(--ink);
    color: #fff;
}

/* THE DEVELOPMENT-ONLY TOOL STRIP. Deliberately does not look like part of the
   application: a dashed amber rule and small text, so nobody demonstrating the
   screen mistakes it for a feature a dealership gets. It is absent in production
   by registration, not by CSS. */
.pics__devtools {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    flex-wrap: wrap;
    margin: 0 0 var(--s3);
    padding: var(--s2) var(--s3);
    border: 1px dashed var(--warn, #b8860b);
    border-radius: var(--radius-sm);
    background: var(--warn-tint);
}

/* The save bar. There are TWO -- one above the grid and one below it -- so Save
   is reachable from either end of a long gallery. Tim, 7 September 2026: "whether
   I'm working at the top of my image stack or the bottom, I still have access to
   the save button without having to scroll." The measured mean is 18.1
   photographs per vehicle and the worst carries 3,465.

   LEFT-ALIGNED, as he asked, and it also puts Save under the first tile's own
   controls rather than diagonally across the gallery from them. */
.pics__orderbar {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: var(--s3);
    margin: var(--s2) 0;
}

.pics__orderbar--top { margin: 0 0 var(--s2); }
.pics__orderbar--bottom { margin: var(--s2) 0 var(--s3); }

/* IDLE -- nothing staged. The two bars behave DIFFERENTLY on purpose.

   The TOP bar keeps its space (visibility), because collapsing it would push
   every tile down the instant a photograph is dragged: the screen jumping under
   the rep, which rule 5.4a exists to stop. The cost is one bar's height of
   whitespace above the gallery, which is the cheaper of the two.

   The BOTTOM bar collapses (display), because nothing sits below it to move.

   visibility: hidden also takes the buttons out of the tab order, so a keyboard
   user cannot reach a Save that would do nothing. */
.pics__orderbar--top.is-idle { visibility: hidden; }
.pics__orderbar--bottom.is-idle { display: none; }

/* aspect-ratio holds the slot open before the image lands, so a gallery on a
   slow link does not reflow line by line as each photograph arrives. object-fit
   contain rather than cover because the stored images are ALREADY letterboxed
   onto a 4:3 canvas by the legacy's ResizeImage -- cropping them again would
   cut the vehicle. */
.pics__tile img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
    object-fit: contain;
    background: var(--bg);
}

.pics__tile figcaption {
    display: flex;
    align-items: center;
    gap: .3rem;
    flex-wrap: wrap;
    padding: .3rem .4rem;
    border-top: 1px solid var(--border);
    font-size: var(--fs-small);
}

/* The position number, deliberately prominent: it is the one thing on the tile
   a stock controller acts on, because it is what a re-order changes. */
.pics__pos {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.4rem;
    height: 1.4rem;
    border-radius: var(--radius-pill);
    background: var(--brand);
    color: #fff;
    font-weight: 600;
    font-size: .72rem;
}

/* The photograph is the way into the editor layer (Tim, 29 September 2026: not a
   new window), so its link is a block -- an inline <a> round a block image
   leaves a strip of dead space under it that is not part of the click. */
.pics__open { display: block; }

/* The one Edit button sits in the caption beside the tags, pushed to the far end
   so it reads as the tile's action rather than another label. margin-left:auto
   works because the caption is a flex row. It replaced separate Crop and
   Background buttons when the two tools were combined into one layer. */
.pics__edit { margin-left: auto; }

/* ---------------------------------------------------------------------------
   THE PHOTOGRAPH EDITOR -- one layer for Crop and Background
   --------------------------------------------------------------------------- */

/* The last save's outcome, carried into the layer when it reopens. .alert sets
   display:flex, which beats the hidden attribute, so hidden is restated here --
   one class plus an attribute outranks .alert's one class. */
.photoedit__note { margin: 0 0 var(--s2); }
.photoedit__note[hidden] { display: none; }

/* A photograph that cannot be edited, shown large instead of in a new window.
   Sized like the background result: never enlarged past its own pixels, and
   capped to the dialog's 92vh less its chrome. */
.photoedit__view {
    display: block;
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: calc(92vh - 13rem);
    margin: 0 auto var(--s2);
}

/* ---------------------------------------------------------------------------
   REPLACE BACKGROUND -- one result card per engine (Tim's side-by-side
   evaluation of Gemini and OpenAI, 29 September 2026). Side by side where there
   is room, stacked on a phone; the base .eq-dialog size is left alone.
   --------------------------------------------------------------------------- */
.bgcmp__source img {
    display: block;
    max-width: 100%;
    max-height: 28vh;
    margin: 0 auto;
}

.bgcmp__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    margin: var(--s3) 0;
}

.bgcmp__results {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
    gap: var(--s3);
}

.bgcmp__card {
    margin: 0;
    padding: var(--s2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
}

.bgcmp__error { color: var(--bad); }

/* ---------------------------------------------------------------------------
   PLACING THE VEHICLE -- Tim, 29 September 2026: "allow anchors on the image
   that has the background removed so that a user can extend or shrink before
   saving it on top of the background image."
   The stage IS the frame the photograph is saved at -- 4:3, 1024x768 or 640x480
   for a small source -- with the showroom and the vehicle as two pictures inside
   it and the anchors drawn over the vehicle. PlaceEngine (crop-engine.js) places
   them in PERCENTAGES, so the stage may be any size and nothing here has to
   agree with a pixel figure in the script.
   --------------------------------------------------------------------------- */

/* Room for the anchors. Each corner's touch pad reaches 22px past the vehicle
   (the 44px floor of rule 5.4e); 14px here plus the card's own padding and
   border keeps it inside the card -- so a vehicle at the frame's edge neither
   loses its corner nor makes the dialog scroll sideways. */
.place { padding: 14px; }

/* THE PICTURE FITS THE WINDOW -- Tim, 29 September 2026: "make the edited image
   fit the viewable screen". The frame is 4:3, so its HEIGHT is what is capped,
   through its width: the dialog's own 92vh (the .eq-dialog rule -- change both
   together) less the chrome above and below the frame -- the title bar, the Crop
   / Background tabs, the body's padding, this card's caption, the Showroom /
   Plain colour switch, the anchors' room, the size row and the Use this one
   button. Every step of that figure was measured on a 1366x768 window rather
   than guessed: 10rem for the result picture alone; 13rem once the tool tabs
   arrived (21px over); 15.5rem with the backdrop switch (12px over); and more
   again with the anchors' room and the size row (VehicleBackgroundReplacementTests
   holds the current figure). The width never passes the card's, and the frame
   is never ENLARGED past what the cap allows -- a 1024x768 result drawn at the
   card's full width used to come out 1184x888, blurred. */
.place__stage {
    position: relative;
    width: 100%;
    max-width: calc((92vh - 20.5rem) * 4 / 3);
    aspect-ratio: 4 / 3;
    margin: 0 auto;
}

/* The frame CLIPS, because the photograph is the frame: anything outside it is
   not in the picture. clip, not hidden -- a hidden box can still be scrolled by
   focus or scrollIntoView, which would slide the showroom out from under the
   anchors. PlaceEngine keeps the whole vehicle inside anyway. */
.place__frame {
    position: absolute;
    inset: 0;
    overflow: clip;
    background: #fff;       /* replaced by the dealership's own colour, from the preview */
}

.place__frame > img {
    position: absolute;
    display: block;
    max-width: none;
    max-height: none;
    margin: 0;
    user-select: none;
    -webkit-user-drag: none;
    pointer-events: none;   /* the anchors' box above takes every pointer */
}

.place__bg { inset: 0; width: 100%; height: 100%; }

/* The picture's display: block above outranks the hidden attribute, which comes
   from the browser's own stylesheet -- so Plain colour would leave the showroom
   showing. Restated here, at a higher specificity. */
.place__bg[hidden] { display: none; }

/* The anchors' box sits exactly over the vehicle: a dashed white line with a thin
   dark edge either side, so it reads on a dark showroom and on a white plain
   colour alike. NOT the crop box's dimmed surround -- that would darken the very
   showroom the vehicle is being judged against. */
.place__box {
    position: absolute;
    box-sizing: border-box;
    border: 2px dashed #fff;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, .55), inset 0 0 0 1px rgba(0, 0, 0, .55);
    cursor: move;
    touch-action: none;     /* a drag on the vehicle must not scroll the dialog */
}

.place__box:focus-visible {
    outline: 3px solid var(--brand);
    outline-offset: 2px;
}

/* The hint, the size readout and Reset, on one line under the stage. */
.place__bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2);
    margin: 0 0 var(--s2);
}

.place__size {
    margin-left: auto;
    font-variant-numeric: tabular-nums;
}

/* What sits behind the car -- Showroom or Plain colour -- switched in the browser
   between previews the server already made from the one engine call. The pressed
   one wears the house tab's active colours (.eq-tab--on), so "which one is showing"
   reads the same way it does on every tab strip. */
.bgcmp__backdrops {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1);
    margin-top: var(--s2);
}

.bgcmp__backdrops .btn[aria-pressed="true"] {
    background: var(--eq-tab-on);
    border-color: var(--eq-tab-line-on);
    color: var(--eq-tab-on-ink-solid);
}

/* ---------------------------------------------------------------------------
   ADD PHOTOGRAPHS
   ---------------------------------------------------------------------------
   The drop zone is a LABEL wrapping the file input, not a div with drag
   handlers. That is what makes it reachable by keyboard and announced by a
   screen reader without any extra attributes -- clicking the label opens the
   picker because the input is its control. The drag events are an addition on
   top; with them broken or unsupported, the label still works.
   --------------------------------------------------------------------------- */
.pics__add { margin: 0 0 var(--s4); }

.pics__drop {
    display: flex;
    flex-direction: column;
    gap: .25rem;
    align-items: center;
    padding: var(--s4);
    border: 2px dashed var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
    cursor: pointer;
    text-align: center;
    transition: border-color .15s, background-color .15s;
}

.pics__drop:hover,
.pics__drop:focus-within { border-color: var(--brand); }

/* Added while a drag is over the zone. A colour change rather than a size or
   border-width change, so nothing reflows under the cursor mid-drag. */
.pics__drop.is-over {
    border-color: var(--brand);
    background: var(--brand-tint);
}

/* The input is present and focusable, just not drawn -- the label is the
   affordance. NOT display:none, which would take it out of the tab order and
   break the keyboard path this shape exists to preserve. */
.pics__drop input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    overflow: hidden;
}

.pics__addactions {
    display: flex;
    align-items: center;
    gap: var(--s3);
    justify-content: flex-end;
    margin-top: var(--s2);
}

/* Download processed images (Tim, 6 October 2026): centred in the gap between Upload and the
   gallery, where he marked it, with its count beside it. Wraps rather than hides on a phone
   (rule 5.4e). */
.pics__download {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--s3);
    margin: var(--s3) 0;
}

@media (prefers-reduced-motion: reduce) {
    .pics__drop { transition: none; }
}

/* ---------------------------------------------------------------------------
   CROP DIALOG
   ---------------------------------------------------------------------------
   The stage is sized in PIXELS BY THE ENGINE, not by CSS, because the box has to
   be positioned in the same coordinate space as the image it sits over. So
   nothing here sets a width -- doing so would make the box and the photograph
   disagree, which is exactly the class of bug that produces a crop of the wrong
   area.

   The engine measures the stage AFTER the dialog is shown. A dialog that has not
   been opened has zero dimensions, so measuring first would size the box to
   nothing.
   --------------------------------------------------------------------------- */
.crop-stage {
    position: relative;
    margin: .5rem auto;
    background: var(--bg);
    line-height: 0;              /* or the inline image leaves a gap under it */
    touch-action: none;          /* pointer drags must not scroll the dialog */
}

.crop-stage img { display: block; }

/* The box is a hole in a dimmed surround rather than an outline, which is how a
   crop tool is expected to read: a huge spread shadow darkens everything outside
   it without needing four extra elements. */
.crop-box {
    position: absolute;
    box-sizing: border-box;
    border: 2px solid #fff;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, .45);
    cursor: move;
}

/* 44px hit areas on touch, per rule 5.4e's floor, achieved with a transparent
   pad rather than a visibly huge handle: the pad is the target, the coloured
   square is the affordance. The Background tool's anchors (.place__h) are the
   same handles, so a corner looks and behaves the same in both tools. */
.crop-box__h,
.place__h {
    position: absolute;
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    touch-action: none;
}

.crop-box__h::after,
.place__h::after {
    content: "";
    width: 12px;
    height: 12px;
    background: #fff;
    border: 1px solid var(--brand);
}

/* The dialog's button row. Its own class rather than .eq-actions, which looks
   like it would fit by name and is a NUMBERED LIST in a pale blue box -- the
   home-page action list. Modelled on .otpform__actions, which is the flex row
   this actually wanted. */
.crop-actions {
    display: flex;
    align-items: center;
    gap: var(--s3);
    flex-wrap: wrap;
    margin-top: var(--s3);
}

.crop-box__h--nw, .place__h--nw { top: -22px; left: -22px; cursor: nwse-resize; }
.crop-box__h--ne, .place__h--ne { top: -22px; right: -22px; cursor: nesw-resize; }
.crop-box__h--sw, .place__h--sw { bottom: -22px; left: -22px; cursor: nesw-resize; }
.crop-box__h--se, .place__h--se { bottom: -22px; right: -22px; cursor: nwse-resize; }

/* A record with no reachable file still occupies a tile, at the same height as
   a real one, so the gallery does not silently close up around a missing
   photograph. */
.pics__missing {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .35rem;
    aspect-ratio: 4 / 3;
    padding: .5rem;
    text-align: center;
    background: var(--bg);
    color: var(--muted);
    font-size: var(--fs-small);
}

/* Customer-entered free text: preserve the rep's line breaks, and keep a reading
   measure so a long note does not run the full width of a 2560px monitor. */
.qd__comments { white-space: pre-wrap; max-width: 68ch; }

.note--timing { color: var(--muted); font-size: var(--fs-small); }

/* .grid-scroll IS DECLARED ONCE, in the grid section above. A second copy lived
   here and would have silently overridden the sticky-header fix -- the same
   defect as the legacy's BaseStyle.css being linked twice from Main.Master. */

@media print {
    .qd__state, .btn-row, .note--timing { display: none !important; }
}

/* ---------------------------------------------------------------------------
   "Remember my details" on the sign-in form.
   --------------------------------------------------------------------------- */
.field--check { margin-top: var(--s2); }

.check {
    display: flex; align-items: flex-start; gap: .5rem;
    cursor: pointer; font-weight: 400;
}
/* The box must not shrink when the label wraps, and it should sit on the first
   line of text rather than centred against a two-line label. */
.check input[type="checkbox"] { flex: 0 0 auto; margin-top: .2rem; }

/* The pre-filled notice. Deliberately quiet -- it is information, not a warning,
   and a shared machine is the only case where it matters. */
.note--filled {
    margin: 0 0 var(--s3);
    padding: .4rem .6rem;
    background: var(--brand-tint); color: var(--brand-deep);
    border-radius: var(--radius);
    font-size: var(--fs-small);
}

/* ---------------------------------------------------------------------------
   THE VEHICLE CASCADE (Sales/NewVehicle) -- step 2 of the quote wizard.

   Reproduces the feel of the legacy's three-panel accordion
   (EQnet/Quote.aspx:1214) without its mechanics: a chosen level collapses to a
   summary line with a "change" link, so the bytes for a list are spent once
   rather than on every subsequent postback.
   --------------------------------------------------------------------------- */

.cascade { margin: 0 0 var(--s4); }

.cascade__steps {
    display: flex; flex-wrap: wrap; gap: var(--s2);
    margin: 0; padding: 0; list-style: none;
}

.cascade__steps > li {
    display: flex; align-items: baseline; gap: .4rem;
    padding: var(--s2) var(--s3);
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--muted);
    font-size: var(--fs-small);
}

/* The step being chosen now carries the brand colour; completed steps stay quiet
   so the eye lands on the one that needs attention. */
.cascade__steps > li.is-current {
    border-color: var(--brand); background: var(--brand-tint); color: var(--ink);
}
.cascade__steps > li.is-done { color: var(--ink-soft); }

.cascade__label {
    font-size: var(--fs-eyebrow); text-transform: uppercase;
    letter-spacing: .04em; color: var(--muted);
}
.cascade__todo { font-style: italic; }

/* A list of choices that behaves like a list of buttons. Deliberately real
   anchors, not JS-driven rows: the cascade must work with scripting off, and each
   level is a genuine URL. */
.picklist {
    display: grid; gap: var(--s2);
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    margin: 0; padding: 0; list-style: none;
}

.picklist__item {
    display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
    padding: var(--s3);
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--ink);
    text-decoration: none; font-weight: 600;
}

.picklist__item:hover,
.picklist__item:focus-visible {
    border-color: var(--brand); background: var(--brand-tint);
}

/* "No stock" is a dash, never a red badge: zero stock is an ordinary state, not an
   error. The legacy hides the link entirely when the count is zero
   (Quote.aspx.vb:5088), so there is nothing for a rep to act on -- but the cell
   still needs to read as deliberately empty rather than as missing data. */
.muted-dash { color: var(--muted); }

/* ---------------------------------------------------------------------------
   CLIENT SEARCH -- the selected result row.

   A picked client stays visible in the list rather than replacing it, because the
   rep is usually comparing two candidates to decide whether they are the same
   person. That comparison is the whole purpose of the screen, so the selection has
   to read as "this one of these" rather than "this one, list gone".
   --------------------------------------------------------------------------- */

/* SUPERSEDED by the global .is-selected rule in section 16, which uses the
   legacy's own #a0dbf9 / navy / bold. Left as a comment rather than deleted so
   nobody re-adds a second, softer selection style and wonders why selection
   looks different on one screen. */

/* ===========================================================================
   15  THE LEGACY SKIN

   Components that reproduce the look of the legacy screens, using the palette
   tokens declared in section 1. Added 25 August 2026 on Tim's instruction to
   match the styling and flow of the original system.

   Each class names the legacy rule it comes from, so a reader can check the
   original rather than trust this comment.
   =========================================================================== */

/* The grey ground the page slab floats on (BaseStyle body: #b6b7bc plus
   Grey_Art_Wallpaper.jpg, background-attachment: fixed).

   The WALLPAPER IS DELIBERATELY NOT REPRODUCED, and this is the one place the
   legacy look is knowingly dropped. That image is 228,983 bytes -- the largest
   always-loaded asset in the application, on every page -- and `fixed` forces a
   full repaint on every scroll, which is precisely the cost these users cannot
   afford on a weak CPU. A flat tone gives the same framing for zero bytes.
   If the texture is ever wanted back, it belongs here as a small tiling SVG,
   not a 1920x1440 JPEG. */
body.eq-skin { background: var(--eq-ground); }

/* The page slab: light sheet, slate hairline, sitting in the grey.
   (BaseStyle .page -- minus its fixed 1340px width, see the note at .eq-column.) */
.eq-slab {
    background: var(--eq-page);
    border: 1px solid var(--eq-page-line);
    color: var(--eq-body-ink);
    margin-block: var(--s5) 0;
}

/* HEADINGS. Small-caps at a LIGHT weight, grey, and the page title underlined
   and centred. This is the most recognisable thing about the legacy screens and
   the easiest to lose: font-weight 200 on a heading is unusual enough that it
   looks like a mistake to anyone tidying up.
   (BaseStyle .Head1 16pt/200, .Head2 14pt/bold/centred/underlined; GM .Head3 12pt/200) */
.eq-h1, .eq-h2, .eq-h3 {
    color: var(--eq-heading);
    font-variant: small-caps;
    text-transform: none;
    margin: 0;
}
.eq-h1 { font-size: 1.45rem; font-weight: 200; }
.eq-h2 {
    font-size: 1.25rem; font-weight: 700;
    text-align: center; text-decoration: underline;
}
.eq-h3 {
    font-size: 1.0625rem; font-weight: 200;
    text-align: center; text-decoration: underline;
}

/* Links inside the legacy skin: the saturated blue, underlined. The rest of the
   application uses slate; here the blue is the point. */
/* SCOPED TO THE PAGE BODY, NOT THE WHOLE SKIN -- and this is a bug fix, not a
   preference. The first version of this rule was `.eq-skin a`, which is a body
   class, so it also caught the dealership title and every navigation tab: both
   are anchors, and (0,1,1) beats .brand's (0,1,0) and ties .nav a while being
   declared later. The result was #034af3 text on a #4b6c9e bar and on #465c71
   tabs -- legible in neither.

   .eq-slab is the <main> element, so the legacy blue now applies to content
   links only and the chrome keeps its own colours. Do not widen this selector
   back to .eq-skin. */
/* A BUTTON IS NOT A LINK, and excluding it here is a bug fix rather than a
   refinement -- found 15 September 2026 when Tim looked at Change
   Username/Password and asked about the styling.

   .eq-slab is <main>, so this rule matched EVERY content link in the
   application, including the ones styled as buttons: at (0,1,1) it beats
   .btn--primary's (0,1,0) and is declared later, so the colour that lost was
   the button's. Measured from the browser rather than guessed --
   CSS.getMatchedStylesForNode named this rule -- and measured across the tree
   afterwards: 77 anchors carry .btn across 37 files, so every one of them has
   been painting #034af3 on its own pill. The Excel exports were blue text on
   green.

   :not(.btn) says the thing that is true: an ordinary link inside the legacy
   skin is the saturated blue, and a control that has been given a button's
   shape keeps the button's colours. */
/* .eq-x IS EXCLUDED FOR THE SAME REASON .btn IS, and this is the rule that was
   repainting every close cross in the application.

   Tim, 16 September 2026, on the appointment booking layer: "x to close layer
   styling issue" -- the cross was rendering as a blue glyph on its pale plate
   instead of white on translucent white.

   THE CAUSE, and it is worth stating because the stylesheet had already given up
   on finding it twice: _Layout wraps every page in
   <main class="app-surface eq-slab">, so this selector at (0,2,1) reaches inside
   every dialog and outranks `a.eq-x` at (0,1,1). The colour a.eq-x declares was
   correct and simply never won. Both earlier attempts read the .eq-x rule and the
   markup, found each correct, and concluded the computed value was inexplicable.
   It was explicable; the winning rule was just somewhere else.

   Excluding rather than raising .eq-x's specificity, because .btn is already
   excluded here for exactly this reason: a control that paints its own colour
   must not be reached by the ambient link colour. */
/* .alertnum IS THE THIRD, AND IT IS THE SAME RULE AMBUSHING A THIRD VICTIM.
   Tim, 21 September 2026, on the Dealership Overview: "if a number exceeds the
   recommended value ... instead of bold blue, it should be Red" -- "bold red".

   HE WAS READING IT CORRECTLY AND THE SCREEN WAS LYING. The bold IS the
   threshold marker: .alertnum's font-weight:700 was arriving, because nothing
   else sets font-weight on those anchors. Only the COLOUR was being taken, so
   a breached threshold rendered bold BLUE -- indistinguishable from an ordinary
   drill-down link, which is the one thing this signal exists to be distinct
   from.

   THIS IS THE THIRD FIX FOR THIS COLOUR AND THE FIRST ONE MEASURED. The two
   before it raised .alertnum's specificity -- first to beat `table.grid td a`,
   then to beat `table.grid--numeric td a { color: inherit }` -- and both read
   the stylesheet to decide they had won. Neither looked here. This selector is
   (0,3,1), because :not() carries its argument's specificity, so it beats even
   `table.grid--numeric td a.alertnum` at (0,2,3) on class count. Chrome's
   CSS.getMatchedStylesForNode named it in one call; three readings of the file
   had not.

   So: exclude, exactly as .btn and .eq-x are excluded, for the reason already
   given above. Do NOT answer this by raising the alert's specificity again --
   that is what failed twice, and the next broad rule would simply win instead.
   tools/driveoverviewalert.py MEASURES the computed colour in a real browser,
   because reading this file is what went wrong all three times. */
/* ===========================================================================
   :where() ROUND THE EXCLUSIONS, AND THIS IS THE ONE THAT STOPS THE PATTERN.
   Added 21 September 2026, after the FOURTH victim.
   ===========================================================================
   Tim, on the quote wizard: "if you have a look at the tabs it's got a dark
   background with a dark blue text which is very difficult to read ... if it's
   a dark background the text should be white."

   THE TABS WERE ALREADY FIXED ONCE, on 25 August 2026, and the fix was correct
   on the day: `a.qstep` is (0,1,1), `.eq-slab a` was (0,1,1), and a tie goes to
   whichever is declared later -- which is a.qstep. That note is still in the
   .qstep block and still says so.

   THEN THE EXCLUSION LIST GREW, and every entry in a plain :not() ADDS a class
   to the specificity:

       .eq-slab a                                    (0,1,1)  Aug -- tie, qstep won
       .eq-slab a:not(.btn)                          (0,2,1)  15 Sep -- qstep lost here
       .eq-slab a:not(.btn):not(.eq-x)               (0,3,1)  16 Sep
       .eq-slab a:not(.btn):not(.eq-x):not(.alertnum) (0,4,1) 21 Sep

   So fixing the buttons silently broke the tabs, five weeks before anybody
   looked at them, and each later fix pushed them further out of reach. Nothing
   failed, nothing logged: the tab rendered, in the wrong colour.

   :where() CONTRIBUTES ZERO SPECIFICITY, so :not(:where(...)) excludes without
   counting. The selector is back to (0,1,1) -- its August value -- a.qstep wins
   the tie again, and a FIFTH exclusion added here can never move it.

   ADD EXCLUSIONS INSIDE THE :where(), never as another :not(). That is the
   whole point of the construct, and a plain :not() appended here restarts the
   ratchet. CLAUDE.md 5.4n and QuoteStepColourTests carry the rule.

   The components that paint their own colour still opt out, and the alert red
   also wins on its own specificity, so it is covered twice. */
.eq-slab a:not(:where(.btn, .eq-x, .alertnum)) { color: var(--eq-link); }
.eq-slab a:not(:where(.btn, .eq-x, .alertnum)):hover { color: var(--eq-link-hover); }

/* A white rounded panel (BaseStyle fieldset: white, 1em padding, 8px radius).
   The legacy sets these at fixed widths inline -- 500px for the Prospecting and
   Stocklist boxes -- so the modifier carries that rather than an inline style. */
/* PADDING TIGHTENED 26 August 2026 (was s4 s5 -- 16px/24px). The legacy's own
   fieldset runs 1em all round on a 12.8px root, which is under 13px; 12/16 here
   is still more generous than the original and saves 8px of height on every
   panel on a screen that carries four of them. */
/* ---------------------------------------------------------------------------
   THE CARD IS PALE BLUE, NOT WHITE.

   Tim, 21 September 2026, on the vehicle editor: "It's too white on white on
   white. It's a very light gray background. Then the card, which is white,
   then text boxes inside is white. I think make the card background a very
   light blue. Match one of the other light blues within the system."

   Three surfaces were within a few percent of each other -- page #f6f6f6,
   card #fff, inputs #fff -- so nothing separated a group of fields from the
   page or from the boxes inside it.

   --eq-formbox IS the light blue he means, and it is not a new invention:
   it is the LEGACY's own colour for exactly this element. GM.css .SectionsDiv,
   "the box a group of fields sits in", is background #e0eefb with a #99bbe8
   border, and the token pair has been sitting in this stylesheet unused by the
   card since the palette was ported. The same blue bands every list in the
   application, so it is already the colour a rep reads as "grouped".

   The BORDER moves with it, because half the legacy box is worse than either:
   a pale blue panel inside a grey hairline reads as unfinished.

   SETTLED, 21 September 2026, after two wrong guesses at WHICH surface. The
   rule Tim actually wants is one sentence and it is worth quoting in full:

     "literally everything there is the same color blue, except the text boxes
      or the date selectors or the drop down. So basically anything that
      captures data. Everything else I'd like to see it looking blue."

   So the panel, the label column and the value column are ONE blue sheet, and
   the only white left on the screen is a control a rep can type into. White
   stops being decoration and starts meaning "you can edit this", which is a
   better signal than any of the three layouts that came before it.

   The two wrong guesses are kept here because each was a reasonable reading
   and neither was right: first the CARD alone went blue, which left caption
   and control white against each other; then the LABEL COLUMN alone, which
   left the value column white. The instruction was never "pick a surface" --
   it was "stop having more than one".
   --------------------------------------------------------------------------- */
/* A DATA GRID INSIDE A CARD KEEPS ITS WHITE TRAY AND ITS BANDING. DELIBERATE.
   Tim, 21 September 2026, shown the Recon tab and asked directly: "i am happy
   on this page with loose end 1 also."

   It looks like the one thing the blue sheet missed, and it is not. .grid-wrap
   stays var(--surface) and table.grid keeps its blue/white rows, because that
   banding is rule 5.4f -- a system-wide standard Tim gave for every list, so
   the reading aid across a wide row wins over the uniform surface on this one
   screen. Note band 1 is the SAME blue as this card, so merging them would
   leave the WHITE rows reading as the stripes: the inverse of the rule, not a
   tidier version of it.

   Do not "finish the job" by flattening the wrapper. It was offered, looked at
   and declined. */
.eq-card {
    background: var(--eq-formbox);
    border: 1px solid var(--eq-formbox-line);
    border-radius: 8px;
    padding: var(--s3) var(--s4);
    margin-inline: auto;
    /* The lift -- design brief 1a.21. Tim, 26 August 2026: "one of the graphical
       design features i like is where there is a Shodow effect on the elements to
       make them look as if they are floating off the page." */
    box-shadow: var(--eq-lift);
}
.eq-card--sm { width: min(100%, 500px); }
.eq-card--md { width: min(100%, 760px); }

/* The pale blue box a group of fields sits in (GM.css .SectionsDiv).
   Labels above their inputs, as the legacy has them. */
/* ===========================================================================
   THE PITCH IN HERE IS DELIBERATELY TIGHT. Measured, 26 August 2026.

   The first version of this box ran a 66px pitch per field against the legacy's
   37px, which made the search card 168px taller than the original -- 521px
   against 353px. On the legacy home page that difference is the difference
   between fitting the Prospecting and Stocklist panels in the viewport and
   pushing them below the fold, which is the one thing Tim asked to protect:
   "we want to try keep everything in view for the user without them needing to
   scroll".

   So: label at small size on a tight line, input one line high, and a 5px gap
   before the next label. Roughly 47px a field. Loosening any of the four values
   below costs about 20px per field and 80px on the card -- check the whole page
   height, not just this box, before doing it.
   =========================================================================== */
.eq-formbox {
    background: var(--eq-formbox);
    border: 1px solid var(--eq-formbox-line);
    color: #000;
    padding: var(--s2) var(--s3) var(--s3);
    margin-block: var(--s3);
}
.eq-formbox label {
    display: block;
    font-size: var(--fs-small);
    line-height: 1.25;
    margin-bottom: .05rem;
}
.eq-formbox input[type="text"],
.eq-formbox input[type="search"],
.eq-formbox input[type="tel"] {
    width: 100%;
    border: 1px solid var(--eq-titlebar);   /* BaseStyle input[type=text] */
    border-radius: 0;                        /* square, as the legacy is   */
    padding: .1rem .3rem;
    font: inherit;
    line-height: 1.3;
    background: #fff;
    margin-bottom: .3rem;
}
.eq-formbox > :last-child { margin-bottom: 0; }

/* THE FIELD THAT IS CURRENTLY THE SEARCH, outlined heavier.

   Only one of these four boxes can hold a search term at a time -- typing in
   one clears the others, which is the legacy's ClearSearchFields behaviour (see
   app.js section 10). Without a mark, a rep who types a surname and then starts
   a phone number sees their surname vanish with nothing saying which box the
   search will now use. The outline answers that at a glance.

   Set by script, so with script off nothing is marked and nothing is cleared --
   the box simply behaves as four ordinary fields, which is the honest
   no-script state. */
.eq-formbox input.is-thesearch { border: 2px solid var(--ink); }

/* THE "EXT" GRID SKIN (GM.css .ExtGridHeaderStyle / .ExtGridRowStyle /
   .ExtGridAltRowStyle): pale blue header, banded rows, every cell hairlined.

   Kept as a SEPARATE class from table.grid rather than restyling that one. The
   two are used for different things: table.grid is the modern data grid on Open
   Quotes and the stock list, where sorting, paging and row-click all live and
   where a lighter treatment reads better at 150 rows. .eq-table is the small
   fixed summary table the legacy uses for counts. Merging them would force one
   compromise on both. */
.eq-table {
    border-collapse: collapse;
    background: #fff;
    margin-inline: auto;
    font-size: var(--fs-body);
}
.eq-table th {
    background: var(--eq-grid-head);
    color: var(--eq-grid-head-ink);
    border: 1px solid var(--eq-grid-line);
    padding: .3rem .5rem;
    text-align: center;
    font-weight: 700;
    line-height: 1.25;
}
.eq-table td {
    border: 1px solid var(--eq-grid-line);
    color: var(--eq-grid-ink);
    padding: .25rem .5rem;
    text-align: center;
}
/* Banding is on the ROW, first band tinted -- matching RowStyle/AlternatingRowStyle. */
.eq-table tbody tr:nth-child(odd)  td { background: var(--eq-grid-row); }
.eq-table tbody tr:nth-child(even) td { background: #fff; }
/* The row label cell (New / Used) reads as a header, not as data. */
.eq-table td.eq-table__rowhead { font-weight: 700; color: var(--eq-grid-head-ink); }

/* A count of zero is NOT a link in the legacy -- there is nothing to open. The
   number still shows, so "0 overdue" and "not measured" stay distinguishable. */
.eq-table .eq-zero { color: var(--muted); }

/* A column whose figure this app does not compute yet. Deliberately a dash and
   NOT a zero: on the screenshot Tim sent, his own row shows 8 overdue reminders,
   so rendering 0 would be a false statement a rep would act on. The dash says
   "not measured here", which is true. */
.eq-table .eq-nodata { color: var(--muted); cursor: help; }

/* ---------------------------------------------------------------------------
   .eq-table AS A DATA GRID (the extras and trade-in tables on a quote).

   Same ExtGrid skin, three differences from the counts table it was written
   for: full width, text left-aligned, and figures right-aligned in a tabular
   figure. The legacy's own ExtGridRowStyle td sets only padding-left and a
   border, so LEFT is the original's alignment -- the centring on .eq-table is
   this app's addition for the small fixed counts grid, where every cell is a
   number.

   A modifier rather than a second table class, so a change to the legacy grid
   look lands on both. table.grid stays where it is: that is the sortable,
   pageable, row-clickable grid on Open Quotes and the stock lists, and it earns
   a lighter treatment at 150 rows.
   --------------------------------------------------------------------------- */
.eq-table--data { width: 100%; }
.eq-table--data th,
.eq-table--data td { text-align: left; }
.eq-table--data th.num,
.eq-table--data td.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
    /* A money column needs about eleven characters ("R1,353,391.30"), not a third
       of the screen. Without this the three-column accessories grid split evenly
       and put "R4,500.00" in the middle of 400px of white. The first column has no
       width, so it absorbs whatever is left -- which is right, because the
       accessory description is the only cell that can be long. */
    width: 9rem;
}

/* The footer carries the total. Same cell treatment as a body row, tinted like
   the header so it reads as a summary rather than as one more line item. */
.eq-table tfoot td {
    border: 1px solid var(--eq-grid-line);
    padding: .25rem .5rem;
    background: var(--eq-grid-row);
    color: var(--eq-grid-head-ink);
}

/* ---------------------------------------------------------------------------
   .eq-detail -- THE LEGACY'S SETTINGS FORM: a label column and a value column
   ---------------------------------------------------------------------------
   Settings > Dealership Details, and every accordion pane after it, is one long
   two-column table: a plain caption ending in " :" on the left, the control on
   the right, one thin border round the whole table and no lines between cells
   (DealershipPref.aspx:821, cellpadding=3 cellspacing=0, border 1pt #eaeaea).

   A SEPARATE CLASS FROM .eq-table, WHICH LOOKS CLOSE AND IS WRONG HERE. That one
   is a small SUMMARY grid: it centres every cell and paints th with the blue grid
   header. Used for a form it renders each caption as a centred blue header cell,
   which is what section 1 shipped with before Tim asked for the legacy styling.
   The captions here are labels, not column headings.

   THE LABEL COLUMN IS FIXED AND THE VALUE COLUMN IS NOT STRETCHED. Rule 5.5 caps
   the page at 2200px for exactly the reason that applies to every row in here --
   "a key-and-value row stretched to 2355px separates the label from its number
   far enough that the eye loses the row". The inputs keep the legacy's own widths
   instead of filling the cell, so the pair stays together on a wide monitor while
   the pane itself still uses the screen.
   --------------------------------------------------------------------------- */
.eq-detail {
    width: 100%;
    border-collapse: collapse;
    border: 1px solid var(--eq-grid-line);

    /* THE LEGACY'S PANEL BLUE, NOT WHITE. Tim, 11 September 2026, pointing at
       the empty space beside a field on Dealership Details: "make the white
       where my mouse pointer is a light opaque blue."

       The same --eq-fieldset he approved on the staff editors, which is
       .TABCOVER_0 read out of EQNetAdmin/StaffAdministration.aspx:159. One blue
       for every label-and-value form in Settings rather than a second shade.

       WHY THIS IS SAFE TO CHANGE ON THE BASE RULE, measured rather than
       assumed: .eq-detail appears in nine files and all nine are Settings --
       Dealership Details with its partials, and Staff Administration. No Sales
       screen, no quote screen, nothing outside the area being restyled.

       The inputs keep their own white, which is what gives the row its shape --
       white boxes on a tinted table, exactly as the legacy's panels read. And
       .eq-detail--ruled sets `background: none` so the staff cards still show
       their own card blue through; this changes nothing there. */
    background: var(--eq-fieldset);
    font-size: var(--fs-body);
    color: var(--eq-body-ink);
    margin: 0 0 var(--s3);
}

/* th, not td, because a caption that names the row IS its header -- it is what a
   screen reader announces with the field. Styled back to plain text: no fill, no
   border, left aligned, normal weight.

   THE OLD REASON GIVEN HERE WAS "as the legacy's <td> renders", AND IT WAS
   WRONG. Measured 11 September 2026: every cell on the legacy's staff panels
   carries `style="border:solid 1px lightgray"` (StaffAdministration.aspx:981
   and throughout), so the legacy is RULED and this is not. The borderless look
   is ours, it is what made the staff editors "white on white on white", and
   .eq-detail--ruled below is the opt-in that puts the legacy's gridlines back.
   Left borderless by DEFAULT because the screens already using .eq-detail were
   built and reviewed against it. */
.eq-detail > tbody > tr > th {
    width: 17rem;
    padding: .25rem .5rem;
    background: none;
    border: 0;
    text-align: left;
    font-weight: 400;
    vertical-align: top;
    line-height: 1.9;      /* sits level with the input beside it */
}

.eq-detail > tbody > tr > td {
    padding: .25rem .5rem;
    border: 0;
    text-align: left;
    vertical-align: top;
}

/* No banding. The legacy's form has none, and stripes across a column of text
   boxes read as a data grid -- which invites a click on the row (rule 5.4f) that
   would do nothing. */
.eq-detail > tbody > tr > td,
.eq-detail > tbody > tr:nth-child(odd) > td { background: none; }

/* The legacy's own box widths: 200px for most, 350px for an address, 100px for a
   telephone number. Reproduced because a screen of full-width boxes reads as a
   different application, and because a 2200px-wide text box for a VAT number is
   worse than a 200px one. max-width keeps them inside a phone. */
.eq-detail input[type="text"],
.eq-detail input[type="email"],
.eq-detail input[type="tel"],
.eq-detail select {
    width: 200px;
    max-width: 100%;
}
.eq-detail input.is-wide   { width: 350px; }
.eq-detail input.is-narrow { width: 100px; }

/* A value the screen shows and does not let anyone change -- the branch code.
   Bold, as the legacy's Label is (DealershipPref.aspx:828, Font-Bold="true"). */
.eq-detail .eq-detail__fixed { font-weight: 700; }

/* ---------------------------------------------------------------------------
   RULED -- the legacy's own gridlines, opted into

   Tim, 11 September 2026, on the staff editors: "white on white on white on
   white." A borderless white table on a white panel gives the eye nothing to
   follow across a row and nothing to separate one field from the next, which on
   a form of thirty tick boxes is the difference between readable and not.

   THE CELLS KEEP NO FILL OF THEIR OWN, and that is deliberate rather than an
   omission: on the legacy the <td> has a border and no background, so it shows
   the card's blue through and the whole table reads as one tinted block ruled
   into rows. Giving the cells white here would put the white back and undo the
   point.

   A MODIFIER, NOT THE DEFAULT. Every other screen using .eq-detail was built
   and reviewed borderless; changing all of them from a staff-screen complaint
   would be a change nobody asked for on screens nobody mentioned.
   --------------------------------------------------------------------------- */
.eq-detail--ruled {
    background: none;
    border-color: var(--eq-rule);
}
.eq-detail--ruled > tbody > tr > th,
.eq-detail--ruled > tbody > tr > td {
    border: 1px solid var(--eq-rule);
}

/* ---------------------------------------------------------------------------
   THE SECTION CARD -- the legacy's .TABCOVER_0, reproduced

   StaffAdministration.aspx:159-166, verbatim in intent: a rounded, bordered,
   light blue block with its own centred heading, grouping one set of rights.

   WIDTH IS NOT REPRODUCED. The legacy pins it to 560px because its modal is
   602px; here the layer is responsive, so the card fills the width it is given
   and rule 5.5 is not broken by a fixed pixel box.
   --------------------------------------------------------------------------- */
.eq-fieldset {
    box-sizing: border-box;
    padding: var(--s4);
    margin: 0 0 var(--s4);
    border: 1px solid var(--eq-fieldset-edge);
    border-radius: 15px;
    background: var(--eq-fieldset);
    /* IT LIFTS, BECAUSE IT IS THE OUTER SURFACE -- Tim, 12 September 2026:
       "the shadow needs to be on the outside div, the light blue against the
       light gray of the main panel, not the inside table on the light blue."

       That is the rule in one sentence, and it is the correction to my first
       attempt: I lifted the grid INSIDE this panel, which cast a shadow onto a
       mid-blue tint at a few pixels' range -- invisible -- while the panel that
       actually meets the grey page stayed flat. The lift belongs where the
       contrast is. Rule 5.4l. */
    box-shadow: var(--eq-lift);
}
.eq-fieldset:last-child { margin-bottom: 0; }

/* Centred, bold, underlined -- exactly the legacy's inline style on each of the
   four <h3>s (:976, :1162, :1326, :1389). */
.eq-fieldset__title {
    margin: 0 0 var(--s3);
    text-align: center;
    font-size: 1rem;
    font-weight: 700;
    text-decoration: underline;
    color: #000;
}

/* The two grounds a staff editor sits on. Without one of these the cards above
   float on white and the whole exercise is pointless -- see --eq-sunken. */
.eq-dialog__body--sunken { background: var(--eq-sunken); }
.eq-dialog__body--quiet  { background: var(--eq-panel-quiet); }

/* One option of a horizontal radio group or a row of tick boxes -- the legacy's
   RepeatDirection="Horizontal" (DealershipPref.aspx:1175, :1417) and its day-of-
   week CheckBoxList.

   inline-flex with a gap, so the box and its words move together: a label that
   wraps between the control and its text leaves an orphaned tick box on the next
   line, and on a row of five day counts that happens at any narrow width. The
   right margin is what separates one option from the next -- the legacy uses a
   table cell per option, which is heavier markup for the same result. */
.eq-detail .eq-radio {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    margin-right: var(--s3);
    white-space: nowrap;
}
.eq-detail .eq-radio:last-child { margin-right: 0; }

/* A textarea in the settings form follows the box widths above rather than
   filling the cell, for the same reason they do. */
.eq-detail textarea {
    width: 200px;
    max-width: 100%;
    font: inherit;
}
.eq-detail textarea.is-wide { width: 350px; }

/* The note beside a control that is deliberately not operable -- the inert
   Evolve setting, and a Lite dealership's fixed lead destination. Beside it, not
   under it, because the row is one line and a second line would look like a
   validation error. */
.eq-detail .note { display: inline; margin-left: var(--s2); }

/* The sub-heading inside a value cell: "Vehicle and Parts sale:" above the
   trading hours. Bold and on its own line, as the legacy's <b> label is. */
.eq-detail__subhead {
    display: block;
    font-weight: 700;
    margin: var(--s2) 0 var(--s1);
}
.eq-detail__subhead:first-child { margin-top: 0; }

/* A GROUP HEADING THAT SPANS BOTH COLUMNS -- "Sold To" and "Deliver To" on the
   floor plan pane, where the legacy centres an underlined <strong> across a
   full-width row.

   A <th colspan="2"> rather than the __subhead above, because these divide the
   ROWS of the table rather than label a value inside one cell: the eleven
   boxes below them are two separate parties, and a reader who loses that
   division fills in the funder's VAT number for the dealership. Centred and
   underlined to match, but in the house ink rather than a browser default. */
.eq-detail__group {
    text-align: center;
    font-weight: 700;
    text-decoration: underline;
    padding-top: var(--s3);
}
.eq-detail > tbody > tr:first-child > .eq-detail__group { padding-top: 0; }

/* The hours themselves: a small inner table of day + two dropdowns, exactly the
   nested <table> the legacy puts in that one cell. */
.eq-hours { border-collapse: collapse; }
.eq-hours th {
    padding: .15rem .5rem .15rem 0;
    background: none;
    border: 0;
    text-align: left;
    font-weight: 400;
    white-space: nowrap;
}
.eq-hours td { padding: .15rem 0; border: 0; text-align: left; }
.eq-hours select { width: 80px; }   /* the legacy's Width="80px" */
.eq-hours .eq-hours__sep { padding: 0 .35rem; color: var(--muted); }

/* The legacy's bulleted link lists inside the Prospecting and Stocklist panels. */
/* The legacy's bulleted link lists inside the Prospecting and Stocklist panels.
   .25rem between items, not .6rem: two items at .6rem cost 14px of a screen
   whose whole problem is height, and the legacy's own list is single-spaced. */
.eq-linklist { margin: var(--s2) 0 0; padding-left: 1.25rem; }
.eq-linklist li { margin-bottom: .25rem; }
.eq-linklist li:last-child { margin-bottom: 0; }
.eq-linklist .eq-count { color: var(--eq-body-ink); margin-left: .5rem; }

/* The instruction line under a panel heading, underlined as the legacy has it
   ("Choose ONE field to search..." on the home page). */
.eq-instruction {
    text-align: center;
    text-decoration: underline;
    margin: var(--s1) auto var(--s2);
    max-width: 46ch;
}

/* The right-hand side of the title bar: module badge, welcome, log out. */
.topbar__right { display: flex; align-items: baseline; gap: var(--s4); margin-left: auto; }

/* EASY SHOWROOM STUDIO'S TITLE BAR (Tim, 8 October 2026): the EasySystems logo on the left, and "Powered by
   EasyQuote" centred between the business name and the signed-in user. Only that host writes these two
   classes; EQ Next's bar is untouched. The logo picture has a white background, so it sits on a white
   rounded tile that reads against the slate bar. The two auto margins on .topbar__powered share the free
   space either side of it equally -- which is "centred between" -- so the user area, which otherwise
   pushes itself right with its own auto margin, is told not to when it follows the link. */
.topbar__logo {
    display: inline-flex; flex: none;
    background: #fff; border-radius: var(--radius); padding: 2px; line-height: 0;
}
.topbar__logo img { display: block; width: 40px; height: 40px; }
.topbar__powered {
    margin-inline: auto; white-space: nowrap;
    color: #fff; font-size: var(--fs-body); text-decoration: none; opacity: .9;
}
.topbar__powered:hover, .topbar__powered:focus-visible { opacity: 1; text-decoration: underline; }
.topbar__powered + .topbar__right { margin-left: 0; }
.topbar__welcome { color: #fff; font-size: var(--fs-body); white-space: nowrap; }
.topbar__welcome strong { font-weight: 700; }
/* The log-out form sits inline inside the "[ ... ]" brackets, as the legacy does.
   It is a real POST form, not a link, because signing out must not be reachable
   by a GET -- a prefetching browser or a crawled link would otherwise end the
   session. Displayed inline so the brackets close around it. */
.topbar__welcome form { display: inline; }
.linkbtn {
    background: none; border: 0; padding: 0; margin: 0;
    font: inherit; color: #fff; text-decoration: underline; cursor: pointer;
}
.linkbtn:hover { color: var(--eq-menu-on); }

/* ---------------------------------------------------------------------------
   THE HIDDEN SUPPORT DIAGNOSTIC -- .diag

   Tim's standing instruction, 27 August 2026: the value is written in the same
   colour as the background it sits on, so it is invisible until somebody who
   knows it is there drags across it. Selecting text changes the colour, and the
   value becomes readable.

   colour: transparent, NOT a background token, and that is the important part.
   A token would have to be the RIGHT token for whatever panel the diagnostic
   happens to sit on -- blue chrome, white card, grey page -- and it would have
   to be re-chosen for the dark theme. Transparent is correct on every
   background in both themes, and cannot drift.

   ::selection IS DECLARED EXPLICITLY, in both syntaxes, because the whole
   mechanism depends on it. Chrome's default selection does override text
   colour, so it would mostly work by luck -- but "mostly, by luck" is not a
   feature, and a browser that only sets the selection BACKGROUND would leave
   transparent glyphs on a blue rectangle and the value unreadable.

   user-select is forced to text: any ancestor setting user-select: none would
   make the value unselectable and therefore permanently invisible.
   --------------------------------------------------------------------------- */
.diag {
    color: transparent;
    -webkit-user-select: text;
    user-select: text;
    font-size: var(--fs-eyebrow);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;

    /* No pointer cue at all: a cursor change would advertise it. */
    cursor: default;
}

/* Readable the moment it is selected, in a colour that works on every
   background the diagnostic can sit on. */
.diag::selection {
    color: #fff;
    background: #000;
}
.diag::-moz-selection {
    color: #fff;
    background: #000;
}

/* PRINTED PAGES DROP IT. A diagnostic is for a support call, not for a document
   a dealer hands a customer -- and on paper there is no selection to reveal it,
   so it would be invisible ink in the literal sense. */
@media print {
    .diag { display: none; }
}

/* The right-hand buttons on the menu strip. */
.menustrip__right { display: flex; align-items: center; gap: 2px; margin-left: auto; }
.menustrip__right .nav-soon {
    background: var(--eq-menu);
    border: 1px solid var(--eq-menu-line);
    color: var(--eq-menu-ink);
    padding: .25rem 1.1rem; line-height: 1.35;
    white-space: nowrap; cursor: help;
}
/* Inside the menu strip a not-built item keeps the tab shape but stays muted, so
   it reads as present-but-inert rather than as a live tab. */
.menustrip .nav-soon { color: rgba(221, 228, 236, .78); }

/* ---------------------------------------------------------------------------
   A LIVE BUTTON ON THE MENU STRIP -- "Transfer to Other Module".

   It is a real <button> inside a <form> because the action is a POST (a GET
   would let a prefetch change which module you are signed in to), and it has to
   be INDISTINGUISHABLE from the .nav-soon items beside it: a rep of twenty years
   reads that strip as two grey buttons and must not have to notice that one is
   now a different kind of element.

   So the shape is copied from .menustrip__right .nav-soon deliberately rather
   than shared through a common class -- the two will diverge, because this one
   gains hover, focus and busy states that an inert label must never have.
   --------------------------------------------------------------------------- */
.menustrip__form { display: contents; }

.menustrip .nav-btn {
    background: var(--eq-menu);
    border: 1px solid var(--eq-menu-line);
    color: var(--eq-menu-ink);
    padding: .25rem 1.1rem;
    line-height: 1.35;
    white-space: nowrap;
    font: inherit;
    cursor: pointer;
}
.menustrip .nav-btn:hover { color: var(--eq-menu-on); }

/* A visible focus ring, because this is now keyboard-reachable and the strip is
   dark -- the browser default outline disappears against it. */
.menustrip .nav-btn:focus-visible {
    outline: 2px solid var(--eq-menu-on);
    outline-offset: -3px;
}

/* The busy state markBusy() applies. The strip is narrow, so the label swap
   ("Transferring...") is the whole indicator -- there is no room for a spinner
   without shifting the buttons beside it, and a shifting menu is worse than a
   changed word. */
.menustrip .nav-btn[disabled] { color: rgba(221, 228, 236, .6); cursor: progress; }

/* ---------------------------------------------------------------------------
   TRANSFER TO OTHER MODULE -- the drop-down of modules the user may move to.

   Tim, 27 August 2026: "it's supposed to come down with a list ... like a drop
   down menu item with the other modules that you have access to". The legacy is
   an <asp:Menu> submenu; this is a <details>/<summary>, which opens, closes,
   takes the keyboard and closes on Escape with NO SCRIPT AT ALL. The legacy
   needs an 80KB ScriptManager for the same behaviour.

   position: static on the parent and absolute on the list, anchored to the
   strip: the menu strip is the positioning context, so the panel hangs below
   the button without widening the strip or being clipped by it.
   --------------------------------------------------------------------------- */
.modmenu { position: relative; }

/* The closed state IS the button, so it reuses .nav-btn and only loses the
   marker the browser draws. list-style on both because the ::-webkit- pseudo is
   what Safari and older Chrome use. */
.modmenu > summary { list-style: none; cursor: pointer; }
.modmenu > summary::-webkit-details-marker { display: none; }
.modmenu > summary::marker { content: ""; }

/* Open reads as pressed, so it is obvious which button produced the panel. */
.modmenu[open] > summary { background: var(--eq-menu); color: var(--eq-menu-on); }

.modmenu__list {
    position: absolute;
    top: 100%;
    right: 0;
    z-index: 40;
    min-width: 16rem;
    display: flex;
    flex-direction: column;
    background: #fff;
    border: 1px solid var(--border-strong);

    /* The same lift every panel in the application carries, so a menu reads as
       floating above the page rather than cut into it. Design brief 1a.21. */
    box-shadow: var(--eq-lift);
}

/* Each item is a SUBMIT BUTTON, not a link -- changing which module you are
   signed in to must not be reachable by a GET. So the button has to be talked
   back into looking like a menu row. */
.modmenu__item {
    display: flex;
    align-items: baseline;
    gap: .5rem;
    width: 100%;
    padding: .45rem .9rem;
    border: 0;
    background: none;
    font: inherit;
    color: var(--ink);
    text-align: left;
    white-space: nowrap;
    cursor: pointer;
}
.modmenu__item + .modmenu__item { border-top: 1px solid var(--border); }
.modmenu__item:hover { background: var(--eq-selected); color: var(--eq-selected-ink); }
.modmenu__item:focus-visible {
    outline: 2px solid var(--eq-menu);
    outline-offset: -2px;
}
.modmenu__item[disabled] { color: var(--muted); cursor: progress; }

/* "in EasyQuote" on a module that exists in the legacy and not yet here. Said on
   the item rather than discovered after clicking it -- and the item is still
   offered, because hiding it would look to an F&I user like their access had
   been taken away. */
.modmenu__soon {
    margin-left: auto;
    font-size: var(--fs-small);
    color: var(--muted);
    font-style: italic;
}

/* A legacy TAB with no screen behind it yet. It keeps the tab shape so the menu
   reads as the legacy menu does -- the row of tabs is how a user of twenty years
   navigates, and a gap where "Reminders" should be is more disorienting than a
   greyed tab. Muted and cursor:help so it is clearly inert. */
.nav > .nav-soon {
    display: flex; align-items: center;
    background: var(--eq-menu);
    border: 1px solid var(--eq-menu-line);
    color: rgba(221, 228, 236, .78);
    padding: .25rem 1.1rem; line-height: 1.35;
    white-space: nowrap; cursor: help;
    border-radius: 0;
}

/* ---------------------------------------------------------------------------
   "IN PROGRESS" -- the draft status on the Open Quotes list.

   Given the legacy grid's own pale blue (--eq-grid-head / --eq-grid-head-ink)
   rather than a new hue, so it reads as part of the same family as the other
   status pills without inventing a colour. It must NOT borrow the traffic-light
   green, black or red: those three carry contact-recency meaning on this exact
   screen and a fourth use of them would dilute it.
   --------------------------------------------------------------------------- */
.pill--inprogress { background: var(--eq-grid-head); color: var(--eq-grid-head-ink); }

/* The row itself gets a left edge and a faint tint, so a pinned draft is
   visibly not part of the sorted, paged set below it. Deliberately quiet: these
   rows are first on the screen, and a loud treatment on the most-visited list in
   the application would wear badly. */
tr.row--inprogress > td {
    background: var(--eq-grid-row);
}
tr.row--inprogress > td:first-child {
    box-shadow: inset 3px 0 0 var(--eq-grid-head-ink);
}

/* ===========================================================================
   16  THE LEGACY RESULTS GRID -- grid--eq

   From Tim's screenshot of the legacy Client Search Result, 25 August 2026, with
   two explicit asks: the same styling, and TIGHTER ROWS -- "if we have a long
   list of customers, it becomes a pain to have to scroll down."

   A MODIFIER on table.grid, not a replacement, so this table keeps the
   row-linking, activation feedback and sticky header the shared grid already
   has. Only the skin and the density change.
   =========================================================================== */

/* ROW DENSITY. .18rem against the default .6rem, which fits roughly twice as
   many clients on a screen -- the legacy rows are about 18px and this lands in
   the same place. The floor is legibility, not tightness: line-height stays at
   1.35 and the hairline between rows is kept, because a dense grid with no rules
   is where the eye loses its line and a rep picks the wrong customer. */
table.grid--eq th,
table.grid--eq td {
    padding: .18rem .5rem;
    line-height: 1.35;
    vertical-align: middle;
    border-bottom: 0;                       /* the cell hairlines do this job */
    border-right: 1px solid var(--eq-grid-line);
    white-space: nowrap;
}
table.grid--eq th:last-child,
table.grid--eq td:last-child { border-right: 0; }

/* Header: pale blue, dark blue, bold -- and NOT uppercased. The legacy uses
   normal case with initial capitals ("Client Type", "Also Known As"), which is
   what makes eight columns readable at this size; the shared grid's
   letter-spaced uppercase would widen every one of them. */
table.grid--eq thead th {
    background: var(--eq-grid-head);
    color: var(--eq-grid-head-ink);
    font-size: var(--fs-small);
    font-weight: 700;
    text-transform: none;
    letter-spacing: 0;
    border: 1px solid var(--eq-grid-line);
    text-align: center;
}

/* Banded rows, first band tinted, matching RowStyle / AlternatingRowStyle. */
table.grid--eq tbody tr:nth-child(odd)  > td { background: var(--eq-grid-row); }
table.grid--eq tbody tr:nth-child(even) > td { background: #fff; }
table.grid--eq tbody tr { border-bottom: 1px solid var(--eq-grid-line); }

/* Hover has to beat the banding, so it is stated for both bands. */
table.grid--eq tbody tr:hover > td { background: var(--eq-menu-on); }

/* ---------------------------------------------------------------------------
   SELECTION IS GLOBAL -- Tim, 25 August 2026:

     "you'll notice that the [line] entry that was previously clicked on has a
      different background color clearly indicating which entry was selected.
      Please, can you add that as a global rule for anything that may have been
      selected?"

   So this is one rule for EVERY grid in the application, not a per-screen
   treatment, and the values are the legacy's own .GridSelectedRowStyle rather
   than something new.

   ALL THREE PROPERTIES CARRY THE SIGNAL AND NONE IS DECORATION:

     background  the thing you see from across the row
     font-weight survives when a row is ALSO tinted by something else -- these
                 grids band their rows, and the call-status traffic lights
                 recolour whole rows on Open Quotes, so a background alone can be
                 competed with
     colour      navy against the pale blue, so the text stays readable

   Weight and colour are also what keep the state legible to someone who cannot
   distinguish the two blues. Do not "simplify" this to a background swap.

   Stated for BOTH bands with > td, because the banding rules target cells and
   would otherwise win on specificity. That is the same reason the hover rule
   below it is written the same way.
   --------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   .is-lastrow SHARES THIS RULE, AND SHARING IT IS THE POINT.

   Tim, 4 September 2026, on returning from a quote to the list he came from:
   "if you have a look in the legacy system, you'll see that there's one line
   that has a different blue highlight. Now that indicates the last line entry
   that I was interacting with so that the user visually can see where they left
   off and then obviously continue from there."

   That is the same blue -- BaseStyle.css's GridSelectedRowStyle, held here as
   --eq-selected / --eq-selected-ink -- so it is the same rule rather than a
   second one with the same values. If the selection treatment is ever restyled,
   "where I left off" moves with it, which is what "the same blue" has to mean
   to survive.

   THE TWO CLASSES STILL MEAN DIFFERENT THINGS, and that is why both names
   exist. .is-selected is "this is the row you chose" -- a picker's current
   answer, rendered by the server. .is-lastrow is "this is where you were",
   remembered by app.js across a navigation and never sent to the server. A page
   can legitimately show one, the other, or neither.
   --------------------------------------------------------------------------- */
table.grid tbody tr.is-selected > td,
table.grid tbody tr.is-lastrow > td,
table.grid--eq tbody tr.is-selected > td,
table.grid--eq tbody tr.is-lastrow > td {
    background: var(--eq-selected);
    color: var(--eq-selected-ink);
    font-weight: 700;
}
/* Selection outranks hover: moving the pointer over a different row must not
   make it look like the selection moved. */
table.grid tbody tr.is-selected:hover > td,
table.grid tbody tr.is-lastrow:hover > td,
table.grid--eq tbody tr.is-selected:hover > td,
table.grid--eq tbody tr.is-lastrow:hover > td {
    background: var(--eq-selected);
}
/* A link inside a selected row inherits the row's ink, or the blue would fight
   the navy and the row would stop reading as one thing. */
table.grid tbody tr.is-selected > td a,
table.grid tbody tr.is-selected > td .textlink,
table.grid tbody tr.is-lastrow > td a,
table.grid tbody tr.is-lastrow > td .textlink {
    color: var(--eq-selected-ink);
    font-weight: 700;
}

/* The legacy's small tables and pickers use the same blue for the same reason. */
.eq-table tbody tr.is-lastrow td { background: var(--eq-selected); color: var(--eq-selected-ink); font-weight: 700; }

/* ---------------------------------------------------------------------------
   THE DEALERSHIP OVERVIEW GRID -- grid--overview

   Tim, 27 August 2026: "can you try and match the legacy dealership overview
   today, view and styling, remembering things like row heights, colors".

   The palette comes from grid--eq above, which already matched. What this adds
   is the two ways the legacy's OVERVIEW grid differs from its CLIENT SEARCH
   grid -- and they differ in the legacy, so this is not a preference:

     ROW HEIGHT   ItemStyle-Height="30px" on every column of both overview
                  grids (ReportDealershipOverview.aspx:312 onward). The client
                  search rows are roughly 18px, which is why grid--eq is dense.
                  Tim asked for tighter rows there on 25 August and 30px here on
                  27 August; both are right, because the legacy screens are not
                  the same.

     ALIGNMENT    ItemStyle-HorizontalAlign="Center" on every column except the
                  salesperson's name, which is Left with HeaderStyle-Width="180px".
                  grid--numeric right-aligns numbers, which is correct for money
                  and wrong here: these are counts in narrow columns and the
                  legacy centres them under centred headings.

   MUST BE DEFINED AFTER grid--eq. Both are one class on the table, so equal
   specificity means source order decides which padding wins. Moving this block
   above grid--eq would silently restore the dense rows.
   --------------------------------------------------------------------------- */

/* 30px rows. Set with height on the cells rather than padding, because that is
   what the legacy sets, and it keeps a two-line wrapped heading from making its
   row taller than the others. */
table.grid--overview tbody td,
table.grid--overview tfoot th,
table.grid--overview tfoot td {
    height: 30px;
    padding-top: 0;
    padding-bottom: 0;
    vertical-align: middle;
}

/* Centred metric cells, matching ItemStyle-HorizontalAlign="Center". Overrides
   grid--numeric's right alignment, which suits money and not counts.

   tfoot TH IS IN THE LIST, and leaving it out was the bug Tim spotted: "please
   center align the dealership totals with the columns above". The totals row is
   built from <th scope="row"> plus <th> cells, not <td>, so a rule naming only
   tfoot td missed all of it and the totals stayed right-aligned under centred
   columns. A total that does not line up with its own column is worse than no
   total. */
table.grid--overview tbody td,
table.grid--overview tfoot td,
table.grid--overview tfoot th,
table.grid--overview thead th { text-align: center; }

/* The salesperson stays LEFT, in both the head and the body, at the legacy's own
   180px. Last so it wins over the centring above. */
table.grid--overview .grid__name,
table.grid--overview th.grid__name {
    text-align: left;
    width: 180px;
    min-width: 180px;
}

/* The legacy grid is 95% wide and centred (Width="95%"
   HorizontalAlign="Center"). Reproduced as a max-width rather than a width so a
   narrow window still fills, and the inner scroll still handles twelve columns.
   The 92% full-viewport rule (project rule 5.5) governs the PAGE, not this
   table inside it. */
table.grid--overview {
    width: 95%;
    margin-inline: auto;
}

/* ---------------------------------------------------------------------------
   THE DRILL-DOWN LISTS -- grid--records, plus one class per record shape.

   Tim, 27 August 2026: "Please also make sure that the screen behind the click
   also matches the styling of the legacy software."

   It does now, and it was mostly free: the legacy's drill-down panels (LeadGrid,
   NoteGrid, QuoteGrid at ReportDealershipOverview.aspx:738, :804, :858) carry the
   SAME ExtGrid* classes and the SAME ItemStyle-Height="30px" as the summary grids
   above them, so grid--eq already supplied the skin and this only adds the height
   and the alignment.

   NOT grid--overview here. That modifier centres every cell, which is right for a
   grid of counts and wrong for a list of records -- a centred column of client
   names is unreadable, and the legacy sets those Left.
   --------------------------------------------------------------------------- */
table.grid--records tbody td,
table.grid--records thead th {
    height: 30px;
    padding-top: 0;
    padding-bottom: 0;
    vertical-align: middle;
}

/* Text left by default, which is what most of these columns are. The exceptions
   are named per shape below. */
table.grid--records tbody td,
table.grid--records thead th { text-align: left; }

/* ---- LeadGrid: Date Added C | New/Used C | Lead Origin L | Client Name L |
        Contact No. C | Vehicle Intrested In L | No Lead Reason L   (aspx:741-777) */
table.grid--rec-lead tbody td:nth-child(1),
table.grid--rec-lead thead th:nth-child(1),
table.grid--rec-lead tbody td:nth-child(2),
table.grid--rec-lead thead th:nth-child(2),
table.grid--rec-lead tbody td:nth-child(5),
table.grid--rec-lead thead th:nth-child(5) { text-align: center; }

/* ---- NoteGrid: Due Date C | Comp Date C | Note Type L | Client Name L |
        Contact No. C                                        (aspx:807-837) */
table.grid--rec-note tbody td:nth-child(1),
table.grid--rec-note thead th:nth-child(1),
table.grid--rec-note tbody td:nth-child(2),
table.grid--rec-note thead th:nth-child(2),
table.grid--rec-note tbody td:nth-child(5),
table.grid--rec-note thead th:nth-child(5) { text-align: center; }

/* ---- QuoteGrid: Quote ID R | Quote Date C | Due Date C | Client Name L |
        Contact No. C | Rep Name L | Vehicle L | Branch C     (aspx:861-897)

   Quote ID is RIGHT, not centred: it is an identifier read down a column, and the
   legacy is specific about it (ItemStyle-HorizontalAlign="Right"
   ItemStyle-Width="70px"). */
table.grid--rec-quote tbody td:nth-child(1),
table.grid--rec-quote thead th:nth-child(1) { text-align: right; width: 70px; }

table.grid--rec-quote tbody td:nth-child(2),
table.grid--rec-quote thead th:nth-child(2),
table.grid--rec-quote tbody td:nth-child(3),
table.grid--rec-quote thead th:nth-child(3),
table.grid--rec-quote tbody td:nth-child(5),
table.grid--rec-quote thead th:nth-child(5),
table.grid--rec-quote tbody td:nth-child(8),
table.grid--rec-quote thead th:nth-child(8) { text-align: center; }

/* ---- The Delivery shape is the quote list without Due Date or Rep Name, so its
        centred columns land one and two places earlier. Written out rather than
        shared, because "the same as quote but shifted" is exactly the kind of
        cleverness that breaks silently when a column moves. */
table.grid--rec-delivery tbody td:nth-child(1),
table.grid--rec-delivery thead th:nth-child(1) { text-align: right; width: 70px; }

table.grid--rec-delivery tbody td:nth-child(2),
table.grid--rec-delivery thead th:nth-child(2),
table.grid--rec-delivery tbody td:nth-child(4),
table.grid--rec-delivery thead th:nth-child(4),
table.grid--rec-delivery tbody td:nth-child(6),
table.grid--rec-delivery thead th:nth-child(6) { text-align: center; }

/* EVERY CELL READS AS A LINK -- but ONLY once app.js has confirmed the row is
   clickable, which is what .is-row-linked means. In the legacy every cell is its
   own LinkButton, so the whole line is blue and underlined; here one real submit
   button carries the ClientID (it identifies a person, so it must not go in a
   URL) and the row click stands in for the rest.

   Gated on .is-row-linked for the same reason the pointer cursor is: if the
   script is blocked, these cells genuinely are not clickable and must not
   pretend to be. Underlining dead text is worse than leaving it plain. */
table.grid--eq.is-row-linked tbody td {
    color: var(--eq-link);
    text-decoration: underline;
}
/* The real control keeps its own colour rather than inheriting the cell's, so
   the name still reads as the primary target. */
table.grid--eq.is-row-linked tbody td .textlink { color: var(--eq-link); }
/* Tags and markers are not part of the link text. */
table.grid--eq tbody td .tag { text-decoration: none; }

/* ---------------------------------------------------------------------------
   THE COLLAPSIBLE RESULTS PANEL

   The legacy wraps the results in an AJAX CollapsiblePanel with a pale blue
   header bar and a disclosure triangle. Reproduced with <details>/<summary> so
   it collapses with NO JavaScript at all -- the toolkit version needs 100KB of
   script to do the same thing.
   --------------------------------------------------------------------------- */
.eq-panelbox {
    border: 1px solid var(--eq-grid-line);
    background: #fff;
    margin-block: var(--s4);
}
.eq-panelbox > summary {
    list-style: none; cursor: pointer;
    background: var(--eq-grid-head);
    color: var(--eq-grid-head-ink);
    font-variant: small-caps;
    font-weight: 700;
    font-size: 1rem;
    padding: .3rem .6rem;
    border-bottom: 1px solid var(--eq-grid-line);
    display: flex; align-items: center; gap: .4rem;
}
.eq-panelbox > summary::-webkit-details-marker { display: none; }
/* The triangle points down when open, right when closed -- the legacy's own
   direction, and the opposite of the browser default for <details>. */
.eq-panelbox > summary::before { content: "\25b6"; font-size: .7em; }
.eq-panelbox[open] > summary::before { content: "\25bc"; }
.eq-panelbox > summary:hover { background: var(--eq-menu-hover); }
.eq-panelbox__body { padding: var(--s3); }

/* ---------------------------------------------------------------------------
   THE BACK BUTTON

   Tim, 25 August 2026: "the back arrow is difficult to see with the dark blue
   inside the gray blue color. Can you maybe find me a better looking back
   button? because, to be honest, I don't like the original button either."

   So this is NOT the legacy's button, by invitation. Three things changed and
   each is doing a job:

   1  IT IS LIGHT ON DARK, NOT DARK ON DARK. The old one was --eq-titlebar
      (#4b6c9e) on the #f6f6f6 slab -- which is a 3.4:1 contrast against its
      surround, and worse against the grey ground beside it. White fill with a
      slate glyph is 12:1 and reads instantly.

   2  IT SAYS "BACK". An arrow alone has to be recognised; a word is read. It
      also gives the control a real accessible name without leaning on
      aria-label, and it roughly triples the click target -- 34px circles are
      small for a mouse and this is a control reps hit constantly.

   3  The chevron is INLINE SVG, not a glyph. The legacy uses a bitmap and my
      first version used the U+25C0 character, which renders at a different
      weight in every font and turns into an emoji triangle on some. An SVG path
      is the same everywhere and costs about 90 bytes.
   --------------------------------------------------------------------------- */
.eq-back {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .25rem .7rem .25rem .55rem;
    background: #fff;
    color: var(--brand-deep);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-sm);
    font-size: var(--fs-small); font-weight: 600;
    line-height: 1.35;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
.eq-back svg { flex: 0 0 auto; }
/* Hover moves toward the brand rather than to a new hue -- the button belongs to
   the chrome, and a hover colour from outside the palette would look like a
   different product's control. */
.eq-back:hover {
    background: var(--brand-deep);
    border-color: var(--brand-deep);
    color: #fff;
    text-decoration: none;
}
.eq-back:focus-visible { outline: 2px solid var(--brand-deep); outline-offset: 2px; }

/* The centred "ADD NEW CLIENT" link, above the results as the legacy has it. */
.eq-addlink {
    display: block; text-align: center;
    margin: var(--s4) auto var(--s3);
    font-variant: small-caps; font-size: 1.05rem; font-weight: 600;
    letter-spacing: .02em;
}

/* The back arrow and the page title on one line, title centred on the page
   rather than beside the arrow -- as the legacy has it, where the arrow floats
   at the left margin and the heading is centred in the content column. */
/* auto 1fr auto, so the title stays centred on the PAGE while the button sizes
   itself. A fixed first column would either clip the label or leave a gap. The
   empty third column is what keeps the heading centred rather than pushed right
   by the button's width. */
.eq-toprow {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: var(--s3);
    margin-block: var(--s2);
}
.eq-toprow > .eq-back { grid-column: 1; justify-self: start; }
.eq-toprow > h1 { grid-column: 2; }

/* The centred ADD NEW CLIENT control. .textlink gives it the link appearance;
   this gives it the legacy's small-caps weight and spacing. */
.eq-addlink.textlink {
    font-variant: small-caps;
    font-size: 1.05rem;
    font-weight: 600;
    letter-spacing: .02em;
    text-decoration: underline;
}

/* THERE IS NO .is-prospect RULE, deliberately. A bulk marketing prospect is
   rendered exactly like any other client, because that is what the legacy does
   -- Tim, 25 August 2026. The rows are still returned; see the note in
   Pages/Sales/Clients.cshtml for why that part must not change. */

/* ===========================================================================
   17  THE LOGIN SCREEN

   From Tim's screenshot of the legacy login, 25 August 2026: a centred
   announcement above, then a three-column row of photograph / form / app
   screenshots, then a support footer.
   =========================================================================== */

/* The login page is the one screen that is NOT full-width by content, but the
   announcement above the form does want the page width, so .auth widens and the
   card inside it keeps its own measure. */
/* TOP-ALIGNED, NOT VERTICALLY CENTRED, and this is the other half of the height
   problem. Section 12's .auth is `min-height: 100dvh; place-items: center`, which
   was right when this page was one card and wrong the moment an announcement went
   above it: the grid then guarantees a full viewport of height and centres
   everything in it, so the form sits lower than it needs to and the page scrolls
   even when the content would have fitted.

   align-content: start puts the content at the top, where the legacy has it, and
   min-height goes to auto so the page is exactly as tall as it needs to be. */
main.auth {
    width: min(100%, max(1200px, 92%));
    margin-inline: auto;
    padding: var(--s4) var(--s4) var(--s5);
    min-height: auto;
    align-content: start;
    justify-items: stretch;
}

/* The announcement block: the legacy's paragraphs are single lines with normal
   leading, not the 1.6 a prose column wants. */
.eq-announce p { line-height: 1.45; margin-block: .35rem; }
.eq-announce__badges { margin-block: var(--s4); }
.eq-announce { margin-bottom: var(--s4); }
.eq-announce__head { font-size: 1.4em; margin-bottom: var(--s2); }

.eq-announce { text-align: center; margin-bottom: var(--s6); }
.eq-announce__head {
    color: #000;
    font-weight: 700;
    font-size: 1.5em;
    text-align: center;
    margin: 0 0 var(--s3);
}
/* Centred running text is an exception to the 68ch measure, because the legacy
   centres it and the block is only three short paragraphs. It still gets a
   measure -- 100ch rather than none -- so it does not run the full 2355px of a
   wide monitor, which would be unreadable. */
.eq-announce p { max-width: 100ch; margin-inline: auto; }
.eq-announce__badges { display: flex; justify-content: center; gap: var(--s5); margin-block: var(--s5); }
.eq-announce__badges img { display: block; height: auto; max-width: 100%; }

/* The support line is RED in the legacy and the colour is doing a job -- it is
   the one line on the page a stuck user needs to find. Uses the legacy's own
   error red rather than a new one. */
.eq-announce__support {
    color: var(--bad);
    font-size: 1.05rem;
    font-weight: 500;
}
.eq-announce__support a { color: var(--bad); text-decoration: underline; }

/* THREE COLUMNS on a wide screen, ONE on a narrow one -- and the order matters
   when it collapses: the form must come first, so a phone user is not scrolled
   past two decorative photographs to reach the thing they came for. The asides
   are display:none below the breakpoint rather than reordered, because on a
   phone the app screenshots are just weight. */
.eq-loginrow {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: var(--s6);
    flex-wrap: wrap;
}
.eq-loginrow__aside { flex: 0 0 auto; }
.eq-loginrow__aside img { display: block; height: auto; max-width: 100%; }
.eq-loginrow__phones { display: flex; gap: var(--s4); }
.eq-loginrow__phones img { width: 175px; }

@media (max-width: 1100px) {
    /* The photograph and the screenshots go, not shrink. They are decoration,
       and on a narrow screen every pixel belongs to the form. */
    .eq-loginrow__aside { display: none; }
}

/* The login card: a bordered box with the legacy's blue input borders. */
.auth__card {
    flex: 0 1 26rem;
    background: #fff;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    padding: var(--s4);
    /* Section 12 gives it a large drop shadow, which suited a card floating on a
       gradient. It now sits on the page slab like every other panel, so it gets
       the same flat treatment as .eq-card. */
    box-shadow: none;
}

/* Remember-me on ONE line, as the legacy has it. The explanatory note moves into
   the label's title so the information survives without a second line -- it is a
   clarification, not something a rep reads every time they sign in. */
.auth__card .field--check .check { align-items: center; }
.auth__card .field--check .check input[type="checkbox"] { margin-top: 0; }

/* The phones are the tallest thing in the row, and in the legacy they are TALLER
   than the card rather than shorter -- they set the height of the block and the
   card sits beside them. 175px was making them the short element and leaving the
   row looking unbalanced. */
.eq-loginrow__phones img { width: 150px; }
.eq-loginrow { gap: var(--s5); }
.auth__card input[type="text"],
.auth__card input[type="password"],
.auth__card select {
    border: 1px solid var(--eq-titlebar);
    border-radius: 0;
}

/* The manual link and the Log In button on one line, as the legacy has them --
   link left, button right. */
.eq-loginactions {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--s4); flex-wrap: wrap;
    margin-top: var(--s4);
}
/* Inside the card a not-built item is plain muted text, not a dark tab. */
.auth__card .nav-soon {
    background: none; border: 0; color: var(--muted);
    padding: 0; font-size: var(--fs-small);
}

/* The support footer, above a hairline as the legacy has it. */
.eq-footer {
    border-top: 1px solid var(--eq-page-line);
    background: var(--eq-page);
    padding: var(--s3) 0;
    font-size: var(--fs-small);
    color: var(--eq-body-ink);
}
.eq-footer > div { display: flex; gap: var(--s4); flex-wrap: wrap; align-items: baseline; }
.eq-footer__label { font-weight: 600; }

/* ---------------------------------------------------------------------------
   18  CONTACT DETAILS
   --------------------------------------------------------------------------- */

/* The Support / Email pairs, centred as a two-column block. A definition list
   rather than the legacy's layout table, so the pairing is in the markup. */
.eq-contact {
    display: grid;
    grid-template-columns: auto auto;
    gap: .2rem var(--s5);
    justify-content: center;
    margin: var(--s4) 0;
}
.eq-contact dt { text-align: left; }
.eq-contact dd { margin: 0; text-align: left; }

.eq-tools__head {
    text-align: center;
    font-size: 1.35rem;
    font-weight: 700;
    color: #000;
    text-decoration: underline;
    font-variant: normal;
    margin: var(--s5) 0 var(--s3);
}
.eq-tools {
    display: flex; justify-content: center; align-items: center;
    gap: var(--s3); flex-wrap: wrap;
}
.eq-tool { display: inline-block; border: 1px solid var(--border-strong); line-height: 0; }
.eq-tool img { display: block; height: auto; max-width: 100%; }
.eq-tool:hover { border-color: var(--eq-titlebar); }
/* A tool whose download is not configured: shown, dimmed, not clickable. Shown
   rather than hidden so the list still says what support can offer. */
.eq-tool--off { opacity: .45; cursor: not-allowed; }

/* The three matching rules under the home page's search heading. One line at
   small size -- it is a reference a rep checks occasionally, not a heading. */
.eq-matchrules {
    text-align: center;
    /* 58ch wrapped this to two lines inside a 760px card, for 18px of height
       that bought nothing. It is one sentence; let it use the card. */
    max-width: 100%;
    margin: 0 auto var(--s2);
    line-height: 1.35;
}

/* ---------------------------------------------------------------------------
   THE CLIENT PROFILE PANE
   --------------------------------------------------------------------------- */

/* The client's name, centred, in the legacy's light small-caps. */
.eq-profilename { text-align: center; margin: var(--s2) 0 var(--s4); }

/* The numbered action list in its pale blue box, 350px and centred as the legacy
   has it (Default.aspx:1913, fieldset .Sections width:350px margin:0 auto). */
.eq-actions {
    width: min(100%, 380px);
    margin: 0 auto var(--s4);
    padding: var(--s3) var(--s3) var(--s3) 2.2rem;
    background: var(--eq-formbox);
    border: 1px solid var(--eq-formbox-line);
    list-style: decimal;
}

/* THE 2.2rem IS ROOM FOR THE NUMBERS, so it belongs to the <ol> and to nothing
   else. This class was written for the client profile's numbered menu and is
   now used as a BUTTON ROW on 37 screens -- all of them <div> -- where the
   indent is dead space that pushes the buttons off the centre of their own box.
   Tim saw it on Change Username/Password, 15 September 2026.

   Split by ELEMENT rather than by adding a modifier to 37 files: the tag
   already says which of the two things it is, and a modifier would be one more
   thing to forget on the 38th. */
div.eq-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    justify-content: center;
    padding: var(--s3);
}
.eq-actions > li { margin-bottom: .4rem; }
.eq-actions > li:last-child { margin-bottom: 0; }
/* Inside this list a not-built item is plain muted text, not a dark tab -- the
   .nav-soon shape belongs to the menu strip. */
.eq-actions .nav-soon {
    background: none; border: 0; padding: 0;
    color: var(--muted); font-size: var(--fs-body);
    display: inline; cursor: help;
}

/* START NEW QUOTE opens in place. The summary has to look like the sibling
   links, or the one live item would read as the odd one out. */
/* AN ACTION IN THE CLIENT PROFILE MENU LOOKS LIKE A LINK.

   Tim, 3 September 2026: "can you match the styling for number five, view edit
   client details, the same as you have done for number one, start new quote?"

   Item 1 is a <details><summary>, item 5 is a <button> in a form, and they had
   drifted apart visually -- .textlink paints --brand-deep and underlines only on
   hover, so item 5 read as disabled text sitting between working links.

   Declared as ONE rule covering both so they cannot drift again. Every remaining
   item in that menu becomes a form-and-button as it is built, and each one adds
   .eq-menuaction rather than a colour of its own.

   .textlink is still needed alongside it on a button: that is what removes the
   border, background and font a browser gives a <button>. This rule only supplies
   the link's colour and underline. */
.eq-newquote > summary,
.eq-menuaction {
    cursor: pointer;
    color: var(--eq-link);
    text-decoration: underline;
}

.eq-newquote > summary { list-style: none; }

.eq-menuaction:hover { color: var(--eq-link-hover); }
.eq-newquote > summary::-webkit-details-marker { display: none; }
.eq-newquote > summary:hover { color: var(--eq-link-hover); }
.eq-newquote__choice { display: flex; gap: var(--s2); margin: var(--s2) 0 var(--s2); }

/* Open Quotes / Closed Quotes -- the legacy's SubMenu pair (Default.aspx:1944).
   Two real submit buttons, because this page is POST-only. */
.eq-subtabs { display: flex; gap: 2px; margin-bottom: var(--s3); }
.eq-subtab {
    font: inherit; cursor: pointer;
    background: var(--eq-menustrip); color: #fff;
    border: 1px solid var(--eq-grid-line);
    padding: .2rem .9rem; line-height: 1.35;
}
.eq-subtab.is-on {
    background: var(--eq-formbox);
    color: var(--eq-grid-head-ink);
    font-weight: 700;
    text-decoration: underline;
}
.eq-subtab:hover { background: var(--eq-menu-hover); color: var(--eq-menu); }

.eq-quoteshead { margin: 0 0 var(--s2); }
.eq-quotesempty { margin: 0; }

/* ===========================================================================
   19  THE QUOTE WIZARD STEP STRIP, AND REQUIRED FIELDS

   From Tim's screenshots of the legacy quote screens, 25 August 2026.
   =========================================================================== */

/* Square tabs in a row, the legacy's SubMenu (BaseStyle.css:485-575). Wraps
   rather than scrolls: six tabs plus "Client Motivation / Needs Analysis" is a
   wide strip, and a rep must never have to scroll sideways to see which step
   they are on. */
.qsteps { margin: var(--s3) 0 0; }

/* CENTRED. Tim, 27 August 2026: "can you also bring the tabs for creating and
   editing vehicles center [aligned] ... Vehicle Details, Accessories, Financial
   Details, Demo, Finalise".

   justify-content is what does it. Worth knowing that this is a flex container and
   not a text alignment: `text-align: center` on the list does nothing here, which
   is the obvious thing to reach for and the reason this comment exists.

   The strip WRAPS on a narrow window, and centring changes what that looks like --
   a wrapped second row centres under the first rather than starting at the left
   edge. That is the better shape for a step strip, because the strip reads as one
   object rather than as a left-aligned list that happens to be too long. */
.qsteps ol {
    display: flex; flex-wrap: wrap; gap: 2px;
    justify-content: center;
    list-style: none; margin: 0; padding: 0;
}
.qstep {
    display: block;
    background: var(--eq-menu);
    border: 1px solid var(--eq-menu-line);
    color: var(--eq-menu-ink);
    padding: .25rem 1.1rem;
    line-height: 1.35;
    font-size: var(--fs-body);
    white-space: nowrap;
    text-decoration: none;
}
/* THE TAB'S OWN COLOUR HAS TO OUT-SPECIFY THE SKIN'S LINK COLOUR, and this line is
   a bug fix rather than a tidy-up. `.eq-slab a` (section 15) is (0,1,1) and paints
   every content link the legacy blue; plain `.qstep` is only (0,1,0), so a
   REACHABLE tab -- the only kind that is an anchor -- was rendering #034af3 on the
   #465c71 slate. Unreadable, and the same class of mistake as the earlier `.eq-skin
   a` bug: a broad element selector reaching a component that had already set its
   own colour.

   `a.qstep` ties at (0,1,1) and is declared later, so it wins. Caught from Tim's
   screenshot of the accessories step, 25 August 2026. Do not collapse this back
   into the .qstep block above -- it would stop working immediately. */
a.qstep { color: var(--eq-menu-ink); }
a.qstep:hover { background: var(--eq-menu-hover); color: var(--eq-menu); text-decoration: none; }

/* The step you are on. Pale inverse, and NOT a link -- see the partial. */
.qstep--current {
    background: var(--eq-menu-on);
    color: var(--eq-menu);
    font-weight: 700;
}

/* Not reachable yet. Grey, per the legacy's .Disabled. */
.qstep--off {
    background: #dedddd;
    color: #555;
    cursor: help;
}

/* ---------------------------------------------------------------------------
   REQUIRED FIELDS ARE PINK -- a global rule

   Tim, 25 August 2026: "the pink color on a cell indicates that it is a required
   cell. Please read the labels because as an example... category or M and M code
   is required. And if one is populated, it removes the required color of the
   other one."

   The colour is the legacy's own input.Error (BaseStyle.css:369) -- #ffb5b5 with
   a red border -- already a token here because form validation reuses it.

   WHY ONE CLASS AND NOT THE HTML `required` ATTRIBUTE: `required` means "the
   browser will block submit", which is NOT what this is. Half of these are
   EITHER/OR -- Category or M&M Code, one of the two -- and the browser has no
   way to express that. So the pink is a SEPARATE, purely visual signal driven by
   the same rule the server enforces, and `required` is used only where a single
   field genuinely must be filled.

   It is also not an error state. Nothing has gone wrong when the form first
   loads; the pink says "this is needed", not "this is invalid". Same colour as
   the legacy, deliberately -- reps read it as one thing.
   --------------------------------------------------------------------------- */
.is-required,
input.is-required,
select.is-required,
textarea.is-required {
    background: var(--error-bg);
    border-color: var(--bad);
}
/* Focus must still win, or a rep cannot see where the caret is. */
.is-required:focus { border-color: var(--brand); box-shadow: var(--ring); }

/* A CELL can be required too, not only a control -- Tim, 17 September 2026,
   asked for the red on the cell behind the Admin Status and Admin Function
   dropdowns rather than on the dropdowns themselves. A select is a small control
   beside a wide label, and pinking it alone is easy to miss on a long form.

   WHY THIS NEEDS ITS OWN RULE AND THE CLASS ALONE DID NOT WORK. Two lines up the
   detail table says

       .eq-detail > tbody > tr > td { background: none; }

   which is one class and three elements -- more specific than a bare
   `.is-required`. So the class landed on the cell, the DOM was correct, and
   nothing changed on screen. Measured, not guessed: the attributes were all
   present in the served markup while the colour never painted.

   THIS IS THE SECOND TIME THIS SHAPE OF BUG HAS BITTEN on this project. Rule
   5.4f records the first: a row band painted on `<tr>` sits UNDER its cells, so
   any cell fill hides it, and every state that must beat a band therefore paints
   `> td` too. Same lesson from the other direction -- when a table has already
   taken a position on its cell backgrounds, a single-class rule cannot overrule
   it. Match the table's own specificity or it is decoration that does nothing.

   Scoped to `.eq-detail` rather than made global with `!important`: this has to
   beat one known rule, not everything. */
.eq-detail > tbody > tr > td.is-required,
.eq-detail--ruled > tbody > tr > td.is-required {
    background: var(--error-bg);
    border-color: var(--bad);
}

/* The green stock number: this vehicle is brochure-ready. On the CELL, not the
   row -- the legacy reserves the row background for "at another branch", so a
   green row would collide with it.

   PaleGreen, which is the legacy's own colour for the even rows
   (Quote.aspx.vb:2124). It alternates two greens there purely to sit with its
   banding; that is not a second meaning, so one colour is used. Dark ink because
   the cell holds a link and pale green plus link blue is not enough contrast. */
table.grid td.cell-brochure,
table.grid--eq tbody tr td.cell-brochure {
    background: #98fb98;
}
table.grid td.cell-brochure a { color: #14532d; font-weight: 600; }

/* ===========================================================================
   20  THE QUOTE WIZARD STEP -- vehicle details, and the stock-list modal

   From Tim's screenshot of the legacy EQnet/Quote.aspx UsedVehicleDetailsView,
   25 August 2026: "I've asked you to copy the style and flow of the Legacy app
   so that I don't have to retrain."

   HEIGHT IS THE CONSTRAINT HERE, not width. Tim, on an earlier screen: "One of
   the general themes that you have is to extend things vertically, which
   requires users to scroll to be able to interact with the page." The legacy fits
   this whole step -- link, both headings, thirteen controls and the forward
   action -- in about 620px, so every measurement below is chosen to land in the
   same place rather than to look comfortable in isolation.
   =========================================================================== */

/* The step's panel. .eq-card supplies the white sheet and the hairline; this adds
   the legacy fieldset's own padding and centres the panel in the page slab. */
.eq-step {
    padding: var(--s4) var(--s4) var(--s3);
    margin-block: var(--s3) var(--s4);
}

/* --------------------------------------------------------------------------
   SELECT FROM STOCK LIST -- large, bold, centred, underlined.
   (lnkSelectUsedStocklist: Font-Bold="true" Font-Size="Large".)
   -------------------------------------------------------------------------- */
/* THE "SELECT FROM STOCK LIST" LINE. Centred under the step's heading.

   RENAMED FROM .eq-pick ON 27 AUGUST 2026, and the rename IS the fix. Tim asked why
   the link was not centred; the answer was a NAME COLLISION -- the picker TABLES
   (section further down) also declared `.eq-pick`, and their `text-align: left`
   came later in the file and won. Two unrelated components sharing one class name,
   one of them silently overriding the other.

   So this is not "add text-align again": that would have left the collision in
   place for whatever property the two disagree on next. Distinct components get
   distinct names.

   The tables kept .eq-pick because they own the larger family (.eq-pick th, td,
   tbody, --list, --deriv); this line, with two usages, moved. */
/* max-width: none MATTERS AS MUCH AS text-align HERE, and it is the second half of
   the same fix. Both of these are <p>, and `p` carries a 68ch reading measure
   globally (the design brief's rule: widening the frame must never widen the line
   length). Centring inside a 68ch box that starts at the card's left padding puts
   the text left of the card's actual centre -- which is exactly how it looked after
   the rename, and it is the same trap the accessories total line hit on 26 August.
   These are labels in a column, not prose. */
.eq-picklink {
    text-align: center;
    max-width: none;
    margin: var(--s4) 0 var(--s2);
}

.eq-biglink {
    font-size: 1.15rem;
    font-weight: 700;
    text-decoration: underline;
}

/* The stock number that was picked, under the link (lblSelectedStockNoFromList). */
.eq-picked { text-align: center; max-width: none; margin: 0 0 var(--s2); }

/* "OR" and "ENTER DETAILS BELOW" are both .Head2 in the legacy, one above the
   other with two <br /> between them. .eq-h3 carries the small-caps underline;
   these only set the rhythm -- and set it TIGHT, because the legacy's four line
   breaks here are the single biggest waste of height on the screen. */
.eq-or { margin-top: var(--s4); }
.eq-step .eq-h3 { margin-block: .15rem; }

/* --------------------------------------------------------------------------
   THE FIELD TABLE

   A GRID, NOT A <TABLE>, and the difference is only semantic: this is a form
   layout, not tabular data, and a screen reader should read a label and its box
   rather than announce a table of thirteen rows. The look is the legacy's --
   labels right-aligned hard against their boxes, two label/box pairs per row,
   the whole block centred.

   justify-content: center is what centres it. The columns are sized to their
   content, so the block is exactly as wide as the widest label plus the widest
   box -- which is how the legacy's align="center" table behaves.
   -------------------------------------------------------------------------- */
.eq-fields {
    display: grid;
    grid-template-columns: max-content auto max-content auto;
    justify-content: center;
    align-items: center;
    column-gap: .4rem;
    row-gap: .2rem;                 /* tight: see the height note at the top */
    margin: var(--s3) 0 var(--s4);
}

.eq-fields > label {
    text-align: right;
    white-space: nowrap;
    font-weight: 400;               /* the legacy's labels are plain text */
    margin: 0;
    padding: 0;
}

/* Every control sits in a wrapper div so the wrapper can hold a hidden field or
   the lookup link beside it. The width lives on the control. */
.eq-fields input[type="text"],
.eq-fields input:not([type]),
.eq-fields select {
    width: 11rem;
}

/* Vehicle Derivative is the long one -- the legacy sets size="42" on it. */
.eq-fields__wide { width: 21rem !important; }

/* Money reads right-aligned. Matches the legacy's NumberTextEntry. */
.eq-fields input.num { text-align: right; }

/* The M&M box and its [Lookup M&M Code] link on one line. */
.eq-fields__pair { display: flex; align-items: center; gap: .4rem; }
.eq-fields__aside { white-space: nowrap; font-size: var(--fs-small); }

/* The legacy leaves the right-hand half of the Colour row empty. This holds the
   row open without a stray control in it. */
.eq-fields__gap { min-height: 1px; }

/* Checkboxes are not 11rem wide. */
.eq-fields input[type="checkbox"] { width: auto; }

/* One pair per row once two will not fit. NOT a scroll: a rep on a laptop must
   never have to scroll sideways to reach the price box. */
@media (max-width: 760px) {
    .eq-fields { grid-template-columns: max-content auto; }
    .eq-fields__wide { width: 11rem !important; }
    .eq-fields__gap { display: none; }
}

/* --------------------------------------------------------------------------
   THE FORWARD ACTION -- green arrow above its own text link, centred.

   The arrow's colours are tokens rather than literals in the SVG so that the one
   inline path can be reused by every step, and so a theme cannot leave a green
   arrow on a green ground.
   -------------------------------------------------------------------------- */
.eq-forward { text-align: center; margin-top: var(--s4); }

/* A button that looks like the legacy's arrow-plus-link, because both parts ARE
   one control there. No pill, no fill: this is the legacy's shape. */
.eq-forward__btn {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: .1rem;
    background: none;
    border: 0;
    padding: .2rem .6rem;
    cursor: pointer;
    font: inherit;
    color: var(--eq-link);
}
.eq-forward__label { text-decoration: underline; }
.eq-forward__btn:hover .eq-forward__label { color: var(--eq-link-hover); }
.eq-forward__btn:focus-visible {
    outline: 2px solid var(--brand-deep);
    outline-offset: 2px;
}
/* While it is submitting, markBusy replaces the contents with a spinner and a
   label -- so the row must not collapse to nothing. */
.eq-forward__btn[data-busy="true"] { flex-direction: row; gap: .4rem; }

/* --------------------------------------------------------------------------
   THE STOCK-LIST MODAL

   A real <dialog>. Rendered with `open` by the server, so with script blocked it
   is an ordinary block in the page and the whole screen still works; script
   upgrades it with showModal(), which brings the backdrop, Escape and the focus
   trap from the platform instead of from a 1,123-control toolkit.

   TWO POSITIONS, ON PURPOSE. The UA stylesheet positions an open dialog
   absolutely, which would float it over the form when script is off, so the
   non-modal case is forced back into the flow and only :modal is fixed.
   -------------------------------------------------------------------------- */
.eq-modal {
    position: static;
    width: min(100%, 1500px);
    margin: var(--s4) auto;
    padding: 0;
    background: var(--eq-page);
    color: var(--eq-body-ink);
    border: 1px solid var(--eq-page-line);
    box-shadow: 0 6px 24px rgba(0, 0, 0, .25);
}

.eq-modal:modal {
    position: fixed;
    max-width: min(100%, 1500px);
    max-height: 92dvh;
    overflow: hidden;               /* the body scrolls, not the frame */
    display: flex;
    flex-direction: column;
}

.eq-modal::backdrop { background: rgba(0, 0, 0, .5); }

/* The header bar: slate, like the legacy's PopUpheader. */
.eq-modal__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    background: var(--eq-menu);
    color: var(--eq-menu-ink);
    padding: .3rem .6rem;
}

.eq-modal__title {
    font-variant: small-caps;
    font-size: 1.05rem;
    font-weight: 700;
    margin: 0;
    color: inherit;
}

/* The X. A link, so it works without script -- see the partial. Sized to a
   comfortable pointer target without adding height to the bar. */


.eq-modal__body {
    padding: var(--s3);
    overflow: auto;                 /* the scrolling region when modal */
}

/* The filter row: one line, wrapping only when it must. */
.eq-stockfilter {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem;
    margin-bottom: var(--s2);
}
.eq-stockfilter > label { margin: 0; white-space: nowrap; font-weight: 400; }
.eq-stockfilter__colon { margin-left: -.2rem; }

/* ONE LINE MEANS THE CONTROLS SIZE TO THEMSELVES. The application's form rule
   sets `select` and `input[type="text"]` to width:100%, and a flex item that
   asks for the whole line gets one -- so every drop-down in this row broke onto
   its own line with its label stacked above it, which is the opposite of the
   comment on .eq-stockfilter above. Found by screenshot on the Supplier List's
   Seller Type selector (23 September 2026) and MEASURED, not read: the winning
   rule was the global `select { width: 100% }`. The Stock List's Vehicle
   Category and Search drop-downs were in the same state and are fixed by the
   same line. The text boxes in these rows only escaped it by accident -- they
   carry no type attribute, so `input[type="text"]` never matched them.

   QUALIFIED WITH THE ELEMENT, rule 5.4n: `input[type="text"]` is (0,1,1), so a
   bare class would lose to it wherever it sat. `.eq-stockfilter input[type=
   "text"]` is (0,2,1) and wins honestly. */
.eq-stockfilter select,
.eq-stockfilter input[type="text"],
.eq-stockfilter input[type="search"] { width: auto; }

/* The M&M notice. Centred, small, and NOT red: it is a standing condition of the
   screen rather than something that has gone wrong, and painting it as an error
   would make a rep hunt for a mistake they have not made. */
.eq-modal__notice {
    text-align: center;
    font-size: var(--fs-small);
    line-height: 1.3;
    margin: var(--s2) 0;
    color: var(--muted);
}

/* The grid scrolls inside the dialog, as the legacy's does at max-height 400px.
   dvh rather than px so a big monitor shows more rows -- the whole point of the
   full-viewport rule. */
.eq-modal__grid { max-height: 52dvh; }

.eq-modal__count { text-align: right; font-size: var(--fs-small); color: var(--muted); margin: .3rem 0 0; }
.eq-modal__footnote { margin-top: var(--s2); }

/* ===========================================================================
   21  THE ACCESSORIES STEP

   From Tim's screenshot of the legacy ExtrasView, 25 August 2026: "please match
   design of legacy". ONE editable grid with the add row as its last line, a copy
   box centred above it, and the catalogue behind a link.

   The measurements are taken from the legacy, which fits eleven columns and four
   rows into about 300px of height. Every value below is chosen to land in the
   same place rather than to look comfortable on its own.
   =========================================================================== */

/* Eleven columns of boxes need more room than a form column. Still a measure and
   not the full viewport: beyond about this width the money columns drift so far
   from the accessory name that the eye loses the row. */
.eq-column--wide { width: min(100%, 1280px); margin-inline: auto; }

/* "Copy accessories from quote ID:" -- centred, one line, at the top. */
.eq-copyrow {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: .4rem;
    margin: 0 0 var(--s3);
}
.eq-copyrow > label { margin: 0; font-weight: 400; white-space: nowrap; }

/* "Select from Accessory List" -- left-aligned, as the legacy has it. */
.eq-pickrow { margin: 0 0 .2rem; }

/* The instruction under it. Small and quiet: it is a standing explanation, not a
   message about anything that has happened. */
.eq-hint {
    font-size: var(--fs-small);
    color: var(--muted);
    margin: 0 0 var(--s2);
    line-height: 1.35;
}

/* --------------------------------------------------------------------------
   THE GRID

   A modifier on .grid--eq, so it keeps the legacy skin and the tight rows and
   only adds what an EDITABLE grid needs. Inputs inside a dense grid are the whole
   problem here: an unstyled input carries its own padding and line-height and
   would double the row height on its own.
   -------------------------------------------------------------------------- */
table.grid--acc { width: 100%; }

/* Cells hold controls, so their own padding drops to almost nothing and the
   control provides the height. */
table.grid--acc tbody td { padding: .1rem .25rem; }

/* Two-line headers: keep them tight and centred, and let them wrap on the break
   rather than stretching the row. */
table.grid--acc thead th {
    white-space: normal;
    line-height: 1.15;
    padding: .2rem .3rem;
    vertical-align: bottom;
}

/* Every box in the grid is small, flush and full-width in its cell. .22rem of
   padding against the form default's .45rem, and no margin at all. */
table.grid--acc input[type="text"],
table.grid--acc input:not([type]) {
    padding: .1rem .3rem;
    line-height: 1.3;
    margin: 0;
    width: 100%;
    min-height: 0;
}

.grid--acc .acc-name { width: 100%; }

/* Money boxes are right-aligned and sized to hold a six-figure amount. */
.grid--acc .acc-money { text-align: right; width: 6.5rem; }

/* The narrow columns, sized as the legacy sizes them (30px, 30px, 50px, 60px,
   40px). ch units rather than px so they follow the font. */
.grid--acc .acc-num  { width: 2.2rem; text-align: right; }
.grid--acc .acc-x    { width: 1.8rem; text-align: center; }
.grid--acc .acc-nc   { width: 3.4rem; text-align: center; white-space: nowrap; }
.grid--acc .acc-opt  { width: 3.6rem; text-align: center; }
.grid--acc .acc-end  { width: 2.8rem; text-align: center; }

/* X, N/C and STD are submit buttons that must read as the legacy's links -- they
   have to submit, because each one carries the whole grid with it. .textlink
   already strips the button chrome; this only removes the padding a dense row
   cannot spare. */
.grid--acc .acc-link { padding: 0; font-size: var(--fs-small); }

/* THE VEHICLE'S ACCESSORY GRIDS (Admin > vehicle > Accessories). The name and
   supplier boxes get the legacy's own 200px (.aspx:11183, :11214): left to the
   table they shrank to a third of that and cut "IMPAC BRONZE WARRANTY" to
   "IMPAC BRONZE W". min-width, not width, so the global input width:100% rule
   is not fought -- and qualified with the element anyway (rule 5.4n). */
table.grid--vehacc td.grid__name input,
table.grid--vehacc td.acc-supplier input { min-width: 200px; }

/* THE CHECKLIST'S SELECT-ALL (vehicle > Recon Checklist). Shown only once
   app.js section 34 has wired it; stated here because a label rule elsewhere
   can outrank the browser's own [hidden], exactly as .alert and .btn needed. */
.eq-checkall[hidden] { display: none; }
.eq-checkall { display: inline-flex; align-items: center; gap: .35rem; margin: 0; }

/* The padlock. A glyph rather than the legacy's 18px lock.png: one fewer request,
   and it takes the row's own colour. */
.acc-lock { font-size: .95rem; line-height: 1; }

/* The add row. Faintly separated from the lines above it, because it is not a
   line on the quote yet -- the legacy achieves the same by making it a second
   table butted against the grid. */
table.grid--acc tbody tr.acc-add > td {
    background: var(--surface);
    border-top: 2px solid var(--eq-grid-line);
}

/* ADD, in capitals and narrow, as the legacy has it (38px wide). */
.grid--acc .acc-add-btn {
    padding: .1rem .35rem;
    font-size: var(--fs-eyebrow);
    letter-spacing: .04em;
}

/* The total line. NOT on the legacy screen -- see the note in the view -- so it
   is deliberately quiet: a footer rule, not a banner. */
table.grid--acc tfoot td {
    padding: .25rem .3rem;
    border-top: 2px solid var(--eq-grid-line);
    background: var(--surface);
}
.grid--acc .acc-total-label { text-align: right; font-weight: 700; }
.grid--acc .acc-total-note { font-size: var(--fs-small); color: var(--muted); }

/* A price that cannot be read. Marked in the box itself, because the box is what
   the rep has to correct. */
.grid--acc input.is-corrupt {
    background: var(--error-bg);
    border-color: var(--bad);
}

/* The accessory list is a shorter dialog than the stock list -- five columns, not
   thirteen. */
.eq-modal--narrow { width: min(100%, 900px); }
.eq-modal--narrow.eq-modal:modal { max-width: min(100%, 900px); }

/* ===========================================================================
   22  THE STATUS CELL ON OPEN QUOTES, AND ITS REMOVE CONTROL

   Tim, 25 August 2026: "give me a button to the right hand side of in progress.
   remove" -- and then "Please put a tooltip over the remove button and indicate
   that once removed, it cannot be restored."

   ONLY ON A QUOTE IN PROGRESS. A finalised quote is CLOSED through the legacy
   process and can never be removed; that is enforced in the handler, not here.
   =========================================================================== */

/* The pill and its step on the left, the action on the right, on one line. The
   action is pushed to the right edge of the cell so it is in a predictable place
   on every row rather than trailing after text of varying length. */
.statuscell {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
}

/* Quiet until wanted. A destructive action should not be the loudest thing in a
   grid of forty rows -- it is there when a rep looks for it, and it does not
   compete with the status pill, which is what the column is for.

   NOT hidden until hover: an action a rep cannot see is an action they cannot
   find, and hover-only controls are unreachable on a touch screen. */
.statuscell__remove {
    font-size: var(--fs-small);
    color: var(--muted);
    white-space: nowrap;
}
.statuscell__remove:hover { color: var(--bad); }

/* ---------------------------------------------------------------------------
   THE ACTION AREA RESERVES ITS WIDTH IN BOTH STATES

   Tim, 26 August 2026: "i would prefer that the test sits above the button so
   that it does not move anything else on that screen."

   Confirming used to lay the prompt, the button and Cancel out on ONE LINE. That
   made the cell wider than the "Remove" link it replaced, so the table's column
   widths were recomputed and EVERY column shifted left -- plainly visible with
   his two screenshots side by side.

   The fix is two things, and one without the other does not work:

     1. The confirm STACKS -- prompt above the buttons -- which is what Tim asked
        for and what keeps the row two lines tall, the same as the pill and its
        step beside it. So the row does not get taller either.
     2. This wrapper reserves the SAME WIDTH whichever state is showing. Stacking
        alone would still widen the cell, because "Yes, remove" next to "Cancel"
        is wider than the word "Remove".

   11rem is measured against the widest state (the button plus its gap plus
   Cancel), with a little slack. It is a min-width rather than a width so a
   longer translation grows the box rather than clipping it -- at which point the
   grid would shift, which is the thing being prevented, so the value and the
   content are related and neither should be changed alone.

   Right-aligned so the "Remove" link stays exactly where it was: the box grows
   leftward into space the cell already had.
   --------------------------------------------------------------------------- */
.statuscell__action {
    flex: none;
    min-width: 11rem;
    display: flex;
    justify-content: flex-end;
}

/* The confirming state. Red, because at this point the next click deletes. */
.statuscell__confirm {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: .15rem;
    white-space: nowrap;
}

/* The two choices, side by side under the prompt. */
.statuscell__choices {
    display: flex;
    align-items: center;
    gap: .4rem;
}

.statuscell__ask { font-size: var(--fs-small); color: var(--bad); font-weight: 700; }

/* THE ONE PLACE IN THIS APPLICATION WITH A RED BUTTON, and it is deliberate: it
   is the only irreversible action a salesperson can take. If a second one appears
   somewhere, check that it really is irreversible before reusing this. */
.btn--danger {
    background: var(--bad);
    border-color: var(--bad);
    color: #fff;
}
.btn--danger:hover {
    /* Darker rather than lighter on hover, so the control never looks disabled at
       the moment it is about to be pressed. #990000 on white measures 8.2:1. */
    background: #990000;
    border-color: #990000;
    color: #fff;
}
.btn--danger:focus-visible { outline: 2px solid var(--bad); outline-offset: 2px; }

/* At narrow widths the pill and the action stack rather than squeezing. */
@media (max-width: 900px) {
    .statuscell { flex-direction: column; align-items: flex-start; gap: .2rem; }

    /* The reserved width is released here: the cell already stacks, so there is no
       column to keep still, and holding 11rem would squeeze the vehicle name. */
    .statuscell__action { min-width: 0; justify-content: flex-start; }
    .statuscell__confirm { align-items: flex-start; }
}

/* ===========================================================================
   23  THE FINANCIAL STEP -- three money columns

   The legacy's FinancialDetailsView, which is the widest and densest screen in
   the wizard: the allowances a rep enters on the left, the purchase-price
   build-up in the middle, the Summary on the right, and Principal debt at the
   bottom of it.

   THREE COLUMNS SIDE BY SIDE IS THE WHOLE POINT of the layout. A rep changes a
   discount on the left and watches the middle and the right move; stacked, that
   relationship is invisible and the screen becomes three screens.
   =========================================================================== */

.eq-money3 {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--s5);
    align-items: start;
}

/* The vehicle name above the columns, underlined, as the legacy heads its middle
   column (lblFinanceVehicleName). */
.eq-money__vehicle {
    text-align: center;
    text-decoration: underline;
    margin: 0 0 var(--s3);
}

/* Each column's own heading. Small-caps grey like the rest of the legacy skin,
   but NOT underlined -- the page and the panel already carry underlines, and a
   third would turn the screen into rules. */
.eq-money__head {
    font-variant: small-caps;
    font-weight: 200;
    font-size: 1.0625rem;
    color: var(--eq-heading);
    margin: 0 0 var(--s2);
    padding-bottom: .15rem;
    border-bottom: 1px solid var(--eq-page-line);
}

/* ---------------------------------------------------------------------------
   A MONEY ROW: label left, amount right, on one line.

   The amounts line up in a column because they are all in the same grid track,
   which is what makes a build-up readable as arithmetic. tabular-nums keeps the
   digits in step so the decimal points sit under each other even in a
   proportional font.
   --------------------------------------------------------------------------- */
.eq-money__row {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--s3);
    align-items: baseline;
    padding: .12rem 0;                  /* tight: the legacy fits ~20 rows */
    line-height: 1.35;
}

.eq-money__row > .num {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* A sub-total. Ruled above, so the eye can see what was added. */
.eq-money__row--sub {
    border-top: 1px solid var(--border);
    margin-top: .1rem;
    font-weight: 600;
}

/* A total. Ruled and bold. */
.eq-money__row--total {
    border-top: 2px solid var(--eq-grid-line);
    margin-top: .2rem;
    font-weight: 700;
}

/* PRINCIPAL DEBT -- the one number this whole screen exists to produce. It is
   what the customer finances and what every instalment on the documents is
   calculated from, so it is the one figure allowed to be louder than the rest. */
.eq-money__row--headline {
    font-size: 1.0625rem;
    background: var(--eq-grid-row);
    padding: .25rem .35rem;
    margin-inline: -.35rem;
}

/* The max-discount breakdown. The legacy prints these in red, and it is not an
   error state -- it is the ceiling a rep is working against, which is exactly
   what they need in view while they discount. Kept red for that reason and
   because a rep of twenty years reads red here as "the limit". */
.eq-money__row--note {
    color: var(--bad);
    font-size: var(--fs-small);
}

/* Over the maximum: a manager's approval is needed. Louder than the breakdown
   above it, because this one is a refusal rather than a limit. */
.eq-money__over {
    background: var(--error-bg);
    color: var(--error-ink);
    font-weight: 600;
    padding: .3rem .4rem;
    margin: .3rem 0;
    font-size: var(--fs-small);
}

/* A divider between groups inside a column. */
.eq-money__rule {
    border-top: 1px solid var(--eq-page-line);
    margin: var(--s3) 0 var(--s2);
    opacity: .5;
}

/* The note about what is not yet editable. Centred under the columns, quiet. */
.eq-money__pass {
    text-align: center;
    margin-top: var(--s4);
    max-width: none;
}

/* Below about 1100px three money columns stop being readable -- the labels wrap
   to three lines each and the arithmetic disappears. Two, then one. */
@media (max-width: 1100px) {
    .eq-money3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
    .eq-money3 { grid-template-columns: 1fr; }
}

/* ===========================================================================
   24  THE FINANCIAL STEP'S BOXES

   The legacy builds each column out of small bordered tables -- a 1pt #808080
   frame round each group -- and the grouping is information, not decoration: it
   says which figures belong to each other. Max Discount is one box, the red
   breakdown is another, the allowances another.

   Reproduced with .eq-box, which is the frame, and .eq-box__row, which is a
   label/value line inside it.
   =========================================================================== */

.eq-box {
    border: 1px solid #808080;          /* the legacy's own 1pt #808080 */
    margin-bottom: var(--s3);
    background: var(--surface);
}

/* A row: label left, figure right. The figures line up in one track, which is
   what lets a build-up read as arithmetic. */
.eq-box__row {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--s2);
    align-items: center;
    padding: .18rem .4rem;
    line-height: 1.35;
}

.eq-box__row > .num,
.eq-box__row > .num strong { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Labels inside a box are plain text, not form-bold. */
.eq-box__row > label { margin: 0; font-weight: 400; }

/* The head row of a box -- Max Discount, or the vehicle name. Tinted, as the
   legacy tints its first row. */
.eq-box__row--head {
    background: var(--eq-grid-row);
    border-bottom: 1px solid var(--eq-grid-line);
}

/* "Trade-in :" and "Summary" head their boxes, bold and underlined, which is
   exactly how the legacy writes them (style="font-weight:bold;
   text-decoration:underline"). */
.eq-box__row--boxhead {
    font-weight: 700;
    text-decoration: underline;
}

/* The vehicle name at the head of the middle column. */
.eq-box__vehicle { font-weight: 700; text-decoration: underline; }

/* A sub-total: ruled above, bold figure, and NO LABEL -- the legacy leaves that
   cell empty and lets the rule do the work. */
.eq-box__row--sub { border-top: 1px solid var(--ink); }

/* A total: ruled above, bold. */
.eq-box__row--total {
    border-top: 1px solid var(--ink);
    font-weight: 700;
}

/* "- Allowance Allocated" is a sub-item of the deposit line above it. The legacy
   indents it with twenty-two &nbsp;, which is 22 spaces of whatever font happens
   to be loaded; a padding does the same job predictably. */
.eq-box__row--indent > span:first-child { padding-left: 1.6rem; }

/* Factory assistance rows carry TWO figures, Excl and Incl. */
.eq-box__row--fa { grid-template-columns: 1fr auto auto; }

/* ---------------------------------------------------------------------------
   THE RED BREAKDOWN

   Six cells per row: label, "R", amount, "is", percentage, "of vehicle". A grid
   rather than a table because it is a sentence broken into aligned parts, not
   tabular data -- and the alignment is what makes the three rows comparable.
   --------------------------------------------------------------------------- */
.eq-box__disc {
    display: grid;
    grid-template-columns: auto auto 1fr auto auto auto;
    gap: .1rem .3rem;
    align-items: baseline;
    padding: .25rem .4rem;
}

/* Red, and NOT an error state: it is the ceiling a rep is working against, which
   is what they need in view while they discount. Kept red because a rep of
   twenty years reads red here as "the limit". */
.eq-box--red { color: var(--bad); }
.eq-box--red .eq-box__disc > span:nth-child(6n+3),
.eq-box--red .eq-box__disc > span:nth-child(6n+5) { font-weight: 700; }

/* ---------------------------------------------------------------------------
   INPUTS INSIDE A BOX

   Small, flush, and right-aligned for money -- the legacy's boxes are about 60px
   wide and the whole left column is only 400px.
   --------------------------------------------------------------------------- */
.eq-box input[type="text"],
.eq-box input:not([type]) {
    padding: .1rem .3rem;
    line-height: 1.3;
    margin: 0;
    min-height: 0;
    width: 5.5rem;
    text-align: right;
}

.eq-box input[type="checkbox"] { width: auto; }

/* The Discount Offered row is label / box / caption / percent box / "%". */
.eq-box__row:has(.eq-box__caption) { grid-template-columns: auto auto auto auto auto; }
.eq-box__caption { font-size: var(--fs-small); white-space: nowrap; padding-inline: .2rem; }
.eq-box__perc { width: 3.5rem !important; }

/* An inert control -- one that belongs on the screen but is not built yet. Shown
   rather than hidden, because the SHAPE of the screen is what a rep navigates
   by; the cursor and the tooltip say it does nothing. */
.eq-money__inert { cursor: help; text-decoration: underline dotted; }

/* ADD TRADE-IN and the other capitalised legacy links. */
.eq-caps { font-weight: 600; }

/* ===========================================================================
   25  THE DEMO DETAILS STEP

   Two questions, a YES and a NO button each, and the forward arrow. The whole
   legacy screen is three rows and a rep answers it in two clicks -- so the only
   job here is to keep it that short.
   =========================================================================== */

.eq-demo__row {
    display: grid;
    grid-template-columns: minmax(0, 22rem) auto auto 1fr;
    align-items: center;
    gap: var(--s3);
    padding: var(--s3) .5rem;
}

/* The question. Small-caps, as the legacy renders it -- it is an <h5><strong>
   inside a table cell, which is why it reads heavier than body text. */
.eq-demo__q {
    font-variant: small-caps;
    font-weight: 600;
    color: var(--eq-heading);
    line-height: 1.3;
}

/* ---------------------------------------------------------------------------
   THE YES / NO BUTTONS

   Deliberately NOT .btn. These are the legacy's plain 50px push buttons, and
   their whole job is to carry a background colour that means an answer -- a
   pill-shaped brand button would lose that.
   --------------------------------------------------------------------------- */
.eq-demo__btn {
    width: 3.4rem;                      /* the legacy's Width="50px" */
    padding: .15rem 0;
    font: inherit;
    font-size: var(--fs-small);
    text-align: center;
    cursor: pointer;
    color: var(--ink);
    background: #efefef;                /* an unanswered button, browser default */
    border: 1px solid #adadad;
}

.eq-demo__btn:hover { background: #e2e2e2; }
.eq-demo__btn:focus-visible { outline: 2px solid var(--brand-deep); outline-offset: 1px; }

/* THE TWO ANSWER COLOURS ARE THE LEGACY'S OWN, and they are not free to change:
   Quote.aspx.vb:10097 sets Drawing.Color.GreenYellow on a chosen YES and
   :10104 sets Drawing.Color.LightPink on a chosen NO. A rep of twenty years
   reads those two shades as the answer.

   Dark ink on both, deliberately: they are pale fixed tints and a dark-theme
   foreground on them would be unreadable. */
.eq-demo__btn.is-yes { background: #ADFF2F; border-color: #7ac70c; font-weight: 700; color: #111; }
.eq-demo__btn.is-no  { background: #FFB6C1; border-color: #e08894; font-weight: 700; color: #111; }

/* THE UNCHOSEN BUTTON IS NOT DISABLED. The legacy clears its colour
   (Color.Empty) and leaves it clickable, so a rep can change their mind. Stated
   because the screenshot's pale NO invites the opposite conclusion. */

.eq-demo__note { margin: var(--s2) .5rem 0; }

/* One question per line on a narrow window, rather than the label squeezing. */
@media (max-width: 620px) {
    .eq-demo__row { grid-template-columns: 1fr auto auto; }
    .eq-demo__q { grid-column: 1 / -1; }
}

/* ===========================================================================
   26  THE FINALISE STEP

   Sections under <h3> headings, in the legacy's order: Quote Origin, Submit
   Quote to F&I, the documentation branch, the comments the customer sees, and
   the FINALISE button.
   =========================================================================== */

.eq-fin { margin-bottom: var(--s5); }

/* The legacy's <h3>. Left-aligned and plain -- these are section labels inside a
   panel, not page titles, so they do not take the centred underline .eq-h2 has. */
.eq-fin__head {
    font-variant: small-caps;
    font-size: 1.0625rem;
    font-weight: 700;
    color: var(--eq-heading);
    margin: 0 0 var(--s2);
}

/* A label-and-control line. */
.eq-fin__row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s2);
}
.eq-fin__row > label { margin: 0; font-weight: 400; }

/* ---------------------------------------------------------------------------
   QUOTE ORIGIN -- the red radio group

   RED LABELS, NOT PINK BOXES. Exactly one of the group must be chosen, and that
   is a different instruction from "fill in this field" -- see 1a.17 in the Web
   Design Brief. The legacy wraps the whole list in .ErrorText, which is the same
   decision made the same way.

   THE COLOUR IS NOT AN ERROR STATE. Nothing has gone wrong when the screen
   loads; red here means "pick one", which is why it is present from the first
   render rather than appearing after a failed submit.

   The legacy floats 150px list items, which lands at five per row at this width.
   auto-fill reproduces that and adapts instead of overflowing.
   --------------------------------------------------------------------------- */
.eq-fin__origins {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: .15rem var(--s3);
}

.eq-fin__origin {
    display: flex;
    align-items: baseline;
    gap: .35rem;
    white-space: nowrap;
    line-height: 1.35;                  /* the legacy's own 1.35em */
}

.eq-fin__origin > label {
    margin: 0;
    color: var(--bad);
    font-weight: 700;
    cursor: pointer;
}

/* The chosen one stops shouting: once an answer exists the group is satisfied,
   so the red has done its job on that row. The others stay red because they are
   still what a rep would switch to.

   :has() is used rather than a server-rendered class so the change is instant on
   click -- the same reasoning as the live VAT pairs. */
.eq-fin__origin:has(input:checked) > label {
    color: var(--eq-body-ink);
    text-decoration: underline;
}

/* ---------------------------------------------------------------------------
   COMMENTS -- the customer sees this
   --------------------------------------------------------------------------- */
.eq-fin__comments {
    width: 100%;
    max-width: 52rem;                   /* the legacy's Columns="60" */
    line-height: 1.4;
    resize: vertical;
}

/* ---------------------------------------------------------------------------
   FINALISE

   Centred, as the legacy centres it, and the only place in the wizard where the
   action gets a full button rather than the green arrow -- because it is not a
   step forward, it is the end.
   --------------------------------------------------------------------------- */
.eq-fin__finish { text-align: center; margin-top: var(--s5); }

.eq-fin__btn {
    letter-spacing: .06em;
    padding-inline: var(--s5);
}

.eq-fin__finish .note { margin: var(--s2) auto 0; }

/* ===========================================================================
   27  THE QUOTE CLOSURE PANEL

   Reproduces the closure fieldset of EQnet/QuoteView.ascx (:3122-3517): a
   legended box, a radio list of the dealership's options, and one sub-form per
   option.

   THE OPTIONS ARE NOT RED. The Quote Origin group in section 26 is, because one
   of it is required. This group is not, because the legacy does not colour it
   either -- and a rep of twenty years should not have to relearn what red means
   on a screen they use daily.
   =========================================================================== */

/* The legacy's fieldset is a fixed 600px centred. Widened to the reading measure
   used elsewhere on this page, because the panes hold 60-column textareas that a
   600px box makes scroll horizontally. */
.eq-close {
    border: 1px solid var(--eq-formbox-line);
    border-radius: var(--radius);
    background: var(--eq-formbox);
    padding: var(--s3) var(--s4) var(--s4);
    margin: var(--s5) 0 0;
    max-width: 56rem;
    margin-inline: auto;
}

.eq-close > legend {
    font-variant: small-caps;
    font-weight: 700;
    letter-spacing: .04em;
    color: var(--eq-heading);
    padding-inline: var(--s2);
}

/* ---------------------------------------------------------------------------
   THE OPTION LIST

   The legacy floats 150px list items inside a 600px panel, giving four per row.
   auto-fill at the same 150px reproduces that and reflows instead of overflowing
   when a franchise has two options rather than eight.
   --------------------------------------------------------------------------- */
.eq-close__options {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: .2rem var(--s3);
    margin-bottom: var(--s3);
    padding-bottom: var(--s3);
    border-bottom: 1px solid var(--eq-formbox-line);
}

.eq-close__option {
    display: flex;
    align-items: baseline;
    gap: .35rem;
    white-space: nowrap;
    line-height: 1.35;                  /* the legacy's own 1.35em */
}

.eq-close__option > label {
    margin: 0;
    font-weight: 400;
    cursor: pointer;
}

/* The chosen option is emphasised so it stays findable once the pane below has
   scrolled the list off the top of a small window. :has() rather than a
   server-rendered class, so it lands on click with no round trip. */
.eq-close__option:has(input:checked) > label {
    font-weight: 700;
    color: var(--eq-heading);
}

/* ---------------------------------------------------------------------------
   THE PANES

   All rendered, one shown. Script sets `hidden` -- see _QuoteClosure.cshtml for
   why that is done in script rather than in markup: without script every pane
   stays visible under its own heading, which is usable, where a CSS-hidden pane
   would be unreachable.
   --------------------------------------------------------------------------- */
.eq-close__pane[hidden] { display: none; }

.eq-close__head {
    font-variant: small-caps;
    font-size: 1.0625rem;
    font-weight: 700;
    color: var(--eq-heading);
    text-align: center;
    margin: 0 0 var(--s3);
}

/* The legacy's instruction lines ("Please enter the competitor Brand and
   Model."), centred as it centres them. */
.eq-close__ask {
    text-align: center;
    margin: var(--s3) 0 var(--s1);
}

.eq-close__ask--warn { color: var(--bad); font-weight: 700; }

.eq-close__or { text-align: center; margin: var(--s2) 0; }

/* Textareas are Columns="60" Rows="5" in the legacy. 52rem is that measure in a
   proportional face; vertical resize only, so a drag cannot break the column. */
.eq-close__text {
    display: block;
    width: 100%;
    max-width: 52rem;
    margin-inline: auto;
    line-height: 1.4;
    resize: vertical;
}

.eq-close__in {
    display: block;
    width: 100%;
    max-width: 26rem;
    margin-inline: auto;
}

/* A date is ten characters. Sizing it to its content stops a lone narrow value
   sitting in the middle of a 26rem box looking unfinished. */
.eq-close__in--date { max-width: 12rem; text-align: center; }

.eq-close__sel { display: block; margin-inline: auto; }

/* The financial reason list: size="5", so all five are visible at once as the
   legacy shows them. Sized to its longest option rather than the container, so it
   reads as a list of choices and not as a panel. */
.eq-close__list {
    display: block;
    margin-inline: auto;
    min-width: 18rem;
}

/* The live character count for the long-reason rule. Small and quiet -- it is
   feedback, not an instruction. */
.eq-close__count {
    text-align: center;
    font-size: .8125rem;
    color: var(--muted);
    margin: var(--s1) 0 0;
}

/* Over the minimum: the count stops being a warning and becomes a confirmation. */
.eq-close__count.is-ok { color: var(--good); }

.eq-close__errors { margin: 0; padding-left: 1.1rem; }

.eq-close__submit { text-align: center; margin-top: var(--s4); }
.eq-close__submit .note { margin: var(--s2) auto 0; }

/* ===========================================================================
   28  THE VEHICLE-DETAILS ACCORDION (new vehicle: Brand / Model / Derivative)

   Reproduces the three-pane accordion from Tim's screenshots of 26 August 2026
   (EQnet/Quote.aspx NewVehicleDetailsView, an AjaxControlToolkit Accordion).
   His note: "the official design for new and used vehicles is essentially
   identical with the exception of selecting which make and model."

   ALL THREE HEADERS ARE ALWAYS PRESENT, ONE BODY IS RENDERED. The headers are
   what makes the screen recognisable and show the rep what they have chosen; a
   closed one costs a line of markup instead of a grid. The legacy renders all
   three grids plus ViewState for all three on every postback.

   ---------------------------------------------------------------------------
   HOW THIS WENT WRONG FIRST TIME, because it is worth not repeating
   ---------------------------------------------------------------------------
   The first version of this section referenced invented token names --
   --eq-chrome, --eq-panel, --eq-rule, --eq-surface -- none of which exist in
   this file. CSS fails SILENTLY on an unknown custom property, so every bar
   rendered with no background, no border and no padding: three lines of grey
   text where the legacy has three blue bars. It compiled, the tests passed, and
   Tim found it in a screenshot.

   Every colour below is a token defined at the top of this file. If a new one is
   needed, DEFINE IT THERE rather than referencing a plausible-sounding name.
   =========================================================================== */

.eq-acc {
    margin: var(--s4) 0 var(--s2);
    display: flex;
    flex-direction: column;
    gap: var(--s2);
}

/* ---------------------------------------------------------------------------
   THE HEADERS

   A vertical gradient with a small-caps label and, when closed, the chosen value
   in a noticeably larger face -- as the legacy renders them.
   --------------------------------------------------------------------------- */
.eq-acc__head {
    display: flex;
    align-items: center;
    gap: .45rem;
    margin: 0;
    padding: .4rem var(--s3);
    border: 1px solid var(--eq-grid-line);
    border-radius: var(--radius-sm);
    font-variant: small-caps;
    font-size: 1rem;
    font-weight: 700;
    letter-spacing: .02em;
    text-decoration: none;
    line-height: 1.4;
}

/* The OPEN pane: the strongest thing on the screen, because it names what the
   rep is being asked for right now. */
.eq-acc__head--open {
    background: linear-gradient(to bottom, var(--eq-acc-open-hi), var(--eq-acc-open));
    border-color: var(--eq-acc-open);
    color: var(--eq-acc-open-ink);
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

/* A CLOSED pane. A link, so it reads as clickable without needing a hint --
   clicking it reopens that level, which is how a choice gets changed. */
a.eq-acc__head--shut {
    background: linear-gradient(to bottom, var(--eq-acc-shut-hi), var(--eq-acc-shut));
    color: var(--eq-acc-shut-ink);
}
a.eq-acc__head--shut:hover,
a.eq-acc__head--shut:focus-visible {
    border-color: var(--eq-acc-open);
    background: var(--eq-acc-shut);
}

/* A pane not yet reachable -- Derivative before a model is chosen. Present so
   the rep can see how many steps the screen has, but plainly not actionable.
   NOT display:none: its absence would make the screen appear to grow. */
.eq-acc__head--inert {
    background: linear-gradient(to bottom, var(--eq-acc-shut-hi), var(--eq-acc-shut));
    color: var(--muted);
    cursor: default;
}

/* The stock panel's heading below the accordion -- same type, no bar. */
.eq-acc__head--plain {
    border: 0;
    padding-inline: 0;
    background: none;
    color: var(--eq-heading);
}

/* "fixed" beside the make and the model of a quote being EDITED.

   A word, not a padlock glyph. The padlock on the accessories grid means "the
   dealership set this"; reusing it here would suggest the same authority set the
   vehicle, and it did not -- the rule is about what an edit is (see
   NewVehicleModel.VehicleIdentityLocked). Small caps and muted, so it reads as a
   state rather than competing with the value it qualifies. */
.eq-acc__fixed {
    margin-left: auto;
    font-size: var(--fs-small);
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--muted);
}

/* A standing rule stated on the screen it governs -- "make and model are fixed".
   Centred and given room, because it explains why a control the rep expected to
   work does not, and a rule buried in the body text gets missed. */
.note--rule {
    margin: 0 auto var(--s3);
    max-width: 68ch;
    text-align: center;
}

/* The chosen value, in the legacy's noticeably larger face. It is the ANSWER, so
   it outweighs the label introducing it. */
.eq-acc__value {
    font-size: 1.3rem;
    font-weight: 700;
    letter-spacing: 0;
}

/* The disclosure triangle. A CSS shape rather than an image or a glyph: no
   request, no font dependency, and it cannot arrive as mojibake -- which a
   literal triangle character in an ASCII-only source would have to. */
.eq-acc__caret {
    flex: none;
    width: 0;
    height: 0;
    border-style: solid;
}
.eq-acc__head--open .eq-acc__caret {
    border-width: .4rem .3rem 0;
    border-color: currentColor transparent transparent;
}
.eq-acc__head--shut .eq-acc__caret {
    border-width: .3rem 0 .3rem .4rem;
    border-color: transparent transparent transparent currentColor;
}

.eq-acc__body {
    padding: var(--s4);
    border: 1px solid var(--eq-grid-line);
    border-top: 0;
    border-radius: 0 0 var(--radius-sm) var(--radius-sm);
    background: #fff;
}

/* ---------------------------------------------------------------------------
   A PANE THAT OPENS AND SHUTS -- NATIVE <details>, NO SCRIPT

   Tim, 10 September 2026: "can all accordians be collapsed except 'Dealership
   details'". Settings > Dealership Details had grown to nineteen panes, all
   open, which is a very long scroll to reach the one you came for.

   WHY <details> RATHER THAN A CLASS AND A CLICK HANDLER, and each of these is a
   real behaviour rather than a preference:

     * it works with JavaScript blocked, which is the standing rule for
       everything on this screen that is not an enhancement;
     * it is keyboard-operable and announced as a disclosure by a screen reader
       for free;
     * the browser's own in-page find EXPANDS a closed pane to show a match.
       On a screen with this many settings that is the difference between
       Ctrl+F working and quietly finding nothing.

   THE OPEN/SHUT STATE COMES FROM [open], NOT FROM A CLASS. A class would have
   to be kept in step with the element's real state by script, and the two
   would disagree the first time the browser opened a pane by itself -- which
   is exactly what the find behaviour above does.

   The quote wizard's own .eq-acc panes are <section> and <a> and are untouched:
   there a "closed" level is a LINK that reopens a completed step, which is a
   different thing from a disclosure. Every selector below is scoped to
   details.eq-acc for that reason.
   --------------------------------------------------------------------------- */
details.eq-acc > summary {
    cursor: pointer;

    /* The default triangle is removed in both syntaxes -- the modern one and
       WebKit's older pseudo-element -- because .eq-acc__caret is already the
       screen's own marker and two would be one too many. */
    list-style: none;

    /* THE BAR STOPS BELOW THE CHROME, NEVER UNDER IT.

       Honoured by BOTH ways a pane gets scrolled to: app.js section 21 calls
       scrollIntoView() when one is opened, and the browser does its own jump
       for a #fragment -- which is how the Default Recons pane is reached after
       a save. Without this the bar lands behind the menu strip and the pane
       looks like it opened somewhere above the top of the screen.

       The extra --s3 is breathing room, so the bar is not flush against the
       menu strip and still reads as the top of its own section. */
    scroll-margin-top: calc(var(--eq-chrome-h) + var(--s3));
}
details.eq-acc > summary::-webkit-details-marker { display: none; }

/* SHUT: the tinted bar, and the body's rounded bottom moves up to the head --
   otherwise a closed pane has a square-cornered bar sitting on nothing.

   THE GRADIENT STARTS AT A TINT, NOT AT WHITE, and that single value is what
   Tim's "white on a white background" was about. Redmond's sheen runs over a
   solid fill; running it FROM #ffffff made the top half of every closed bar
   indistinguishable from the page behind it. */
details.eq-acc > summary.eq-acc__head {
    background: linear-gradient(to bottom, var(--eq-set-shut-hi), var(--eq-set-shut));
    border-color: var(--eq-set-shut-edge);
    color: var(--eq-set-shut-ink);
    border-radius: var(--radius-sm);
}
details.eq-acc > summary.eq-acc__head:hover,
details.eq-acc > summary.eq-acc__head:focus-visible {
    border-color: var(--eq-set-hover-edge);
    background: var(--eq-set-hover);
    color: var(--eq-set-hover-ink);
}

/* OPEN: the strongest thing on the screen, because it names what is being
   worked on right now. */
details.eq-acc[open] > summary.eq-acc__head {
    background: linear-gradient(to bottom, var(--eq-set-open-hi), var(--eq-set-open));
    border-color: var(--eq-set-open-edge);
    color: var(--eq-set-open-ink);
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

/* THE BODY IS TINTED TOO, and this is the other half of the contrast fix.
   .eq-acc__body is #fff for the wizard, where a pane sits on the page's own
   grey; here the surface behind it is white, so a white body under a coloured
   bar has no edge of its own and the pane reads as a bar floating on nothing.
   Redmond's .ui-widget-content is #f2f5f7 inside #ddd, which is what the
   legacy screen shows. Scoped to details.eq-acc so the wizard is untouched. */
details.eq-acc > .eq-acc__body {
    background: var(--eq-set-body);
    border-color: var(--eq-set-body-edge);
}

/* The caret follows the element's real state: right when shut, down when open.
   Same two shapes the wizard uses, keyed off [open] instead of a class. */
details.eq-acc > summary .eq-acc__caret {
    border-width: .3rem 0 .3rem .4rem;
    border-color: transparent transparent transparent currentColor;
}
details.eq-acc[open] > summary .eq-acc__caret {
    border-width: .4rem .3rem 0;
    border-color: currentColor transparent transparent;
}

/* A pane is one unit: no gap between its head and its body, and the gap in
   .eq-acc stays as the space BETWEEN panes. */
details.eq-acc { gap: 0; }
details.eq-acc + details.eq-acc,
details.eq-acc + form,
form + details.eq-acc { margin-top: var(--s2); }

/* ---------------------------------------------------------------------------
   WHERE THE NARROW COLUMN CAME FROM -- Tim, 11 September 2026, with the legacy
   screen beside this one:

     "everything's left aligned in your version. Um, you know, your initial
      instruction was to use the full width of the screen, so I do understand
      why you've done it. but visually, it ... there's not enough information to
      fill the entire screen. So like this, we've got the accordions centered
      and within a smaller width."

   That is the origin of .eq-settings. Four days later Tim asked for the two
   widths to be NAMED, and the block immediately below is the answer -- read it
   rather than this, because it is the rule. This paragraph is kept only for the
   quote, which is the reason a narrow column exists at all.

   TWO CLAIMS THAT USED TO BE HERE ARE WITHDRAWN, and they are named rather than
   quietly deleted, because both were argued at length and a reader who saw them
   once will look for them:

     * "DELIBERATELY NOT APPLIED TO STAFF ADMINISTRATION ... its centrepiece is
       a wide staff grid." Reversed by Tim on 11 September -- that grid lost four
       columns the same day, so five columns are not data-dense and the exemption
       went with the reason for it. Staff Administration is narrow, and it is the
       screen Tim later named as the EXAMPLE of narrow.

     * The full-bleed hybrid -- a centred 1100px track with .grid-wrap breaking
       out to the whole surface. It was invented on 15 September so an
       eighteen-column report would not be squeezed, and removed the same day,
       because it made "narrow" mean two different things depending on which
       element you looked at. The reports that needed the room are .eq-full now.
   --------------------------------------------------------------------------- */
/* ===========================================================================
   THE TWO SCREEN WIDTHS, AND THEIR NAMES -- Tim, 15 September 2026

     "I think we need to clarify and give these terms a name. So ... a report
      that needs to be full width we can call full width and if there's
      something that needs to be narrow we can call it narrow. Can you document
      this somewhere in your styling so that if I talk about a table being
      narrow styling that you know what I'm referring to."

   So there are exactly TWO, they are named after what he says out loud, and
   every screen is one of them.

   ---------------------------------------------------------------------------
   FULL WIDTH   .eq-full
   ---------------------------------------------------------------------------
   The content uses the whole usable surface -- heading, filter bar, grid and
   footnotes on one left edge, out to rule 5.5's 92% with its 2200px ceiling.

   For a report or a list whose value is in seeing many columns at once. It is
   also what a page gets by DEFAULT: a screen with neither class on it renders
   full width, because .app-surface is the surface.

   ---------------------------------------------------------------------------
   NARROW       .eq-narrow
   ---------------------------------------------------------------------------
   The content -- INCLUDING ITS TABLES -- is capped at 1100px and centred.

   For a screen of caption-and-control rows, and for a list that simply reads
   better held in: Tim's examples are the Admin Staff Administration screen and
   the Sales Reminders list. His original words for the settings screens were
   "the accordions centered and within a smaller width".

   NARROW MEANS EVERYTHING, AND THAT IS A CORRECTION MADE THE SAME DAY. An
   earlier version capped the text column and let a .grid-wrap break out to the
   full surface -- a hybrid invented to stop an eighteen-column report being
   clipped. It made "narrow" mean two different things depending on the element,
   which is exactly the ambiguity this vocabulary exists to remove. The reports
   that needed the room are .eq-full now, so nothing needs the hybrid.

   ---------------------------------------------------------------------------
   1100px, and why it is not a rule-5.5 violation
   ---------------------------------------------------------------------------
   .app-surface is untouched -- 92%, floor 1200px, ceiling 2200px -- so the
   frame is exactly as rule 5.5 sets it. What is capped is one screen's CONTENT
   column inside that frame, and it is capped for rule 5.5's own stated reason:
   the rule argues its 2200px ceiling as "a key-and-value row stretched to
   2355px separates the label from its number far enough that the eye loses the
   row". A caption-and-control screen is nothing but such rows, so the same
   argument applies at a lower number. 1100px is also the legacy's own order of
   magnitude -- DealershipPref.aspx pins its panels at 750px inside a 1340px
   shell -- while giving the boxes and their captions room the legacy never did.

   ---------------------------------------------------------------------------
   HOW TO USE THEM
   ---------------------------------------------------------------------------
     <div class="app-surface">
       <div class="eq-narrow"> ... </div>     a settings screen, or a list Tim
                                              has asked to be held in
       <div class="eq-full">   ... </div>     a report, a wide grid
     </div>

   .eq-settings is the settings-screen wrapper and is NARROW by definition --
   there is no such thing as a full-width settings form -- so it carries the
   narrow behaviour itself and needs no second class.
   =========================================================================== */
.eq-narrow,
.eq-settings {
    display: grid;
    grid-template-columns: 1fr min(100%, 1100px) 1fr;
}

.eq-narrow > *,
.eq-settings > * {
    grid-column: 2;
}

/* FULL WIDTH is the default, so this class exists to be SAID rather than to do
   much: on a bare page it changes nothing, and on a narrow wrapper it releases
   every child to the whole surface. Naming it is the point -- a screen that
   says .eq-full has been decided about, where a screen with no class at all
   might simply never have been considered. */
.eq-full > * {
    grid-column: 1 / -1;
}

/* ---------------------------------------------------------------------------
   FRANCHISE LEADS -- a traffic light in every cell

   The legacy sets Drawing.Color.LightGreen, LightPink and LightYellow on
   INDIVIDUAL CELLS of each row (ReportFLeads.aspx.vb:145-235), where the
   costing list colours whole rows. Same three named colours, same tokens,
   scoped to this screen for the same reason .eq-costing is: a rule that painted
   every grid cell in the application would be impossible to reason about.

   COLOUR IS NEVER THE ONLY CARRIER. Every cell also says its state in words --
   "Accepted", "Not booked", "Approved" -- because green against pink is exactly
   what a colour-blind rep cannot read, and the legacy offers nothing else at
   all. The colour is the fast path for everyone who can use it.
   --------------------------------------------------------------------------- */
.eq-fleads table.grid tbody td.is-done { background: var(--eq-row-green); }
.eq-fleads table.grid tbody td.is-due  { background: var(--eq-row-yellow); }
.eq-fleads table.grid tbody td.is-none { background: var(--eq-row-red); }

/* The bands underneath would otherwise show through -- rule 5.4f paints them on
   > td, so a cell colour has to be declared at the same level to win. It is,
   above, and this comment is here so the next reader does not "simplify" the
   banding by moving it back onto the row. */

/* ---------------------------------------------------------------------------
   THE PICK TABLES

   LEFT-ALIGNED, and only as wide as they need to be -- which is what the legacy
   does. .eq-table centres its cells because it is a small fixed summary table;
   these are lists to read DOWN, and a centred column of nine brand names is
   noticeably harder to scan than a left-aligned one.
   --------------------------------------------------------------------------- */
.eq-pick {
    border-collapse: collapse;
    background: #fff;
    font-size: var(--fs-body);
    text-align: left;
}

.eq-pick th {
    background: var(--eq-grid-head);
    color: var(--eq-grid-head-ink);
    border: 1px solid var(--eq-grid-line);
    padding: .3rem .6rem;
    text-align: left;
    font-weight: 700;
    line-height: 1.25;
    white-space: nowrap;
}

.eq-pick td {
    border: 1px solid var(--eq-grid-line);
    color: var(--eq-grid-ink);
    padding: .2rem .6rem;
    text-align: left;
    vertical-align: top;
}

/* Banding on the row, first band tinted -- as .eq-table does, and as the legacy
   RowStyle/AlternatingRowStyle pair does. It is here to help the eye track
   across seven columns, not to decorate. */
.eq-pick tbody tr:nth-child(odd)  td { background: var(--eq-grid-row); }
.eq-pick tbody tr:nth-child(even) td { background: #fff; }
.eq-pick tbody tr:hover td { background: var(--eq-selected); }

/* THE WHOLE CELL IS THE TARGET, not just the words in it. The legacy's rows are
   narrow and its links short, so aiming at them is fiddly on a trackpad -- and a
   rep does this dozens of times a day. */
.eq-pick td a {
    display: block;
    text-decoration: underline;
}

/* The single-column brand and model lists sit at a readable minimum rather than
   shrinking to the widest name -- "MG" alone would otherwise give a 4rem table. */
/* ---------------------------------------------------------------------------
   THE CHOSEN ROW IN A PICKER TABLE.

   Tim, 27 August 2026, about the derivative list on an edit: "exactly the same
   as when I search for a client, and I click on that client ... it shows with
   the different blue, which client is currently selected. I want the same for
   the derivative."

   WHY THIS RULE HAS TO EXIST SEPARATELY. The global .is-selected rule in
   section 16 targets table.grid and table.grid--eq. The picker tables are
   .eq-pick, which owns its own banding and hover, so the global rule never
   reached them and adding the class alone did nothing visible.

   AND WHY IT IS NOT JUST A BACKGROUND SWAP: .eq-pick's HOVER already uses
   --eq-selected, so a selected row coloured the same would be
   indistinguishable from whatever row the pointer happens to be over. The
   weight and the navy ink are what separate them -- and they are also what
   keeps the state legible to someone who cannot tell the two blues apart.
   Same three-signal reasoning as the grid rule; do not simplify either.
   --------------------------------------------------------------------------- */
.eq-pick tbody tr.is-selected td {
    background: var(--eq-selected);
    color: var(--eq-selected-ink);
    font-weight: 700;
}
/* Selection outranks hover: moving the pointer over another row must not look
   like the selection moved. */
.eq-pick tbody tr.is-selected:hover td {
    background: var(--eq-selected);
}
/* Every cell of a picker row is a link, so the links have to take the row's ink
   or the blue would fight the navy and the row would stop reading as one thing. */
.eq-pick tbody tr.is-selected td a {
    color: var(--eq-selected-ink);
    font-weight: 700;
}

.eq-pick--list { min-width: 18rem; }

/* The derivative table earns the full width: seven columns, four of them money. */
.eq-pick--deriv { width: 100%; }

.eq-pick .num    { text-align: right; }
.eq-pick .num a  { text-align: right; }
.eq-pick th.num  { text-align: right; }

/* Wide content scrolls INSIDE its own container -- the page body must never
   scroll sideways. Layout rule 5.5. */
.eq-scroll { overflow-x: auto; }

.eq-stock { margin-top: var(--s5); }

/* The VAT Exempt line, above the accordion where the legacy puts it. */
.eq-vatex {
    display: flex;
    align-items: center;
    gap: .4rem;
    margin-bottom: var(--s4);
}
.eq-vatex label { margin: 0; font-weight: 400; }

/* ===========================================================================
   29  THE QUOTE'S ACTION PANEL

   Reproduces QuoteView.ascx:994-1048: the Hot checkbox centred above a narrow
   fieldset holding the assistance button and the numbered action list, with the
   finance decision below it.

   THE PANEL IS 300px IN THE LEGACY (style="width:300px; margin:0 auto" on the
   fieldset) and that narrowness is the design, not a limitation -- it is a
   column of thirteen links to be read down, not a toolbar. Reproduced with a
   floor so a long label does not wrap on a laptop.
   =========================================================================== */

.qa { margin-block: var(--s4); }

/* The action notice sits over the panel it came from, at the panel's own width.
   Full-bleed it read as a page-level banner rather than as the result of the
   thing directly below it. */
.qa > .alert { width: min(100%, 22rem); margin-inline: auto; text-align: center; }

/* Status - Hot, centred above the panel. */
.qa__hot {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: var(--s2);
    margin-bottom: var(--s3);
    color: var(--eq-body-ink);
}
.qa__hot label { display: flex; align-items: center; gap: .4rem; margin: 0; }

/* The no-script fallback, hidden once app.js has run. The direction matters:
   visible by default, hidden by script, so a blocked asset leaves a working
   control rather than an inert one. */
.has-js .nojs-only { display: none; }

/* The panel itself. .eq-card supplies the sheet and the lift. */
.qa__panel {
    width: min(100%, 22rem);
    margin-inline: auto;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: var(--s3) var(--s4);
    box-shadow: var(--eq-lift);
}

.qa__assist { text-align: center; margin-bottom: var(--s2); }

/* A control that is shown but does nothing yet. Not `disabled`: a disabled
   button is skipped by keyboard navigation and its tooltip never opens, so the
   reason -- which is the only useful thing about it -- becomes unreachable. */
.is-inert, .qa__soon {
    color: var(--muted);
    cursor: help;
}
.is-inert { opacity: .75; }

/* The discount-override block. Error red, because it is the reason the print
   actions are missing and a rep needs to connect the two. */
.qa__blocked {
    color: var(--error-ink);
    font-weight: 600;
    font-size: var(--fs-small);
    text-align: center;
    margin: 0 0 var(--s2);
    max-width: none;
}

/* The numbered list. The numbers are how a rep and their manager refer to these
   on the phone -- but they are positional and change with the quote, so nothing
   in the code uses them. See QuoteActions. */
.qa__list {
    margin: 0;
    padding-left: 1.6rem;
    font-size: var(--fs-small);
}
.qa__list li { margin-bottom: .3rem; }
.qa__list li:last-child { margin-bottom: 0; }

/* An action's form must not add a line break inside its list item -- the numbered
   list reads as a column of links and a block-level form between the number and
   the label would break that. */
.qa__form { display: inline; }

/* A working action reads as a link, because in the legacy it is one. */
.qa__go {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: var(--eq-link);
    text-decoration: underline;
    cursor: pointer;
}

/* The finance decision, below the panel. */
.qa__finance {
    display: flex;
    justify-content: center;
    margin-top: var(--s3);
    font-size: var(--fs-small);
}
.qa__financeform { display: flex; align-items: center; gap: var(--s2); }
.qa__financelabel { color: var(--eq-body-ink); }

/* ===========================================================================
   30  THE PHONE

   Tim asked, 27 August 2026, for the sales module as a mobile app with free rein
   on styling. The answer chosen was ONE SET OF SCREENS: an Android APK whose UI
   is these very pages, in a WebView (EasyQuoteNext.Android). So "the mobile app"
   is this stylesheet plus that shell, and there is no second implementation of
   any screen, any validation rule or any calculation.

   Why that shape, in one line, because it is the reason this section exists at
   all rather than a second project: sharing a calculation library is not enough.
   InclRepayment is copy-pasted six times in the legacy with three different sets
   of money constants, and the accessory defect of 25 August was a screen reading
   two of five boxes with a perfectly correct calculation behind it. Only having
   one set of screens prevents the second kind.

   ---------------------------------------------------------------------------
   THE RULE THAT KEEPS THIS FROM BECOMING A FUNCTIONAL CHANGE
   ---------------------------------------------------------------------------
   NOTHING INTERACTIVE IS HIDDEN ON A PHONE. It is re-laid-out.

   A breakpoint that drops a column, a button or a link silently removes
   functionality from a rep, and it does it invisibly - the desktop still looks
   right, so nobody notices. That is the same class of defect as every other one
   this project has had to correct. Re-locating a control is fine; moving it into
   a card, a second row or a scrollable strip is fine. `display: none` on
   anything a rep can operate is not, and MobileLayoutTests enforces it.

   The one deliberate `display: none` below is `thead`, and it is not a
   violation: a column heading is not interactive, and its text is re-rendered
   in front of every cell it labels via `data-label`. The information moves; it
   does not disappear.

   ---------------------------------------------------------------------------
   760px, AND WHY THAT NUMBER
   ---------------------------------------------------------------------------
   It is the breakpoint this stylesheet already used in two places before this
   section existed, so reusing it avoids a third threshold nobody can remember.
   It also sits above every phone in portrait and below every tablet in
   landscape, which is the split that matters here: below it, a six-column data
   grid cannot be read, and that is the change this section is really about.

   Note what needs NO work, because it is worth knowing the rules already hold:

     - `.app-surface` (rule 5.5) is `min(100%, max(1200px, 92%))`. On a 400px
       phone the max() picks 1200px and the min() clamps it to 100%, so it is
       already full width. The 1200px floor cannot widen a narrow viewport - it
       looks like it would and it does not.
     - The pickers are already `<dialog>` elements sized `min(100%, 1500px)`, so
       they are already full-width on a phone. An earlier plan said the
       searchable dropdown would need to opt out of a JavaScript enhancement on
       mobile; there is no such enhancement in this application - the pickers are
       real dialogs over real links - so there was nothing to opt out of.
     - Rule 5.4a (never jump to the top) and its grid-scroll positions work
       unchanged, PROVIDED the WebView has DOM storage enabled. See the comment
       on ConfigureWebView in MainActivity: with it off, every write throws, the
       try/catch swallows it as the rule requires, and the jump-to-top behaviour
       returns silently.
     - Rule 5.4b's hidden diagnostic works unchanged: long-press selects text on
       Android, which is exactly the reveal gesture it was designed around.
   =========================================================================== */

@media (max-width: 760px) {

    /* -----------------------------------------------------------------------
       THE TITLE BAR

       Wraps instead of overflowing, and the dealership name gives up size
       first. The branch code and the username are the two things support asks
       for on a call, so both stay on screen; only the type scale moves.
       ----------------------------------------------------------------------- */
    .topbar__inner {
        flex-wrap: wrap;
        gap: var(--s2) var(--s3);
        padding-block: var(--s2);
    }
    .brand { font-size: 1.0625rem; }

    /* -----------------------------------------------------------------------
       THE MENU STRIP STAYS WHERE IT IS, AND SCROLLS SIDEWAYS

       IT WAS A FIXED BOTTOM BAR FIRST, AND THAT WAS WRONG. Recorded because the
       reasoning was plausible and the result was invisible.

       The idea was that on a phone held in one hand the bottom of the screen is
       easiest to reach, so `position: fixed; bottom: 0`. On the emulator the bar
       simply DID NOT APPEAR - and it took several probes to establish why,
       because every other explanation looked likelier: the rules were correct,
       the breakpoint matched, the served stylesheet was current, and the element
       was present in the markup with real links in it.

       The cause: this application is wrapped in an APK targeting SDK 36, so
       Android draws the activity EDGE TO EDGE. The WebView's viewport extends
       underneath the system navigation bar, so `bottom: 0` resolves to a strip
       of screen the user cannot see. Nothing in CSS reports this; the bar is
       laid out perfectly, off the visible area.

       So the bar is left in the flow, sticky under the title bar exactly as on
       the desktop, and only its OVERFLOW changes. Three reasons this is the
       better answer rather than a retreat:

         1. It cannot be broken by a host that draws under the system bars, which
            is now the Android default and will be the default everywhere.
         2. A rep of twenty years finds the menu where the desktop puts it. The
            bottom bar was my styling preference, not a requirement.
         3. Nothing is hidden either way - the strip scrolls sideways, so every
            item a rep has on the desktop is present and reachable.

       If a bottom bar is ever wanted, the fix belongs in the SHELL (handling
       window insets and setting a CSS variable), not in a media query.
       ----------------------------------------------------------------------- */
    .menustrip__inner {
        overflow-x: auto;
        overflow-y: hidden;
        /* The page must never scroll sideways (rule 5.5); this box does instead. */
        overscroll-behavior-x: contain;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;              /* a scrollbar over a 44px bar is noise */
    }
    .menustrip__inner::-webkit-scrollbar { display: none; }

    /* NOWRAP, so the strip is one line that scrolls rather than several that
       grow upward and eat the page.

       ORDER AND WIDTH ARE RESET, AND THAT IS A BUG FIX, NOT TIDYING. Read this
       before deleting either declaration.

       An older arrangement had .nav inside .topbar__inner, and
       `@media (max-width: 900px)` still carries `.nav { order: 3; width: 100% }`
       from that layout. Both survive into this breakpoint, and together they broke
       the bottom bar in a way that looked like the navigation had vanished:

         - order: 3 put .nav AFTER .menustrip__right, so "Transfer to Other
           Module" and "Notifications and Updates" occupied the whole visible bar
           and Home Page, Open Quotes and Reminders sat off-screen to the right,
           reachable only by dragging. Found on the emulator, 27 August 2026 -- the
           bar rendered, so nothing looked broken.
         - width: 100% then sized .nav to the full bar on its own, which is wrong
           for a strip meant to hold the nav AND the right-hand items.

       So the primary navigation is put back first, and .menustrip__right after it.
       Explicit orders on both, rather than relying on document order, because the
       whole problem was an inherited order from somewhere else. */
    .nav {
        flex-wrap: nowrap;
        order: 0;
        width: auto;
    }
    .menustrip__right { order: 1; }
    .nav a, .nav > .nav-soon, .nav details > summary {
        white-space: nowrap;
        min-height: 44px;                   /* see the touch-target note below */
        align-items: center;
    }

    /* The "not yet built" submenu opens UPWARD now that its summary is at the
       bottom of the screen. Without this it renders below the viewport and looks
       like a menu that does nothing. */
    .nav details > .submenu {
        top: auto;
        bottom: calc(100% + .35rem);
        max-height: 60vh;
        overflow-y: auto;
    }

    main.app-surface { padding-block: var(--s4) var(--s5); }
    .app-surface { padding-inline: var(--s3); }

    /* -----------------------------------------------------------------------
       DATA GRIDS BECOME CARDS

       A six-column grid on a 400px screen is unreadable at any font size, and
       the alternative - a horizontally scrolling table - means a rep drags
       sideways to read one quote and loses the client's name off the left edge.
       So below this breakpoint every row becomes a card and every cell carries
       its own label.

       HOW RULE 5.4d IS SATISFIED HERE. The rule is that column headings must
       never scroll out of sight. In card mode there ARE no column headings -
       each value is labelled in place, so the heading is never more than a line
       away from the value it describes, which is the outcome the rule wanted
       rather than a workaround for it. `--grid-viewport` is therefore lifted:
       there is nothing to stick, and a viewport-tall box inside a page that
       also scrolls would be two scrollbars for one list.

       THE LABELS COME FROM `data-label` ON EACH `<td>`, added in the grid
       markup. That costs a few bytes per cell and compresses to almost nothing;
       the alternative - rendering a table AND a card list and hiding one - would
       double the payload of the heaviest screens, and bytes on the wire are this
       project's acceptance criterion.
       ----------------------------------------------------------------------- */
    .grid-scroll {
        max-height: none;                   /* nothing to stick to; see above */
        overflow: visible;
    }
    .grid-wrap {
        background: none;
        box-shadow: none;
        border-radius: 0;
        overflow: visible;
    }

    table.grid, table.grid tbody, table.grid tfoot, table.grid tr,
    table.grid th, table.grid td {
        display: block;
    }

    /* -------------------------------------------------------------------
       THE HEADING ROW: HIDDEN ONLY WHEN IT HOLDS NOTHING TO CLICK.

       READ THIS BEFORE SIMPLIFYING IT TO `thead { display: none }`. That is
       what this rule was on its first draft, and it was WRONG - it would have
       removed sorting from every grid on the phone.

       On Open Quotes (and every other sortable grid) each `<th>` contains a
       SORT LINK, not just a label. So a blanket display:none would hide six
       controls, silently, on the busiest screen in the module - the precise
       defect the "nothing interactive is hidden" rule at the top of this
       section exists to prevent, committed by the rule's own author.

       So: a heading row with nothing interactive in it disappears, because its
       text is re-rendered by td::before in front of every value it labelled.
       A heading row WITH links stays, re-laid-out below as a sort strip.

       :has() IS THE TEST, and its failure direction is the safe one. On a
       WebView too old to support :has() the selector never matches, so the
       heading row simply stays visible on every grid - untidy, and nothing
       lost. The reverse default would hide controls on exactly the devices
       least able to cope.
       ------------------------------------------------------------------- */
    table.grid thead:not(:has(a, button, input, select, [role="button"])) {
        display: none;
    }

    /* A heading row that DID keep its links becomes a horizontally scrollable
       strip of sort chips above the cards. Same links, same URLs, same server
       behaviour - only the arrangement changes. */
    table.grid thead:has(a, button, input, select, [role="button"]) {
        display: block;
    }
    table.grid thead:has(a) tr {
        display: flex;
        flex-wrap: nowrap;
        gap: var(--s2);
        overflow-x: auto;
        overscroll-behavior-x: contain;
        scrollbar-width: none;
        padding-bottom: var(--s2);
    }
    table.grid thead:has(a) tr::-webkit-scrollbar { display: none; }
    table.grid thead:has(a) th {
        position: static;               /* nothing to stick to in card mode */
        display: block;
        flex: 0 0 auto;
        border-bottom: 0;
        border-radius: var(--radius-sm);
        padding: 0;
    }
    table.grid thead:has(a) th a {
        display: flex;
        align-items: center;
        min-height: 44px;
        padding: 0 var(--s3);
    }

    /* Each row is a card. */
    table.grid tbody tr {
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius);
        box-shadow: var(--shadow-sm);
        margin-bottom: var(--s3);
        padding: var(--s2) var(--s3);
    }
    /* The desktop rules paint rows on hover and strip the last border; neither
       makes sense once a row is a card. */
    table.grid tbody tr:hover { background: var(--surface); }

    /* THE ALTERNATING BANDS ARE CANCELLED IN CARD MODE, deliberately.

       The bands exist to carry the eye ACROSS a row (Tim, 4 September 2026).
       A card has no row to cross -- each value sits on its own line with its
       own label -- so banding the cells would paint stripes INSIDE every card
       and banding the cards would fight the border, radius, shadow and margin
       that already separate them.

       This is the same shape of answer as rule 5.4d's sticky heading, which is
       satisfied in card mode by labelling each value instead. Cancelled, not
       forgotten. */
    table.grid tbody tr > td { background: transparent; }

    table.grid tbody td {
        border-bottom: 1px dashed var(--border);
        padding: .45rem 0;
        /* Label left, value right, and the value keeps the alignment its column
           had - a money column stays right-aligned and tabular. */
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: var(--s3);
        text-align: right;
    }
    table.grid tbody tr td:last-child { border-bottom: 0; }

    /* THE LABEL. Only rendered when the cell actually has one, so a grid whose
       markup has not been given data-label yet degrades to an unlabelled card
       rather than to a card with an empty gutter. */
    table.grid tbody td[data-label]::before {
        content: attr(data-label);
        flex: 0 0 auto;
        max-width: 45%;
        text-align: left;
        color: var(--ink-soft);
        font-size: var(--fs-eyebrow);
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: .06em;
    }

    /* A cell whose value is the whole point of the card - the first column on
       most grids - reads as a heading instead of a labelled field. */
    table.grid tbody td.eq-card-title {
        display: block;
        text-align: left;
        font-size: 1.0625rem;
        font-weight: 700;
        color: var(--ink);
        padding-top: 0;
    }
    table.grid tbody td.eq-card-title::before { content: none; }

    /* The totals row stops being sticky - there is no scrollport for it to
       stick to - and becomes a final, visually distinct card. Rule 5.4d's
       bottom-sticky totals have the same answer as its headings: in card mode
       the number is adjacent to what it totals. */
    table.grid tfoot th, table.grid tfoot td {
        position: static;
        display: flex;
        justify-content: space-between;
        gap: var(--s3);
        text-align: right;
    }
    table.grid tfoot tr {
        background: var(--brand-tint);
        border: 1px solid var(--border-strong);
        border-radius: var(--radius);
        margin-bottom: var(--s3);
        padding: var(--s2) var(--s3);
    }
    table.grid tfoot td[data-label]::before,
    table.grid tfoot th[data-label]::before {
        content: attr(data-label);
        text-align: left;
        color: var(--ink-soft);
        font-size: var(--fs-eyebrow);
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: .06em;
    }

    /* -----------------------------------------------------------------------
       TOUCH TARGETS

       44px is the floor, and it is a floor rather than a size: a control that
       is already bigger is left alone. The native buttons in the APK shell use
       48dp for the same reason, so nothing inside the page is smaller than the
       chrome around it.

       16px on inputs is not about the phone zooming (Android does not); it is
       legibility on the low-spec, often older screens this project exists for,
       and it stops a rep zooming in to check a price they typed.
       ----------------------------------------------------------------------- */
    button, .btn, [type="submit"], [type="button"], [type="reset"], summary {
        min-height: 44px;
    }

    /* THE CLOSE X IS OFTEN AN <a>, so the button rule above does not reach it.
       Named explicitly, because a close cross is the control a rep is most
       likely to stab at on a phone -- and getting it wrong closes nothing while
       looking like a dead screen. */
    .eq-x {
        min-width: 44px;
        min-height: 44px;
    }
    input:not([type="checkbox"]):not([type="radio"]), select, textarea {
        min-height: 44px;
        font-size: 16px;
    }
    /* A checkbox or radio stays its native size - growing the box looks broken -
       so the LABEL carries the target instead, which is what a thumb aims at. */
    label:has(> input[type="checkbox"]), label:has(> input[type="radio"]),
    input[type="checkbox"] + label, input[type="radio"] + label {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }

    /* Row-navigating grids: the whole card is the target, so the link inside it
       does not need to be, but it must not be smaller than a thumb either. */
    table.grid tbody td a { min-height: 24px; display: inline-flex; align-items: center; }

    /* -----------------------------------------------------------------------
       PICKERS AND MODALS

       Already `min(100%, 1500px)`, so already full width. The only change is
       height: 92dvh leaves a sliver of page visible above and below, which on a
       phone reads as a rendering fault rather than as a dialog. Full height,
       and the frame's rounding and margin go with it.
       ----------------------------------------------------------------------- */
    .eq-modal:modal {
        max-height: 100dvh;
        height: 100dvh;
        margin: 0;
        border: 0;
    }

    /* -----------------------------------------------------------------------
       ONE-COLUMN FORMS

       Any two-up or three-up field grid collapses. Declared by attribute rather
       than by listing every step's class so a step added later inherits it -
       the alternative is a list that silently stops covering new screens.
       ----------------------------------------------------------------------- */
    .eq-fin__origins { grid-template-columns: 1fr 1fr; }

    /* -----------------------------------------------------------------------
       THE OTHER TWO TABLE FAMILIES: SCROLL, NOT CARDS. FOR NOW.

       This application has three table families, counted rather than assumed:
       `.grid` on 18 tables, `.eq-table` on 3 and `.eq-pick` on 3. Everything
       above applies to `.grid`, which is the data-grid family and the one the
       card treatment was designed for.

       `.eq-table` and `.eq-pick` get an internal horizontal scroll instead.
       That is a DELIBERATE, NARROWER choice, not an oversight:

         - `.eq-table` is at most four columns (the extras summary is Accessory,
           Excl VAT, Incl VAT), which fits a phone without help.
         - `.eq-pick` is the picker tables, which live inside a dialog that is
           already full-screen here. The used-stock picker is fourteen columns
           and WOULD benefit from cards - it is follow-up work, named in
           Docs/DESIGN-MobileShell.md rather than left as a silent gap, and it
           is outside the used-vehicle path this proof of concept covers.

       Scrolling hides nothing: every column is reachable by dragging. It is
       simply less comfortable than a card, which is the honest trade for a
       screen not yet in scope.

       THE TABLE ITSELF IS THE SCROLL BOX, because there is no wrapper to use.
       The first version of this rule targeted `.eq-table-wrap` and
       `.eq-pick-wrap` -- CLASSES THAT DO NOT EXIST ANYWHERE IN THE MARKUP. I
       invented them by analogy with .grid-scroll, so the entire mitigation did
       nothing and the home page's counts table overflowed the viewport, which
       rule 5.5 forbids outright. Found on the emulator, 27 August 2026.

       Note StylesheetClassTests deliberately does NOT catch this direction (a
       rule targeting a class the markup never emits) on the grounds that it is
       harmless. Here it was not harmless: the rule WAS the fix.

       `display: block` on a <table> makes it an ordinary block box that can
       scroll, with its rows laid out in an anonymous table inside. It is the
       standard technique for a table with no wrapper, and it is why min-width
       is released -- a table refuses to shrink below its min-content width, so
       `width: min(100%, 760px)` in the markup could not prevent the overflow on
       its own.
       ----------------------------------------------------------------------- */
    table.eq-table, table.eq-pick {
        display: block;
        overflow-x: auto;
        overscroll-behavior-x: contain;
        min-width: 0;
        max-width: 100%;
    }
    .eq-modal__body {
        overflow-x: auto;
        overscroll-behavior-x: contain;
    }

    /* -----------------------------------------------------------------------
       .eq-detail STACKS, IT DOES NOT SCROLL SIDEWAYS.

       Different answer from .eq-table above, and for a reason: that one is a
       grid of figures whose columns mean something next to each other, so
       scrolling keeps the comparison. This is a caption and one control, and
       a 17rem label column beside a 200px box does not fit 400px. Stacked, the
       caption sits directly above its own box and nothing is hidden -- which
       is what rule 5.4e requires and what a sideways scroll would fail, since
       the input would start off screen.
       ----------------------------------------------------------------------- */
    .eq-detail > tbody > tr > th,
    .eq-detail > tbody > tr > td {
        display: block;
        width: auto;
        line-height: inherit;
    }
    .eq-detail > tbody > tr > th { padding-bottom: 0; font-weight: 700; }
    .eq-detail > tbody > tr > td { padding-top: .1rem; padding-bottom: .5rem; }

    /* The legacy's pixel widths are a desktop shape. On a phone the box uses
       the width there is. */
    .eq-detail input[type="text"],
    .eq-detail input[type="email"],
    .eq-detail input[type="tel"],
    .eq-detail input.is-wide,
    .eq-detail input.is-narrow,
    .eq-detail select { width: 100%; }

    /* The hours keep their two dropdowns side by side -- they are a pair and
       reading one without the other is meaningless -- but the day label moves
       above them. */
    .eq-hours th, .eq-hours td { display: block; text-align: left; }
    .eq-hours select { width: 45%; }
}

/* ---------------------------------------------------------------------------
   PRINT, AGAIN

   The bottom bar is fixed, and a fixed element repeats on every printed page.
   The desktop print rules already drop the chrome; this makes sure the mobile
   position cannot reintroduce it if someone prints from a phone.
   --------------------------------------------------------------------------- */
@media print {
    /* The menu strip is no longer repositioned on a phone (see section 30), so
       there is nothing here to undo. Kept as the place to put it if that ever
       changes: a fixed element repeats on every printed page. */
    .menustrip { box-shadow: none; }
}

/* ===========================================================================
   31  THE TRADE-IN EDITOR

   The dialog behind "ADD TRADE-IN" on the financial step, reproducing the
   legacy's AddTradeInPopupPanel (Quote.aspx:2302). Fourteen inputs, a
   per-dealership required set, and the AutoBid lookup for the 43 dealerships
   that subscribe to it.

   It reuses .eq-modal for the frame and .grid for the list, so there is very
   little here: a field grid, an actions row, and the error list.
   =========================================================================== */

.eq-ti { display: block; }

/* ---------------------------------------------------------------------------
   THE FIELD GRID

   Fourteen inputs is too many for one column and too many for a fixed three,
   so the columns are fluid: as many 15rem tracks as fit. On a phone that
   resolves to one, which is why section 30 needs no rule for this dialog.

   The legacy lays these out as a 3-column HTML table 250 lines long. The
   ORDER is the legacy's -- deal terms first (offered, settlement, over
   allowance, expiry), then the vehicle -- because that is the order a rep
   reads off an appraisal sheet.
   --------------------------------------------------------------------------- */
.eq-ti__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: var(--s3) var(--s4);
    margin-bottom: var(--s3);
}

.eq-ti__field { display: flex; flex-direction: column; gap: .2rem; }

.eq-ti__field > label {
    font-size: var(--fs-small);
    font-weight: 650;
    color: var(--eq-heading);
}

/* ---------------------------------------------------------------------------
   THE ROW ACTIONS

   Edit and Remove, side by side, in the grid's last cell. Kept on one line so
   a nine-column grid does not grow a row taller for two links.

   In card mode (section 30) the cell becomes a flex row like every other, and
   these two sit at its right -- which reads correctly without a rule of its
   own because there is no label to oppose.
   --------------------------------------------------------------------------- */
.eq-ti__actions {
    display: flex;
    align-items: center;
    gap: var(--s3);
    white-space: nowrap;
}

/* The dialog's own buttons: primary first, cancel beside it, separated from the
   fields above. */
.eq-ti__actions--form {
    justify-content: flex-start;
    gap: var(--s3);
    margin-top: var(--s4);
    padding-top: var(--s3);
    border-top: 1px solid var(--eq-page-line);
    white-space: normal;
    flex-wrap: wrap;
}

/* ---------------------------------------------------------------------------
   THE AUTOBID LOOKUP

   A real <fieldset>, because it is a genuine sub-group with its own action and
   a screen reader should hear it as one. Boxed and tinted so it reads as "this
   part talks to somebody else" -- the values it fills come from a third party,
   and a rep should be able to see which fields arrived that way.

   Shown only when the dealership subscribes, so most reps never see it.
   --------------------------------------------------------------------------- */
.eq-ti__autobid {
    display: flex;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: var(--s2) var(--s3);
    margin: 0 0 var(--s4);
    padding: var(--s3);
    border: 1px solid var(--eq-page-line);
    border-radius: var(--radius);
    background: var(--brand-tint);
}

.eq-ti__autobid > legend {
    padding: 0 .35rem;
    font-size: var(--fs-eyebrow);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--ink-soft);
}

.eq-ti__autobid > label { font-size: var(--fs-small); font-weight: 650; }
.eq-ti__autobid > input { max-width: 16rem; }

/* The explanatory line goes on its own row rather than beside the button. */
.eq-ti__autobid > .note { flex: 1 0 100%; margin: 0; }

/* ---------------------------------------------------------------------------
   THE TOTALS LABEL

   Right-aligned against the three figures it introduces, because the eye
   travels from the number back to the word. It is a <th scope="row"> spanning
   five columns, so the default left alignment would put "Totals" a long way
   from the first figure.
   --------------------------------------------------------------------------- */
.eq-ti__totallabel { text-align: right; }

/* ---------------------------------------------------------------------------
   THE ERROR LIST

   The legacy's AddTradeInErrorList is an <asp:BulletedList> inside the popup,
   and the wording in it is reproduced verbatim in TradeInRules. This is the
   same shape: a real list, because it genuinely is several independent
   problems and a rep fixes them one at a time.

   No bullet marker -- the alert box it sits in already says "these are
   problems", and a bullet on a single-item list reads as an outline.
   --------------------------------------------------------------------------- */
.eq-errors {
    margin: 0;
    padding-left: 0;
    list-style: none;
}
.eq-errors > li { margin: 0 0 .2rem; }
.eq-errors > li:last-child { margin-bottom: 0; }


/* ==========================================================================
   31. THE OFFER TO PURCHASE WIZARD
   ==========================================================================
   THE QUOTE WIZARD'S PIECES, NOT A LOOK OF ITS OWN. Tim, 30 September 2026:
   "please bring the styling of the OTP process inline with the required
   styling for this system". Until then this section drew its own pill strip,
   an auto-fill grid of stacked fields and a dark pill button -- argued for on
   rule 5.5's "use the width", and it made the OTP the one wizard in the
   application that looked like a different product. It now renders with the
   house pieces: .qsteps (section 19), .eq-step, .eq-fields, .eq-clienttype and
   _QuoteForward's green arrow. What is left here is only what those do not
   already cover. The field ORDER is unchanged, which is what a rep navigates by.
   ========================================================================== */

/* A REACHED STEP IS A SUBMIT, not a link, so going back posts what is typed on
   the current step. It must read as the quote wizard's reachable tab, so the
   button's own chrome is reset and a.qstep's colour and hover are repeated --
   .qstep alone would leave the UA's button font and grey face. */
button.qstep {
    font: inherit;
    cursor: pointer;
    color: var(--eq-menu-ink);
}
button.qstep:hover {
    background: var(--eq-menu-hover);
    color: var(--eq-menu);
}

/* The form needs no box of its own; the step panel around it supplies that.
   Declared so the class is not silently undefined (StylesheetClassTests). */
.otpform { display: block; }

/* The legacy's own one-line notes under a step's heading, centred with it --
   a p's 68ch reading measure kept, and centred in the panel. */
.otpform__lead {
    max-width: 68ch;
    margin: var(--s2) auto var(--s3);
    text-align: center;
}

/* "Step n of 7 -- nothing is saved until Finalise", under the forward arrow. */
.otpform__note {
    max-width: none;
    margin: var(--s2) 0 0;
    text-align: center;
}

/* The way back to the quote, under the panel. */
.otpform__back {
    max-width: none;
    margin: 0 0 var(--s4);
    text-align: center;
}

/* A box the stock record filled, and so locked (step 4). Greyed, as the legacy
   greys a disabled box, so it reads as "not yours to change" before a rep tries
   to type -- and readonly rather than disabled, so it still posts. */
.eq-fields input[readonly] {
    background: var(--eq-page);
    color: var(--eq-body-ink);
}

/* Hobbies: four to a row on the legacy, but auto-fill here so the column count
   follows the panel. Ten short labels; each is the house .check label. */
.otphobbies {
    display: grid;
    gap: var(--s2) var(--s4);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr));
    margin: var(--s3) 0 var(--s4);
}

/* Step 6's two tick columns, centred as the legacy's ItemStyle-HorizontalAlign
   centres them (OfferToPurchase.aspx:3028-3037). */
.otpcash__tick {
    width: 7rem;
    text-align: center;
}

/* The OTP view's print and back buttons, under its last panel -- a .btn-row, with
   the rhythm of the panels above it. */
.otpview__actions { margin: 0 0 var(--s4); }

/* THE OTP VIEW'S FRAMED COLUMNS SIT AT THEIR OWN HEIGHT. .kv-cols stretches every
   column to the tallest, and a .kv stretches its rows to fill -- so beside a
   seven-row column a three-row one drew three double-height rows. The quote view
   rarely shows it, its columns being near-equal; an OTP's are not (most of the
   vehicle's descriptive columns are blank on an offer raised from a quote), so
   here each column keeps its natural height. Scoped to this page on purpose: the
   quote view's equal frames are the look Tim approved there. */
.otpview .kv-cols { align-items: start; }

/* ==========================================================================
   32. TRAINING
   ==========================================================================
   Restyled 1 September 2026 to match the legacy screen, on Tim's instruction and
   against his screenshots of EQnet/Training.aspx.

   Plain <details> accordions rather than a scripted one: no JavaScript, works with
   the script blocked, and the browser keeps the open state across a post. The chapter
   a rep is confined to must render under every condition, so it depends on nothing.
   The legacy uses jQuery UI's accordion widget; what is reproduced here is its LOOK,
   not its implementation.

   --------------------------------------------------------------------------
   WHERE THE COLOURS COME FROM, AND THE ONE DELIBERATE DIFFERENCE
   --------------------------------------------------------------------------
   The legacy bars are jQuery UI's Cupertino theme, loaded by Main.Master:17. Its
   actual values are:

       closed   bg #d7ebf9   border #aed0ea   ink #2779aa
       hover    bg #e4f1fb   border #74b2e2   ink #0070a3
       open     bg #3baae3   border #2694e8   ink #ffffff
       panel    bg #f2f5f7   border #aed0ea

   This uses the --eq-acc-* tokens instead, which are within a few percent of those
   and are ALREADY the accordion colours on the vehicle-details screen (section 28).
   One accordion blue in the application beats two that are almost the same, and the
   sibling screen's shade is the one Tim has already signed off. If an exact Cupertino
   match is ever wanted, change those six tokens rather than adding a seventh set here.

   The questions table is the opposite case: --eq-grid-head, --eq-grid-head-ink and
   --eq-grid-line ARE the legacy's ExtGridHeaderStyle values (#afd7fb / #15428b /
   #99bbe8, GM.css:493-509), so that part matches exactly.
   ========================================================================== */

/* The legacy fixes this at 1100px and centres it. Kept as a max rather than a fixed
   width, so a narrow window shrinks it instead of scrolling the page sideways --
   which rule 5.5 forbids outright. */
.tvid {
    width: min(100%, 1100px);
    margin-inline: auto;
    padding-block: var(--s4) var(--s6);
}

/* Centred, bold, BLACK small-caps. Not .eq-h3: that is grey, underlined and weight
   200, and the legacy heading here is an h1 carrying a Label with Font-Bold and
   ForeColor="Black" (Training.aspx:85), so it reads considerably heavier. */
.tvid__title {
    margin: 0 0 var(--s4);
    text-align: center;
    font-variant: small-caps;
    font-size: 1.4rem;
    font-weight: 700;
    color: #000;
}

/* The welcome copy, centred, as the legacy sets it. max-width is released because the
   rule at the top of this file gives every p a 68ch reading measure, and that would
   break the three centred lines into a narrow column. */
.tvid__intro {
    max-width: none;
    text-align: center;
    color: var(--eq-body-ink);
    margin: 0 0 var(--s5);
    line-height: 1.6;
}

.tvid__acc { margin-block: var(--s4); }

/* --------------------------------------------------------------------------
   THE CHAPTER BARS
   -------------------------------------------------------------------------- */
.tchapter { margin-bottom: 2px; }          /* jQuery UI's own 2px gap between headers */

.tchapter__head {
    display: flex;
    align-items: center;
    gap: .45rem;
    padding: .2rem .5rem .2rem .7rem;      /* the legacy's own !important override */
    line-height: 20px;
    cursor: pointer;
    border: 1px solid var(--eq-grid-line);
    border-radius: 6px;
    background: linear-gradient(to bottom, var(--eq-acc-shut-hi), var(--eq-acc-shut));
    color: var(--eq-acc-shut-ink);
    font-variant: small-caps;              /* inherited from BaseStyle's h3 in the legacy */
    font-weight: 700;
    font-size: 1rem;
    list-style: none;
}

/* The native disclosure triangle is replaced by one matching jQuery UI's
   ui-icon-triangle sprite, so the bar reads the same way. Hiding the marker is
   decoration ON the control, not the control -- MobileLayoutTests draws exactly
   that distinction. */
.tchapter__head::-webkit-details-marker { display: none; }
.tchapter__head::marker { content: ""; }

.tchapter__head:hover { border-color: var(--eq-acc-open); }

.tchapter[open] > .tchapter__head {
    background: linear-gradient(to bottom, var(--eq-acc-open-hi), var(--eq-acc-open));
    border-color: var(--eq-acc-open);
    color: var(--eq-acc-open-ink);
    border-radius: 6px 6px 0 0;
}

.tchapter__caret {
    flex: none;
    width: 0; height: 0;
    border-style: solid;
    border-width: .3rem 0 .3rem .4rem;                       /* pointing right, shut */
    border-color: transparent transparent transparent currentColor;
}
.tchapter[open] > .tchapter__head .tchapter__caret {
    border-width: .4rem .3rem 0;                             /* pointing down, open  */
    border-color: currentColor transparent transparent;
}

.tchapter__name { flex: 1 1 auto; }

/* The open pane. jQuery UI gives it the header's border minus the top edge and the
   bottom two corners, which is what makes bar and body read as one object. */
.tchapter__body {
    border: 1px solid var(--eq-grid-line);
    border-top: 0;
    border-radius: 0 0 6px 6px;
    background: #f2f5f7;
    padding: var(--s4) var(--s5);
}

/* The chapter title again, centred and bold, inside the pane. It looks redundant and
   it is deliberate: the legacy prints it twice (Training.aspx:109-111), once in the
   bar with the score appended and once here without it. */
.tchapter__subtitle {
    margin: 0 0 var(--s4);
    text-align: center;
    font-variant: small-caps;
    font-size: 1.15rem;
    font-weight: 700;
    color: #000;
}

/* 1024 x 576 centred, dropping to full width on a narrow screen -- the legacy's own
   .video rule and its 1060px breakpoint (Training.aspx:11-27), expressed as an aspect
   ratio so it cannot letterbox. */
.tchapter__video {
    width: min(100%, 1024px);
    margin: 0 auto var(--s4);
    aspect-ratio: 16 / 9;
    background: #000;
    overflow: hidden;
}
.tchapter__video iframe { width: 100%; height: 100%; border: 0; display: block; }

/* --------------------------------------------------------------------------
   THE QUESTIONS TABLE

   A real two-column table, as the legacy GridView renders. The wrapper scrolls
   sideways on a narrow screen rather than letting the PAGE scroll (rule 5.5).
   -------------------------------------------------------------------------- */
.tquiz-wrap { overflow-x: auto; margin-bottom: var(--s4); }

.tquiz {
    width: min(100%, 1024px);
    margin-inline: auto;
    border-collapse: collapse;
    background: #fff;
    font-size: var(--fs-body);
}
.tquiz th {
    background: var(--eq-grid-head);
    color: var(--eq-grid-head-ink);
    border: 1px solid var(--eq-grid-line);
    padding: 5px 3px 4px 5px;
    line-height: 20px;
    text-align: center;
    font-weight: 700;
}
.tquiz td {
    border: 1px solid var(--eq-grid-line);
    padding: .25rem .5rem;
    color: var(--eq-body-ink);
}

/* No row banding, because the legacy grid declares no RowStyle or AlternatingRowStyle
   on this screen -- only a HeaderStyle. Plain white rows with hairlines is the
   GridView default and is what the screen actually looks like. */

.tquiz__q { width: 60%; }
.tquiz__q label { font-size: inherit; font-weight: 400; color: inherit; }

.tquiz__a { text-align: center; }
.tquiz__a select { width: min(100%, 350px); }   /* the legacy's fixed 350px */

/* --------------------------------------------------------------------------
   THE ACTIONS
   -------------------------------------------------------------------------- */
.tchapter__actions { text-align: center; margin: 0; }

/* "Complete Chapter Questions" is a LinkButton in the legacy, coloured blue -- so it
   is a link here too, not a pill. It is still a real submit button underneath, so it
   keeps the busy state rule 5.4 requires; only its skin is a link. */
.tlink {
    border: 0;
    background: none;
    padding: 0;
    font: inherit;
    color: var(--eq-link);
    text-decoration: underline;
    cursor: pointer;
}
.tlink:hover { color: var(--eq-link-hover); }
.tlink[data-busy="true"] { pointer-events: none; opacity: .75; }

/* The chapter result. Plain red text under the link, exactly as lblErrorSave renders
   -- not a tinted alert box, which is what this screen had before. */
.tmsg {
    display: block;
    margin: var(--s2) 0;
    color: var(--bad);
    max-width: none;
}

.tvid__foot {
    max-width: none;
    text-align: center;
    color: var(--eq-body-ink);
    margin-top: var(--s5);
}
.tvid__foot .btn { margin-top: var(--s2); }

/* --------------------------------------------------------------------------
   NARROW SCREENS

   Nothing is hidden -- rule 5.4e. The table keeps both its columns and scrolls
   inside its own wrapper; the video and the bars simply narrow.
   -------------------------------------------------------------------------- */
@media (max-width: 760px) {
    .tvid { padding-inline: var(--s2); }
    .tchapter__body { padding: var(--s3); }
    .tquiz { font-size: var(--fs-small); }
    .tquiz__a select { width: 100%; min-width: 160px; }
}

/* ==========================================================================
   33. THE HOME PAGE DRILL-DOWN

   Each counter on the overview table opens the list behind it, as the legacy's
   popup does (Default.aspx.vb:1009-1027). Reproduced as a real <dialog open>
   rendered server-side when ?show= names a counter -- so the ordinary home
   page carries none of this markup, and closing it is a link rather than a
   script.

   Why a native <dialog> and not a positioned div: the browser supplies the
   backdrop, Escape-to-close and focus containment, all of which would
   otherwise be hand-written JavaScript on a page whose whole point is to be
   light. It also degrades honestly -- with <dialog> unsupported the list
   renders as a plain block instead of vanishing.
   ========================================================================== */

.eq-drill {
    width: min(96vw, 1100px);
    max-height: 86vh;
    overflow: auto;
    padding: 0;
    border: 1px solid var(--eq-grid-line);
    border-radius: var(--radius);
    background: #fff;
    color: var(--ink);
    box-shadow: var(--eq-lift);
}

/* The dialog is centred by the browser; the backdrop is dimmed here because
   the default is close to transparent in some engines. */
.eq-drill::backdrop { background: rgba(16, 24, 40, .45); }

/* The title bar sticks, so a long list keeps its heading and its close control
   in view -- the same argument as rule 5.4d for grid headings. */
.eq-drill__bar {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s3) var(--s4);
    background: var(--eq-grid-head);
    border-bottom: 1px solid var(--eq-grid-line);
}

.eq-drill__title {
    margin: 0;
    font-size: 1rem;
    font-weight: 700;
    font-variant: small-caps;
    color: var(--eq-grid-head-ink);
}

/* A link, not a button, because it navigates back to the page without the
   query parameter -- which is what actually closes this. Sized past the 44px
   touch floor rule 5.4e sets. */


.eq-drill .grid-scroll,
.eq-drill .note { margin: var(--s4); }

/* The table inside the dialog is already bounded by the dialog's own
   max-height, so it must NOT also take --grid-viewport -- two scrollbars for
   one list is the trap rule 5.4e records for the phone layout. */
.eq-drill .grid-scroll { max-height: none; }


/* ==========================================================================
   34. REMINDERS

   The grid itself needs nothing: the traffic-light rules in section 25 already
   colour a row from zone-red / zone-green / zone-black, and the legacy applies
   exactly the same three colours here as it does to the call status on Open
   Quotes (Default.aspx.vb:608-639 against OpenQuotes.aspx.vb:365-390). Same
   meanings, same hex values, so a second set of classes would be two things to
   keep in step for no visual gain.

   What IS needed is the same three colours on a run of INLINE text, for the
   one-line summary above the grid ("6 overdue. 1 due today."). The zone classes
   cannot serve that: they are scoped to table.grid tr so that a stray class on
   a div cannot repaint a panel.

   Deliberately not tokens. These two are the legacy's own fixed call-status
   values and must not follow a theme -- red means overdue at every dealership,
   in both light and dark, and the whole point of section 25's note is that reps
   rely on the specific colours.
   ========================================================================== */

.zone-red-text   { color: #cc0000; }
.zone-green-text { color: #459e00; }

/* ==========================================================================
   35. THE CLIENT DETAILS FORM

   Transcribed from the legacy screen on Tim's instruction of 3 September 2026:
   "I would like you to copy the styling from this page."

   The legacy is a two-column label/value table with alternating row stripes,
   and required boxes filled pink with a red border. Reproduced because a rep of
   twenty years finds a field by where it sits and what colour it is, and this
   form has 33 of them -- a redesign would cost recognition for no gain.

   Source classes, from EQnet/Styles/BaseStyle.css:

       input.Error     { background-color: #ffb5b5; border: 1px solid red; }
       input.ErrorMax  { ... as above, plus width: 320px; }
       .ErrorTextSmall { color: red; font-weight: bold; font-size: 11px; }
       .AltTableRow    { background-color: #eaeaea; }

   TWO RED TREATMENTS, TWO MEANINGS -- this is the part worth understanding
   before changing anything here:

     .eq-req    a pink BOX. "This dealership requires this field." Comes from
                DealershipRequiredFieldsTbl and is present from the first render,
                before the rep has typed anything.
     .eq-bad    "You left this out just now." Only after a refused save.
     .eq-reqsmall  red bold text. "At least one of this GROUP." Hard-coded in the
                legacy rather than configurable, and it applies to the phone
                numbers.

   A field can carry .eq-req, .eq-bad, both, or neither. Keeping them separate is
   what lets the screen say "this was always required" and "and you missed it"
   as two different statements.
   ========================================================================== */

/* The header notice. Always rendered, as the legacy does -- it explains the
   convention, and a notice that appears and disappears teaches nobody what pink
   means. */
.eq-reqnote {
    color: var(--error-ink);
    font-weight: 700;
    text-align: center;
    margin: 0 0 1rem;

    /* Opting out of the 68ch reading measure that section 5.5 gives every <p>.
       This is a one-line label on a form, not running prose. */
    max-width: none;
}

/* --------------------------------------------------------------------------
   Client Type. Centred above the fields, exactly where the legacy puts it,
   because it decides which fields are policed and so has to be chosen first.
   -------------------------------------------------------------------------- */
.eq-clienttype {
    border: 0;
    margin: 0 0 1rem;
    padding: 0;
    text-align: center;
}

.eq-clienttype legend {
    float: none;
    padding: 0 0 0.4rem;
    font-weight: 600;
    color: var(--ink);
}

/* Red and bold, as the legacy has them (ForeColor="Red" Font-Bold="true").
   Loud for a label, and deliberately so: picking the wrong type puts a client
   under the wrong rule set and the wrong contact-number branch. */
.eq-ct {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    margin: 0 0.9rem;
    color: var(--error-ink);
    font-weight: 700;

    /* 44px touch floor, per rule 5.4e. A radio a rep cannot hit on a phone is a
       control that is effectively hidden, which that rule forbids. */
    min-height: 44px;
}

/* FLEET CLIENT: rendered and disabled, never removed. Rule 5.4e forbids hiding
   a control a rep can operate, and omitting it would read as "that option was
   taken away" rather than "not rebuilt yet". Muted so it does not compete with
   the two that work, and it keeps the cursor that says so. */
.eq-ct--off {
    color: var(--ink-soft);
    font-weight: 600;
    cursor: not-allowed;
}

/* --------------------------------------------------------------------------
   The field table.
   -------------------------------------------------------------------------- */
.eq-form {
    border-collapse: collapse;

    /* Not full width, and not the app surface's 92% either. A key-and-value row
       stretched across a wide monitor separates the label from its box far
       enough that the eye loses the row -- the same argument that put the 2200px
       ceiling in rule 5.5, applied to a form. The legacy's own table is about
       600px (Panel Width="600px"); this is a little wider for the longer labels
       and then stops. */
    width: min(100%, 720px);
    margin: 0 auto;
}

.eq-form th,
.eq-form td {
    padding: 0.3rem 0.6rem;
    text-align: left;
    vertical-align: middle;
    font-weight: 400;
}

/* The label column. Fixed share rather than shrink-to-fit, so the boxes line up
   down the form instead of stepping in and out with the length of each label. */
.eq-form th {
    width: 15rem;
    color: var(--ink);
}

.eq-form th label { cursor: pointer; }

/* The stripe. #eaeaea in the legacy; --brand-tint here so it survives dark
   mode, where a hard-coded light grey would be a white bar across a dark form.
   The legacy's own class is .AltTableRow, applied row by row in code-behind;
   nth-child does the same job with no markup at all. */
.eq-form tbody tr:nth-child(even) { background: var(--brand-tint); }

/* The note row under the contact numbers -- no label, no stripe of its own, so
   it reads as a footnote to the group above rather than as another field. */
.eq-form__note { background: none !important; }

.eq-form input,
.eq-form select {
    /* Inherit the app's own control styling, then let the modifiers below change
       only the colours. 16px minimum on a phone, per rule 5.4e, or Android zooms
       the whole page on focus. */
    font: inherit;
    padding: 0.25rem 0.4rem;
    border: 1px solid var(--border-strong);
    background: var(--surface);
    color: var(--ink);
    max-width: 100%;
}

/* The legacy's width: 320px, from ErrorMax / DropMax. Applied by class rather
   than to every input, because the phone-number and postal-code boxes are
   deliberately short -- a 320px box for a 10-character cell number tells the rep
   the wrong thing about what goes in it. */
.eq-form .eq-wide { width: 20rem; }

/* --------------------------------------------------------------------------
   THE TWO RED TREATMENTS.
   -------------------------------------------------------------------------- */

/* Required by this dealership. #ffb5b5 fill, 1px red border -- the legacy's
   input.Error exactly. */
.eq-form .eq-req {
    background: var(--error-bg);
    border: 1px solid var(--bad);
    color: #1a1a1a;
}

/* Rejected on the last save. A heavier border on the same fill, so a rep
   scanning a refused form can tell "the eight pink ones are required" from "and
   these two are why it would not save". Not a different colour: a third colour
   on a form that already has two would have to be learned. */
.eq-form .eq-bad {
    background: var(--error-bg);
    border: 2px solid var(--bad);
    color: #1a1a1a;
    outline: none;
}

/* The INK on those two is a literal #1a1a1a rather than --ink, and that is
   deliberate. --error-bg is a fixed pink in both themes because it is a MEANING
   and not a surface, so the text on it has to be a fixed dark too -- letting
   --ink apply would put near-white text on pink in dark mode and make the
   required fields the only unreadable part of the screen. */

/* "(one of these are required)". The legacy's .ErrorTextSmall: red, bold, 11px.
   Kept small so three of them down the phone block do not shout over the fields
   they annotate. */
.eq-reqsmall {
    color: var(--error-ink);
    font-weight: 700;
    font-size: 0.6875rem;
}

/* --------------------------------------------------------------------------
   Phone. The label/value table becomes stacked pairs.

   NOTHING IS HIDDEN, per rule 5.4e -- every field, every label and every notice
   is still present and operable; the label simply moves above its box instead of
   beside it, because a 15rem label column leaves nothing for the input on a
   400px screen.
   -------------------------------------------------------------------------- */
@media (max-width: 760px) {
    .eq-form,
    .eq-form tbody,
    .eq-form tr,
    .eq-form th,
    .eq-form td {
        display: block;
        width: auto;
    }

    .eq-form tr { padding: 0.35rem 0; }

    .eq-form th {
        padding-bottom: 0.15rem;
        font-weight: 600;
    }

    /* Full width on a phone, overriding the 20rem measure -- there is no second
       column to protect. */
    .eq-form .eq-wide,
    .eq-form input,
    .eq-form select {
        width: 100%;

        /* 16px floor. Below this Android zooms the viewport on focus and the rep
           has to pinch back out after every field. */
        font-size: max(1rem, 16px);
        min-height: 44px;
    }

    .eq-clienttype { text-align: left; }
    .eq-ct { display: flex; margin: 0.2rem 0; }
}

/* --------------------------------------------------------------------------
   THE MESSAGE THAT SITS BESIDE THE FIELD

   Tim, 3 September 2026, comparing a rejected e-mail address with a rejected
   ID number: the browser's own e-mail check "pointed me exactly where I needed
   to fix the problem", while the server's ID check put "the red at the top of
   the screen, and at the bottom, I wasn't aware what the problem was."

   So a server-side rejection now says WHY next to the box it is about, and
   app.js section 12 brings the screen to it. The summary at the top of the form
   stays -- it is what shows a rep everything at once when several fields are
   wrong -- but it is no longer the only signal.

   Styled as the legacy's .ErrorTextSmall (red, bold, 11px), because that is
   already what small red print means on these screens. It sits BELOW its field
   rather than beside it: several of these messages are a full sentence, and on
   the same line they would push a 320px input off the edge of the table.
   -------------------------------------------------------------------------- */
.eq-fielderr {
    display: block;
    margin-top: 0.15rem;
    color: var(--error-ink);
    font-weight: 700;
    font-size: 0.6875rem;

    /* Long enough to read, narrow enough not to widen the value column and
       stretch every other row in the table with it. */
    max-width: 22rem;
}

/* The rejected control itself. Deliberately not a third colour -- the pink and
   red already mean "wrong" on this form -- so what marks it out from a merely
   required field is the heavier border set by .eq-bad, plus the focus ring the
   browser draws when app.js puts the cursor here. */
.eq-form [aria-invalid="true"] {
    border-color: var(--bad);
}

/* ==========================================================================
   36. THE CLIENT LAYER

   Tim, 3 September 2026: "can we make this a layer that floats on top of the
   original page? we going to work on the next tabs".

   The legacy's own shape -- Default.aspx:232 is an overlay titled CLIENT DETAILS
   with a close link and nine tabs, opened from the client screen and from the
   quote screen alike.

   A REAL <dialog>, so the top layer, the backdrop, Escape-to-close and focus
   trapping all come from the browser rather than from an overlay of our own.
   The same reason the searchable pickers in this app are real dialogs.

   The `open` attribute is written by the SERVER. That is what makes the layer
   survive a post: a refused save re-renders with it still open, so the rep's
   form and its messages are still in front of them. A layer opened only by
   script would vanish on exactly the request where it must not.
   ========================================================================== */

.eq-dialog {
    /* Wide, because the form is a label column plus a value column and 33 rows of
       it. Capped the same way rule 5.5 caps a page: past a point width stops
       helping. The legacy's own overlay is 1241px, which is where 1240px comes
       from -- but it is a min() so a laptop or a phone gets the viewport. */
    width: min(96vw, 1240px);

    /* ====================================================================
       THE LAYER IS A CONSTANT HEIGHT. THE TABS MUST NOT MOVE.
       ====================================================================
       Tim, 4 September 2026: "as I'm navigating my way through the headings,
       so client details, client notes and reminders, client vehicles, the page
       is stretching and expanding depending on how much content is on each
       tab. The thing that I don't want to happen is I don't want the top of
       the page to move ... if the size remains the same and obviously on a
       shorter set of information there's just blank space at the bottom. That
       would be my better option."

       THIS WAS max-height, AND max-height IS WHAT MOVED THE TABS. A modal
       dialog is centred by the UA (`inset: 0` plus `margin: auto`), so its
       height is content-driven up to the cap and the leftover space is split
       ABOVE AND BELOW IT. Client Details fills the cap and the strip sits near
       the top of the screen; Client Vehicles with two rows is a third as tall,
       so half the difference is taken off the top and the whole layer -- title
       bar, tab strip and all -- slides down the screen. Nothing was scrolling
       and nothing was wrong with the tabs: the BOX changed size underneath
       them.

       A stated height fixes it at the cause. The flex column then hands the
       leftover height to .eq-dialog__body, which is Tim's "blank space at the
       bottom" -- and it costs a short tab nothing, because the body was
       already the scrolling part.

       92vh, NOT 100vh: the UA clamps a modal to about `100% - 2em - 6px`
       anyway, so asking for the full viewport would silently be reduced to a
       DIFFERENT number and put the guesswork back. Under that clamp the value
       we ask for is the value we get. */
    height: 92vh;

    padding: 0;
    border: 1px solid var(--border-strong);
    border-radius: var(--r2, 6px);
    background: var(--surface);
    color: var(--ink);
    box-shadow: 0 18px 50px rgb(0 0 0 / 28%);
}

/* THE DISPLAY IS SCOPED TO [open], AND THAT IS NOT A DETAIL.

   Tim, 3 September 2026: "When I press the x at the top right to close that
   layer, the x in the menu disappears, but it's not actually clearing the rest
   of the layer. And the screen behind still scrolls up and down behind it, but
   you can't actually interact with it."

   The cause: this rule used to set `display: flex` unconditionally, which
   OVERRIDES the browser's own `dialog:not([open]) { display: none }`. So
   close() did its half of the job -- out of the top layer, backdrop gone,
   interactivity gone -- while the element stayed VISIBLE in normal flow, sitting
   over the page as a dead picture of itself. And because it was no longer in the
   top layer it rendered at its document position instead, which is why the title
   bar went out of view while the fields stayed.

   Scoping the display to [open] hands the closed state back to the browser. Any
   `<dialog>` given a display of its own needs the same treatment -- the older
   .eq-modal avoids the trap by never setting one and using :modal instead.

   The FLEX COLUMN is what makes the body the only scrolling part, so the title
   bar and the tab strip stay put while the fields move. */
.eq-dialog[open] {
    display: flex;
    flex-direction: column;
}

/* Only a MODAL dialog gets ::backdrop, so with the script blocked -- when the
   layer is a plain <dialog open> in the page flow -- there is no stray dark
   sheet over the screen. */
.eq-dialog::backdrop {
    background: rgb(15 23 33 / 55%);
}

/* The title bar. Slate chrome, matching the app's own bar and the legacy's
   overlay heading. */
.eq-dialog__bar {
    /* Never shrinks. Without this the flex column is free to squash it when the
       body is tall -- which is what clipped the tab strip below it. */
    flex: 0 0 auto;

    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3, 1rem);
    padding: .55rem .9rem;
    background: var(--eq-menustrip, #465c71);
    color: #fff;
}

.eq-dialog__title {
    margin: 0;
    font-size: 1rem;
    font-weight: 700;
    letter-spacing: .04em;
    color: #fff;
}

/* ===========================================================================
   THE CLOSE BUTTON -- ONE X FOR THE WHOLE APPLICATION

   Tim, 4 September 2026, looking at the Client Overview: "Can we fix the x in
   the top right hand corner ... That x is very difficult to see. I would like
   to have a standardized X button across the system."

   THE CLASS IS .eq-x AND NOT .eq-close, WHICH WAS ALREADY TAKEN -- by the
   quote CLOSURE panel (won/lost), a whole component with a dozen __ children.
   The first draft used .eq-close and every close cross silently inherited a
   bordered panel with a 56rem max-width. Nothing failed and nothing warned;
   only reading the SERVED stylesheet found it. Two meanings of the word
   "close" in one stylesheet is the trap, and .eq-x cannot be mistaken for
   either.

   IT WAS THREE DIFFERENT BUTTONS. .eq-dialog__close on the overview,
   .eq-modal__close on the pickers and .eq-drill__close on the home drill-down
   -- three sizes, three colours, three hover behaviours. This replaces all of
   them, and the old class names are gone rather than aliased so a fourth
   cannot quietly appear.

   ============================================================
   WHY IT WAS HARD TO SEE, AND BOTH CAUSES WERE REAL
   ============================================================
   1. THE HOVER COLOUR. It was `color: var(--eq-link, #cfe2f3)`. The fallback is
      a pale blue that would have been fine, but --eq-link IS defined, as
      #034af3 -- a saturated blue. So hovering turned the X bright blue on the
      dark slate bar, which is the state Tim screenshotted. A fallback that
      never applies is not a safety net.

   2. IT HAD NO BOX. The overview's X became an <a> earlier the same day, so
      that closing could navigate back to where the rep came from. min-width
      and min-height DO NOT APPLY to a non-replaced inline element, so the 44px
      touch target rule 5.4e requires silently disappeared and the control
      shrank to the glyph. Nothing warned; the anchor looked correct.

   So the standard is display: inline-flex -- which makes the box real for an
   <a> and a <button> alike -- and it NEVER changes hue, only weight.

   ============================================================
   HOW ONE CLASS WORKS ON A DARK BAR AND A PALE ONE
   ============================================================
   color: inherit. All three bars set their own ink (#fff on the overview,
   --eq-menu-ink on a picker, --eq-grid-head-ink on the drill-down), so the X is
   always the bar's own text colour and cannot be wrong against it.

   The hover fill is a pseudo-element painting currentColor at low opacity,
   NOT color-mix(). color-mix would be tidier and needs a browser this project
   cannot assume -- the target is low-spec machines, some of them old. An
   absolutely-positioned overlay works everywhere and costs nothing.
   =========================================================================== */
.eq-x {
    /* inline-flex, not inline: this is what gives the box its size at all.
       min-width and min-height DO NOT APPLY to a non-replaced inline element, so
       an inline <a> silently has no box and shrinks to the bare glyph -- which
       was half of why the old cross was hard to see and hard to hit. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;

    /* 36px reads as a button on a dense bar without making the bar taller. The
       44px touch floor is applied under the mobile breakpoint, where a thumb
       rather than a pointer is aiming at it. */
    min-width: 36px;
    min-height: 36px;
    padding: 0;
    margin: 0;
    border-radius: var(--radius-sm);

    /* ====================================================================
       THE COLOURS ARE STATED. NOT color: inherit -- AND THAT IS THE FIX
       THAT SHOULD HAVE BEEN MADE THE FIRST TIME.
       ====================================================================
       The first attempt took `color: inherit` so one class could serve a dark
       bar and a pale one. On paper it is right: every bar declares its own ink,
       and `.eq-x` at (0,1,0) outranks the bare `a` rule at (0,0,1). Tim
       reported the cross still looked wrong, and re-reading both the stylesheet
       and the rendered markup showed each to be correct -- which means the
       value the element actually computed was not the one the cascade implied.

       Rather than diagnose that a third time by inference, the colour is now
       DECLARED. An explicit value cannot be lost to an inherited one, and a
       translucent white plate under a white glyph is legible on every dark bar
       in this application without depending on what its parent says.

       ====================================================================
       AND THE THIRD DIAGNOSIS WAS FINALLY MADE, 16 September 2026.
       ====================================================================
       Declaring the colour was still not enough, and the paragraph above says
       why without realising it: it weighs `.eq-x` against "the bare `a` rule"
       and never looks for a THIRD rule. There is one.

       _Layout wraps every page in <main class="app-surface eq-slab">, and
       `.eq-slab a:not(.btn)` is (0,2,1) -- it outranks `a.eq-x` at (0,1,1) and
       painted every close cross in the application the ambient link colour.
       Tim saw it on the appointment booking layer: "x to close layer styling
       issue".

       Fixed at that rule by excluding .eq-x, exactly as .btn was already
       excluded there. THE LESSON, because it cost three attempts: when a
       declared value does not win, the rule that beats it is not always near
       the rule you are reading -- search for every selector that can MATCH the
       element, not only the ones that describe it.

       a.eq-x and button.eq-x are named as well, at (0,1,1), so no
       element-level anchor rule -- present or future -- can outrank it.
       ==================================================================== */
    background: rgba(255, 255, 255, .16);
    border: 1px solid rgba(255, 255, 255, .55);
    color: #fff;

    font: inherit;
    font-size: 1.2rem;
    font-weight: 700;
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
}

/* Element-qualified. Same declarations; the point is the selector. */
a.eq-x,
button.eq-x {
    background: rgba(255, 255, 255, .16);
    border: 1px solid rgba(255, 255, 255, .55);
    color: #fff;
    text-decoration: none;
}

/* WEIGHT, NOT HUE. Hover and keyboard focus brighten the plate; neither changes
   the colour. Changing it is the original mistake: the old rule went to
   var(--eq-link, #cfe2f3), and --eq-link IS defined -- as #034af3 -- so hovering
   turned the cross bright blue on a dark slate bar. A fallback that never
   applies is not a safety net. */
.eq-x:hover,
.eq-x:focus-visible,
a.eq-x:hover,
a.eq-x:focus-visible,
button.eq-x:hover,
button.eq-x:focus-visible {
    background: rgba(255, 255, 255, .32);
    color: #fff;
    text-decoration: none;
}

.eq-x:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
   ON A PALE BAR -- the home page drill-down sits on --eq-grid-head (#afd7fb).

   A MODIFIER RATHER THAN INHERITANCE, deliberately. One class adapting by
   `color: inherit` is exactly what was just removed: elegant, dependent on the
   cascade behaving as the stylesheet reads, and wrong in the browser twice. Two
   stated skins cannot be ambiguous about what they paint.
   --------------------------------------------------------------------------- */
.eq-x--onlight,
a.eq-x--onlight,
button.eq-x--onlight {
    background: #fff;
    border: 1px solid var(--eq-grid-line);
    color: var(--eq-grid-head-ink);
}
.eq-x--onlight:hover,
.eq-x--onlight:focus-visible,
a.eq-x--onlight:hover,
a.eq-x--onlight:focus-visible {
    background: var(--eq-acc-shut);
    color: var(--eq-grid-head-ink);
    text-decoration: none;
}
.eq-x--onlight:focus-visible {
    outline: 2px solid var(--eq-grid-head-ink);
    outline-offset: 2px;
}





/* The body scrolls; the bar and tabs above it do not. Same argument as rule
   5.4d's sticky column headings: the thing that tells you where you are must not
   leave the screen. */
/* THE BODY IS THE ONLY THING THAT SCROLLS, and it takes all the leftover height.

   flex: 1 1 auto with min-height: 0 is the pair that matters, and the min-height is
   the half everyone forgets: a flex item will not shrink below its CONTENT size
   without it, so the body stayed as tall as 33 form rows and the browser took the
   space out of the bar and the tab strip above it instead. That is what clipped the
   tabs and gave them a scrollbar. */
.eq-dialog__body {
    flex: 1 1 auto;
    min-height: 0;

    overflow-y: auto;
    /* overscroll-behavior moved to .is-scrollable -- see that rule. */
    padding: var(--s3, 1rem);
}

/* ===========================================================================
   `overscroll-behavior: contain` IS FOR OVERLAYS. A GRID HANDS THE SCROLL BACK.

   Tim, 11 September 2026, on the staff LIST: "If my mouse cursor is inside that
   list, and I'm trying to scroll inside that list, I cannot scroll. But if I
   move my cursor outside of the list, I can scroll the page. This needs to be
   looked at system wide and a very definitive rule put in place so that this
   can't happen again."

   and, 12 September 2026, having scrolled a grid to its last row:

     "once the inner element gets to the bottom of its possible scroll range,
      can additional scrolling now control the outer element? So in theory, I
      can still scroll and would scroll the outer element down without me having
      to move my mouse cursor outside of the inner element."

   THE RULE, in two halves:

       1. `contain` may only be IN EFFECT on an element that can actually
          scroll at that moment. Never declare it statically.
       2. It applies to OVERLAYS ONLY -- a dialog, a modal, a menu. A DATA GRID
          chains: at its last row the page takes over.

   WHY HALF ONE. `overflow: auto` makes a box a scroll CONTAINER whether or not
   its content overflows, and `contain` applies to a scroll container regardless
   of whether it has any scroll range. So on a SHORT list the box scrolls
   nowhere, refuses to pass the wheel on, and the page sits still. The wheel
   appears dead over exactly the content the reader is looking at. It was
   declared statically on .grid-scroll and .eq-dialog__body, so every grid and
   every dialog was one short list away from it. app.js section 22 measures each
   box and adds .is-scrollable only while scrollHeight exceeds clientHeight.

   WHY HALF TWO, and it REVERSES a line in rule 5.4d rather than refining it.
   5.4d asked for `contain` on the grid box "or the page lurches the moment the
   grid reaches its last row". Tim has now used that and decided the opposite
   trade is better: the grid box is nearly viewport-tall, so reaching its last
   row leaves the totals row and the footer below the fold with no way to get to
   them except moving the cursor off the table. An overlay is different in kind
   and keeps `contain` -- a modal that scrolls the page behind it is a genuine
   annoyance, and the page behind is not where the reader is working.

   NOTHING IMPLEMENTS THE CHAINING. Chaining is what a browser does unaided;
   `contain` is what switches it off. So this is a deletion for grids, which is
   why it could be promised as reliable -- there is no substitute mechanism to
   go wrong, and no wheel listener anywhere in this application (there is not
   one today, and section 22's header explains why that is deliberate).

   THE ONE THING CSS CANNOT DO, so nobody hunts for it: browsers LATCH a wheel
   gesture to the element it started in, so a single continuous flick still
   stops at the grid's edge and it takes a fresh scroll for the page to take
   over. That latching is the browser's own defence against the very lurch 5.4d
   feared. Removing the pause would need a wheel handler that second-guesses
   momentum scrolling -- offered, and rejected, 12 September 2026.

   A GRID INSIDE A DIALOG STILL COMPOSES: the grid chains to the dialog body,
   the body contains at its own end, and the page behind a modal cannot move.

   The `overscroll-behavior-x: contain` declarations in the mobile bands are
   deliberately untouched: they constrain the HORIZONTAL axis on side-scrolling
   strips, so a vertical wheel is not affected by them.
   =========================================================================== */
.eq-dialog__body.is-scrollable,
.eq-modal__body.is-scrollable,
.nav details > .submenu.is-scrollable { overscroll-behavior: contain; }

.eq-dialog__foot {
    display: flex;
    gap: var(--s2, .5rem);
    align-items: center;
    margin-top: var(--s3, 1rem);
}

/* ---------------------------------------------------------------------------
   A ONE-FORM LAYER -- content height, and only as wide as the form

   Tim, 11 September 2026, on the User Details layer: "can you apply a similar
   styling to the user details button / page". The screenshot showed a ten-row
   form in a 1240px box 92vh tall, so most of the layer was empty grey.

   WHY THIS IS A MODIFIER AND .eq-dialog MUST NOT BE CHANGED. The base rule's
   `height: 92vh` is not a default that nobody thought about -- it is Tim's
   ruling of 4 September, and the comment on it explains at length that a
   max-height let the Client Overview's TAB STRIP slide down the screen as the
   reader moved between tabs of different lengths. Relaxing it there would put
   that back.

   The staff editors have no tabs. Each is ONE form that cannot change height
   while it is open, so the argument for a stated height simply does not arise,
   and a content-sized box is right. max-height still caps a long one.

   Two widths because the two layers genuinely differ, and the legacy sizes them
   separately for the same reason: its Details panel is 500px and its Rights
   panel 602px (StaffAdministration.aspx:595, :956). Ours are wider because the
   captions are words rather than abbreviations.
   --------------------------------------------------------------------------- */
.eq-dialog--form {
    width: min(96vw, 46rem);
    height: auto;
    max-height: 92vh;

    /* THE BODY SCROLLS, NOT THE FRAME. Paired with the form rule below --
       this line ALONE removes the frame's scrollbar without giving the body
       one, which leaves a layer that cannot be scrolled at all. See below. */
    overflow: hidden;
}

/* ---------------------------------------------------------------------------
   THE FORM HAS TO CARRY THE FLEX CHAIN, AND THIS IS THE REAL FIX

   Tim, 11 September 2026: "the wheel on my mouse doesn't interact with the
   scroll bar of this layer", then after my first attempt: "all you've done is
   remove the scroll bar on the side ... if anything, you have made it worse".

   He was right both times, and my first diagnosis was wrong. I blamed
   overscroll-behavior. THE ACTUAL CAUSE IS THE DOM:

       .eq-dialog[open]  is display:flex, flex-direction:column
       .eq-dialog__body  is flex:1 1 auto; min-height:0; overflow-y:auto

   That pair is what makes the body the scroller -- and it only works while the
   body is a DIRECT flex child of the dialog. On the client overview it is. On
   the staff editors a <form> wraps the body and the footer, so the body is a
   GRANDCHILD: it has no flex parent, flex:1 1 auto does nothing, its height is
   its content, and it never overflows and never scrolls.

   The frame scrolled instead, because a <dialog> gets overflow:auto from the UA
   stylesheet -- which is why dragging the frame's scrollbar worked while the
   wheel over the body did nothing. Adding overflow:hidden to the frame then
   took away the one thing that DID scroll, and nothing replaced it.

   So the form becomes both a flex ITEM of the dialog and a flex CONTAINER for
   the body and footer, and the shrink chain reaches the body again.

   THE LESSON, because it generalises past this screen: .eq-dialog__body only
   scrolls when every ancestor between it and the dialog is a flex column with
   min-height:0. Wrapping it in anything -- a form, a div, a partial -- breaks
   that silently, and the failure looks like a CSS bug rather than a structural
   one. DialogScrollTests asserts the pairing.
   --------------------------------------------------------------------------- */
.eq-dialog--form > form {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}
.eq-dialog--form.eq-dialog--wide { width: min(96vw, 58rem); }

/* The footer as a real bar rather than a button floating on the body's ground:
   its own surface, a rule above it, and the buttons CENTRED -- which is where
   the legacy puts them (a 33% centred cell, :805-806). Never scrolls away,
   because .eq-dialog__foot sits outside .eq-dialog__body. */
.eq-dialog__foot--bar {
    flex: 0 0 auto;
    justify-content: center;
    margin-top: 0;
    padding: var(--s3);
    background: var(--eq-panel-quiet);
    border-top: 1px solid var(--eq-rule);
}

/* --------------------------------------------------------------------------
   THE TAB STRIP

   Captions are verbatim from Default.aspx:238-247. Unbuilt tabs are rendered and
   inert rather than omitted, per rule 5.4e -- a tab that vanishes reads as a
   feature that was removed, and a rep needs to see that the rest are coming.

   Scrolls sideways rather than wrapping: seven captions including "Client
   Prospect Contact Form" will not fit a laptop, and a strip that wraps to three
   rows pushes the form off the screen.
   -------------------------------------------------------------------------- */
/* THE TAB STRIP DOES NOT SCROLL. IT WRAPS.

   Tim, 3 September 2026: "that section is too small, and as you can see, you can't
   read the text, and there's actually a side scroll bar for that section. Please
   extend that to make sure that all of that is visible. There shouldn't be any
   scroll bar to the right hand side of that section."

   This had overflow-x: auto, and that was the whole fault -- it is the same trap
   rule 5.4d records in CLAUDE.md and it caught its own author. THERE IS NO BOX
   THAT SCROLLS HORIZONTALLY AND NOT VERTICALLY: setting overflow-x to auto
   computes overflow-y to auto as well. So the strip became a scroll container in
   both axes, and once the flex column squeezed it (see .eq-dialog__body) it
   clipped its captions and grew the vertical scrollbar in the screenshot.

   Fixed by removing the overflow entirely and letting the row WRAP instead. That
   is what guarantees "all of that is visible" at every width, which a scroller
   never can -- a scroller hides tabs behind an edge and hopes somebody drags it.
   At the layer's 1240px all seven captions sit on one row anyway, matching the
   legacy's single-row strip; a narrow window gets two rows rather than a hidden
   tab.

   flex: 0 0 auto so the column cannot squash it again. */
/* --------------------------------------------------------------------------
   THE TAB STRIP IS THE LEGACY'S CUPERTINO SKIN.

   Tim, 5 September 2026, shown the flat version: "please use a bit more
   colour. this is the legacy styling and i think it looks better."

   So the colours are the legacy's own, taken from the stylesheet rather than
   sampled off a screenshot. The legacy strip is jQuery UI's CUPERTINO theme
   (EQNetAdmin/Scripts/jQuery/css/cupertino/jquery-ui-1.10.3.custom.css:822),
   which is a different family from the Ext grid skin the rest of this
   application borrows -- and that is faithful, not inconsistent: in the legacy
   the tabs really are cupertino and the grids really are Ext.

     inactive  border #aed0ea, background #3baae3 under an 80% white "glass"
               overlay, bold, black text
     active    border #2694e8, the SAME #3baae3 under a 50% overlay, bold,
               white text

   Cupertino gets those two tints from a pair of PNG gradients. Self-hosting a
   bitmap for a tab background is the sort of thing this project exists to
   remove, so the overlays are reproduced as gradients over the one blue --
   which is why both tints are expressed from --eq-tab-blue rather than
   hard-coded: the legacy's own relationship between the two states is that
   they are one colour at two strengths, and writing two unrelated hex values
   would lose that.
   -------------------------------------------------------------------------- */

.eq-tabs {
    flex: 0 0 auto;

    background: var(--eq-tab-strip);
    border-bottom: 1px solid var(--eq-tab-line-on);
    padding-top: 4px;
}

.eq-tabs ul {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    margin: 0;
    padding: 0 .5rem;
    list-style: none;
}

/* .eq-tab is a <button> on every inactive tab and a <span> on the active one, so
   the browser's own button skin has to be reset here -- otherwise the strip shows
   three different shapes for three states of the same control. Font is inherited
   for the same reason: a button does not inherit it by default and the strip
   would render in the UA's form font. */
.eq-tab {
    display: inline-block;
    padding: .45rem .8rem;
    font: inherit;
    font-size: .8125rem;
    line-height: inherit;
    text-align: left;

    /* Cupertino's inactive tab: the one blue, heavily lightened, with a real
       border and a rounded top. The flat version had a transparent border and
       no background, which is what read as colourless. */
    border: 1px solid var(--eq-tab-line);
    border-bottom: 0;
    border-radius: 4px 4px 0 0;
    background: var(--eq-tab-off);
    color: var(--eq-tab-off-ink);
    font-weight: 700;
    cursor: pointer;

    /* A caption never breaks mid-phrase -- "Client Prospect Contact Form" on three
       lines is unreadable. The UL wraps BETWEEN tabs; each tab stays on one line. */
    white-space: nowrap;

    /* 44px touch floor, per rule 5.4e. A tab a rep cannot hit on a phone is a
       control that is effectively hidden, which that rule forbids. */
    min-height: 44px;
}

/* A tab that can be pressed says so on hover. The active one and the unbuilt ones
   are not buttons in the interactive sense, so they are excluded. */
.eq-tab:hover:not(.eq-tab--on):not(.eq-tab--soon) {
    background: var(--eq-tab-hover);
    color: var(--eq-tab-on-ink);
    border-color: var(--eq-tab-line-on);
}

/* The <details> summary that opens the repeat question also carries .btn, and a
   summary shows a disclosure marker by default. Removed in both syntaxes -- the
   ::-webkit- form is still needed on the WebView the Android shell uses. */
.eq-note__tick { list-style: none; }
.eq-note__tick::-webkit-details-marker { display: none; }

/* The active tab joins the body it belongs to -- lifted, and its bottom edge
   removed so it reads as the front of the stack. Cupertino's own active state:
   the SOLID blue with WHITE text, which is the contrast the flat white-on-tint
   version was missing. */
.eq-tab--on {
    background: var(--eq-tab-on);
    border-color: var(--eq-tab-line-on);
    color: var(--eq-tab-on-ink-solid);
    font-weight: 700;
    margin-bottom: -1px;
    padding-bottom: calc(.45rem + 1px);

    /* White text on a mid blue is the one place on this strip where a hairline
       of separation earns its keep -- without it the lettering sits directly on
       a saturated ground and reads soft on a low-contrast panel. */
    text-shadow: 0 1px 0 rgba(0, 0, 0, .25);
}

/* NOT BUILT YET. Deliberately the only state with NO colour: it is not a tab a
   stock controller can open, so it must not compete with the ten that are. The
   same signal .nav-soon gives elsewhere, so the state is recognisable across
   the application. */
.eq-tab--soon {
    background: none;
    border-color: transparent;
    color: var(--muted);
    font-weight: 400;
    font-style: italic;
    cursor: not-allowed;
}

@media (max-width: 760px) {
    /* Full bleed on a phone: a 96vw layer with a rounded border wastes the only
       axis that matters. */
    .eq-dialog {
        width: 100vw;
        max-width: 100vw;

        /* Constant height here too, for the reason in the base rule -- and full
           bleed, because a phone has no room to spare.

           TWO DECLARATIONS ON PURPOSE. On a phone 100vh is the LARGE viewport,
           which includes the space behind the browser's own address bar, so a
           layer that tall has its last rows pushed out of sight. 100dvh is the
           visible height and tracks the bar as it hides. A browser that does
           not know dvh keeps the vh line, which is what it does today. */
        height: 100vh;
        height: 100dvh;
        max-height: 100dvh;

        border: 0;
        border-radius: 0;
    }
}

/* ---------------------------------------------------------------------------
   PRINT -- THE HEIGHT IS RELEASED

   The stated height above is a SCREEN rule: it exists so the tab strip cannot
   move while a rep clicks between tabs. On paper there is no strip to hold
   still, and a viewport-relative height resolves to one page box -- so keeping
   it would print the first page's worth of the tab and clip the rest inside the
   body's own scroller, silently.

   Height auto and overflow visible let the content run onto as many pages as it
   needs, which is the only useful thing a printed client record can do.
   --------------------------------------------------------------------------- */
@media print {
    .eq-dialog {
        height: auto;
        max-height: none;
    }

    .eq-dialog__body {
        overflow: visible;
    }
}

/* ==========================================================================
   37. THE CLIENT NOTES / REMINDERS TAB

   Tim, 3 September 2026: "The next tab I would like to work on is the client
   notes slash reminders. Please look at the screenshot and then look at the
   legacy code."

   The layout is the legacy's, because a rep of twenty years finds this screen by
   its shape: the client and their numbers on the left, the add-note form on the
   right, then a rule, the two filters, and the history beneath.

   THE COLOURS ARE THE ONES THAT CARRY MEANING and they are the same three the
   home-page Reminders screen uses -- red overdue, green today, black later
   (Main.Master:44-66, and DOSSIER-Reminders section 5). They are on the TYPE and
   the DUE DATE only. The note text and the added-date stay in the body colour in
   the legacy, which is what makes the colour read as "this reminder is overdue"
   rather than "this whole row is an error".

   THE ROW STRIPE is the legacy's RepeatItemStyle0/1 from GM.css -- #e0eefb and
   white. Worth knowing why that file: GM.css looks like a per-dealer theme and is
   not. Login.aspx.vb:387 sets EQnet_CSSFileName to the literal "GM.css" on every
   login path unconditionally, and ClientCodes.CSSFileName is empty for all 351
   dealers, so the striping is universal. Full record in
   Docs/DOSSIER-ClientNotes.md section 9, which also corrects two claims in
   CLAUDE.md about that stylesheet.

   The stripe is expressed through --brand-tint rather than as #e0eefb, so it
   survives dark mode -- a hard-coded pale blue would be a light bar across a dark
   list.
   ========================================================================== */

/* The top block: client on the left, add-note form on the right, as the legacy's
   two-cell table has them. Grid rather than a table because there is no tabular
   relationship between the two -- and it collapses to one column on a phone. */
.eq-notes {
    display: grid;
    grid-template-columns: minmax(14rem, 20rem) minmax(0, 1fr);
    gap: var(--s3, 1rem);
    align-items: start;
}

.eq-notes__name {
    margin: 0 0 .6rem;
    font-size: 1.05rem;
    color: var(--ink);
}

.eq-notes__contact {
    border-collapse: collapse;
    width: 100%;
    margin: 0 0 .75rem;
}

.eq-notes__contact th,
.eq-notes__contact td {
    padding: .15rem .4rem .15rem 0;
    text-align: left;
    font-weight: 400;
    vertical-align: top;
}

.eq-notes__contact th {
    width: 9rem;
    color: var(--ink-soft);
}

/* A control that exists but is not wired yet -- the appointments button. Muted
   and not-allowed, the same signal .eq-tab--soon and .nav-soon give, so the state
   is recognisable across the whole application. Rendered rather than omitted,
   per rule 5.4e. */
.eq-notes__soon {
    opacity: .55;
    cursor: not-allowed;
}

.eq-notes__row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s3, 1rem);
}

.eq-notes__field {
    display: flex;
    flex-direction: column;
    gap: .2rem;
    margin: 0 0 .5rem;
}

.eq-notes__field > span { color: var(--ink-soft); }

.eq-notes__field--wide { display: block; }

.eq-notes__field select,
.eq-notes__field input[type="date"],
.eq-notes__field input[type="text"],
.eq-notes__add textarea {
    font: inherit;
    padding: .25rem .4rem;
    border: 1px solid var(--border-strong);
    background: var(--surface);
    color: var(--ink);
    max-width: 100%;
}

/* The note box fills the column, as the legacy's Columns="60" Rows="5" does in
   its own table cell. */
.eq-notes__add textarea {
    width: 100%;
    min-height: 6.5rem;
    resize: vertical;
}

/* A locked date box reads as TEXT and not as an empty input, which is the point:
   the legacy's DisabledTextBox is transparent with no border for exactly this
   reason. readonly rather than disabled, because a disabled control posts
   nothing and a derived Birthday date would be thrown away. */
.eq-notes__field input[type="date"][readonly] {
    background: none;
    border-color: transparent;
    color: var(--ink-soft);
    cursor: default;
}

/* The legacy's own prompt above the note box. Small and quiet -- it is an
   instruction, not a heading. Opting out of the 68ch reading measure that rule
   5.5 gives every <p>. */
.eq-notes__prompt {
    margin: .25rem 0;
    font-size: .8125rem;
    color: var(--ink-soft);
    max-width: none;
}

.eq-notes__hint {
    font-size: .8125rem;
    color: var(--ink-soft);
    max-width: none;
}

.eq-notes__save {
    text-align: center;
    margin: .5rem 0 0;
}

.eq-notes__rule {
    border: 0;
    border-top: 1px solid var(--border);
    margin: var(--s3, 1rem) 0;
}

/* -------------------------------------------------------------------------
   The two filters.
   ------------------------------------------------------------------------- */
.eq-notes__filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem var(--s3, 1rem);
    margin: 0 0 var(--s3, 1rem);
}

.eq-notes__filter {
    display: inline-flex;
    align-items: center;
    gap: .35rem;

    /* 44px touch floor, per rule 5.4e. */
    min-height: 44px;
    cursor: pointer;
}

.eq-notes__filter-pair {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .35rem .6rem;
}

.eq-notes__empty {
    color: var(--ink-soft);
    max-width: none;
}

/* -------------------------------------------------------------------------
   WHICH DATES AM I LOOKING AT, and the way to see the rest.

   Tim, 4 September 2026: the notes list on a busy client ran to "ten, twenty
   pages" before the row he had clicked, so it is now bounded to a month either
   side of today. That makes the caption load-bearing rather than decorative:
   a filtered list with no caption cannot be told apart from an empty one, and
   a rep would read two rows on a nine-year client as "no history".

   Deliberately quiet -- ink-soft, no border, no panel. It is an answer to a
   question the rep has not asked yet; it must be findable without competing
   with the reminders underneath it, which are the actual work.
   ------------------------------------------------------------------------- */
.eq-notes__window {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem var(--s3, 1rem);
    margin: 0 0 var(--s2, .5rem);
}

.eq-notes__windowtext {
    margin: 0;
    color: var(--ink-soft);

    /* Out of the 68ch reading measure that rule 5.5 gives running prose. This is
       a status line above a data list, not a paragraph, and wrapping it early
       pushes the button below it for no reason. */
    max-width: none;
}

/* The toggle, rendered above the list only when the whole history is showing and
   below it always -- see _ClientNotesShowAll.cshtml for why both.

   Its own margin at the top, because under a long list it needs separating from
   the last row or it reads as part of it. */
.eq-notes__showall {
    margin: var(--s3, 1rem) 0 0;
}

/* Above the list the partial sits inside the caption row, where the row's own gap
   already spaces it -- so the top margin would push the button off the baseline
   of the text beside it. */
.eq-notes__window .eq-notes__showall {
    margin: 0;
}

/* -------------------------------------------------------------------------
   The history.
   ------------------------------------------------------------------------- */
.eq-notelist {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* The legacy's cell border, 1pt #dbd8d8. Rows butt against each other with a
   single shared edge -- border-collapse behaviour, done by hand because this is a
   list and not a table. */
.eq-note {
    border: 1px solid var(--border);
    border-top: 0;
    padding: .5rem .6rem;
}

.eq-note:first-child { border-top: 1px solid var(--border); }

/* RepeatItemStyle0 / RepeatItemStyle1. Striped by INDEX in the markup rather than
   by nth-child, because the legacy stripes the rows it actually renders -- so a
   filtered list still alternates properly. */
.eq-note--0 { background: var(--brand-tint); }
.eq-note--1 { background: var(--surface); }

/* ---------------------------------------------------------------------------
   THE NOTE THE REP ARRIVED TO SEE.

   Tim, 4 September 2026, on clicking a Birthday row in the reminders list: it
   "highlights in that same blue colour ... the line entry that is associated to
   the line entry I clicked in my reminders list."

   THE COLOUR IS THE LEGACY'S OWN, AND IT IS NOT THE GRID'S SELECTION BLUE.
   ClientNotes.ascx.vb:437 sets an inline background-color of #99CCFF on the
   note row, where the grids use #a0dbf9 (BaseStyle.css GridSelectedRowStyle).
   Two different blues for two different lists, in the system Tim is comparing
   against -- so both are reproduced rather than unified. Held as a token so the
   value exists once, unlike the legacy's inline style.

   AFTER the stripe rules, so it outranks .eq-note--0/1 at equal specificity.
   The stripe class is deliberately left on the element: a highlighted note is
   still an odd or even row, and it goes back to being striped the moment the
   highlight is gone.
   --------------------------------------------------------------------------- */
.eq-note.is-lastnote {
    background: var(--eq-note-picked);

    /* A left edge as well as the fill. The fill alone is the legacy's whole
       treatment, and on a list of pale blue and white rows one more blue can be
       missed at a glance -- which defeats the point of pointing at it. */
    box-shadow: inset 3px 0 0 var(--eq-selected-ink);
}

.eq-note__head {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem var(--s3, 1rem);
    align-items: baseline;
    font-size: .8125rem;
}

.eq-note__added,
.eq-note__user { color: var(--ink); }

/* The three that carry meaning. Bold on the two urgent ones, plain on the third,
   exactly as the legacy sets Font-Bold only for red and green. */
.eq-note__type--overdue { color: var(--bad); font-weight: 700; }
.eq-note__type--today   { color: var(--good, #459E00); font-weight: 700; }
.eq-note__type--future  { color: var(--ink); }

/* white-space: pre-wrap is load-bearing. The Valuation note type seeds a SIX-LINE
   template ("Brand :", "Model :", ...) and the legacy renders it into an asp:Label
   that collapses every newline, so a rep sees one run-on line. Preserving them is
   the difference between a form and a sentence. */
.eq-note__body {
    margin: .35rem 0 0;
    white-space: pre-wrap;
    color: var(--ink);
}

.eq-note__action {
    margin: .4rem 0 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
}

.eq-note__done {
    display: inline-flex;
    align-items: baseline;
    gap: .4rem;
    color: var(--good, #459E00);
    font-weight: 700;
}

.eq-note__doneat {
    font-size: .8125rem;
    font-weight: 400;
    color: var(--ink-soft);
}

/* A reminder that exists and cannot be acted on yet -- not due for another
   fortnight, or waiting on the deferred mileage panel. Shown rather than hidden,
   because the rep needs to know it is there, and muted so it does not look like a
   button that is broken. */
.eq-note__soon {
    font-size: .8125rem;
    color: var(--muted);
    cursor: help;
}

/* -------------------------------------------------------------------------
   The repeat question.

   A <details> panel rather than a modal, which is what the legacy uses (two
   stacked jQuery UI dialogs at z-index 10000000). The panel needs no script at
   all, cannot end up behind an overlay, and rule 5.4a keeps the rep's place when
   it posts.
   ------------------------------------------------------------------------- */
/* The <details> itself. inline-block so its summary sits on the action row with
   the other controls rather than taking a line of its own, and the panel it opens
   still flows beneath. */
.eq-note__repeat {
    display: inline-block;
}

.eq-note__repeatbody {
    margin: .5rem 0 0;
    padding: .6rem;
    border: 1px solid var(--border-strong);
    background: var(--surface);
}

.eq-note__repeatbody p {
    margin: 0 0 .5rem;
    color: var(--bad);
    max-width: none;
}

.eq-note__repeatform {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    gap: .5rem var(--s3, 1rem);
}

.eq-note__reason {
    display: flex;
    flex-direction: column;
    gap: .2rem;
    font-size: .8125rem;
    color: var(--ink-soft);
}

.eq-note__reason input {
    font: inherit;
    padding: .25rem .4rem;
    border: 1px solid var(--border-strong);
    background: var(--surface);
    color: var(--ink);
    min-width: 14rem;
}

@media (max-width: 760px) {
    /* One column on a phone: the client block above the add-note form, in the
       order the markup already has them. Nothing is hidden -- rule 5.4e -- it is
       re-laid-out. */
    .eq-notes { grid-template-columns: 1fr; }

    .eq-notes__contact th { width: 8rem; }

    /* The head line stacks rather than wrapping mid-value, so "Added :" keeps its
       date and "User :" keeps its name. */
    .eq-note__head { flex-direction: column; }
}

/* ==========================================================================
   38. THE CLIENT VEHICLES TAB

   Tim, 3 September 2026: "Please start work on viewing the customer vehicle
   information."

   The third tab of the client layer, transcribed from EQnet/ClientVehicle.ascx.
   Nine columns in the legacy's order, VIEW ONLY for this pass -- the legacy's
   three editable cells and its Update/Remove links are deferred to their own
   pass, and the tab says so at its foot rather than leaving a rep to wonder.

   Almost all of this section is TYPOGRAPHY AND WIDTH. The grid itself needs no
   new rules: .grid, .grid-wrap and .grid-scroll already carry the sticky
   heading of rule 5.4d, the internal scroll, and the card layout of rule 5.4e.
   Reusing them is the point -- a heading that sticks on one report and not
   another is worse than either.
   ========================================================================== */

.eq-vehicles {
    /* The legacy wraps its grid in a div of max-height 550px with overflow auto
       (ClientVehicle.ascx:22), so an internal scroll is what a rep of twenty
       years already expects here. That is .grid-scroll's job now, and it does it
       with the heading pinned, which the legacy's div did not. */
    padding: var(--s3) 0 0;
}

.eq-vehicles__head {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: var(--s3);
    margin-bottom: var(--s3);
}

.eq-vehicles__title {
    /* Centred and letter-spaced because the legacy's is an h2 with align="center"
       rendered in small caps by the theme (see Tim's screenshot of 3 September).
       The shape of the heading is how the tab is recognised. */
    margin: 0;
    font-size: 1.05rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.eq-vehicles__count {
    /* Not in the legacy. Added because the first question about a list is how
       long it is, and one client on FOR123 holds 276 vehicles. */
    font-size: 0.8rem;
    color: var(--muted);
}

.eq-vehicles__empty {
    /* The legacy's EmptyDataText, which it renders inside the grid frame. Here it
       replaces the grid entirely, so it needs its own centring. */
    text-align: center;
    padding: var(--s5) 0;
    color: var(--muted);

    /* Opt out of the 68ch reading measure that p carries for running prose. This
       is a status line, not prose, and a measure would left-align it oddly. */
    max-width: none;
}

table.grid--vehicles td.num, table.grid--vehicles th.num {
    /* Mileage right-aligned rather than centred, which is how .grid--leads treats its
       .num cells. Right is correct here because mileages are compared down the column
       and aligned digits make that possible; the leads grid centres because its .num
       cells hold single-character markers, not quantities. */
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.eq-vehicles__vin {
    /* A VIN is 17 unbroken alphanumerics and there are nine columns beside it, so
       without this one cell decides the width of the whole grid and pushes
       Mileage and Last Service Date off the edge.

       anywhere, not break-all: it breaks the VIN only when the cell is genuinely
       too narrow, rather than breaking every long value on principle. And the
       font is tabular so a broken VIN still reads as one string rather than as
       two words. */
    overflow-wrap: anywhere;
    font-variant-numeric: tabular-nums;
}

.eq-vehicles__baddate {
    /* The Last Service Date column is nvarchar(100), so it can hold text that is
       not a date at all. This marks such a value rather than presenting it as a
       date -- see Docs/DEFECT-ClientVehicles-TextDates.md.

       Deliberately NOT the error pink. Nothing is broken and the rep has done
       nothing wrong; the value simply was never a date. Error styling here would
       have a rep phoning support about data that is merely untidy. */
    font-style: italic;
    color: var(--muted);
}

.eq-vehicles__note {
    /* What is not reproduced, stated on screen. Rule 5.0 item 3: an unreproduced
       behaviour must read as a decision rather than an oversight. */
    margin: var(--s3) 0 0;
    font-size: 0.8rem;
    color: var(--muted);
    text-align: center;
    max-width: none;
}

@media (max-width: 760px) {
    /* Rule 5.4e: nothing interactive is hidden, the grid becomes cards. There is
       nothing interactive on this tab at all in this pass, so all that is needed
       is to stop the heading and the footnote fighting for one line. */
    .eq-vehicles__head { flex-direction: column; align-items: center; gap: var(--s1); }

    /* In card mode each value is labelled from data-label, so a VIN sits on its
       own line with room to breathe and needs no break. */
    .eq-vehicles__vin { overflow-wrap: normal; }
}

/* ==========================================================================
   39. THE CLIENT QUOTE LINK

   Every row of a client's quote list opens that quote in the layer's Quote tab
   (Tim, 3 September 2026). The legacy does the same -- selecting a row in that
   grid opens the layer at tab index 3 (Default.aspx.vb:1642).

   It has to be a BUTTON, because the layer's state lives in the POST body and a
   GET would arrive with none of it. So this makes a submit button look like the
   link it behaves as. Distinct from .linkbtn, which is the white variant used in
   the blue topbar and would be invisible in a grid cell.
   ========================================================================== */

.eq-quotelink {
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    color: var(--eq-link);
    text-decoration: underline;
    cursor: pointer;

    /* The Quote # column is .mono, and the button must inherit that rather than
       reset to the body font -- a quote number is compared down the column. */
    font-family: inherit;
}

.eq-quotelink:hover,
.eq-quotelink:focus-visible { color: var(--eq-link-hover); }

/* --------------------------------------------------------------------------
   The Client Quote tab's own two states. The quote itself needs no rules --
   it is _QuoteDetail, which both quote pages already style.
   -------------------------------------------------------------------------- */

.eq-cquote { padding: var(--s5) 0; }

.eq-cquote__empty {
    /* The legacy tab is simply BLANK until a rep clicks a quote row
       (Default.aspx.vb:301). Said in words instead, so a rep can tell a working
       empty tab from a broken one -- and told what to do next.

       max-width: none opts out of the 68ch reading measure p carries for running
       prose; this is a status line, and a measure would left-align it oddly. */
    text-align: center;
    color: var(--muted);
    max-width: none;
}

/* ===========================================================================
   38. THE ADMIN USED VEHICLE STOCKLIST

   Replaces the list half of EQNetAdmin/VehicleStocklistVer3.aspx. Read-first
   record: Docs/DOSSIER-UsedStocklist.md.

   COLOUR CARRIES MEANING HERE, as the call traffic lights do on the home page,
   and the values are the legacy's own (GridStock_RowDataBound,
   VehicleStocklistVer3.aspx.vb:1432-1476) so a stock controller of twenty years
   reads the new list the way they read the old one:

     green Stock No cell      required fields captured AND brochure ready
     pink row                 required fields missing (the row's title says which)
     pink M&M Code cell       IgnoreMMCategory
     cyan Make + Model cells  consignment

   Two shades of each, alternating with the band, exactly as the legacy
   alternates MediumSpringGreen/PaleGreen and #9DFEFC/#CAFEFD by row parity --
   so the tint reads over the striping rather than fighting it.

   EVERY RULE PAINTS > td, NEVER THE ROW. Rule 5.4f: a background on the <tr>
   paints underneath its cells, so the band's own cell fill would hide it.
   These sit late in the file on purpose -- they share specificity with the
   band rules in section 8 and must come after them to win.
   =========================================================================== */
:root {
    --stock-ready-odd:   #00fa9a;   /* MediumSpringGreen -- the legacy's odd rows  */
    --stock-ready-even:  #98fb98;   /* PaleGreen         -- the legacy's even rows */
    --stock-missing:     #ffb6c1;   /* LightPink         -- the whole row          */
    --stock-consign-odd: #9dfefc;
    --stock-consign-even:#cafefd;
}

table.grid.stock-grid tbody tr > td.stock-ready                  { background: var(--stock-ready-even); }
table.grid.stock-grid tbody tr:nth-child(odd) > td.stock-ready   { background: var(--stock-ready-odd); }

/* The pink row beats the band AND the green cell: a vehicle cannot be both
   ready and missing fields, but the cell rule below would otherwise win on
   order, so the row rule is written with the higher specificity. */
table.grid.stock-grid tbody tr.stock-missing > td,
table.grid.stock-grid tbody tr.stock-missing > td.stock-ready     { background: var(--stock-missing); }

table.grid.stock-grid tbody tr > td.stock-ignoremm               { background: var(--stock-missing); }

table.grid.stock-grid tbody tr > td.stock-consign                { background: var(--stock-consign-even); }
table.grid.stock-grid tbody tr:nth-child(odd) > td.stock-consign { background: var(--stock-consign-odd); }

/* Hover keeps the meaning. The generic hover tint would replace a green or
   pink cell with the brand tint for as long as the pointer rests on it, which
   makes the colour flicker off exactly when the admin is looking at that row. */
table.grid.stock-grid tbody tr:hover > td.stock-ready,
table.grid.stock-grid tbody tr:hover > td.stock-ignoremm,
table.grid.stock-grid tbody tr:hover > td.stock-consign,
table.grid.stock-grid tbody tr.stock-missing:hover > td          { filter: brightness(.96); }

/* ---------------------------------------------------------------------------
   A REMOVED VEHICLE IS A RED ROW. That is the legacy's signal, and all of it.

   Tim, 21 September 2026: "the legacy logic of a removed vehicle is just red
   font. And I would prefer to stay with the legacy so that people don't need
   to learn new systems and new functionality."

   THE NOTE THAT USED TO BE HERE SAID "Not a legacy colour" AND WAS WRONG.
   It claimed removal was conveyed only by sorting these rows last and by a
   separate Removed grid, and on that reasoning invented muted ink plus a
   "REMOVED" tag. The legacy does exactly one thing --
   VehicleStocklistVer3.aspx.vb:2621, `SRow.ForeColor = Drawing.Color.Red` --
   and the tag was solving a problem that did not exist while eating width in
   the narrowest column of a twenty-column grid.

   #FF0000 because that is literally Drawing.Color.Red. NOT the #d40000 used
   for a breached threshold: this is a reproduction, and a stock controller of
   twenty years is reading the same colour they read today. Measured for
   honesty rather than to argue with it -- 4.0:1 on white and 3.5:1 on the
   banded row, both under WCAG AA's 4.5 for body text. #d40000 would give
   5.9:1 and is one token away if that ever matters more than fidelity.

   IT PAINTS > td, not the <tr>, for the reason rule 5.4f gives: a colour on
   the row loses to anything the cell sets.

   NOT REPRODUCED, and recorded rather than left to be discovered: the legacy
   also sets a row TOOLTIP naming the removal date, the date the vehicle moves
   to Removed Stock, the dealership's RemoveStockDay preference, who removed it
   and why (`:2622`). The read model carries only a bool, so the three columns
   behind it are not queried -- adding them is a deliberate change to the
   stocklist query, not a side effect of a colour fix.
   --------------------------------------------------------------------------- */
table.grid.stock-grid tbody tr.is-removed > td { color: #FF0000; }

/* The EDIT placeholder: rendered and inert (rule 5.4e), the same not-yet
   signal .eq-tab--soon and .nav-soon give elsewhere. */
.stock-soon {
    color: var(--muted);
    font-size: var(--fs-small);
    cursor: not-allowed;
}

/* The three totals, wording verbatim from the legacy. One line on a wide
   screen, three on a narrow one. */
.stock-totals {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem var(--s4, 1.5rem);
    margin: var(--s2, .5rem) 0;
    color: var(--ink-soft);
}

.stock-totals strong { color: var(--ink); font-variant-numeric: tabular-nums; }

/* The legend. The legacy explains none of its colours; this does, always. */
.stock-legend {
    margin: 0 0 var(--s2, .5rem);
    font-size: var(--fs-small);
    color: var(--muted);
    max-width: none;   /* a status line, not prose -- out of the 68ch measure */
}

.stock-key {
    display: inline-block;
    padding: 0 .4rem;
    border-radius: var(--radius-sm, 3px);
    color: var(--ink);
}
.stock-key--ready    { background: var(--stock-ready-even); }
.stock-key--missing  { background: var(--stock-missing); }
.stock-key--ignoremm { background: var(--stock-missing); }
.stock-key--consign  { background: var(--stock-consign-even); }

/* Print keeps the colours: they are the information. */
@media print {
    table.grid.stock-grid tbody tr > td.stock-ready,
    table.grid.stock-grid tbody tr.stock-missing > td,
    table.grid.stock-grid tbody tr > td.stock-ignoremm,
    table.grid.stock-grid tbody tr > td.stock-consign { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

/* A form that sits inside a table cell beside other controls.

   A destructive action must be a POST, and a POST needs a form -- but a <form>
   is display: block, so it drops the button onto its own line and the row grows
   a third taller. Inline-block keeps it beside the Edit link while leaving the
   form a real form.

   Used by the Recon tab's Remove. Any row action that destroys something needs
   the same shape, so this is a utility rather than a recon class. */
.eq-inline {
    display: inline-block;
    margin: 0;
}

/* ===========================================================================
   31. BROCHURE IMAGES -- Dealership Details section 10b
   ===========================================================================
   Four independent widgets, each showing what a branch has and offering to
   change it. A grid rather than a stack because they are peers: an
   administrator comparing "which of these do we have" reads four cards far
   faster than four full-width blocks separated by a scroll.

   auto-fit with a 260px floor, so it is four across on a wide monitor, two on
   a laptop and one on a phone with no breakpoint of its own -- rule 5.4e
   wants a re-layout rather than anything hidden, and this gets it for free. */
.eq-images {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 16px;
  margin: 12px 0 4px;
}

.eq-image {
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 12px;
  background: var(--surface);
}

.eq-image__name {
  margin: 0 0 8px;
  font-size: 0.95rem;
  font-weight: 600;
}

/* The picture itself. max-width keeps a 1920-wide stock background inside its
   card (rule 5.5), and the checkerboard shows through a TRANSPARENT PNG --
   which matters here, because preserving transparency is the one thing this
   section's scaler exists to get right. Without it a transparent logo looks
   identical to a white one, and nobody would notice it had been flattened. */
.eq-image__shot {
  display: block;
  max-width: 100%;
  height: auto;
  border: 1px solid var(--border);
  border-radius: 4px;
  background-color: #fff;
  background-image:
    linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%),
    linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%);
  background-size: 16px 16px;
  background-position: 0 0, 8px 8px;
}

/* The file input is left as the browser's own. A styled one would need script
   to report the chosen filename, and with script blocked the rep would have no
   idea whether their file had been picked. */
.eq-image__pick {
  display: block;
  margin: 8px 0;
}

.eq-image__pick input[type="file"] {
  max-width: 100%;
}

/* ===========================================================================
   32. LEAD DISTRIBUTION -- Dealership Details section 4c
   ===========================================================================
   Two editors in one pane, New and Used, each with an origin picker, a method
   and a rotation grid. Boxed so the eye can tell where one ends and the other
   begins -- without it they read as one very long form, and saving the wrong
   half changes which salespeople get leads. */
.eq-dist {
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 12px 14px;
  margin: 12px 0;
}

/* Continuing the heading scale below .eq-h3, which already exists. Sized by
   the same steps so a fourth and fifth level do not arrive as body text. */
.eq-h4 {
  margin: 0 0 8px;
  font-size: 1rem;
  font-weight: 600;
}

.eq-h5 {
  margin: 14px 0 6px;
  font-size: 0.9rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}


/* ---------------------------------------------------------------------------
   NOTE OF THANKS -- a preview of the letter that actually gets posted.

   Not a legacy feature. It is here because the five boxes on the legacy screen
   cannot show the one thing nobody guesses: the LEFT signature prints UNDER the
   rep's own name, so it is a job title rather than a name. See
   Docs/DOSSIER-NoteOfThanks.md section D3.

   The two line treatments are the consumer's, not decoration:
     __body  keeps newlines, because ThanksPDF converts them to <br /> for the
             body and the right signature;
     __line  collapses them, because it does NOT for the header, footer and left
             signature -- so a two-line footer really does print as one line.
   --------------------------------------------------------------------------- */
.thanks-preview {
    padding: var(--s4);
    border: 1px dashed var(--border);
    border-radius: 10px;
    background: var(--surface);
}

.thanks-preview__body { white-space: pre-wrap; }

/* Collapsed on purpose -- this is what the printed document does. */
.thanks-preview__line {
    white-space: normal;
    color: var(--muted);
}

.thanks-preview__signs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s4);
    justify-content: space-between;
    margin: var(--s5) 0 var(--s4);
}

.thanks-preview__signs > div {
    flex: 1 1 14rem;
    min-width: 0;          /* or a long signature refuses to shrink */
}

.thanks-preview__rule {
    display: block;
    border-top: 1px solid var(--ink);
    margin-bottom: .25rem;
}

.thanks-preview__rep { display: block; font-style: italic; color: var(--muted); }

/* ---------------------------------------------------------------------------
   A SORT-ORDER BOX. Narrow on purpose: it holds a small integer, and a
   full-width number input in a grid cell pushes every other column off the
   screen on a laptop. The legacy sets Width="30px" on the same control.
   --------------------------------------------------------------------------- */
.num-sm {
    width: 4.5rem;
    min-width: 0;
}

/* A qualifier inside a page heading -- "(Used On Invoices)". Quieter and lighter
   than the heading it sits in, because it explains the title rather than being
   part of it. The legacy carries the same words at the same weight as the rest
   of its <h1>, which makes the heading read as one long sentence. */
.h1-qual {
    font-weight: 400;
    font-size: .7em;
    color: var(--muted);
    white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   A YES/NO CELL THAT IS A FORM, NOT A CHECKBOX
   ---------------------------------------------------------------------------
   Training Settings flips three per-rep flags. The legacy uses AutoPostBack
   checkboxes, each of which uploads the whole ViewState; these are three-field
   forms instead. One form per cell keeps the posts independent -- flipping one
   flag must not carry the other two -- and .inline-form is what stops each one
   claiming a block of its own inside the table cell. */
.inline-form { display: inline; margin: 0; }

/* Quiet enough to read as a VALUE rather than an action, while still being the
   real control. The state is the label, so the eye reads down the column. */
.btn--quiet {
    background: transparent;
    color: var(--ink);
    border-color: var(--border);
    padding: 0.15rem 0.6rem;
    min-width: 3.2rem;
}
.btn--quiet:hover { background: var(--brand-tint); text-decoration: none; }

/* ---------------------------------------------------------------------------
   SECTION 33 -- LEAD STAGE PILLS (Reports > Franchise Leads)

   The legacy paints cell backgrounds directly: LightGreen, LightYellow,
   LightPink (ReportFLeads.aspx.vb:28-84). Those literals are unreadable in dark
   mode and carry no meaning to a screen reader, so the three states become
   tokens here and each pill also carries a WORD (concern FLD-5).

   The greens and reds are the estate's own call-status traffic lights
   (CLAUDE.md 3.10, Main.Master:44-66), tinted for a background rather than used
   as text, so a rep of twenty years reads the same signal.
   --------------------------------------------------------------------------- */
.stage {
    display: inline-block; padding: .1rem .5rem; border-radius: var(--radius-sm);
    font-size: var(--fs-small); font-weight: 600; white-space: nowrap;
}
.stage--done   { background: color-mix(in srgb, #459E00 22%, transparent); color: inherit; }
.stage--missed { background: color-mix(in srgb, #E8A800 28%, transparent); color: inherit; }
.stage--out    { background: color-mix(in srgb, #CC0000 18%, transparent); color: inherit; }

a.stage { text-decoration: underline; }

.legend { display: flex; flex-wrap: wrap; gap: var(--s4); margin-block: var(--s3); }

/* ---------------------------------------------------------------------------
   SECTION 34 -- THE USED STOCKLIST'S ROW HEIGHT

   Tim, 14 September 2026, looking at a stocklist where one vehicle occupied six
   lines: "my goal here is to reduce the Height of a vehicle line. to do this, we
   need to give more width to the Model column."

   THE ROW HEIGHT IS SET BY THE MODEL COLUMN AND BY NOTHING ELSE. Every other
   cell on the row is short -- a stock number, a year, a colour, a number -- so
   the row is exactly as tall as the model name needs, and a model like
   "Tiguan 1.4 TSi Bluemotion Trend-Fun (118KW)" wrapped to five lines in a
   column barely wide enough for two words. Widening it is the whole fix; a
   smaller font or tighter padding would shrink every row by a pixel or two and
   leave the six-line rows six lines tall.

   Where the width comes from, in order:

     1. Min Sales Price Incl Vat is REMOVED from this grid (Tim, same message).
        That is the widest heading on the row and it frees real space.
     2. The headings break over two lines, so "Vehicle Location" no longer
        forces its column to the width of both words side by side.

   min-width, not width: a table still distributes what is left over, so this
   sets a floor rather than freezing the layout. 22rem holds the longest model
   name measured on FOR123 on two lines instead of five.
   --------------------------------------------------------------------------- */
table.grid.stock-grid th.stock-model,
table.grid.stock-grid td.stock-model {
    min-width: 22rem;
}

/* The headings now carry an explicit <br />, and thead th is white-space:
   nowrap so each LINE stays unbroken. line-height is tightened because two
   uppercase eyebrow-sized lines at the body line-height are taller than the
   tallest data cell, which would put the height straight back. */
table.grid.stock-grid thead th { line-height: 1.15; vertical-align: bottom; }

/* Below the mobile breakpoint the grid becomes one card per row (rule 5.4e),
   where there are no columns to widen and no headings to break -- so both
   rules are released rather than fighting the card layout. */
@media (max-width: 760px) {
    table.grid.stock-grid th.stock-model,
    table.grid.stock-grid td.stock-model { min-width: 0; }
}

/* ===========================================================================
   35. A ROW THAT NEEDS ATTENTION
   ---------------------------------------------------------------------------
   The OTP Invoice Report paints an invoiced deal that has never been costed --
   money out of the door with no recorded cost, and the whole reason the screen
   exists. The legacy sets `e.Row.BackColor = LightYellow`
   (EQNetAdmin/ReportOTPInvoice.aspx.vb:788), so the colour is the legacy's own
   and a user of twenty years recognises it.

   IT PAINTS `> td`, NOT THE ROW, for the reason rule 5.4f gives: a background on
   a <tr> renders UNDERNEATH its cells, so the alternating band would hide it.
   Every state that must beat a band paints the cells.

   IT IS NEVER THE ONLY SIGNAL. The row also carries the words "Never costed" in
   its own column, because colour reaches neither a screen reader nor a printout,
   and this is exactly the row a manager prints and takes to a meeting.

   Selection still outranks it -- the same argument as hover in section 16: a row
   the reader has chosen must not stop looking chosen because it also needs
   attention.
   --------------------------------------------------------------------------- */
table.grid tbody tr.is-warn > td {
    background: var(--eq-warn-row, #fffbe6);
}
table.grid tbody tr.is-warn:hover > td {
    background: var(--eq-warn-row-hover, #fff6cc);
}
table.grid tbody tr.is-warn.is-selected > td,
table.grid tbody tr.is-warn.is-lastrow > td {
    background: var(--eq-selected);
    color: var(--eq-selected-ink);
}

/* Paper has no fill worth spending ink on, and the word in the Costed column
   carries the same meaning. */
@media print {
    table.grid tbody tr.is-warn > td { background: none; }
}


/* ===========================================================================
   36. THE VEHICLE PRICELIST -- the legacy's own accordion and stacked cells

   Tim, 14 September 2026, with a screenshot of the legacy screen beside the
   new one: "please copy the styling from the legacy".

   WHAT IS REUSED RATHER THAN REINVENTED: the grid itself wears .grid--eq
   (section 16), which was already measured from EQNetAdmin/Styles/GM.css --
   #99bbe8 rules, #15428b header ink on #afd7fb, banded #e0eefb over white. The
   legacy pricelist grid is that same GridView skin, so it needed no new CSS.

   WHAT IS NEW HERE is only what that screen has and the others do not: the
   jQuery UI accordion bars, the paired cells that stack two inputs, and the
   centred heading block.

   SCOPED TO .eq-pricelist on purpose. Other screens use details.eq-acc for a
   collapsed add-row, and giving every one of them a solid blue bar is a
   system-wide restyle nobody asked for.
   =========================================================================== */

.eq-pricelist > h1,
.eq-costing > h1,
.eq-pricelist .eq-pricelist__selected { text-align: center; }

/* The legacy underlines its page heading and the selected model. */
.eq-pricelist > h1,
.eq-costing > h1 { text-decoration: underline; }
.eq-pricelist .eq-pricelist__selected strong { text-decoration: underline; }

/* ---------------------------------------------------------------------------
   THE BARS

   A <details> element, so this works with no script at all -- where the legacy
   needs jQuery UI. The look is the theme's: pale blue closed, solid blue open
   with white text, and a caret that turns.
   --------------------------------------------------------------------------- */
.eq-pricelist details.eq-acc { display: block; margin: 0 0 var(--s2); }

.eq-pricelist details.eq-acc > summary {
    list-style: none;
    cursor: pointer;
    padding: .35rem .6rem;
    border: 1px solid var(--eq-acc-bar-edge);
    background: var(--eq-acc-bar);
    color: var(--eq-acc-bar-ink);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .02em;
    user-select: none;
}

/* The default triangle is replaced by one that matches the legacy's position. */
.eq-pricelist details.eq-acc > summary::-webkit-details-marker { display: none; }
.eq-pricelist details.eq-acc > summary::before {
    content: "\25b8";           /* a right-pointing triangle */
    display: inline-block;
    width: 1em;
    transition: transform .12s ease;
}
.eq-pricelist details.eq-acc[open] > summary::before { transform: rotate(90deg); }

.eq-pricelist details.eq-acc[open] > summary {
    background: var(--eq-acc-bar-on);
    border-color: var(--eq-acc-bar-on-edge);
    color: #fff;
}

.eq-pricelist details.eq-acc > summary:hover { filter: brightness(1.03); }

/* Keyboard focus must stay visible on a solid blue bar. */
.eq-pricelist details.eq-acc > summary:focus-visible {
    outline: 2px solid var(--eq-acc-bar-on-edge);
    outline-offset: 2px;
}

.eq-pricelist .eq-acc__body {
    border: 1px solid var(--eq-acc-bar-edge);
    border-top: 0;
    background: var(--surface);
    padding: var(--s3);
}

/* ---------------------------------------------------------------------------
   THE PAIRED CELLS

   Five of the legacy's columns hold TWO controls stacked -- Price Excl over CO2
   Tax Excl, and so on -- which is why those headings are two lines. The pair is
   a grid rather than two block inputs so both halves are always the same width
   whatever the column does.
   --------------------------------------------------------------------------- */
.eq-pricelist .eq-pair { display: grid; gap: 2px; }
.eq-pricelist .eq-pair > input { width: 100%; min-width: 0; }

/* Inputs inside the grid are the legacy's size: small, plain, and no taller
   than the row. The shared control padding would make each row three times
   the height and put a tenth of this grid on screen. */
.eq-pricelist table.grid input[type="text"],
.eq-pricelist table.grid input[type="number"],
.eq-pricelist table.grid select,
.eq-costing table.grid input[type="text"],
.eq-costing table.grid select {
    padding: 1px 3px;
    font-size: var(--fs-small);
    line-height: 1.3;
    height: auto;
    min-height: 0;
}

/* The row number, which the legacy renders as the GridView's own counter. */
.eq-pricelist .eq-rownum {
    color: var(--ink-soft);
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* The legacy centres these two links above the grid. */
.eq-pricelist .eq-pricelist__actions { text-align: center; margin: var(--s2) 0; }

/* MOBILE: section 30 turns every .grid row into a card below 760px, and a
   paired cell must not force a second column inside one. Nothing is hidden --
   rule 5.4e -- the pair simply stops being a grid and stacks naturally. */
@media (max-width: 760px) {
    .eq-pricelist .eq-pair { display: block; }
    .eq-pricelist .eq-pair > input { margin-bottom: 2px; }
}


/* ===========================================================================
   37. THE COSTINGS LIST -- the legacy's centred filter block and its legend

   Tim, 14 September 2026, with the legacy screen beside the new one: "PLEASE
   MATCH STYLING get logic from legacy".

   The GRID itself needs nothing new -- it wears .grid--eq (section 16), already
   measured from EQNetAdmin/Styles/GM.css. What this adds is the shape around
   it: a centred stack of filters rather than the house filter bar, the two-line
   legend, and the two row colours the legend explains.
   =========================================================================== */

.eq-costing .eq-costing__filters {
    display: grid;
    gap: .35rem .6rem;
    grid-template-columns: max-content max-content;
    justify-content: center;
    align-items: center;
    margin: var(--s3) 0;
}
.eq-costing .eq-costing__filters > label { text-align: right; }

/* The date row carries the month steps either side, as the legacy does. */
.eq-costing .eq-costing__dates {
    display: flex;
    gap: .5rem;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    margin: var(--s3) 0 var(--s2);
}

.eq-costing .eq-costing__go { text-align: center; margin: var(--s3) 0 var(--s2); }

.eq-costing .eq-costing__scope {
    display: flex;
    gap: 1.2rem;
    justify-content: center;
    align-items: center;
    margin-bottom: var(--s3);
}
.eq-costing .eq-costing__scope label {
    display: inline-flex;
    gap: .3rem;
    align-items: center;
}

/* ---------------------------------------------------------------------------
   THE LEGEND

   Two lines, and the first wears the same red as the rows it describes --
   which is how the legacy renders it, and why it reads at a glance.
   --------------------------------------------------------------------------- */
.eq-costing .eq-costing__legend { text-align: center; margin: var(--s3) 0 var(--s2); }
.eq-costing .eq-costing__legend p { margin: 0 auto; max-width: none; font-weight: 700; }
.eq-costing .eq-costing__legend .is-red {
    background: var(--eq-row-red);
    display: inline-block;
    padding: 0 .3rem;
}

/* ---------------------------------------------------------------------------
   THE TWO ROW COLOURS

   Painted on > td, per rule 5.4f -- a background on the ROW sits underneath its
   cells, so the banding would hide it. Both must beat the odd/even bands.
   --------------------------------------------------------------------------- */
.eq-costing table.grid tbody tr.is-uncosted > td { background: var(--eq-row-yellow); }
.eq-costing table.grid tbody tr.is-stale    > td { background: var(--eq-row-red); }

/* A hand-typed stock number is ONE CELL, not the row -- the legacy colours
   cell 5 alone and hangs its tooltip there. */
.eq-costing table.grid tbody td.is-suspect { background: var(--eq-row-red); }

/* Hover must still beat both, or a coloured row stops responding. */
.eq-costing table.grid tbody tr:hover > td { background: var(--eq-menu-on); }

@media print {
    .eq-costing table.grid tbody tr.is-uncosted > td,
    .eq-costing table.grid tbody tr.is-stale > td,
    .eq-costing table.grid tbody td.is-suspect { background: none; }
}

/* ===========================================================================
   THE APPOINTMENT MONTH GRID -- Sales > View / Add Appointments

   The legacy draws this with FullCalendar 5.5.1 plus moment.js
   (AppointmentScheduler.aspx:17-19). Tim's ruling, 16 September 2026: render the
   month on the SERVER instead.

   A month is arithmetic the server already has the rows for, so two client
   libraries buy nothing on the low-spec machines this project exists for -- and
   a server-rendered grid still works with script blocked, which the legacy's
   calendar does not.

   It is a real <table>, not a grid of divs, because it IS tabular: seven day
   columns and six week rows, with a column heading each. That also means it
   inherits the banding, the sticky heading (5.4d) and the mobile card mode
   (5.4e) that every other .grid in the application gets, for free.
   =========================================================================== */

/* The month stepper. Space-between rather than a centred flex row, so the two
   month names sit at the edges and the current month stays put in the middle as
   the names change length ("May 2026" against "September 2026"). */
.eq-monthnav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    margin: var(--s3) 0 var(--s2);
}

/* EQUAL COLUMNS, and this is what stops the month reflowing as bookings move.
   Without table-layout: fixed a day holding three names would be three times the
   width of an empty one, so stepping from a quiet month to a busy one would
   redraw the whole grid in a different shape. */
table.grid.eq-month { table-layout: fixed; }

/* A day cell is a box, not a line of text: top-aligned so the date number stays
   in the same place whatever the cell holds, and tall enough that an empty month
   still reads as a calendar rather than as seven thin stripes. */
table.grid.eq-month > tbody > td,
table.grid.eq-month td {
    vertical-align: top;
    height: 5.5rem;
    padding: var(--s1);
}

.eq-month__num {
    display: block;
    font-weight: 600;
    font-size: .8rem;
    margin-bottom: .15rem;
}

/* A day belonging to the neighbouring month. DIMMED, NEVER HIDDEN -- a rep
   booking on the 1st needs to see the days either side of it, and rule 5.4e's
   "nothing is hidden" applies to what is read as much as to what is clicked.
   Opacity rather than a grey token so it dims correctly on both the banded and
   the unbanded row in either theme. */
table.grid.eq-month td.is-outside { opacity: .45; }

/* Today. An inset ring rather than a background, because the row banding (5.4f)
   already owns the cell's background and a fill here would fight it -- and the
   band is the reading aid, so it wins. */
table.grid.eq-month td.is-today {
    box-shadow: inset 0 0 0 2px var(--eq-accent, #465c71);
}

.eq-month__items {
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: .75rem;
    line-height: 1.3;
}

.eq-month__items li {
    /* One booking per line, clipped rather than wrapped: a busy day must not make
       its week row three times the height of the others. The full list is in the
       table below, which is why clipping here loses nothing. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.eq-month__time {
    font-variant-numeric: tabular-nums;
    opacity: .75;
    margin-right: .2rem;
}

/* The appointment TYPE, between the time and the name -- Tim's order, 16 September
   2026. Slightly dimmed so the three parts of the line separate at a glance
   without three different colours: the time is the anchor, the type is what the
   rep is doing, the name is who with. */
.eq-month__type {
    opacity: .85;
    margin-right: .2rem;
}

/* A CANCELLED APPOINTMENT, WHICH DELIBERATELY STAYS ON THE LIST (APT-2).

   Tim's ruling, 16 September 2026: hide DELETED, keep CANCELLED. So a cancelled
   booking is still on the screen and MUST NOT look like a live one -- struck
   through and dimmed, in both the month and the list.

   Measured on RAC348 before the ruling: 1,123 appointments listed, 349 of them
   cancelled or deleted. Keeping 71 of those visible is only workable if they
   read as cancelled at a glance. */
table.grid.eq-month .eq-month__items li.is-cancelled,
table.grid tbody tr.is-cancelled > td {
    text-decoration: line-through;
    opacity: .55;
}

/* Below the mobile band a .grid row becomes a card (5.4e) and a seven-column
   month cannot. The month grid keeps its table shape and scrolls sideways in its
   own wrapper instead -- which is what .grid-scroll already does for every wide
   grid, so this only has to opt OUT of the card transformation. */
@media (max-width: 760px) {
    table.grid.eq-month,
    table.grid.eq-month thead,
    table.grid.eq-month tbody,
    table.grid.eq-month tr,
    table.grid.eq-month td,
    table.grid.eq-month th { display: revert; }

    table.grid.eq-month td { height: 4rem; font-size: .7rem; }
}

/* Cancel / Delete sit side by side in their cell. Inline-flex so the cell does not
   become as wide as the table lets it. */
.eq-rowactions {
    display: inline-flex;
    gap: var(--s2);
}

/* CLICKING AN EMPTY SPACE IN A DAY BOOKS THAT DAY -- Tim, 16 September 2026:
   "when you click and empty space within a day, that opens a layer that allows
   you to capture the new booking info." The legacy does this with FullCalendar's
   dateClick; here the cell holds an absolutely-positioned anchor that fills it.

   WHY AN OVERLAY ANCHOR RATHER THAN A CLICK HANDLER ON THE CELL: no script, so it
   survives JavaScript being blocked, it is keyboard-reachable and it gets the
   browser's own focus ring for free. A <td> cannot be a link, and wrapping the
   cell's contents in one would make the bookings inside it part of the link.

   The cell is the positioning context and the anchor covers it. The bookings list
   is then raised above the anchor so that a future click on a BOOKING is not
   swallowed by the book-this-day link underneath it. */
table.grid.eq-month td { position: relative; }

.eq-month__hit {
    position: absolute;
    inset: 0;
    /* No text, no underline -- it is a hit area. The label lives in aria-label so
       a screen reader still hears which day it books. */
    text-decoration: none;
}

/* A faint wash on hover, so it is discoverable that the day itself is clickable.
   Deliberately quiet: the row banding (5.4f) is the reading aid and must still be
   the strongest thing in the grid. */
.eq-month__hit:hover { background: rgb(70 92 113 / 8%); }

.eq-month__hit:focus-visible {
    outline: 2px solid var(--brand-deep);
    outline-offset: -2px;
}

/* Above the hit area, so the day number and any bookings stay readable and
   selectable rather than sitting under a transparent anchor. */
.eq-month__num,
.eq-month__items { position: relative; }

/* A SHORT FORM LAYER SIZES TO ITS CONTENT.

   The booking layer is four rows -- client, date, type, time. In the base
   .eq-dialog height the frame is 92vh, which is right for the client overview
   (the tab strip must not move as tabs change height -- Tim, 4 September 2026)
   and wrong here: it left three quarters of the layer empty with the Book button
   stranded at the bottom of the screen, a long way from the fields it submits.

   .eq-dialog--form already intends this with `height: auto`, and it is not
   enough on its own: the frame is a flex COLUMN and its body and form both carry
   `flex: 1 1 auto`, so anything that gives the column a definite height hands the
   slack straight to the body. fit-content states the outcome rather than relying
   on nothing else setting a height.

   max-height is kept, so a form that really is long still caps and scrolls its
   body exactly as the staff editors do. */
.eq-dialog--fit {
    height: fit-content;
    max-height: 92vh;
}

/* ===========================================================================
   THE + AT THE FOOT OF EVERY DAY, AND THE DAY SEPARATORS
   Tim, 16 September 2026, with a mock-up:

     "I think we should have a styling at the bottom of each day with a + and
      always stays on screen ... also can we have a very light vertical day
      separator almost like a border to a cell but a very very thin light gray
      separator just to make it easier to see the individual days"

   WHY THE + EARNS ITS PLACE, because a transparent hit area is invisible by
   definition: the whole cell was already clickable, and nothing said so. A rep
   had to be told. The + is the affordance for a behaviour that already existed.

   ALWAYS RENDERED, NEVER HOVER-ONLY -- that is the "always stays on screen"
   half, and it is also what makes the screen work on a phone, where there is no
   hover at all.
   =========================================================================== */

/* The hit area becomes the + 's positioner: it already fills the cell, so
   aligning its one child to the bottom centre puts the + where Tim drew it,
   and clicking ANYWHERE in the cell -- the + included -- still books the day. */
.eq-month__hit {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding-bottom: .2rem;
}

.eq-month__plus {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* STRETCHED -- Tim, 16 September 2026: "can you stretch the oval pill for the
       + to make it look a bit better". A near-circular badge read as a stray dot;
       an elongated oval reads as a button, which is what it is.

       A proportion of the cell rather than a fixed width, so the oval keeps the
       same shape relative to its day as the month is drawn wider or narrower --
       a fixed 3rem pill looks lost in a 2200px month and cramped on a laptop.
       The min() floor stops it growing silly on a very wide screen. */
    width: min(55%, 3.4rem);
    height: 1.1rem;
    border-radius: 999px;
    background: var(--eq-link);
    color: #fff;
    font-size: .8rem;
    line-height: 1;
    font-weight: 600;

    /* QUIET UNTIL WANTED. Forty-two saturated pills would out-shout the bookings,
       which are what the rep came to read. It lifts to full strength on hover and
       on keyboard focus, so the affordance is legible without competing. */
    opacity: .45;
    transition: opacity .12s ease-in-out;
}

.eq-month__hit:hover .eq-month__plus,
.eq-month__hit:focus-visible .eq-month__plus { opacity: 1; }

/* Room for it. Without this the bookings list runs under the + on a busy day --
   five on one day is realistic, and Tim's own screenshot has exactly that. */
table.grid.eq-month td { padding-bottom: 1.3rem; }

/* ---------------------------------------------------------------------------
   THE DAY SEPARATOR

   "a very very thin light gray separator just to make it easier to see the
   individual days."

   ON THE LEFT EDGE OF EVERY CELL BUT THE FIRST, so there is a line BETWEEN days
   and none down the outside of the table, where the panel's own edge already is.

   It must stay lighter than the row banding (5.4f): the bands are the reading
   aid that lets the eye follow a week across, and a separator strong enough to
   compete would break weeks into seven boxes. Hence a near-white grey rather
   than a border token.
   --------------------------------------------------------------------------- */
table.grid.eq-month > thead > tr > th + th,
table.grid.eq-month > tbody > tr > td + td {
    border-left: 1px solid rgb(0 0 0 / 7%);
}

/* An appointment inside a day cell is a link that OPENS it -- Tim, 16 September
   2026: "clicking the name of a person in the calendar must open that event".

   It inherits the line's colour rather than taking the link colour, because the
   cell already carries its own meaning: a cancelled booking is struck through and
   dimmed, and a blue link would fight that. The whole line is the target, which
   is rule 5.4f's instinct applied inside a calendar cell. */
.eq-month__open {
    display: block;
    color: inherit;
    text-decoration: none;
}

.eq-month__open:hover { text-decoration: underline; }

/* The event layer's buttons. Stacked and centred, as the legacy's popup stacks
   them, so the four actions read as a list of choices rather than a toolbar. */
.eq-eventactions {
    display: flex;
    justify-content: center;
    padding-top: var(--s2);
}

.eq-eventactions--stack {
    flex-direction: column;
    align-items: center;
    gap: var(--s2);
}

/* Each action is the same width, so the column has one edge rather than four --
   the legacy's buttons are ragged and it reads as an accident. */
.eq-eventactions--stack .btn,
.eq-eventactions--stack form { width: min(100%, 18rem); }
.eq-eventactions--stack form .btn { width: 100%; }

/* ===========================================================================
   38. THE PUBLIC SIGNUP WIZARD -- the first thing a customer ever sees

   Tim, 18 September 2026: "Please pay careful attention to styling. As this is
   going to be a client-facing page, and I want you to spend a lot of effort and
   time making sure that these pages are user-friendly good to look at etc."

   IT IS THE ONLY SCREEN IN THIS APPLICATION WITH NO SIGNED-IN USER BEHIND IT,
   and the only one a stranger judges us by. Every other screen is used by
   somebody who has already bought the product; this one has to earn that.

   WHY IT DOES NOT REUSE .auth__card. That card is `flex: 0 1 26rem`, sized for
   four fields and a Log In button, and deliberately flat because it sits on the
   page slab beside the marketing photographs. A registration form is longer,
   grouped into sections, and stands alone -- so it gets its own block rather
   than a pile of overrides that would drag the login page around with it.

   THE SHAPE: one centred column, capped at a comfortable width. This is rule
   5.5a's NARROW, and it is narrow for 5.5's own stated reason -- a
   caption-and-control row stretched across a wide monitor separates the label
   from its field far enough that the eye loses the row.
   =========================================================================== */

.signup {
    /* 34rem is about sixty characters of label plus a field. Wider and the form
       starts to feel like a spreadsheet; narrower and the helper lines wrap
       every time. */
    width: min(100%, 34rem);
    margin-inline: auto;
}

/* Wider where the page is SHOWING something rather than asking -- the branch
   code reveal and the finished panel. */
.signup--wide { width: min(100%, 42rem); }

.signup__card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--s6);
    /* THE ONE LIFT, on the outermost surface -- rule 5.4l. Nothing inside it
       lifts, or the card reads as a flat slab with panels drawn on it. */
    box-shadow: var(--eq-lift);
}

.signup__head { text-align: center; margin-bottom: var(--s5); }

.signup__title {
    font-size: 1.6rem;
    font-weight: 650;
    color: var(--brand-deep);
    margin: 0 0 var(--s2);
    line-height: 1.2;
}

/* The one line of running text that sets expectations. Given a measure, because
   a centred paragraph running the full card width is harder to read than the
   same words in a column. */
.signup__lede {
    color: var(--ink-soft);
    margin: 0 auto;
    max-width: 44ch;
    line-height: 1.5;
}

/* ---------------------------------------------------------------------------
   THE PROGRESS RAIL

   Three steps, named, with the current one marked. It is not decoration: this
   wizard is INTERRUPTED BY AN EMAIL, which is unusual enough that somebody who
   does not know it is coming reads "check your email" as the form having
   failed. Showing the shape up front is what makes the interruption read as
   part of the process rather than the end of it.

   An ordered list, so a screen reader announces it as one -- and the current
   step carries aria-current in the markup rather than relying on colour.
   --------------------------------------------------------------------------- */
.signup__steps {
    display: flex;
    justify-content: center;
    list-style: none;
    margin: 0 0 var(--s5);
    padding: 0;
    counter-reset: signup-step;
}

.signup__step {
    display: flex;
    align-items: center;
    gap: var(--s2);
    font-size: var(--fs-small);
    color: var(--muted);
    counter-increment: signup-step;
}

/* The connector. A plain rule rather than an arrow: an arrow implies you can go
   forwards, and from step one you cannot until an email arrives. */
.signup__step + .signup__step::before {
    content: "";
    width: var(--s5);
    height: 1px;
    background: var(--border-strong);
    margin-inline: var(--s2);
}

.signup__stepno {
    display: grid;
    place-items: center;
    width: 1.65rem;
    height: 1.65rem;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border-strong);
    background: var(--surface);
    color: var(--muted);
    font-weight: 600;
    font-size: var(--fs-small);
}
.signup__stepno::before { content: counter(signup-step); }

.signup__step--now { color: var(--brand-deep); font-weight: 600; }
.signup__step--now .signup__stepno {
    background: var(--brand);
    border-color: var(--brand);
    color: #fff;
}

/* A step already passed. Ticked rather than numbered, because the number has
   stopped being useful the moment it is behind you. */
.signup__step--done .signup__stepno {
    background: var(--good-tint);
    border-color: var(--good);
    color: var(--good);
}
.signup__step--done .signup__stepno::before { content: "\2713"; }

/* On a phone the LABELS go, not the rail. Rule 5.4e forbids hiding anything
   interactive; these are text, and the numbered circles still say where you
   are. */
@media (max-width: 560px) {
    .signup__steplabel { display: none; }
    .signup__step + .signup__step::before { width: var(--s4); margin-inline: var(--s1); }
}

/* ---------------------------------------------------------------------------
   FIELD GROUPS

   Headed sections rather than one long list of boxes. Eight fields in a row
   reads as a chore; the same eight in three named groups reads as three small
   questions, and the person can see the end of each one.
   --------------------------------------------------------------------------- */
.signup__group { border: 0; padding: 0; margin: 0 0 var(--s5); }
.signup__group:last-of-type { margin-bottom: var(--s4); }

.signup__grouphead {
    font-size: var(--fs-eyebrow);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--muted);
    font-weight: 650;
    padding: 0 0 var(--s2);
    margin: 0 0 var(--s3);
    border-bottom: 1px solid var(--border);
    width: 100%;
}

.signup__field { margin-bottom: var(--s4); }
.signup__field:last-child { margin-bottom: 0; }

.signup__field > label {
    display: block;
    font-weight: 600;
    color: var(--ink);
    margin-bottom: var(--s1);
}

.signup__field input[type="text"],
.signup__field input[type="email"],
.signup__field input[type="tel"],
.signup__field input[type="password"],
.signup__field select {
    width: 100%;
    padding: .55rem .7rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--ink);
    /* 16px MINIMUM, or iOS zooms the whole page when the field takes focus --
       which on a registration form reads as the page breaking. */
    font-size: 1rem;
    font-family: inherit;
}

.signup__field input:focus,
.signup__field select:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: var(--ring);
}

/* The helper line under a field. Small, muted, and ALWAYS present where one is
   wanted rather than appearing only after a refusal -- being told what is
   expected beats being told you were wrong. */
.signup__hint {
    display: block;
    margin-top: var(--s1);
    font-size: var(--fs-small);
    color: var(--muted);
    line-height: 1.45;
}

.signup__hint--warn { color: var(--error-ink); font-weight: 600; }

/* ---------------------------------------------------------------------------
   THE CHOICE CARDS -- "what do you sell"

   Two radio buttons drawn as full-width cards. A <select> would be smaller and
   worse here: the answer decides whether the New Vehicles half of the product
   exists for this dealership at all, so it deserves to be visible rather than
   folded into a dropdown the eye skips over.

   A REAL <input type="radio"> underneath, so it works with the keyboard, with a
   screen reader, and with script blocked. The card is the label.
   --------------------------------------------------------------------------- */
.signup__choices { display: grid; gap: var(--s2); }

.signup__choice {
    display: flex;
    align-items: flex-start;
    gap: var(--s3);
    padding: var(--s3) var(--s4);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
    cursor: pointer;
    /* No transition: this project's users are on low-spec machines and a
       repaint on hover across a whole form is real work for no gain. */
}

.signup__choice:hover { border-color: var(--brand); background: var(--brand-tint); }

.signup__choice input[type="radio"] { margin-top: .2rem; flex: 0 0 auto; }

.signup__choice strong { display: block; color: var(--ink); font-weight: 600; }
.signup__choice span   { display: block; font-size: var(--fs-small); color: var(--muted); }

/* :has is what lets the CARD respond to its own radio. Where it is unsupported
   the card simply does not tint -- the radio itself still shows the answer, so
   nothing that matters is lost. */
.signup__choice:has(input:checked) {
    border-color: var(--brand);
    background: var(--brand-tint);
    box-shadow: inset 0 0 0 1px var(--brand);
}

/* ---------------------------------------------------------------------------
   THE BRANCH CODE REVEAL

   The one moment in the flow worth making a fuss of. It is the dealership's
   identity in this system and in every support call for the rest of its life,
   and the customer has to carry it away from this screen -- so it is displayed
   as a thing to read and copy, not as another row in a list.
   --------------------------------------------------------------------------- */
.signup__code {
    text-align: center;
    padding: var(--s5) var(--s4);
    margin: var(--s4) 0;
    border: 1px solid var(--brand);
    border-radius: var(--radius);
    background: var(--brand-tint);
}

.signup__code dt {
    font-size: var(--fs-eyebrow);
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--brand-deep);
    font-weight: 650;
    margin-bottom: var(--s2);
}

.signup__code dd {
    margin: 0;
    /* MONOSPACE, and that is a decision rather than a look: this is a five-digit
       code somebody reads aloud to support and types into a login form, so the
       shapes that get confused -- 1 and l, 0 and O -- have to be distinct. */
    font-family: var(--font-mono);
    font-size: 2.4rem;
    font-weight: 700;
    letter-spacing: .12em;
    color: var(--brand-deep);
    line-height: 1.1;
}

.signup__code p {
    margin: var(--s3) 0 0;
    font-size: var(--fs-small);
    color: var(--ink-soft);
    max-width: none;
}

/* A plain key-and-value pair for the things that are NOT the headline. */
.signup__facts { margin: 0 0 var(--s4); }
.signup__facts div {
    display: flex;
    justify-content: space-between;
    gap: var(--s4);
    padding: var(--s2) 0;
    border-bottom: 1px solid var(--border);
}
.signup__facts dt { color: var(--muted); font-size: var(--fs-small); }
.signup__facts dd { margin: 0; font-weight: 600; color: var(--ink); }

/* ---------------------------------------------------------------------------
   ACTIONS, MESSAGES AND THE FINISHED STATE
   --------------------------------------------------------------------------- */
.signup__actions { margin-top: var(--s5); }

/* Full width and tall. It is the only button on the page, and on a phone it is
   what a thumb is reaching for. */
.signup__actions .btn { width: 100%; padding-block: .6rem; font-size: var(--fs-lead); }

.signup__alt { text-align: center; margin-top: var(--s4); font-size: var(--fs-small); }

/* A refusal. Uses the estate's own error colours so it reads the same as every
   other "this is wrong" in the application. */
.signup__problem {
    padding: var(--s3) var(--s4);
    margin-bottom: var(--s4);
    border-radius: var(--radius-sm);
    background: var(--bad-tint);
    border: 1px solid var(--bad);
    color: var(--error-ink);
    font-weight: 600;
}

/* The tick at the top of a finished step. Big enough to read as an outcome
   rather than as an icon beside a sentence. */
.signup__done {
    display: grid;
    place-items: center;
    width: 3.25rem;
    height: 3.25rem;
    margin: 0 auto var(--s4);
    border-radius: var(--radius-pill);
    background: var(--good-tint);
    color: var(--good);
    font-size: 1.75rem;
    font-weight: 700;
}

/* "Check your email" is a WAIT, not a success -- so it gets the informational
   tint and not the green. Telling somebody they have finished when they have
   one step left is how a step gets skipped. */
.signup__waiting {
    display: grid;
    place-items: center;
    width: 3.25rem;
    height: 3.25rem;
    margin: 0 auto var(--s4);
    border-radius: var(--radius-pill);
    background: var(--info-tint);
    color: var(--brand);
    font-size: 1.6rem;
}

/* The development shortcut, set apart so it can never be mistaken for part of
   the customer's journey. */
.signup__dev {
    margin-top: var(--s5);
    padding: var(--s3) var(--s4);
    border: 1px dashed var(--warn);
    border-radius: var(--radius-sm);
    background: var(--warn-tint);
    font-size: var(--fs-small);
}
.signup__dev p { margin: 0 0 var(--s2); max-width: none; }

@media (max-width: 560px) {
    .signup__card { padding: var(--s4); border-radius: var(--radius); }
    .signup__title { font-size: 1.35rem; }
    .signup__code dd { font-size: 1.9rem; }
}

/* ===========================================================================
   39. THE NEW-DEALERSHIP CHECKLIST

   Tim, 18 September 2026: "every time the admin user logs in, it must bring up
   this page that says we still need the following".

   It is a LANDING PAGE, not a gate, so its job is to be quick to read and
   obviously finishable. Each item is a card carrying its own state and its own
   button, and a completed one steps back rather than disappearing -- a
   dealership that cannot see what it already sent will send it again.
   =========================================================================== */

/* The progress bar. A bar ALONE would be decoration, which is why the heading
   beside it states the same thing as a fraction: "4 of 6 complete" survives the
   bar not rendering, and it is the sentence somebody actually wants. */
.onboard__bar {
    height: .5rem;
    border-radius: var(--radius-pill);
    background: var(--brand-tint-strong);
    overflow: hidden;
}

.onboard__bar > span {
    display: block;
    height: 100%;
    background: var(--good);
    border-radius: var(--radius-pill);
    /* No transition: the value only changes across a page load, so there is
       nothing to animate between. */
}

.onboard__item { margin-bottom: var(--s4); }

/* DONE STEPS BACK RATHER THAN VANISHING. Hiding a completed item would leave a
   dealership unable to see what it already sent -- and a page that empties as
   you work it is disorienting, because the thing you just did is the thing that
   disappears. */
.onboard__item--done { border-left: 3px solid var(--good); }

.onboard__head { display: flex; align-items: flex-start; gap: var(--s3); }
.onboard__head h2 { margin: 0 0 var(--s1); }
.onboard__head .note { margin: 0; }

/* The tick, or an empty circle for an outstanding item. Sized so the column of
   items has one left edge whether each is done or not -- a marker that changes
   width would make the headings jitter down the page. */
.onboard__tick {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 1.5rem;
    height: 1.5rem;
    margin-top: .1rem;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border-strong);
    background: var(--surface);
    color: var(--good);
    font-weight: 700;
    font-size: var(--fs-small);
}

.onboard__item--done .onboard__tick {
    background: var(--good-tint);
    border-color: var(--good);
}

/* ---------------------------------------------------------------------------
   THE CAPTURE BOXES ARE SIZED TO WHAT GOES IN THEM

   Tim, 18 September 2026: "please shorten the text box for things like that and
   reg numbers give me a bit of space between the required field text box and the
   save buttons".

   THE WIDTHS ARE DEALERSHIP DETAILS' OWN -- 200px for a code, 350px for an
   address -- and that is the point rather than a coincidence. The same values
   are typed on both screens, and a VAT number that is a short box on one and a
   full-width bar on the other reads as two different systems. The comment beside
   those rules already made this exact argument: "a 2200px-wide text box for a
   VAT number is worse than a 200px one".

   max-width keeps them inside a phone, where the band below turns them full
   width like every other field.
   --------------------------------------------------------------------------- */
.onboard__item input[type="text"] {
    width: 200px;
    max-width: 100%;
}

/* An address is the one value here that genuinely needs the room. */
.onboard__item input.is-wide { width: 350px; }

/* SPACE BETWEEN THE LAST FIELD AND THE BUTTON. Without it the Save sits hard
   against the box it saves, which reads as part of the field rather than as the
   action -- and on a touch screen the two are one thumb apart.

   No `form` in the selector since 4 October 2026: the whole stage is now ONE
   form around every card (so a Save keeps every box -- see GettingStarted), and
   ".onboard__item form .btn" would have matched nothing. */
.onboard__item .btn { margin-top: var(--s4); }

/* And between stacked fields, so the two address boxes are visibly two
   questions rather than one control that wrapped. */
.onboard__item .field + .field { margin-top: var(--s3); }

@media (max-width: 560px) {
    .onboard__item input[type="text"],
    .onboard__item input.is-wide { width: 100%; }
}

/* A file input is the one control browsers style themselves, so it is left
   alone apart from giving it room. Fighting it produces something that looks
   wrong on exactly one browser and nobody can say which. */
.onboard__item input[type="file"] {
    display: block;
    margin-top: var(--s1);
    max-width: 100%;
}

/* A REFUSED FILE BOX TURNS RED (rule 5.4p) -- with an OUTLINE, because a file
   input draws no border, so the house `input[aria-invalid] { border-color }`
   rule colours nothing on it and the screen would take the person to a box that
   looks exactly as before. Three classes' worth of specificity, so it outranks
   the focus rule's `outline: none` while the box is focused. Set by the server's
   refusal (data-eq-invalid) and by app.js section 38 for a file too big to send. */
.onboard__item input[type="file"][aria-invalid="true"] {
    outline: 2px solid var(--bad);
    outline-offset: 2px;
}

/* WHY THAT FILE WAS REFUSED, beside its box rather than only at the top of a
   ten-card page -- Tim, 23 September 2026, on a refusal he could not see: "the
   red is now at the top of the screen ... I wasn't aware what the problem was".
   Empty takes no room. Not interactive, so rule 5.4e has nothing to say about
   it being collapsed. */
.onboard__problem {
    display: block;
    margin-top: var(--s1);
    color: var(--bad);
    font-weight: 600;
}
.onboard__problem:empty { display: none; }

@media (max-width: 560px) {
    .onboard__head { gap: var(--s2); }
}

/* ===========================================================================
   40. A SECOND PART INSIDE ONE CARD

   Tim, 18 September 2026, on Staff Admin: "I don't want two cards on this
   particular page. The staff card needs to extend vertically downwards and
   include the content the add a user below the table but all in that one
   card."

   The two halves are still two things -- a list, and the act of adding to it --
   so they keep a heading and a hairline between them. What they stop being is
   two floating panels, which said they were unrelated when one is something you
   do TO the other.

   IT IS NOT A CARD AND MUST NOT BECOME ONE. Rule 5.4l: only the outermost
   surface lifts, and a card inside a card gives two shadows -- at which point
   the pair reads as one flat slab with a panel drawn on it, which is the exact
   look that rule exists to prevent.
   =========================================================================== */
.eq-card__part {
    margin-top: var(--s4);
    padding-top: var(--s4);
    border-top: 1px solid var(--border);
}

/* The heading sits closer to its own content than to the rule above it, so the
   hairline reads as a separator rather than as an underline belonging to the
   heading. */
.eq-card__part > h3 { margin: 0 0 var(--s3); }


/* ===========================================================================
   41. AN EDITABLE KEY-AND-VALUE ROW
   ===========================================================================
   Tim, 19 September 2026, on the used-vehicle editor: "I prefer the layout
   you've given me here but I do still need to be able to edit all of the
   variables as per the legacy."

   So the EDITABLE row keeps the read-only row's grid exactly -- same label
   column, same gap, same rule between rows -- and only gains room for a
   control. Anything that changed the geometry would answer a different request
   from the one he made.

   The vertical padding relaxes a little because an input is taller than a line
   of text, and without it the rows read as cramped against their own rules. */
.kv__row--edit {
    /* STRETCH, NOT CENTER, and this is a fix rather than a preference.
       Tim, 21 September 2026, marking the gap above a caption: "1 and 2 same
       colour blue."

       A tall row -- the location pair, a tick with a hint under it, a box with
       "Required." beneath -- is taller than its caption. `align-items: center`
       shrank the LABEL CELL to its text and floated it in the middle, so the
       blue appeared as a short block with white above and below it instead of
       a column running down the panel. The caption is still centred; it is now
       centred inside a full-height cell (see .kv dt). */
    align-items: stretch;
}
/* On the CELLS, because the row no longer carries padding -- the label column
   is a filled column now and has to run the full height of its row. */
.kv__row--edit > dt,
.kv__row--edit > dd { padding-block: .3rem; }

/* The box fills its half of the row rather than sitting at some arbitrary
   width: the label column is already fixed, so a full-width control is the
   only one that lines up down the column. */
.kv__row--edit input[type="text"] {
    width: 100%;
    max-width: 22rem;
}

/* A tick needs no width, and stretching one leaves a click target the width of
   the screen attached to a 14px box. */
.kv__row--edit .check { display: inline-flex; align-items: center; gap: var(--s2); }

/* The hint under a box is secondary to it, so it sits tight underneath rather
   than spaced as if it were another field. */
.kv__row--edit small.note { display: block; margin-top: .15rem; }

/* THE VALUE CENTRES TOO, NOT JUST THE CAPTION.

   Tim, 23 September 2026: "the label in the left hand of the two columns is
   centered, but the text box on the right hand side is vertically aligned to
   the top, um, which just looks weird. It would make more sense if they were
   both centered."

   .kv dt already centres its text inside a full-height cell; the value cell did
   not, so on any row taller than one line -- a box with "Required." under it, a
   tick with a hint, the location pair -- the caption sat in the middle and the
   control sat at the top. A flex column centres the control AND anything under
   it as one group, which is what makes the two halves agree. */
.kv__row--edit > dd {
    display: flex;
    flex-direction: column;
    justify-content: center;
}

/* ---------------------------------------------------------------------------
   A FIELD THE CATALOGUE OWNS -- read-only, and it must LOOK it

   Tim, 23 September 2026, on two of them at once: the vehicle department "is a
   label and cannot be edited", and make, model and the categories are "grayed
   out ... they're labels. Only if you tick the ignore MNM category can you then
   manually type text".

   READONLY, NEVER DISABLED, and the difference is the whole reason this rule
   exists rather than an attribute on the markup: a disabled input POSTS NOTHING,
   so it would bind to "" and blank the column on the next save. The legacy gets
   away with `Enabled = False` because Web Forms posts the value back out of
   ViewState. This application has no ViewState, so readonly is the only port
   that keeps the data.

   A LOOKUP BOX IS EXCLUDED, and it is readonly for a different reason. The
   Supplier box on Purchased From cannot be typed in because its PICKER fills
   it, not because anything owns it -- the rep operates it through the
   magnifier, so it must look like every other box. Tim, 24 September 2026:
   "the colour inside this blue rectangle should be white". Excluded inside
   :where(), so the rule keeps its (0,2,1) and nothing next in line moves
   (rule 5.4n's ratchet).
   --------------------------------------------------------------------------- */
.kv__row--edit input[readonly]:not(:where(.kv-lookup input)),
.kv__row--edit select[readonly] {
    background: var(--eq-formbox);
    border-color: transparent;
    color: var(--eq-body-ink);
    cursor: default;
}

.kv__row--edit input[readonly]:not(:where(.kv-lookup input)):focus {
    /* Still focusable, so the value can be selected and copied -- it is a label
       that happens to be a box, not a dead region. */
    outline: 2px solid var(--eq-focus, #4a90d9);
    outline-offset: 1px;
}

/* ---------------------------------------------------------------------------
   ADD NEW, in the drop-down rather than in a second box

   Tim, 23 September 2026: "instead of having the or type a new one taking up
   space on the screen which is hardly ever used add in the drop down add new
   and if they select add new then it can come up with a layer that allows them
   to type in the new value and proceed".

   THE TEXT BOX IS STILL THERE, hidden only when script is running. It is the
   ONLY way a new location can ever exist -- there is no locations table, the
   list is a DISTINCT over the dealership's own stock -- so with script blocked
   the box reappears and the screen keeps working exactly as it did. The
   "Add new" option is INSERTED BY SCRIPT for the same reason: unscripted it
   would be an option that posts a literal "Add new" as a location.
   --------------------------------------------------------------------------- */
.has-js .kv__new { display: none; }

.kv-new-dialog { width: min(94vw, 26rem); }
.kv-new-dialog .field { margin-bottom: .6rem; }
.kv-new-dialog input[type="text"] { width: 100%; }

/* VIN No 2 needs no rule of its own: the ROW is gone from the markup and the
   VALUE is carried by a plain hidden input, which is invisible without help.
   A .kv__row--hidden class was written here first and matched nothing -- and a
   rule matching nothing is one somebody later writes markup to satisfy. */

/* ---------------------------------------------------------------------------
   THE ACTION CARD AT THE FOOT OF AN EDITABLE TAB

   It is a card so the Save sits on the same ground as the fields above it
   rather than floating on the page, and it carries no heading because a row of
   buttons that announces itself is noise. */
.eq-card--actions { display: flex; flex-direction: column; gap: var(--s3); }

/* The dropdown and its "or type a new one" box sit on one line, because they
   are one decision -- pick an existing location OR name a new one. Wrapping is
   allowed so a narrow window stacks them rather than clipping the box. */
.kv__row--edit select { max-width: 22rem; }
.kv__row--edit .kv__new { margin-top: .25rem; max-width: 14rem; }


/* ===========================================================================
   42. THE DATE FIELD -- THE WHOLE BOX OPENS THE PICKER
   ===========================================================================
   Specification: Docs/STANDARD-DateSelector.md. Rule: CLAUDE.md 5.4m.

   Tim, 21 September 2026, choosing from a nine-option bench: "I like option
   two. But I don't want just the icon to the right hand side. Can we make the
   full text box clickable so that when you click the text box or the icon, it
   displays the date selector?" -- and, on the variant that drops the browser's
   duplicate glyph, "i like 2b".

   EVERY RULE THAT CHANGES THE BROWSER'S OWN CONTROL IS GATED ON .has-js, AND
   THAT IS THE MOST IMPORTANT LINE IN THIS BLOCK.

   Hiding the browser's calendar glyph is only safe because our button and our
   click handler replace it. If the script is blocked or fails -- routine on the
   flaky South African links this project exists for -- an ungated rule would
   remove the ONLY way to open a calendar and leave a field that looks perfectly
   normal and has quietly lost a feature. Gated, a script failure degrades to a
   plain native date field with its own glyph, which is a complete, working
   control.

   THE SELECTOR IS [data-eq-date], NOT .eq-date__box input[type="date"], and
   that is deliberate rather than shorthand. One attribute then governs the
   stylesheet and the one listener in app.js together, so a field that opts out
   -- a readonly date, whose picker could not open anyway -- opts out of both at
   once, from one place in the partial. Keyed off the element type instead,
   a readonly field would keep the dressing and lose the behaviour, which is the
   icon that does nothing. */
.eq-date__box { display: flex; align-items: stretch; }
.eq-date__box input[type="date"] { flex: 1 1 auto; min-width: 0; }

/* The pointer says the whole box is a trigger. Only true once the script that
   makes it one has loaded, hence .has-js -- a text caret over a box that does
   nothing on click would promise the opposite of the truth. */
.has-js input[data-eq-date] {
    cursor: pointer;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

/* Tim's "2b": one icon, not two. WebKit-prefixed, so Chrome and Edge only --
   which is precisely the pair that draws a duplicate. Firefox exposes no stable
   selector for its own indicator and keeps it; that is accepted, not a bug to
   chase.

   DO NOT ASSERT THIS WITH getComputedStyle. Chrome reports "block" for this UA
   shadow pseudo-element even when an author rule sets display:none, so the
   obvious test fails against correct markup -- it did, and this rule was nearly
   "fixed" on the strength of it. Verify it by eye or by pixel. */
.has-js input[data-eq-date]::-webkit-calendar-picker-indicator { display: none; }

/* Hidden by default and shown by script, deliberately: without the script the
   button would be a control that does nothing. The partial does not even
   render it on a readonly field, so this is the second of two gates. */
.eq-date__btn { display: none; }
.has-js .eq-date__btn {
    display: inline-flex; align-items: center; justify-content: center;
    flex: 0 0 auto; width: 2.4rem; cursor: pointer;
    border: 1px solid var(--border-strong); border-left: 0;
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    background: var(--brand-tint); color: var(--brand-deep);
}
.has-js .eq-date__btn:hover { background: var(--brand-tint-strong); }
.eq-date__btn:focus-visible { outline: none; box-shadow: var(--ring); }

/* 44px touch floor, per rule 5.4e. The field itself already grows at this
   breakpoint; the button has to be told. */
@media (max-width: 760px) {
    .has-js .eq-date__btn { width: 2.75rem; }
}

/* The stocklist's own action row -- UPDATE REMOVED VEHICLES, under the grid
   where the legacy puts it (.aspx:6940). Inside .grid-wrap, so it needs the
   padding the grid itself does not carry, and it is right-aligned to sit under
   the Remove column rather than stranded on the left of a 20-column table. */
/* ---------------------------------------------------------------------------
   UPDATE REMOVED VEHICLES -- the action bar, top and bottom

   Tim, 22 September 2026: "that block doesn't need to exist so you can get rid
   of it it should be the same background color as the card background I want
   the update removed vehicles button centered top and bottom".

   IT USED TO CARRY .eq-actions AND THAT WAS THE BUG. That class is the CLIENT
   PROFILE'S numbered-list container -- `list-style: decimal`, a 380px cap, a
   2.2rem left indent to make room for the numbers, and a light blue ground. All
   three symptoms came from it at once: the blue block, the odd width, and a top
   button that could not centre because it was being indented for numbers that
   were not there. Do not reach for .eq-actions again for a row of buttons.

   No ground of its own now: it sits on the card and shows it.
   --------------------------------------------------------------------------- */
.stock-actions {
    display: flex;
    justify-content: center;
    padding: var(--s3);
    background: none;
    border: 0;
    width: auto;
    max-width: none;
}

/* The bottom copy keeps the rule that separates it from the last grid row. The
   top copy has the filters above it and needs none. */
.stock-actions:not(.stock-actions--top) { border-top: 1px solid var(--eq-grid-line); }

/* ---------------------------------------------------------------------------
   THE BLUE, AND THE GREEN

   Tim, same instruction: the button and the unchosen day options take the blue
   the removed block used to have; the chosen day takes a light green.

   Both are tokens this application already owns, not new colours:
     --eq-formbox       #e0eefb  the light blue of every form box (and the
                                 grid's own band 1 -- it is the legacy's blue)
     --stock-ready-even #98fb98  PaleGreen, the legacy's own "ready" green,
                                 already used on ready-to-advertise stock
                                 numbers in the grid below

   Using the ready green for "chosen" is deliberate: on this screen green
   already means "this one is good", so it reads without a legend.
   --------------------------------------------------------------------------- */
.stock-actions .btn,
.stock-history__choices .btn {
    background: var(--eq-formbox);
    border-color: var(--eq-formbox-line);
    color: var(--ink);
}

.stock-actions .btn:hover,
.stock-history__choices .btn:hover {
    background: var(--eq-grid-row-hover, #cfe4f8);
}


/* ===========================================================================
   REMOVE VEHICLE FROM STOCKLIST -- the removal layer

   Reproduces the legacy modal (VehicleStocklistVer3.aspx:12135-12310): a
   heading bar, one collapsible section per ticked vehicle carrying
   "StockNo - Make Model", the question, the two reason buttons, and the
   reason box that "Other" reveals.

   The colours are the legacy's own so a stock controller of twenty years sees
   what they see today -- the blue vehicle bar is the slate chrome token, and
   the reason box keeps its pink, which in the legacy is BackColor="LightPink"
   and means "this is the field you must fill in", not "this is an error".
   =========================================================================== */

.eq-dialog--removal { width: min(96vw, 60rem); }

/* One section per vehicle. */
.removal-vehicle + .removal-vehicle { margin-top: .75rem; }

/* THE BLUE BAR. A <summary> is the accordion header; the marker is replaced by
   our own triangle so it matches the legacy's, which points DOWN when open. */
.removal-vehicle__head {
    background: linear-gradient(180deg, #5b9bd5, #3f88c5);
    color: #fff;
    font-weight: 700;
    letter-spacing: .02em;
    padding: .5rem .9rem;
    border-radius: 3px;
    cursor: pointer;
    list-style: none;
    text-align: center;
}
.removal-vehicle__head::-webkit-details-marker { display: none; }
.removal-vehicle__head::before {
    content: "\25B8";                 /* right-pointing triangle, closed */
    display: inline-block;
    margin-right: .5rem;
    transition: transform .12s ease;
}
.removal-vehicle[open] > .removal-vehicle__head::before { transform: rotate(90deg); }

@media (prefers-reduced-motion: reduce) {
    .removal-vehicle__head::before { transition: none; }
}

.removal-vehicle__body {
    background: var(--eq-formbox);
    border: 1px solid var(--eq-formbox-line);
    border-top: 0;
    padding: 1.1rem .9rem 1.3rem;
    text-align: center;
}

.removal-vehicle__q { margin: 0 0 1rem; max-width: none; }

.removal-vehicle__choice {
    display: flex;
    gap: 2rem;
    justify-content: center;
    flex-wrap: wrap;
}

/* The legacy's buttons are Width="100px" and plain. Kept wide enough to read as
   a pair of equal choices rather than one primary action. */
.removal-btn { min-width: 8rem; }
.removal-btn.is-chosen { border-color: var(--eq-accent); font-weight: 700; }

/* REVEALED BY "Other" -- and hidden ONLY when the script has run. Ungated, a
   failed script would leave the rep a button that does nothing and no way to
   type a reason at all. */
.has-js .removal-vehicle__reason { display: none; }
.has-js .removal-vehicle__reason.is-revealed { display: block; }

.removal-vehicle__reason { margin-top: 1.1rem; }
.removal-vehicle__reasonlabel { margin-right: .4rem; }

/* LightPink, from the legacy (txtRemoveVehicleOtherReason). It marks the field
   as the one to fill in; it is not an error colour, so it does not use the
   error tokens. */
.removal-reason {
    background: #ffb5b5;
    min-width: 16rem;
}

.eq-actions--centre { justify-content: center; }

/* MOVE REMOVED VEHICLES TO HISTORY AFTER N DAYS -- the legacy's radRemovedDays,
   in the same place on the screen. Four submit buttons rather than an
   auto-posting radio group, so it works with script blocked. */
.stock-history {
    text-align: center;
    margin: 0 0 1rem;
}
.stock-history__label { margin-right: .5rem; }
.stock-history__choices {
    display: inline-flex;
    gap: .4rem;
    flex-wrap: wrap;
    vertical-align: middle;
}
/* The chosen one reads as chosen. aria-pressed says the same to a screen
   reader, so this is decoration over a stated fact rather than the only signal. */
.stock-history__choices .is-chosen,
.stock-history__choices .is-chosen:hover {
    /* PaleGreen -- the same green the grid uses for a vehicle that is ready.
       Green already means "this is the good one" on this screen. */
    background: var(--stock-ready-even);
    border-color: var(--stock-ready-odd);
    font-weight: 700;
}
.stock-history__note {
    margin: .5rem auto 0;
}
/* The same red the removed rows use, so the sentence points at what you can
   see. --eq-removed-ink holds it once; see the stock-grid rule. */
.stock-history__red { color: #FF0000; font-weight: 700; }

/* ---------------------------------------------------------------------------
   THE USED-VEHICLE FIELDS -- discount ceiling and override password

   The legacy's pnlusedvehiclefield, USED section only. Centred with the rest of
   the header so the column of controls reads as one block rather than three
   loose rows.
   --------------------------------------------------------------------------- */
.stock-usedfields {
    text-align: center;
    margin: 0 0 1rem;
}
.stock-usedfields__row {
    display: flex;
    gap: .5rem;
    align-items: center;
    justify-content: center;
    margin-bottom: .35rem;
    flex-wrap: wrap;
}
/* QUALIFIED WITH THE ELEMENT, and that is not decoration.
   `input[type="text"] { width: 100% }` is (0,1,1) -- a type selector PLUS an
   attribute selector -- so a bare single class at (0,1,0) loses to it however
   late it appears in the file. Both of these rendered full width until it was
   MEASURED with tools/eqmeasure.py; reading the stylesheet said they should
   win. Same shape as rule 5.4n, on width rather than colour.
   The house already does it this way -- see .eq-optouts__form input[type="text"]. */
.stock-usedfields input.stock-usedfields__pct  { width: 5rem; }
.stock-usedfields input.stock-usedfields__pass { width: 12rem; }
.stock-usedfields__error {
    color: var(--eq-error-ink, #990000);
    font-weight: 700;
    margin: .4rem 0 0;
    max-width: none;
}

/* ---------------------------------------------------------------------------
   THE TWO LEGENDS, in the legacy's own colours

   Red for "these rows are incomplete", green for "this is how you finish one".
   Both are BOLD in the legacy (Font-Bold="true"), and both are centred with the
   header. They carry real reading weight, so they keep a reading measure rather
   than stretching to 2200px (rule 5.5's prose exception).
   --------------------------------------------------------------------------- */
.stock-legend--missing {
    color: #FF0000;
    font-weight: 700;
    text-align: center;
    margin: .75rem auto;
}
.stock-legend--ready {
    color: #008000;
    font-weight: 700;
    margin: .75rem auto 1rem;
}
/* The list that used to sit here is a hover now (see the page model's
   ReadyCriteria), so the ul and li rules that dressed it are gone rather than
   left behind -- a rule matching nothing is a rule somebody later writes markup
   to satisfy. What remains is one centred sentence. */
.stock-legend--ready { text-align: center; }

/* ---------------------------------------------------------------------------
   AS WIDE AS THE SENTENCE NEEDS -- the three explanatory blocks

   Tim, 22 September 2026: "if we are able to reduce the height by increasing
   the width of the text please do so", naming the green bullets, the red
   warning and the removal note.

   They were capped at 62ch and 70ch, which is rule 5.5's reading measure. That
   measure exists for a RETURN SWEEP -- the eye losing its place finding the
   start of the next line in a paragraph. Each of these is a single short
   statement, and at the width below each one fits on ONE line, so there is no
   next line to find and the measure was buying nothing while costing six lines
   of screen.

   `width: fit-content` is what makes this work without picking a number per
   block: each shrinks to its own longest line and stays centred on the same
   axis. Measured at 1920px (blocks render at 12.2px), the one-line widths are

       removal note            927px      was 3 lines
       red "missing fields"    727px      was 2
       green intro             593px      was 2
       longest bullet          553px      two bullets were 2

   so one ceiling of 1000px clears all four with room for a longer sentence
   later. `min(100%, ...)` keeps it inside a narrow viewport, so the phone band
   (5.4e) needs nothing of its own.

   DO NOT replace this with a fixed width. A block sized to its content cannot
   leave a short sentence stranded in a wide box, and cannot wrap a long one
   because somebody tuned the number for a different block.
   --------------------------------------------------------------------------- */
.stock-history__note,
.stock-legend--missing,
.stock-legend--ready {
    width: fit-content;
    max-width: min(100%, 1000px);
    margin-inline: auto;
}

/* The header's link row. One link today -- VIEW REMOVED STOCK -- and the place
   the legacy's other five (Add Stock Vehicle, Import, Bring Trade Ins, Bring
   Offer To Buy, Export) join it as they are built. */
/* ---------------------------------------------------------------------------
   THE STOCKLIST'S ACTION LINKS -- five different destinations, not a sentence

   Tim, 23 September 2026: "from a styling point of view, these links need to be
   separated with clear separation between the different sections."

   He is right, and the cause is that they were five inline anchors in a <p>.
   The browser separates inline elements with ONE collapsed space, which is
   exactly the gap between two words -- so ADD STOCK VEHICLE VIEW REMOVED STOCK
   BRING TRADE INS INTO STOCK read as one continuous phrase, and the eye had to
   parse the words to find where one action ended and the next began. Nobody
   wrote that; it is what inline layout does.

   A flex row with a real gap and a rule between items. They stay LINKS rather
   than becoming buttons: the page already carries two rows of pill buttons
   (the 7/14/30/45 day choices and UPDATE REMOVED VEHICLES), and a third row of
   pills would compete with the actual controls instead of reading as "and here
   is where else you can go".

   NO COLOUR IS DECLARED HERE, deliberately. The ambient rule in the width
   vocabulary block paints every link in the application and rule 5.4n's whole
   lesson is that adding a colour to beat it starts a specificity ratchet. These
   are ordinary links and they want the ordinary link colour.
   --------------------------------------------------------------------------- */
.stock-links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;

    /* No column gap: the padding on each item provides it, so the divider sits
       midway between two labels rather than hard against one of them. */
    row-gap: .4rem;

    margin: .75rem 0 .25rem;
    max-width: none;
}

.stock-links > a { padding: 0 1.1rem; }

/* The divider, on every item but the first.

   `a + a` rather than `:not(:first-child)` because the anchors are CONDITIONAL
   -- trade-ins, offers to buy and the import each render only when their gate
   allows -- so "first child" is a different link depending on the dealership,
   and the adjacent-sibling form is right whichever ones are present. */
.stock-links > a + a { border-left: 1px solid var(--eq-formbox-line); }

/* Below the mobile band they STACK, which also disposes of the one artefact a
   divider has: a row that wraps would begin with a rule hanging off its left
   edge. Rule 5.4e -- re-laid-out, never hidden. */
@media (max-width: 760px) {
    .stock-links {
        flex-direction: column;
        row-gap: .6rem;
    }

    .stock-links > a {
        padding: 0;
        border-left: 0;
    }
}

/* ---------------------------------------------------------------------------
   THE M&M CODE LOOKUP -- the magnifying glass, and its dialog

   Reproduces the legacy's ImageButton sitting INSIDE the M&M Code box
   (VehicleStocklistVer3.aspx:7821, which does it with a negative margin and an
   absolute position). Here the box and the button share a relative wrapper, so
   nothing has to be nudged by hand and the button cannot drift off the field
   when the row is narrower.
   --------------------------------------------------------------------------- */
.kv-lookup {
    position: relative;
    display: block;
}

/* The box leaves room for the button rather than running under it. */
.kv-lookup input[type="text"] {
    /* QUALIFIED WITH THE ELEMENT. `input[type="text"]` is (0,1,1) -- a type
       selector plus an attribute selector -- so a bare class at (0,1,0) loses to
       it wherever it sits in the file. CLAUDE.md 5.4n, the width half. */
    width: 100%;
    padding-right: 2rem;
}

.kv-lookup__btn {
    position: absolute;
    top: 50%;
    right: .35rem;
    transform: translateY(-50%);

    display: none;            /* .has-js turns it on -- see below */
    align-items: center;
    justify-content: center;

    width: 1.6rem;
    height: 1.6rem;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm, 3px);
    background: none;
    color: var(--muted);
    cursor: pointer;
}

/* GATED, AND THIS IS THE LOAD-BEARING LINE. The button opens a <dialog> through
   showModal(), so without script it would be an ordinary-looking control that
   does nothing. Hidden unless the document says script ran. Same rule the date
   standard needed (CLAUDE.md 5.4m); the M&M box stays fully typeable either
   way, so this degrades to the status quo rather than to broken. */
.has-js .kv-lookup__btn { display: flex; }

.kv-lookup__btn:hover,
.kv-lookup__btn:focus-visible { color: var(--ink); }

/* The dialog itself. Narrow on purpose: it is five stacked controls, and
   rule 5.5a's argument for NARROW applies exactly -- a caption-and-control
   column stretched wide separates the label from its control. */
.mm-picker { width: min(94vw, 30rem); }

.mm-picker .field { margin-bottom: .6rem; }
.mm-picker select,
.mm-picker input[type="text"] { width: 100%; }

.mm-picker__note { margin: 0 0 .75rem; max-width: none; }

/* The legacy's literal "OR" between the typed box and the cascade. Kept,
   because it is the sentence that explains the screen. */
.mm-picker__or {
    margin: .5rem 0;
    text-align: center;
    font-weight: 700;
    color: var(--muted);
    max-width: none;
}

.mm-picker__cascade {
    border: 1px solid var(--line, #d7dee6);
    border-radius: var(--radius, 6px);
    padding: .75rem;
}

.mm-picker__error {
    margin: .6rem 0 0;
    color: var(--eq-error-ink, #990000);
    font-weight: 700;
    max-width: none;
}

/* Back to the live list, under the archive's filter bar. */
.removed-stock__back { margin: 0 0 .75rem; max-width: none; }

/* THE MANUAL OTP BOX, below the grid. The legacy puts it on one line with its
   Proceed link (.aspx:12270-12272). */
.removal-vehicle__manual {
    margin: 1rem 0 .4rem;
    display: flex;
    gap: .5rem;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
}
/* Qualified for the same reason as the used-vehicle boxes above: a bare class
   loses to input[type="text"], so this box was full width inside the removal
   layer. Measured, not read. */
.removal-vehicle__manual input.removal-otpid { width: 9rem; }

/* A refusal on the Sale path. The legacy's own ForeColor="Red" Bold labels. */
.removal-vehicle__error {
    color: var(--eq-error-ink, #990000);
    font-weight: 700;
    margin: .6rem 0 0;
    max-width: none;
}

/* The OTP grid inside the layer is NOT the outer surface, so it stays flat --
   rule 5.4l. .eq-dialog .grid-wrap already says so; this is only the width. */
.eq-dialog--removal .grid-wrap { margin-top: .6rem; }

/* The top copy of the action bar sits above the grid (Tim, 21 September 2026:
   "top and bottom"), so it needs the spacing below rather than above. */
.stock-actions--top { margin: 0 0 .6rem; }


/* ===================================================================
   IMPORT VEHICLE LIST -- the numbered instructions

   The legacy shows these as eight lines of plain text inside the modal
   (EQNetAdmin/VehicleStockListImport.aspx), and they are the only
   documentation the feature has: a dealership learns the file format
   from here and nowhere else. So they get real list spacing rather
   than being squeezed as boilerplate.

   The measure is deliberately narrower than the .eq-narrow column the
   screen sits in. These are running prose, which rule 5.5 carves out
   from the full-width rule for exactly this reason -- a line of
   instructions set across 1100px is measurably harder to read than one
   set across 68 characters.
   =================================================================== */
.import-steps {
  margin: 0;
  padding-left: 1.4rem;
  max-width: 68ch;
}

.import-steps li { margin-bottom: .5rem; }
.import-steps li:last-child { margin-bottom: 0; }

/* ===========================================================================
   THE PURCHASED FROM TAB: radio rows, the supplier picker, and the pink

   Reproduces the legacy's VehicleStocklistVer3 supplier pane. The colour is the
   legacy's own error pink, already held as --error-bg, so a stock controller of
   twenty years sees the same signal they see today.
   =========================================================================== */

/* The two radio pairs -- seller type, and Dealer / P2P. A fieldset carries the
   accessible group name; it has a browser default border and margin that would
   draw a box round two words, so both are cleared. */
.kv__radios {
    border: 0;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2) var(--s4);
}

/* ---------------------------------------------------------------------------
   THE REQUIRED FIELDS

   aria-required is the marker, set by app.js section 31 from the server's own
   list. NOT a class of our own: it is the attribute that already means this and
   a screen reader announces it, which the legacy's BackColor cannot.

   SCOPED TO THE FORM THAT DECLARES THE LIST, and that is rule 5.4n applied
   rather than quoted: a bare [aria-required] rule would repaint every required
   field in the application the day another screen sets one. The specificity is
   (0,2,1) -- form attribute, marker attribute, element -- so it wins over the
   plain input rules without starting a ratchet.
--------------------------------------------------------------------------- */
form[data-eq-required] input[aria-required="true"] {
    background: var(--error-bg);
}

/* It must NOT beat a refusal. Rule 5.4p paints aria-invalid red and scrolls to
   it; a field that is both required and refused is showing the refusal, which
   is the more urgent of the two. Declared after, and at the same specificity,
   so the cascade settles it by order rather than by a raised selector. */
form[data-eq-required] input[aria-required="true"][aria-invalid="true"] {
    background: var(--bad-tint);
}

/* ---------------------------------------------------------------------------
   THE PICKER HAS NO RULES OF ITS OWN, AND THAT IS THE STANDARD RATHER THAN AN
   OMISSION.

   The row that opens it is .kv-lookup -- the M&M code box's control. The dialog
   is .eq-modal.eq-modal--narrow with a table.grid.grid--eq, class for class the
   Accessory List, the Stock List and the trade-in list. So the field, the
   magnifier, the slate header, the filter row, the banding, the hover, the
   whole-row trigger and the count line all come from rules already above.

   Two drafts of this tab carried their own: first two full-width buttons and
   an .js-only gate, then a .supplier-picker block that re-declared hover, focus,
   a list cap and an italic "New Supplier" row -- against two colour tokens that
   did not exist, hidden behind fallbacks. Tim, 23 September 2026: "why is the
   styling of this not the same as other pages?" Because it had its own. Nothing
   is re-declared here, so nothing can drift.
--------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   SHOWROOM STUDIO -- /Admin/ShowroomStudio (Tim, 30 September 2026)

   Two panels side by side (the showroom, the vehicle), a progress panel, then the
   results. auto-fit with a min() floor, so it is two columns on a desktop and one
   on a phone without hiding anything (rule 5.4e). Existing tokens only; no
   shadow on anything nested in a .card (rule 5.4l); no overflow or overscroll
   rule (5.4k) -- the prompt text wraps instead of scrolling.
   --------------------------------------------------------------------------- */
.studio__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
    gap: var(--s4);
    align-items: start;
    margin-bottom: var(--s4);
}

/* The header line: the vehicle, then Back to vehicle pictures and Back to stock
   list (Tim, 7 October 2026: "two more prominent buttons roughly where I've
   placed them") -- beside it on a wide screen, wrapping under it on a narrow one,
   nothing hidden (rule 5.4e). */
.studio__head {
    display: flex;
    align-items: center;
    gap: var(--s2) var(--s4);
    flex-wrap: wrap;
    margin-bottom: var(--s3);
}
.studio__head > .note { margin: 0; }
.studio__back { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }

/* The crop card's news, and -- once the photograph is done -- the same two ways
   back beside it (Tim, 7 October 2026: "include these buttons at this point once
   the image has been accepted"); under it on a narrow screen. */
.studio__cropnotice {
    display: flex;
    align-items: center;
    gap: var(--s2) var(--s4);
    flex-wrap: wrap;
    margin-bottom: var(--s3);
}
.studio__cropnotice > .alert { margin: 0; }

.studio__image {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--radius);
    background: var(--bg);
}

.studio__form {
    display: grid;
    gap: var(--s2);
    justify-items: start;
    margin-top: var(--s3);
    padding-top: var(--s3);
    border-top: 1px solid var(--border);
}

.studio__form textarea { width: 100%; }

/* THE SHOWROOM IS A THUMBNAIL UNTIL IT IS BEING EDITED. Tim, 30 September 2026: "the
   showroom image I think needs to be a lot smaller, almost thumbnail size, and only if
   I need to edit or upload it can it expand ... a thumbnail with a button that says
   edit showroom background". Shut, its card takes a thumbnail's column and the vehicle
   card the rest; open (the card's <details>), the two share the width as before and
   the full view replaces the thumbnail rather than repeating it. Above the phone band
   only: below it the cards stack anyway (rule 5.4e), and the thumbnail simply sits at
   its own size. */
@media (min-width: 761px) {
    .studio__grid { grid-template-columns: minmax(240px, 320px) minmax(0, 1fr); }
    .studio__grid:has(.studio__roomedit[open]),
    .studio__grid:has(.studio__roomhistory[open]) {
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
    }
}

.studio__thumb {
    display: block;
    width: 100%;
    max-width: 300px;
    height: auto;
    border-radius: var(--radius);
    background: var(--bg);
}

.studio__room:has(.studio__roomedit[open]) .studio__roomthumb { display: none; }

/* The Crop button's bar, standing in for the shut crop card (Tim, 30 September 2026:
   "move the crop button down"). One line, the button then what it does. [hidden] is
   restated because display:flex beats the browser's own [hidden] rule. */
.studio__cropbar {
    display: flex;
    align-items: center;
    gap: var(--s3);
    flex-wrap: wrap;
}
.studio__cropbar[hidden] { display: none; }

/* Create and Process all images side by side (Tim: "an option next to create showroom
   photograph"), wrapping on a narrow screen rather than squeezing. */
.studio__actions {
    display: flex;
    align-items: center;
    gap: var(--s3);
    flex-wrap: wrap;
}

/* The summary IS the button: no disclosure triangle, and its words follow the state
   -- "Edit showroom background" shut, "Close" open -- so one control does both. */
.studio__roomedit > summary { list-style: none; width: max-content; margin-top: var(--s2); }
.studio__roomedit > summary::-webkit-details-marker { display: none; }
.studio__roomedit > summary .studio__when-open { display: none; }
.studio__roomedit[open] > summary .studio__when-shut { display: none; }
.studio__roomedit[open] > summary .studio__when-open { display: inline; }
.studio__roomedit[open] > summary { margin: 0 0 var(--s3); }

/* THE SHOWROOM HISTORY (Tim, 7 October 2026: "I should be able to go back and select any of
   them from history without needing to re generate"). Its summary is a button that names
   its state, as Edit showroom background's does; open, the card takes its full column (the
   grid rule above) and the current thumbnail steps aside, since the history holds it too,
   tagged In use. Tiles in a grid that fills whatever width there is -- one column on a
   phone, nothing hidden (rule 5.4e). Every showroom is 3:2, and so is its tile. */
.studio__roomhistory > summary { list-style: none; width: max-content; margin-top: var(--s2); }
.studio__roomhistory > summary::-webkit-details-marker { display: none; }
.studio__roomhistory > summary .studio__when-open { display: none; }
.studio__roomhistory[open] > summary .studio__when-shut { display: none; }
.studio__roomhistory[open] > summary .studio__when-open { display: inline; }
.studio__room:has(.studio__roomhistory[open]) .studio__roomthumb { display: none; }

.studio__rooms {
    list-style: none;
    margin: var(--s3) 0 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: var(--s3);
}
.studio__roomtile {
    display: grid;
    gap: var(--s2);
    align-content: start;
    justify-items: start;
    padding: var(--s2);
    border: 2px solid var(--border);
    border-radius: var(--radius);
}
.studio__roomtile.is-current { border-color: var(--brand); }
.studio__roomtile img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 3 / 2;
    object-fit: cover;
    border-radius: var(--radius);
    background: var(--bg);
}
.studio__roomtile .note { margin: 0; }

/* THE CROP ON A PAGE. .crop-box dims everything outside it with a 9999px spread
   shadow, and in the Pictures tab's layer the dialog body clipped that to the
   dialog. The studio hosts the stage on the PAGE (Tim, 30 September 2026: "Studio,
   with Crop inside"), where nothing clips it -- so the whole page went dark except
   the box. The stage clips it itself: inset(-22px) rather than overflow: hidden, so
   a corner handle at the picture's edge keeps its whole 44px target (rule 5.4e;
   clip-path clips hit-testing as well as paint). The 22px ring it leaves reads as
   part of the dimmed surround, and the margin keeps it off the text above and below. */
#crop .crop-stage {
    clip-path: inset(-22px);
    margin: 22px auto;
}

/* THE CARD STOPS BELOW THE CHROME, NEVER UNDER IT -- the accordion panes' rule, for
   the same two ways it is reached: "Use this one" calls scrollIntoView(), and the
   crop and Use posts land back on #crop. Without it the card's title and the
   outcome of the save sit behind the menu strip (found on the first screenshot). */
#crop { scroll-margin-top: calc(var(--eq-chrome-h) + var(--s3)); }

/* The photograph picker: the Pictures tab's thumbnails, smaller, as radio tiles.
   The radio stays VISIBLE in the corner -- it is the control, and a hidden one
   would leave keyboard and screen-reader users guessing which tile is chosen. */
.studio__picks {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
    gap: var(--s2);
    margin: 0;
    padding: 0;
    border: 0;
    width: 100%;
}
.studio__picks legend { margin-bottom: var(--s2); }

.studio__pick {
    position: relative;
    display: block;
    border: 2px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    cursor: pointer;
}
.studio__pick img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
    object-fit: contain;
    border-radius: calc(var(--radius) - 2px);
}
.studio__pick input { position: absolute; top: .35rem; left: .35rem; margin: 0; }
.studio__pick .pics__pos { position: absolute; right: .35rem; bottom: .35rem; }
.studio__pick:has(input:checked) { border-color: var(--brand); }
.studio__pick:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }

.studio__check { display: flex; gap: var(--s2); align-items: center; }

/* "Already a showroom picture" on a photograph tile -- the studio replaced it, and
   Process all images skips it (Tim, 30 September 2026: "Yes, skip them"). Top right,
   clear of the radio (top left) and the position number (bottom right). */
.studio__pick .studio__picktag { position: absolute; top: .35rem; right: .35rem; }

/* "REPORT A PROBLEM WITH THIS PHOTOGRAPH" -- Tim, 8 October 2026: an exclamation-mark button at the
   bottom LEFT of every photograph, the position number's mirror image (it sits bottom right), the same
   size and pill shape. Top left is the radio, top right the "Showroom" tag, so this corner is free.

   The colour is the application's own "needs attention" amber family (--warn, darkened), not the brand
   slate: the number is information, this is a way to say something is WRONG, and the two must not read
   as one family of decoration. White on #8f5500 is about 6:1 (computed from the WCAG formula, not
   measured on screen); --warn itself (#b26a00) is only about 4.2:1, under the 4.5:1 floor for text this small.

   hidden until the page script is running (the rule below): without script the dialog it opens cannot
   open, and a button that did nothing would be inert (rule 4.1). `[hidden]` is restated because this
   rule's own display would otherwise beat the attribute. */
.studio__pick .studio__report {
    position: absolute;
    left: .35rem;
    bottom: .35rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.4rem;
    height: 1.4rem;
    padding: 0;
    border: 0;
    border-radius: var(--radius-pill);
    background: #8f5500;
    color: #fff;
    font: inherit;
    font-weight: 700;
    font-size: .82rem;
    line-height: 1;
    cursor: pointer;
}
.studio__pick .studio__report[hidden] { display: none; }
.studio__pick .studio__report:hover { background: #6f4200; }
.studio__pick .studio__report:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* A thumb, not a pointer (rule 5.4e): the 44px floor, on the phone band. The visible disc grows with it
   rather than a transparent halo, because a halo would overlap the position number's own target. */
@media (max-width: 760px) {
    .studio__pick .studio__report { min-width: 2.75rem; height: 2.75rem; font-size: 1.15rem; }
}

/* The report dialog's result line sits inside the body, above the footer's buttons. */
#reportDialog .eq-dialog__body { padding: var(--s3); }
#reportDialog textarea { width: 100%; box-sizing: border-box; margin: .25rem 0 .6rem; }

/* THE QUEUE CARD -- Process all images, as it goes (Tim, 30 September 2026: it runs in
   the background, and its status is "On screen only"). The brand edge of the progress
   panel, because it is the same kind of news; it is reached by #queue after a press,
   so it stops below the chrome as the crop card does. */
.studio__queue { margin-bottom: var(--s4); border-left: 4px solid var(--brand); }
#queue { scroll-margin-top: calc(var(--eq-chrome-h) + var(--s3)); }

/* A FINISHED PRESS IS HISTORY (Tim, 7 October 2026: "or at least have it linked to a
   'History' button"). Shut, one slim line like the crop bar -- the button, then what it
   holds; open, the card body below it. The summary is the whole line, so either part
   opens it; its words follow the state, as Edit showroom background's do. */
.studio__history { margin-top: var(--s4); }   /* last on the page, below the Results (Tim, 7 October 2026) */
.studio__history > summary {
    list-style: none;
    display: flex;
    align-items: center;
    gap: var(--s3);
    flex-wrap: wrap;
    cursor: pointer;
}
.studio__history > summary::-webkit-details-marker { display: none; }
.studio__history > summary .studio__when-open { display: none; }
.studio__history[open] > summary .studio__when-shut { display: none; }
.studio__history[open] > summary .studio__when-open { display: inline; }
.studio__history[open] > summary { margin-bottom: var(--s3); }

/* The refreshed part (app.js section 37 replaces it whole). The summary line carries
   the spinner while anything is pending; each photograph is one line with its tag. */
.studio__queuejobs > p[role="status"] { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.studio__jobs { margin: 0 0 var(--s3); padding-left: 1.4rem; }
.studio__jobs li { margin: .15rem 0; }
.studio__jobs .tag { margin-right: var(--s2); }
/* Process anyway (12 October 2026): a post, styled as a link so it reads inline with the skip message. */
.studio__jobs form.studio__anyway { display: inline; margin: 0 0 0 var(--s2); }
.studio__jobs .studio__anyway button { display: inline; background: none; border: 0; padding: 0; font: inherit;
    color: var(--eq-link); text-decoration: underline; cursor: pointer; }
.studio__jobs .studio__anyway button:hover { color: var(--eq-link-hover); }
@media (max-width: 760px) { .studio__jobs .studio__anyway button { min-height: 44px; } }

/* The progress panel: a step list whose current step is bold and whose finished
   steps carry a tick. It sits OUTSIDE the dimmed work area so it stays legible. */
.studio__progress { margin-bottom: var(--s4); border-left: 4px solid var(--brand); }
.studio__progress.is-failed { border-left-color: var(--bad); }
.studio__progress h2 { display: flex; align-items: center; gap: var(--s2); }
.studio__progress.is-failed .spinner { display: none; }
/* Process anyway (8 October 2026): under the "Left alone" message, a 44px target on a phone. */
.studio__override { margin-top: var(--s3); }
.studio__override .btn { min-height: 44px; }
.studio__override .note { margin-top: var(--s2); }

.studio__steps { margin: 0 0 var(--s2); padding-left: 1.4rem; }
.studio__steps li { color: var(--muted); }
.studio__steps li.is-current { color: var(--ink); font-weight: 600; }
.studio__steps li.is-done { color: var(--good); }
.studio__steps li.is-done::after { content: " \2713"; }
.studio__steps li.is-failed { color: var(--bad); font-weight: 600; }

/* One result: the picture beside its description, stacked on a phone. */
.studio__result {
    display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    gap: var(--s4);
    padding: var(--s4) 0;
    border-top: 1px solid var(--border);
}
.studio__result:first-of-type { border-top: 0; }
.studio__result.is-new .studio__image { outline: 3px solid var(--brand); outline-offset: 3px; }
.studio__result h3 { margin-top: 0; }
/* min-width: 0 lets a long unbroken line in the prompt wrap instead of widening the grid column. */
.studio__detail { min-width: 0; }
.studio__open { display: block; }

.studio__meta {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: .15rem var(--s3);
    margin: 0 0 var(--s3);
    font-size: var(--fs-small);
}
.studio__meta dt { color: var(--muted); }
.studio__meta dd { margin: 0; }

.studio__checks { list-style: none; margin: 0 0 var(--s2); padding: 0; display: grid; gap: .3rem; }
.studio__notes { margin: 0 0 var(--s2); padding-left: 1.2rem; }

.studio__prompt { margin-top: var(--s3); }
.studio__prompt pre {
    white-space: pre-wrap;
    word-break: break-word;
    font-size: var(--fs-small);
    background: var(--bg);
    padding: var(--s2) var(--s3);
    border-radius: var(--radius);
}

/* THE AI ALLOWANCE METER (_AiAllowanceMeter.cshtml, session 7) -- rendered only
   where an allowance limits the account, so never on EasyQuote. The sentence
   carries the meaning; the bar repeats it and is aria-hidden. The onboarding
   bar's shape (.onboard__bar), in the brand colour while photographs are left
   and muted once they are used up: used up is a state of the month, not an
   error. No transition -- the value changes only across a page load. */
.studio__allowance { margin-bottom: var(--s4); }
.studio__allowance > p { margin-bottom: var(--s2); }
.studio__allowancebar {
    height: .5rem;
    border-radius: var(--radius-pill);
    background: var(--brand-tint-strong);
    overflow: hidden;
}
.studio__allowancebar > span {
    display: block;
    height: 100%;
    background: var(--brand);
    border-radius: var(--radius-pill);
}
.studio__allowance.is-used-up .studio__allowancebar > span { background: var(--muted); }

@media (max-width: 760px) {
    .studio__result { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------------
   THE SHOWROOM TAB -- the Showroom Studio product's /Showroom (Tim, 7 October
   2026: "I think we need to have a Tab called 'Showroom'"). Here, in the one
   shared stylesheet, because the product has no stylesheet of its own; EasyQuote
   has no such page, so nothing below reaches it.

   The Studio's own parts carry everything else -- the tiles (.studio__rooms),
   the picture (.studio__image), the forms (.studio__form), the progress panel
   (.studio__progress) -- so only the layout is new: the picture in use beside
   the form that makes a new one, the picture the wider of the two, stacked on a
   phone (rule 5.4e). Existing tokens only; nothing nested in a .card lifts (5.4l).
   --------------------------------------------------------------------------- */
.showroomtab__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
    gap: var(--s4);
    align-items: start;
    margin-bottom: var(--s4);
}
@media (min-width: 761px) {
    /* In use is only as wide as its picture (Tim, 8 October 2026: the card ran on, white, past the
       capped image): fit-content takes the picture's own width, never more than 65% of the row, and
       Your showrooms gets all the rest. The card's text paragraphs are held to 68ch, so they cannot
       stretch it. */
    .showroomtab__grid { grid-template-columns: fit-content(65%) minmax(0, 1fr); }
}

/* THE SHOWROOM IN USE IS SHOWN SMALL, AND THE WHOLE CARD FITS THE SCREEN ON LOAD (Tim, 8 October
   2026). Only what is DISPLAYED is capped -- the file is untouched -- and by HEIGHT, not a fixed
   width x height, so a 3:2 showroom today and a 16:9 one later both fit with no change here (the
   width follows from the picture's own ratio). width:auto overrides the width attribute the markup
   carries; with width and height auto and both maxima, the browser keeps the ratio.

   THE CAP is what is left of the window once everything else in the card, and above it, has had its
   room: --showroom-spare. It is a MEASURED number (the title bar, the heading and intro, the card's
   heading, caption, note and button) -- change what is in the card and measure it again. Never
   above 480px however tall the window, and never below 160px, so a very short window scrolls
   rather than shows a thumbnail. */
.showroomtab { --showroom-spare: 470px; }
.showroomtab #inuse .studio__image {
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: clamp(160px, calc(100vh - var(--showroom-spare)), 480px);
}

/* THE LAYOUT: In use beside Your showrooms, Make a new showroom below, across both. */
.showroomtab__grid > #make { grid-column: 1 / -1; }
.showroomtab__grid > #make .studio__form { max-width: 640px; }
/* Your showrooms sits in the narrower right column: its tiles take what width there is. */
.showroomtab__grid > #showrooms .studio__rooms {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr));
}

/* A tile's two buttons -- Use this showroom, and the red Delete (Tim, 7 October
   2026: "I need a delete button to remove a showroom studio") -- side by side,
   wrapping on a narrow tile rather than squeezing. */
.showroomtab__tileactions {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-wrap: wrap;
}

/* The make card's first form follows its heading directly, so it has none of
   the rule above it that separates the upload form from Generate. */
.showroomtab__first { margin-top: 0; padding-top: 0; border-top: 0; }

/* Every post here lands back on one of these cards (#inuse, #make), and the
   progress panel scrolls itself into view: each stops below the chrome, as the
   Studio's crop and queue cards do. */
.showroomtab .card { scroll-margin-top: calc(var(--eq-chrome-h) + var(--s3)); }

/* The product's account page: Change your business name (Tim, 7 October 2026:
   "also i need to be able to edit my company name") -- a <details> whose summary
   is the button, as Edit showroom background's is. A rename lands on the card
   (#business), which stops below the chrome -- by its class, so no id rule here
   can reach an EasyQuote page. */
.account__business { scroll-margin-top: calc(var(--eq-chrome-h) + var(--s3)); }
.account__rename { margin-top: var(--s3); }
.account__rename > summary { list-style: none; width: max-content; cursor: pointer; }
.account__rename > summary::-webkit-details-marker { display: none; }
.account__rename[open] > summary { margin-bottom: var(--s3); }

/* ---------------------------------------------------------------------------
   SENT MAIL (Settings > Sent Mail, 12 October 2026)
   --------------------------------------------------------------------------- */

/* The WARN tone of a mail wording (MailDeliveryWording: "Queued: will retry",
   "Abandoned: not sent", "Delayed: still trying"). Its own name, on the existing
   warn tokens, rather than borrowing .pill--otp's colour: a class named for a
   quote state would be "tidied" the day OTP pills change hue. Good and bad reuse
   .pill--ok and .pill--stop, whose names already say what they mean. */
.pill--warn { background: var(--warn-tint); color: var(--warn); }

/* The recipient, IN FULL (Tim's decision 2). An address has no spaces, so without
   this one long address sets the width of the whole grid and pushes Status and
   Delivery status off the edge. anywhere, not break-all: it breaks only when the
   cell is genuinely too narrow -- the VIN column's rule (.eq-vehicles__vin). */
.sentmail__to { overflow-wrap: anywhere; }
