/* ==========================================================================
   DesiMenus — component layer for the Blazor consoles.

   Reads ONLY the custom properties defined in desimenus-tokens.css. No colour,
   no radius, no type size is written literally in this file — per §13.2 of the
   design system, the token file stays the single place a value is authored.

   That rule is what makes the SaaS console differ from the Web Admin by
   exactly one line (see [data-console="saas"] below) rather than by
   a second stylesheet nobody remembers to keep in step.

   No utility framework. Tailwind or Bootstrap would drag in a second source of
   spacing and colour truth, and the first time someone reached for `p-4`
   instead of `--ds-space-4` the token file would stop being authoritative.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; }

html, body {
    margin: 0;
    padding: 0;
    height: 100%;
}

body {
    background: var(--ds-canvas);
    color: var(--ds-text);
    font: var(--ds-body);
    -webkit-font-smoothing: antialiased;
}

/* Tabular figures everywhere a number can change.

   Without `tnum`, a total that ticks from 1,199 to 1,201 shifts every digit
   left of the change, and a column of amounts fails to line up. On a screen
   whose whole job is money, that is not cosmetic. */
.numeric,
.numeric-sm,
.numeric-xl {
    font-feature-settings: var(--ds-numeric-features);
    font-variant-numeric: tabular-nums;
}

.numeric    { font: var(--ds-numeric); }
.numeric-sm { font: var(--ds-numeric-sm); }
.numeric-xl { font: var(--ds-numeric-xl); }

/* Focus ring. Two-tone so it survives on both light and dark surfaces —
   §7.4 of the design system. */
:focus-visible {
    outline: var(--ds-focus-width) solid var(--ds-focus);
    outline-offset: var(--ds-focus-offset);
    box-shadow: 0 0 0 var(--ds-focus-width) var(--ds-focus-inner);
    border-radius: var(--ds-radius-xs);
}

/* --------------------------------------------------------------------------
   THE RAIL — the one shape signature of the product (§5)

   A full-height stroke on a card's leading edge encoding state through THREE
   independent channels: hue, stroke pattern, and an adjacent icon + label.

   Three channels rather than one because colour alone fails in three real
   situations this product lives in:
     * a KOT reprinted on a 58 mm thermal printer is monochrome;
     * a chef may be colour-blind;
     * a screen in direct sunlight loses hue separation before it loses
       pattern separation.

   The rail also REPLACES the leading border-radius, so a railed card is square
   on that edge and rounded on the other three. That asymmetry is why a
   DesiMenus screenshot is recognisable at thumbnail size.
   -------------------------------------------------------------------------- */

.rail-card {
    position: relative;
    display: block;
    background: var(--ds-surface);
    border: 1px solid var(--ds-border);
    border-left: none;                                /* the rail is the left edge */
    border-radius: var(--ds-radius-railed-card);      /* 0 md md 0 */
    padding: var(--ds-pad-y) var(--ds-pad-x);
    padding-left: calc(var(--ds-pad-x) + var(--ds-rail-w));
    transition: background var(--ds-motion-fast) var(--ds-ease-standard);
}

.rail-card::before {
    content: "";
    position: absolute;
    inset-inline-start: 0;
    top: 0;
    bottom: 0;
    width: var(--ds-rail-w);
    background: var(--rail-hue, var(--ds-border-strong));
}

/* Pressed darkens the surface one step. It never lifts — a list that lifts on
   press reads as a button, and these rows are usually navigational. */
.rail-card:active { background: var(--ds-surface-sunken); }

/* ---- Rail patterns (§5) ---------------------------------------------------
   Solid   resolved, nothing pending
   Dashed  in progress, time is passing
   Double  blocked; a person must decide
   Hollow  void or cancelled
   -------------------------------------------------------------------------- */

.rail-solid::before  { background: var(--rail-hue); }

.rail-dashed::before {
    /* 8 px dash / 6 px gap. Kitchen density steps to 14/10 below. */
    background: repeating-linear-gradient(
        to bottom,
        var(--rail-hue) 0 8px,
        transparent 8px 14px);
}

[data-density="kitchen"] .rail-dashed::before {
    background: repeating-linear-gradient(
        to bottom,
        var(--rail-hue) 0 14px,
        transparent 14px 24px);
}

.rail-double::before {
    /* Two 2 px strokes, 2 px apart. Reads as "stop" at a glance without
       relying on the hue being red. */
    background: linear-gradient(
        to right,
        var(--rail-hue) 0 2px,
        transparent 2px 4px,
        var(--rail-hue) 4px 6px,
        transparent 6px);
}

.rail-hollow::before {
    background: transparent;
    border: 1.5px solid var(--rail-hue);
}

/* A hollow rail always pairs with struck-through content — §1.10 of the
   screen spec. Encoded here so a caller cannot forget it. */
.rail-hollow .rail-card__title,
.rail-hollow .rail-card__value {
    text-decoration: line-through;
    color: var(--ds-text-tertiary);
}

/* ---- Rail state hues, bound to the state map in §5.1 -----------------------
   These names match the domain states exactly. If a state is not here, no rail
   exists for it — which is the design system's rule, and keeps someone from
   inventing a fifth pattern for a sixth meaning. */

.rail-open        { --rail-hue: var(--ds-state-open); }
.rail-in-kitchen  { --rail-hue: var(--ds-state-in-kitchen); }
.rail-served      { --rail-hue: var(--ds-state-served); }
.rail-billed      { --rail-hue: var(--ds-state-billed); }
.rail-cancelled   { --rail-hue: var(--ds-state-cancelled); }
.rail-pending     { --rail-hue: var(--ds-state-pending); }
.rail-settled     { --rail-hue: var(--ds-state-settled); }
.rail-failed      { --rail-hue: var(--ds-state-failed); }
.rail-refunded    { --rail-hue: var(--ds-state-refunded); }
.rail-void        { --rail-hue: var(--ds-state-void); }
.rail-locked      { --rail-hue: var(--ds-state-locked); }
.rail-neutral     { --rail-hue: var(--ds-border-strong); }

.rail-card__header {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    justify-content: space-between;
}

.rail-card__title {
    font: var(--ds-title);
    color: var(--ds-text);
    margin: 0;
}

.rail-card__meta {
    font: var(--ds-caption);
    color: var(--ds-text-secondary);
    margin-top: var(--ds-space-1);
}

.rail-card__value {
    font: var(--ds-numeric);
    font-feature-settings: var(--ds-numeric-features);
    text-align: right;
    white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Status chip (§12.2)

   Fixed structure: 16 px icon, 6 px gap, label. Tinted fill from the family's
   50 step, border from 200, text from 600.
   -------------------------------------------------------------------------- */

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-2);
    height: var(--ds-chip-h);
    padding: 0 var(--ds-space-3);
    border-radius: var(--ds-radius-pill);
    border: 1px solid transparent;
    font: var(--ds-label);
    white-space: nowrap;
    background: var(--chip-fill, var(--ds-surface-sunken));
    color: var(--chip-hue, var(--ds-text-secondary));
    border-color: var(--chip-hue, var(--ds-border-strong));
}

.chip__icon { width: 16px; height: 16px; flex: 0 0 16px; }

.chip-open       { --chip-hue: var(--ds-state-open);       --chip-fill: var(--ds-state-open-fill); }
.chip-in-kitchen { --chip-hue: var(--ds-state-in-kitchen); --chip-fill: var(--ds-state-in-kitchen-fill); }
.chip-served     { --chip-hue: var(--ds-state-served);     --chip-fill: var(--ds-state-served-fill); }
.chip-billed     { --chip-hue: var(--ds-state-billed);     --chip-fill: var(--ds-state-billed-fill); }
.chip-cancelled  { --chip-hue: var(--ds-state-cancelled);  --chip-fill: var(--ds-state-cancelled-fill); }
.chip-pending    { --chip-hue: var(--ds-state-pending);    --chip-fill: var(--ds-state-pending-fill); }
.chip-settled    { --chip-hue: var(--ds-state-settled);    --chip-fill: var(--ds-state-settled-fill); }
.chip-failed     { --chip-hue: var(--ds-state-failed);     --chip-fill: var(--ds-state-failed-fill); }
.chip-refunded   { --chip-hue: var(--ds-state-refunded);   --chip-fill: var(--ds-state-refunded-fill); }
.chip-void       { --chip-hue: var(--ds-state-void);       --chip-fill: var(--ds-state-void-fill); }
.chip-locked     { --chip-hue: var(--ds-state-locked);     --chip-fill: var(--ds-state-locked-fill); }

/* --------------------------------------------------------------------------
   Buttons (§12.1)

   Busy state holds a minimum 300 ms and preserves the button's width, so the
   layout never jumps when a fast response returns. The width preservation is
   done by keeping the label in the DOM at zero opacity rather than replacing
   it — swapping text for a spinner is what causes the jump.
   -------------------------------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-2);
    position: relative;
    height: var(--ds-btn-h);
    min-width: var(--ds-min-target);
    padding: 0 var(--ds-space-5);
    border-radius: var(--ds-radius-sm);
    border: 1px solid transparent;
    font: var(--ds-label);
    cursor: pointer;
    transition: background var(--ds-motion-fast) var(--ds-ease-standard);
}

.btn:disabled {
    cursor: not-allowed;
    background: var(--ds-slate-100);
    color: var(--ds-text-tertiary);
    border-color: transparent;
}

.btn-primary     { background: var(--ds-action); color: var(--ds-text-on-action); }
.btn-primary:hover:not(:disabled)  { background: var(--ds-action-hover); }
.btn-primary:active:not(:disabled) { background: var(--ds-action-pressed); }

.btn-secondary   { background: transparent; color: var(--ds-link); border-color: var(--ds-border-strong); }
.btn-secondary:hover:not(:disabled) { background: var(--ds-action-subtle); }

.btn-ghost       { background: transparent; color: var(--ds-text); }
.btn-ghost:hover:not(:disabled) { background: var(--ds-surface-sunken); }

.btn-destructive { background: var(--ds-mirchi-600); color: var(--ds-slate-0); }
.btn-destructive:hover:not(:disabled) { background: var(--ds-mirchi-500); }

/* Locked (§12.3) — NOT disabled.

   "Disabled" means not possible right now: faint, uninteractive.
   "Locked" means possible, but not by you: BRIGHT, tappable, opens the manager
   override prompt.

   The visual difference is deliberately large. A waiter should be able to spot
   every locked control on a screen at a glance, because spotting them is how
   they know to call a manager. */
.btn-locked {
    background: var(--ds-state-locked-fill);
    color: var(--ds-state-locked);
    border: 1.5px double var(--ds-haldi-500);
    cursor: pointer;
    opacity: 1;
}

.btn__label--busy { visibility: hidden; }

.btn__spinner {
    position: absolute;
    width: 16px; height: 16px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: ds-spin 640ms linear infinite;
}

@keyframes ds-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
    .btn__spinner { animation-duration: 2400ms; }
    * { transition-duration: 0ms !important; }
}

/* --------------------------------------------------------------------------
   Application shell
   -------------------------------------------------------------------------- */

.app-shell {
    display: grid;
    grid-template-columns: auto 1fr;
    /* Row 1 is the header (auto height), row 2 is everything else. The rail
       spans BOTH rows (below), so visually it still reads as one full-height
       sidebar next to a header-plus-content column — the header never
       intrudes into the rail's own space the way it would if the whole shell
       were a single row with the header stacked inside work-surface. */
    grid-template-rows: auto 1fr;
    min-height: 100vh;
}

/* ⚠ THE ONE LINE THAT DISTINGUISHES THE TWO CONSOLES (§13.2).

   SaaS Admin differs from Web Admin by exactly one token: the brand accent
   moves to jamun. That is a SAFETY FEATURE, not decoration — a support agent
   with both consoles open needs to know at a glance which one they are about
   to click "suspend tenant" in. */
/* Matches BOTH shells: .app-shell (console) and .auth-shell (login).
   Scoping this to .app-shell was why the sign-in screen stayed violet. */
[data-console="saas"] {
    --ds-action: var(--ds-neel-600);
    --ds-action-hover: var(--ds-neel-500);
    --ds-action-pressed: var(--ds-neel-700);
    --ds-action-subtle: var(--ds-neel-50);
    --ds-link: var(--ds-neel-600);
    --ds-focus: var(--ds-neel-600);

    /* The rail is its own dark surface. This is the console signal doing
       real work: a tinted accent is easy to miss on a glance across two
       monitors, a navy sidebar is not. */
    /* ⚠ DELIBERATELY NOT near-black. The first attempt used neel-950
       (#091830) and it was genuinely uncomfortable: a near-black panel
       beside a near-white work surface is a ~19:1 jump that the eye has to
       re-adapt to on every glance between them. This slate-indigo sits
       around 11:1 against its own text and much closer to the content
       area, which is what stops the edge burning. */
    --ds-rail-bg: #1C3054;
    --ds-rail-bg-deep: #172846;
    --ds-rail-fg: #E3EBF8;
    --ds-rail-fg-muted: #A9BDDD;
    --ds-rail-hover: rgba(255, 255, 255, 0.08);
    --ds-rail-active: rgba(255, 255, 255, 0.12);
    --ds-rail-marker: #7FA9F0;
    --ds-rail-border: rgba(255, 255, 255, 0.10);
}

.rail-nav {
    display: flex;
    flex-direction: column;
    grid-row: 1 / -1;
    width: 248px;
    background: var(--ds-surface);
    border-right: 1px solid var(--ds-border);
    padding: var(--ds-space-4) var(--ds-space-3);
    gap: var(--ds-space-1);
}

.rail-nav[data-collapsed="true"] { width: 64px; }

.rail-nav__brand {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    padding: var(--ds-space-2) var(--ds-space-3) var(--ds-space-5);
}

.rail-nav__brand-name { font: var(--ds-title); color: var(--ds-text); }

.rail-nav__console-badge {
    font: var(--ds-overline);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ds-action);
}

.rail-nav__link {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    height: var(--ds-row-h);
    padding: 0 var(--ds-space-3);
    border-radius: var(--ds-radius-sm);
    color: var(--ds-text-secondary);
    font: var(--ds-body);
    text-decoration: none;
}

.rail-nav__link:hover { background: var(--ds-surface-sunken); color: var(--ds-text); }

.rail-nav__link.active {
    background: var(--ds-action-subtle);
    color: var(--ds-action);
    font-weight: 500;
}

.rail-nav__section {
    font: var(--ds-overline);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ds-text-tertiary);
    padding: var(--ds-space-4) var(--ds-space-3) var(--ds-space-1);
}

.work-surface {
    display: flex;
    flex-direction: column;
    grid-column: 2;
    min-width: 0;                 /* lets grid children shrink instead of overflowing */
    padding: var(--ds-space-6);
    gap: var(--ds-space-5);
    overflow-y: auto;
}

/* --------------------------------------------------------------------------
   App header (Pass 7) — global search, notification bell, outlet switcher,
   user menu. Sits in the shell's own grid row, ABOVE work-surface's scroll
   region, so it never scrolls away — a manager mid-scroll through a long
   report still has the outlet switcher and the bell in reach.
   -------------------------------------------------------------------------- */

.app-header {
    grid-column: 2;
    display: flex;
    align-items: center;
    gap: var(--ds-space-4);
    height: 60px;
    padding: 0 var(--ds-space-6);
    background: var(--ds-surface);
    border-bottom: 1px solid var(--ds-border);
    position: sticky;
    top: 0;
    z-index: 20;
}

.app-header__search {
    flex: 0 1 360px;
    min-width: 160px;
    position: relative;
}

.app-header__search-icon {
    position: absolute;
    left: var(--ds-space-3);
    top: 50%;
    transform: translateY(-50%);
    color: var(--ds-text-tertiary);
    pointer-events: none;
}

.app-header__search .field__input {
    width: 100%;
    padding-left: calc(var(--ds-space-3) * 2 + 16px);
    height: 36px;
}

.app-header__spacer { flex: 1 1 auto; min-width: var(--ds-space-4); }

.app-header__icon-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: var(--ds-radius-sm);
    border: none;
    background: transparent;
    color: var(--ds-text-secondary);
    cursor: pointer;
}

.app-header__icon-btn:hover { background: var(--ds-surface-sunken); color: var(--ds-text); }

.app-header__badge {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--ds-mirchi-500);
    border: 2px solid var(--ds-surface);
}

.app-header__user-btn {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    height: 36px;
    padding: 0 var(--ds-space-2) 0 var(--ds-space-3);
    border: none;
    border-radius: var(--ds-radius-sm);
    background: transparent;
    color: var(--ds-text);
    font: var(--ds-label);
    cursor: pointer;
    max-width: 220px;
}

.app-header__user-btn:hover { background: var(--ds-surface-sunken); }

.app-header__user-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.app-header__outlet-btn {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    height: 36px;
    padding: 0 var(--ds-space-3);
    border: 1px solid var(--ds-border-strong);
    border-radius: var(--ds-radius-sm);
    background: transparent;
    color: var(--ds-text);
    font: var(--ds-label);
    cursor: pointer;
    max-width: 220px;
}

.app-header__outlet-btn:hover { background: var(--ds-surface-sunken); }
.app-header__outlet-btn span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --------------------------------------------------------------------------
   Generic dropdown / popover — the bell, the outlet switcher and the user
   menu all use this ONE structure rather than three bespoke ones, so a
   click-outside-to-close and an escape-to-close only need writing once.
   -------------------------------------------------------------------------- */

.dropdown { position: relative; }

.dropdown__panel {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    min-width: 260px;
    max-width: 360px;
    max-height: 420px;
    overflow-y: auto;
    background: var(--ds-surface);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-md);
    box-shadow: 0 8px 24px rgba(11, 18, 25, 0.14);
    padding: var(--ds-space-2);
    z-index: 30;
}

.dropdown__panel--wide { min-width: 320px; }

/* ⚠ FLEX, BECAUSE THE HEADING GREW ACTIONS AND NOBODY TOLD THE STYLE. CE-10 put
   a "Mark all read" button inside this element and it rendered hard against the
   text — "YOUR MESSAGESMark all read", as one word. space-between is safe for the
   headings that have no actions: a lone child stays exactly where it was. */
.dropdown__section-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ds-space-2);
    font: var(--ds-overline);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ds-text-tertiary);
    padding: var(--ds-space-2) var(--ds-space-3) var(--ds-space-1);
}

/* The heading's action cluster — "Mark all read" beside "Clear all". */
.dropdown__section-actions {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    flex-shrink: 0;
}

.dropdown__item {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    width: 100%;
    padding: var(--ds-space-2) var(--ds-space-3);
    border: none;
    background: transparent;
    color: var(--ds-text);
    font: var(--ds-body);
    text-align: left;
    text-decoration: none;
    border-radius: var(--ds-radius-sm);
    cursor: pointer;
}

.dropdown__item:hover { background: var(--ds-surface-sunken); }
.dropdown__item[aria-current="true"] { background: var(--ds-action-subtle); color: var(--ds-action); }

.dropdown__empty {
    padding: var(--ds-space-4) var(--ds-space-3);
    text-align: center;
    color: var(--ds-text-tertiary);
    font: var(--ds-caption);
}

.dropdown__divider { height: 1px; background: var(--ds-border); margin: var(--ds-space-2) 0; }

/* One alert (low stock / failed payment / dunning) from the notification
   pipeline, rendered read-only in the bell — see AppHeader.razor for why
   this is a tenant-wide operational feed rather than a personal inbox. */
.notification-item {
    display: flex;
    align-items: flex-start;
    gap: var(--ds-space-2);
    padding: var(--ds-space-2) var(--ds-space-3);
    border-radius: var(--ds-radius-sm);
}

.notification-item__icon { flex-shrink: 0; margin-top: 2px; }

/* flex: 1 so the action cluster below is pushed to the right edge rather than
   sitting immediately after a short message's text. */
.notification-item__body { min-width: 0; flex: 1 1 auto; }

/* "Read" and "Clear", stacked at the row's trailing edge. */
.notification-item__actions {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    flex-shrink: 0;
}
.notification-item__title { font: var(--ds-label); color: var(--ds-text); }
.notification-item__meta { font: var(--ds-caption); color: var(--ds-text-tertiary); margin-top: 2px; }

.notification-item--failed .notification-item__icon { color: var(--ds-state-failed); }
.notification-item--warn .notification-item__icon { color: var(--ds-state-pending); }

/* --------------------------------------------------------------------------
   CE-10 — the in-app inbox in the header bell.

   ⚠ THE BODY TEXT IS CLAMPED TO TWO LINES RATHER THAN LEFT TO WRAP. A bell
   dropdown holds ten messages; one long message would push the other nine off
   the bottom of the screen, and the person would conclude nothing else had
   arrived. The notifications page shows the whole text.
   -------------------------------------------------------------------------- */

.notification-item__text {
    font: var(--ds-caption);
    color: var(--ds-text-secondary);
    margin-top: 2px;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Unread is marked by weight and a rule down the left, not by colour alone —
   §11's contrast floor, and the only other state on this row is "failed",
   which is already red. */
.notification-item--unread {
    background: var(--ds-surface-raised);
    box-shadow: inset 2px 0 0 var(--ds-action);
}

.notification-item--unread .notification-item__title { font-weight: 600; }
.notification-item--unread .notification-item__icon { color: var(--ds-action); }

/* --------------------------------------------------------------------------
   The notifications page (/notifications). Card-per-message rather than a
   data-table, because the body is prose of arbitrary length — a table row would
   either clip it or make every row a different height.
   -------------------------------------------------------------------------- */

.notification-card { transition: box-shadow 120ms ease; }

.notification-card--unread {
    box-shadow: inset 3px 0 0 var(--ds-action);
}

.notification-card--unread .card__title { font-weight: 600; }

/* The counted badge. Wider than the dot it replaces, so it is positioned from
   the same corner but sized by its content. */
.app-header__badge--count {
    width: auto;
    height: auto;
    min-width: 16px;
    top: 0;
    right: 0;
    padding: 0 4px;
    border-radius: 999px;
    font: var(--ds-caption);
    font-weight: 600;
    line-height: 16px;
    text-align: center;
    color: #fff;
}

/* --------------------------------------------------------------------------
   Breadcrumbs — any screen more than one level deep (§1.1). Sits above the
   page header rather than inside it, so the header's title can stay the
   large §12 display style without a breadcrumb trail competing for weight.
   -------------------------------------------------------------------------- */

.breadcrumbs {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--ds-space-1);
    font: var(--ds-caption);
    color: var(--ds-text-tertiary);
}

.breadcrumbs a {
    color: var(--ds-text-secondary);
    text-decoration: none;
}

.breadcrumbs a:hover { color: var(--ds-action); text-decoration: underline; }

.breadcrumbs__current { color: var(--ds-text); font-weight: 500; }

.breadcrumbs__sep { color: var(--ds-text-tertiary); }

/* --------------------------------------------------------------------------
   Page header
   -------------------------------------------------------------------------- */

.page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--ds-space-4);
}

.page-header__title { font: var(--ds-display); margin: 0; }
.page-header__subtitle { font: var(--ds-body); color: var(--ds-text-secondary); margin-top: var(--ds-space-1); }
.page-header__actions { display: flex; gap: var(--ds-space-2); flex-shrink: 0; }

/* --------------------------------------------------------------------------
   Cards, metrics, tables
   -------------------------------------------------------------------------- */

.card {
    background: var(--ds-surface);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-card);
    padding: var(--ds-space-5);
}

.card__title { font: var(--ds-title); margin: 0 0 var(--ds-space-4); }

.metric-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--ds-space-4);
}

.metric__label {
    font: var(--ds-overline);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ds-text-secondary);
}

.metric__value {
    font: var(--ds-numeric-xl);
    font-feature-settings: var(--ds-numeric-features);
    margin-top: var(--ds-space-2);
}

.metric__caption { font: var(--ds-caption); color: var(--ds-text-tertiary); margin-top: var(--ds-space-1); }

/* Data tables run at COMPACT density regardless of the app setting (§12.9).
   A report is read by someone scanning for an outlier, and more rows on screen
   is worth more than a larger tap target on a surface nobody taps. */
.data-table {
    width: 100%;
    border-collapse: collapse;
    font: var(--ds-body);
}

.data-table th {
    font: var(--ds-label);
    color: var(--ds-text-secondary);
    text-align: left;
    padding: var(--ds-space-2) var(--ds-space-3);
    border-bottom: 1px solid var(--ds-border-strong);
    white-space: nowrap;
}

.data-table td {
    padding: var(--ds-space-2) var(--ds-space-3);
    border-bottom: 1px solid var(--ds-border);
}

.data-table tbody tr:hover { background: var(--ds-surface-sunken); }

.data-table .num {
    text-align: right;
    font: var(--ds-numeric-sm);
    font-feature-settings: var(--ds-numeric-features);
}

/* --------------------------------------------------------------------------
   DataTable.razor (Pass 7) — the shared component every list screen and
   report detail table uses. §1.2 / §12.9: sticky header, sticky first
   column below 1366px, sortable headers, a totals row, and the toolbar that
   carries the column-visibility and density toggles.
   -------------------------------------------------------------------------- */

.data-table-toolbar {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    margin-bottom: var(--ds-space-3);
}

.data-table-toolbar__spacer { flex: 1 1 auto; }

.data-table-wrap {
    overflow-x: auto;
    /* Sticky header and sticky first column are both scoped to THIS
       ancestor rather than to .work-surface, so a table dropped into a
       right-side detail panel or a modal gets the same behaviour without
       depending on the page around it. */
    position: relative;
}

.data-table-wrap .data-table thead th {
    position: sticky;
    top: 0;
    background: var(--ds-surface-sunken);
    z-index: 2;
}

@media (max-width: 1366px) {
    .data-table-wrap .data-table td:first-child,
    .data-table-wrap .data-table th:first-child {
        position: sticky;
        left: 0;
        background: var(--ds-surface);
        z-index: 1;
    }

    .data-table-wrap .data-table thead th:first-child { z-index: 3; background: var(--ds-surface-sunken); }
}

.data-table th.sortable {
    cursor: pointer;
    user-select: none;
}

.data-table th.sortable:hover { color: var(--ds-text); }

.data-table th .sort-indicator {
    display: inline-flex;
    align-items: center;
    margin-left: 4px;
    opacity: 0.4;
}

.data-table th.sorted .sort-indicator,
.data-table th.sorted-secondary .sort-indicator { opacity: 1; }

.data-table th.sorted-secondary .sort-indicator { color: var(--ds-text-tertiary); }

/* Totals row — §12.9: slate-50 fill, 2px slate-200 top border, numeric at
   600 weight. Lives in a <tfoot> so it stays visually distinct from data
   rows and is skipped by row-hover styling. */
.data-table tfoot td {
    padding: var(--ds-space-2) var(--ds-space-3);
    background: var(--ds-surface-sunken);
    border-top: 2px solid var(--ds-border-strong);
    font: var(--ds-numeric);
    font-weight: 600;
    font-feature-settings: var(--ds-numeric-features);
}

.data-table tfoot td.num { text-align: right; }
.data-table tfoot tr:hover { background: var(--ds-surface-sunken); }

/* Column-visibility checklist, inside a .dropdown__panel. */
.column-toggle-item {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    padding: var(--ds-space-1) var(--ds-space-3);
    font: var(--ds-body);
}

/* Compact toggle — a per-user preference, not a per-table one, so it is a
   single control in the toolbar rather than repeated everywhere. It sets
   data-density on the WRAP, which is what §1.1 means by "density is a
   per-region setting" — the table goes dense without the rest of the
   comfortable-density shell changing around it. */
.data-table-wrap[data-density="compact"] .data-table th,
.data-table-wrap[data-density="compact"] .data-table td {
    padding: var(--ds-space-1) var(--ds-space-2);
}

.data-table-wrap[data-density="compact"] .data-table { font: var(--ds-body); }

.density-toggle {
    display: inline-flex;
    border: 1px solid var(--ds-border-strong);
    border-radius: var(--ds-radius-sm);
    overflow: hidden;
}

.density-toggle button {
    border: none;
    background: transparent;
    color: var(--ds-text-secondary);
    padding: 0 var(--ds-space-3);
    height: 32px;
    font: var(--ds-label);
    cursor: pointer;
}

.density-toggle button + button { border-left: 1px solid var(--ds-border-strong); }
.density-toggle button[aria-pressed="true"] { background: var(--ds-action); color: var(--ds-text-on-action); }

.pagination-bar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--ds-space-3);
    margin-top: var(--ds-space-4);
}

.pagination-bar__range { font: var(--ds-caption); color: var(--ds-text-secondary); margin-right: auto; }

.pagination-bar select.field__input {
    height: 32px;
    width: auto;
    padding: 0 var(--ds-space-2);
}

/* --------------------------------------------------------------------------
   Screen states (§12.8)

   Loading / empty / error / permission-denied are FIRST-CLASS, not
   afterthoughts. Every data surface renders exactly one of them or its
   content — a page that shows an empty table when it actually failed to load
   is worse than one that shows nothing.
   -------------------------------------------------------------------------- */

.screen-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-3);
    padding: var(--ds-space-16) var(--ds-space-6);
    text-align: center;
    color: var(--ds-text-secondary);
}

.screen-state__title { font: var(--ds-title); color: var(--ds-text); }
.screen-state__body  { font: var(--ds-body); max-width: 46ch; }

.screen-state--error .screen-state__title { color: var(--ds-state-failed); }

.skeleton {
    background: linear-gradient(90deg,
        var(--ds-surface-sunken) 0%,
        var(--ds-border) 50%,
        var(--ds-surface-sunken) 100%);
    background-size: 200% 100%;
    animation: ds-shimmer 1400ms ease-in-out infinite;
    border-radius: var(--ds-radius-xs);
    height: var(--ds-row-h);
}

@keyframes ds-shimmer { to { background-position: -200% 0; } }

/* --------------------------------------------------------------------------
   Forms
   -------------------------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: var(--ds-space-2); }

.field__label { font: var(--ds-label); color: var(--ds-text-secondary); }

.field__input {
    height: var(--ds-btn-h);
    padding: 0 var(--ds-space-3);
    border: 1px solid var(--ds-border-strong);
    border-radius: var(--ds-radius-sm);
    background: var(--ds-surface);
    color: var(--ds-text);
    font: var(--ds-body);
}

.field__input:focus { border-color: var(--ds-action); }

.field__error { font: var(--ds-caption); color: var(--ds-state-failed); }

.field--invalid .field__input { border-color: var(--ds-state-failed); }

/* Inline banner — used for form-level errors and the assist-mode warning. */
.banner {
    /* ⚠ NOT display:flex. It was, and that broke every banner whose text
       wraps — including the assist-session dialog, where the line came out
       as three narrow columns with a stray space before "'s".

       In a flex container each TEXT RUN becomes its own anonymous flex
       item, so "…you accessed", <strong>Name</strong> and "'s data…" were
       three siblings laid out side by side with a gap between them. It
       only looked correct while the content happened to fit on one line.

       103 banners in this project pass bare text, so fixing the markup was
       never the right move — the container was. Block with an inline icon
       lets the sentence flow and still aligns an icon. */
    display: block;
    gap: var(--ds-space-3);
    padding: var(--ds-space-3) var(--ds-space-4);
    border-radius: var(--ds-radius-sm);
    border: 1px solid var(--banner-hue, var(--ds-border-strong));
    background: var(--banner-fill, var(--ds-surface-sunken));
    color: var(--banner-hue, var(--ds-text));
    font: var(--ds-body);
}

.banner-error  { --banner-hue: var(--ds-state-failed);  --banner-fill: var(--ds-state-failed-fill); }
.banner-warn   { --banner-hue: var(--ds-state-pending); --banner-fill: var(--ds-state-pending-fill); }
.banner-info   { --banner-hue: var(--ds-action);        --banner-fill: var(--ds-action-subtle); }

/* ⚠ ADDED BECAUSE THEY WERE ALREADY BEING USED AND SILENTLY DOING NOTHING.
   `banner-success` and `banner-warning` appear in markup (Outlets.razor's
   FSSAI expiry notice, CO-1's verified-address confirmation) but had no rule,
   so both fell back to the neutral default — a success message and a warning
   rendered identically to an ordinary grey box. A missing CSS class is not an
   error anywhere in the pipeline, which is exactly why this went unnoticed.

   `banner-warning` is an alias of `banner-warn` rather than a second look:
   two names for one state is a mistake to absorb, not to design around. */
.banner-success { --banner-hue: var(--ds-state-settled); --banner-fill: var(--ds-state-settled-fill); }
.banner-warning { --banner-hue: var(--ds-state-pending); --banner-fill: var(--ds-state-pending-fill); }

/* --------------------------------------------------------------------------
   Ambient bar (§12.7) — the assist-mode warning strip.

   ⚠ Deliberately impossible to miss. When a DesiMenus engineer is inside a
   tenant's account, that fact must be visible on EVERY screen, permanently,
   not tucked into a status bar. See IAssistModeService: the whole design
   assumes this capability will eventually be abused and optimises for making
   it obvious and recorded.
   -------------------------------------------------------------------------- */

.ambient-bar {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-3);
    padding: var(--ds-space-2) var(--ds-space-4);
    background: var(--ds-jamun-500);
    color: var(--ds-slate-0);
    font: var(--ds-label);
}

/* --------------------------------------------------------------------------
   Login
   -------------------------------------------------------------------------- */

/* ══════════════════════════════════════════════════════════════════════
   THE SIGN-IN SCREEN

   ⚠ THE ONE PLACE IN THIS PRODUCT WHERE DECORATION IS ALLOWED, AND THE
     REASON IS §2.5 RATHER THAN AN EXCEPTION TO IT.

   "Restraint at the counter" forbids decorative illustration on SERVICE
   surfaces — the screens somebody works on for eight hours, where anything
   ornamental competes with a number that matters. Sign-in is not one of
   those. Nobody works here; it is looked at for four seconds, twice a day,
   and it is the only screen a new restaurant sees before it sees its own
   data. The SaaS console already took this view — its auth shell carries a
   radial wash — so this is the established reading of the rule.

   ⚠ THE FIELD IS DARK AND THE TEXTURE SITS UNDER A SCRIM. The first
   version put dark motifs on the pale canvas and it was too bright: a
   white card on a near-white textured page has nothing holding it, so the
   card stopped reading as a card and the texture competed with the only
   thing on screen anyone came for. Inverting it fixes both at once — the
   card is now the brightest object in the frame by a wide margin, and the
   motifs recede to the edges of vision where texture belongs.

   FOUR LAYERS, TOP TO BOTTOM (CSS paints the first listed on top):

     1. THE SCRIM. A radial wash of deep counter blue, thinnest directly
        behind the card and heaviest at the corners. It is what makes this
        an overlay rather than a wallpaper: the motifs stay legible near
        the edges and fade to almost nothing behind the form, so nothing
        crosses the card's own contrast.
     2 & 3. THE SPICE MOTIFS — star anise, a curry-leaf sprig, a chilli, a
        cardamom pod, coriander seeds, a fenugreek leaf. Two passes at
        different scales and offsets; one 240px tile alone reads as a grid,
        two overlapping ones do not.
     4. A soft lift of primary-800 behind everything so the field is not
        one flat colour.

   ⚠ THE TEXTURE IS AN INLINE data: URI, NOT A FILE. wwwroot carries no
   image assets — the brand files come out of build-assets.py — so a lone
   decorative PNG here would be the first hand-placed image in the project
   and the one nobody regenerates. It also loads with the stylesheet: no
   second request, no flash of untextured background.

   ⚠ AND IT IS NOT FOOD PHOTOGRAPHY. Every restaurant on this console
   sells something different, and a photograph of somebody else's biryani
   on the screen where an owner signs in is a claim about their menu.
   Whole spices are the one culinary vocabulary that belongs to the
   cuisine rather than to a dish.
   ══════════════════════════════════════════════════════════════════════ */

.auth-shell {
    display: grid;
    place-items: center;
    min-height: 100vh;
    padding: var(--ds-space-6);

    /* ⚠ THE TEXTURE MOVED OUT OF background-image AND ONTO ::before, AND
       THE REASON IS filter. CSS cannot blur one background layer — filter
       applies to an element and everything in it, so blurring the shell
       would blur the sign-in card with it. A pseudo-element is the only way
       to get an out-of-focus backdrop behind sharp content, and out-of-focus
       is the whole point: a crisp repeating motif behind a form reads as
       wallpaper and pulls the eye into counting it, where the same motif at
       2px of blur reads as a surface and stays where it belongs. */
    position: relative;
    overflow: hidden;
    isolation: isolate;

    background-color: var(--ds-primary-900);
    background-image:
        radial-gradient(900px 560px at 50% -8%, var(--ds-primary-800) 0%, transparent 70%);
    background-repeat: no-repeat;
}

/* THE MOTIF LAYER — star anise, a curry-leaf sprig, a chilli, a cardamom
   pod, coriander seeds, a fenugreek leaf. Two passes at different scales
   and offsets: one 240px tile alone reads as a grid, two overlapping ones
   do not.

   ⚠ inset IS NEGATIVE ON PURPOSE. A blur samples beyond its own box, so a
   layer blurred at its exact edges fades to transparent along all four
   sides and draws a soft frame around the viewport. Oversizing it by more
   than the blur radius pushes that artefact off-screen. */
.auth-shell::before {
    content: "";
    position: absolute;
    inset: -80px;
    z-index: 0;

    /* ⚠ THE '#' IN THE STROKE COLOUR IS ENCODED EXACTLY ONCE, AS %23.
       This texture vanished twice before anyone could judge it, and both
       times the cause was %2523 — a colour pre-escaped by hand and then
       escaped again by the URL encoder, which leaves the literal string
       "%23F2F8FF" as a paint value. An invalid colour is not an error in
       SVG: the shape simply does not paint, the CSS stays valid, the
       browser says nothing, and the symptom is a background that looks
       like it was never added. If the motifs ever disappear again, search
       this rule for %2523 before touching opacity or blur. */
    background-image:
        url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22240%22%20height%3D%22240%22%20viewBox%3D%220%200%20240%20240%22%3E%20%3Cg%20fill%3D%22none%22%20stroke%3D%22%23F2F8FF%22%20stroke-width%3D%223.4%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%20%3Cg%20transform%3D%22translate%2846%2C44%29%22%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%280%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%2845%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%2890%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%28135%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%28180%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%28225%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%28270%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%28315%29%22%2F%3E%3Ccircle%20cx%3D%220%22%20cy%3D%220%22%20r%3D%222.6%22%2F%3E%3C%2Fg%3E%20%3Cg%20transform%3D%22translate%28150%2C38%29%20rotate%28-18%29%22%3E%3Cpath%20d%3D%22M0%200%20L34%200%22%2F%3E%3Cpath%20d%3D%22M6%200%20C10%20-9%2018%20-9%2020%200%20C18%209%2010%209%206%200Z%22%2F%3E%3Cpath%20d%3D%22M18%200%20C22%20-8%2029%20-8%2031%200%20C29%208%2022%208%2018%200Z%22%2F%3E%3C%2Fg%3E%20%3Cg%20transform%3D%22translate%28196%2C140%29%20rotate%2824%29%22%3E%3Cpath%20d%3D%22M0%200%20C10%202%2018%2012%2016%2026%20C15%2033%209%2036%205%2032%20C0%2027%20-2%2012%200%200Z%22%2F%3E%3Cpath%20d%3D%22M0%200%20L-6%20-7%20M0%200%20L2%20-8%22%2F%3E%3C%2Fg%3E%20%3Cg%20transform%3D%22translate%2858%2C168%29%20rotate%28-30%29%22%3E%3Cpath%20d%3D%22M0%200%20C7%203%207%2019%200%2024%20C-7%2019%20-7%203%200%200Z%22%2F%3E%3Cpath%20d%3D%22M0%202%20L0%2022%22%2F%3E%3C%2Fg%3E%20%3Cg%3E%3Ccircle%20cx%3D%22118%22%20cy%3D%22112%22%20r%3D%223.4%22%2F%3E%3Ccircle%20cx%3D%22130%22%20cy%3D%22122%22%20r%3D%222.4%22%2F%3E%3Ccircle%20cx%3D%22108%22%20cy%3D%22124%22%20r%3D%222%22%2F%3E%3Ccircle%20cx%3D%2224%22%20cy%3D%22112%22%20r%3D%222.2%22%2F%3E%3Ccircle%20cx%3D%22214%22%20cy%3D%2266%22%20r%3D%222.4%22%2F%3E%3Ccircle%20cx%3D%22140%22%20cy%3D%22206%22%20r%3D%223%22%2F%3E%3Ccircle%20cx%3D%22152%22%20cy%3D%22196%22%20r%3D%222%22%2F%3E%3C%2Fg%3E%20%3Cg%20transform%3D%22translate%28200%2C206%29%20rotate%2812%29%22%3E%3Cpath%20d%3D%22M0%200%20C9%20-4%2018%202%2016%2012%20C7%2015%20-1%209%200%200Z%22%2F%3E%3Cpath%20d%3D%22M0%200%20L16%2012%22%2F%3E%3C%2Fg%3E%20%3C%2Fg%3E%3C%2Fsvg%3E"),
        url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22240%22%20height%3D%22240%22%20viewBox%3D%220%200%20240%20240%22%3E%20%3Cg%20fill%3D%22none%22%20stroke%3D%22%23F2F8FF%22%20stroke-width%3D%223.4%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%20%3Cg%20transform%3D%22translate%2846%2C44%29%22%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%280%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%2845%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%2890%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%28135%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%28180%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%28225%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%28270%29%22%2F%3E%3Cpath%20d%3D%22M0%20-15%20C4%20-7%204%20-5%200%200%20C-4%20-5%20-4%20-7%200%20-15Z%22%20transform%3D%22rotate%28315%29%22%2F%3E%3Ccircle%20cx%3D%220%22%20cy%3D%220%22%20r%3D%222.6%22%2F%3E%3C%2Fg%3E%20%3Cg%20transform%3D%22translate%28150%2C38%29%20rotate%28-18%29%22%3E%3Cpath%20d%3D%22M0%200%20L34%200%22%2F%3E%3Cpath%20d%3D%22M6%200%20C10%20-9%2018%20-9%2020%200%20C18%209%2010%209%206%200Z%22%2F%3E%3Cpath%20d%3D%22M18%200%20C22%20-8%2029%20-8%2031%200%20C29%208%2022%208%2018%200Z%22%2F%3E%3C%2Fg%3E%20%3Cg%20transform%3D%22translate%28196%2C140%29%20rotate%2824%29%22%3E%3Cpath%20d%3D%22M0%200%20C10%202%2018%2012%2016%2026%20C15%2033%209%2036%205%2032%20C0%2027%20-2%2012%200%200Z%22%2F%3E%3Cpath%20d%3D%22M0%200%20L-6%20-7%20M0%200%20L2%20-8%22%2F%3E%3C%2Fg%3E%20%3Cg%20transform%3D%22translate%2858%2C168%29%20rotate%28-30%29%22%3E%3Cpath%20d%3D%22M0%200%20C7%203%207%2019%200%2024%20C-7%2019%20-7%203%200%200Z%22%2F%3E%3Cpath%20d%3D%22M0%202%20L0%2022%22%2F%3E%3C%2Fg%3E%20%3Cg%3E%3Ccircle%20cx%3D%22118%22%20cy%3D%22112%22%20r%3D%223.4%22%2F%3E%3Ccircle%20cx%3D%22130%22%20cy%3D%22122%22%20r%3D%222.4%22%2F%3E%3Ccircle%20cx%3D%22108%22%20cy%3D%22124%22%20r%3D%222%22%2F%3E%3Ccircle%20cx%3D%2224%22%20cy%3D%22112%22%20r%3D%222.2%22%2F%3E%3Ccircle%20cx%3D%22214%22%20cy%3D%2266%22%20r%3D%222.4%22%2F%3E%3Ccircle%20cx%3D%22140%22%20cy%3D%22206%22%20r%3D%223%22%2F%3E%3Ccircle%20cx%3D%22152%22%20cy%3D%22196%22%20r%3D%222%22%2F%3E%3C%2Fg%3E%20%3Cg%20transform%3D%22translate%28200%2C206%29%20rotate%2812%29%22%3E%3Cpath%20d%3D%22M0%200%20C9%20-4%2018%202%2016%2012%20C7%2015%20-1%209%200%200Z%22%2F%3E%3Cpath%20d%3D%22M0%200%20L16%2012%22%2F%3E%3C%2Fg%3E%20%3C%2Fg%3E%3C%2Fsvg%3E");
    /* ⚠ THE TILE IS DRAWN AT 240 AND PAINTED AT 420/640, WHICH IS WHERE THE
       WEIGHT COMES FROM. An SVG background scales its strokes with the box,
       so a 3.4px stroke lands at ~6px and ~9px on screen. Two earlier
       attempts kept the tile at its natural size and lost the motifs
       entirely: a 2px stroke under a 2px blur has almost no peak opacity
       left, and whatever survived was then taken by the scrim. Blur eats
       thin lines; the answer is thicker lines, not less blur. */
    background-size: 360px 360px, 560px 560px;
    background-position: 0 0, 150px 210px;
    background-repeat: repeat, repeat;

    filter: blur(3.5px);
    opacity: 0.13;
}

/* THE SCRIM — deep counter blue, thinnest directly behind the card and
   heaviest at the corners.

   ⚠ IT IS FAR LIGHTER THAN THE FIRST TWO ATTEMPTS, WHICH ERASED WHAT THEY
   WERE MEANT TO SOFTEN. The first ran 0.62 at the centre to 0.94 at the
   corners; the second 0.10 to 0.74. Both times the answer was "no
   texture", and both times it was right. Softening is the blur's job. The
   scrim only has to keep the corners from competing with the card. */
.auth-shell::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background: radial-gradient(1100px 760px at 50% 46%,
        rgba(8, 42, 71, 0.18) 0%,
        rgba(8, 42, 71, 0.34) 52%,
        rgba(5, 26, 46, 0.66) 100%);
}

/* Everything the layout actually renders sits above both layers. */
.auth-shell > * {
    position: relative;
    z-index: 1;
}

/* ⚠ THE CARD GETS A DEEPER SHADOW ON THE DARK FIELD, NOT THE SAME ONE.
   elev-2 is tuned for a card lying on a pale canvas, where the shadow's job
   is to lift it a few millimetres. Here the card is a lit object in a dark
   room and needs the throw to match, or it looks pasted on. */
.auth-shell .auth-card {
    border-color: rgba(255, 255, 255, 0.10);
    box-shadow: 0 24px 60px rgba(3, 18, 33, 0.55), 0 2px 8px rgba(3, 18, 33, 0.35);
}

/* Anything the layout puts OUTSIDE the card — a version string, a support
   line — would otherwise be ink-on-navy. */
.auth-shell > :not(.auth-card) { color: var(--ds-primary-100); }

/* Decoration, and nothing else, so it goes when somebody has asked the
   operating system for less of everything. The field stays dark: the
   contrast argument for the card holds either way. */
@media (prefers-contrast: more) {
    .auth-shell::before { display: none; }
    .auth-shell::after  { background: rgba(8, 42, 71, 0.55); }
}

.auth-card {
    width: 100%;
    max-width: 400px;
    background: var(--ds-surface);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-lg);
    padding: var(--ds-space-8);
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-4);
    box-shadow: var(--ds-elev-2);
}

.auth-card__title { font: var(--ds-title-lg); margin: 0; }
.auth-card__subtitle { font: var(--ds-body); color: var(--ds-text-secondary); margin: 0; }

/* ⚠ THE STACKED LOCKUP, WHICH IS WHAT DesiMenus_Visual_Assets §1.2 NAMES
   FOR THIS EXACT SCREEN (desimenus-lockup-stacked.svg, "AUTH-02 login
   card"). The card carried the words "Sign in to DesiMenus" and no mark at
   all, which is the one screen where the product has to identify itself:
   it is the first thing a new restaurant sees and the only thing a person
   at a shared machine has to check before typing a password into it. */
.auth-card__lockup {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--ds-space-2);
    margin-bottom: var(--ds-space-2);
}

.auth-card__lockup .brand-mark { color: var(--ds-brand); }

.auth-card__wordmark {
    font-family: "IBM Plex Serif", Georgia, serif;
    font-weight: 600;
    font-size: 22px;
    line-height: 1;
    color: var(--ds-text);
}

.auth-card__wordmark em { font-style: normal; color: var(--ds-action); }

/* ⚠ THE HEADING TAKES FOCUS ON EVERY NAVIGATION AND MUST NOT LOOK LIKE AN
   INPUT. Routes.razor's <FocusOnNavigate Selector="h1" /> gives the h1
   tabindex="-1" and focuses it so a screen reader announces the new page —
   and Chrome then matches :focus-visible on it, painting the global 2px
   ring around the title. On the sign-in card that ring sits directly above
   two real text fields and reads as a third one, which is why it is worth
   suppressing rather than living with. Removing it costs no affordance: a
   heading is not operable, and the focus here exists to be ANNOUNCED, not
   to be seen. */
h1:focus-visible,
h2:focus-visible,
h3:focus-visible { outline: none; }

/* --------------------------------------------------------------------------
   Utilities — kept to a deliberate minimum so nobody starts building a
   utility framework in here.
   -------------------------------------------------------------------------- */

.stack   { display: flex; flex-direction: column; gap: var(--ds-space-4); }
.stack-2 { display: flex; flex-direction: column; gap: var(--ds-space-2); }
.row     { display: flex; align-items: center; gap: var(--ds-space-3); }
.row-end { display: flex; align-items: center; gap: var(--ds-space-3); justify-content: flex-end; }
.grow    { flex: 1 1 auto; min-width: 0; }
.muted   { color: var(--ds-text-secondary); }
.caption { font: var(--ds-caption); color: var(--ds-text-tertiary); }

/* --------------------------------------------------------------------------
   Pass 41 — SIDE-BY-SIDE FORM FIELDS.

   ⚠ .row IS THE WRONG CONTAINER FOR TWO FIELDS AND ALWAYS WAS. It is
   `align-items: center`, so two .field blocks of unequal height are CENTRED
   against each other — and the moment one field's label wraps to a second line
   or carries a longer caption than its neighbour, their inputs stop lining up.
   The form looks ragged for a reason nothing in the markup hints at, because
   the markup is symmetrical and the rendered heights are not.

   It survived this long because every earlier two-up form had short, equal
   labels: MembershipPlans pairs "Discount %" with "Monthly fee", both one line,
   so the centring had nothing to expose. The wallet's pack dialog paired
   "Deposit — what the diner pays" with "Bonus — what you add on top", the first
   wrapped, and the two number inputs came out at visibly different heights.

   ⚠ AND .grow MAKES THE COLUMNS UNEQUAL, WHICH IS THE SECOND HALF OF THE SAME
   BUG. `flex: 1 1 auto` takes its flex BASIS from the content, so the field
   whose help text is a longer sentence claims more of the row: one input ends up
   half the width of the other for reasons the author never wrote down.

   So this is a grid with explicitly equal tracks and top alignment. Both
   problems are structural rather than a matter of tidying the copy — a label is
   allowed to wrap, and captions are allowed to differ in length.

   ⚠ A NEW CLASS RATHER THAN A FIX TO .row, DELIBERATELY. .row is used in over a
   hundred places for toolbars, chip strips and button groups, where centring is
   exactly right. Changing it to suit forms would re-align all of them.
   -------------------------------------------------------------------------- */

.form-row {
    display: grid;
    grid-template-columns: repeat(var(--form-row-cols, 2), minmax(0, 1fr));
    gap: var(--ds-space-4);
    align-items: start;
}

/* One field per line below the tablet breakpoint: two number inputs side by side
   in 340 px is worse than two stacked. Matches the design system's single-column
   fallback for forms. */
@media (max-width: 640px) {
    .form-row { grid-template-columns: minmax(0, 1fr); }
}

/* Help text under a field. Sized like .caption but bound to the field's own
   gap so it reads as part of the control rather than as loose prose. */
.field__help {
    font: var(--ds-caption);
    color: var(--ds-text-tertiary);
    margin: 0;
}

/* --------------------------------------------------------------------------
   Modal
   -------------------------------------------------------------------------- */

.modal-scrim {
    position: fixed;
    inset: 0;
    z-index: 40;
    background: var(--ds-scrim);
    display: grid;
    place-items: center;
    padding: var(--ds-space-6);
}

.modal {
    background: var(--ds-surface);
    border-radius: var(--ds-radius-lg);
    box-shadow: var(--ds-elev-2);
    width: 100%;
    max-width: 520px;
    max-height: 85vh;
    display: flex;
    flex-direction: column;
}

.modal__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--ds-space-5) var(--ds-space-6);
    border-bottom: 1px solid var(--ds-border);
}

.modal__title { font: var(--ds-title); margin: 0; }
.modal__close { min-width: auto; padding: 0 var(--ds-space-2); }

.modal__body {
    padding: var(--ds-space-6);
    overflow-y: auto;
}

.modal__footer {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    padding: var(--ds-space-4) var(--ds-space-6);
    border-top: 1px solid var(--ds-border);
}

/* --------------------------------------------------------------------------
   Toasts — top-right, 5 s on desktop (§1.2)
   -------------------------------------------------------------------------- */

.toast-host {
    position: fixed;
    top: var(--ds-space-4);
    right: var(--ds-space-4);
    z-index: 60;
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-2);
    max-width: 420px;
}

.toast {
    display: flex;
    align-items: flex-start;
    gap: var(--ds-space-3);
    padding: var(--ds-space-3) var(--ds-space-4);
    border-radius: var(--ds-radius-sm);
    background: var(--ds-surface);
    border: 1px solid var(--toast-hue, var(--ds-border-strong));
    border-left: 4px solid var(--toast-hue, var(--ds-border-strong));
    box-shadow: var(--ds-elev-2);
    font: var(--ds-body);
    color: var(--ds-text);
}

.toast-success { --toast-hue: var(--ds-state-settled); }
.toast-error   { --toast-hue: var(--ds-state-failed); }
.toast-info    { --toast-hue: var(--ds-action); }

.toast__dismiss {
    border: none;
    background: none;
    cursor: pointer;
    color: var(--ds-text-tertiary);
    font-size: 14px;
    line-height: 1;
    padding: 0;
}

/* --------------------------------------------------------------------------
   Veg / non-veg markers

   ⚠ NOT decorative. The green square and brown triangle are the standard
   Indian food classification glyphs and are expected on any menu surface.
   Rendered as a SHAPE plus a colour, never a colour swatch alone — the same
   reasoning as the rail: a colour-blind reader and a monochrome print must
   both still be able to tell them apart.
   -------------------------------------------------------------------------- */

.veg-marker {
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 1.5px solid;
    position: relative;
}

.veg-marker.veg { border-color: var(--ds-pudina-600); }

.veg-marker.veg::after {
    content: "";
    position: absolute;
    inset: 2.5px;
    border-radius: 50%;
    background: var(--ds-pudina-600);
}

.veg-marker.nonveg { border-color: var(--ds-tandoor-600); }

.veg-marker.nonveg::after {
    content: "";
    position: absolute;
    left: 2px; right: 2px; bottom: 2.5px;
    border-left: 4.5px solid transparent;
    border-right: 4.5px solid transparent;
    border-bottom: 8px solid var(--ds-tandoor-600);
}

/* Screen-reader-only text. Used for live-region announcements that would be
   visual noise on screen but are the only signal a screen reader gets. */
.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.clickable { cursor: pointer; }

/* ==========================================================================
   PRINT

   The reliable export path. Every browser's print dialog offers "Save as
   PDF", so a good print stylesheet IS a PDF exporter with no dependency, no
   licence and no server round trip.

   Design system §13.4 governs the approach even though that section is
   written for thermal paper: PATTERN OVER HUE, and no mid-tones. An office
   laser printer renders a tinted background as grey mush and a coloured rail
   as an indistinct stripe, so print drops both and leans on rules, weight and
   monospaced figures instead.
   ========================================================================== */

@media print {

    /* Force light regardless of the screen theme. A dark-theme report prints
       as a solid black page, which wastes a toner cartridge and is unreadable. */
    :root {
        --ds-canvas: #FFFFFF;
        --ds-surface: #FFFFFF;
        --ds-surface-sunken: #FFFFFF;
        --ds-text: #000000;
        --ds-text-secondary: #333333;
        --ds-text-tertiary: #555555;
        --ds-border: #CCCCCC;
        --ds-border-strong: #000000;
    }

    @page {
        size: A4;
        margin: 14mm;
    }

    body { background: #FFFFFF; }

    /* Everything that is not the report itself disappears: navigation,
       filters, buttons, toasts. */
    .no-print,
    .rail-nav,
    .toast-host,
    .page-header__actions,
    .report__filters,
    .btn { display: none !important; }

    /* Print-only furniture appears. */
    .print-only { display: block !important; }

    .app-shell { display: block; }

    .work-surface {
        padding: 0;
        overflow: visible;
        gap: 0;
    }

    /* Cards lose their chrome — a printed page does not need a border around
       every block, and the ink is better spent on the figures. */
    .card {
        border: none;
        border-radius: 0;
        padding: 0;
        margin-bottom: 10mm;

        /* Keep a section and its table on the same sheet where possible. A
           heading orphaned at the foot of a page is the classic printed-report
           failure. */
        break-inside: avoid;
    }

    .card__title {
        font-size: 12pt;
        border-bottom: 1px solid #000;
        padding-bottom: 2mm;
        margin-bottom: 3mm;
    }

    /* Tables at print density. Header repeats on every page — a five-page
       table whose columns are unlabelled after the first sheet is unusable. */
    .data-table { font-size: 9pt; }
    .data-table thead { display: table-header-group; }
    .data-table tfoot { display: table-footer-group; }
    .data-table tr { break-inside: avoid; }
    .data-table th { border-bottom: 1pt solid #000; }
    .data-table td { border-bottom: 0.5pt solid #CCC; }

    /* Metric cards become a plain row of figures. */
    .metric-grid {
        display: flex;
        gap: 6mm;
        break-inside: avoid;
    }

    .metric__value { font-size: 14pt; }

    /* Banners keep their meaning but lose their fill — the border and the
       weight carry it instead. This is the §13.4 rule applied to A4. */
    .banner {
        border: 1pt solid #000;
        background: none !important;
        color: #000 !important;
        break-inside: avoid;
    }

    .banner-error { border-width: 2pt; }

    /* The quadrant chart prints. It is the one graphic worth the ink, because
       the four-way split is the whole point of the report and a table of
       classifications does not convey it. */
    .quadrant {
        max-width: 160mm;
        break-inside: avoid;
    }

    /* Rails become leading glyphs, per §13.4's translation table:
       solid █, dashed ┋, double ║, hollow ░. */
    .rail-card {
        border: none;
        border-bottom: 0.5pt solid #CCC;
        padding-left: 0;
        break-inside: avoid;
    }

    .rail-card::before { display: none; }

    .rail-solid  .rail-card__title::before { content: "\2588  "; }
    .rail-dashed .rail-card__title::before { content: "\250B  "; }
    .rail-double .rail-card__title::before { content: "\2551  "; }
    .rail-hollow .rail-card__title::before { content: "\2591  "; }

    /* Chips lose their fill and keep their outline. */
    .chip {
        border: 0.5pt solid #000;
        background: none !important;
        color: #000 !important;
    }

    .bar { display: none; }

    a { text-decoration: none; color: #000; }
}

/* Print-only elements are hidden on screen. */
.print-only { display: none; }

.report__print-header {
    border-bottom: 1pt solid #000;
    padding-bottom: 3mm;
    margin-bottom: 5mm;
}

.report__print-brand { font-size: 11pt; }
.report__print-title { font-size: 16pt; font-weight: 600; margin-top: 1mm; }
.report__print-meta { font-size: 9pt; color: #333; margin-top: 1mm; }

.report__print-footer {
    border-top: 0.5pt solid #999;
    padding-top: 2mm;
    margin-top: 8mm;
    font-size: 8pt;
    color: #333;
}

/* ==========================================================================
   Report-specific screen styles
   ========================================================================== */

.report { display: flex; flex-direction: column; gap: var(--ds-space-5); }
.report__body { display: flex; flex-direction: column; gap: var(--ds-space-5); }

.quadrant {
    width: 100%;
    height: auto;
    max-height: 420px;
    margin-top: var(--ds-space-4);
}

.quadrant__label {
    font: var(--ds-overline);
    fill: var(--ds-text-tertiary);
    letter-spacing: 0.04em;
}

.quadrant__axis {
    font: var(--ds-caption);
    fill: var(--ds-text-secondary);
}

.quadrant__point { cursor: default; }
.quadrant__point:hover circle { r: 8; }

.quadrant__swatch {
    display: inline-block;
    width: 12px; height: 12px;
    border-radius: 50%;
}

/* Inline distribution bar. One div, and it makes a share readable at a glance
   in a way a column of percentages does not. */
.bar {
    height: 8px;
    background: var(--ds-surface-sunken);
    border-radius: var(--ds-radius-pill);
    overflow: hidden;
}

.bar__fill {
    height: 100%;
    background: var(--ds-action);
    border-radius: var(--ds-radius-pill);
}

/* --------------------------------------------------------------------------
   Availability board (86 list)

   Tiles, not table rows. This screen is used mid-service, often one-handed,
   and §8's 48 px hit-target floor is a floor rather than a target — so the
   WHOLE tile is the control.
   -------------------------------------------------------------------------- */

.availability-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--ds-space-3);
}

.availability {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-2);
    min-height: 88px;
    padding: var(--ds-space-4);
    border-radius: var(--ds-radius-md);
    border: 1px solid var(--ds-border);
    background: var(--ds-surface);
    text-align: left;
    cursor: pointer;
    font: var(--ds-body);
    color: var(--ds-text);
    transition: background var(--ds-motion-fast) var(--ds-ease-standard);
}

.availability:disabled { cursor: wait; opacity: 0.6; }

.availability__name { font: var(--ds-title); }

.availability__state {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    font: var(--ds-label);
}

.availability__price {
    font: var(--ds-numeric-sm);
    color: var(--ds-text-secondary);
}

.availability--on {
    border-left: 4px solid var(--ds-state-served);
}

.availability--on .availability__state { color: var(--ds-state-served); }

/* An 86'd item is unmistakable: the hollow-rail treatment from §5 applied to
   a tile — struck-through name, tinted surface, and the leading edge left
   open rather than filled. Someone scanning this board should see what is off
   without reading a single word. */
.availability--off {
    background: var(--ds-state-failed-fill);
    border-left: 4px double var(--ds-state-failed);
}

.availability--off .availability__name {
    text-decoration: line-through;
    color: var(--ds-text-tertiary);
}

.availability--off .availability__state { color: var(--ds-state-failed); }

/* Editable chips — a modifier option is a control, not a label. */
.chip--editable {
    cursor: pointer;
    border-color: var(--ds-border-strong);
}

.chip--editable:hover {
    background: var(--ds-action-subtle);
    border-color: var(--ds-action);
    color: var(--ds-action);
}

.chip--add {
    cursor: pointer;
    border-style: dashed;
    color: var(--ds-action);
    background: transparent;
}

.chip--add:hover { background: var(--ds-action-subtle); }

/* An invalid table row — used by the recipe editor for a unit mismatch.
   Tinted rather than outlined, because the row already contains the controls
   that need to keep their own focus treatment. */
.row--invalid > td {
    background: var(--ds-state-failed-fill);
}

/* ==========================================================================
   Pass 7 — right-side detail panel (§1.1)

   420 px, matching the Flutter desktop convention (Design System §13.1).
   The whole point of this component: a list never loses its filter state
   when a row is opened, because opening a row never navigates away.
   ========================================================================== */

.detail-panel-scrim {
    position: fixed;
    inset: 0;
    background: var(--ds-scrim);
    z-index: 40;
    animation: ds-fade-in var(--ds-motion-base) var(--ds-ease-standard);
}

.detail-panel {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: 420px;
    max-width: 100vw;
    background: var(--ds-surface);
    border-left: 1px solid var(--ds-border);
    box-shadow: -8px 0 24px rgba(11, 18, 25, 0.14);
    z-index: 41;
    display: flex;
    flex-direction: column;
    animation: ds-slide-in-right var(--ds-motion-base) var(--ds-ease-standard);
}

@keyframes ds-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes ds-slide-in-right { from { transform: translateX(24px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
    .detail-panel-scrim, .detail-panel { animation: none; }
}

.detail-panel__header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--ds-space-3);
    padding: var(--ds-space-5);
    border-bottom: 1px solid var(--ds-border);
    flex-shrink: 0;
}

.detail-panel__title { font: var(--ds-title-lg); margin: 0; }
.detail-panel__subtitle { font: var(--ds-body); color: var(--ds-text-secondary); margin-top: var(--ds-space-1); }
.detail-panel__close { min-width: auto; padding: 0 var(--ds-space-2); flex-shrink: 0; }

.detail-panel__body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: var(--ds-space-5);
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-5);
}

.detail-panel__footer {
    flex-shrink: 0;
    display: flex;
    justify-content: flex-end;
    gap: var(--ds-space-2);
    padding: var(--ds-space-4) var(--ds-space-5);
    border-top: 1px solid var(--ds-border);
}

/* ==========================================================================
   Pass 7 — session security (AVAL-001 / AVAL-002)

   A LOCK, not a logout — the overlay sits above the current screen rather
   than navigating to /login, so an in-progress report export or a
   half-written form is still there the moment the person re-authenticates.
   ========================================================================== */

.idle-lock-scrim {
    position: fixed;
    inset: 0;
    background: var(--ds-scrim);
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
}

.idle-lock-card {
    width: 360px;
    max-width: calc(100vw - var(--ds-space-8));
    background: var(--ds-surface);
    border-radius: var(--ds-radius-lg);
    padding: var(--ds-space-6);
    box-shadow: 0 16px 48px rgba(11, 18, 25, 0.28);
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-4);
}

.idle-lock-card__icon {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: var(--ds-action-subtle);
    color: var(--ds-action);
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ==========================================================================
   Pass 7 — the shared report chart (SVG, no charting library)

   ONE approach, chosen once here, so every report reads as the same system
   rather than each report screen picking its own visualization idiom. Plain
   inline SVG rather than a JS charting library: no extra network request,
   currentColor / CSS variables work for theming for free, and a report is
   read, not manipulated — nobody needs to pan or zoom a sales trend line.
   ========================================================================== */

.chart {
    width: 100%;
    height: 220px;
    overflow: visible;
}

.chart__gridline { stroke: var(--ds-border); stroke-width: 1; }
.chart__axis-label { font: var(--ds-caption); fill: var(--ds-text-tertiary); }
.chart__line { fill: none; stroke: var(--ds-action); stroke-width: 2; }
.chart__area { fill: var(--ds-action-subtle); opacity: 0.6; }
.chart__point { fill: var(--ds-action); }
.chart__point:hover { r: 5; }
.chart__bar { fill: var(--ds-action); }
.chart__bar:hover { fill: var(--ds-action-hover); }
.chart__bar-label { font: var(--ds-numeric-sm); fill: var(--ds-text-secondary); }
.chart__empty { fill: var(--ds-text-tertiary); font: var(--ds-body); }

/* A distinct hue per series, drawn from the SAME palette every rail/chip
   already uses (§5) rather than an invented chart palette — so a "cancelled"
   segment on a chart is the same hue as a cancelled chip anywhere else. */
.chart__series-0 { stroke: var(--ds-action); fill: var(--ds-action); }
.chart__series-1 { stroke: var(--ds-jamun-500); fill: var(--ds-jamun-500); }
.chart__series-2 { stroke: var(--ds-haldi-500); fill: var(--ds-haldi-500); }
.chart__series-3 { stroke: var(--ds-mirchi-400); fill: var(--ds-mirchi-400); }
.chart__series-4 { stroke: var(--ds-pudina-500); fill: var(--ds-pudina-500); }

.chart-legend { display: flex; flex-wrap: wrap; gap: var(--ds-space-4); margin-top: var(--ds-space-2); }
.chart-legend__item { display: flex; align-items: center; gap: 6px; font: var(--ds-caption); color: var(--ds-text-secondary); }
.chart-legend__swatch { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }

/* As-of label — §6: every report states its generation time. */
.report-asof {
    display: flex;
    align-items: center;
    gap: 6px;
    font: var(--ds-caption);
    color: var(--ds-text-tertiary);
}

/* ==========================================================================
   Pass 7 — "My exports" tray (§1.8)
   ========================================================================== */

.export-item {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    padding: var(--ds-space-2) var(--ds-space-3);
    border-radius: var(--ds-radius-sm);
}

.export-item__icon { flex-shrink: 0; color: var(--ds-text-tertiary); }
.export-item__body { min-width: 0; flex: 1 1 auto; }
.export-item__title { font: var(--ds-label); color: var(--ds-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.export-item__meta { font: var(--ds-caption); color: var(--ds-text-tertiary); margin-top: 2px; }
.export-item__action { flex-shrink: 0; }


/* =====================================================================
   PLATFORM CONSOLE — DARK RAIL + COLLAPSE
   ---------------------------------------------------------------------
   Appended rather than edited into the rules above so the tenant console
   is untouched: every selector here is scoped to
   [data-console="saas"], which cannot match the Web Admin shell.

   Two problems being solved at once:

     * The console read flat. Everything sat on white at the same
       elevation, so nothing announced itself as navigation, content or
       action. A dark rail gives the eye an anchor and the page a
       structure, without adding decoration to the content area — which
       stays quiet, because that is where the numbers live.

     * §13.2 wants the two consoles tellable apart. The accent moving from
       jamun to neel keeps that, and the rail makes it unmissable.
   ===================================================================== */

/* ⚠ THESE RULES ARE NO LONGER SCOPED TO THE SaaS CONSOLE.
   They were .rail-nav back when only that console
   had a dark rail. The warm-theme pass gave the tenant console one too,
   and the two differ ONLY in the --ds-rail-* token values — counter blue
   here, slate-indigo there — so one set of rules now serves both.

   ⚠ §13.2 NOW RESTS ON THE LOCKUP AND THE ACCENT, NOT ON THE RAIL. Both
   rails are dark blues — counter blue here, slate-indigo there — because a
   product does not get a second brand colour so that its own staff can
   navigate. What tells them apart is the wordmark's second line ("Web
   Admin" against "SaaS Admin"), which every rail carries at all times, and
   the accent azure-against-neel that each console repeats on every button
   and every focus ring on the screen. */
.rail-nav {
    background: var(--ds-rail-bg);
    border-right: 1px solid var(--ds-rail-border);
    color: var(--ds-rail-fg);
    transition: width 160ms ease;
}

.rail-nav__brand-name { color: #FFFFFF; }
.rail-nav__console-badge {
    color: var(--ds-rail-marker);
    letter-spacing: 0.08em;
}

.rail-nav__link {
    color: var(--ds-rail-fg-muted);
    position: relative;
    /* The 3px inset keeps the active marker from touching the panel edge,
       which reads as a rendering fault rather than an indicator. */
    margin: 0 3px;
}

.rail-nav__link:hover {
    background: var(--ds-rail-hover);
    color: #FFFFFF;
}

.rail-nav__link.active {
    background: var(--ds-rail-active);
    color: #FFFFFF;
    font-weight: 500;
}

/* Section headers ("MENU", "INVENTORY") were slate-tertiary — a light grey
   chosen for a white rail, and near-invisible on a dark one. They take the
   rail's own muted foreground, held back with opacity rather than a fourth
   colour so the rail keeps to three. */
.rail-nav__section {
    color: var(--ds-rail-fg-muted);
    opacity: 0.7;
}

/* A left marker as well as a fill: the fill alone is a colour-only signal,
   and this console is used on cheap external monitors where a 20% tint can
   vanish. Same reasoning as StatusChip always carrying an icon. */
.rail-nav__link.active::before {
    content: "";
    position: absolute;
    left: -3px;
    top: 6px;
    bottom: 6px;
    width: 3px;
    border-radius: 0 2px 2px 0;
    background: var(--ds-rail-marker);
}

/* --- Collapse -------------------------------------------------------
   Driven by data-rail on the shell so one attribute moves the whole
   layout. Labels are hidden with font-size rather than display:none:
   the link text is a bare text node with no element to target, and
   collapsing the type is the one technique that does not require
   rewriting every NavLink in the file. The icon opts back in. */

[data-rail="collapsed"] .rail-nav {
    width: 68px;
    padding-left: var(--ds-space-2);
    padding-right: var(--ds-space-2);
}

[data-rail="collapsed"] .rail-nav__link {
    font-size: 0;
    justify-content: center;
    gap: 0;
    padding: 0;
}

[data-rail="collapsed"] .rail-nav__link .icon,
[data-rail="collapsed"] .rail-nav__link svg {
    font-size: 1rem;
    flex: 0 0 auto;
}

[data-rail="collapsed"] .rail-nav__brand-name,
[data-rail="collapsed"] .rail-nav__console-badge {
    display: none;
}

/* ⚠ SECTION HEADINGS GO TOO, AND THEY NEED THEIR OWN RULE. The link labels
   collapse via font-size: 0, which works because a link has an icon to keep
   the row a row. A section heading is text and nothing else, so the same
   trick would leave an invisible 0px element still contributing its
   padding — four of them, adding a stack of unexplained gaps down a 68px
   rail. It is replaced by a hairline instead, so the grouping the headings
   carried survives the collapse rather than being lost with the words. */
[data-rail="collapsed"] .rail-nav__section {
    font-size: 0;
    padding: var(--ds-space-3) 0 var(--ds-space-2);
    margin: 0 var(--ds-space-2);
    border-bottom: 1px solid var(--ds-rail-border);
}

.rail-nav__collapse {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-2);
    /* No margin-top:auto. It dates from the toggle sitting at the foot of
       the rail; inside the brand row it would drop the button to the
       bottom of a row it is supposed to be centred in. */
    height: 36px;
    padding: 0 var(--ds-space-2);
    flex: 0 0 auto;
    border: 0;
    border-radius: var(--ds-radius-sm);
    background: transparent;
    color: var(--ds-rail-fg-muted);
    font: var(--ds-caption);
    cursor: pointer;
}

.rail-nav__collapse:hover { background: var(--ds-rail-hover); color: #FFFFFF; }
.rail-nav__collapse:focus-visible { outline: 2px solid var(--ds-rail-marker); outline-offset: 2px; }

[data-rail="collapsed"] .rail-nav__collapse span { display: none; }

/* --- Lifting the content area out of flatness ------------------------
   Restrained on purpose. The rail is where the boldness was spent; the
   work surface stays quiet so the figures on it stay the loudest thing
   on screen. */

[data-console="saas"] .work-surface { background: var(--ds-neel-50); }

[data-console="saas"] .card {
    border: 1px solid var(--ds-border);
    box-shadow: 0 1px 2px rgba(9, 24, 48, 0.04);
}

/* Stat cards get a hairline of accent along the top edge — enough to
   separate "a number that matters" from "a panel", without a coloured
   background that would fight the numeral for attention. */
[data-console="saas"] .metric-grid > .card {
    border-top: 2px solid var(--ds-neel-400);
}

/* The figure itself is the reason this screen exists, so it gets the ink. */
[data-console="saas"] .metric__value { color: var(--ds-neel-900); }
[data-console="saas"] .metric__label {
    color: var(--ds-text-secondary);
    letter-spacing: 0.04em;
}

/* ==========================================================================
   THE TENANT CONSOLE'S TREATMENT

   The SaaS block above is the template; this is its counter-blue twin. The
   same restraint applies — the rail is where the boldness is spent, and the
   work surface stays quiet so the figures stay the loudest thing on it.
   ========================================================================== */

.work-surface { background: var(--ds-canvas); }

.card { box-shadow: var(--ds-elev-1); }

/* -- The metric accent ------------------------------------------------
   Each tile in a metric grid takes a short rule along its top edge, and
   the grid rotates through four hues so a row of figures reads as a row
   of distinct things rather than one long panel.

   ⚠ THE ROTATION IS POSITIONAL, WHICH IS BOTH THE TRICK AND THE LIMIT.
   nth-child means a given tile keeps its colour on every visit — "net
   sales is the green one" becomes true and stays true — with no markup
   change anywhere and nothing for a page author to remember. It also
   means the colour follows the POSITION rather than the figure, so it
   must never be allowed to carry meaning. That is why --ds-metric-a..d
   are decorative by definition and stated as such at the token.

   ⚠ AND A TILE THAT HAS SOMETHING TO SAY OVERRIDES IT. Anything carrying
   data-state wears its state colour instead; two colour systems on one
   tile is how a reader learns to trust neither. */
.metric-grid > * {
    position: relative;
    padding-top: var(--ds-space-3);
}

.metric-grid > *::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 28px;
    height: 3px;
    border-radius: 0 0 2px 0;
    background: var(--ds-metric-accent, var(--ds-metric-a));
}

.metric-grid > *:nth-child(4n+1) { --ds-metric-accent: var(--ds-metric-a); }
.metric-grid > *:nth-child(4n+2) { --ds-metric-accent: var(--ds-metric-b); }
.metric-grid > *:nth-child(4n+3) { --ds-metric-accent: var(--ds-metric-c); }
.metric-grid > *:nth-child(4n+4) { --ds-metric-accent: var(--ds-metric-d); }

.metric-grid > *[data-state]::before { background: currentColor; }

/* -- Card titles ------------------------------------------------------
   A short warm rule under the title. Short rather than full-width: a rule
   spanning the card reads as a divider and cuts the title off from its own
   content, where a 32px stub reads as an underline belonging to the words
   above it. */
.card__title {
    padding-bottom: var(--ds-space-2);
    background-image: linear-gradient(to right, var(--ds-action) 0 32px, transparent 32px);
    background-repeat: no-repeat;
    background-position: left bottom;
    background-size: 100% 2px;
}

/* The SaaS console keeps its plain title, so the two consoles differ in
   one more place a glance can catch. */
[data-console="saas"] .card__title {
    padding-bottom: 0;
    background-image: none;
}

/* -- Empty states -----------------------------------------------------
   "Nothing open" is the most-seen screen in a quiet restaurant, and a grey
   glyph on white made it read like a failure rather than like a calm
   evening. The brand blue at 300 says the same thing in a friendlier voice
   without inventing a colour, and without borrowing a semantic hue that
   would make an empty state look like a status. */
.screen-state__icon,
.screen-state .icon { color: var(--ds-primary-300); }

@media (prefers-reduced-motion: reduce) {
    .rail-nav { transition: none; }
}

@media (max-width: 900px) {
    /* The rail already stacks below 900px; a collapse toggle there would
       control something that is no longer a rail. */
    .rail-nav__collapse { display: none; }
}


/* --- Rail overflow + toggle placement --------------------------------
   The platform rail carries 15 links. At 900px of viewport that overflows,
   and a toggle pinned to the foot with margin-top:auto ends up below the
   fold — which is why the collapse control appeared not to exist.

   So: the rail is a fixed-height flex column, the LINK LIST scrolls, and
   the toggle lives in the brand row at the top where it is always on
   screen regardless of how many links are added later. */

.rail-nav {
    height: 100vh;
    position: sticky;
    top: 0;

    /* ⚠ THE RAIL ITSELF SCROLLS. Two previous attempts put the overflow on
       an inner .rail-nav__scroll and relied on flex to constrain it — and it
       kept not engaging, because that depends on the rail resolving to
       exactly 100vh AND its flex children shrinking correctly AND the empty
       .grow spacer (flex: 1 1 auto) not competing for the same free space.
       Three things that all have to be right.

       Native overflow on the rail depends on one thing: the rail has a
       height. Fewer moving parts, and the brand row is pinned separately so
       the collapse toggle still never scrolls away. */
    overflow-y: auto;
    overflow-x: hidden;

    scrollbar-width: thin;
    scrollbar-color: rgba(255,255,255,0.22) transparent;
    /* Padding must count inside the 100vh or the column is taller than the
       viewport before a single link is drawn. */
    box-sizing: border-box;
}

.rail-nav__brand {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ds-space-2);
    flex: 0 0 auto;
}

/* ⚠ A rule that used to live here read
       .rail-nav > :not(.rail-nav__brand):not(.rail-nav__collapse)
   and set flex: 0 0 auto to stop the footer stretching. Its two :not()
   classes gave it specificity (0,4,0), which beat .rail-nav__scroll's
   (0,2,0) — so it silently pinned the scroller to "don't grow, don't
   shrink" and the overflow never engaged. The footer is fixed-size
   already; the rule was solving a problem that did not exist.

   ⚠ Pass 35 — THIS COMMENT WAS NEVER TERMINATED, AND THAT ATE THE RULE
   BELOW IT. The next comment-close in the file belonged to the inner comment
   inside .rail-nav__scroll, so everything from the selector down to
   min-height's explanation was commented out; the remaining declarations
   became a stray top-level prelude and the closing brace was discarded as a
   parse error. The rule therefore never applied — flex: 1 1 auto, the
   min-height: 0 the comment below calls "THE line that makes scrolling
   work", and overflow-y: auto were all absent. The symptom is precisely the
   bug this block was written to explain having fixed: the SaaS rail runs to
   twenty-odd links across seven sections, overflowed, and could not be
   scrolled to reach the last of them.

   ⚠ AND DO NOT WRITE A LITERAL COMMENT-CLOSE SEQUENCE IN THIS PARAGRAPH.
   CSS comments do not nest and have no escape, so quoting the characters
   inside the prose re-creates the exact bug being described — which is how
   the first attempt at this note broke the rule a second time. */

.rail-nav__scroll {
    flex: 1 1 auto;

    /* THE line that makes scrolling work. A flex item's default
       min-height:auto refuses to shrink below its content, so the box
       grows past the rail and overflow-y: auto never has anything to do.
       Every "my flex child won't scroll" bug is this one. */
    min-height: 0;

    overflow-y: auto;
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-1);
    /* A hairline scrollbar rather than the OS default, which renders as a
       bright slab on a dark panel. */
    scrollbar-width: thin;
    scrollbar-color: rgba(255,255,255,0.22) transparent;
}

.rail-nav__scroll::-webkit-scrollbar { width: 6px; }
.rail-nav__scroll::-webkit-scrollbar-thumb {
    background: rgba(255,255,255,0.22);
    border-radius: 3px;
}

/* The toggle now sits in the brand row, so it must not stretch. */
.rail-nav__collapse {
    margin-top: 0;
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    padding: 0;
}

.rail-nav__collapse span { display: none; }

[data-rail="collapsed"] .rail-nav__brand { justify-content: center; }

/* --- Sign-in screen --------------------------------------------------
   Quieter and squarer than the console: one card, generous rhythm, and
   the accent reserved for the single action. */

.auth-shell[data-console="saas"] {
    background:
        radial-gradient(1100px 620px at 50% -12%, var(--ds-neel-100) 0%, transparent 62%),
        var(--ds-neel-50);
}

.auth-shell[data-console="saas"] .auth-card {
    border: 1px solid var(--ds-neel-200);
    box-shadow: 0 12px 32px rgba(23, 40, 70, 0.10);
}

/* ⚠ THE SPICE TEXTURE AND ITS SCRIM ARE THE TENANT SIGN-IN'S ALONE.
   The rules that draw them hang off .auth-shell, which this console shares,
   so without this they would paint a dark navy field with culinary motifs
   over the platform login — a screen for DesiMenus staff, on the console
   that is deliberately not warm and deliberately not food-branded. It keeps
   its pale indigo wash. */
.auth-shell[data-console="saas"]::before,
.auth-shell[data-console="saas"]::after { content: none; }

.auth-shell[data-console="saas"] > :not(.auth-card) { color: inherit; }

.auth-shell.rail-nav__console-badge {
    color: var(--ds-neel-600);
    letter-spacing: 0.10em;
}


/* --- Brand mark ------------------------------------------------------
   Inherits currentColor, so the rail's white and the sign-in card's
   indigo both come free. */

.brand-mark { display: block; flex: 0 0 auto; }

.rail-nav__brand-lockup {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    min-width: 0;
    color: #FFFFFF;
}

/* Collapsed: the mark IS the branding. This is the payoff for choosing a
   mark that survives at 28px instead of a wordmark. */
[data-rail="collapsed"] .rail-nav__brand-lockup { gap: 0; }

.auth-shell[data-console="saas"] .auth-card__brand {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    color: var(--ds-neel-600);
    margin-bottom: var(--ds-space-3);
}


/* --- Brand lockup ----------------------------------------------------
   Mirrors site.css so the console and the marketing site read as one
   product. Values copied, not approximated:
       .brand      { gap: 10px; color: var(--primary-600) }
       .brand-word { font: 600 19px/1 display; color: var(--text) }
       .brand-word em { font-style: normal; color: var(--primary-600) }
   ------------------------------------------------------------------- */

.brand { display: inline-flex; align-items: center; gap: 10px; color: var(--ds-action); }
/* IBM Plex SERIF, not the UI sans. site.css line 140:
       .brand-word { font: 600 19px/1 var(--font-display); }
       --font-display: "IBM Plex Serif", Georgia, serif;
   The console loads Sans, Devanagari and Mono but NOT Serif, so App.razor
   gains it — without that the wordmark silently falls back to Georgia and
   stops matching the site. */
.brand-word {
    font-family: "IBM Plex Serif", Georgia, serif;
    font-weight: 600;
    font-size: 19px;
    line-height: 1;
    color: var(--ds-text);
}
.brand-word em { font-style: normal; color: var(--ds-action); }

/* The rail is navy, so the two-tone split would put dark-on-dark. Both
   halves go white and the MARK carries the colour instead. */
.rail-nav__brand-name em { font-style: normal; color: #FFFFFF; }
/* The mark carries the logo value on the rail — §3's "accents on dark
   surfaces", which is the one job primary-400 was reserved for. */
.rail-nav__brand-lockup .brand-mark { color: var(--ds-rail-marker); }

.rail-nav__brand-lockup {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
}

/* The restaurant's own name, under the DesiMenus lockup and visibly not
   part of it. The visual-assets rules are explicit that a tenant's identity
   never locks up with the mark as a unit: this is the console saying whose
   data is on screen, not a co-branded logo. */
.rail-nav__tenant {
    margin-top: var(--ds-space-3);
    padding-top: var(--ds-space-3);
    border-top: 1px solid var(--ds-rail-border);
    color: var(--ds-rail-fg);
    font: var(--ds-label);
}

.rail-nav__tenant-label {
    display: block;
    font: var(--ds-overline);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ds-rail-fg-muted);
    margin-bottom: 2px;
}

[data-rail="collapsed"] .rail-nav__tenant { display: none; }

.auth-shell[data-console="saas"] .auth-card__brand {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    margin-bottom: var(--ds-space-5);
}

/* The rail wordmark is the same lockup, so the same face. */
.rail-nav__brand-name {
    font-family: "IBM Plex Serif", Georgia, serif;
    font-weight: 600;
}


/* --- Banner icon alignment (now that .banner is block) ---------------- */

.banner > svg:first-child,
.banner > .icon:first-child {
    float: left;
    margin-right: var(--ds-space-3);
    /* Optical: sits on the cap height of the first line rather than its box. */
    margin-top: 1px;
}

.banner::after { content: ""; display: block; clear: both; }


/* --- Dialogs on the platform console ---------------------------------
   The modal was the one surface the theme never reached: white chrome,
   default accent, and a footer that looked detached from the console it
   opens over. Scoped to saas so Web Admin keeps its own treatment. */

[data-console="saas"] .modal {
    border: 1px solid var(--ds-neel-200);
    box-shadow: 0 24px 48px rgba(15, 36, 71, 0.18);
    overflow: hidden;
}

[data-console="saas"] .modal__header {
    background: var(--ds-neel-50);
    border-bottom: 1px solid var(--ds-neel-100);
}

[data-console="saas"] .modal__title { color: var(--ds-neel-900); }

[data-console="saas"] .modal__footer {
    background: var(--ds-neel-50);
    border-top: 1px solid var(--ds-neel-100);
}

/* The scrim picks up the brand's own navy rather than neutral black, so a
   dialog reads as part of this console and not a browser-default overlay. */
[data-console="saas"] .modal-scrim { background: rgba(9, 24, 48, 0.52); }


/* --- Form fields inside a .row --------------------------------------
   .row is align-items:center, which is right for a toolbar and wrong for
   a pair of form fields: when one field carries a caption it becomes
   taller, and centring then pushes its NEIGHBOUR down by half the
   difference. That is the offset between "Max outlets" and "Max staff
   users" in the plan dialog — the fields were fine, the alignment was
   doing exactly what it was told.

   Top-aligning the fields specifically leaves .row's centring intact
   everywhere it is genuinely wanted. */

.row > .field { align-self: flex-start; }


/* --- Definition list for record detail --------------------------------
   A two-column key/value grid. Used by the invoice detail screen; kept
   generic because every "one record, read it" page wants the same shape.
   Collapses to one column on narrow viewports rather than squeezing the
   value into a sliver. */

.detail-list {
    display: grid;
    grid-template-columns: minmax(140px, max-content) 1fr;
    gap: var(--ds-space-3) var(--ds-space-4);
    margin: 0;
}

.detail-list dt { font: var(--ds-label); color: var(--ds-text-secondary); }
.detail-list dd { margin: 0; font: var(--ds-body); color: var(--ds-text); }

@media (max-width: 640px) {
    .detail-list { grid-template-columns: 1fr; gap: var(--ds-space-1) 0; }
    .detail-list dd { margin-bottom: var(--ds-space-3); }
}

/* --- PDF preview ------------------------------------------------------
   Tall enough to read a page without scrolling the dialog itself; the
   browser's own viewer handles paging inside. */
.pdf-preview {
    width: 100%;
    height: min(68vh, 780px);
    /* .modal--wide overrides this to fill the dialog. */
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-sm);
    background: var(--ds-surface-sunken);
}

/* --- Wide modal (document preview) -----------------------------------
   The default 520px is right for a form and useless for a page of A4:
   the invoice rendered at about a third of readable size. This takes the
   viewport, leaving only enough margin to keep the scrim visible so the
   dialog still reads as a layer rather than a new screen. */

.modal--wide {
    max-width: min(1100px, 94vw);
    width: 94vw;
    max-height: 94vh;
    height: 94vh;
}

.modal--wide .modal__body { flex: 1 1 auto; min-height: 0; display: flex; }
.modal--wide .pdf-preview { height: 100%; flex: 1 1 auto; }

/* Loading sits OVER the frame rather than instead of it — see the note in
   InvoicePreviewDialog on why the frame must exist from the first render. */
.pdf-preview-wrap { position: relative; flex: 1 1 auto; display: flex; min-height: 0; }
.pdf-preview-loading {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    background: var(--ds-surface);
    border-radius: var(--ds-radius-sm);
}

/* --- Rail scrolling, final form -------------------------------------- */

.rail-nav::-webkit-scrollbar { width: 6px; }
.rail-nav::-webkit-scrollbar-thumb {
    background: rgba(255,255,255,0.22);
    border-radius: 3px;
}

/* Brand + collapse toggle stay put while the links move under them. The
   background is required: without it the links show through as they pass. */
.rail-nav__brand {
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--ds-rail-bg);
    /* Lockup left, collapse toggle right. Both rails carry exactly these
       two children, so the rule belongs here rather than in either
       console's own block. */
    justify-content: space-between;
}

/* The inner wrapper is now just a group — it must NOT create a second
   scroll context or constrain its own height. */
.rail-nav__scroll {
    flex: 0 0 auto;
    overflow: visible;
    min-height: 0;
}

/* ⚠ .grow is flex:1 1 auto. Inside a scrolling column it expands to fill
   whatever space is free and pushes the footer out of reach. Harmless in a
   toolbar, wrong here. */
.rail-nav > .grow { flex: 0 0 var(--ds-space-5); }

/* ==========================================================================
   Pass 33 — the bill profile editor (/settings/bill-profile)

   Two columns: the form, and the paper it produces. The paper is the reason
   this screen has a layout of its own rather than being another stack of
   fields in a card — bill wording is read once, at a table, on a strip of
   till roll about as wide as a hand, and a form that cannot show that is a
   form people fill in wrong. Below 1100px the paper drops underneath, where
   it is still useful and no longer squeezes the fields to nothing.
   ========================================================================== */

.bill-editor {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    gap: var(--ds-space-5);
    align-items: start;
}

/* ⚠ THE BREAKPOINT IS MEASURED ON THE CONTENT COLUMN, NOT THE WINDOW, AND
   THE FIRST VALUE HERE (1100px) WAS WRONG BECAUSE IT FORGOT THE RAIL.
   The nav rail is ~300px, so a 1400px window leaves ~1080px of content —
   under 1100, and the layout collapsed to one column on a screen easily
   wide enough for two. 860px is measured against what the form and a 320px
   sheet of paper actually need side by side. */
@media (max-width: 860px) {
    .bill-editor { grid-template-columns: minmax(0, 1fr); }
    .bill-preview { position: static; }
}

/* Follows the form down a long page — the point of a preview is to be
   looked at while typing, not scrolled back to. */
.bill-preview { position: sticky; top: var(--ds-space-4); }

/* Which scope is being edited. Deliberately a left marker rather than a
   filled row: the table is also the place overrides are read off at a
   glance, and a highlighted band competes with the status chips. */
.row--selected > td:first-child { box-shadow: inset 3px 0 0 0 var(--ds-primary-400); }
.row--selected > td { background: var(--ds-action-subtle); }

/* --------------------------------------------------------------------------
   The paper itself. Monospace, narrow, centred, dashed rules — a receipt is
   the one place in this product where a proportional font would be a lie
   about the output device.
   -------------------------------------------------------------------------- */

.receipt {
    width: 280px;
    margin: 0 auto;
    padding: var(--ds-space-4) var(--ds-space-3);
    background: #FFFDF8;                 /* thermal roll, not paper white */
    color: #1A1A1A;
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-xs);
    font-family: var(--ds-font-numeric);
    font-size: 12px;
    line-height: 17px;
    text-align: center;
    /* A torn edge would be a nice touch and a bad one: it implies the
       preview shows where the paper actually cuts, which it does not. */
    box-shadow: var(--ds-elev-1);
}

.receipt__logo   { max-width: 140px; max-height: 56px; object-fit: contain; margin-bottom: var(--ds-space-2); }
.receipt__name   { font-weight: 700; font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase; }
.receipt__addr   { font-size: 11px; opacity: 0.75; }
.receipt__welcome{ margin: var(--ds-space-2) 0; font-style: italic; }

.receipt__rule   { border-top: 1px dashed #9A9A9A; margin: var(--ds-space-2) 0; }

.receipt__line   { display: flex; justify-content: space-between; gap: var(--ds-space-3); text-align: left; }
.receipt__line--total { font-weight: 700; }

.receipt__foot   { font-size: 11px; margin-top: 2px; }

/* Terms wrap and are left-aligned — it is a paragraph, and centring one is
   how a receipt becomes unreadable. */
.receipt__terms  { font-size: 10px; line-height: 14px; text-align: left; opacity: 0.85; white-space: pre-wrap; }

/* A placeholder square, labelled as one. Drawing a fake QR pattern would
   invite somebody to scan it. */
.receipt__qr {
    width: 84px; height: 84px;
    margin: var(--ds-space-3) auto var(--ds-space-1);
    display: flex; align-items: center; justify-content: center;
    border: 2px dashed #9A9A9A;
    border-radius: var(--ds-radius-xs);
    font-size: 11px; letter-spacing: 0.08em; opacity: 0.6;
}

/* ==========================================================================
   Pass 34 — the item × outlet assignment grid (/menu/outlet-assignment)

   A chain with a dozen branches produces a table wider than any screen, so
   the grid scrolls sideways inside its card and the item column is pinned.
   A name column that scrolled away would leave rows of anonymous ticks —
   which is the one thing this screen must never show, because every tick is
   a dish appearing on or disappearing from a menu.
   ========================================================================== */

.assign-scroll {
    overflow-x: auto;
    /* Room for the sticky column's shadow, which would otherwise be clipped
       by the scroll container at rest. */
    padding-bottom: var(--ds-space-2);
}

.assign-grid { min-width: 100%; }

/* ⚠ position: sticky on a <td>/<th>, not on a column — CSS has no way to
   pin a table column, and wrapping the names in their own table would have
   meant keeping two tables' row heights in step forever. The background is
   required: without it the scrolling cells show through as they pass. */
.assign-grid__name {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--ds-surface);
    min-width: 260px;
    box-shadow: 1px 0 0 0 var(--ds-border);
}

.assign-grid__group > .assign-grid__name { background: var(--ds-surface-sunken); }

.assign-grid__col {
    text-align: center;
    min-width: 104px;
    vertical-align: bottom;
}

.assign-grid__outlet {
    font: var(--ds-label);
    /* Branch names are long and the column is narrow; wrapping beats an
       ellipsis here because the whole column is identified by this word. */
    white-space: normal;
    word-break: break-word;
}

.assign-grid__bulk {
    display: flex;
    justify-content: center;
    gap: var(--ds-space-1);
    margin-top: var(--ds-space-1);
}

.assign-grid__cell { text-align: center; }

/* An unsaved tick, marked so the pending counter is not the only evidence.
   Deliberately a background rather than a border: a border would shift the
   checkbox by a pixel on every change and make the grid twitch. */
.assign-grid__cell--dirty { background: var(--ds-state-pending-fill); }

.assign-tick {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-1);
    /* §8 of the design system puts the hit-target floor at 48px, and a bare
       checkbox is 13. The label is the target. */
    min-width: 48px;
    min-height: 48px;
    cursor: pointer;
}

.assign-tick__mark {
    font: var(--ds-caption);
    color: var(--ds-text-tertiary);
}

/* Small ghost button for the per-column All/None pair — the standard .btn
   height would make the header three rows deep on every column. */
.btn-xs {
    height: auto;
    min-height: 0;
    padding: 2px var(--ds-space-2);
    font: var(--ds-caption);
}

/* A button that reads as a link, for the per-row all/none inside a caption.
   A real .btn there would outweigh the row it belongs to. */
.btn-link {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    color: var(--ds-link);
    text-decoration: underline;
    cursor: pointer;
}

.btn-link:hover { color: var(--ds-action-hover); }

/* Pass 34 — "on here, off anyway": the tile is available at this outlet and
   the item is 86'd tenant-wide, so nothing can order it. Warning-coloured
   rather than red: the setting on this screen is correct, the obstacle is a
   floor up. */
.availability__note {
    font: var(--ds-caption);
    color: var(--ds-state-pending);
    margin-top: var(--ds-space-1);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

/* A read-only value sitting where a control would be — the single-outlet
   case of a picker that must still say what it is acting on. Deliberately
   NOT styled as a disabled <select>: a greyed-out control invites clicking,
   and there is nothing to click. It reads as a stated fact instead. */
.field__static {
    display: flex;
    align-items: center;
    /* --ds-btn-h, matching .field__input exactly, so the label above lines
       up whether the row below it is a dropdown or a stated name. */
    height: var(--ds-btn-h);
    padding: 0 var(--ds-space-3);
    font: var(--ds-body);
    color: var(--ds-text);
    background: var(--ds-surface-sunken);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-sm);
}

/* ═══════════════════════════════════════════════════════════════════════
   Pass 35, WP-0.1 — CIRCUIT FAILURE AND RECONNECTION
   ═══════════════════════════════════════════════════════════════════════
   Styling for the two framework-owned elements in App.razor. Both are
   hidden until Blazor decides otherwise; everything here is about what
   they look like at the one moment they appear.

   ⚠ THE FRAMEWORK DRIVES VISIBILITY, NOT US. #blazor-error-ui is revealed
   by an INLINE style="display: block" (which is why the flex row lives on
   an inner element — an inline style beats a stylesheet one). The reconnect
   modal is revealed by the framework adding one of its own classes:
   components-reconnect-show / -hide / -failed / -rejected. Those four names
   are its vocabulary — do not rename them, and do not add a `display` to
   the base rule that would fight them.
   ═══════════════════════════════════════════════════════════════════════ */

#blazor-error-ui {
    display: none;
    position: fixed;
    inset: auto 0 0 0;
    z-index: 2000;
    padding: var(--ds-space-3) var(--ds-space-5);
    background: var(--ds-state-failed-fill);
    border-top: 2px solid var(--ds-state-failed);
    box-shadow: var(--ds-elev-2);
}

.circuit-error__inner {
    display: flex;
    align-items: center;
    gap: var(--ds-space-4);
    max-width: 1100px;
    margin: 0 auto;
}

.circuit-error__text {
    flex: 1;
    font: var(--ds-body);
    color: var(--ds-text);
}

/* ── Reconnect modal ─────────────────────────────────────────────────── */

.reconnect {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 2100;
    align-items: center;
    justify-content: center;
    background: rgba(8, 42, 71, 0.55);
}

/* The only class that reveals it. */
#components-reconnect-modal.components-reconnect-show { display: flex; }

.reconnect__card {
    display: flex;
    align-items: flex-start;
    gap: var(--ds-space-4);
    max-width: 460px;
    padding: var(--ds-space-6);
    background: var(--ds-surface);
    border-radius: var(--ds-radius-card);
    box-shadow: var(--ds-elev-2);
}

.reconnect__title { font: var(--ds-title); margin-bottom: var(--ds-space-2); }
.reconnect__body  { font: var(--ds-body); color: var(--ds-text-secondary); margin: 0; }

.reconnect__spinner {
    flex: none;
    width: 24px;
    height: 24px;
    border: 3px solid var(--ds-border);
    border-top-color: var(--ds-action);
    border-radius: 50%;
    animation: reconnect-spin 0.9s linear infinite;
}

@keyframes reconnect-spin { to { transform: rotate(360deg); } }

/* ⚠ THREE MUTUALLY EXCLUSIVE MESSAGES IN ONE CARD, switched by the
   framework's state class rather than by script. While retrying, the
   reassuring line shows and the reload button is hidden — offering a reload
   during a retry invites the user to throw away a session that was about to
   come back on its own. Once it has genuinely failed, that reverses. */
.reconnect__body--failed,
.reconnect__reload { display: none; }

#components-reconnect-modal.components-reconnect-failed .reconnect__body,
#components-reconnect-modal.components-reconnect-rejected .reconnect__body { display: none; }

#components-reconnect-modal.components-reconnect-failed .reconnect__body--failed,
#components-reconnect-modal.components-reconnect-rejected .reconnect__body--failed,
#components-reconnect-modal.components-reconnect-failed .reconnect__reload,
#components-reconnect-modal.components-reconnect-rejected .reconnect__reload { display: block; }

/* A failed reconnection is not in progress, so the spinner stops claiming
   it is. */
#components-reconnect-modal.components-reconnect-failed .reconnect__spinner,
#components-reconnect-modal.components-reconnect-rejected .reconnect__spinner { display: none; }

@media (prefers-reduced-motion: reduce) {
    .reconnect__spinner { animation-duration: 3s; }
}

/* Pass 36 — a dropdown row with a label on the left and its state on the
   right, used by the recipe portion pickers.

   ⚠ .dropdown__item is display:flex with a gap, which puts two spans side by
   side at the left edge. That reads as one run-on label ("Large  own recipe")
   rather than as a value and its status, which is the entire job of the
   second span here. */
.dropdown__item--split { justify-content: space-between; }
.dropdown__item--split > .muted { flex: none; }

/* ==========================================================================
   MS-1 — the way back to the public marketing site, under the sign-in card.

   ⚠ DELIBERATELY QUIET. The person this line is for followed a "Login" link
   and is not staff; the person it must not distract is staff, mid-password.
   Small, low-contrast against the dark field but still above the 4.5:1 floor,
   and separated from the card rather than attached to it.

   ⚠ .auth-shell IS `place-items: center` GRID, so a second child simply
   stacks under the card and stays centred. No layout change was needed on
   the shell itself — which is why this is a style-only addition and not a
   markup restructure.
   ========================================================================== */
.auth-shell__footnote {
    margin: var(--ds-space-4) 0 0;
    text-align: center;
    font-size: var(--ds-font-size-sm, 13px);
    color: rgba(255, 255, 255, 0.72);
}

.auth-shell__footnote .btn-link {
    color: rgba(255, 255, 255, 0.88);
    text-decoration: underline;
    text-underline-offset: 3px;
}

.auth-shell__footnote .btn-link:hover,
.auth-shell__footnote .btn-link:focus-visible {
    color: #fff;
}

/* ==========================================================================
   MS-1 — form layout for the marketing console.

   ⚠ WHY THIS EXISTS INSTEAD OF REUSING .row AND .field.

   The first cut of /saas/marketing-content used `.modal--wide` for its
   editors. That class was written for the invoice preview: ONE iframe that
   should fill the dialog, so it sets

       .modal--wide .modal__body { display: flex; }

   with the default row direction. A single child fills the box; a FORM's
   dozen children become a dozen horizontal columns with no wrapping. The
   result was a pricing editor with every field side by side, labels
   collapsed into one-word-per-line slivers, and a horizontal scrollbar.

   `.row` was the second half of the problem: `display: flex` with no
   `flex-wrap` and `align-items: center`, correct for a pair of buttons and
   wrong for a field grid.

   So forms here own their layout outright. Everything below is grid-based,
   wraps at a real breakpoint, and — importantly — does not care what the
   container it lands in is doing.
   ========================================================================== */

/* --- A modal sized for a form ------------------------------------------
   Wider than the 520px default (these editors have two columns) but NOT
   `modal--wide`, which would drag the row-flex body back in. */
.modal--form {
    max-width: min(880px, 94vw);
    width: 100%;
    max-height: 88vh;
}

/* --- The form itself ---------------------------------------------------- */
.mkt-form {
    display: grid;
    gap: var(--ds-space-5);
    /* ⚠ Defends against a flex parent: without this the whole form becomes
       one shrink-to-fit flex item and the grid inside it collapses. */
    width: 100%;
    min-width: 0;
}

/* --- A labelled group of fields ----------------------------------------
   fieldset/legend rather than a div and an h3: this is a group of form
   controls, and that is what the element is for. A screen reader announces
   the legend with every field inside it, which is the whole point of
   "proper headings" on a form this long. */
.mkt-group {
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-md);
    padding: var(--ds-space-4) var(--ds-space-5) var(--ds-space-5);
    margin: 0;
    min-width: 0;
}

.mkt-group__title {
    font: var(--ds-label);
    color: var(--ds-text-secondary);
    text-transform: uppercase;
    letter-spacing: .07em;
    padding: 0 var(--ds-space-2);
    margin-left: calc(var(--ds-space-2) * -1);
}

.mkt-group__hint {
    font: var(--ds-caption);
    color: var(--ds-text-tertiary);
    margin: 0 0 var(--ds-space-4);
}

/* --- The field grid ----------------------------------------------------- */
.mkt-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--ds-space-4);
    align-items: start;
}

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

/* Anything that needs the full width of its group — textareas, bullet
   lists, the sign-in URLs. */
.mkt-span { grid-column: 1 / -1; }

.mkt-field {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-2);
    /* minmax(0,1fr) above stops the column expanding; this stops the
       control inside it doing the same. Without both, one long select
       option widens the whole grid. */
    min-width: 0;
}

.mkt-field > label,
.mkt-field__label {
    font: var(--ds-label);
    color: var(--ds-text-secondary);
}

.mkt-field input[type="text"],
.mkt-field input[type="number"],
.mkt-field input:not([type]),
.mkt-field select,
.mkt-field textarea {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    box-sizing: border-box;
}

.mkt-field textarea {
    resize: vertical;
    min-height: 78px;
    line-height: 1.5;
    font-family: inherit;
}

/* Help text sits UNDER its control, never beside it. Beside is what
   produced the one-word-per-line column in the first version. */
.mkt-help {
    font: var(--ds-caption);
    color: var(--ds-text-tertiary);
    margin: 0;
}

/* --- Checkbox rows ------------------------------------------------------
   A full-width row: box, then label and help stacked beside it. The label
   never becomes a narrow column, because the text block is the flexible
   part and the box is fixed. */
.mkt-check {
    grid-column: 1 / -1;
    display: flex;
    align-items: flex-start;
    gap: var(--ds-space-3);
    padding: var(--ds-space-3) var(--ds-space-4);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-sm);
    background: var(--ds-surface-sunken);
    cursor: pointer;
    min-width: 0;
}

.mkt-check input[type="checkbox"] {
    flex: none;
    margin-top: 2px;
    width: 18px;
    height: 18px;
}

.mkt-check__text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.mkt-check__label { font: var(--ds-body-strong, var(--ds-label)); color: var(--ds-text-primary); }
.mkt-check__help { font: var(--ds-caption); color: var(--ds-text-tertiary); }

/* --- Page structure ----------------------------------------------------- */
.mkt-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ds-space-2);
    padding-bottom: var(--ds-space-4);
    margin-bottom: var(--ds-space-5);
    border-bottom: 1px solid var(--ds-border);
}

.mkt-tabs__label {
    width: 100%;
    font: var(--ds-caption);
    color: var(--ds-text-tertiary);
    text-transform: uppercase;
    letter-spacing: .07em;
    margin-bottom: var(--ds-space-1, 4px);
}

.mkt-tab-badge {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 7px;
    border-radius: 999px;
    font: var(--ds-caption);
    background: var(--ds-state-pending-fill);
    color: var(--ds-state-pending);
}

.mkt-section { margin-bottom: var(--ds-space-6); min-width: 0; }

.mkt-section__head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--ds-space-3);
    margin-bottom: var(--ds-space-4);
}

.mkt-section__title { font: var(--ds-title); margin: 0 0 4px; }
.mkt-section__sub { font: var(--ds-caption); color: var(--ds-text-tertiary); margin: 0; max-width: 68ch; }

/* Tables in this console carry free text an operator typed, so they need a
   scroll container of their own rather than pushing the page sideways. */
.mkt-table-wrap { width: 100%; overflow-x: auto; }
.mkt-table-wrap .data-table { min-width: 640px; }
.mkt-cell-muted { color: var(--ds-text-tertiary); }
.mkt-cell-actions { text-align: right; white-space: nowrap; }
.mkt-cell-actions .btn + .btn { margin-left: var(--ds-space-2); }

/* The Site tab is one long form, so its save control follows the operator
   down rather than living at the bottom of a 900px scroll. */
.mkt-save-bar {
    position: sticky;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--ds-space-3);
    padding: var(--ds-space-4) 0;
    margin-top: var(--ds-space-5);
    background: var(--ds-surface);
    border-top: 1px solid var(--ds-border);
}

@media (max-width: 760px) {
    .mkt-grid,
    .mkt-grid--3 { grid-template-columns: minmax(0, 1fr); }
    .modal--form { max-width: 96vw; }
}

/* ==========================================================================
   CO-1 — the self-serve checkout (/checkout).

   ⚠ ITS OWN NAMESPACE (.co-*) AND NOTHING ELSE'S. This page is the only
   screen in the console rendered for somebody who is not signed in and has
   no tenant, and it is the one screen where a layout regression costs a
   sale rather than an inconvenience. Reusing .modal__* or .rail-* here would
   couple a public purchase funnel to the internals of an admin console that
   changes for entirely unrelated reasons.

   It DOES reuse .mkt-* for the form groups (CO-1 and MS-1 have the same
   problem: a long form that must not collapse into a flex row) and .btn/
   .banner for controls, because those are genuinely shared primitives.
   ========================================================================== */

.co-shell {
    min-height: 100vh;
    background: var(--ds-surface-sunken);
    padding: var(--ds-space-6) var(--ds-space-5) var(--ds-space-8);
}

/* --- Header and progress ------------------------------------------------ */
.co-head {
    max-width: 1120px;
    margin: 0 auto var(--ds-space-6);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ds-space-4);
}

.co-head__brand {
    font: var(--ds-title);
    font-weight: 700;
    color: var(--ds-primary-600, var(--ds-text-primary));
    letter-spacing: -0.01em;
}

.co-steps {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ds-space-4);
}

.co-steps__item {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    font: var(--ds-caption);
    color: var(--ds-text-tertiary);
}

.co-steps__dot {
    display: grid;
    place-items: center;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    border: 1px solid var(--ds-border-strong);
    font: var(--ds-caption);
    /* ⚠ Fixed size, not padding-driven: a one-digit and a two-digit step
       number must not produce two different circle widths. */
    flex: 0 0 24px;
}

.co-steps__item.is-current { color: var(--ds-text-primary); font-weight: 600; }
.co-steps__item.is-current .co-steps__dot {
    background: var(--ds-action);
    border-color: var(--ds-action);
    color: #fff;
}

.co-steps__item.is-done .co-steps__dot {
    background: var(--ds-state-settled-fill);
    border-color: var(--ds-state-settled);
    color: var(--ds-state-settled);
}

/* --- The two-column body ------------------------------------------------ */
.co-grid {
    max-width: 1120px;
    margin: 0 auto;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    gap: var(--ds-space-6);
    align-items: start;
}

.co-main { min-width: 0; }

.co-panel {
    max-width: 1120px;
    margin: 0 auto;
    background: var(--ds-surface);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-md);
    padding: var(--ds-space-6);
    display: grid;
    gap: var(--ds-space-5);
    min-width: 0;
}

.co-panel__title { font: var(--ds-headline, var(--ds-title)); margin: 0; }
.co-panel__sub   { font: var(--ds-body); color: var(--ds-text-secondary); margin: 0; max-width: 62ch; }

.co-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ds-space-3);
    justify-content: flex-end;
}

/* --- Monthly / annual toggle -------------------------------------------- */
.co-toggle {
    display: inline-flex;
    padding: 4px;
    gap: 4px;
    border: 1px solid var(--ds-border);
    border-radius: 999px;
    background: var(--ds-surface-sunken);
    justify-self: center;
}

.co-toggle__btn {
    border: 0;
    background: transparent;
    border-radius: 999px;
    padding: var(--ds-space-2) var(--ds-space-5);
    font: var(--ds-body-strong, var(--ds-label));
    color: var(--ds-text-secondary);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-2);
}

.co-toggle__btn.is-active {
    background: var(--ds-surface);
    color: var(--ds-text-primary);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}

.co-toggle__save {
    font: var(--ds-caption);
    color: var(--ds-state-settled);
    background: var(--ds-state-settled-fill);
    border-radius: 999px;
    padding: 2px 8px;
    white-space: nowrap;
}

/* --- Plan cards --------------------------------------------------------- */
.co-plans {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--ds-space-4);
    align-items: stretch;
}

.co-plan {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-2);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-md);
    padding: var(--ds-space-5);
    background: var(--ds-surface);
    min-width: 0;
}

/* ⚠ An INSET ring rather than a wider border. A border that thickens on
   selection changes the card's box size and shunts every other card sideways
   as the buyer clicks around. */
.co-plan.is-selected {
    border-color: var(--ds-action);
    box-shadow: inset 0 0 0 1px var(--ds-action);
}

.co-plan.is-quote-only { background: var(--ds-surface-sunken); }

.co-plan__tier {
    font: var(--ds-caption);
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--ds-text-tertiary);
}

.co-plan__name  { font: var(--ds-title); }
.co-plan__price { font: var(--ds-headline, var(--ds-title)); font-weight: 700; }
.co-plan__price--none { font: var(--ds-title); color: var(--ds-text-secondary); }
.co-plan__per   { font: var(--ds-caption); font-weight: 400; color: var(--ds-text-tertiary); margin-left: 4px; }
.co-plan__note  { font: var(--ds-caption); color: var(--ds-text-tertiary); }
.co-plan__setup { font: var(--ds-caption); color: var(--ds-text-secondary); }

.co-plan__features {
    list-style: none;
    padding: 0;
    margin: var(--ds-space-2) 0 var(--ds-space-4);
    display: grid;
    gap: 6px;
    font: var(--ds-caption);
    color: var(--ds-text-secondary);
}

.co-plan__features li::before { content: "✓ "; color: var(--ds-state-settled); }

/* ⚠ Pushes the button to the bottom so cards of differing feature counts
   still line their buttons up. */
.co-plan .btn { margin-top: auto; }

/* --- Order summary ------------------------------------------------------ */
.co-summary {
    background: var(--ds-surface);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-md);
    padding: var(--ds-space-5);
    display: grid;
    gap: var(--ds-space-4);
    /* Follows the buyer down a long form. */
    position: sticky;
    top: var(--ds-space-5);
    min-width: 0;
}

.co-summary__title { font: var(--ds-title); margin: 0; }

.co-summary__plan {
    display: grid;
    gap: 2px;
    padding-bottom: var(--ds-space-3);
    border-bottom: 1px solid var(--ds-border);
}

.co-summary__plan span { font: var(--ds-caption); color: var(--ds-text-tertiary); }

.co-lines { margin: 0; display: grid; gap: var(--ds-space-2); }

.co-lines__row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--ds-space-4);
    font: var(--ds-body);
}

.co-lines__row dt { margin: 0; display: grid; gap: 2px; min-width: 0; }
.co-lines__row dd { margin: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }

.co-lines__detail { font: var(--ds-caption); color: var(--ds-text-tertiary); }

.co-lines__row.is-waived dd { color: var(--ds-state-settled); }
.co-lines__row.is-waived dt { color: var(--ds-text-tertiary); }

.co-lines__row--sub {
    padding-top: var(--ds-space-2);
    border-top: 1px solid var(--ds-border);
}

.co-lines__row--total {
    padding-top: var(--ds-space-3);
    border-top: 1px solid var(--ds-border-strong);
    font: var(--ds-title);
    font-weight: 700;
}

.co-summary__renew { font: var(--ds-caption); color: var(--ds-text-secondary); margin: 0; }

.co-notice {
    font: var(--ds-caption);
    color: var(--ds-state-pending);
    background: var(--ds-state-pending-fill);
    border-radius: var(--ds-radius-sm);
    padding: var(--ds-space-2) var(--ds-space-3);
    margin: 0;
}

/* --- Verify and completion ---------------------------------------------- */
.co-code input {
    font: var(--ds-headline, var(--ds-title));
    letter-spacing: .35em;
    text-align: center;
    max-width: 12ch;
    padding: var(--ds-space-3);
    border: 1px solid var(--ds-border-strong);
    border-radius: var(--ds-radius-sm);
}

.co-pay, .co-creds {
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-md);
    padding: var(--ds-space-5);
    display: grid;
    gap: var(--ds-space-3);
    background: var(--ds-surface-sunken);
}

.co-creds h2 { font: var(--ds-title); margin: 0; }

.co-creds__warn {
    font: var(--ds-caption);
    color: var(--ds-state-pending);
    margin: 0;
}

.co-creds dl { margin: 0; display: grid; gap: var(--ds-space-2); }
.co-creds dl > div { display: flex; gap: var(--ds-space-3); flex-wrap: wrap; }
.co-creds dt { font: var(--ds-caption); color: var(--ds-text-tertiary); min-width: 12ch; }
.co-creds dd { margin: 0; }

.co-creds code {
    font-family: var(--ds-font-mono, ui-monospace, monospace);
    background: var(--ds-surface);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-sm);
    padding: 2px 8px;
    user-select: all;
}

/* --- The honeypot -------------------------------------------------------
   ⚠ POSITIONED OFF-SCREEN RATHER THAN display:none OR hidden. A field that
   is display:none is skipped by a meaningful fraction of bots, which defeats
   the point; one that is merely off-screen looks fillable to a script and
   invisible to a person. Paired with tabindex="-1" and aria-hidden in the
   markup so it stays out of keyboard and screen-reader order. */
.co-hp {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* --- Narrow screens ----------------------------------------------------- */
@media (max-width: 900px) {
    .co-grid { grid-template-columns: minmax(0, 1fr); }

    /* ⚠ The summary UNSTICKS and moves ABOVE the form on a phone. Sticky
       inside a single-column flow would pin it over the fields the buyer is
       typing into, and a total they cannot see while filling the form in is
       the thing this panel exists to prevent. */
    .co-summary { position: static; order: -1; }
}

@media (max-width: 560px) {
    .co-shell { padding: var(--ds-space-4) var(--ds-space-3) var(--ds-space-6); }
    .co-panel { padding: var(--ds-space-4); }
    .co-steps__label { display: none; }
    .co-actions .btn { flex: 1 1 100%; }
}
