/* ═════════════════════════════════════════════════════════════════════════
   MIRROR WEB — APPOINTMENTS SURFACE
   Mirror Web UX/UI Reconstruction (MWU), screen 2.  Phase 7, 2026-08-31.
   docs/mirror-web-ux-rebuild-appointments-2026-08-31/05-integration-plan.md

   ── WHAT THIS FILE IS ────────────────────────────────────────────────────
   The Laravel reproduction of the APPROVED, FROZEN prototype
     docs/…/prototype/appointments.html
     sha256 b106985841323c9acd47257842ac5b4e40ff7e269377d760a082d92f4b6ea753
   Every rule below is ported from that file's §4/§5, which is the UX/UI
   contract (R1a). Where the prototype cites `[SOURCE] file:line`, the citation
   is carried across so the chain back to the Flutter app is never broken.

   ⛔ SOLE OWNER of `mirror-appt-*`. It contains NO `portal-` selector, so this
   sheet and portal.css can never select the same element. That is how a fourth
   CSS generation is prevented rather than merely promised — portal.css already
   declares `.portal-account-summary` three times from three design
   generations, and the shipped card is an emergent composite nobody designed.

   ⛔ portal.css / custom.css / mirror-tokens.css / mirror-primitives.css are
   SHA-256 PINNED (S5CustomerAppIconsTest.php:355-358) and are NOT edited.
   ⛔ mirror-tokens-mwu.css is NOT edited either — owner decision D-2, so the
   pilot's file and its asserted `--mirror-logo-mark` line stay out of reach.
   ═════════════════════════════════════════════════════════════════════════ */

:root {
    /* ═════════════════════════════════════════════════════════════════════
       TOKENS — only what is genuinely NEW.
       35 of the frozen prototype's 49 tokens already exist, with identical
       values, in mirror-tokens.css or mirror-tokens-mwu.css; those are REUSED,
       not redeclared. Audited mechanically against both files before writing.
       ═════════════════════════════════════════════════════════════════════ */

    /* The page ground. [SOURCE] constants.dart:17 lightGrayColor.
       ⚠️ mirror-tokens.css:67 already binds this VALUE to `--mirror-field-fill`
       ("the app fills its inputs with it"). That is a different semantic role —
       a field fill is not a page ground — so this is a second name for one
       value ON PURPOSE, exactly as the mwu layer keeps `--mirror-hairline`
       distinct from `--mirror-line-soft`. */
    --mirror-ground:            #F5F5F5;

    /* [SOURCE] custom_dialog.dart:21 — BorderRadius.circular(15). No base
       token carries 15px. */
    --mirror-radius-dialog:     15px;

    /* [SOURCE] bookings_view.dart:160, single_order_screen.dart:115,282,307,336
       — Card(elevation: 2). Material's elevation-2, expressed in CSS. */
    --mirror-card-shadow:       0 1px 2px rgba(0, 0, 0, .14),
                                0 2px 4px rgba(0, 0, 0, .10);

    /* ── The 18px step. ⚠️ COLLISION, ROUTED AROUND ─────────────────────
       The frozen prototype declares `--mirror-text-lg: 18px`
       ([SOURCE] single_order_screen.dart:241 · custom_button.dart:191-193).
       mirror-tokens.css:131 ALREADY declares `--mirror-text-lg` as **20px**
       (titleLarge). Writing `var(--mirror-text-lg)` here would silently render
       the detail header and the desktop button TWO PIXELS LARGER than the
       approved prototype, and redeclaring the name would change that meaning
       for every other surface on the page.
       No base token is 18px, so the value gets its own namespaced name.
       Caught by a mechanical token audit, not by eye. */
    --mirror-appt-text-lg:      18px;

    /* ── Status colours — [ADAPTATION], and the reason is MEASURED ───────
       [SOURCE] order.dart:249-266 returns Flutter FRAMEWORK Material-500
       constants and bookings_view.dart:185 paints WHITE 12sp on them.
       Measured against white (WCAG AA needs 4.5:1):
         orange #FF9800 2.16:1 · blue #2196F3 3.12:1 · green #4CAF50 2.78:1
         red    #F44336 3.68:1 · gray1 #979797 2.92:1   — EVERY ONE FAILS.
       The composition is preserved exactly (solid fill, white text, radius 8,
       padding 8x4); only the HUE STEP darkens, staying in the same Material
       family wherever a Material step reaches AA. Orange does not reach it even
       at Material 900 (#E65100 = 3.79:1), so pending takes a deeper amber.
       Frozen prototype §1; finding F-9. */
    --mirror-status-pending:    #B45309;  /* 5.02:1                        */
    --mirror-status-processing: #1565C0;  /* 5.75:1 — Material Blue 800    */
    --mirror-status-completed:  #2E7D32;  /* 5.13:1 — Material Green 800   */
    --mirror-status-cancelled:  #C62828;  /* 5.62:1 — Material Red 800     */
    --mirror-status-neutral:    #616161;  /* 6.19:1 — Material Grey 700    */

    /* [ADAPTATION] Desktop master–detail rail. No Flutter counterpart — the
       app is portrait-LOCKED (main.dart:12). Wider than the pilot's 360px
       because a booking card carries services, payment and invoice; at 360 it
       wraps badly. Ruling B-3 fixes the PATTERN, not the width. */
    --mirror-master-width:      420px;

    /* [ADAPTATION] Typography, identical reasoning to the pilot's
       `--mirror-font-profile`: portal.css declares `html, body { font-family }`
       TWICE — rule 2 asks for Sora, rule 204 asks for Inter, and the later
       wins (FUP-3) — so this surface declares its own stack rather than
       inheriting an accident. Sora leads so the intent is honoured when FUP-3
       is fixed; Inter follows so nothing visibly changes on release; the Arabic
       faces are named because NEITHER Sora NOR Inter has Arabic coverage.
       Licence-free, and NOT a NeoSans substitute — OQ-3 verified NeoSans is not
       licensed for web serving. */
    --mirror-appt-font:
        'Sora', Inter, 'Segoe UI', Tahoma,
        'Noto Naskh Arabic', 'Geeza Pro', 'Arabic Typesetting',
        ui-sans-serif, system-ui, sans-serif;
}

/* ═════════════════════════════════════════════════════════════════════════
   0 · SHELL ESCAPE — inherited from the pilot, not rediscovered.
   Each of these cost the Customer Profile build a rework.
   ═════════════════════════════════════════════════════════════════════════ */

/* ⛔ `portal.css:344` makes `main.page-body` its OWN `overflow-x: auto`
   container. In the pilot the document measured clean while `.page-body`
   measured 383 against 375 — a horizontal scrollbar on an inner scrollport
   that every document-level check missed. Scoped with :has() so no other page
   sharing this shell is affected.
   ⚠️ It is ALSO the clip that makes the full bleed below safe. */
body:has(.mirror-appt) .page-body { overflow-x: hidden; }

/* ⛔ The bottom bar's room is ALREADY reserved by the shell:
   layouts/customer/nav.blade.php:137-139 renders `.mirror-cnav-spacer` at
   height 72px. The frozen prototype carries `padding-bottom: var(--mirror-nav-h)`
   on its own `.a-app` because it has no shell to do it.
   ⚙️ R1b technique substitution, REGISTERED: the surface deliberately does NOT
   repeat that padding. Adding it would double the gutter to 144px — the exact
   class of defect that hid a doubled 48px gutter in the pilot for two rounds. */

/* ⛔ THE FULL BLEED — and the clip goes on an ancestor, not here.
   [SOURCE] The app bar and the tab row span the SCREEN
   (bookings_view.dart:11-19, :48-63), and the frozen prototype measures both
   at the full 390. Both shells wrap the slot in a PADDED `main.page-body`, so
   a surface that does not escape it renders inset.

   FOUND AT RUNTIME, Phase 8 — the pilot's own lesson repeating: this file
   DOCUMENTED the technique above and never applied it. Measured at 390 against
   the approved prototype: app bar 343 vs 390, card 295 vs 342, and the tab row
   consequently overflowed (418 > 343). Nothing in the source, the suite, or
   the static CSS diff could see it — only driving the running surface could.

   ⚠️ The bleed goes on the WRAPPER and the clip on an ancestor that spans the
   FULL viewport, because `50vw` counts the scrollbar while the layout width
   does not; a clip on this element would clip the very thing escaping. That
   clip is the `.page-body` rule above.
   `calc(50% - 50vw)` is direction-agnostic, so RTL needs no special case, and
   the >=1024 block below restores `margin-inline: auto` for the capped
   master-detail canvas. */
.mirror-appt {
    margin-inline: calc(50% - 50vw);
    font-family: var(--mirror-appt-font);
    background: var(--mirror-ground);
    color: var(--mirror-body);
    -webkit-font-smoothing: antialiased;
    min-height: 60vh;
}

/* Bootstrap's reboot loads before this file and resets buttons and headings.
   ⛔ Wrapped in :where() so they contribute ZERO specificity. Without it
   `.mirror-appt button` (0,1,1) out-specifies `.mirror-appt-btn` (0,1,0) and
   silently strips a button back to plain text — which is exactly what happened
   in the pilot and was caught only at runtime. */
:where(.mirror-appt, .mirror-appt-overlay) :where(button) {
    font: inherit; color: inherit; background: none;
    border: 0; padding: 0; cursor: pointer;
}
:where(.mirror-appt, .mirror-appt-overlay) :where(a) { color: inherit; text-decoration: none; }
:where(.mirror-appt, .mirror-appt-overlay) :where(svg) { display: block; }
:where(.mirror-appt, .mirror-appt-overlay) :where(h1, h2, p, ul, dl, dd) { margin: 0; padding: 0; }
:where(.mirror-appt, .mirror-appt-overlay) :where(ul) { list-style: none; }

:where(.mirror-appt, .mirror-appt-overlay) :where(button, a, [tabindex]):focus-visible {
    outline: none;
    box-shadow: var(--mirror-focus-ring);
    border-radius: var(--mirror-radius-md);
}

.mirror-appt-sr {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* [SOURCE] bouncing_widgets.dart — BounceIt wraps every tappable surface in
   the app and scales it down on press. */
.mirror-appt-bounce { transition: transform var(--mirror-motion); }
.mirror-appt-bounce:active { transform: scale(var(--mirror-tap-scale)); }

@media (prefers-reduced-motion: reduce) {
    .mirror-appt *, .mirror-appt-overlay * {
        animation-duration: .001ms !important;
        transition-duration: .001ms !important;
    }
}

/* ═════════════════════════════════════════════════════════════════════════
   1 · APP BAR — frozen prototype §3
   [SOURCE] bookings_view.dart:11-19 title + ONE refresh action ·
            single_order_screen.dart:15-18 title + back button.
   ⚠️ The shell renders its own `#customer-header` above this one. Owner
   decision D-1: they stack, following the pilot's approved composition.
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-topbar {
    position: sticky; top: 0; z-index: 20;
    display: flex; align-items: center; gap: var(--mirror-space-2);
    min-height: 56px;
    padding: var(--mirror-space-2) var(--mirror-space-3);
    background: var(--mirror-surface);
    border-bottom: 1px solid var(--mirror-hairline);
}
.mirror-appt-topbar h1 {
    flex: 1 1 auto;
    font-size: var(--mirror-text-md); font-weight: 700; color: var(--mirror-ink);
}
.mirror-appt-iconbtn {
    display: grid; place-items: center;
    width: var(--mirror-hit-min); height: var(--mirror-hit-min);
    border-radius: 50%; background: transparent;
    color: var(--mirror-ink); flex: 0 0 auto;
}
.mirror-appt-iconbtn:hover { background: var(--mirror-ground); }
.mirror-appt-iconbtn svg { width: 22px; height: 22px; }

/* [ADAPTATION] The busy turn on Refresh. The app's icon gives no feedback at
   all; the shipped web swaps the control's label (Row D ruling D10), and that
   wording is reused. ⛔ Nothing here implies a full reload: the shimmer stays
   reserved for the FIRST open, the only time BookingsCubit emits loading()
   (bookings_cubit.dart:10 vs :16-29). Finding F-10a.
   ⚠️ Matched on the attribute's PRESENCE, not on `[aria-busy="true"]`:
   `wire:loading.attr` sets the attribute and the exact value it writes is
   Livewire's business, not this stylesheet's. Verified at runtime, Phase 8. */
.mirror-appt-iconbtn[aria-busy] svg { animation: mirror-appt-spin 700ms linear infinite; }
@keyframes mirror-appt-spin { to { transform: rotate(360deg); } }

/* [SOURCE] custom_appbar.dart — the back chevron mirrors with direction. */
[dir="rtl"] .mirror-appt-back svg { transform: scaleX(-1); }

/* ═════════════════════════════════════════════════════════════════════════
   2 · TABS — frozen prototype §4.1
   [SOURCE] bookings_view.dart:48-63 — a WHITE container holding a scrollable,
   centre-aligned TabBar; selected label + indicator mainColor, unselected grey.
   Reproduced as a real ARIA tablist, which the Flutter TabBar is.
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-tabs {
    position: sticky; top: 56px; z-index: 15;
    display: flex; gap: var(--mirror-space-1);
    justify-content: flex-start;
    background: var(--mirror-surface);
    border-bottom: 1px solid var(--mirror-line-soft);
    padding-inline: var(--mirror-space-2);
    overflow-x: auto;                    /* [SOURCE] isScrollable: true */
    scrollbar-width: none;
}
.mirror-appt-tabs::-webkit-scrollbar { display: none; }
.mirror-appt-tab {
    flex: 0 0 auto; position: relative;
    background: transparent;
    padding: var(--mirror-space-3);
    min-height: var(--mirror-hit-min);
    font-size: var(--mirror-text-sm); font-weight: 600;
    color: var(--mirror-hint-strong);    /* [ADAPTATION] AA — 4.68:1 measured */
    white-space: nowrap;
}
.mirror-appt-tab[aria-selected="true"] { color: var(--mirror-brand); }
.mirror-appt-tab[aria-selected="true"]::after {
    content: ""; position: absolute;
    inset-inline: var(--mirror-space-2); bottom: 0;
    height: 2px; background: var(--mirror-brand); border-radius: 2px 2px 0 0;
}
.mirror-appt-tabs-refresh { margin-inline-start: auto; }

/* ═════════════════════════════════════════════════════════════════════════
   3 · STATUS PILL — frozen prototype §4.2
   [SOURCE] bookings_view.dart:176-187 — radius 8, padding 8x4, WHITE 12sp on
   getStatusColor(). Composition preserved exactly; hue step darkened (F-9).
   ⚙️ R1b, REGISTERED: the prototype's `--mirror-radius-sm` is 8px, but
   mirror-tokens.css:101 declares that name as 4px. `--mirror-radius-md`
   (:102) IS 8px, so the runtime reaches the approved value through it.
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-pill {
    display: inline-block; flex: 0 0 auto;
    padding: var(--mirror-space-1) var(--mirror-space-2);
    border-radius: var(--mirror-radius-md);
    font-size: var(--mirror-text-xs); font-weight: 600; line-height: 1.4;
    color: #FFFFFF; background: var(--mirror-status-neutral);
    white-space: nowrap;
}
.mirror-appt-pill[data-status="pending"]         { background: var(--mirror-status-pending); }
.mirror-appt-pill[data-status="confirmed"]       { background: var(--mirror-status-processing); }
.mirror-appt-pill[data-status="completed"]       { background: var(--mirror-status-completed); }
.mirror-appt-pill[data-status="user_cancel"],
.mirror-appt-pill[data-status="provider_cancel"] { background: var(--mirror-status-cancelled); }
/* `no_show` / `expired` keep the neutral fill AND their own labels — never
   "Cancelled", which would state something untrue about the booking (F-5). */

/* ═════════════════════════════════════════════════════════════════════════
   4 · THE LIST CARD — frozen prototype §4.3
   [SOURCE] bookings_view.dart:155-289 — BounceIt > Card(elevation 2,
   radius 12) > Padding(16). Row order and content reproduced exactly.
   ⚙️ R1b: radius 12 reaches the approved value through `--mirror-radius-lg`
   (mirror-tokens.css:103), rather than minting a duplicate name.
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-list  { padding: var(--mirror-space-4) var(--mirror-space-5); }
.mirror-appt-cards { display: grid; gap: var(--mirror-space-4); }  /* [SOURCE] :147 separator 16 */

.mirror-appt-card {
    display: block; width: 100%; text-align: start;
    background: var(--mirror-surface);
    border-radius: var(--mirror-radius-lg);
    box-shadow: var(--mirror-card-shadow);
    padding: var(--mirror-space-4);
    color: inherit;
}
.mirror-appt-card:hover {
    box-shadow: 0 2px 6px rgba(0, 0, 0, .16), 0 6px 14px rgba(0, 0, 0, .10);
}
.mirror-appt-card[aria-current="true"] {
    box-shadow: 0 0 0 2px var(--mirror-brand), var(--mirror-card-shadow);
}
.mirror-appt-card-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--mirror-space-2);
}
.mirror-appt-card-id {                          /* [SOURCE] :173 16sp bold */
    font-size: var(--mirror-text-md); font-weight: 700; color: var(--mirror-ink);
}
.mirror-appt-card-when {                        /* [SOURCE] :192-197 14sp grey */
    margin-top: var(--mirror-space-3);
    font-size: var(--mirror-text-sm); color: var(--mirror-hint-strong);
}
.mirror-appt-card-sec { margin-top: var(--mirror-space-3); }
.mirror-appt-sec-title {                        /* [SOURCE] :204-208 14sp bold */
    margin-bottom: var(--mirror-space-2);
    font-size: var(--mirror-text-sm); font-weight: 700; color: var(--mirror-ink);
}

/* [SOURCE] :210-224 / :239-253 — one ListTile per service and per payment
   line: a brand-coloured leading icon, then the label. */
.mirror-appt-tiles { display: grid; gap: var(--mirror-space-2); }
.mirror-appt-tile  { display: flex; align-items: flex-start; gap: var(--mirror-space-3); }
.mirror-appt-tile svg {
    flex: 0 0 auto; width: 20px; height: 20px;
    color: var(--mirror-brand); margin-top: 1px;
}
/* [SOURCE] :244-252 — a ListTile stacks `title` ABOVE `subtitle`. These are
   spans, so the stacking is DECLARED; without it the two run together on one
   line, which is what the first prototype build did (defect P-1). */
.mirror-appt-tile-main {
    display: block; font-size: var(--mirror-text-sm); color: var(--mirror-body);
}
.mirror-appt-tile-sub {
    display: block; font-size: var(--mirror-text-xs);
    color: var(--mirror-hint-strong); margin-top: 2px;
}

/* ═════════════════════════════════════════════════════════════════════════
   5 · THE TWO EMPTY STATES — frozen prototype §4.4. They are DIFFERENT (F-4).
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-empty {
    display: grid; place-items: center; gap: var(--mirror-space-3);
    padding: var(--mirror-space-6) var(--mirror-space-5);
    text-align: center;
}
/* (a) NO ORDERS AT ALL — [SOURCE] :31-40. Centred BOLD 14sp, NO artwork; the
       branch returns BEFORE the TabController, so there is no tab bar either. */
.mirror-appt-empty--all .mirror-appt-empty-text {
    font-size: var(--mirror-text-sm); font-weight: 700; color: var(--mirror-ink);
}
/* (b) THIS TAB IS EMPTY — [SOURCE] :120-139. The app's own no_bookings.svg at
       30% of the viewport's SHORTEST side, a 12px gap, then the same copy at
       regular weight. `min(30vw, 30vh)` is the shortest-side rule in CSS. */
.mirror-appt-empty--tab img { width: min(30vw, 30vh); max-width: 260px; height: auto; }
.mirror-appt-empty--tab .mirror-appt-empty-text {
    font-size: var(--mirror-text-sm); color: var(--mirror-body);
}

/* ═════════════════════════════════════════════════════════════════════════
   6 · LOAD ERROR — frozen prototype §4.5. [ADAPTATION]; the reason is a DEFECT
   [DEFECT F-2] bookings_cubit.dart:21 emits BookingsState.error(msg), but the
   view (bookings_view.dart:20-89) has NO error branch and NO listener, so a
   failed load falls through to :31 and is presented as "No Orders yet".
   A failed request shown as an empty account is actively misleading, so it is
   NOT ported. Owner ruling OQ-A3.
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-error {
    display: grid; justify-items: center; gap: var(--mirror-space-3);
    margin: var(--mirror-space-5) auto;
    padding: var(--mirror-space-5);
    max-width: 420px; text-align: center;
    background: var(--mirror-surface);
    border: 1px solid var(--mirror-line-soft);
    border-radius: var(--mirror-radius-lg);
}
.mirror-appt-error svg { width: 40px; height: 40px; color: var(--mirror-status-cancelled); }
.mirror-appt-error p   { font-size: var(--mirror-text-sm); color: var(--mirror-body); }

/* ═════════════════════════════════════════════════════════════════════════
   7 · LOADING — frozen prototype §4.6
   [SOURCE] :22-27 — a ListView of TEN CustomShimmer with padding 16.
   ⚠️ N/A at runtime: the web renders this data server-side, so there is no
   first-paint loading moment. See 06-implementation-report.md §8 item 4.
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-shimmers { padding: var(--mirror-space-4); display: grid; gap: var(--mirror-space-4); }
.mirror-appt-shimmer {
    height: 92px; border-radius: var(--mirror-radius-lg);
    background: linear-gradient(100deg, #ECECEC 30%, #F7F7F7 50%, #ECECEC 70%);
    background-size: 220% 100%;
    animation: mirror-appt-shine 1.4s ease-out infinite;
}
@keyframes mirror-appt-shine {
    from { background-position: 180% 0; }
    to   { background-position: -80% 0; }
}

/* ═════════════════════════════════════════════════════════════════════════
   8 · BUTTONS — frozen prototype §4.7 · [SOURCE] custom_button.dart
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    min-height: var(--mirror-hit-min);
    padding: 0 var(--mirror-space-5);
    border-radius: var(--mirror-radius-md);
    font-size: var(--mirror-text-sm); font-weight: 600;
    background: var(--mirror-brand); color: var(--mirror-text-on-brand);
}
.mirror-appt-btn:hover { background: var(--mirror-brand-dark); }
.mirror-appt-btn--outline {
    background: transparent; color: var(--mirror-brand);
    box-shadow: inset 0 0 0 2px var(--mirror-brand);   /* [SOURCE] broderside width 2 */
}
/* [SOURCE] single_order_screen.dart:218-226 — Cancel is transparent, RED
   label, 2px RED border, radius 8, full width as CustomButton renders. */
.mirror-appt-btn--danger {
    width: 100%; background: transparent;
    color: var(--mirror-status-cancelled);
    box-shadow: inset 0 0 0 2px var(--mirror-status-cancelled);
}
.mirror-appt-btn--danger:hover { background: rgba(198, 40, 40, .06); }
.mirror-appt-btn[disabled] { opacity: .55; cursor: default; }

/* ═════════════════════════════════════════════════════════════════════════
   9 · DETAIL SECTIONS — frozen prototype §4.8 · [SOURCE] single_order_screen
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-detail {
    padding: var(--mirror-space-4) var(--mirror-space-5) var(--mirror-space-6);
}
.mirror-appt-detail-head {                        /* [SOURCE] :235-258 */
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--mirror-space-3);
}
.mirror-appt-detail-id {
    font-size: var(--mirror-appt-text-lg);        /* 18px — see the token note */
    font-weight: 700; color: var(--mirror-ink);
}
.mirror-appt-section { margin-top: var(--mirror-space-5); }  /* [SOURCE] 24.hSpace */
.mirror-appt-section > h2 {                                   /* [SOURCE] :261-267 16sp bold */
    margin-bottom: var(--mirror-space-2);
    font-size: var(--mirror-text-md); font-weight: 700; color: var(--mirror-ink);
}
.mirror-appt-section p { font-size: var(--mirror-text-sm); color: var(--mirror-hint-strong); }

/* [SOURCE] :31-72 — Date and Time side by side, each label above a bold value */
.mirror-appt-when      { display: flex; gap: var(--mirror-space-4); }
.mirror-appt-when > div { flex: 1 1 0; }
.mirror-appt-when dt   { font-size: var(--mirror-text-sm); color: var(--mirror-hint-strong); }
.mirror-appt-when dd   {
    margin-top: var(--mirror-space-1);
    font-size: var(--mirror-text-sm); font-weight: 700; color: var(--mirror-ink);
}

/* [SOURCE] :81-104 — provider chip, brand at 10% opacity, radius 6 */
.mirror-appt-chip {
    display: inline-flex; align-items: center; gap: var(--mirror-space-1);
    padding: var(--mirror-space-1) var(--mirror-space-2);
    border-radius: 6px;
    background: rgba(var(--mirror-brand-rgb), .10);
    color: var(--mirror-brand); font-size: var(--mirror-text-xs); font-weight: 700;
}
.mirror-appt-chip svg { width: 16px; height: 16px; }

/* [SOURCE] :114-171 — employee card, 50x50 circular image + name + job title */
.mirror-appt-emp {
    display: flex; align-items: center; gap: var(--mirror-space-4);
    background: var(--mirror-surface);
    border-radius: var(--mirror-radius-md);
    box-shadow: var(--mirror-card-shadow);
    padding: var(--mirror-space-3);
}
/* [ADAPTATION] Three-state image discipline, from pilot mistake #2: a stored
   path is NOT a file on disk. The fallback is rendered ALWAYS, beneath the
   photo, and `onerror` reveals it — so a 404 can never draw the browser's
   broken-image glyph. */
.mirror-appt-emp-photo { position: relative; width: 50px; height: 50px; flex: 0 0 auto; }
.mirror-appt-emp-fallback {
    position: absolute; inset: 0; display: grid; place-items: center;
    border-radius: 50%;
    background: var(--mirror-ground); color: var(--mirror-hint-strong);
}
.mirror-appt-emp-fallback svg { width: 30px; height: 30px; }  /* [SOURCE] :139 size 30 */
.mirror-appt-emp-photo img {
    position: relative; z-index: 1;
    width: 50px; height: 50px; border-radius: 50%;
    object-fit: cover; display: block;
}
.mirror-appt-emp-photo img[hidden] { display: none; }
/* [SOURCE] :146-166 — name ABOVE job title, in a Column. Declared, not assumed. */
.mirror-appt-emp-name {
    display: block; font-size: var(--mirror-text-sm);
    font-weight: 700; color: var(--mirror-ink);
}
.mirror-appt-emp-role {
    display: block; font-size: var(--mirror-text-xs);
    color: var(--mirror-hint-strong); margin-top: var(--mirror-space-1);
}

/* [SOURCE] :359-378 _buildDetailRow — label start, bold value end, 8px pad */
.mirror-appt-rows {
    background: var(--mirror-surface);
    border-radius: var(--mirror-radius-md);
    box-shadow: var(--mirror-card-shadow);
    padding: var(--mirror-space-4);
}
.mirror-appt-row {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: var(--mirror-space-4);
    padding: var(--mirror-space-2) 0;
}
.mirror-appt-row dt { font-size: var(--mirror-text-sm); color: var(--mirror-hint-strong); }
.mirror-appt-row dd {
    font-size: var(--mirror-text-sm); font-weight: 700;
    color: var(--mirror-ink); text-align: end;
}

/* [SOURCE] :280-296 — each service is its own elevation-2 card, radius 8 */
.mirror-appt-svc {
    display: flex; align-items: center; gap: var(--mirror-space-3);
    background: var(--mirror-surface);
    border-radius: var(--mirror-radius-md);
    box-shadow: var(--mirror-card-shadow);
    padding: var(--mirror-space-3);
    margin-bottom: var(--mirror-space-2);
    font-size: var(--mirror-text-sm);
}
.mirror-appt-svc svg {
    width: 20px; height: 20px; color: var(--mirror-brand); flex: 0 0 auto;
}

.mirror-appt-cancel { margin-top: var(--mirror-space-6); }

/* ═════════════════════════════════════════════════════════════════════════
   10 · LIFECYCLE STEPPER — frozen prototype §4.13
   ⚖️ OWNER RULING OQ-A7 (kept on revision). A `WEB ADAPTATION` with no Flutter
   counterpart: a visual restatement of BookingStateMachine.php:37-47.
   Flex order follows the writing direction, so it mirrors in RTL with no rule.
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-steps {
    display: flex; align-items: flex-start; gap: var(--mirror-space-1);
    margin-top: var(--mirror-space-5);
    padding: var(--mirror-space-3);
    background: var(--mirror-surface);
    border: 1px solid var(--mirror-line-soft);
    border-radius: var(--mirror-radius-lg);
}
.mirror-appt-step {
    flex: 1 1 0;
    display: flex; flex-direction: column; align-items: center;
    gap: var(--mirror-space-1); text-align: center;
    font-size: 11px; font-weight: 700; letter-spacing: .04em;
    color: var(--mirror-hint-strong);
}
.mirror-appt-step::before {
    content: attr(data-step);
    display: grid; place-items: center;
    width: 28px; height: 28px; border-radius: 999px;
    background: var(--mirror-line-soft); color: var(--mirror-hint-strong);
    font-size: var(--mirror-text-xs); font-weight: 800;
}
.mirror-appt-step[data-phase="past"] { color: var(--mirror-brand); }
.mirror-appt-step[data-phase="past"]::before {
    content: "\2713"; background: var(--mirror-brand); color: #fff;
}
.mirror-appt-step[data-phase="current"] { color: var(--mirror-brand); }
.mirror-appt-step[data-phase="current"]::before {
    background: var(--mirror-brand); color: #fff;
    box-shadow: 0 0 0 3px rgba(var(--mirror-brand-rgb), .20);
}
/* The terminal negative states. `no_show` and `expired` are NOT cancellations
   and never borrow that word (F-5), so they carry their own label — and their
   own mark: × for a cancellation, ! for the others. */
.mirror-appt-step[data-phase="stopped"] { color: var(--mirror-status-cancelled); }
.mirror-appt-step[data-phase="stopped"]::before {
    content: "\00D7"; background: var(--mirror-status-cancelled); color: #fff;
}
.mirror-appt-step[data-phase="halted"] { color: var(--mirror-status-neutral); }
.mirror-appt-step[data-phase="halted"]::before {
    content: "!"; background: var(--mirror-status-neutral); color: #fff;
}

/* ═════════════════════════════════════════════════════════════════════════
   11 · CANCEL CONFIRMATION — frozen prototype §4.9
   [SOURCE] custom_dialog.dart:77-126 is showDialog(...) — a centred `Dialog`
   wrapped in BackdropFilter(blur σ=5); white, radius 15, padding 12w x 24h.
   ⛔ Because it is a Dialog and NOT a bottom sheet, the Skill's
   "bottom sheet → phone sheet / ≥600 centred modal" archetype does NOT apply.
   It stays a centred modal at EVERY width. Turning it into a phone sheet would
   be an invented interaction (R2).
   ⚠️ The overlay lives OUTSIDE the surface wrapper so no ancestor transform or
   overflow can trap it — a `position: fixed` box creates a stacking context
   even at `z-index: auto`, which is what sealed the pilot's sheet under the
   navigation bar despite a higher z-index.
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-scrim {
    position: fixed; inset: 0; z-index: 1400;
    background: rgba(0, 0, 0, .28);
    backdrop-filter: blur(5px);
    -webkit-backdrop-filter: blur(5px);
    display: grid; place-items: center;
    padding: var(--mirror-space-5);
    animation: mirror-appt-fade var(--mirror-motion);
}
@keyframes mirror-appt-fade { from { opacity: 0; } }
.mirror-appt-dialog {
    width: min(420px, 100%);
    background: var(--mirror-surface);
    border-radius: var(--mirror-radius-dialog);
    padding: var(--mirror-space-5) var(--mirror-space-3);
    box-shadow: var(--mirror-overlay-shadow);
    animation: mirror-appt-pop var(--mirror-motion-sheet);
}
@keyframes mirror-appt-pop { from { opacity: 0; transform: scale(.96); } }
.mirror-appt-dialog h2 {
    font-size: var(--mirror-text-sm); font-weight: 700; color: var(--mirror-ink);
}
.mirror-appt-dialog p {
    margin-top: var(--mirror-space-4);
    font-size: var(--mirror-text-xs); color: var(--mirror-body);
}
/* [SOURCE] :97-121 — Yes (outlined) FIRST, then No (filled) inside an
   `Expanded`, so NO IS WIDER. 8px apart. Reproduced, asymmetry included. */
.mirror-appt-dialog-actions {
    display: flex; gap: var(--mirror-space-2); margin-top: var(--mirror-space-3);
}
.mirror-appt-dialog-actions .mirror-appt-btn:last-child { flex: 1 1 auto; }

/* ═════════════════════════════════════════════════════════════════════════
   12 · RESPONSIVE — frozen prototype §5
   Thresholds are the APP'S OWN: custom_button.dart:180-198 (600 / 1024).
   ═════════════════════════════════════════════════════════════════════════ */

/* ── ≥600 · TABLET — one centred column, capped at the measure ─────────── */
@media (min-width: 600px) {
    .mirror-appt-btn { font-size: var(--mirror-text-md); min-height: 50px; }  /* [SOURCE] :186-188 */
    .mirror-appt-topbar,
    .mirror-appt-tabs,
    .mirror-appt-list,
    .mirror-appt-detail,
    .mirror-appt-shimmers {
        max-width: var(--mirror-content-max);
        margin-inline: auto;
        width: 100%;
    }
    /* [SOURCE] tabAlignment: TabAlignment.center */
    .mirror-appt-tabs { justify-content: center; }
}

/* ── ≥1024 · DESKTOP — master–detail ──────────────────────────────────────
   [ADAPTATION — registered, and the biggest design decision on this screen]
   Flutter has NO desktop: main.dart:12 locks the app to portrait. Ruling B-3
   fixes master–detail as the pattern, so the list becomes the master pane and
   the booking fills the detail pane. This PRESERVES the push/pop hierarchy the
   app expresses — list is parent context, the booking is the child.
   ⚠️ When the active tab has nothing to show the grid collapses to ONE column
   and the tab-empty artwork is centred, rather than inventing a "nothing
   selected" placeholder pane that has no counterpart anywhere.
   ═════════════════════════════════════════════════════════════════════════ */
@media (min-width: 1024px) {
    .mirror-appt-btn { font-size: var(--mirror-appt-text-lg); min-height: 56px; }  /* [SOURCE] :191-193 */

    .mirror-appt {
        max-width: 1200px; margin-inline: auto;
        padding: 0 var(--mirror-space-5) var(--mirror-space-5);
    }
    .mirror-appt[data-split="1"] .mirror-appt-panes {
        display: grid;
        grid-template-columns: var(--mirror-master-width) minmax(0, 1fr);
        gap: var(--mirror-space-5);
        align-items: start;
    }
    .mirror-appt[data-split="1"] .mirror-appt-master {
        position: sticky; top: var(--mirror-space-5);
        max-height: calc(100dvh - var(--mirror-nav-h) - var(--mirror-space-6));
        overflow: auto;
        background: var(--mirror-surface);
        border-radius: var(--mirror-radius-lg);
        box-shadow: 0 1px 0 var(--mirror-line-soft), 0 10px 40px rgba(43, 22, 35, .08);
    }
    /* Both panes are visible at once, so neither is hidden. */
    .mirror-appt[data-split="1"] .mirror-appt-master,
    .mirror-appt[data-split="1"] .mirror-appt-detail-pane { display: block; }

    .mirror-appt[data-split="1"] .mirror-appt-topbar {
        max-width: none;
        border-radius: var(--mirror-radius-lg) var(--mirror-radius-lg) 0 0;
    }
    .mirror-appt[data-split="1"] .mirror-appt-tabs { max-width: none; top: 56px; }
    .mirror-appt[data-split="1"] .mirror-appt-list {
        max-width: none; padding-inline: var(--mirror-space-4);
    }
    .mirror-appt[data-split="1"] .mirror-appt-detail-pane {
        background: var(--mirror-surface);
        border-radius: var(--mirror-radius-lg);
        box-shadow: 0 1px 0 var(--mirror-line-soft), 0 10px 40px rgba(43, 22, 35, .08);
        overflow: hidden;
    }
    .mirror-appt[data-split="1"] .mirror-appt-detail { max-width: none; }
    /* The detail's own back button is redundant when both panes are visible. */
    .mirror-appt[data-split="1"] .mirror-appt-detail-pane .mirror-appt-back { display: none; }
}

/* ═════════════════════════════════════════════════════════════════════════
   13 · WHICH PANE IS VISIBLE — frozen prototype §6
   ⚙️ R1b technique substitution, REGISTERED. The prototype drives this from
   `html[data-view]` because a single file has no routing. The runtime has two
   routes, so BOTH panes are rendered server-side on BOTH routes and CSS
   decides which is shown — identical visible result at every width, and it is
   what makes the ≥1024 master–detail reachable at all without JavaScript.
   ═════════════════════════════════════════════════════════════════════════ */
.mirror-appt-master,
.mirror-appt-detail-pane { display: none; }
.mirror-appt[data-view="list"]   .mirror-appt-master      { display: block; }
.mirror-appt[data-view="detail"] .mirror-appt-detail-pane { display: block; }

/* Pagination is web-only (reverse parity R5): the app fetches every booking at
   once and groups client-side, so it never needs one. Kept visually quiet — it
   is chrome the approved design does not feature, not a component of it. */
.mirror-appt-pagination { margin-top: var(--mirror-space-5); }
.mirror-appt-pagination:empty { display: none; }

/* ═════════════════════════════════════════════════════════════════════════
   14 · THE TOAST MUST CLEAR THE SHELL'S BOTTOM BAR
   ⚠️ This is FUP-6, raised by the Customer Profile pilot and now due again:
   "the primitive itself is wrong for EVERY customer-shell surface that raises
   a toast, not just this one" (mirror-profile.css:1021-1024).

   `.mirror-toast-region` is pinned at `z-index: 1000` and
   `inset-block-end: 24px` (mirror-primitives.css:132-137) — both BELOW and
   BEHIND `.mirror-cnav`, which is `z-index: 1200` and 72px tall
   (nav.blade.php:138-145). In the pilot every successful save raised a toast
   that was in the DOM, measurably the right size and colour — and completely
   INVISIBLE, painted under the navigation bar. A screenshot of that state
   showed nothing, which is exactly how it would have shipped.

   The frozen prototype already specifies the clearance: `.a-toast` sits at
   `bottom: calc(var(--mirror-nav-h) + var(--mirror-space-4))`.

   ⛔ mirror-primitives.css is SHA-256 pinned, so the general fix is still an
   owner-ruled action; this stays scoped with :has() exactly as the pilot's is,
   and FUP-6 stays open. Two scoped overrides now argue for fixing the
   primitive rather than adding a third.
   ═════════════════════════════════════════════════════════════════════════ */
body:has(.mirror-appt) .mirror-toast-region {
    z-index: 1250;
    inset-block-end: calc(var(--mirror-nav-h) + env(safe-area-inset-bottom, 0px) + var(--mirror-space-4));
}
