/* ============================================================
   APOLONIJA DESIGN TOKENS — "Monochrome"

   The register is Linear: a neutral graphite base, hairline
   borders instead of drop shadows, small radii, a compact Fluent 2
   type ramp (see --apo-text-* below), and ONE accent — which here
   is not a hue at all.

   THE MONOCHROME CONTRACT. The accent is ink: near-black on the
   light build, white on the dark one. It inverts with the theme,
   which is the whole idea — the product is black on white or white
   on black, and colour appears only where it carries meaning
   (success / warning / error). That is what makes a clinical
   back office read as instrument rather than as brochure: nothing
   on screen is decorative, so anything coloured is information.

   This replaced "Graphite & Champagne", where PANTONE 466 C led
   every surface. The gold ramp's NAMES survive below because ~40
   page files consume them; their values are now neutral steps.
   Nothing named "gold" is gold any more — do not reintroduce a hue
   through one of those names.

   TOKEN CONTRACT: every --apo-* name predates this redesign and is
   consumed by ~40 page files. Names are frozen; values are not.
   Remapping the values here re-skins the entire product.
   ============================================================ */
:root {
    color-scheme: light;

    /* --- Legacy hue names, now neutral steps -----------------
       "navy" was the emblem's charcoal-navy. It is the ink ramp's
       dark half now, kept as an alias so the page files that spell
       it this way keep working. */
    --apo-navy-950: #08090A;
    --apo-navy-900: #16181C;
    --apo-navy-800: #24262B;
    --apo-navy-700: #3B3E45;
    --apo-navy-100: #E9EAED;
    --apo-navy-50:  #F4F5F6;

    /* The former champagne ramp, re-cut as neutrals on the same axis as
       --apo-ink-*. Every step is a lightness, not a hue: gold-600 is the
       ink accent's own value, gold-400 the mid grey that carries quiet
       marks, gold-50 the faintest wash the system draws. Contrast is now a
       property of the step rather than something the colour had to be
       corrected for — which is why the ramp lost its per-step ratio notes.
       The chief practical gain: every one of these can carry text. */
    --apo-gold-700: #08090A;   /* 19.9:1 on white */
    --apo-gold-600: #16181C;   /* 16.1:1 — text, focus, borders */
    --apo-gold-500: #3B3E45;   /*  9.6:1 — UI-component borders */
    --apo-gold-400: #6E717A;   /*  5.2:1 — quiet marks, still AA */
    --apo-gold-300: #A9ACB2;
    --apo-gold-100: #E4E5E8;
    --apo-gold-50:  #F4F5F6;

    /* Was Pantone 4545 C, the warm hairline. A cool hairline now. */
    --apo-sand-400: #D9DBDE;

    /* Was Pantone 7540 C, the wordmark's slate. Same role — a mid-dark ink
       that can carry text — with the blue taken out. */
    --apo-slate-500: #3B3E45;

    /* The former secondary house colours. These exist so avatar tints and
       data-visualisation series have more than one step to work with; in a
       monochrome system that difference is lightness, so the pairs are
       deliberately spread rather than adjacent — two greys 1 step apart are
       one grey with a rendering bug. */
    --apo-teal-700: #34373D;
    --apo-teal-600: #4A4D54;
    --apo-teal-400: #B7BAC0;
    --apo-teal-50:  #F2F3F4;

    --apo-plum-700: #1D1F23;
    --apo-plum-400: #7A7D84;
    /* Kept as an indirection so avatar tints can flip theme without the
       ramp needing a dark half. */
    --apo-plum-fg:  var(--apo-plum-700);

    /* --- Neutral ramp ---------------------------------------
       White ground. Because canvas and card are the same value,
       ALL structure is carried by the border + shadow pair — see
       --apo-border and the elevation block below. Those two must
       stay in step: weaken either and the layout collapses into a
       single undifferentiated sheet, which is the failure mode this
       system already hit once. Greys are reserved for recessed
       surfaces (table headers, card header strips, hovers).

       The ramp is one temperature end to end — a barely-there cool
       cast (hue ~250, chroma ~2) that keeps large white areas from
       reading yellow on warm-calibrated clinic monitors, and never
       enough to be seen as blue. */
    --apo-canvas:   #FFFFFF;
    --apo-paper:    #FFFFFF;
    --apo-ink-950:  #08090A;
    --apo-ink-900:  #16181C;   /* primary text   — 16.1:1 */
    --apo-ink-800:  #24262B;
    --apo-ink-700:  #3B3E45;
    --apo-ink-600:  #55585F;   /* secondary text —  7.7:1 */
    --apo-ink-500:  #6E717A;   /* meta, labels   —  5.2:1 */
    --apo-ink-400:  #969AA3;   /* placeholders   —  3.0:1 */
    --apo-ink-300:  #C0C3C9;
    --apo-ink-200:  #DCDEE2;
    --apo-ink-100:  #E9EAED;
    --apo-ink-50:   #F4F5F6;

    --apo-ivory: var(--apo-canvas);

    /* --- Semantic surfaces ----------------------------------
       ONE accent, and it is ink. --apo-accent is what text, focus
       outlines and active borders take; on white that is near-black
       at 19.9:1, so unlike the champagne it replaced there is no
       second "safe" variant to remember and no contrast maths to do
       before using it. In dark it becomes white. Nothing else about
       these rules changes between the two builds. */
    --apo-accent:      var(--apo-gold-700);
    --apo-accent-hover:#000000;
    --apo-accent-fg:   #FFFFFF;
    --apo-accent-soft: #F1F2F3;
    --apo-accent-line: var(--apo-sand-400);

    /* Was PANTONE 466 C, held literal so the brand colour would not invert
       with the ramp. In a monochrome system the opposite is true: the mark
       IS ink, so it MUST invert — near-black on paper, white on the dark
       build — or half the product's badges disappear into their own
       background. Kept literal for the same reason as before: the value is
       a decision, not a step lookup. */
    --apo-brand-gold:      #16181C;
    --apo-brand-gold-fg:   #FFFFFF;
    --apo-brand-gold-hover:#000000;

    /* The primary call to action: black fill, white label, 19.4:1. This is
       the one place the monochrome system is unambiguously better than the
       colour one it replaced — the old champagne-under-slate pairing was
       numerically AA at 4.55:1 but both tones were mid-weight, so the label
       sat IN the fill rather than on it. Nothing reads more decisively than
       white on black, and a back office is judged on how fast its commit
       button is found.

       Hover goes UP, not down: a near-black fill has nowhere darker to go,
       so the lift is what registers as a response. Dark mode mirrors the
       whole arrangement — white fill, near-black label, hover steps down. */
    --apo-cta-bg:    #101215;
    --apo-cta-fg:    #FFFFFF;
    --apo-cta-hover: #292C31;
    --apo-cta-ring:  color-mix(in srgb, #FFFFFF 45%, transparent);

    /* 2027 command finish — flat and edge-lit. No button in this product
       casts a shadow, in either theme.

       What was here before floated: a two-layer drop shadow under the fill, a
       top-lit gradient across it, and a 1px lift on hover. All three are gone.
       Under a 32px control on a white clinical canvas a drop shadow reads as a
       web widget rather than as software — and three surfaces each shipped
       their own version of it (the pill, the icon button, the login submit),
       so one object had three depths depending on where you met it.

       Definition comes from the fill's own edge instead: --apo-cta-edge is the
       fill one step darker, which separates the button from paper the way a
       printed key line does. It holds at any zoom, survives a screenshot, and
       does not smear a grey halo across the row beneath it.

       The old --apo-button-shadow / -hover / -pressed / -surface-shadow tokens
       are deleted rather than set to none: a token named "shadow" that
       evaluates to nothing is an invitation to fill it back in. */
    --apo-cta-edge:       color-mix(in srgb, var(--apo-cta-bg) 82%, black);
    --apo-cta-edge-hover: color-mix(in srgb, var(--apo-cta-hover) 82%, black);

    /* Focus is the only light a button emits, and it is SOLID, not a wash.

       A box-shadow spread paints outside the border box, so the ring
       composites over the PAGE rather than over the button — which means a
       translucent one is measured against white. The champagne this
       replaced resolved to #C2B498 at 50% alpha and scored 2.04:1 on paper,
       under the 3:1 WCAG 2.4.11 asks of a focus indicator. Ink at full
       strength is 19.9:1 there and needs no arithmetic to stay legal.

       Slimness stays at 2px, matching --apo-ring. The difference between the
       two is deliberate: on an input the 1px border itself turns to ink and
       the halo only steadies it, so 14% is enough. A button's border does
       not change, so the ring has to carry the whole signal. */
    --apo-button-focus-ring: 0 0 0 2px var(--apo-accent);

    /* ONE filled-action colour. Filled Primary and Filled Secondary both
       resolve here; the names survive because ~40 files consume them, but
       they are one colour. */
    --apo-fill:        var(--apo-cta-bg);   /* filled buttons / avatar / toast ground */
    --apo-fill-hover:  var(--apo-cta-hover);
    --apo-fill-fg:     var(--apo-cta-fg);

    /* On a white ground the border IS the layout. Deliberately a step
       stronger than a typical hairline so card edges read at a glance. */
    --apo-border:        #E6E7EA;
    --apo-border-strong: #D3D5D9;
    --apo-surface-hover: #F5F6F7;
    --apo-surface-sunk:  #FAFAFB;

    /* --- Sidebar --------------------------------------------
       The sidebar used to be the one dark surface in light mode — a warm
       charcoal slab that existed to stop the app reading as a white admin
       starter. Monochrome removes the need for that trick and the cost that
       came with it: a permanent near-black column beside a white workspace
       is the highest-contrast edge on screen, sitting in peripheral vision
       for an eight-hour shift, and it forced every piece of nav chrome to
       be authored twice — once for the dark rail, once for the light page.

       It is a RECESSED PAPER PANEL now. One step off white, separated from
       the workspace by a hairline: the navigation reads as the quieter
       plane and the record being worked on is the brightest thing on the
       screen, which is the correct hierarchy for a clinical tool. The dark
       build inverts the same relationship — there the sidebar is the
       DEEPEST surface, for exactly the same reason.

       Selection draws a shape rather than a brightness: a soft ink wash on
       the row plus an edge marker flush to the panel's border. That frees
       the idle ramp to stay legible — every label >= 8:1, every icon >= 6:1,
       every section label >= 4.9:1. Contrast is not the accent's job. */
    --apo-nav-bg:        #F8F9FA;
    --apo-nav-bg-elev:   #F8F9FA;
    --apo-nav-border:    #E6E7EA;

    --apo-nav-fg:        #4A4D54;   /*  8.4:1 — idle labels        */
    --apo-nav-fg-muted:  #74777E;   /*  4.9:1 — section + status   */
    --apo-nav-icon:      #62656C;   /*  6.3:1 — idle icons, no opacity trick */
    --apo-nav-fg-strong: #08090A;
    --apo-nav-hover-bg:  rgba(8, 9, 11, 0.04);
    --apo-nav-hover-fg:  #16181C;
    --apo-nav-active-bg: rgba(8, 9, 11, 0.065);
    --apo-nav-active-fg: #08090A;
    --apo-nav-active-icon: #08090A;
    --apo-nav-marker:    #08090A;
    /* The sidebar's own scrollbar. It was two hard-coded rgba(255,255,255)
       literals, which is exactly the kind of thing that survives a re-skin
       and then paints a white bar down a white panel. */
    --apo-nav-scrollbar: rgba(8, 9, 11, 0.18);

    /* --- Status ---------------------------------------------
       The ONLY colour left in the product, which is what makes it
       carry information rather than decoration. Kept deliberately
       muted — a clinic ledger is not a dashboard demo — and each
       one meets AA against its own tinted ground.

       Info is neutral graphite on purpose: "here is a note" is not
       a state that needs a hue, and giving it one puts a fifth
       colour on screen competing with the three that matter. */
    --apo-success:  #16794A;
    --apo-success-bg: #EAF4EE;
    --apo-warning:  #8A5D0A;
    --apo-warning-bg: #F8F0DF;
    --apo-error:    #B3372F;
    --apo-error-bg: #FAEBEA;
    --apo-info:     #3F444C;
    --apo-info-bg:  #F1F2F3;

    /* --- Elevation ------------------------------------------
       Borders carry the hierarchy. Shadows exist only for things
       that genuinely float above the page, and they are a step
       lighter than they were: with the sidebar no longer supplying
       a dark anchor, a heavy shadow is the loudest thing on a white
       screen. Two layers still — a tight contact shadow that
       anchors the edge plus a wider ambient one — because a single
       blurred shadow on white just looks like haze. */
    --apo-shadow-xs: 0 1px 1px rgba(8, 9, 11, 0.035);
    --apo-shadow-sm: 0 1px 1px rgba(8, 9, 11, 0.04), 0 2px 6px rgba(8, 9, 11, 0.045);
    --apo-shadow-md: 0 1px 2px rgba(8, 9, 11, 0.05), 0 6px 16px rgba(8, 9, 11, 0.07);
    --apo-shadow-lg: 0 2px 6px rgba(8, 9, 11, 0.06), 0 16px 40px rgba(8, 9, 11, 0.13);

    /* The switch thumb's contact shadow — the ONE control-level shadow in the
       system, and a documented exception to "shadows only float": a disc
       travelling inside a filled pill has to sit visibly ON it, and on the
       checked track a hairline alone vanishes into the fill. See the SWITCHES
       block in mudblazor-overrides.css and design-system.md § Elevation. */
    --apo-shadow-switch-thumb: 0 1px 2.5px rgba(0, 0, 0, 0.24), 0 0 0 0.5px rgba(0, 0, 0, 0.06);

    /* Focus halo. 2px at 14% — a breath of ink around the 1px accent
       border, not a second border: a 3px/22% ring reads as a fat outline
       and makes every focused field look inflated. The border carries the
       signal; the ring only steadies it. */
    --apo-ring: 0 0 0 2px color-mix(in srgb, var(--apo-accent) 14%, transparent);

    /* --- Radii — small and consistent ----------------------- */
    /* Linear's scale. Crisp corners are half of what makes that product
       read precise — radii large enough to soften but never enough to read
       as pills. The two display steps came down (12→10, 16→14): at the new
       control size a 16px card corner beside an 8px button corner was two
       different softnesses in one frame. */
    --apo-radius-sm: 4px;
    --apo-radius-md: 6px;
    --apo-radius-lg: 8px;
    --apo-radius-xl: 10px;
    --apo-radius-2xl: 14px;   /* notice / syscard display corner */

    /* Auth card and photograph share the 32px card radius measured on
       https://claude.ai/login. */
    --apo-radius-plate: 32px;

    /* ONE radius for controls inside the back office — buttons, icon
       buttons, inputs, selects. Auth uses its own measured reference tokens.
       Buttons used to be stadium pills while the
       icon buttons and search fields beside them sat at 10px, so a page
       header put a lozenge next to a rounded rectangle and the row read as
       two design systems photographed together.

       8px, down from 10px, because the control height came down with it:
       the corner-to-height proportion is what has to hold, not the number.
       Constant across the size ladder — 28 / 32 / 40 — because a radius
       that scales makes small controls read rounder than large ones.

       --apo-radius-input is the older name for the same value and stays
       because the input rules already spell it that way. */
    --apo-radius-control: 8px;
    --apo-radius-input: var(--apo-radius-control);

    /* Pills and badges — avatars, status dots, counters, tags. 999px, not
       50%: 50% turns a rectangle into an ellipse; 999px caps the corners and
       keeps the sides straight. No COMMAND control uses this any more. */
    --apo-radius-full: 999px;

    /* --- Spacing — the 4/8 grid ------------------------------
       THE ONLY LEGAL GAPS AND PADDINGS. Same contract as the type ramp:
       do not write a raw padding/gap in a page file, pick a step. The
       register this system cites is built on this grid; before these
       tokens existed every component re-invented its own increments
       (7px rows, 11px card heads, 22/26/30px page paddings). */
    --apo-space-1: 4px;
    --apo-space-2: 8px;
    --apo-space-3: 12px;
    --apo-space-4: 16px;
    --apo-space-5: 20px;
    --apo-space-6: 24px;
    --apo-space-7: 32px;
    --apo-space-8: 40px;
    --apo-space-9: 48px;
    --apo-space-10: 64px;

    /* --- Motion — quick, never bouncy ----------------------- */
    --apo-ease: cubic-bezier(0.25, 0.8, 0.4, 1);
    --apo-duration-fast: 110ms;
    --apo-duration-base: 170ms;
    --apo-duration-slow: 280ms;

    /* ONE FAMILY. Display used to be Inter Tight — a second optical cut
       bought a slightly narrower headline and cost a 300KB webfont plus a
       seam wherever a display string sat beside a body one (page title over
       breadcrumb, stat value over its label). The reference product sets its
       headings in the same Inter as its rows and lets TRACKING carry the
       display register; that is what --apo-tracking-title/-display below are
       for, and it is why they tighten as the sizes grow. */
    --apo-font-body:    "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
    --apo-font-display: var(--apo-font-body);
    --apo-font-mono:    "JetBrains Mono", "SFMono-Regular", ui-monospace, Menlo, Consolas, monospace;
    /* Retained as a name only. The auth screen's engraved-serif welcome went
       with the champagne it was drawn to sit beside; nothing in the product
       requests a serif now. Left defined so a stale reference degrades to
       Georgia rather than to an invalid declaration. */
    --apo-font-serif:   Georgia, "Times New Roman", serif;

    /* --- Tracking — paired with the type ramp ----------------
       One tracking step per size tier, referenced from BOTH this file and
       Typography in ApolonijaTheme.cs — the same lockstep contract as the
       sizes. Before this, five named tokens were referenced 4 times while
       ~30 letter-spacing literals spanned 14 distinct values.

       These do more work than they used to: with one family across the
       whole ramp, tracking IS the display register. Inter set at 20px on
       body tracking reads as a big paragraph; at -0.022em it reads as a
       title.

       Documented optical exceptions, kept as literals at their rules:
         · nav idle -0.006em / active -0.002em — the active step compensates
           the 450→550 weight change so the row does not reflow
         · tabs active -0.017em — same weight compensation
         · table headers 0.055em — the tracked-caps header band
         · .apo-mono -0.02em — a non-Inter cut with its own optical size */
    --apo-tracking-caption:  0;
    --apo-tracking-body:     -0.011em;
    --apo-tracking-subtitle: -0.014em;
    --apo-tracking-title:    -0.022em;
    --apo-tracking-display:  -0.028em;
    --apo-tracking-widest:   0.09em;     /* eyebrows, tracked caps */

    /* Older names for the same three steps. */
    --apo-tracking-tighter: var(--apo-tracking-display);
    --apo-tracking-tight:   var(--apo-tracking-subtitle);
    --apo-tracking-normal:  var(--apo-tracking-caption);
    --apo-tracking-wide:    0.04em;

    /* --- Type ramp — Fluent 2 (Microsoft) -------------------
       THE ONLY LEGAL FONT SIZES IN THIS APP. Do not write a raw
       font-size in a page or component file; pick a step here.

       This replaced 31 distinct values across ~130 declarations.
       The old set had three text levels crammed into a 2px band
       (12.19 / 11.25 / 10.31px) which no reader can tell apart, so
       hierarchy was carried by weight and colour alone, and two
       components had quietly invented an 11-value scale of their
       own (0.6/0.64/0.66/0.68/0.7/0.72/0.76/0.84/0.9/0.92/0.95rem).

       Values are Fluent's, in px on purpose. The 15px root below is
       a deliberate density lever that tightens MudBlazor's internal
       rem sizing by ~6%; expressing the ramp in rem would have made
       every step a fraction of 15 (0.8125rem = 12.19px) and put us
       straight back where we started. px keeps the ramp exact and
       leaves the lever doing its job.

       Retuned to Linear's compact register: 13px is the working
       size, 14/600 heads a card, 16 heads a page. The top of the
       ramp came down a notch (title1 24→22) because with one family
       and tighter tracking a 24px head no longer needed the size to
       announce itself. */
    --apo-text-caption2:  11px;   /* eyebrows, tracked caps, badges  */
    --apo-text-caption1:  12px;   /* labels, table headers, meta     */
    --apo-text-body:      13px;   /* DEFAULT — cells, inputs, copy   */
    --apo-text-subtitle2: 14px;   /* card + dialog titles            */
    --apo-text-subtitle1: 16px;   /* page titles                     */
    --apo-text-title3:    18px;   /* section heroes                  */
    --apo-text-title2:    20px;   /* stat values                     */
    --apo-text-title1:    22px;   /* display                         */

    /* Fluent pairs each step with a fixed line height rather than a
       ratio, which is what keeps mixed-size rows sitting on a common
       baseline. Unitless would re-introduce per-size drift. */
    --apo-leading-caption2:  14px;
    --apo-leading-caption1:  16px;
    --apo-leading-body:      20px;
    --apo-leading-subtitle2: 22px;
    --apo-leading-subtitle1: 26px;
    --apo-leading-title3:    26px;
    --apo-leading-title2:    28px;
    --apo-leading-title1:    30px;

    /* Icon ramp — Fluent's 12/16/20/24. Icons are declared with
       font-size too, so they belong to the same contract. */
    --apo-icon-xs: 12px;
    --apo-icon-sm: 16px;
    --apo-icon-md: 20px;
    --apo-icon-lg: 24px;
    --apo-icon-xl: 32px;

    /* ONE control height for the whole product. Inputs were rendering at 54px
       on pages and 38px in dialogs — a 16px difference — because MudBlazor's
       Margin.Dense was applied inconsistently: ClinicProfile had it on 0 of 14
       fields, ManagedUserDialog on 12 of 17, LoyaltyRuleDialog on 0 of 8.

       Fixing that by adding Margin="Margin.Dense" to ~110 call sites would
       have left the next person to add a field free to forget it again. The
       height is enforced in CSS instead, so it holds regardless of the prop
       and applies to fields nobody has written yet.

       32px, down from 36px: the reference product's exact control height, and
       the size the inputs were ALREADY declaring internally — a 36px button
       standing beside a 32px field is the kind of half-step nobody names and
       everybody feels. Four pixels off every control is also the single
       cheapest density gain available to a back office whose users spend the
       day in tables. */
    --apo-control-h: 32px;
}


/* ============================================================
   DARK — the same product with the ink inverted. Near-black
   graphite, a white accent, borders at 8-13% white.

   Nothing structural differs from the light build: the sidebar is
   still the recessed plane, the CTA is still the highest-contrast
   object on screen, the accent is still ink. Only which end of the
   ramp counts as "ink" changes.
   ============================================================ */
[data-theme="dark"] {
    color-scheme: dark;

    /* SURFACE STEPS, measured in L* not contrast ratio — contrast ratio
       compresses to nothing down here (paper vs canvas was a "1.05:1" that
       tells you only that both are black-ish). In L* the old pair was 2.4
       apart and cards simply did not read as surfaces; a dark UI needs ~4-6.

       The sidebar is the deepest surface in the stack, which is the same
       role it plays on the light build — one step back from the workspace.

           sidebar  #08090A   L*  2.6   <- deepest, a distinct plane
           canvas   #0D0E10   L*  4.3
           paper    #17181B   L*  8.3   <- +4.0 over canvas, reads as a card */
    --apo-canvas:   #0D0E10;
    --apo-paper:    #17181B;
    --apo-ink-950:  #FFFFFF;
    --apo-ink-900:  #F4F5F6;
    --apo-ink-800:  #DDDEE1;
    --apo-ink-700:  #B6B8BD;
    --apo-ink-600:  #9498A0;
    --apo-ink-500:  #797D85;
    --apo-ink-400:  #5D6169;
    --apo-ink-300:  #444750;
    --apo-ink-200:  #2B2D33;
    --apo-ink-100:  #212328;
    --apo-ink-50:   #191A1E;
    --apo-ivory:    var(--apo-canvas);

    --apo-navy-950: #050607;
    --apo-navy-900: #0B0C0E;
    --apo-navy-800: #17181B;
    --apo-navy-700: #24262B;
    --apo-navy-100: #212328;
    --apo-navy-50:  #191A1E;

    /* The legacy champagne names, inverted with the ramp: what was the
       darkest step on white is the lightest step on black, so a rule that
       reached for gold-600 to carry text still gets something that carries
       text. */
    --apo-gold-700: #FFFFFF;
    --apo-gold-600: #F4F5F6;
    --apo-gold-500: #DDDEE1;
    --apo-gold-400: #9498A0;
    --apo-gold-300: #5D6169;
    --apo-gold-100: #2B2D33;
    --apo-gold-50:  #1B1D21;

    --apo-sand-400: #33363C;
    --apo-slate-500: #B6B8BD;

    --apo-teal-700: #DDDEE1;
    --apo-teal-600: #B6B8BD;
    --apo-teal-400: #4A4E56;
    --apo-teal-50:  #1B1D21;

    --apo-plum-700: #F4F5F6;
    --apo-plum-400: #6A6E77;
    --apo-plum-fg:  #DDDEE1;

    --apo-accent:      #FFFFFF;
    --apo-accent-hover:#FFFFFF;
    --apo-accent-fg:   #08090A;
    --apo-accent-soft: #1C1E22;
    --apo-accent-line: #33363C;

    /* White fill, near-black label — 18.6:1, the mirror of the light
       build's black-on-white and, like it, the most decisive object on the
       screen. Hover steps DOWN here for the same reason it steps up there:
       the fill is already at the end of the ramp. */
    --apo-cta-bg:    #FFFFFF;
    --apo-cta-fg:    #08090A;
    --apo-cta-hover: #E3E4E6;
    --apo-cta-ring:  color-mix(in srgb, #08090A 45%, transparent);
    /* The edge exists to separate a fill from its ground. A white rectangle
       on near-black separates itself, so both resolve to the fill and paint
       nothing — the token stays in the chain rather than being deleted so
       the button rules need no dark-mode branch. */
    --apo-cta-edge:       var(--apo-cta-bg);
    --apo-cta-edge-hover: var(--apo-cta-hover);
    --apo-button-focus-ring: 0 0 0 2px var(--apo-accent);

    --apo-brand-gold:      #FFFFFF;
    --apo-brand-gold-fg:   #08090A;
    --apo-brand-gold-hover:#E3E4E6;

    --apo-fill:        var(--apo-cta-bg);
    --apo-fill-hover:  var(--apo-cta-hover);
    --apo-fill-fg:     var(--apo-cta-fg);

    --apo-border:        #26282D;
    --apo-border-strong: #35383E;
    --apo-surface-hover: #1E2024;
    --apo-surface-sunk:  #121316;

    --apo-nav-bg:        #08090A;
    --apo-nav-bg-elev:   #0B0C0E;
    --apo-nav-border:    #1E2024;
    --apo-nav-fg:        #9DA2AA;   /*  7.9:1 */
    --apo-nav-fg-muted:  #7C818A;   /*  5.2:1 */
    --apo-nav-icon:      #8A8F98;   /*  6.2:1 */
    --apo-nav-fg-strong: #FFFFFF;
    --apo-nav-hover-bg:  rgba(255, 255, 255, 0.05);
    --apo-nav-hover-fg:  #F4F5F6;
    --apo-nav-active-bg: rgba(255, 255, 255, 0.08);
    --apo-nav-active-fg: #FFFFFF;
    --apo-nav-active-icon: #FFFFFF;
    --apo-nav-marker:    #FFFFFF;
    --apo-nav-scrollbar: rgba(255, 255, 255, 0.16);

    --apo-success:  #45BE7C;
    --apo-success-bg: #10251A;
    --apo-warning:  #D9A441;
    --apo-warning-bg: #251D0E;
    --apo-error:    #E4796F;
    --apo-error-bg: #2A1513;
    --apo-info:     #9CA3AE;
    --apo-info-bg:  #1B1D21;

    --apo-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
    --apo-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
    --apo-shadow-switch-thumb: 0 1px 2.5px rgba(0, 0, 0, 0.45), 0 0 0 0.5px rgba(0, 0, 0, 0.2);
    --apo-shadow-md: 0 8px 20px rgba(0, 0, 0, 0.55), 0 2px 4px rgba(0, 0, 0, 0.4);
    --apo-shadow-lg: 0 24px 56px rgba(0, 0, 0, 0.7), 0 4px 12px rgba(0, 0, 0, 0.5);
}

/* ============================================================
   ACCOUNT DROPDOWN
   The person card in the top-right corner. The head is the person — an ink
   circle with reversed initials (the same pairing as the primary action),
   name, and the role set as an eyebrow. The hairline under it fades out the
   way the sign-in masthead rule does, so the card shares the product’s
   editorial hand rather than a component library’s.

   These rules used to sit INSIDE the :root block above, where CSS nesting
   quietly resolved them as `:root .apo-account-pop`. They matched, so
   nothing looked wrong — but a token block is not a place to keep
   component CSS, and the next person to reorder :root would have deleted a
   working component by accident.
   ============================================================ */
.apo-account-pop {
    border: 1px solid var(--apo-border) !important;
    border-radius: var(--apo-radius-lg) !important;
    box-shadow: var(--apo-shadow-lg) !important;
    background: var(--apo-paper);
    min-width: 264px;
    overflow: hidden;
}

.apo-account-pop__list {
    padding: 6px !important;
}

.apo-account__menu-head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 10px 11px;
}

.apo-account__menu-avatar {
    width: 40px;
    height: 40px;
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--apo-brand-gold);
    color: var(--apo-brand-gold-fg);
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-body);
    font-weight: 650;
    letter-spacing: 0.01em;
    line-height: 1;
}

.apo-account__menu-copy {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

.apo-account__menu-name {
    color: var(--apo-ink-900);
    font-size: var(--apo-text-body);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-body);
    line-height: 1.2;
}

.apo-account__menu-role {
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption2);
    font-weight: 650;
    letter-spacing: var(--apo-tracking-widest);
    text-transform: uppercase;
    line-height: 1.2;
}

.apo-account__menu-mail {
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.apo-account__menu-rule {
    height: 1px;
    margin: 0 10px 6px;
    background: linear-gradient(90deg,
        var(--apo-accent-line) 0%,
        color-mix(in srgb, var(--apo-accent-line) 40%, transparent) 55%,
        transparent 100%);
}

.apo-account-pop__list .mud-menu-item,
.apo-account-pop__list .mud-list-item {
    border-radius: var(--apo-radius-md);
    min-height: 36px;
    padding: 0 10px;
}

.apo-account-pop__list .mud-menu-item:hover,
.apo-account-pop__list .mud-list-item:hover {
    background: var(--apo-surface-hover);
}

.apo-account__menu-item {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    font-size: var(--apo-text-body);
    color: var(--apo-ink-800);
}

.apo-account__menu-item svg {
    width: 16px;
    height: 16px;
    flex: 0 0 auto;
    color: var(--apo-ink-400);
    transition: color var(--apo-duration-fast) var(--apo-ease);
}

.apo-account-pop__list .mud-menu-item:hover .apo-account__menu-item svg,
.apo-account-pop__list .mud-list-item:hover .apo-account__menu-item svg {
    color: var(--apo-ink-700);
}

/* ============================================================
   REDUCED MOTION — one guard for every tokenized transition.
   Because motion is expressed through the three duration tokens,
   collapsing them here silences the whole product at once —
   0.01ms rather than 0 so `transitionend` handlers still fire.
   Keyframe animations declare their own durations, so any
   animation must still add its own guard (the reconnect halo
   below does).
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
    :root {
        --apo-duration-fast: 0.01ms;
        --apo-duration-base: 0.01ms;
        --apo-duration-slow: 0.01ms;
    }
}

/* ============================================================
   BASE
   15px root is the density lever: it tightens MudBlazor's internal
   rem-based sizing — 59 font-sizes and 40 padding/height values —
   by ~6% without touching a single page file.

   It no longer sets OUR text sizes. Those come from the --apo-text-*
   ramp, which is in px precisely so this lever can keep compressing
   MudBlazor without dragging the ramp off Fluent's values with it.
   Do not "simplify" this to 16px: that is a ~7% inflation of every
   MudBlazor control in the product.
   ============================================================ */
html {
    font-size: 15px;
}

/* font-size MUST NOT be set here. This rule also matches <html>, and it comes
   after the lever above, so anything it declares silently becomes the rem
   basis for the entire app — MudBlazor included.

   The old value hid this: `0.9375rem` on the root element resolves against the
   INITIAL font-size (16px), not the lever, so it landed on exactly 15px and
   looked like it agreed. Swapping in a 14px ramp token moved the root to 14px
   and shrank every MudBlazor control by ~7%. Body text is set on <body> alone,
   below. */
html, body {
    margin: 0;
    padding: 0;
    height: 100%;
    background: var(--apo-canvas);
    color: var(--apo-ink-900);
    font-family: var(--apo-font-body);
    font-weight: 400;
    line-height: 1.55;
    letter-spacing: var(--apo-tracking-body);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
    font-feature-settings: "cv05", "cv11", "ss03";
    font-variant-ligatures: contextual common-ligatures;
}

/* <body> only — see the warning above. MudBlazor also sets body font-size from
   Typography.Default, which points at this same token, so the two agree. */
body {
    font-size: var(--apo-text-body);
}

h1, h2, h3, h4, h5, h6, .apo-display {
    font-family: var(--apo-font-display);
    color: var(--apo-ink-900);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-tight);
    line-height: 1.25;
}

h1 { font-size: var(--apo-text-title3);   letter-spacing: var(--apo-tracking-tighter); }
h2 { font-size: var(--apo-text-subtitle1);  }
h3 { font-size: var(--apo-text-subtitle2); }
h4 { font-size: var(--apo-text-body); }

/* Eyebrows are quiet metadata now — not decorative small-caps. */
.apo-eyebrow {
    font-family: var(--apo-font-body);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: var(--apo-tracking-widest);
    font-size: var(--apo-text-caption1);
    color: var(--apo-ink-500);
    line-height: 1;
}

.apo-numeric {
    font-family: var(--apo-font-display);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    letter-spacing: var(--apo-tracking-tight);
    font-feature-settings: "tnum";
}

button,
input,
textarea,
select {
    font: inherit;
}

/* Focus fallback for anything the design system has not dressed by hand.
   :where() keeps it at zero specificity so a real component rule always wins.

   It used to also set `border-radius: var(--apo-radius-sm)`, which was a
   quiet disaster: :where() contributes nothing, so the compound scored the
   same (0,1,0) as `.mud-icon-button` — and app.css loads after
   mudblazor-overrides.css, so it won the tie. Every icon button in the
   product snapped from a 10px corner to a 4px one the instant it took
   keyboard focus, which is both the wrong shape and a geometry change on a
   state. The declaration was never needed anyway: `outline` follows the
   element's own border-radius in every browser this app supports. */
:where(a, button, input, textarea, select, [tabindex]):focus-visible {
    outline: 2px solid var(--apo-accent);
    outline-offset: 1px;
}

/* RAMP EXCEPTION, deliberate. em, not a ramp step: JetBrains Mono runs
   optically larger than Inter at the same size, so this trims 5% off
   whatever it is nested in. A fixed step would break that relationship at
   every size it is used inside. */
.apo-mono {
    font-family: var(--apo-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 0.95em;
    letter-spacing: -0.02em;
}

/* Keyboard hint chip — used by the search trigger and shortcuts. */
.apo-kbd {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 1px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-sm);
    background: var(--apo-paper);
    color: var(--apo-ink-500);
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-caption1);
    font-weight: 500;
    line-height: 1;
}

/* Suppress the default focus outline on elements focused programmatically via
   Blazor's <FocusOnNavigate />. Real keyboard focus rings are preserved via
   :focus-visible on interactive controls. */
[tabindex="-1"]:focus,
[tabindex="-1"]:focus-visible {
    outline: none;
    box-shadow: none;
}

/* Scrollbars — thin, appear on hover of the scrolling region. */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
    background: transparent;
    border-radius: 10px;
    border: 3px solid transparent;
    background-clip: content-box;
}
*:hover > ::-webkit-scrollbar-thumb,
::-webkit-scrollbar-thumb:hover {
    background: var(--apo-ink-300);
    background-clip: content-box;
}

/* ============================================================
   FRAMEWORK SURFACES
   Blazor's own error bar and reconnect dialog. Both are rendered
   outside the component tree, so they never inherit the MudTheme
   and have to be dressed by hand or they arrive as unstyled
   browser defaults.
   ============================================================ */

/* ============================================================
   SYSTEM MESSAGE CARDS
   Everything the app says about ITSELF — an unhandled error, a
   newer build going live — rather than about the user's data.

   One card, two tones. The previous error surface was a slim dark
   bar with the message and a "refresh" link squeezed onto one line;
   it read as browser chrome rather than as this product, and its
   only offer was to make the user do the recovery by hand.

   The card is the same object as the reconnect dialog, scaled down:
   the clinic's emblem in a ringed disc on the left, a tracked eyebrow
   naming the kind of message, the message itself, and — because these
   all resolve by reloading — a countdown that performs the reload
   instead of asking for it.

   State is the 6px dot beside the eyebrow, the same instrument as the
   reconnect dialog, the system notice and every status in the tables.
   It used to be a red gradient seam across the card's top edge plus a
   tinted ring around the emblem — severity painted onto the chrome,
   which is exactly the register the rest of the family gave up.
   ============================================================ */

/* The lane keeps both cards in one column above the page. It is
   pointer-transparent so an empty lane never swallows a click, and it
   sits bottom-CENTRE because snackbars own the bottom-right corner. */
.apo-syslane {
    position: fixed;
    left: 50%;
    bottom: 20px;
    transform: translateX(-50%);
    z-index: 3000;
    display: flex;
    /* column-reverse: whichever card appeared last sits nearest the
       bottom edge, so a new message never pushes an older one under
       the cursor that was about to dismiss it. */
    flex-direction: column-reverse;
    align-items: stretch;
    gap: 10px;
    width: min(430px, calc(100vw - 32px));
    pointer-events: none;
}

.apo-syscard {
    display: none;
    position: relative;
    box-sizing: border-box;
    align-items: flex-start;
    gap: 13px;
    padding: 15px 16px;
    pointer-events: auto;
    background: var(--apo-paper);
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-xl);
    box-shadow: var(--apo-shadow-lg);
    color: var(--apo-ink-900);
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-body);
    line-height: 1.45;
    overflow: hidden;
    animation: apo-syscard-in var(--apo-duration-slow) var(--apo-ease) both;
}

/* MudBlazor.min.css ships its own `#blazor-error-ui` rule — a full-width red
   slab pinned to the bottom edge at z-index 9999 — and an ID selector outranks
   a class one no matter the source order. Every property it sets has to be
   named here or the card inherits half of it: the emblem and the copy would
   sit on Material's error red. Same lesson as the reconnect dialog below. */
#blazor-error-ui.apo-syscard {
    position: static;
    left: auto;
    bottom: auto;
    width: auto;
    z-index: auto;
    padding: 15px 16px;
    background-color: var(--apo-paper);
    color: var(--apo-ink-900);
    box-shadow: var(--apo-shadow-lg);
}

/* Blazor shows its error element by writing display:block onto the style
   attribute, so only an attribute-level selector can win. The update card
   is ours and uses the hidden attribute instead. */
#blazor-error-ui.apo-syscard[style*="display: block"],
#blazor-error-ui.apo-syscard[style*="display:block"],
#apo-update-notice.apo-syscard:not([hidden]),
#apo-draft-restored.apo-syscard:not([hidden]) {
    display: flex !important;
}

@keyframes apo-syscard-in {
    from { opacity: 0; transform: translateY(10px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .apo-syscard {
        animation: none;
    }
}

/* --- the emblem, in a disc that carries the tone ------------------- */
.apo-syscard__brand {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    margin-top: 1px;
    border-radius: var(--apo-radius-full);
    border: 1px solid var(--apo-border);
    background: var(--apo-surface-sunk);
}

.apo-syscard__emblem {
    display: block;
    border-radius: 50%;
}

/* --- copy ---------------------------------------------------------- */
.apo-syscard__body {
    flex: 1;
    min-width: 0;
}

.apo-syscard__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption2);
    font-weight: 500;
    letter-spacing: 0.09em;
    text-transform: uppercase;
}

/* THE ONE MARK THAT CARRIES STATE — see the reconnect dialog. Neutral for
   the calm messages (a newer build, a restored draft); the error card's is
   drawn in the error tone, and it breathes while the card is counting down
   to the reload it is about to perform. */
.apo-syscard__dot {
    flex: 0 0 auto;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--apo-ink-300);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-ink-300) 20%, transparent);
}

.apo-syscard--alert .apo-syscard__dot {
    background: var(--apo-error);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-error) 22%, transparent);
}

.apo-syscard:has(.apo-syscard__meta:not([hidden])) .apo-syscard__dot {
    animation: apo-reconnect-pulse 1.6s var(--apo-ease) infinite;
}

@media (prefers-reduced-motion: reduce) {
    .apo-syscard__dot {
        animation: none !important;
    }
}

.apo-syscard__title {
    margin: 3px 0 0;
    font-family: var(--apo-font-display);
    font-size: var(--apo-text-subtitle2);
    font-weight: 600;
    letter-spacing: -0.022em;
    line-height: 1.3;
    color: var(--apo-ink-900);
}

.apo-syscard__meta {
    margin: 7px 0 0;
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    font-variant-numeric: tabular-nums;
}

/* Shown instead of the countdown when a form is dirty: the automatic
   reload is held so typed-but-unsubmitted work survives. Reassurance,
   not a warning — so it sits in the neutral register the reconnect
   dialog's rescue note settled, not an amber panel. */
.apo-syscard__note {
    margin: 9px 0 0;
    padding: 7px 9px;
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-md);
    background: var(--apo-surface-sunk);
    color: var(--apo-ink-600);
    font-size: var(--apo-text-caption1);
    line-height: 1.5;
}

/* --- actions ------------------------------------------------------- */
.apo-syscard__actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
}

#blazor-error-ui .apo-syscard__action,
#apo-update-notice .apo-syscard__action {
    display: inline-flex;
    align-items: center;
    height: 30px;
    padding: 0 14px;
    border: 1px solid var(--apo-cta-edge);
    border-radius: var(--apo-radius-control);
    background: var(--apo-cta-bg);
    color: var(--apo-cta-fg);
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-caption1);
    font-weight: 500;
    line-height: 1;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--apo-duration-fast) var(--apo-ease),
                border-color var(--apo-duration-fast) var(--apo-ease);
}

#blazor-error-ui .apo-syscard__action:hover,
#apo-update-notice .apo-syscard__action:hover {
    background: var(--apo-cta-hover);
    border-color: var(--apo-cta-edge-hover);
}

/* Stopping the automatic refresh is the exception, not the expected path. */
.apo-syscard__ghost {
    display: inline-flex;
    align-items: center;
    height: 30px;
    padding: 0 12px;
    border: 1px solid var(--apo-border-strong);
    border-radius: var(--apo-radius-control);
    background: transparent;
    color: var(--apo-ink-600);
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-caption1);
    font-weight: 500;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--apo-duration-fast) var(--apo-ease),
                color var(--apo-duration-fast) var(--apo-ease);
}

.apo-syscard__ghost:hover {
    background: var(--apo-surface-hover);
    color: var(--apo-ink-900);
}

#blazor-error-ui .apo-syscard__dismiss {
    position: absolute;
    top: 9px;
    right: 10px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: var(--apo-radius-md);
    color: var(--apo-ink-400);
    font-size: var(--apo-text-subtitle1);
    line-height: 1;
    cursor: pointer;
    transition: background var(--apo-duration-fast) var(--apo-ease),
                color var(--apo-duration-fast) var(--apo-ease);
}

#blazor-error-ui .apo-syscard__dismiss:hover {
    background: var(--apo-surface-hover);
    color: var(--apo-ink-900);
}

@media (max-width: 600px) {
    .apo-syslane {
        width: calc(100vw - 24px);
        bottom: calc(12px + env(safe-area-inset-bottom));
    }
}

/* ============================================================
   THE SYSTEM NOTICE — /error, the permission wall, signing out.

   NoticePanel.razor, centred alone on SystemLayout's stage. A system message
   is still a dialog — the same rule that rebuilt the reconnect overlay below —
   so this is the product's dialog verbatim: title rail, body, footer rail.
   What was here before was a card leading with a badge-stamped emblem under a
   red gradient strip, parked in the sign-in split's left column; every
   framework's default error page, wearing our logo.

   State lives in ONE place: the 6px dot on the rail, the same instrument as
   every status in the tables. Severity never touches the card's chrome.

   Global rather than scoped on purpose: the pages that supply the footer
   actions render them in their own scope, and this family — .apo-notice,
   .apo-reconnect, .apo-syscard — should read from one sheet anyway.
   ============================================================ */
.apo-notice {
    width: 100%;
    background: var(--apo-paper);
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-2xl);
    box-shadow: var(--apo-shadow-lg);
    overflow: hidden;
    text-align: left;
    animation: apo-notice-in var(--apo-duration-slow) var(--apo-ease) both;
}

/* The one entrance in the product's vocabulary: a short rise as the card
   takes the stage. Nothing moves after that. */
@keyframes apo-notice-in {
    from {
        opacity: 0;
        transform: translateY(5px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .apo-notice {
        animation: none !important;
    }
}

/* --- title rail — recessed ground, hairline, the same object as a dialog
       title and a table's column header. --- */
.apo-notice__rail {
    display: flex;
    align-items: center;
    gap: 9px;
    min-height: 44px;
    padding: 10px 16px;
    background: var(--apo-surface-sunk);
    border-bottom: 1px solid var(--apo-border);
}

.apo-notice__emblem {
    flex: 0 0 auto;
    display: block;
}

/* BrandVariant.Adaptive ships both emblem cuts; restated here so the theme
   reveals exactly one (see the identical note on the reconnect dialog). */
.apo-notice__emblem.apo-brand-logo--dark {
    display: none;
}

[data-theme="dark"] .apo-notice__emblem.apo-brand-logo--light {
    display: none;
}

[data-theme="dark"] .apo-notice__emblem.apo-brand-logo--dark {
    display: block;
}

.apo-notice__rail-title {
    flex: 1 1 auto;
    min-width: 0;
    color: var(--apo-ink-700);
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-caption1);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-caption);
    line-height: 1.3;
}

/* THE ONE MARK THAT CARRIES STATE. Same 6px dot with a 2px halo as the
   reconnect dialog and every status in the tables. */
.apo-notice__dot {
    flex: 0 0 auto;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--apo-ink-300);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-ink-300) 20%, transparent);
}

.apo-notice--warning .apo-notice__dot {
    background: var(--apo-warning);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-warning) 22%, transparent);
}

.apo-notice--error .apo-notice__dot {
    background: var(--apo-error);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-error) 22%, transparent);
}

/* While the page is counting down to its own retry, the dot breathes — the
   card is doing something, and this is the one pixel allowed to say so. */
html[data-apo-notice-refresh="counting"] .apo-notice__dot {
    animation: apo-reconnect-pulse 1.6s var(--apo-ease) infinite;
}

@media (prefers-reduced-motion: reduce) {
    html[data-apo-notice-refresh="counting"] .apo-notice__dot {
        animation: none !important;
    }
}

/* --- body --- */
.apo-notice__main {
    padding: 22px 20px 24px;
}

.apo-notice__title {
    font-family: var(--apo-font-display);
    font-size: var(--apo-text-title3);
    font-weight: 600;
    letter-spacing: -0.022em;
    line-height: 1.3;
    color: var(--apo-ink-900);
    margin: 0;
}

.apo-notice__body {
    color: var(--apo-ink-600);
    font-size: var(--apo-text-body);
    line-height: 1.6;
    margin: 8px 0 0;
    max-width: 46ch;
}

/* The held-refresh note. Reassurance, not a warning — the reload waits so
   typed work survives — so it sits in the neutral register the reconnect
   dialog's rescue note settled, not an amber panel. */
.apo-notice__note {
    margin: 16px 0 0;
    padding: 9px 11px;
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-md);
    background: var(--apo-surface-sunk);
    color: var(--apo-ink-600);
    font-size: var(--apo-text-caption1);
    line-height: 1.5;
}

/* --- footer rail — the same object as a dialog's action footer --- */
.apo-notice__footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 12px;
    flex-wrap: wrap;
    padding: 12px 16px;
    border-top: 1px solid var(--apo-border);
    background: var(--apo-surface-sunk);
}

/* The countdown cluster keeps the rail's left edge; actions keep the right. */
.apo-notice__auto {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-right: auto;
}

.apo-notice__actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

/* Which of the countdown parts is on screen is decided by CSS alone, keyed
   off a data attribute circuit.js writes on <html>. Two things rule out the
   obvious `hidden` attribute: Blazor's first interactive render replaces this
   subtree and would restore whatever the markup said, and MudBlazor ships
   `[hidden] { display: none !important }`, which no author rule can reopen.
   The state lives outside the render tree, so neither can touch it. */
.apo-notice__meta,
.apo-notice__stop,
.apo-notice__note {
    display: none;
}

html[data-apo-notice-refresh="counting"] .apo-notice__meta { display: block; }
html[data-apo-notice-refresh="counting"] .apo-notice__stop { display: inline-flex; }
html[data-apo-notice-refresh="held"] .apo-notice__note { display: block; }

.apo-notice__meta {
    margin: 0;
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    font-variant-numeric: tabular-nums;
    line-height: 1.3;
}

/* Stopping the countdown is a footnote-sized decision: a quiet text control
   beside the number it silences, not a third button in the action row. */
.apo-notice__stop {
    align-items: center;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--apo-ink-600);
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-caption1);
    font-weight: 500;
    line-height: 1.3;
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, var(--apo-ink-600) 35%, transparent);
    text-underline-offset: 3px;
    cursor: pointer;
    transition: color var(--apo-duration-fast) var(--apo-ease);
}

.apo-notice__stop:hover {
    color: var(--apo-ink-900);
}

/* --- the footer's buttons. Pages pass these as plain anchors, so the pair
       is drawn here once for the whole family: a filled primary and a quiet
       hairline neighbour, the reconnect dialog's exact pair. --- */
.apo-notice__action,
.apo-notice__ghost {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: var(--apo-control-h);
    padding: 0 14px;
    border-radius: var(--apo-radius-control);
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-body);
    font-weight: 500;
    letter-spacing: var(--apo-tracking-body);
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--apo-duration-fast) var(--apo-ease),
                border-color var(--apo-duration-fast) var(--apo-ease);
}

.apo-notice__action {
    border: 1px solid var(--apo-cta-edge);
    background: var(--apo-cta-bg);
    color: var(--apo-cta-fg);
}

.apo-notice__action:hover {
    background: var(--apo-cta-hover);
    border-color: var(--apo-cta-edge-hover);
    text-decoration: none;
}

.apo-notice__ghost {
    border: 1px solid var(--apo-border);
    background: var(--apo-paper);
    color: var(--apo-ink-900);
}

.apo-notice__ghost:hover {
    background: var(--apo-surface-hover);
    border-color: var(--apo-border-strong);
    text-decoration: none;
}

.apo-notice__action:focus-visible,
.apo-notice__ghost:focus-visible,
.apo-notice__stop:focus-visible {
    outline: none;
    box-shadow: var(--apo-button-focus-ring);
}

@media (max-width: 600px) {
    .apo-notice__main {
        padding: 20px 16px 22px;
    }

    .apo-notice__footer {
        flex-direction: column-reverse;
        align-items: stretch;
    }

    .apo-notice__auto {
        margin-right: 0;
        justify-content: space-between;
    }

    .apo-notice__actions {
        width: 100%;
    }

    .apo-notice__actions .apo-notice__action,
    .apo-notice__actions .apo-notice__ghost {
        flex: 1 1 auto;
    }
}

/* ============================================================
   THE SESSION DIALOG

   Every selector below is ID-scoped. The framework ships its own stylesheet for
   #components-reconnect-modal, and an ID selector outranks a class one no
   matter the source order — a plain .apo-reconnect rule lost, leaving the
   dialog on Blazor's opaque white backdrop at z-index 9999.

   IT IS THE PRODUCT'S DIALOG, not a bespoke one. Title rail, body, footer rail:
   the same three bands, the same 40px rails, the same recessed grounds and
   hairlines as every editor in the app. What was here before was a centred
   96px haloed emblem over centred copy with centred buttons — which made the
   one surface a user meets when something has gone wrong the ONLY surface in
   the product that did not look like the rest of it. A system message is still
   a dialog.

   State lives in ONE place: a 6px dot on the rail. Three states, one mark that
   changes colour, rather than three blocks each drawing their own indicator —
   which is how the previous version ended up with two of its three states
   looking identical.
   ============================================================ */
#components-reconnect-modal.apo-reconnect {
    display: none;
    position: fixed;
    inset: 0;
    align-items: center;
    justify-content: center;
    padding: 24px;
    backdrop-filter: blur(2px);
    /* !important is required, not sloppiness: MudBlazor.min.css ships
       `#components-reconnect-modal { z-index: 9999 !important;
       background-color: var(--mud-palette-background) !important; }`, which
       painted an opaque panel over the whole viewport. Specificity cannot beat
       !important, so these two declarations have to match it. */
    background: color-mix(in srgb, var(--apo-ink-950) 45%, transparent) !important;
    z-index: 3100 !important;
}

/* Blazor drives the dialog purely by toggling these classes on the element. */
#components-reconnect-modal.apo-reconnect.components-reconnect-show,
#components-reconnect-modal.apo-reconnect.components-reconnect-retrying,
#components-reconnect-modal.apo-reconnect.components-reconnect-failed,
#components-reconnect-modal.apo-reconnect.components-reconnect-rejected {
    display: flex;
}

#components-reconnect-modal.apo-reconnect.components-reconnect-hide {
    display: none;
}

#components-reconnect-modal .apo-reconnect__dialog {
    width: 100%;
    max-width: 400px;
    background: var(--apo-paper);
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-lg);
    box-shadow: var(--apo-shadow-lg);
    overflow: hidden;
    text-align: left;
}

/* --- title rail — the same object as a dialog title and a table's column
       header: recessed ground, hairline, 40px. --- */
#components-reconnect-modal .apo-reconnect__rail {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 40px;
    padding: 8px 14px;
    background: var(--apo-surface-sunk);
    border-bottom: 1px solid var(--apo-border);
}

#components-reconnect-modal .apo-reconnect__emblem {
    flex: 0 0 auto;
    display: block;
}

/* BrandVariant.Adaptive ships both emblem cuts and lets the global
   .apo-brand-logo--light/--dark pair reveal one. That pair is class-level and
   the rule above is ID-level, so without these the dialog drew BOTH marks side
   by side. Restated at ID level, the theme picks one again. */
#components-reconnect-modal .apo-reconnect__emblem.apo-brand-logo--dark {
    display: none;
}

[data-theme="dark"] #components-reconnect-modal .apo-reconnect__emblem.apo-brand-logo--light {
    display: none;
}

[data-theme="dark"] #components-reconnect-modal .apo-reconnect__emblem.apo-brand-logo--dark {
    display: block;
}

#components-reconnect-modal .apo-reconnect__rail-title {
    flex: 1 1 auto;
    min-width: 0;
    color: var(--apo-ink-700);
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-caption1);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-caption);
    line-height: 1;
}

/* THE ONE MARK THAT CARRIES STATE. Same 6px dot with a 2px halo as every
   status in the tables, so "connection" reads on the same instrument as
   "invoice paid" — a system this small should not own two status languages. */
#components-reconnect-modal .apo-reconnect__dot {
    flex: 0 0 auto;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--apo-ink-300);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-ink-300) 20%, transparent);
    transition: background var(--apo-duration-base) var(--apo-ease);
}

#components-reconnect-modal.components-reconnect-show .apo-reconnect__dot,
#components-reconnect-modal.components-reconnect-retrying .apo-reconnect__dot {
    background: var(--apo-accent);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-accent) 20%, transparent);
    animation: apo-reconnect-pulse 1.6s var(--apo-ease) infinite;
}

#components-reconnect-modal.components-reconnect-failed .apo-reconnect__dot {
    background: var(--apo-warning);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-warning) 22%, transparent);
    animation: apo-reconnect-pulse 2.4s var(--apo-ease) infinite;
}

@keyframes apo-reconnect-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
    #components-reconnect-modal .apo-reconnect__dot {
        animation: none !important;
    }
}

/* --- body --- */
#components-reconnect-modal .apo-reconnect__body {
    position: relative;
    padding: 16px 14px 18px;
}

/* One block per Blazor state; the parent's class decides which is shown. */
#components-reconnect-modal .apo-reconnect__block {
    display: none;
    flex-direction: column;
}

#components-reconnect-modal.components-reconnect-show .apo-reconnect__block--retrying,
#components-reconnect-modal.components-reconnect-retrying .apo-reconnect__block--retrying,
#components-reconnect-modal.components-reconnect-failed .apo-reconnect__block--failed,
#components-reconnect-modal.components-reconnect-rejected .apo-reconnect__block--rejected {
    display: flex;
}

#components-reconnect-modal .apo-reconnect__title {
    font-family: var(--apo-font-display);
    font-size: var(--apo-text-subtitle2);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-subtitle);
    color: var(--apo-ink-900);
    margin: 0;
    line-height: var(--apo-leading-subtitle2);
}

#components-reconnect-modal .apo-reconnect__text {
    color: var(--apo-ink-600);
    font-size: var(--apo-text-body);
    line-height: 1.55;
    margin: 4px 0 0;
}

/* Blazor's attempt counter. Quiet metadata, not a headline number. */
#components-reconnect-modal .apo-reconnect__attempt {
    margin: 10px 0 0;
    color: var(--apo-ink-400);
    font-size: var(--apo-text-caption1);
    font-variant-numeric: tabular-nums;
}

/* Collapse it when Blazor has not populated the counters, so an orphan slash
   never shows. */
#components-reconnect-modal .apo-reconnect__attempt:not(:has(#components-reconnect-current-attempt:not(:empty))) {
    display: none;
}

#components-reconnect-modal .apo-reconnect__attempt-sep {
    color: var(--apo-ink-300);
    margin: 0 2px;
}

#components-reconnect-modal .apo-reconnect__countdown {
    margin: 10px 0 0;
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    font-variant-numeric: tabular-nums;
}

/* THE PROMISE, not a warning. This element used to be an amber panel saying
   the refresh had been CANCELLED because of unsaved work and that the user
   should copy it out by hand — the last warm-toned surface in the product and
   the worst sentence in it. Same element, opposite meaning: the typing is
   snapshotted and comes back, so it reads as reassurance and is drawn in the
   neutral register everything else uses. */
#components-reconnect-modal .apo-reconnect__rescue {
    margin: 12px 0 0;
    padding: 8px 10px;
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-md);
    background: var(--apo-surface-sunk);
    color: var(--apo-ink-600);
    font-size: var(--apo-text-caption1);
    line-height: 1.5;
}

/* An indeterminate hairline for the two states that are waiting on something.
   A bar rather than a spinner: it says a process is running without implying
   the user is being made to wait for work of their own. */
#components-reconnect-modal .apo-reconnect__progress {
    display: none;
    position: relative;
    height: 2px;
    margin: 16px 0 0;
    overflow: hidden;
    border-radius: var(--apo-radius-full);
    background: var(--apo-ink-100);
}

#components-reconnect-modal.components-reconnect-show .apo-reconnect__progress,
#components-reconnect-modal.components-reconnect-retrying .apo-reconnect__progress,
#components-reconnect-modal.components-reconnect-failed .apo-reconnect__progress {
    display: block;
}

#components-reconnect-modal .apo-reconnect__progress span {
    position: absolute;
    inset: 0 auto 0 0;
    width: 38%;
    border-radius: inherit;
    background: var(--apo-accent);
    animation: apo-reconnect-sweep 1.5s var(--apo-ease) infinite;
}

#components-reconnect-modal.components-reconnect-failed .apo-reconnect__progress span {
    background: var(--apo-warning);
    animation-duration: 2.6s;
}

@keyframes apo-reconnect-sweep {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(365%); }
}

@media (prefers-reduced-motion: reduce) {
    #components-reconnect-modal .apo-reconnect__progress span {
        animation: none !important;
        width: 100%;
        opacity: 0.35;
    }
}

/* --- footer rail — the same object as a dialog's action footer --- */
#components-reconnect-modal .apo-reconnect__footer {
    display: none;
    align-items: center;
    justify-content: flex-end;
    padding: 12px 14px;
    border-top: 1px solid var(--apo-border);
    background: var(--apo-surface-sunk);
}

/* Retrying has no footer: there is nothing useful to press while Blazor is
   already doing the only thing that helps, and an inert button row is how a
   dialog teaches people that its buttons do not matter. */
#components-reconnect-modal.components-reconnect-failed .apo-reconnect__footer,
#components-reconnect-modal.components-reconnect-rejected .apo-reconnect__footer {
    display: flex;
}

#components-reconnect-modal .apo-reconnect__actions {
    display: none;
    align-items: center;
    gap: 8px;
}

#components-reconnect-modal.components-reconnect-failed .apo-reconnect__actions--failed,
#components-reconnect-modal.components-reconnect-rejected .apo-reconnect__actions--rejected {
    display: flex;
}

/* Both buttons need MudBlazor's `#components-reconnect-modal button` rule
   neutralised — it uppercases the label and forces `margin: 40px auto
   !important`, which centred them and broke this row. Spacing lives on the
   flex container instead. */
#components-reconnect-modal .apo-reconnect__actions .apo-reconnect__action,
#components-reconnect-modal .apo-reconnect__actions .apo-reconnect__cancel {
    margin: 0 !important;
    text-transform: none;
    letter-spacing: var(--apo-tracking-body);
    line-height: 1;
    min-width: 0;
    height: var(--apo-control-h);
    padding: 0 14px;
    border-radius: var(--apo-radius-control);
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-body);
    font-weight: 500;
    cursor: pointer;
    transition: background var(--apo-duration-fast) var(--apo-ease),
                border-color var(--apo-duration-fast) var(--apo-ease);
}

/* Quieter than the primary: waiting is the exception, not the expected path. */
#components-reconnect-modal .apo-reconnect__cancel {
    border: 1px solid var(--apo-border);
    background: var(--apo-paper);
    color: var(--apo-ink-900);
}

#components-reconnect-modal .apo-reconnect__cancel:hover {
    background: var(--apo-surface-hover);
    border-color: var(--apo-border-strong);
}

#components-reconnect-modal .apo-reconnect__action {
    border: 1px solid var(--apo-cta-edge);
    background: var(--apo-cta-bg);
    color: var(--apo-cta-fg);
}

#components-reconnect-modal .apo-reconnect__action:hover {
    background: var(--apo-cta-hover);
    border-color: var(--apo-cta-edge-hover);
}

#components-reconnect-modal .apo-reconnect__action:focus-visible,
#components-reconnect-modal .apo-reconnect__cancel:focus-visible {
    outline: none;
    box-shadow: var(--apo-button-focus-ring);
}

@media (max-width: 600px) {
    #components-reconnect-modal .apo-reconnect__footer {
        flex-direction: column-reverse;
        align-items: stretch;
    }

    #components-reconnect-modal .apo-reconnect__actions {
        width: 100%;
    }

    #components-reconnect-modal .apo-reconnect__actions .apo-reconnect__action,
    #components-reconnect-modal .apo-reconnect__actions .apo-reconnect__cancel {
        flex: 1 1 auto;
    }
}

.apo-authorizing-pane {
    min-height: 240px;
}

/* ============================================================
   UTILITY BITS
   ============================================================ */
/* Cards no longer lift on hover — they change border tone. Motion
   is reserved for things that actually respond to you. */
.apo-card-lift {
    transition: border-color var(--apo-duration-fast) var(--apo-ease),
                background var(--apo-duration-fast) var(--apo-ease);
}

.apo-card-lift:hover {
    border-color: var(--apo-border-strong) !important;
}

/* THE BRAND KEEPS ITS COLOUR. The monochrome system governs the CHROME —
   surfaces, type, controls, status — not the clinic's own mark.

   These were desaturated for a while, on the argument that a gold serif in the
   masthead above a black-and-white workspace reads as two brands. The argument
   was wrong about which of the two is the guest: the interface is the thing
   that should stay quiet, and the emblem and wordmark are the one place the
   clinic is entitled to be itself. PANTONE 466 C over slate 7540 C, exactly as
   the brand book publishes it and exactly as it appears on the door.

   Nothing here filters, tints or recolours. If a mark ever needs to sit on a
   ground it cannot survive, ship the cut that suits that ground — see
   BrandLogo.razor's light/dark pairs — rather than draining the artwork. */
.apo-brand-logo {
    display: block;
}

/* BrandVariant.Adaptive renders both emblem cuts; the theme picks one. */
.apo-brand-logo--dark { display: none; }
[data-theme="dark"] .apo-brand-logo--light { display: none; }
[data-theme="dark"] .apo-brand-logo--dark { display: block; }

/* ============================================================
   SHARED SHELL LAYOUT
   ============================================================ */
.apo-page-container {
    max-width: 1440px;
    margin: 0 auto;
    /* 40px gutters at full width — the reference's lists breathe at the
       edges, and the removed card chrome returns nothing else to the margin. */
    padding: var(--apo-space-7) var(--apo-space-8) var(--apo-space-10);
}

@media (max-width: 1440px) {
    .apo-page-container {
        padding: 24px 28px 56px;
    }
}

@media (max-width: 1200px) {
    .apo-page-container {
        padding: 22px 22px 48px;
    }
}

@media (max-width: 768px) {
    .apo-page-container {
        padding: 18px 16px 40px;
    }
}

/* Ensure MudBlazor picks up the global font stack even where its CSS vars leak through. */
.mud-typography, .mud-input, .mud-button, .mud-table, .mud-chip, .mud-nav-link {
    font-family: var(--apo-font-body) !important;
}

/* Topbar control visibility on narrow viewports. .apo-topbar__search,
   __notifications and __theme are applied to <MudTextField> / <MudIconButton>
   child components whose roots aren't reachable by Blazor scoped CSS, so the
   rules have to live here. */
/* The panel glyph is visible at EVERY width since 2026-08: on desktop it
   folds the sidebar to the 56px icon rail (Ctrl/Cmd+\ does the same), below
   1024px it opens the off-canvas drawer — one control, two meanings, the
   viewport decides. Desktop sizing lives in TopBar.razor.css. */
.apo-topbar__menu-btn {
    display: inline-flex !important;
}

@media (max-width: 1023px) {
    /* Search lives in the drawer's masthead on mobile, beside the brand — the
       bar keeps only the panel toggle, the emblem and the account cluster. */
    .apo-topbar__search-trigger {
        display: none !important;
    }
}

@media (max-width: 1023px) {
    /* The language switcher is a MudMenu activator and the avatar lives in a
       child component, so their mobile sizing has to be global too. */
    .apo-topbar__right .apo-lang-switcher .mud-button-root {
        min-height: 40px;
    }

    /* One ancestor deeper than the scoped rules: the component stylesheet
       loads after this file, and at equal specificity it was winning the tie
       and holding the avatar at its desktop 30px. */
    .apo-topbar .apo-topbar__account .apo-account--compact {
        padding: 4px;
    }

    .apo-topbar .apo-topbar__account .apo-account__avatar {
        width: 32px;
        height: 32px;
    }
}

@media (max-width: 720px) {
    .apo-topbar__theme {
        display: none !important;
    }
}

/* ============================================================
   CARD STYLES — applied to <MudPaper>-rooted components.
   These CANNOT live in component-scoped .razor.css files because
   Blazor scoped CSS adds its [b-xxxxx] attribute only to plain
   HTML elements inside the component, not to child-component roots.
   ============================================================ */

/* --- KPI stat tile ---
   Flat, bordered, dense. The number is the only thing with weight. */
/* THE METRIC TILE, rebuilt.

   What was here is the tile every generated dashboard ships: an icon in a
   rounded grey well, a label beside it, a number under it, a tinted delta
   chip. Four of them in a row across the top of a page. It is not wrong so
   much as anonymous — you have seen this exact object in every admin
   template, which is precisely why it cannot read as a designed thing.

   Rebuilt as a reading, not a widget: the number leads at display size, the
   label is quiet metadata above it, the icon is a bare 14px glyph rather than
   a boxed one, and the delta drops its tint for a coloured arrow. No shadow —
   the tile is a frame on the page, the same as every other surface. */
.apo-stat-card {
    padding: 13px 16px 14px !important;
    background: var(--apo-paper) !important;
    border-radius: var(--apo-radius-lg) !important;
    display: flex;
    flex-direction: column;
    gap: 0;
    min-height: 0 !important;
    border: 1px solid var(--apo-border) !important;
    box-shadow: none;
    position: relative;
    overflow: hidden;
}

.apo-stat-card__head {
    display: flex;
    align-items: center;
    gap: 8px;
    position: relative;
    z-index: 1;
}

/* Bare glyph, no well. The rounded grey square around a 16px icon is pure
   packaging: it adds a second rectangle, a second radius and a second surface
   tone to a tile whose whole job is to show one number. The icon still marks
   which metric this is; it just stops announcing itself. */
.apo-stat-card__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--apo-ink-400);
    flex: 0 0 auto;
}

.apo-stat-card__icon .mud-icon-root {
    font-size: 15px !important;
}

.apo-stat-card__label {
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    font-weight: 500;
    line-height: 1.3;
    letter-spacing: var(--apo-tracking-caption);
}

/* The number is the tile. It was one step above its own label (20px over
   13px), which is not a hierarchy, it is a rounding error — the tile read as
   a sentence with a slightly larger word in it. At the top of the ramp over a
   12px label it reads as a figure being reported. */
.apo-stat-card__value {
    font-family: var(--apo-font-display);
    font-size: var(--apo-text-title1);
    font-weight: 600;
    color: var(--apo-ink-900);
    line-height: 1.15;
    letter-spacing: var(--apo-tracking-display);
    font-variant-numeric: tabular-nums;
    margin-top: 10px;
    position: relative;
    z-index: 1;
}

.apo-stat-card__footer {
    display: flex;
    align-items: center;
    gap: var(--apo-space-2);
    flex-wrap: nowrap;
    font-size: var(--apo-text-caption1);
    color: var(--apo-ink-500);
    margin-top: 8px;
    position: relative;
    z-index: 1;
    min-width: 0;
}

/* No tint behind the delta. A tinted pill is a third rectangle inside a tile
   that already has a border and a sparkline band, and the colour was doing
   the work twice — the arrow already says which way it went. Colour on the
   glyph and the figure, nothing behind them. */
.apo-stat-card__delta {
    display: inline-flex;
    align-items: center;
    gap: 1px;
    font-weight: 550;
    padding: 0;
    font-size: var(--apo-text-caption1);
    letter-spacing: var(--apo-tracking-caption);
    line-height: 1.5;
    font-variant-numeric: tabular-nums;
    flex: 0 0 auto;
}

.apo-stat-card__delta .mud-icon-root {
    font-size: var(--apo-icon-xs) !important;
}

.apo-stat-card__delta--up {
    color: var(--apo-success);
}

.apo-stat-card__delta--down {
    color: var(--apo-error);
}

.apo-stat-card__caption {
    color: var(--apo-ink-500);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

/* Trend line, not a trend PANEL. The framed footer band made sense when each
   tile was its own bordered box — the band gave the chart a floor. In the
   metric strip it does the opposite: only some readings carry a sparkline, so
   the band appeared under two of four segments and read as two grey
   rectangles hanging off the bottom of the panel, which looks like a
   rendering fault rather than a chart.

   Bare line on the tile's own ground instead. A tile with a trend has one; a
   tile without simply has nothing there, and nobody can tell that something
   is missing — which is the only way an optional element can sit inside a
   shared frame. */
.apo-stat-card__sparkline {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 34px;
    padding: 0;
    z-index: 0;
    pointer-events: none;
    line-height: 0;
    background: transparent;
    overflow: hidden;
}

.apo-stat-card:has(.apo-stat-card__sparkline) {
    padding-bottom: 40px !important;
}

@media (max-width: 768px) {
    .apo-stat-card__value {
        font-size: var(--apo-text-title3);
    }
}

/* ============================================================
   THE METRIC STRIP

   Four numbers across the top of a page is the correct information
   design — it is the PACKAGING that gives every generated dashboard
   away. Four separate bordered boxes with four radii and three gaps
   between them means eight vertical edges and four rounded corner
   pairs devoted to saying "these are four things", which the eye
   already knew from the four numbers.

   One instrument panel instead: a single frame, hairline dividers
   between the readings, no gaps. It is the same device a printed
   report or a trading terminal uses, it costs less ink, and it says
   these figures belong to one page rather than being four widgets
   that happen to be adjacent.

   Dividers are drawn with inset box-shadow rather than borders, so
   they survive the tiles' own `border: 0 !important` and never
   change anyone's border-box. The children may be an <a> wrapper, a
   MudPaper or a plain skeleton <div> depending on the page and the
   loading state, so everything here targets `> *` and lets the tile
   inside stay whatever it is.
   ============================================================ */
.apo-metric-strip {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0;
    margin-bottom: 16px;
    background: var(--apo-paper);
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-lg);
    overflow: hidden;
}

/* THE DIVIDER IS A BORDER ON THE STRIP'S CHILD, not an inset shadow and not
   a border on the tile. An inset shadow was the first attempt and it drew
   nothing: the tile inside paints an opaque --apo-paper ground, and a child's
   background covers its parent's inset shadow. A border on the tile is worse
   — the tile's own `border: 0 !important` reset would have to be fought for
   every breakpoint.

   Every child of the strip is therefore a wrapper (an <a> drill-through, or a
   plain <div> around a loading skeleton), never a bare .apo-stat-card. That
   invariant is what keeps these rules to three lines each; if you add a tile
   here, wrap it. */
/* Three readings rather than four. Stated as a modifier rather than letting
   the grid auto-fit, because a strip that silently reflows to whatever it is
   given is a strip whose segments change width between two pages showing the
   same kind of number. */
.apo-metric-strip--three {
    grid-template-columns: repeat(3, minmax(0, 1fr));
}

.apo-metric-strip > * {
    min-width: 0;
}

.apo-metric-strip > * + * {
    border-left: 1px solid var(--apo-border);
}

/* The tiles give up their own frame — the strip owns it now. */
.apo-metric-strip .apo-stat-card {
    border: 0 !important;
    border-radius: 0 !important;
    box-shadow: none;
    height: 100%;
}

/* With no border of its own left to darken, a hoverable tile answers with
   its ground instead — still exactly one property, as the hover doctrine
   requires. */
.apo-metric-strip .apo-card-lift:hover {
    background: var(--apo-surface-hover) !important;
}

.apo-metric-strip .apo-stat-card__link:focus-visible {
    border-radius: 0;
    outline-offset: -2px;
}

/* Two up: the divider moves to the second column and a rule appears under the
   first row. Written as explicit nth-child cases because a tile can need one,
   the other, or both. */
@media (max-width: 1200px) {
    .apo-metric-strip,
    .apo-metric-strip--three {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .apo-metric-strip > * + * {
        border-left: 0;
    }

    .apo-metric-strip > *:nth-child(2n) {
        border-left: 1px solid var(--apo-border);
    }

    .apo-metric-strip > *:nth-child(n + 3) {
        border-top: 1px solid var(--apo-border);
    }
}

@media (max-width: 640px) {
    .apo-metric-strip,
    .apo-metric-strip--three {
        grid-template-columns: minmax(0, 1fr);
    }

    .apo-metric-strip > *:nth-child(2n) {
        border-left: 0;
    }

    .apo-metric-strip > * + * {
        border-top: 1px solid var(--apo-border);
    }
}

/* --- Dashboard cards (schedule, activity, chart) --- */
.apo-dash-card {
    background: var(--apo-paper) !important;
    border: 1px solid var(--apo-border) !important;
    border-radius: var(--apo-radius-lg) !important;
    padding: 0 !important;
    box-shadow: none !important;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

/* A CARD'S NAME SITS ON THE CARD'S OWN GROUND, under a hairline. It used to
   take a recessed band, on the argument that a card head and a table's column
   header are one object; they are not. The column header names the COLUMNS —
   it belongs to the grid and is read against its rows, so it keeps its
   recessed ground. This names the CARD, and a filled strip capping a bordered
   white box is the stock generated-dashboard header, in this product a
   sidebar of five of them stacked. The hairline stays: it is what separates
   the name from the content, and above a grid it is what keeps the card's
   name and the column names from merging into one ambiguous band of grey. */
.apo-dash-card__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--apo-space-4);
    padding: 10px var(--apo-space-4);
    min-height: 42px;
    border-bottom: 1px solid var(--apo-border);
}

.apo-dash-card__title {
    color: var(--apo-ink-900);
    font-family: var(--apo-font-display);
    font-size: var(--apo-text-body);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-subtitle);
    line-height: 1.3;
    margin: 0;
}

.apo-dash-card__subtitle {
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    line-height: 1.4;
    margin: 2px 0 0;
}

.apo-dash-card__body {
    padding: 6px 6px 8px;
}

.apo-dash-card__live-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 7px;
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-full);
    background: var(--apo-paper);
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    font-weight: 500;
    letter-spacing: var(--apo-tracking-caption);
}

.apo-dash-card__live-badge span {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--apo-success);
}

.apo-dash-chart__total {
    font-size: var(--apo-text-body);
    color: var(--apo-ink-900);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: var(--apo-tracking-title);
}

.apo-dash-chart__summary {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* Untinted, for the same reason as the stat tile's delta. */
.apo-dash-chart__trend {
    color: var(--apo-success);
    padding: 0;
    font-size: var(--apo-text-caption1);
    font-weight: 550;
    font-variant-numeric: tabular-nums;
}

/* A falling comparison must not wear the success colour. */
.apo-dash-chart__trend--down {
    color: var(--apo-error);
}

.apo-dash-appointments__list,
.apo-dash-activity__list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
}

/* Dense 36px rows with a monospaced time gutter — reads like a
   schedule, scans like a table. */
.apo-dash-appointments__item {
    display: grid;
    grid-template-columns: 44px 22px minmax(0, 1fr) auto;
    align-items: center;
    gap: 10px;
    padding: var(--apo-space-2);
    border-radius: var(--apo-radius-md);
    transition: background var(--apo-duration-fast) var(--apo-ease);
}

.apo-dash-appointments__item:hover {
    background: var(--apo-surface-hover);
}

.apo-dash-appointments__time {
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    font-weight: 400;
    font-variant-numeric: tabular-nums;
}

.apo-dash-appointments__avatar {
    width: 22px !important;
    height: 22px !important;
    font-size: var(--apo-text-caption2) !important;
    font-weight: 500 !important;
    background: var(--apo-surface-sunk) !important;
    color: var(--apo-ink-600) !important;
    border: 1px solid var(--apo-border);
}

.apo-dash-appointments__who {
    min-width: 0;
    display: flex;
    align-items: baseline;
    gap: 8px;
}

.apo-dash-appointments__patient {
    font-weight: 500;
    color: var(--apo-ink-900);
    font-size: var(--apo-text-body);
    letter-spacing: var(--apo-tracking-body);
    line-height: 1.4;
    white-space: nowrap;
}

.apo-dash-appointments__meta {
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.apo-dash-activity__item {
    display: grid;
    grid-template-columns: 22px minmax(0, 1fr) auto;
    align-items: center;
    gap: 10px;
    padding: var(--apo-space-2);
    border-radius: var(--apo-radius-md);
    transition: background var(--apo-duration-fast) var(--apo-ease);
}

.apo-dash-activity__item:hover {
    background: var(--apo-surface-hover);
}

.apo-dash-activity__icon {
    color: var(--apo-ink-500) !important;
    font-size: var(--apo-icon-sm) !important;
}

.apo-dash-activity__icon-wrap {
    width: 22px;
    height: 22px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--apo-radius-sm);
    background: var(--apo-surface-sunk);
    border: 1px solid var(--apo-border);
    flex: 0 0 auto;
}

.apo-dash-activity__copy {
    min-width: 0;
}

.apo-dash-activity__text {
    color: var(--apo-ink-800);
    font-size: var(--apo-text-body);
    line-height: 1.45;
    letter-spacing: var(--apo-tracking-body);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.apo-dash-activity__time {
    color: var(--apo-ink-400);
    font-size: var(--apo-text-caption1);
    letter-spacing: var(--apo-tracking-caption);
    white-space: nowrap;
}

/* --- List-page data card (MudPaper wrapping a MudDataGrid) ---
   Framed again, by request: the full-bleed experiment left list pages
   without a single drawn edge, and the product reads more premium with
   every table set in a rounded hairline card — the register Claude's data
   surfaces use, and the one the login card already established here. The
   toolbar header, quiet column labels and row treatments from the redesign
   all stay; only the frame returns. */
/* THE FRAME STAYS; THE FLOAT GOES.

   A data card is a region of the page, not an object resting on it. With a
   drop shadow it hovered — a 1200px-wide slab casting a grey halo down the
   canvas, which at this size reads as a widget embedded in a page rather than
   as the page's own structure. The border is the frame, and a frame is what
   was wanted; the shadow was the part that made it look pasted on.

   Radius comes down with it. 10px on an object this large reads soft; 8px is
   the same corner the cards, popovers and dialogs take, so every rectangle in
   the product now shares one geometry. */
.apo-data-card {
    background: var(--apo-paper) !important;
    border: 1px solid var(--apo-border) !important;
    border-radius: var(--apo-radius-lg) !important;
    overflow: hidden;
    box-shadow: none;
    min-width: 0;
}

/* ============================================================
   LOCKED VALUE

   A value the platform shows but does not own — see LockedField.razor
   for why it is not a disabled input.

   The ground is the same recessed tone a disabled field takes, so the
   two read as the same STATE even though they are different objects;
   the difference is that this one is sized by its content and can
   never hide any of it.
   ============================================================ */
.apo-locked {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

.apo-locked__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    min-width: 0;
}

/* Matches .mud-input-label exactly, so a locked field and the editable
   field under it sit on one label line. */
.apo-locked__label {
    color: var(--apo-ink-700);
    font-size: var(--apo-text-caption1);
    font-weight: 500;
    line-height: var(--apo-leading-caption1);
    letter-spacing: var(--apo-tracking-caption);
}

.apo-locked__owner {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: 0 0 auto;
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption2);
    line-height: 1;
    white-space: nowrap;
}

.apo-locked__lock {
    width: 11px;
    height: 11px;
    fill: currentColor;
    opacity: 0.75;
}

.apo-locked__value {
    padding: 8px 12px;
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-control);
    background: var(--apo-surface-sunk);
    color: var(--apo-ink-800);
    font-size: var(--apo-text-body);
    line-height: 1.55;
    /* The whole point: sized by content, wrapping rather than clipping,
       and breaking a long unbroken string instead of scrolling it out
       of the box. */
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* A record's title carries the same weight it would in the editable
   field, so switching between locked and editable does not resize the
   page's first line. */
.apo-locked--display .apo-locked__value {
    font-size: var(--apo-text-title3);
    font-weight: 650;
    letter-spacing: var(--apo-tracking-title);
    line-height: var(--apo-leading-title3);
}

.apo-locked__empty {
    color: var(--apo-ink-400);
    font-style: italic;
}

.apo-locked__hint {
    margin: 0;
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    line-height: var(--apo-leading-caption1);
}

/* ============================================================
   THE CARD HEAD

   A titled data card was assembled by hand on every page that had one:
   a <MudText Typo="Typo.h6"> wrapped in a MudStack here, a d-flex with
   justify-space-between there, mb-1 / mb-3 / mb-4 underneath depending
   on the file, and the card itself carrying pa-5 or pa-6 — or, on
   Family accounts, NO padding at all, which put the title hard against
   the card's top-left corner and its count chip against the top-right.
   That is the bug this exists to make impossible: the head is no longer
   something a page composes, so a page cannot compose it wrongly.

   It is the SAME OBJECT as .apo-dash-card__head: the card's NAME, on
   the card's own paper, under a hairline. It is deliberately NOT the
   same object as a table's column header or a dialog's title bar,
   which keep their recessed ground — those two name something other
   than the card they sit in (the columns below, the window around),
   and a filled strip is how a page says "this band is chrome, not
   content". A card's own name is content.

   The recessed band was the first attempt and it is what makes a
   sidebar of five panels read as five generated widgets: white box,
   grey cap, white box, grey cap. The hairline alone carries the
   separation, and above a grid it still keeps the card's name from
   merging with the column names underneath it.

   The rail is full bleed, so the CARD carries no padding. Content that
   is not a grid goes in .apo-card-body; a grid needs nothing, because a
   grid's own cells already carry the gutter.
   ============================================================ */
.apo-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-height: 42px;
    padding: 10px 14px;
    border-bottom: 1px solid var(--apo-border);
}

.apo-card-head__titles {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.apo-card-head__title {
    color: var(--apo-ink-900);
    font-family: var(--apo-font-display);
    font-size: var(--apo-text-body);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-subtitle);
    line-height: var(--apo-leading-body);
}

.apo-card-head__desc {
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    line-height: var(--apo-leading-caption1);
}

/* The accessory: a count, a status, a filter, a button. Never wider than
   it needs to be, so the title keeps the room it needs to truncate in. */
.apo-card-head__actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 0 0 auto;
    flex-wrap: wrap;
    justify-content: flex-end;
}

/* Anything in a card that is NOT a full-bleed grid. */
.apo-card-body {
    padding: 16px 14px;
}

.apo-card-body > * + * {
    margin-top: 14px;
}

/* A head sitting directly on a grid KEEPS its rule. Dropping it was the
   first attempt and the card's name and the column names ran together
   into a single header of ambiguous shape. They are two different
   statements — what this card is, and what these columns are — so the
   hairline between them is doing real work, not decoration. */

@media (max-width: 600px) {
    .apo-card-head {
        flex-wrap: wrap;
        align-items: flex-start;
    }

    .apo-card-head__actions {
        width: 100%;
        justify-content: flex-start;
    }
}

/* Let the grid scroll horizontally inside its region on narrow screens,
   rather than forcing the whole viewport to scroll. */
.apo-data-card .mud-data-grid,
.apo-data-card .mud-table-container {
    overflow-x: auto;
}

.apo-data-card .mud-table-container {
    scrollbar-width: thin;
}

/* Plain <MudTable> pages take the same hairline row rules as the grid;
   MudBlazor's default palette line is replaced by the system border. */
.apo-data-card .mud-table-root .mud-table-cell {
    border-bottom: 1px solid var(--apo-border);
}

.apo-data-card .mud-table-root tr:last-child .mud-table-cell {
    border-bottom: none;
}

/* --- Sidebar scroll region ---
   .apo-nav__list is applied to <MudNavMenu>, a CHILD COMPONENT root, so the
   scoped rules in NavMenu.razor.css never matched it — including overflow-y.
   The list therefore grew to its full content height (1101px) inside a 731px
   sidebar, and the drawer's overflow:hidden clipped the Settings group and the
   whole account/sign-out footer off-screen, with no way to reach them.

   min-height:0 is the load-bearing line: a flex child defaults to
   min-height:auto and refuses to shrink below its content, so flex:1 alone
   would not have produced a scroll region. */
.apo-nav__list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    /* 22px of air below the masthead rule. The brand block stays pinned at 52px
       so it keeps its shared baseline with the top bar; the breathing room goes
       here instead, where it separates the wordmark from the first row without
       breaking that alignment. */
    padding: 22px 0 16px;
    /* End edge only: rows are full bleed and the selected row's marker must sit
       flush against the sidebar's left edge, so no gutter may be reserved there.
       "stable" still prevents a reflow when the scrollbar appears. */
    scrollbar-gutter: stable;
    scrollbar-width: thin;
    scrollbar-color: var(--apo-nav-scrollbar) transparent;
}

.apo-nav__list::-webkit-scrollbar {
    width: 8px;
}

.apo-nav__list::-webkit-scrollbar-track {
    background: transparent;
}

.apo-nav__list::-webkit-scrollbar-thumb {
    background: var(--apo-nav-scrollbar);
    border-radius: 999px;
    border: 2px solid transparent;
    background-clip: content-box;
}

.apo-nav__list::-webkit-scrollbar-thumb:hover {
    background: color-mix(in srgb, var(--apo-nav-scrollbar) 100%, var(--apo-nav-fg) 40%);
    background-clip: content-box;
}

/* --- Key/value reference table ---
   Used by the Settings pages via <MudSimpleTable Class="apo-kv-table">. The
   class was referenced by three call sites but had never been defined, so
   these tables fell back to raw MudBlazor defaults. */
.apo-kv-table table {
    width: 100%;
}

.apo-kv-table table td {
    padding: 7px 0 !important;
    border-bottom: 1px solid var(--apo-border) !important;
    font-size: var(--apo-text-body);
    line-height: 1.45;
    vertical-align: top;
}

.apo-kv-table table tr:last-child td {
    border-bottom: none !important;
}

/* Label column: fixed measure so values align into a true second column
   instead of ragging against variable-length labels. */
.apo-kv-table table td:first-child {
    color: var(--apo-ink-500);
    width: 40%;
    padding-right: 14px !important;
    font-weight: 400;
}

.apo-kv-table table td:last-child {
    color: var(--apo-ink-900);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}

@media (max-width: 600px) {
    .apo-kv-table table td:first-child {
        width: 45%;
    }
}

/* --- Empty state --- */
/* AN EMPTY LIST IS NOT AN EVENT. This was 56px of padding around a 92px
   drawing with a 130px glow behind it — a full-page announcement that a table
   has no rows yet, on a screen someone opens forty times a day. The hero
   empty state is a marketing device; in an operational tool it is the most
   over-designed object in the product, and over-design reads as generated
   just as reliably as under-design does.

   Same drawing, told quietly: two-thirds the size, a third of the air, and
   the copy tightened onto one measure. It still says what is missing and how
   to add it — it just stops making an occasion of it. */
.apo-empty-state {
    padding: 40px 32px !important;
    text-align: center;
    background: var(--apo-paper) !important;
    border-radius: var(--apo-radius-lg) !important;
    border: 1px solid var(--apo-border) !important;
    display: flex !important;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    max-width: 480px;
    margin: 0 auto;
    box-shadow: none !important;
}

/* The artwork is line engraving, so it is sized rather than boxed — the 36px
   grey square this replaced made the glyph read as a disabled button. 68px is
   large enough for the ring and its motif to resolve as a drawing and small
   enough to stay an illustration rather than a hero. */
.apo-empty-art {
    width: 68px;
    height: 68px;
    margin-bottom: 2px;
    overflow: visible;
    color: var(--apo-ink-400);
}

/* One stroke weight across the whole set — the ring and the motif are drawn
   with the same pen, which is what makes ten different drawings look like one
   family. vector-effect keeps that true at any rendered size. */
.apo-empty-art__line,
.apo-empty-art__ring {
    stroke: currentColor;
    stroke-width: 1.25;
    stroke-linecap: round;
    stroke-linejoin: round;
    fill: none;
    vector-effect: non-scaling-stroke;
}

/* The ring sits back so the motif reads first; the inner arc sits back further
   still, which is what gives the pair its engraved depth. */
.apo-empty-art__ring { opacity: 0.42; }
.apo-empty-art__ring--inner { opacity: 0.22; }

/* Full-strength ink, once per drawing. Everything else in the frame is drawn
   at --apo-ink-400 or dimmed further, so this one stroke is the darkest thing
   in it and decides where the eye finishes — on the tick, the second face, the
   last line of the page. It was 466 C; a single saturated stroke was the
   monochrome system's last hold-out, and value carries the same emphasis. */
.apo-empty-art__gold {
    stroke: var(--apo-brand-gold);
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    fill: none;
    vector-effect: non-scaling-stroke;
}

/* A wash behind the drawing, sized to the ring rather than the box, so the
   artwork sits on the page instead of on a panel. */
.apo-empty-state__art {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;
}

.apo-empty-state__art::before {
    content: "";
    position: absolute;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: radial-gradient(circle,
        color-mix(in srgb, var(--apo-brand-gold) 6%, transparent) 0%,
        transparent 68%);
}

.apo-empty-state h2 {
    margin: 0;
    font-size: var(--apo-text-body);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-subtitle);
}

.apo-empty-state__body {
    color: var(--apo-ink-500);
    margin: 0;
    max-width: 320px;
    line-height: 1.5;
    font-size: var(--apo-text-caption1);
}

.apo-empty-state__action {
    margin-top: 6px;
}

/* When EmptyState is used as a data grid's NoRecordsContent it is already
   inside .apo-data-card, so its own border/shadow/background produced a card
   floating inside a card. Strip the chrome and let it be plain content. */
.mud-data-grid .apo-empty-state,
.mud-table .apo-empty-state,
.apo-data-card .apo-empty-state,
.apo-dash-card .apo-empty-state {
    border: none !important;
    box-shadow: none !important;
    background: transparent !important;
    padding: 34px 24px !important;
    max-width: none;
}

/* MudBlazor's table cell typography leaks into the nested copy and renders it
   heavy; the body must stay quiet against the title. */
.mud-data-grid .apo-empty-state__body,
.mud-table .apo-empty-state__body,
.apo-data-card .apo-empty-state__body,
.apo-dash-card .apo-empty-state__body {
    font-weight: 400 !important;
    color: var(--apo-ink-500) !important;
}



/* ============================================================
   SIGN-IN FORM SURFACES
   Shared by Login, ForgotPassword and ResetPassword. These began in
   Login.razor.css, which scopes them to that one component — the moment
   two more pages reused the classes the rules silently stopped applying
   to them. Anything used by more than one component belongs here.
   ============================================================ */
/* One MEASURE for everything that stacks under the head — card, error
   alert, back link. The head is free to run wider (see .apo-auth__column);
   these three must share an edge or the stack reads ragged. */
.apo-login {
    --apo-login-measure: 464px;
    /* Auth controls match https://claude.ai/login. */
    --apo-login-control-radius: 10px;
    --apo-login-control-h: 40px;
    --apo-login-button-font-size: 15px;
    --apo-login-field-ink: #0b0b0b;
    --apo-login-field-fill: rgb(255 255 255 / 50%);
    --apo-login-field-placeholder: #898781;
    --apo-login-field-focus-surface: #fcfcfb;
    --apo-login-field-focus-glow: #cde2fb;
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
}

[data-theme="dark"] .apo-login {
    --apo-login-field-ink: var(--apo-ink-900);
    --apo-login-field-fill: color-mix(in srgb, var(--apo-paper) 50%, transparent);
    --apo-login-field-placeholder: var(--apo-ink-400);
    --apo-login-field-focus-surface: var(--apo-paper);
    --apo-login-field-focus-glow: rgb(42 120 214 / 35%);
}

/* --- heading (outside the card) ------------------------------------
   The welcome is the page's one display moment. It used to be an engraved
   serif at up to 46px, drawn to sit beside the champagne; a serif is a
   voice, and this product no longer has one to match it. Inter at a real
   display size with display tracking says the same thing in the product's
   own hand — and the sign-in screen is the first thing anyone sees, so it
   is the last place the type should disagree with the app behind it.

   CENTRED over the card, which reverses the 2026-08 call to left-align it
   with the fields. Alen asked for the reference composition verbatim, and
   that composition is symmetrical: brand pinned to the corner, then one
   centred axis running welcome → subtitle → card. The "two starting
   points" objection held when the head was set at 32px and sat close to
   the fields; at display size with real air under it the head reads as
   the page's title rather than as the form's first line. */
.apo-login__head {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 10px;
    margin-bottom: 36px;
    width: 100%;
}

/* RAMP EXCEPTION, deliberate: the auth display sits above the app's title
   steps the way the wordmark artwork does — it is staging, not UI. */
.apo-login__title {
    font-family: var(--apo-font-display);
    font-size: clamp(28px, 3vw, 40px);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-display);
    color: var(--apo-ink-900);
    margin: 0;
    line-height: 1.08;
    text-wrap: balance;
}

.apo-login__sub {
    font-family: var(--apo-font-body);
    color: var(--apo-ink-500);
    margin: 0;
    line-height: 1.5;
    font-size: var(--apo-text-subtitle1);
    font-weight: 400;
    letter-spacing: var(--apo-tracking-body);
    max-width: 380px;
    text-wrap: balance;
}

/* --- credential form ------------------------------------------------
   A real card now — paper sheet, hairline, soft shadow — because the split
   layout gives the page two objects (card and photograph) and they must
   read as siblings: both placed on the canvas, both softly lifted. */
.apo-login__card {
    display: flex;
    flex-direction: column;
    gap: 16px;
    width: 100%;
    max-width: var(--apo-login-measure);
    padding: var(--apo-space-7) var(--apo-space-6) var(--apo-space-6);
    background: var(--apo-paper);
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-plate);
    /* The plate shadow: a hairline contact edge plus one wide, very low
       ambient layer. Deliberately softer and further than --apo-shadow-md —
       this card floats on an empty canvas with nothing to compare against,
       where md's tighter 6px/7% layer reads as a drawn edge rather than as
       lift. The photograph opposite carries no shadow at all; its size is
       its weight. */
    box-shadow: 0 1px 2px rgba(8, 9, 11, 0.04),
                0 12px 32px -8px rgba(8, 9, 11, 0.10);
}

[data-theme="dark"] .apo-login__card {
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4),
                0 16px 40px -8px rgba(0, 0, 0, 0.6);
}

.apo-login__field {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.apo-login__label {
    font-size: var(--apo-text-caption1);
    font-weight: 500;
    color: var(--apo-ink-700);
    letter-spacing: var(--apo-tracking-body);
}

/* Claude's field treatment: 16px type, a translucent fill, and an inset
   hairline that does not change the control's dimensions on focus. */
.apo-login__card input[type="email"],
.apo-login__card input[type="password"] {
    font-family: var(--apo-font-body);
    font-size: var(--apo-text-subtitle1);
    font-weight: 400;
    line-height: 24px;
    letter-spacing: normal;
    height: var(--apo-login-control-h);
    width: 100%;
    box-sizing: border-box;
    padding: 0 var(--apo-space-4);
    border: 0;
    border-radius: var(--apo-login-control-radius);
    background: var(--apo-login-field-fill);
    color: var(--apo-login-field-ink);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--apo-login-field-ink) 10%, transparent);
    transition: box-shadow 60ms cubic-bezier(0.4, 0, 0.2, 1);
}

.apo-login__card input[type="email"]:hover:not(:focus):not(.invalid),
.apo-login__card input[type="password"]:hover:not(:focus):not(.invalid) {
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--apo-login-field-ink) 20%, transparent);
}

.apo-login__card input[type="email"]:focus,
.apo-login__card input[type="password"]:focus {
    outline: none;
    box-shadow: inset 0 0 0 1px var(--apo-login-field-focus-surface),
                0 0 0 1px #2a78d6,
                0 0 6px 1px var(--apo-login-field-focus-glow);
}

.apo-login__card input::placeholder {
    color: var(--apo-login-field-placeholder);
    opacity: 1;
}

/* High-contrast modes suppress shadows, so provide a visible field and focus. */
@media (forced-colors: active) {
    .apo-login__card input[type="email"],
    .apo-login__card input[type="password"] {
        outline: 1px solid CanvasText;
        outline-offset: -1px;
    }

    .apo-login__card input[type="email"]:focus,
    .apo-login__card input[type="password"]:focus {
        outline: 2px solid Highlight;
        outline-offset: 1px;
    }
}

.apo-login__forgot {
    align-self: center;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--apo-space-1) var(--apo-space-2);
    border-radius: var(--apo-radius-sm);
    color: var(--apo-ink-500);
    font-size: var(--apo-text-body);
    line-height: var(--apo-leading-body);
    text-align: center;
    text-decoration: none;
    text-underline-offset: 3px;
    transition: color var(--apo-duration-fast) var(--apo-ease);
}

.apo-login__forgot:hover {
    color: var(--apo-accent);
    text-decoration: underline;
}

/* The one button on the first screen anyone sees — the same CTA treatment as
   the rest of the product (see --apo-cta-bg): wordmark slate, white label,
   champagne keyline on hover. */
.apo-login__submit {
    width: 100%;
    box-sizing: border-box;
    background: var(--apo-cta-bg);
    color: var(--apo-cta-fg);
    font-family: var(--apo-font-body);
    font-weight: 500;
    font-size: var(--apo-login-button-font-size);
    line-height: var(--apo-leading-subtitle2);
    letter-spacing: var(--apo-tracking-body);
    border: 1px solid var(--apo-cta-edge);
    height: var(--apo-login-control-h);
    padding: 0 var(--apo-space-4);
    border-radius: var(--apo-login-control-radius);
    cursor: pointer;
    transition: background var(--apo-duration-fast) var(--apo-ease),
                border-color var(--apo-duration-fast) var(--apo-ease);
    margin-top: 2px;
}

.apo-login__submit[href] {
    display: flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
}

/* One shift, like every hover in the app: the fill deepens, nothing else.
   The keyline lives on :focus-visible via the global focus ring. */
.apo-login__submit:hover {
    background: var(--apo-cta-hover);
    border-color: var(--apo-cta-edge-hover);
}

.apo-login__submit:active {
    background: color-mix(in srgb, var(--apo-cta-hover) 91%, black);
}

.apo-login__error {
    border-radius: var(--apo-radius-md) !important;
    margin-bottom: 12px;
    width: 100%;
    max-width: var(--apo-login-measure);
}

@media (max-width: 600px) {
    .apo-login__card {
        padding: 22px 18px 18px;
    }
}

/* Shared by the forgot-password and reset-password pages, which reuse
   .apo-login__card. */
/* Centred under the card, the way the reference hangs its secondary
   action below the sheet — the head above is centred now, so a link
   pinned to one edge would be the only off-axis thing on the page. */
.apo-login__backlink {
    margin-top: 18px;
    text-align: center;
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    text-decoration: none;
    transition: color var(--apo-duration-fast) var(--apo-ease);
}

.apo-login__backlink:hover {
    color: var(--apo-accent);
}

.apo-login__submit:disabled {
    opacity: 0.42;
    cursor: default;
    box-shadow: none;
    transform: none;
}

/* Blazor marks invalid fields with .invalid once a field has been touched. */
.apo-login__card input.invalid {
    box-shadow: inset 0 0 0 1px var(--apo-error);
}

.apo-login__card input.invalid:focus {
    box-shadow: inset 0 0 0 1px var(--apo-error),
                0 0 0 1px #2a78d6,
                0 0 6px 1px var(--apo-login-field-focus-glow);
}

.apo-login__card .validation-message {
    color: var(--apo-error);
    font-size: var(--apo-text-caption1);
    margin-top: 2px;
}

/* ============================================================
   UX PASS — findings from a review of all 29 deployed routes.
   Each rule below fixes something observed in the running app,
   not a hypothetical.
   ============================================================ */

/* --- Skip link -------------------------------------------------------
   The only real a11y gap in the review: 29 sidebar links sit before the
   content in tab order, so a keyboard user traversed the whole nav on
   every page. Visually hidden until focused. */
.apo-skip-link {
    position: absolute;
    left: 8px;
    top: -60px;
    z-index: 200;
    padding: 9px 14px;
    border-radius: var(--apo-radius-md);
    background: var(--apo-fill);
    color: var(--apo-fill-fg);
    font-size: var(--apo-text-caption1);
    font-weight: 600;
    text-decoration: none;
    transition: top var(--apo-duration-fast) var(--apo-ease);
}

.apo-skip-link:focus-visible {
    top: 8px;
    outline: 2px solid var(--apo-accent);
    outline-offset: 2px;
}

/* --- Row actions -----------------------------------------------------
   The action column wrapped its icon buttons onto separate lines, which
   pushed /identity/users rows to ~57px and made the two glyphs read as a
   vertical stack rather than a pair. */
.mud-table-cell .apo-row-actions,
.mud-data-grid .mud-table-cell .apo-row-actions {
    display: inline-flex;
    flex-direction: row;
    align-items: center;
    gap: 2px;
    white-space: nowrap;
}

/* Fallback for grids that emit bare icon buttons into the last cell
   without a wrapper — keeps them on one line. */
.mud-data-grid .mud-table-body .mud-table-cell:last-child {
    white-space: nowrap;
}

/* --- Pagination on empty grids --------------------------------------
   "Redaka po stranici: 25 · 0-0 od 0" is chrome describing nothing. The
   empty state already explains the situation. Keyed off our own
   .apo-empty-state, which only renders inside NoRecordsContent. */
.mud-data-grid:has(.apo-empty-state) .mud-table-pagination,
.mud-table:has(.apo-empty-state) .mud-table-pagination {
    display: none;
}

/* The sunk fill that used to distinguish this from the global (Cmd-K) search
   is gone: with the data card removed there is no recessed surface to sink
   into, and Linear's toolbar search is a plain paper field. Position — left
   in the content toolbar vs. the top bar — now carries the scope signal. */
.apo-page-search .mud-input-label {
    font-size: var(--apo-text-caption1) !important;
}

/* --- Empty state action ---------------------------------------------
   EmptyState always supported an Action slot; nothing passed one, so
   every empty page was a dead end. Give it room now that it is used. */
.apo-empty-state__action {
    margin-top: 12px;
    display: flex;
    gap: 8px;
    justify-content: center;
    flex-wrap: wrap;
}

/* --- Stat card drill-through -----------------------------------------
   Lives here, not in StatCard.razor.css: `.apo-stat-card` is the root of a
   MudPaper CHILD COMPONENT, so Blazor's scope attribute never lands on it and
   a scoped `.apo-stat-card__link:hover .apo-stat-card` would compile to a
   selector that matches nothing. Same trap as .apo-nav__list above. */
.apo-stat-card__link,
.apo-stat-card__slot {
    display: block;
    text-decoration: none;
    color: inherit;
}

.apo-stat-card__link .apo-stat-card,
.apo-stat-card__slot .apo-stat-card {
    height: 100%;
    transition: border-color var(--apo-duration-fast) var(--apo-ease),
                background var(--apo-duration-fast) var(--apo-ease);
}

.apo-stat-card__link:hover .apo-stat-card {
    background: var(--apo-surface-hover) !important;
}

.apo-stat-card__link:focus-visible {
    outline: 2px solid var(--apo-accent);
    outline-offset: 2px;
    border-radius: var(--apo-radius-lg);
}

/* ============================================================
   IDENTITY CELL + STATUS — the bespoke half of the data grid.

   A generic grid gives every field its own column, so a user row
   spent two of eight columns on name and e-mail and still wrapped
   "Apolonija SuperAdmin" onto two lines. Name and address are one
   idea — who this is — so they get one cell with the avatar
   anchoring them. That returns a column's worth of width to the
   data and drops the row back to a single line.
   ============================================================ */
/* THE IDENTITY BLOCK IS A <button> ON THE USERS GRID — the row's own
   focusable control — and a <button> given display:flex takes
   `justify-content: center` from the UA stylesheet. So every identity was
   centred inside its column: four rows, four different left edges, and the
   column's own axis never used by anything.

   It is the kind of defect that survives review because each row looks
   correct on its own; it only reads as wrong as a COLUMN, which is how
   nobody caught it while the table was being designed row by row. Stated
   explicitly here rather than on the button, so the block aligns the same
   way whichever element it is rendered as. */
.apo-identity {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    text-align: left;
    gap: 10px;
    min-width: 0;
}

/* The button cut. It carried no rules at all, so it was living on whatever
   reset happened to reach it — which is also how the centring got in. */
.apo-identity--action {
    appearance: none;
    width: 100%;
    padding: 0;
    margin: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    cursor: pointer;
    border-radius: var(--apo-radius-sm);
}

.apo-identity--action:focus-visible {
    outline: 2px solid var(--apo-accent);
    outline-offset: 2px;
}

/* A circle, matching Linear's member rows and our own account menu — the
   squircle experiment made list avatars the one square-ish disc in a product
   whose account chip and menu head are both round. */
.apo-identity__avatar {
    flex: 0 0 auto;
    /* 28px, down from 32px. The avatar is what SETS the table row's height —
       32px plus the cell's own padding is 48px of content in a row nominally
       44px tall, so identity rows quietly ran taller than every other row in
       the same grid. 28 + 12 lands exactly on the 40px row. */
    width: 28px;
    height: 28px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--apo-accent-soft);
    color: var(--apo-accent);
    border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
    font-size: var(--apo-text-caption1);
    font-weight: 600;
    letter-spacing: 0.02em;
    font-variant-numeric: tabular-nums;
    user-select: none;
}

/* Identity avatars carry a per-person tint, assigned by name hash so each
   person keeps theirs everywhere. A directory where every face is the same
   disc reads as a mock-up.

   In a monochrome system that difference is LIGHTNESS, not hue, so the four
   steps are deliberately spread across the ramp rather than being adjacent —
   two greys one step apart are one grey with a rendering bug. Backgrounds mix
   their step into the paper token so both themes derive their own surface;
   foregrounds take the ramp step that carries text in each theme. */
.apo-identity__avatar--teal {
    background: color-mix(in srgb, var(--apo-teal-400) 20%, var(--apo-paper));
    color: var(--apo-teal-700);
}

.apo-identity__avatar--plum {
    background: color-mix(in srgb, var(--apo-plum-400) 18%, var(--apo-paper));
    color: var(--apo-plum-fg);
}

.apo-identity__avatar--slate {
    background: color-mix(in srgb, var(--apo-ink-400) 16%, var(--apo-paper));
    color: var(--apo-ink-700);
}

.apo-identity__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.25;
}

.apo-identity__name {
    color: var(--apo-ink-900);
    font-weight: 550;
    font-size: var(--apo-text-body);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.apo-identity__mail {
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- Status ----------------------------------------------------------
   A filled pill states the value twice — once in colour, once in the
   word — and pills stack up badly down a column. A dot carries the
   state and lets the label stay ordinary text, so the column reads as
   a column rather than a row of buttons. */
.apo-status {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font-size: var(--apo-text-caption1);
    color: var(--apo-ink-600);
    white-space: nowrap;
}

.apo-status::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    flex: 0 0 auto;
    background: var(--apo-ink-300);
    /* A 2px halo, down from 3px. The dot needs SOME bloom to survive at 6px,
       but at 3px it was a glow — a soft coloured aura around every status in
       every row, which is decoration pretending to be legibility. */
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-ink-300) 20%, transparent);
}

.apo-status--ok::before {
    background: var(--apo-success);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-success) 18%, transparent);
}

.apo-status--warn::before {
    background: var(--apo-warning);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-warning) 18%, transparent);
}

.apo-status--off::before {
    background: var(--apo-ink-400);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-ink-400) 18%, transparent);
}

.apo-status--error::before {
    background: var(--apo-error);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--apo-error) 18%, transparent);
}

/* --- Role / category badge -------------------------------------------
   An OUTLINE chip: hairline edge, no fill, 11px. What something IS — a role,
   a type, a category. States keep the dot treatment above; identities take
   the chip.

   This reverses the 2026-08 de-boxing, and the reason is worth writing down,
   because the objection then was correct: a hard border made every tag read
   as a button and a column of them was the noisiest thing on the row. The
   answer to that is a HAIRLINE, not the removal of the edge. Filled grey
   pills solved the noise by making twenty small grey blobs march down a
   column instead — quieter per item, worse as a column, and the single most
   generic object in the product. At --apo-border on paper the edge is a
   whisper; the chip cannot be confused with a button, because every button
   here is 32px tall with an 8px corner and this is 19px with a 4px one. */
.apo-badge {
    display: inline-flex;
    align-items: center;
    max-width: 100%;
    height: 19px;
    padding: 0 7px;
    border: 1px solid var(--apo-border-strong);
    border-radius: var(--apo-radius-sm);
    background: transparent;
    color: var(--apo-ink-700);
    font-size: var(--apo-text-caption2);
    font-weight: 550;
    letter-spacing: var(--apo-tracking-caption);
    line-height: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- Group tags ------------------------------------------------------
   Raw "SUPERADMINISTRATORS" shouted down the column in caps. As a tag it
   reads as a membership rather than a value, and the count chip keeps the
   column from growing with the number of memberships. */
.apo-tags {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-width: 0;
}

/* inline-block, NOT inline-flex: text-overflow has no effect on a flex
   container's own text, so a flex tag hard-clips a long name mid-glyph
   instead of showing that it was truncated.

   Same outline chip as .apo-badge, one stop quieter — a membership is
   context, a role is an attribute. See the note there for why the edge came
   back after the 2026-08 de-boxing. */
.apo-tag {
    display: inline-block;
    max-width: 200px;
    vertical-align: middle;
    padding: 2px 7px;
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-sm);
    background: transparent;
    color: var(--apo-ink-600);
    font-size: var(--apo-text-caption2);
    font-weight: 500;
    letter-spacing: var(--apo-tracking-caption);
    line-height: 1.35;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* "+2" — a counter, not a name. Dashed, so it reads as "there is more here"
   rather than as another membership with a very short label. */
.apo-tag--count {
    color: var(--apo-ink-500);
    background: transparent;
    border-style: dashed;
}

.apo-tags__empty {
    color: var(--apo-ink-400);
    font-size: var(--apo-text-caption1);
}

/* Secondary columns — timestamps, counters, references — step back so the
   row's identity carries it. Opt-in per column via CellClass. */
.mud-data-grid .mud-table-body .mud-table-cell.apo-cell--meta,
.mud-table-root .mud-table-cell.apo-cell--meta {
    color: var(--apo-ink-500);
}

/* The same token as a SPAN inside a cell — the " · subtitle" trailing a row's
   name, the em dash standing in for an empty value. Twelve call sites were
   writing this class on a span and getting nothing: the two selectors above
   both require .mud-table-cell, so the span matched neither and a service's
   subtitle read at full ink beside its name. */
.apo-cell--meta {
    color: var(--apo-ink-500);
}

/* Fixed-measure columns, opt-in via CellClass like .apo-cell--meta above.
   These lived in Identity/Users.razor.css, where Blazor's [b-*] scoping
   quietly limited them to that one page — the CellClass references on
   Services, PriceList, DentalPortal and Invoices matched nothing, so the
   Invoices amount column was never actually right-aligned. Same trap the
   comment at the top of that file documents for .apo-data-card. */
.apo-status-cell {
    width: 110px;
}

/* Money right-aligns — the deliberate exception to the start-aligned policy
   in mudblazor-overrides.css. */
.apo-amount-cell {
    width: 140px;
    text-align: right;
}

/* The row-open control. Looks like the cell's text, behaves like a button —
   which is the point: the row is the affordance, this exists so the keyboard
   has something to land on. */
.apo-row-open {
    appearance: none;
    border: none;
    background: none;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    border-radius: var(--apo-radius-sm);
    /* MudBlazor's global `button` reset makes every button an inline-flex box
       that CENTRES its content. It does not show while the button shrink-wraps
       its text — but drop one into a column-flex cell, where it stretches to
       the full width, and the row's name centres itself over its own subtitle.
       text-align cannot reach it: the text is a flex child, so justification
       is what decides. */
    justify-content: flex-start;
}

.apo-row-open:focus-visible {
    outline: 2px solid var(--apo-accent);
    outline-offset: 3px;
}

/* .apo-row-open likewise keeps its resting colour under the row wash. */

/* ============================================================
   DIALOG SECTIONS — what replaced the tabs inside modals.

   Title beside its fields rather than above them: the label
   column gives the eye a fixed rail to scan down, so finding
   "Uloge i grupe" in a long form is a glance rather than a read.
   Below 960px there is no room for two columns and it stacks.
   ============================================================ */
/* ============================================================
   DIALOG BODY VOCABULARY

   Forty modals, and the body of each was composed from whatever the author
   reached for that day: <MudStack Spacing="3">, <MudGrid Spacing="2">, a bare
   <MudText Class="mb-3">, an outlined <MudAlert> pressed into service as a
   header. The chrome was already standardised — one title rail, one footer,
   one width ladder, all owned by ApoDialogs — but the body was not, so the
   modals still read as forty different screens.

   THREE SHAPES, and every dialog is built from them:

     · a PROMPT — one question. A lede sentence, then the field. No section
       rail; a 444px dialog asking for an email address does not need a
       labelled group structure, and giving it one is how a one-line question
       ends up looking like a settings page.

     · a RECORD EDITOR — several groups of fields. <DialogSection> per group,
       with the group's name on its own rail beside the fields at md and
       wider. Defined below.

     · a SUMMARY — what you are about to act on, stated once at the top.

   The summary shape is the one worth naming, because its absence is what
   made half these dialogs look wrong: an <MudAlert Severity="Info"> was
   being used to show WHICH patient or invoice you had opened. An alert says
   "something needs your attention"; a patient's name is not an alert, and
   rendering it as one puts a coloured rule and a warning glyph beside a
   person's name every time you open their booking request.
   ============================================================ */
.apo-dialog-stack {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

/* The opening sentence: what this dialog is for, or what the field below it
   will do. One size, one colour, one measure — it was Typo.body1 in one
   dialog, body2 in the next, body2 + Color.Secondary in a third, with
   mb-3 / mb-4 / nothing under it. */
.apo-dialog-lede {
    margin: 0 0 14px;
    color: var(--apo-ink-600);
    font-size: var(--apo-text-body);
    line-height: 1.55;
    max-width: 64ch;
}

.apo-dialog-stack > .apo-dialog-lede,
.apo-dialog-section__body > .apo-dialog-lede {
    margin-bottom: 0;
}

/* The record this dialog is acting on. A recessed panel, not an alert. */
.apo-dialog-summary {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 10px 12px;
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-md);
    background: var(--apo-surface-sunk);
    min-width: 0;
}

.apo-dialog-summary__title {
    color: var(--apo-ink-900);
    font-size: var(--apo-text-body);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-subtitle);
    line-height: var(--apo-leading-body);
}

.apo-dialog-summary__meta {
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    line-height: var(--apo-leading-caption1);
    font-variant-numeric: tabular-nums;
}

/* A read-only value inside a dialog — the closed-request summary, a status
   that can no longer be changed. Same label/value relationship a field has,
   so a locked dialog reads as the same form with its controls removed. */
.apo-dialog-readonly {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

.apo-dialog-readonly__label {
    color: var(--apo-ink-700);
    font-size: var(--apo-text-caption1);
    font-weight: 500;
    line-height: var(--apo-leading-caption1);
}

.apo-dialog-readonly__value {
    color: var(--apo-ink-900);
    font-size: var(--apo-text-body);
    line-height: 1.55;
    white-space: pre-wrap;
}

/* A number being reported rather than a value being read back — a points
   balance, an estimated audience. Same label/value pair, the figure set at
   the top of the ramp so it carries the way a metric tile's does. */
.apo-dialog-readonly--figure .apo-dialog-readonly__value {
    font-family: var(--apo-font-display);
    font-size: var(--apo-text-title2);
    font-weight: 600;
    line-height: 1.15;
    letter-spacing: var(--apo-tracking-display);
    font-variant-numeric: tabular-nums;
}

/* THE SECTION RAIL SWITCHES ON THE DIALOG'S WIDTH, NOT THE VIEWPORT.

   The old rule was a `@media (max-width: 960px)` — the viewport — with a
   comment claiming it matched "the dialog's own max width". It did not: on a
   1512px screen EVERY dialog got the two-column rail, including the 444px and
   600px ones, where a 200px label column leaves under 400px for the fields
   and a two-up field row collapses into slivers. That is most of what made
   the narrow dialogs read as broken rather than merely tight.

   MudBlazor stamps the chosen MaxWidth onto the dialog as .mud-dialog-width-*,
   so the rail can key off the object it actually belongs to. Single column is
   the base — the safe layout — and md/lg/xl opt in. */
.apo-dialog-section {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 10px;
    padding: 18px 0;
    border-top: 1px solid var(--apo-border);
}

.mud-dialog-width-md .apo-dialog-section,
.mud-dialog-width-lg .apo-dialog-section,
.mud-dialog-width-xl .apo-dialog-section {
    grid-template-columns: 190px minmax(0, 1fr);
    gap: 24px;
    padding: 20px 0;
}

/* A wide dialog on a narrow screen is a narrow dialog. */
@media (max-width: 1000px) {
    .mud-dialog-width-md .apo-dialog-section,
    .mud-dialog-width-lg .apo-dialog-section,
    .mud-dialog-width-xl .apo-dialog-section {
        grid-template-columns: minmax(0, 1fr);
        gap: 10px;
        padding: 18px 0;
    }
}

/* The first section in a dialog sits directly under the header, which already
   draws a hairline — a second one would double the rule. */
.apo-dialog-section--flush {
    border-top: none;
    padding-top: 2px;
}

.apo-dialog-section__head {
    /* Sticks while its own fields scroll past, so the label for what you are
       editing stays visible in a long section. Only meaningful in the
       two-column rail; in one column the head is a band above its fields and
       sticking it would have it slide over them. */
    align-self: start;
    min-width: 0;
}

.mud-dialog-width-md .apo-dialog-section__head,
.mud-dialog-width-lg .apo-dialog-section__head,
.mud-dialog-width-xl .apo-dialog-section__head {
    position: sticky;
    top: 0;
}

@media (max-width: 1000px) {
    .mud-dialog-width-md .apo-dialog-section__head,
    .mud-dialog-width-lg .apo-dialog-section__head,
    .mud-dialog-width-xl .apo-dialog-section__head {
        position: static;
    }
}

.apo-dialog-section__title {
    margin: 0;
    font-family: var(--apo-font-display);
    font-size: var(--apo-text-body);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-subtitle);
    line-height: var(--apo-leading-body);
    color: var(--apo-ink-900);
}

.apo-dialog-section__desc {
    margin: 4px 0 0;
    font-size: var(--apo-text-caption1);
    line-height: var(--apo-leading-caption1);
    color: var(--apo-ink-500);
}

.apo-dialog-section__body {
    min-width: 0;
}

/* Anything stacked directly in a section body — a grid, an alert, a list —
   takes one rhythm, so a section built from two blocks is spaced the same as
   one built from five. Page files used to spell this out with mb-2 / mb-4 /
   mt-3 utilities, which is how eleven dialogs ended up with eleven vertical
   rhythms. */
.apo-dialog-section__body > * + * {
    margin-top: 14px;
}

/* ============================================================
   DIALOG BODY — one grid rhythm for every modal in the product.

   Every dialog reaches for <MudGrid> to lay its fields out, and each one
   picked its own Spacing: 1 (4px), 2 (8px), or the 24px default. Three
   rhythms across forty modals, and the difference is plainly visible when
   you open two of them in a row.

   Normalised here rather than by editing forty call sites, because the next
   dialog somebody writes will pick its own number too. 14px matches the
   section body's stack rhythm above, so a two-up field row and a stacked
   pair sit on the same grid.
   ============================================================ */
.mud-dialog-content .mud-grid {
    margin: -7px;
    width: calc(100% + 14px);
}

.mud-dialog-content .mud-grid > .mud-grid-item {
    padding: 7px;
}

/* SWITCHES AND CHECKBOXES SIT ON THE FIELD'S OPTICAL LINE.

   A <MudSwitch> in a grid cell beside a text field rendered at the TOP of the
   row while the field's input sat 22px lower, under its label — so every
   "Active" toggle in the product floated above the control it was paired
   with. The cell is centred on the same 54px box the labelled field occupies
   (22px label + 32px input), which puts the switch's centre exactly on the
   input's.

   Checkboxes get the compact box instead: they appear in LISTS (roles,
   members, permissions), where a 54px row would triple the height of a
   twenty-item column for no gain. */
/* :has(.mud-switch), NOT :has(> .mud-switch). MudBlazor wraps a boolean
   control in .mud-input-control > .mud-input-control-input-container before
   the <label class="mud-switch"> itself, so the switch is a great-grandchild
   of the grid cell. The child-combinator version of this rule matched nothing
   and shipped looking exactly like the bug it was meant to fix. */
.mud-dialog-content .mud-grid-item:has(.mud-switch) {
    display: flex;
    align-items: center;
    min-height: 54px;
}

.mud-dialog-content .mud-grid-item:has(.mud-checkbox):not(:has(.mud-switch)) {
    display: flex;
    align-items: center;
    min-height: 30px;
}

/* The wrapper MudBlazor puts between them would otherwise keep its own
   stacked-field layout and re-introduce the offset. */
.mud-dialog-content .mud-grid-item:has(.mud-switch) > .mud-input-control,
.mud-dialog-content .mud-grid-item:has(.mud-checkbox) > .mud-input-control {
    width: 100%;
    margin: 0;
}

/* Standalone blocks between the header and the form — a source badge, a
   "this record is locked" warning, a loading bar. They carried mb-4 / mb-2 /
   nothing depending on the file. */
.mud-dialog-content > .mud-alert,
.mud-dialog-content > .mud-progress-linear {
    margin-bottom: 16px;
}

.mud-dialog-content > .mud-alert:last-child,
.mud-dialog-content > .mud-progress-linear:last-child {
    margin-bottom: 0;
}

/* The numeric field's spinner. MudBlazor paints two stacked chevrons hard
   against the field's right edge at all times — a permanent pair of grey
   marks in a form that has no other control chrome. They surface on hover and
   focus, which is when a step control is of any use, and stay out of the way
   while the form is being read. */
.mud-input-numeric-spin {
    opacity: 0;
    transition: opacity var(--apo-duration-fast) var(--apo-ease);
}

.mud-input:hover .mud-input-numeric-spin,
.mud-input:focus-within .mud-input-numeric-spin {
    opacity: 1;
}

.mud-input-numeric-spin .mud-button-root {
    color: var(--apo-ink-400);
}

.mud-input-numeric-spin .mud-button-root:hover {
    color: var(--apo-ink-800);
    background: transparent;
}

/* --- Editorial rail ---------------------------------------------------
   The editor's sidebar as ONE sheet: a single frame holding hairline-
   separated sections, each named by a quiet label — the profile page's
   grammar, standing in for what used to be six stacked bordered cards
   (card, gap, card, gap …), which is the shape of a generated admin
   panel and spent more ink on frames than on facts.

   Global rather than scoped for a hard reason, not a stylistic one: the
   sections come from two components (ArticleEditor and its embedded
   ContentWorkflowPanel), and a scoped class on a child component's root
   matches nothing — which is exactly how the workflow panel shipped
   frameless for a month without anyone being able to say why. */
.apo-editorial-rail {
    background: var(--apo-paper);
    border: 1px solid var(--apo-border);
    border-radius: var(--apo-radius-lg);
    overflow: hidden;
}

.apo-editorial-rail__section {
    padding: 14px;
    border-bottom: 1px solid var(--apo-border);
}

.apo-editorial-rail__section:last-child {
    border-bottom: none;
}

/* The section's name — the same register as the profile sheet's rail and a
   dialog section's title: it names a group of fields, it does not compete
   with them. The row layout is for the one accessory a label carries here
   (the workflow section's status dot). */
.apo-editorial-rail__label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    /* No margin of its own: the section's universal rhythm below owns every
       vertical gap, the label's included. */
    margin-bottom: 0;
    color: var(--apo-ink-700);
    font-size: var(--apo-text-caption1);
    font-weight: 600;
    letter-spacing: var(--apo-tracking-caption);
    line-height: 1.4;
}

/* ONE rhythm between everything a section stacks — label, selects, switch
   lists, captions. Universal on purpose: naming component classes here was
   tried first and named the wrong ones (a MudSelect's direct child is
   .mud-select, not .mud-input-control), which left the metadata selects
   sitting on MudBlazor's own 4px. */
.apo-editorial-rail__section > * + * {
    margin-top: 10px;
}

/* --- Switch row -------------------------------------------------------
   The iOS Settings register: what the thing is on the left, the switch on
   the right, rows separated by hairlines. One grammar for every boolean a
   dialog carries — a consent, a membership, a creation option — so a form
   full of yes/no questions reads as one instrument panel rather than as a
   scatter of switches hanging off their own labels.

   The switch is the only thing in the row that toggles. The text is a
   description, not a second hit target — a row that toggles when you try to
   select its label is how settings get flipped by accident. */
.apo-switch-list {
    display: flex;
    flex-direction: column;
}

.apo-switch-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 16px;
    padding: 8px 0;
    border-bottom: 1px solid var(--apo-border);
}

.apo-switch-row:last-child {
    border-bottom: none;
}

.apo-switch-row__text {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}

.apo-switch-row__label {
    color: var(--apo-ink-900);
    font-size: var(--apo-text-body);
    line-height: var(--apo-leading-body);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.apo-switch-row__hint {
    color: var(--apo-ink-500);
    font-size: var(--apo-text-caption1);
    line-height: var(--apo-leading-caption1);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A full-sentence row — the consent attestation, and any future row whose
   label is legally meaningful. The default label truncates because rows are
   scanned; a sentence someone is attesting to must never lose its second
   half to an ellipsis. */
.apo-switch-row--sentence .apo-switch-row__label {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    line-height: 1.5;
}

/* The bare switch: MudSwitch reserves a label gutter even with no label, and
   inside a row grammar that gutter pushes the pill off the right edge. */
.apo-switch-row .mud-switch {
    margin: 0;
    gap: 0;
}

/* --- Permission row --------------------------------------------------
   A grid, not a flex row with justify-space-between. The flex version gave
   the text no min-width, so a long code slid under the select next to it —
   "identity.permissions.manage" was rendering on top of its own dropdown.
   The 1fr column can shrink and truncate; the select keeps a fixed width. */
.apo-permission-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 168px;
    align-items: center;
    gap: 16px;
    /* 8px, matching .apo-switch-row above — the two grammars stack in the
       same dialog, and a one-step drift between them reads as a mistake. */
    padding: 8px 0;
    border-bottom: 1px solid var(--apo-border);
}

.apo-permission-row:last-child {
    border-bottom: none;
}

.apo-permission-row__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.3;
}

.apo-permission-row__name {
    font-size: var(--apo-text-body);
    color: var(--apo-ink-900);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.apo-permission-row__code {
    font-size: var(--apo-text-caption1);
    color: var(--apo-ink-500);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* This select is labelled by the permission name beside it, so it must not
   also carry a stacked label's top margin. */
.apo-permission-row__select {
    margin: 0 !important;
}

@media (max-width: 700px) {
    .apo-permission-row {
        grid-template-columns: minmax(0, 1fr);
        gap: 6px;
    }
}


/* ============================================================
   THE DEVICE SHELL

   The iPhone the embedded Flutter build is meant to be seen in. It lived in
   MobileAppEmulator.razor.css until the content editor's exact preview needed
   it too — and needed it for a reason beyond looks. The embedded build
   RESERVES 58pt at the top and 34pt at the bottom (AppConfig.embeddedTop/
   BottomViewPadding) for the status bar and the home indicator DRAWN HERE. A
   surface that embeds the app without this chrome does not get a clean phone,
   it gets the app pushed off both edges with nothing standing in the gap.

   Global rather than scoped because two components render it (DeviceFrame is
   the only one that should) and mobile-emulator.js addresses
   .apo-device-screen / .apo-device-screen__app by name.
   ============================================================ */
.apo-device-frame {
    position: relative;
    box-sizing: border-box;
    /* iPhone 17 Pro: a 402x874pt display behind bezels a step thinner than the
       15 Pro's. 422 = 402 + 2x9px bezel + 2x1px border; the aspect keeps the
       screen at exactly the logical resolution the spec panel states. */
    width: min(422px, calc(100vw - 52px));
    aspect-ratio: 422 / 894;
    padding: 9px;
    border: 1px solid #48505b;
    /* Concentric with the display: 62pt screen radius + 10px to the shell. */
    border-radius: 72px;
    background: linear-gradient(135deg, #4b5058 0%, #12161b 22%, #050607 52%, #2f343c 100%);
    box-shadow: 0 3px 6px rgba(3, 6, 12, 0.18),
                0 24px 70px rgba(3, 6, 12, 0.25),
                inset 0 0 0 1px rgba(255, 255, 255, 0.13);
}

.apo-device-frame::after {
    content: "";
    position: absolute;
    inset: 4px;
    z-index: 2;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 68px;
    pointer-events: none;
}

.apo-device-frame__action,
.apo-device-frame__volume-up,
.apo-device-frame__volume-down,
.apo-device-frame__power,
.apo-device-frame__camera {
    position: absolute;
    width: 3px;
    border-radius: 3px 0 0 3px;
    background: linear-gradient(180deg, #353b43, #0c0f13 48%, #515861);
    box-shadow: -1px 0 1px rgba(0, 0, 0, 0.3);
}

.apo-device-frame__action {
    top: 112px;
    left: -4px;
    height: 26px;
}

.apo-device-frame__volume-up {
    top: 160px;
    left: -4px;
    height: 60px;
}

.apo-device-frame__volume-down {
    top: 231px;
    left: -4px;
    height: 60px;
}

.apo-device-frame__power {
    top: 190px;
    right: -4px;
    height: 88px;
    border-radius: 0 3px 3px 0;
}

/* The Camera Control key the 16/17 generation carries low on the right edge —
   flush with the shell, so it draws shorter and sits closer than the keys. */
.apo-device-frame__camera {
    top: 388px;
    right: -3px;
    height: 34px;
    border-radius: 0 2px 2px 0;
}

.apo-device-screen {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    overflow: hidden;
    border-radius: 62px;
    background: #f4eee1;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}

.apo-ios-status {
    /* Floats over the frame rather than reserving a strip above it: the app
       paints its own background — vertical gradient and gold glow — right up
       under the clock, so there is no flat band to mismatch. The embedded
       build reserves this height itself (AppConfig.embeddedTopViewPadding). */
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* The 17 Pro reserves a taller safe area, and the row's centre lines up
       with the island's (island top 11 + 37/2 = 29.5). */
    height: 58px;
    min-height: 58px;
    padding: 0;
    pointer-events: none;
    background: transparent;
    color: #121314;
    font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
    /* RAMP EXCEPTION, deliberate. This is a simulated iOS status bar inside
       the device mock, not app chrome — it follows Apple's metrics (and
       -apple-system above), not ours. Pulling it onto the Fluent ramp would
       make the mock less faithful, not more consistent. */
    font-size: 15px;
    font-weight: 650;
    letter-spacing: -0.02em;
}

/* The clock and the status cluster sit CENTRED in the ears beside the island,
   which is where iOS puts them — the previous fixed edge padding pinned both
   to the corners, which is the tell of a mock-up. Each ear is
   (402 - 125-wide island) / 2 = 138px, so a fixed-width box centred on that
   span lands the content exactly where the real device does. */
.apo-ios-status time,
.apo-ios-status__system {
    position: relative;
    z-index: 2;
    width: 139px;
}

.apo-ios-status time {
    display: flex;
    justify-content: center;
}

.apo-ios-status__island {
    position: absolute;
    top: 11px;
    left: 50%;
    width: 125px;
    height: 37px;
    border-radius: 999px;
    background: #050506;
    transform: translateX(-50%);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
}

.apo-ios-status__island::after {
    content: "";
    position: absolute;
    top: 12px;
    right: 12px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: radial-gradient(circle at 38% 38%, #1b3751, #050506 70%);
}

.apo-ios-status__system {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
}

.apo-ios-status__signal {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 11px;
}

.apo-ios-status__signal i {
    display: block;
    width: 3px;
    border-radius: 2px;
    background: currentColor;
}

.apo-ios-status__signal i:nth-child(1) { height: 4.5px; }
.apo-ios-status__signal i:nth-child(2) { height: 6.5px; }
.apo-ios-status__signal i:nth-child(3) { height: 8.5px; }
.apo-ios-status__signal i:nth-child(4) { height: 11px; }

/* Now an inline SVG, so the font-size/rotation hacks that were compensating for
   a mis-chosen text glyph are gone. */
.apo-ios-status__wifi {
    display: inline-flex;
    align-items: center;
    line-height: 0;
}

.apo-ios-status__battery {
    position: relative;
    box-sizing: border-box;
    width: 25px;
    height: 13px;
    padding: 1.5px;
    border: 1px solid currentColor;
    border-radius: 4.3px;
}

.apo-ios-status__battery::after {
    content: "";
    position: absolute;
    top: 3.5px;
    right: -3.5px;
    width: 2px;
    height: 5px;
    border-radius: 0 1px 1px 0;
    background: currentColor;
    opacity: 0.5;
}

.apo-ios-status__battery i {
    display: block;
    width: 82%;
    height: 100%;
    border-radius: 1px;
    background: currentColor;
}

.apo-device-screen__app {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    background: #f4eee1;
}

.apo-device-screen iframe {
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
    background: #f4eee1;
}

.apo-device-screen__loader {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    background: #f4eee1;
    color: #686159;
    font-size: var(--apo-text-caption1);
    transition: opacity var(--apo-duration-base) var(--apo-ease),
                visibility var(--apo-duration-base) var(--apo-ease);
}

.apo-device-screen__app[data-loaded="true"] .apo-device-screen__loader {
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
}

/* The mock's own surfaces follow the app's chosen theme, which the emulator
   script reads from the app's persisted preference. Without this the status
   bar stays cream above a dark app — the one seam that gives the mock away. */
.apo-device-screen[data-app-theme="dark"],
.apo-device-screen[data-app-theme="dark"] .apo-device-screen__app,
.apo-device-screen[data-app-theme="dark"] iframe,
.apo-device-screen[data-app-theme="dark"] .apo-device-screen__loader {
    background: #0b0e13;
}

/* The bar itself stays transparent in both themes — only the clock and the
   status glyphs have to survive the app's background. */
.apo-device-screen[data-app-theme="dark"] .apo-ios-status {
    /* Was a cream drawn to match the old warm chrome; both this shell and the
       app inside it are monochrome now. */
    color: #F2F2F3;
}

.apo-device-screen[data-app-theme="dark"] .apo-device-screen__loader {
    color: #7c8693;
}

.apo-device-screen[data-app-theme="dark"] .apo-ios-home-indicator {
    background: rgba(241, 235, 221, 0.85);
}

.apo-ios-status {
    transition: color var(--apo-duration-base) var(--apo-ease);
}

.apo-ios-home-indicator {
    transition: background-color var(--apo-duration-base) var(--apo-ease);
}

.apo-ios-home-indicator {
    position: absolute;
    bottom: 8px;
    left: 50%;
    z-index: 4;
    width: 139px;
    height: 5px;
    border-radius: 999px;
    background: rgba(12, 15, 18, 0.82);
    transform: translateX(-50%);
    pointer-events: none;
}


/* The editor's preview pane: a dialog is far shorter than 894px, so the
   device takes the height it is given and keeps the app at its true 402pt
   WIDTH — the dimension that decides how copy wraps, which is the whole point
   of previewing it. The keys are dropped with the fixed height: their offsets
   are metrics of the full shell and would bunch at the top of a short one.
   The corner is scaled down to stay concentric at that height rather than
   turning the pane into a lozenge. */
.apo-device-frame--fill {
    width: 422px;
    max-width: 100%;
    height: 100%;
    aspect-ratio: auto;
    border-radius: 44px;
}

/* Concentric at the smaller corner, on the shell's own -4 / -10 steps. */
.apo-device-frame--fill::after {
    border-radius: 40px;
}

.apo-device-frame--fill .apo-device-screen {
    border-radius: 34px;
}

@media (max-width: 560px) {
    .apo-device-frame {
        border-radius: 60px;
    }

    .apo-device-screen {
        border-radius: 50px;
    }

    .apo-device-frame--fill {
        border-radius: 40px;
    }

    .apo-device-frame--fill .apo-device-screen {
        border-radius: 33px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .apo-device-screen__loader {
        transition: none;
    }
}
