/* ============================================================================
   om-kit.css - the om-* component layer.
   Ported from docs/design-system/kit/css/{surfaces,controls,forms,data,overlay}.css
   with every physical directional property converted to a logical one
   (CLAUDE.md rule 5, docs/design-system/README.md conflict 6).
   Port rationale, the conversions applied and the kit-vs-spec conflict
   resolutions live in docs/design-system/kit-port-notes.md - not here, so the
   delivered stylesheet stays inside its byte budget without a build step.
   Depends on om-tokens.css (all custom properties) and om-base.css (keyframes,
   reset, .om-num/.om-mono/.om-scroll/.om-flip). Declares no custom property at
   document root scope. navigation.css is deliberately NOT ported (D5).
   ========================================================================= */

/* ============================ SURFACES ============================ */
.om-card{position:relative;display:flex;flex-direction:column;background:var(--card-bg);border:var(--card-border);border-radius:var(--card-radius);box-shadow:var(--card-shadow);transition:var(--t-elevate),border-color var(--motion-fast) var(--ease-out)}
.om-card--pad{padding:var(--pad-card)}
.om-card--pad-lg{padding:var(--pad-card-lg)}
.om-card--pad-sm{padding:var(--pad-card-sm)}
.om-card--flush{padding:0}
.om-card--radius-md{border-radius:var(--radius-xl)}
.om-card--sunken{background:var(--surface-sunken);box-shadow:none}
.om-card--ghost{background:transparent;border-color:var(--border-light);box-shadow:none}
.om-card--inverse{background:var(--gradient-ink);border-color:transparent;color:var(--text-inverse)}
.om-card--inverse .om-card__title,.om-card--inverse h1,.om-card--inverse h2,.om-card--inverse h3,.om-card--inverse h4{color:var(--text-inverse)}
.om-card--brand{background:var(--gradient-primary);border-color:transparent;color:var(--text-inverse);box-shadow:var(--glow-md)}
.om-card--interactive{cursor:pointer}
.om-card--interactive:hover{transform:translateY(var(--lift-3));box-shadow:var(--card-shadow-hover);border-color:var(--blue-200)}
.om-card--interactive:active{transform:translateY(-1px);box-shadow:var(--shadow-sm);transition-duration:var(--motion-fast)}
.om-card--interactive:focus-visible{outline:none;box-shadow:var(--card-shadow-hover),var(--shadow-ring)}
.om-card--glow{border-color:var(--blue-200);box-shadow:var(--shadow-sm),var(--glow-sm)}
.om-card--glow:hover{box-shadow:var(--shadow-lg),var(--glow-md)}
.om-card--selected{border-color:var(--primary);box-shadow:var(--shadow-sm),var(--glow-ring)}
.om-card--rail::before{content:"";position:absolute;inset:0 0 auto 0;height:var(--rail-h);border-radius:var(--card-radius) var(--card-radius) 0 0;background:var(--om-rail,var(--blue-500))}

.om-card__head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--pad-card) var(--pad-card) 0}
.om-card--flush>.om-card__head{padding:var(--pad-card) var(--pad-card) 0}
.om-card__title{font:var(--type-card-title);letter-spacing:var(--ls-tight);color:var(--text-primary);margin:0}
.om-card__subtitle{font:var(--type-caption);color:var(--text-muted);margin-top:var(--space-05)}
.om-card__body{padding:var(--pad-card)}
.om-card__foot{display:flex;align-items:center;gap:var(--space-3);padding:0 var(--pad-card) var(--pad-card)}

.om-section-title{display:flex;align-items:center;gap:var(--space-2);font:var(--type-overline);letter-spacing:var(--ls-caps-wide);text-transform:uppercase;color:var(--text-muted)}
.om-divider{height:var(--hairline);background:var(--divider);border:0;margin:0}
.om-divider--v{width:var(--hairline);height:auto;align-self:stretch}

.om-panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);box-shadow:var(--shadow-xl);overflow:hidden}
.om-skel-row{display:flex;align-items:center;gap:var(--space-3);padding:var(--pad-cell-y) 0}

.om-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-3);padding:var(--space-7) var(--space-5);text-align:center}
.om-empty__glyph{display:grid;place-items:center;width:64px;height:64px;border-radius:var(--radius-pill);background:var(--primary-soft);color:var(--blue-500);font-size:var(--icon-empty)}
/* .om-denied__title joins here rather than repeating the declaration: the denied
   state IS an empty state that names a role, and om-denied is derived from this
   block (see PERMISSION DENIED below). One rule, so they cannot drift. */
.om-empty__title,.om-denied__title{font:var(--type-card-title);color:var(--text-primary)}
.om-empty__text{font:var(--type-body);color:var(--text-muted);max-width:38ch}
/* The action that resolves the emptiness. DESIGN-LANGUAGE.md: never ship an
   empty state without an icon, a sentence, and that action — so it gets a slot
   of its own instead of leaning on the parent gap. */
.om-empty__actions{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--space-2);margin-top:var(--space-1)}
/* Lane — the reception board wants ONE muted line inside a column that is
   already narrow, so the illustration is dropped rather than shrunk. */
.om-empty--lane{padding:var(--space-4) var(--space-3)}
.om-empty--lane .om-empty__glyph{display:none}
.om-empty--lane .om-empty__title{font:var(--type-caption);color:var(--text-faint)}

/* ---------------------------- BUTTON ---------------------------- */
.om-btn{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);
  height:var(--control-h);padding:0 var(--space-4);border-radius:var(--radius-md);
  font:var(--type-button);letter-spacing:var(--ls-base);white-space:nowrap;
  border:1px solid transparent;cursor:pointer;overflow:hidden;isolation:isolate;
  transition:transform var(--motion-base) var(--ease-out),box-shadow var(--motion-base) var(--ease-out),var(--t-color)}
.om-btn:disabled,.om-btn[aria-disabled="true"]{cursor:not-allowed;transform:none !important;box-shadow:none !important}
.om-btn__label{position:relative;z-index:var(--z-local-2);display:inline-flex;align-items:center;gap:var(--space-2)}
.om-btn__sheen{position:absolute;inset:0;z-index:var(--z-local-1);pointer-events:none;background:var(--gradient-sheen);transform:translateX(-120%);opacity:0}
.om-btn:hover .om-btn__sheen{animation:om-sheen 620ms var(--ease-out);opacity:1}
.om-btn:disabled .om-btn__sheen{animation:none;opacity:0}
/* Trailing arrow nudges 3px along the inline axis on hover. Conversion 12. */
.om-btn__trail{--om-nudge-dir:-3px}
[dir="ltr"] .om-btn__trail{--om-nudge-dir:3px}
.om-btn:hover .om-btn__trail{animation:om-nudge-inline 500ms var(--ease-out)}
@keyframes om-nudge-inline{0%,100%{transform:translateX(0)}50%{transform:translateX(var(--om-nudge-dir))}}

.om-btn--sm{height:var(--control-h-sm);padding:0 var(--space-3);font-size:var(--fs-xs);border-radius:var(--radius-sm);gap:var(--space-2)}
.om-btn--lg{height:var(--control-h-lg);padding:0 var(--space-5);font-size:var(--fs-base)}
.om-btn--block{width:100%}
.om-btn--pill{border-radius:var(--radius-pill)}

.om-btn--primary{background:var(--gradient-primary);color:var(--text-inverse);box-shadow:var(--elev-cta)}
.om-btn--primary:hover{transform:translateY(var(--lift-2));box-shadow:var(--elev-cta-hover)}
.om-btn--primary:active{transform:translateY(0) scale(var(--press-scale));box-shadow:var(--glow-sm);transition-duration:var(--motion-fast)}
.om-btn--primary:focus-visible{outline:none;box-shadow:var(--elev-cta),var(--shadow-ring)}
.om-btn--primary:disabled{background:var(--surface-disabled);color:var(--text-disabled)}

.om-btn--secondary{background:var(--surface);color:var(--text-primary);border-color:var(--border);box-shadow:var(--shadow-sm)}
.om-btn--secondary:hover{border-color:var(--blue-300);color:var(--text-brand);background:var(--blue-50);transform:translateY(var(--lift-1));box-shadow:var(--shadow-sm),var(--glow-xs)}
.om-btn--secondary:active{transform:scale(var(--press-scale-subtle));background:var(--blue-100)}
.om-btn--secondary:disabled{background:var(--surface-disabled);color:var(--text-disabled);border-color:transparent}

.om-btn--ghost{background:transparent;color:var(--text-secondary)}
.om-btn--ghost:hover{background:var(--blue-50);color:var(--text-brand)}
.om-btn--ghost:active{background:var(--blue-100);transform:scale(var(--press-scale-subtle))}
.om-btn--ghost:disabled{color:var(--text-disabled);background:transparent}

.om-btn--soft{background:var(--blue-50);color:var(--blue-700);border-color:transparent}
.om-btn--soft:hover{background:var(--blue-100);transform:translateY(var(--lift-1));box-shadow:var(--glow-xs)}
.om-btn--soft:active{transform:scale(var(--press-scale-subtle));background:var(--blue-200)}

.om-btn--inverse{background:var(--surface-inverse);color:var(--text-inverse)}
.om-btn--inverse:hover{transform:translateY(var(--lift-2));box-shadow:var(--shadow-lg)}
.om-btn--inverse:active{transform:scale(var(--press-scale))}

/* The kit cast this shadow in ITS OWN red, a hue the product does not use --
   not --danger. --glow-danger is the palette's own expression of exactly this
   shadow, so the rest state takes the token rather than re-deriving it. */
.om-btn--danger{background:var(--danger);color:var(--text-inverse);box-shadow:var(--glow-danger)}
.om-btn--danger:hover{background:var(--danger);transform:translateY(var(--lift-2));box-shadow:var(--glow-danger)}
.om-btn--danger:active{transform:scale(var(--press-scale))}
.om-btn--danger-ghost{background:transparent;color:var(--danger)}
.om-btn--danger-ghost:hover{background:var(--danger-soft)}

/* Same defect on the success side: the kit's green, not --success. */
.om-btn--success{background:var(--success);color:var(--text-inverse);box-shadow:var(--glow-success)}
.om-btn--success:hover{background:var(--success);transform:translateY(var(--lift-2));box-shadow:var(--glow-success)}
.om-btn--success:active{transform:scale(var(--press-scale))}

.om-btn--loading{pointer-events:none}
.om-btn--loading .om-btn__label{opacity:.35}
.om-btn__spinner{position:absolute;inset:0;z-index:var(--z-local-3);display:grid;place-items:center}
.om-btn__spinner i{display:block;width:16px;height:16px;border-radius:var(--radius-pill);border:2px solid currentColor;border-top-color:transparent;animation:om-spin 620ms var(--ease-linear) infinite}
.om-btn--attention{animation:om-glow-breathe var(--motion-ambient) var(--ease-in-out) infinite}
.om-btn--attention:hover{animation:none}

/* -------------------------- ICON BUTTON -------------------------- */
.om-iconbtn{position:relative;display:inline-grid;place-items:center;width:40px;height:40px;border-radius:var(--radius-md);
  color:var(--text-secondary);background:transparent;border:1px solid transparent;cursor:pointer;
  transition:transform var(--motion-base) var(--ease-out),box-shadow var(--motion-base) var(--ease-out),var(--t-color)}
.om-iconbtn:hover{background:var(--blue-50);color:var(--text-brand);transform:translateY(var(--lift-1))}
.om-iconbtn:active{transform:scale(var(--press-scale));background:var(--blue-100)}
.om-iconbtn:disabled{color:var(--text-disabled);background:transparent;cursor:not-allowed;transform:none}
.om-iconbtn--sm{width:32px;height:32px;border-radius:var(--radius-sm)}
.om-iconbtn--lg{width:48px;height:48px}
.om-iconbtn--round{border-radius:var(--radius-pill)}
.om-iconbtn--outline{border-color:var(--border);background:var(--surface);box-shadow:var(--shadow-sm)}
.om-iconbtn--outline:hover{border-color:var(--blue-300);box-shadow:var(--shadow-sm),var(--glow-xs)}
.om-iconbtn--solid{background:var(--gradient-primary);color:var(--text-inverse);box-shadow:var(--elev-cta)}
.om-iconbtn--solid:hover{background:var(--gradient-primary);color:var(--text-inverse);box-shadow:var(--elev-cta-hover);transform:translateY(var(--lift-2))}
.om-iconbtn--tint{background:var(--blue-50);color:var(--blue-600)}
.om-iconbtn--danger:hover{background:var(--danger-soft);color:var(--danger)}
.om-iconbtn__dot{position:absolute;top:var(--space-15);inset-inline-end:var(--space-15);width:8px;height:8px;border-radius:var(--radius-pill);background:var(--danger);box-shadow:0 0 0 2px var(--surface)}
.om-iconbtn__count{position:absolute;top:var(--space-05);inset-inline-end:var(--space-05);min-width:17px;height:17px;padding:0 var(--space-1);border-radius:var(--radius-pill);
  background:var(--danger);color:var(--text-inverse);font:var(--fw-bold) var(--fs-2xs)/17px var(--font-sans);text-align:center;box-shadow:0 0 0 2px var(--surface)}

/* ----------------------------- BADGE ----------------------------- */
.om-badge{display:inline-flex;align-items:center;gap:var(--space-15);height:24px;padding:0 var(--space-3);border-radius:var(--radius-pill);
  font:var(--fw-bold) var(--fs-2xs)/1 var(--font-sans);letter-spacing:.005em;white-space:nowrap;
  background:var(--n-100);color:var(--text-secondary);border:1px solid transparent;transition:var(--t-all)}
/* The ramp is 8 / 12 / 16 — one --space rung per size. It used to be 8 / 10 / 12,
   and when the base's frozen 10px went onto --space-3 it landed on --lg's 12px and
   the two became the same declaration: a --lg badge had a default badge's side
   padding in a taller box, and the --lg rule was a no-op a later edit would change
   with no effect. Moving --lg up a rung restores a real three-step ramp. */
.om-badge--lg{height:28px;padding:0 var(--space-4);font-size:var(--fs-xs)}
.om-badge--sm{height:20px;padding:0 var(--space-2);font-size:var(--fs-2xs);gap:var(--space-1)}
.om-badge--square{border-radius:var(--radius-sm)}
.om-badge__dot,.om-chip__dot{width:6px;height:6px;border-radius:var(--radius-pill);background:currentColor;flex:none;opacity:.9}
.om-badge--lg .om-badge__dot{width:7px;height:7px}
.om-badge__icon{font-size:var(--icon-micro);margin-inline-start:calc(var(--space-05) * -1)}
.om-badge--pulse .om-badge__dot,.om-chip--pulse .om-chip__dot{animation:om-breathe 1800ms var(--ease-in-out) infinite}
.om-badge--ring{box-shadow:0 0 0 3px var(--om-badge-ring,var(--blue-a12))}
.om-badge--outline{background:transparent;border-color:currentColor}

.om-badge--stable{background:var(--st-completed-bg);color:var(--st-completed-fg)}
.om-badge--observation{background:var(--st-arrived-bg);color:var(--st-arrived-fg)}
.om-badge--critical{background:var(--st-cancelled-bg);color:var(--st-cancelled-fg)}
.om-badge--deceased{background:var(--st-no-show-bg);color:var(--st-no-show-fg)}
.om-badge--waiting{background:var(--st-arrived-bg);color:var(--st-arrived-fg)}
.om-badge--ready{background:var(--st-scheduled-bg);color:var(--st-scheduled-fg)}
.om-badge--inroom{background:var(--st-in-progress-bg);color:var(--st-in-progress-fg)}
.om-badge--done{background:var(--st-completed-bg);color:var(--st-completed-fg)}
.om-badge--noshow{background:var(--st-no-show-bg);color:var(--st-no-show-fg)}
.om-badge--paid{background:var(--pay-paid-bg);color:var(--pay-paid-fg)}
.om-badge--due{background:var(--pay-due-bg);color:var(--pay-due-fg)}
.om-badge--partial{background:var(--pay-partial-bg);color:var(--pay-partial-fg)}
.om-badge--insured{background:var(--pay-insured-bg);color:var(--pay-insured-fg)}
/* Semantic tones. PORT SUPPLEMENT: the source kit shipped only --brand/--neutral/--ink,
   but config/vocabulary.php's non-appointment domains (severity, subscription, activity,
   stock movement) speak in success/warning/danger. Same soft-tint contract as --paid/--due
   /--partial, which are these three tokens under domain names. */
.om-badge--success{background:var(--success-soft);color:var(--success-ink)}
.om-badge--warning{background:var(--warning-soft);color:var(--warning-ink)}
.om-badge--danger{background:var(--danger-soft);color:var(--danger-ink)}
.om-badge--brand{background:var(--blue-50);color:var(--blue-700)}
.om-badge--neutral{background:var(--n-100);color:var(--n-600)}
.om-badge--ink{background:var(--n-900);color:var(--text-inverse)}

/* ---------------------------- AVATAR ---------------------------- */
.om-avatar{position:relative;display:inline-grid;place-items:center;flex:none;width:var(--avatar-md);height:var(--avatar-md);
  border-radius:var(--radius-pill);background:var(--blue-100);color:var(--blue-700);overflow:visible;
  font:var(--fw-bold) var(--fs-xs)/1 var(--font-sans);white-space:nowrap;transition:var(--t-elevate)}
.om-avatar>img{width:100%;height:100%;border-radius:inherit;object-fit:cover}
.om-avatar--xs{width:var(--avatar-xs);height:var(--avatar-xs);font-size:var(--fs-2xs)}
.om-avatar--sm{width:var(--avatar-sm);height:var(--avatar-sm);font-size:var(--fs-xs)}
.om-avatar--lg{width:var(--avatar-lg);height:var(--avatar-lg);font-size:var(--fs-base)}
.om-avatar--xl{width:var(--avatar-xl);height:var(--avatar-xl);font-size:var(--fs-2xl)}
.om-avatar--square{border-radius:var(--radius-lg)}
/* Gender tint + glyph (T8) — the patient "photo", because the system stores no
   portraits. This retires the parallel .gender-avatar family in om-clinic.css and
   design-tokens.css, which declared its own 38/32/56/88 ramp for what is the same
   component; the avatar ramp above is now the only one. Decorative by design — the
   accessible identity is the patient's name beside it and x-gender-chip's label,
   never this tint alone.
   The glyph sizes off the BOX, not off the avatar's own font-size: that font-size
   is scaled for two initials, and a fa- glyph set from it renders about half the
   size the square was drawn for. */
   Radius is --tile, not the existing --square: --square is --radius-lg, which on a
   40px box is a 40% corner and photographs as a circle, and round-vs-square is how
   the product tells a staff avatar from a patient icon. --tile keeps the source
   ramp (--radius-md, stepping up to --radius-lg once the box is big enough to
   carry it) so that distinction survives the merge. */
.om-avatar--tile{border-radius:var(--radius-md)}
.om-avatar--tile.om-avatar--lg,.om-avatar--tile.om-avatar--xl{border-radius:var(--radius-lg)}
.om-avatar__glyph{font-size:var(--fs-lg)}
.om-avatar--sm .om-avatar__glyph{font-size:var(--fs-base)}
.om-avatar--lg .om-avatar__glyph{font-size:var(--fs-2xl)}
.om-avatar--xl .om-avatar__glyph{font-size:var(--fs-4xl)}
.om-avatar--male{background:var(--gender-male-soft);color:var(--gender-male)}
.om-avatar--female{background:var(--gender-female-soft);color:var(--gender-female)}
.om-avatar--other{background:var(--gender-other-soft);color:var(--gender-other)}
.om-avatar--ring-tone{box-shadow:0 0 0 3px var(--surface),0 0 0 5px currentColor}
.om-avatar--ring{box-shadow:0 0 0 2px var(--surface),0 0 0 4px var(--blue-200)}
.om-avatar--glow{box-shadow:0 0 0 3px var(--surface),var(--glow-sm)}
.om-avatar--interactive{cursor:pointer}
.om-avatar--interactive:hover{transform:translateY(var(--lift-1)) scale(var(--hover-scale));box-shadow:var(--shadow-md)}
.om-avatar__status{position:absolute;inset-inline-end:calc(var(--hairline) * -1);bottom:calc(var(--hairline) * -1);width:11px;height:11px;border-radius:var(--radius-pill);border:2px solid var(--surface);background:var(--n-300)}
.om-avatar--lg .om-avatar__status,.om-avatar--xl .om-avatar__status{width:14px;height:14px;border-width:3px}
.om-avatar__status--online{background:var(--success)}
.om-avatar__status--busy{background:var(--danger)}
.om-avatar__status--away{background:var(--warning)}
.om-avatar-stack{display:flex;align-items:center}
.om-avatar-stack>.om-avatar{margin-inline-start:calc(var(--space-2) * -1);box-shadow:0 0 0 2px var(--surface);transition:transform var(--motion-base) var(--ease-out)}
.om-avatar-stack>.om-avatar:first-child{margin-inline-start:0}
.om-avatar-stack:hover>.om-avatar{transform:translateY(var(--lift-2))}

/* ---------------------- SEGMENTED CONTROL ---------------------- */
.om-seg{position:relative;display:inline-flex;align-items:center;gap:var(--space-05);padding:var(--space-05);
  background:var(--surface-sunken);border:1px solid var(--border-light);border-radius:var(--radius-md)}
/* see kit-port-notes.md: om-seg__thumb - om-form.js sets BOTH insets */
.om-seg__thumb{position:absolute;top:var(--space-05);bottom:var(--space-05);inset-inline-start:0;inset-inline-end:100%;border-radius:calc(var(--radius-md) - var(--space-05));background:var(--surface);
  box-shadow:var(--shadow-sm),var(--ring-hairline-soft);
  transition:inset-inline-start var(--motion-slow) var(--ease-spring),inset-inline-end var(--motion-slow) var(--ease-spring)}
.om-seg__btn{position:relative;z-index:var(--z-local-1);display:inline-flex;align-items:center;gap:var(--space-15);height:32px;padding:0 var(--space-3);
  border-radius:calc(var(--radius-md) - var(--space-05));font:var(--fw-medium) var(--fs-xs)/1 var(--font-sans);color:var(--text-secondary);
  white-space:nowrap;transition:var(--t-color)}
.om-seg__btn:hover{color:var(--text-primary)}
.om-seg__btn[aria-selected="true"]{color:var(--text-brand);font-weight:var(--fw-bold)}
.om-seg__count{display:inline-grid;place-items:center;min-width:18px;height:18px;padding:0 var(--space-15);border-radius:var(--radius-pill);
  background:var(--n-200);color:var(--text-secondary);font:var(--fw-bold) var(--fs-2xs)/1 var(--font-sans);transition:var(--t-color)}
.om-seg__btn[aria-selected="true"] .om-seg__count{background:var(--blue-500);color:var(--text-inverse)}
.om-seg--lg .om-seg__btn{height:38px;padding:0 var(--space-4);font-size:var(--fs-sm)}
.om-seg--block{display:flex;width:100%}
.om-seg--block .om-seg__btn{flex:1;justify-content:center}

/* ============================= FORMS ============================= */
.om-field{display:flex;flex-direction:column;gap:var(--space-2)}
.om-field__label{display:flex;align-items:center;gap:var(--space-15);font:var(--type-label);color:var(--text-secondary);
  padding-inline-start:var(--space-05);transition:var(--t-color)}
.om-field__label em{font-style:normal;color:var(--danger)}
.om-field:focus-within .om-field__label{color:var(--text-brand)}
.om-field__hint{font:var(--type-caption);color:var(--text-muted);padding-inline-start:var(--space-05)}
.om-field__msg{display:flex;align-items:center;gap:var(--space-15);font:var(--type-caption);padding-inline-start:var(--space-05);animation:om-rise-sm var(--motion-base) var(--ease-out)}
.om-field__msg--error{color:var(--danger)}
.om-field__msg--success{color:var(--success)}

.om-input{position:relative;display:flex;align-items:center;gap:var(--space-2);
  height:var(--control-h);padding:0 var(--space-3);background:var(--surface);
  border:1px solid var(--border);border-radius:var(--radius-md);
  transition:border-color var(--motion-fast) var(--ease-out),box-shadow var(--motion-base) var(--ease-out),background-color var(--motion-fast) var(--ease-out)}
.om-input:hover{border-color:var(--border-strong)}
.om-input:focus-within{border-color:var(--primary);box-shadow:var(--glow-focus)}
.om-input>input,.om-input>select,.om-input>textarea{flex:1;min-width:0;width:100%;height:100%;border:0;outline:none;background:transparent;
  font:var(--fw-medium) var(--fs-sm)/1.4 var(--font-sans);letter-spacing:var(--ls-base);color:var(--text-primary)}
.om-input__icon{flex:none;font-size:var(--icon-tab);color:var(--text-muted);transition:var(--t-color)}
.om-input:focus-within .om-input__icon{color:var(--text-brand)}
.om-input__affix{flex:none;font:var(--fw-medium) var(--fs-xs)/1 var(--font-sans);color:var(--text-muted)}
.om-input--sm{height:var(--control-h-sm);border-radius:var(--radius-sm);padding:0 var(--space-3)}
.om-input--sm>input{font-size:var(--fs-xs)}
.om-input--lg{height:var(--control-h-lg);border-radius:var(--radius-lg)}
.om-input--pill{border-radius:var(--radius-pill);padding:0 var(--space-4)}
.om-input--sunken{background:var(--surface-sunken);border-color:transparent}
.om-input--sunken:focus-within{background:var(--surface)}
.om-input--error{border-color:var(--danger)}
.om-input--error:focus-within{border-color:var(--danger);box-shadow:var(--glow-danger)}
.om-input--error .om-input__icon{color:var(--danger)}
.om-input--success{border-color:var(--success)}
.om-input--success:focus-within{border-color:var(--success);box-shadow:var(--glow-success)}
.om-input--disabled,.om-input--disabled:hover{background:var(--surface-disabled);border-color:transparent;box-shadow:none}
.om-input--disabled>input{color:var(--text-disabled);cursor:not-allowed}
.om-input--textarea{height:auto;min-height:104px;padding:var(--space-3) var(--space-3);align-items:flex-start}
.om-input--textarea>textarea{height:auto;min-height:80px;resize:vertical;line-height:var(--lh-base)}

.om-select{position:relative}
/* Clearance is DERIVED from the chevron it must clear, not guessed. The chevron
   sits at inset-inline-end:var(--space-3) and is --icon-tile wide, so the text
   has to stop one --space-1 before its far edge.
   A plain --space-5 is wrong here twice over: it is density-remapped, so it shrank
   to 17.6px at compact — the CLINICAL DEFAULT — and put the text 2.4px UNDER the
   chevron; and even the old fixed 24px left only 5.2px, which the survey had
   already flagged as too tight. Expressed as a relationship, it cannot drift again
   if the chevron moves or grows. */
.om-select>select{appearance:none;-webkit-appearance:none;padding-inline-end:calc(var(--space-3) + var(--icon-tile) + var(--space-1));cursor:pointer}
.om-select__chev{position:absolute;inset-inline-end:var(--space-3);pointer-events:none;font-size:var(--icon-tile);color:var(--text-muted);
  transition:transform var(--motion-base) var(--ease-out),color var(--motion-fast) var(--ease-out)}
.om-select:focus-within .om-select__chev{transform:rotate(180deg);color:var(--text-brand)}

.om-search{display:flex;align-items:center;gap:var(--space-3);height:44px;padding:0 var(--space-4);
  background:var(--surface-sunken);border:1px solid transparent;border-radius:var(--radius-pill);
  transition:var(--t-all)}
.om-search:hover{background:var(--n-100)}
.om-search:focus-within{background:var(--surface);border-color:var(--blue-200);box-shadow:var(--glow-focus)}
.om-search>input{flex:1;min-width:0;border:0;outline:none;background:transparent;font:var(--fw-medium) var(--fs-sm)/1 var(--font-sans);color:var(--text-primary)}
.om-search__icon{font-size:var(--icon-tab);color:var(--text-muted);transition:var(--t-color)}
.om-search:focus-within .om-search__icon{color:var(--text-brand)}
.om-search__kbd{flex:none;padding:var(--space-05) var(--space-15);border-radius:var(--radius-sm);background:var(--surface);border:1px solid var(--border);
  font:var(--fw-bold) var(--fs-2xs)/1.4 var(--font-sans);color:var(--text-muted);box-shadow:var(--shadow-sm)}
.om-search:focus-within .om-search__kbd{opacity:0}

/* SCREEN-READER-ONLY. It lived in om-clinic.css, which is the APP sheet, while
   the element that needs it is the real <input> behind .om-check right below —
   so a standalone page showing one checkbox had to link the whole app sheet for
   this one rule. --hairline is the token for exactly this job: om-tokens.css
   calls it "cancels a border, clips an a11y box".
   Table captions and skip links read it here too. */
.visually-hidden{position:absolute!important;width:var(--hairline);height:var(--hairline);padding:0;margin:calc(var(--hairline) * -1);overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
.om-check{display:inline-flex;align-items:flex-start;gap:var(--space-3);cursor:pointer}
.om-check__box{position:relative;flex:none;display:grid;place-items:center;width:20px;height:20px;margin-top:var(--hairline);
  border:1.5px solid var(--border-strong);border-radius:var(--radius-sm);background:var(--surface);
  transition:var(--t-all)}
.om-check:hover .om-check__box{border-color:var(--blue-400);box-shadow:var(--glow-xs)}
.om-check__box>i{font-size:var(--icon-btn);color:var(--text-inverse);opacity:0;transform:scale(.4);
  transition:opacity var(--motion-fast) var(--ease-out),transform var(--motion-base) var(--ease-spring)}
.om-check input{position:absolute;opacity:0;width:0;height:0}
.om-check input:checked+.om-check__box{background:var(--gradient-primary);border-color:var(--blue-500);box-shadow:var(--glow-sm)}
.om-check input:checked+.om-check__box>i{opacity:1;transform:scale(1)}
.om-check input:indeterminate+.om-check__box{background:var(--gradient-primary);border-color:var(--blue-500)}
.om-check input:indeterminate+.om-check__box>i{opacity:1;transform:scale(1)}
.om-check input:focus-visible+.om-check__box{box-shadow:var(--glow-ring)}
.om-check input:disabled+.om-check__box{background:var(--surface-disabled);border-color:var(--border);box-shadow:none}
.om-check__text{font:var(--type-body);color:var(--text-primary)}
.om-check__text small{display:block;font:var(--type-caption);color:var(--text-muted);margin-top:var(--hairline)}
.om-check--disabled{cursor:not-allowed}
.om-check--disabled .om-check__text{color:var(--text-disabled)}

.om-radio{display:inline-flex;align-items:flex-start;gap:var(--space-3);cursor:pointer}
.om-radio__box{position:relative;flex:none;display:grid;place-items:center;width:20px;height:20px;margin-top:var(--hairline);
  border:1.5px solid var(--border-strong);border-radius:var(--radius-pill);background:var(--surface);transition:var(--t-all)}
.om-radio:hover .om-radio__box{border-color:var(--blue-400);box-shadow:var(--glow-xs)}
.om-radio__box::after{content:"";width:8px;height:8px;border-radius:var(--radius-pill);background:var(--text-inverse);
  transform:scale(0);transition:transform var(--motion-base) var(--ease-spring)}
.om-radio input{position:absolute;opacity:0;width:0;height:0}
.om-radio input:checked+.om-radio__box{background:var(--gradient-primary);border-color:var(--blue-500);box-shadow:var(--glow-sm)}
.om-radio input:checked+.om-radio__box::after{transform:scale(1)}
.om-radio input:focus-visible+.om-radio__box{box-shadow:var(--glow-ring)}
.om-radio input:disabled+.om-radio__box{background:var(--surface-disabled);border-color:var(--border);box-shadow:none}
.om-radio-card{position:relative;display:flex;align-items:flex-start;gap:var(--space-3);padding:var(--space-4) var(--space-4);
  border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface);cursor:pointer;transition:var(--t-all)}
.om-radio-card:hover{border-color:var(--blue-300);background:var(--blue-50)}
.om-radio-card--selected{border-color:var(--primary);background:var(--blue-50);box-shadow:var(--glow-ring)}
/* Live selection: the tile follows the radio without a reload; --selected is the SSR hint. */
.om-radio-card:has(input:checked){border-color:var(--primary);background:var(--blue-50);box-shadow:var(--glow-ring)}
.om-radio-card input{position:absolute;opacity:0;width:0;height:0}
.om-radio-card:hover .om-radio__box{border-color:var(--blue-400);box-shadow:var(--glow-xs)}
.om-radio-card input:checked+.om-radio__box{background:var(--gradient-primary);border-color:var(--blue-500);box-shadow:var(--glow-sm)}
.om-radio-card input:checked+.om-radio__box::after{transform:scale(1)}
.om-radio-card input:focus-visible+.om-radio__box{box-shadow:var(--glow-ring)}

/* Switch - kit geometry; the thumb travels the inline axis. Conversion 1. */
.om-switch{position:relative;display:inline-block;width:var(--switch-w);height:var(--switch-h);flex:none}
.om-switch input{position:absolute;opacity:0;width:0;height:0}
.om-switch__track{display:block;width:var(--switch-w);height:var(--switch-h);border-radius:var(--radius-pill);background:var(--n-200);
  cursor:pointer;transition:background-color var(--motion-slow) var(--ease-out),box-shadow var(--motion-slow) var(--ease-out)}
.om-switch__track::before{content:"";position:absolute;top:50%;inset-inline-start:var(--space-05);width:var(--switch-thumb);height:var(--switch-thumb);border-radius:var(--radius-pill);background:var(--text-inverse);
  transform:translateY(-50%);box-shadow:0 2px 5px var(--ink-a24);
  transition:inset-inline-start var(--motion-slow) var(--ease-spring)}
.om-switch input:checked+.om-switch__track{background:var(--blue-500);box-shadow:var(--glow-sm)}
.om-switch input:checked+.om-switch__track::before{inset-inline-start:calc(var(--switch-w) - var(--switch-thumb) - var(--space-05))}
.om-switch input:focus-visible+.om-switch__track{box-shadow:var(--glow-ring)}
.om-switch input:disabled+.om-switch__track{background:var(--surface-disabled);cursor:not-allowed;box-shadow:none}
.om-switch--danger input:checked+.om-switch__track{background:var(--danger);box-shadow:var(--glow-danger)}
.om-switch--success input:checked+.om-switch__track{background:var(--success);box-shadow:var(--glow-success)}

.om-switchfield{display:flex;align-items:center;justify-content:space-between;gap:var(--space-5);
  padding:var(--space-4) var(--space-4);border-radius:var(--radius-md);transition:background-color var(--motion-fast) var(--ease-out)}
.om-switchfield:hover{background:var(--surface-hover)}
.om-switchfield__text{display:flex;flex-direction:column;gap:var(--space-05)}
.om-switchfield__title{font:var(--type-body-strong);color:var(--text-primary)}
.om-switchfield__desc{font:var(--type-caption);color:var(--text-muted);max-width:52ch}

.om-form-grid{display:grid;gap:var(--space-5);grid-template-columns:repeat(2,minmax(0,1fr))}
.om-form-grid--1{grid-template-columns:1fr}
/* Controls that belong on ONE line — a settle box beside its method select
   beside its submit, in a table cell or a toolbar. align-items:flex-end so a
   labelled field and a bare button share a baseline instead of the button
   floating to the top of the taller control. */
.om-inlineform{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--space-2)}
.om-form-grid__full{grid-column:1/-1}
/* .om-form-grid's single-column branch moved into the block below the pager:
   this region now has ONE mobile block, not three. */

/* ---------------------------- TABLE ---------------------------- */
/* The index archetype's card. Was .data-table in om-clinic.css (T9). overflow
   is hidden so the rounded corner clips the sticky header; the clinical variant
   deliberately does NOT get this card, because a row tooltip has to escape it. */
.om-tablecard{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);
  box-shadow:var(--shadow-sm);overflow:hidden}
/* The clinical board. It differs from the admin card on the one property that
   matters, `overflow`: a queue row opens tooltips that must escape it, and
   --live drops the column widths to table-layout:fixed so no width produces a
   scrollbar. Cells must not clip either; truncation belongs to the inner text.
   Was .live-table in om-clinic.css, and THAT BLOCK IS NOT RETIRED: the two
   dashboards still hand-roll `<div class="live-table">` around a raw <table>. */
.om-tablecard--live{overflow:visible}
.om-tablecard--live .om-table{table-layout:fixed}
.om-tablecard--live .om-table td{overflow:visible}
.om-tablecard--live .om-table td>*{max-width:100%}
/* Responsive visibility, used by the table's card-list swap. Was .hide-mobile /
   .only-mobile in om-clinic.css. `display:flex` on the mobile side is not a
   generic choice — .om-cardlist IS a column flexbox and inherits it here.

   THE DOUBLED CLASS IS LOAD-BEARING, NOT A TYPO. Written once (0,1,0) these
   helpers lost to any LATER single-class rule that set `display` on the same
   element, and one did: `.om-cardlist{display:flex}` sits ~110 lines below and
   re-enabled the card list at EVERY width. MEASURED on /pharmacy/receivables at
   1280 — .om-table-wrap display:block w=950 AND .om-cardlist display:flex w=950,
   both visible, every row listed twice, each copy carrying its own settle form.
   Position, not specificity, decided it, which is the trap a media query always
   sets. Self-doubling lifts the pair to (0,2,0) so NO single-class rule can tie
   or beat them from anywhere in any sheet, and the fix stops depending on where
   the next component rule happens to land. gate-visibility-helpers.sh enforces
   the doubling; un-double either line and it fails at commit. */
.om-hide-mobile.om-hide-mobile{display:block}
.om-only-mobile.om-only-mobile{display:none}
/* Always-on scroll wrapper. See kit-port-notes.md: om-table-wrap.
   THE 360px RULE. A wide table scrolls INSIDE its own box; the PAGE never scrolls
   sideways. overflow-x:auto alone does not deliver that: a grid or flex item's
   min-width defaults to auto, so the wrapper is sized by its widest content and
   pushes the document instead of scrolling. min-width:0 — plus max-width:100% for
   the block-layout case, where the parent has no intrinsic width to constrain it
   — is what makes the declared overflow actually take effect. */
.om-table-wrap{position:relative;width:100%;max-width:100%;min-width:0;overflow-x:auto}
/* Clinical boards opt OUT: table-layout:fixed + column dropping, so a row
   tooltip can escape the row. See kit-port-notes.md: om-table-wrap. */
.om-table-wrap--fixed{overflow-x:visible}
.om-table{width:100%;border-collapse:separate;border-spacing:0;min-width:var(--om-table-min,0)}
.om-table--min-md{--om-table-min:720px}
.om-table--min-lg{--om-table-min:980px}
.om-table thead th{position:sticky;top:0;z-index:var(--z-local-2);background:var(--surface);
  padding:var(--space-3) var(--pad-cell-x) var(--space-3);text-align:start;white-space:nowrap;
  border-bottom:1px solid var(--border);
  font:var(--type-overline);letter-spacing:var(--ls-caps-wide);text-transform:uppercase;color:var(--text-muted)}
.om-table thead th.om-th--sortable{cursor:pointer;user-select:none;transition:var(--t-color)}
.om-table thead th.om-th--sortable:hover{color:var(--text-brand)}
.om-th__inner{display:inline-flex;align-items:center;gap:var(--space-1)}
.om-th__caret{font-size:var(--icon-btn);opacity:0;margin-inline-start:calc(var(--space-05) * -1);
  transition:opacity var(--motion-fast) var(--ease-out),transform var(--motion-base) var(--ease-out)}
.om-th--sortable:hover .om-th__caret{opacity:.45}
.om-th--sorted .om-th__caret{opacity:1;color:var(--text-brand)}
.om-th--sorted-desc .om-th__caret{transform:rotate(180deg)}
.om-table thead th.om-th--end,.om-table td.om-td--end{text-align:end}
.om-table thead th.om-th--center,.om-table td.om-td--center{text-align:center}

.om-table tbody td{padding:var(--pad-cell-y) var(--pad-cell-x);border-bottom:1px solid var(--border-light);
  font:var(--type-body-sm);color:var(--text-primary);vertical-align:middle}
.om-table tbody tr{position:relative;transition:background-color var(--motion-fast) var(--ease-out)}
.om-table tbody tr:last-child td{border-bottom:0}
.om-table tbody tr.om-tr:hover{background:var(--blue-50)}
.om-table tbody tr.om-tr>td:first-child{position:relative}
.om-table tbody tr.om-tr>td:first-child::before{content:"";position:absolute;inset-inline-start:0;top:var(--space-15);bottom:var(--space-15);width:3px;
  border-start-end-radius:var(--radius-2xs);border-end-end-radius:var(--radius-2xs);background:var(--blue-500);transform:scaleY(0);transform-origin:top;
  transition:transform var(--motion-base) var(--ease-out)}
.om-table tbody tr.om-tr:hover>td:first-child::before{transform:scaleY(1)}
.om-table tbody tr.om-tr--selected{background:var(--surface-selected)}
.om-table tbody tr.om-tr--selected>td:first-child::before{transform:scaleY(1)}
.om-table tbody tr.om-tr--critical>td:first-child::before{background:var(--danger);transform:scaleY(1)}
.om-table tbody tr.om-tr--muted td{color:var(--text-muted)}
.om-table tbody tr.om-tr--muted .om-cell__name{color:var(--text-secondary)}
.om-table--compact tbody td{padding:var(--space-2) var(--pad-cell-x)}
.om-table--compact thead th{padding:var(--space-3) var(--pad-cell-x) var(--space-2)}

.om-cell-id{display:flex;align-items:center;gap:var(--space-3);min-width:0}
.om-cell__name{font:var(--type-body-strong);color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.om-cell__sub{font:var(--fw-regular) var(--fs-xs)/1.35 var(--font-sans);color:var(--text-muted);
  font-variant-numeric:tabular-nums;white-space:nowrap}
.om-cell-stack{display:flex;flex-direction:column;gap:var(--space-05);min-width:0}
.om-cell-time{display:flex;flex-direction:column;gap:var(--space-05)}
.om-cell-time b{font:var(--type-body-strong);font-variant-numeric:tabular-nums;color:var(--text-primary)}
.om-cell-time span{font:var(--fw-regular) var(--fs-2xs)/1.3 var(--font-sans);color:var(--text-muted);text-transform:uppercase;letter-spacing:var(--ls-caps)}

.om-table__toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--space-4);flex-wrap:wrap;padding:var(--pad-card) var(--pad-card) var(--space-4)}
.om-table__foot{display:flex;align-items:center;justify-content:space-between;gap:var(--space-4);padding:var(--space-4) var(--pad-card);border-top:1px solid var(--border-light)}
.om-table__count{font:var(--type-caption);color:var(--text-muted)}

/* ---- PAGER (T9) — the index archetype's third control, after sort and filter.
   Laravel rendered `->links()` through Paginator::useBootstrapFive(), so every
   paginated screen in the app emitted .pagination/.page-item/.page-link and was
   repainted by four rules in design-tokens.css plus two !important overrides in
   doctor-mobile.css. The paginator VIEW is the single place that markup is
   produced, so replacing it retires all six at once and no call site changes.
   Targets are --tap-min: a pager is the one control a phone user aims at most
   and the Bootstrap default was 36px wide with a 0.78rem label. */
.om-pager{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:var(--space-1);margin:0;padding:0;list-style:none}
/* The <li> stays for the list semantics and stops being a box: without this the
   flex items are the list items and every sizing rule below lands on the wrong
   element — measured, the mobile branch produced a 34.5px control inside a
   44px-tall row, which is the same defect the doctor-mobile rules had. */
.om-pager>li{display:contents}
.om-pager__link{display:inline-flex;align-items:center;justify-content:center;min-width:var(--tap-min);height:var(--tap-min);
  padding:0 var(--space-3);border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);
  color:var(--primary);font:var(--fw-bold) var(--fs-sm)/1 var(--font-sans);font-variant-numeric:tabular-nums;
  text-decoration:none;transition:var(--t-all)}
.om-pager__link:hover{background:var(--primary-soft);border-color:var(--primary);color:var(--primary)}
.om-pager__link:focus-visible{outline:none;box-shadow:var(--shadow-ring)}
.om-pager__link--current{background:var(--gradient-primary);border-color:transparent;color:var(--text-inverse);box-shadow:var(--elev-cta)}
.om-pager__link--disabled{background:var(--surface-sunken);border-color:var(--border-light);color:var(--text-muted);pointer-events:none}
/* The … separator is not a control: it must not look like one or take a tab stop. */
.om-pager__gap{display:inline-flex;align-items:center;justify-content:center;min-width:var(--space-6);height:var(--tap-min);color:var(--text-muted)}
/* THE MOBILE BLOCK for the form-grid / table / pager region — one block rather
   than three, and it sits here because a media query carries no specificity: the
   visibility pair below has to come AFTER .om-only-mobile's base rule or it loses
   to it on position alone. Both arms are doubled for the reason given at the base
   rule; they still have to beat the base rule, and equal specificity + later
   position is what does that.
   The pager drops its numbered rungs under 768px, leaving prev/next to share the
   row. Laravel's `onEachSide` cannot express a breakpoint, so the trim is here. */
@media (max-width:767px){
  .om-form-grid{grid-template-columns:1fr}
  .om-hide-mobile.om-hide-mobile{display:none}
  .om-only-mobile.om-only-mobile{display:flex}
  /* The clinical board stops being unclipped below 768px. --fixed exists so a row
     tooltip can escape the card (D8) on the assumption that table-layout:fixed
     never needs a scrollbar — true at desktop and tablet, false at 360px, where
     MEASURED the seven-column board pushed the document to scrollWidth 370 in a
     360 viewport by way of its hover-reveal action group. Nothing is given up:
     the (hover:none) branch already reveals those actions inline, and a tooltip
     that must escape its card is a pointer affordance this width lacks. */
  .om-table-wrap--fixed{overflow-x:auto}
  .om-pager__link--num,.om-pager__gap{display:none}
  /* min-width stays --tap-min: growing a control must never shrink it below the floor. */
  .om-pager__link{flex:1 1 auto}

  /* THE GENERIC CARD LIST. A row is a card, a cell is a labelled line, the
     header goes with the scrollbar, the last cell is the row's actions. For the
     59 of 64 x-data-tables with no hand-written `cards` slot, which slid
     sideways under a thumb — MEASURED a 723px table in a 368px window with two
     26px buttons in it. The label comes from om-table.js. §19. */
  [data-om-autocards] .om-table-wrap{overflow:visible}
  [data-om-autocards] .om-table{display:block;min-width:0}
  [data-om-autocards] .om-table thead{display:none}
  [data-om-autocards] .om-table tbody{display:flex;flex-direction:column;gap:var(--space-3);padding:var(--space-3)}
  [data-om-autocards] .om-table tbody tr{display:flex;flex-direction:column;gap:var(--space-2);
    padding:var(--space-3) var(--space-4);background:var(--surface);border:1px solid var(--border);
    border-radius:var(--radius-md);box-shadow:var(--shadow-sm)}
  [data-om-autocards] .om-table tbody td{display:flex;align-items:center;justify-content:space-between;
    gap:var(--space-3);width:auto;height:auto;min-height:0;padding:0;border:0;background:transparent;text-align:start}
  [data-om-autocards] .om-table tbody td[data-om-label]::before{content:attr(data-om-label);flex:none;
    font:var(--type-caption);color:var(--text-muted)}
  /* The first cell leads the card and the last one acts on it: neither is a
     label/value pair. */
  [data-om-autocards] .om-table tbody td:first-child::before,
  [data-om-autocards] .om-table tbody td:last-child::before{content:none}
  [data-om-autocards] .om-table tbody td:last-child{justify-content:stretch;padding-top:var(--space-2);
    border-top:1px solid var(--border)}
  [data-om-autocards] .om-table tbody td:last-child>*{flex:1}
  [data-om-autocards] .om-table tbody td[colspan]{justify-content:center;border:0;padding:var(--space-4)}
  [data-om-autocards] .om-table tbody td:empty{display:none}
  [data-om-autocards] .om-actiongrid{max-width:none;width:100%}
}

.om-cardlist{display:flex;flex-direction:column;gap:var(--space-3);padding:var(--space-3)}
.om-rowcard{display:flex;flex-direction:column;gap:var(--space-3);padding:var(--space-4);background:var(--surface);
  border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-sm);
  transition:var(--t-elevate);-webkit-tap-highlight-color:transparent}
/* One surface: the card had no :hover, so only the button under the pointer lit
   up while the card stayed white. The card leads; secondary drops its fill. */
.om-rowcard:hover{border-color:var(--blue-300);background:var(--blue-50);box-shadow:var(--shadow-md)}
.om-rowcard:hover .om-btn--secondary{background:transparent}
.om-rowcard:active{transform:scale(.99);box-shadow:none}
.om-rowcard--critical{border-color:var(--danger-soft);background:linear-gradient(0deg,var(--surface),var(--surface)) padding-box,var(--danger-soft) border-box}
.om-rowcard__top{display:flex;align-items:center;gap:var(--space-3)}
/* Grid, not flex-wrap: unequal facts wrapped raggedly and left labels unaligned. */
.om-rowcard__meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(6rem,1fr));
  gap:var(--space-2) var(--space-3);padding-top:var(--space-3);border-top:1px dashed var(--border)}
.om-rowcard__kv{display:flex;flex-direction:column;gap:var(--space-05);min-width:0}
/* No uppercase, no caps tracking: uppercase is a no-op on Arabic and tracking
   breaks its cursive joining. Weight and colour carry the label. */
.om-rowcard__kv span{font:var(--type-overline);color:var(--text-muted)}
.om-rowcard__kv b{font:var(--type-body-strong);font-variant-numeric:tabular-nums;color:var(--text-primary)}
.om-rowcard__cta{display:flex;gap:var(--space-3)}

/* ROSTER VARIANT — a patient list is scanned, not read. The base card stacks
   label over value and gives two actions equal weight; identity leads here, the
   three facts sit on one line, and one action is obvious. Labels are clipped,
   NOT display:none, so a screen reader still hears them. Tokens only, so both
   themes and every clinic palette follow. */
.om-rowcard--roster{gap:var(--space-2);padding:var(--space-3) var(--space-4)}
.om-rowcard--roster .om-avatar{width:var(--tap-min);height:var(--tap-min);font-size:var(--fs-lg)}
.om-rowcard--roster .om-rowcard__meta{display:flex;flex-wrap:wrap;
  gap:var(--space-1) var(--space-3);padding-top:var(--space-2);
  border-top:1px solid var(--border-light)}
.om-rowcard--roster .om-rowcard__kv{flex-direction:row;align-items:baseline;gap:var(--space-1)}
.om-rowcard--roster .om-rowcard__kv span{position:absolute;width:.0625rem;height:.0625rem;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap}
.om-rowcard--roster .om-rowcard__kv b{font:var(--type-body-sm);font-weight:var(--fw-bold)}
.om-rowcard--roster .om-rowcard__kv:not(:last-child)::after{content:"·";color:var(--text-faint);
  margin-inline-start:var(--space-2)}
.om-rowcard--roster .om-rowcard__cta{gap:var(--space-2)}
.om-rowcard--roster .om-rowcard__cta>:first-child{flex:1 1 auto}
.om-rowcard--roster .om-rowcard__cta>:last-child{flex:0 0 var(--tap-min);
  width:var(--tap-min);min-width:var(--tap-min);padding:0}
/* font-size:0, NOT display:none — the icon lives INSIDE .om-btn__label, so
   hiding the label hid the stethoscope with it (measured 0x0). Zeroing the font
   collapses the text node while .om-icon--btn keeps its own explicit size. */
.om-rowcard--roster .om-rowcard__cta>:last-child .om-btn__label{font-size:0;gap:0}

/* --------------------------- PROGRESS --------------------------- */
.om-progress{position:relative;width:100%;height:8px;border-radius:var(--radius-pill);background:var(--n-200);overflow:hidden}
.om-progress--thin{height:5px}
.om-progress--thick{height:12px}
/* display:block is load-bearing, not cosmetic. The fill is a <span>, so without
   it the box is display:inline and width/height DO NOT APPLY to a non-replaced
   inline element — the bar paints an empty track at every value and the striped
   ::after never renders. .om-funnel escapes this only because it is display:flex,
   which blockifies its span children. Beware: getComputedStyle().width reports
   the COMPUTED value ("98%") for an inline box whose USED width is 0, so a
   computed-style probe reads back as healthy. Measure geometry with
   getBoundingClientRect()/offsetWidth. */
.om-progress__fill{display:block;height:100%;border-radius:var(--radius-pill);background:var(--gradient-primary);
  transition:width var(--motion-slow) var(--ease-out);box-shadow:var(--glow-inset)}
.om-progress--success .om-progress__fill{background:var(--success)}
.om-progress--warning .om-progress__fill{background:var(--warning)}
.om-progress--danger .om-progress__fill{background:var(--danger)}
/* The barber-pole is white at 28%. The white-alpha ladder has no .28 step, so
   the stripes ride their own overlay and carry the alpha as opacity — exact,
   and no colour notation. */
.om-progress--striped .om-progress__fill{position:relative}
.om-progress--striped .om-progress__fill::after{content:"";position:absolute;inset:0;border-radius:inherit;opacity:.28;
  background-image:linear-gradient(115deg,var(--text-inverse) 25%,transparent 25% 50%,var(--text-inverse) 50% 75%,transparent 75%);
  background-size:24px 24px;animation:om-progress-stripe 900ms var(--ease-linear) infinite}
.om-progress-row{display:flex;align-items:center;gap:var(--space-3)}
.om-progress-row b{font:var(--type-body-strong);font-variant-numeric:tabular-nums;color:var(--text-primary);min-width:38px;text-align:end}

.om-ring{position:relative;display:grid;place-items:center}
.om-ring svg{transform:rotate(-90deg)}
.om-ring__track{stroke:var(--n-200);fill:none}
.om-ring__fill{stroke:var(--blue-500);fill:none;stroke-linecap:round;transition:stroke-dashoffset var(--motion-slow) var(--ease-out)}
.om-ring__label{position:absolute;text-align:center;font:var(--fw-bold) var(--fs-base)/1 var(--font-sans);font-variant-numeric:tabular-nums;color:var(--text-primary)}
.om-ring__label small{display:block;font:var(--fw-regular) var(--fs-2xs)/1.3 var(--font-sans);color:var(--text-muted);text-transform:uppercase;letter-spacing:var(--ls-caps);margin-top:var(--space-05)}

/* -------------------------- STAT WIDGET -------------------------- */
.om-stat{display:flex;align-items:center;gap:var(--space-4);padding:var(--space-4) var(--space-4);
  background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);
  box-shadow:var(--card-shadow);transition:var(--t-elevate),border-color var(--motion-fast) var(--ease-out)}
.om-stat:hover{transform:translateY(var(--lift-2));box-shadow:var(--card-shadow-hover);border-color:var(--blue-200)}
/* x-stat-tile becomes an <a> when it is given href, and a link colour on a card
   would repaint the whole tile — the lift and the border ARE the affordance. The
   underline half of this is now handled for every control family at once, in
   om-base.css's link-hover rule; only the colour reset still has to be said. */
a.om-stat,a.om-stat:hover{color:inherit}
.om-stat__icon{display:grid;place-items:center;flex:none;width:52px;height:52px;border-radius:var(--radius-pill);
  background:var(--primary-soft);color:var(--blue-500);font-size:var(--icon-glyph);
  transition:box-shadow var(--motion-slow) var(--ease-out),transform var(--motion-base) var(--ease-spring)}
.om-stat:hover .om-stat__icon{box-shadow:var(--glow-md);transform:scale(1.06)}
.om-stat__body{display:flex;flex-direction:column;gap:var(--space-05);min-width:0}
/* NOT nowrap. It was, while nothing emitted .om-stat — and the moment T12 pointed
   x-stat-tile here, a label like "إجمالي المصاريف الشهرية" in a 190px grid track
   at 360px would have run out of its own card, because nothing clips it. The
   vocabulary this replaces (.tile-label) wrapped, so wrapping is also the
   behaviour the 14 live tiles already had. */
.om-stat__label{font:var(--type-label);color:var(--text-muted)}
.om-stat__value{font:var(--type-stat);font-variant-numeric:tabular-nums;color:var(--text-primary);letter-spacing:var(--ls-tight)}
.om-stat__value em{font-style:normal;font-size:var(--fs-base);font-weight:var(--fw-medium);color:var(--text-muted);margin-inline-start:var(--space-05)}
.om-stat__delta{display:inline-flex;align-items:center;gap:var(--space-05);font:var(--fw-bold) var(--fs-2xs)/1 var(--font-sans)}
.om-stat__delta--up{color:var(--success)}
.om-stat__delta--down{color:var(--danger)}
.om-stat__delta--flat{color:var(--text-muted)}
/* القطبية | Polarity. --up/--down state the DIRECTION and drive the arrow and the
   spoken word; these two invert only the COLOUR, for a metric where a rise is bad
   news. Without them a climbing no-show or cancellation rate rendered green in the
   one section built to say otherwise. --muted is for a change with no good or bad
   to it, like a shift in visit mix. */
.om-stat__delta--inverted.om-stat__delta--up{color:var(--danger)}
.om-stat__delta--inverted.om-stat__delta--down{color:var(--success)}
.om-stat__delta--muted{color:var(--text-muted)}
.om-stat__delta i{font-size:var(--icon-micro)}
.om-stat--tinted{background:var(--gradient-primary);border-color:transparent;color:var(--text-inverse);box-shadow:var(--glow-md)}
/* On the tinted (gradient) stat the muted inks are white at a partial alpha.
   Carrying that alpha as opacity on the leaf reproduces the composite exactly
   without a colour literal; the white-alpha ladder has no .78/.70 step. */
.om-stat--tinted .om-stat__label{color:var(--text-inverse);opacity:.78}
.om-stat--tinted .om-stat__value{color:var(--text-inverse)}
.om-stat--tinted .om-stat__value em{color:var(--text-inverse);opacity:.7}
.om-stat--tinted .om-stat__delta{color:var(--text-inverse)}
/* A fill and a shadow colour cannot carry their alpha as opacity, so these two
   need a real rung. --white-a18/--white-a60 were added to the om-tokens.css
   additions layer for exactly this; the alphas are the kit's own. */
.om-stat--tinted .om-stat__icon{background:var(--white-a18);color:var(--text-inverse)}
.om-stat--tinted:hover .om-stat__icon{box-shadow:0 0 22px -4px var(--white-a60)}
/* Tone modifiers are named for the VARIANT the component takes, so a reader of
   x-stat-tile can find the rule. This one was `--critical` while no markup
   pointed at .om-stat at all; T12 renamed it rather than adding a second
   spelling of the same tone. */
.om-stat--danger .om-stat__icon{background:var(--danger-soft);color:var(--danger)}
.om-stat--danger:hover .om-stat__icon{box-shadow:var(--glow-danger)}
/* info and secondary carry no glow of their own: there is no --glow-info rung,
   and inventing one for two tiles is a token added to avoid an omission. The
   base .om-stat:hover --glow-md still fires, so they lift like every other tile. */
.om-stat--info .om-stat__icon{background:var(--info-soft);color:var(--info)}
.om-stat--secondary .om-stat__icon{background:var(--surface-sunken);color:var(--text-muted)}
.om-stat--success .om-stat__icon{background:var(--success-soft);color:var(--success)}
.om-stat--success:hover .om-stat__icon{box-shadow:var(--glow-success)}
.om-stat--warning .om-stat__icon{background:var(--warning-soft);color:var(--warning)}
.om-stat--warning:hover .om-stat__icon{box-shadow:var(--glow-warning)}
.om-stat--compact{padding:var(--space-4) var(--space-4);gap:var(--space-3);border-radius:var(--radius-md)}
.om-stat--compact .om-stat__icon{width:40px;height:40px;font-size:var(--icon-tile)}
.om-stat--compact .om-stat__value{font-size:var(--fs-xl)}

/* -------------------------- MINI CALENDAR -------------------------- */
.om-cal{display:flex;flex-direction:column;gap:var(--space-3)}
.om-cal__head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2)}
.om-cal__month{font:var(--type-card-title);color:var(--text-primary)}
/* see kit-port-notes.md: om-cal - do not remove the track cap */
.om-cal__grid{display:grid;grid-template-columns:repeat(7,minmax(0,var(--om-cal-cell,46px)));justify-content:center;gap:var(--space-05)}
.om-cal__dow{display:grid;place-items:center;height:26px;font:var(--type-overline);letter-spacing:var(--ls-caps);
  text-transform:uppercase;color:var(--text-muted)}
.om-cal__day{position:relative;display:grid;place-items:center;aspect-ratio:1;min-height:34px;max-height:var(--om-cal-cell,46px);border-radius:var(--radius-sm);
  font:var(--fw-medium) var(--fs-xs)/1 var(--font-sans);font-variant-numeric:tabular-nums;color:var(--text-primary);
  cursor:pointer;transition:var(--t-all)}
.om-cal__day:hover{background:var(--blue-50);color:var(--text-brand);transform:scale(1.06)}
.om-cal__day--muted{color:var(--text-disabled)}
.om-cal__day--today{color:var(--text-brand);font-weight:var(--fw-bold);box-shadow:inset 0 0 0 1.5px var(--blue-300)}
.om-cal__day--selected{background:var(--gradient-primary);color:var(--text-inverse);font-weight:var(--fw-bold);box-shadow:var(--glow-sm)}
.om-cal__day--selected:hover{background:var(--gradient-primary);color:var(--text-inverse)}
.om-cal__dot{position:absolute;bottom:var(--space-1);inset-inline:0;margin-inline:auto;width:max-content;display:flex;gap:var(--space-05)}
.om-cal__dot i{width:3px;height:3px;border-radius:var(--radius-pill);background:var(--blue-500)}
.om-cal__day--selected .om-cal__dot i{background:var(--text-inverse)}

/* ---------------------------- CHARTS ---------------------------- */
.om-chart{position:relative;width:100%}
.om-chart svg{display:block;width:100%;overflow:visible}
/* The box a Chart.js <canvas> is drawn into. Chart.js sizes the canvas to its
   PARENT when maintainAspectRatio is false, so the parent must have a height of
   its own or the canvas collapses to zero — which is what every hand-rolled
   `style="max-height:250px"` beside a chart was really compensating for. An
   aspect ratio rather than a fixed height: it needs no px, and it keeps the
   plot readable at 360px instead of squashing a full-width chart into 250. */
.om-canvas{position:relative;width:100%;aspect-ratio:16/9}
.om-canvas>canvas{display:block}
.om-chart__grid line{stroke:var(--chart-grid);stroke-width:1}
.om-chart__axis text{fill:var(--chart-axis);font:var(--fw-medium) var(--fs-2xs)/1 var(--font-sans)}
.om-chart__ticks{display:flex;direction:ltr;justify-content:space-between;gap:var(--space-2);padding-top:var(--space-2)}
.om-chart__ticks span{font:var(--fw-medium) var(--fs-2xs)/1 var(--font-sans);color:var(--chart-axis);
  font-variant-numeric:tabular-nums;flex:1;text-align:center;white-space:nowrap}
.om-chart__ticks span:first-child{text-align:start}
.om-chart__ticks span:last-child{text-align:end}
.om-chart__line{fill:none;stroke:var(--chart-1);stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;
  stroke-dasharray:1400;stroke-dashoffset:1400;animation:om-dash 1400ms var(--ease-out) forwards}
.om-chart__line--2{stroke:var(--chart-2);stroke-width:2;stroke-dasharray:6 5;animation:none;stroke-dashoffset:0}
.om-chart__area{animation:om-fade-in var(--motion-slow) var(--ease-out) 300ms both}
.om-chart__pt{fill:var(--surface);stroke:var(--chart-1);stroke-width:2.5;transition:r var(--motion-base) var(--ease-spring)}
.om-chart__hit{fill:transparent;cursor:pointer}
.om-chart__hit:hover+.om-chart__pt,.om-chart__pt--active{r:6}
.om-chart__bar{fill:var(--chart-1);transition:fill var(--motion-fast) var(--ease-out),filter var(--motion-base) var(--ease-out);transform-box:fill-box;transform-origin:bottom}
.om-chart__bar{animation:om-bar-grow var(--motion-slow) var(--ease-out) both}
@keyframes om-bar-grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}
.om-chart__bar:hover{filter:drop-shadow(0 4px 10px var(--blue-a45))}
.om-chart__bar--muted{fill:var(--blue-200)}
.om-chart__donut-seg{transition:transform var(--motion-base) var(--ease-spring),filter var(--motion-base) var(--ease-out);transform-origin:center}
.om-chart__donut-seg:hover{transform:scale(1.035);filter:drop-shadow(0 4px 12px var(--ink-a12))}
.om-chart__legend{display:flex;flex-wrap:wrap;gap:var(--space-2) var(--space-4)}
.om-chart__legend li{display:flex;align-items:center;gap:var(--space-15);font:var(--type-caption);color:var(--text-secondary)}
.om-chart__legend i{width:8px;height:8px;border-radius:var(--radius-2xs);flex:none}
.om-chart__tip{position:absolute;z-index:var(--z-tooltip);pointer-events:none;padding:var(--space-15) var(--space-3);border-radius:var(--radius-sm);
  background:var(--n-900);color:var(--text-inverse);font:var(--fw-bold) var(--fs-2xs)/1.4 var(--font-sans);white-space:nowrap;
  box-shadow:var(--shadow-lg);animation:om-rise-sm var(--motion-fast) var(--ease-out)}

/* Palette swatch — the gallery's proof that the series tokens resolve, and the
   only place the six rungs are visible side by side. Geometry comes from
   .om-chart__legend i, which is more specific; these carry the fill alone. */
.om-swatch{border:1px solid var(--border)}
.om-swatch--chart-1{background:var(--chart-1)}
.om-swatch--chart-2{background:var(--chart-2)}
.om-swatch--chart-3{background:var(--chart-3)}
.om-swatch--chart-4{background:var(--chart-4)}
.om-swatch--chart-5{background:var(--chart-5)}
.om-swatch--chart-6{background:var(--chart-6)}
.om-swatch--chart-grid{background:var(--chart-grid)}
.om-swatch--chart-axis{background:var(--chart-axis)}

.om-spark{display:block;width:100%;height:32px;overflow:visible}
.om-spark path.l{fill:none;stroke:var(--chart-1);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.om-spark path.a{fill:var(--blue-a12);stroke:none}
.om-spark--danger path.l{stroke:var(--danger)}
.om-spark--danger path.a{fill:var(--danger-soft)}
.om-spark--success path.l{stroke:var(--success)}
.om-spark--success path.a{fill:var(--success-soft)}

/* --------------------------- TIMELINE --------------------------- */
.om-timeline{display:flex;flex-direction:column}
.om-timeline__item{position:relative;display:flex;gap:var(--space-4);padding-bottom:var(--space-4)}
.om-timeline__item:last-child{padding-bottom:0}
.om-timeline__rail{position:relative;flex:none;display:flex;justify-content:center;width:28px}
.om-timeline__rail::before{content:"";position:absolute;top:22px;bottom:-14px;width:2px;background:var(--border)}
.om-timeline__item:last-child .om-timeline__rail::before{display:none}
.om-timeline__node{position:relative;z-index:var(--z-local-1);display:grid;place-items:center;width:28px;height:28px;border-radius:var(--radius-pill);
  background:var(--surface);border:1.5px solid var(--border);color:var(--text-muted);font-size:var(--icon-micro)}
.om-timeline__node--brand{background:var(--blue-50);border-color:var(--blue-200);color:var(--blue-600)}
.om-timeline__node--now{background:var(--gradient-primary);border-color:transparent;color:var(--text-inverse);animation:om-pulse-ring 2200ms var(--ease-out) infinite}
.om-timeline__node--danger{background:var(--danger-soft);border-color:var(--danger-soft);color:var(--danger)}
.om-timeline__node--success{background:var(--success-soft);border-color:var(--success-soft);color:var(--success)}
.om-timeline__body{display:flex;flex-direction:column;gap:var(--space-05);padding-top:var(--space-05);min-width:0}
.om-timeline__title{font:var(--type-body-strong);color:var(--text-primary)}
.om-timeline__meta{font:var(--type-caption);color:var(--text-muted);font-variant-numeric:tabular-nums}
.om-timeline__note{font:var(--type-body-sm);color:var(--text-secondary);margin-top:var(--space-05)}

/* ------------------------- DEFINITION LIST ------------------------- */
/* ---- RECORD HEADER (T11) — the detail archetype's identity block.
   Replaces .patient-header, which four views each declared in their OWN in-view
   stylesheet (three with a hardcoded indigo gradient plus a dark-theme override,
   one with a flat surface and no radius) and which doctor-mobile.css then
   corrected with four !important rules — one class, five definitions.
   The surface is --surface-soft, not a gradient: the identity block sits at the
   top of a card whose body is --surface, and a tint is what separates them. */
.om-recordhead{display:flex;flex-direction:column;align-items:center;gap:var(--space-2);
  padding:var(--space-5) var(--space-4);text-align:center;background:var(--surface-soft);
  border-bottom:1px solid var(--border-light)}
.om-recordhead__name{font:var(--type-section);color:var(--ink);margin:0}
.om-recordhead__id{display:flex;align-items:center;gap:var(--space-1);margin:0;
  font:var(--type-caption);color:var(--ink-muted)}
.om-recordhead__facts{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--space-2)}
/* The action is a decision about the record, so it gets its own row and the full
   width of the block rather than sitting beside the facts it depends on. */
.om-recordhead__actions{display:flex;flex-direction:column;align-self:stretch;gap:var(--space-2);margin-top:var(--space-2)}

.om-dl{display:grid;gap:var(--space-4) var(--space-5);grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}
.om-dl__item{display:flex;flex-direction:column;gap:var(--space-05);min-width:0}
.om-dl__k{font:var(--type-overline);letter-spacing:var(--ls-caps);text-transform:uppercase;color:var(--text-muted)}
.om-dl__v{font:var(--type-body-strong);color:var(--text-primary);font-variant-numeric:tabular-nums}
.om-dl__v--muted{font-weight:var(--fw-regular);color:var(--text-secondary)}

/* --------------------------- VITALS TILE --------------------------- */
.om-vital{display:flex;flex-direction:column;gap:var(--space-2);padding:var(--space-4) var(--space-4);border-radius:var(--radius-md);
  background:var(--surface-sunken);border:1px solid transparent;transition:var(--t-all)}
.om-vital:hover{background:var(--surface);border-color:var(--border);box-shadow:var(--shadow-sm)}
.om-vital__k{display:flex;align-items:center;gap:var(--space-15);font:var(--type-overline);letter-spacing:var(--ls-caps);text-transform:uppercase;color:var(--text-muted)}
.om-vital__v{display:flex;align-items:baseline;gap:var(--space-05);font:var(--fw-bold) var(--fs-xl)/1 var(--font-sans);font-variant-numeric:tabular-nums;color:var(--text-primary)}
.om-vital__v span{font:var(--fw-medium) var(--fs-xs)/1 var(--font-sans);color:var(--text-muted)}
.om-vital--alert{background:var(--danger-soft)}
.om-vital--alert .om-vital__v{color:var(--danger)}
.om-vital--alert .om-vital__k{color:var(--danger)}

/* ======================= POPOVER / DROPDOWN ======================= */
.om-pop{position:relative;display:inline-flex}
/* THE OFFSETS ARE LOAD-BEARING. This rule declared `position:absolute` and no
   offsets at all, which does NOT mean "top left of .om-pop" — with every inset
   auto an absolutely positioned box sits at its STATIC position, i.e. exactly
   where it would have been in flow. .om-pop is display:inline-flex, so the panel
   was laid out as a flex item BESIDE its trigger, not below it, widening the
   trigger's own box. Measured by deleting this line again with the JS in place,
   through a real click and getBoundingClientRect: trigger bottom 479, panel top
   443 — the panel began 36px ABOVE the trigger's bottom edge, overlapping the
   control that opened it (scripts/probe/interact.js --suite overlay goes red on
   exactly that assertion and green when the line is back). Naming the block-start
   offset is the fix; `top` is the block axis and has no RTL mirror, so it is the
   correct property rather than a physical-direction violation. */
.om-pop__panel{position:absolute;z-index:var(--z-overlay);min-width:180px;padding:var(--space-2);
  top:calc(100% + var(--space-2));inset-inline-start:0;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);
  box-shadow:var(--shadow-xl);transform-origin:top right;
  opacity:0;transform:scale(.94) translateY(-6px);pointer-events:none;
  transition:opacity var(--motion-fast) var(--ease-out),transform var(--motion-slow) var(--ease-out)}
.om-pop__panel--open{opacity:1;transform:none;pointer-events:auto}
/* transform-origin has no logical form; the [dir] pair IS the conversion (9). */
[dir="ltr"] .om-pop__panel{transform-origin:top left}
/* align-end used to flip the transform-origin ONLY, so `align="end"` moved the
   spring's pivot and left the panel in the same place — the modifier was
   documented as an alignment and behaved as an animation detail. The inset pair
   is what actually aligns it, and it is logical, so RTL mirrors for free. */
.om-pop__panel--align-end{transform-origin:top left;inset-inline-start:auto;inset-inline-end:0}
[dir="ltr"] .om-pop__panel--align-end{transform-origin:top right}
.om-pop__panel--wide{min-width:320px;padding:var(--space-3)}
.om-pop__panel--flush{padding:0;overflow:hidden}
.om-pop__head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-3) var(--space-3) var(--space-2)}
.om-pop__head b{font:var(--type-card-title);color:var(--text-primary)}
.om-pop__sep{height:var(--hairline);background:var(--divider);margin:var(--space-2) 0}
.om-pop__label{padding:var(--space-2) var(--space-3) var(--space-2);font:var(--type-overline);letter-spacing:var(--ls-caps-wide);text-transform:uppercase;color:var(--text-muted)}

.om-menuitem{display:flex;align-items:center;gap:var(--space-3);width:100%;padding:var(--space-3);border-radius:var(--radius-sm);
  font:var(--type-body);color:var(--text-primary);text-align:start;cursor:pointer;
  transition:background-color var(--motion-fast) var(--ease-out),color var(--motion-fast) var(--ease-out),padding-inline-start var(--motion-base) var(--ease-out)}
.om-menuitem>i:first-child{font-size:var(--icon-tab);color:var(--text-muted);flex:none;transition:var(--t-color)}
.om-menuitem:hover{background:var(--blue-50);color:var(--text-brand);padding-inline-start:var(--space-4)}
.om-menuitem:hover>i:first-child{color:var(--text-brand)}
.om-menuitem--danger{color:var(--danger)}
.om-menuitem--danger>i:first-child{color:var(--danger)}
.om-menuitem--danger:hover{background:var(--danger-soft);color:var(--danger-ink)}
.om-menuitem--selected{background:var(--blue-50);color:var(--text-brand);font-weight:var(--fw-bold)}
.om-menuitem__kbd{margin-inline-start:auto;font:var(--fw-medium) var(--fs-2xs)/1 var(--font-sans);color:var(--text-muted);
  padding:var(--space-05) var(--space-15);border:1px solid var(--border);border-radius:var(--radius-sm)}
.om-menuitem__sub{display:block;font:var(--type-caption);color:var(--text-muted);margin-top:var(--hairline)}

.om-notif{display:flex;align-items:flex-start;gap:var(--space-3);width:100%;padding:var(--space-3);border-radius:var(--radius-md);
  text-align:start;cursor:pointer;transition:background-color var(--motion-fast) var(--ease-out)}
.om-notif:hover{background:var(--surface-hover)}
.om-notif__glyph{display:grid;place-items:center;flex:none;width:38px;height:38px;border-radius:var(--radius-lg);
  background:var(--blue-50);color:var(--blue-600);font-size:var(--icon-tile)}
.om-notif__glyph--danger{background:var(--danger-soft);color:var(--danger)}
.om-notif__glyph--success{background:var(--success-soft);color:var(--success)}
.om-notif__glyph--warning{background:var(--warning-soft);color:var(--warning)}
.om-notif__title{font:var(--type-body-strong);color:var(--text-primary)}
.om-notif__text{font:var(--type-caption);color:var(--text-secondary);margin-top:var(--space-05)}
.om-notif__time{font:var(--fw-medium) var(--fs-2xs)/1 var(--font-sans);color:var(--text-muted);margin-top:var(--space-15);text-transform:uppercase;letter-spacing:var(--ls-caps)}
.om-notif--unread{background:var(--blue-a04)}
.om-notif--unread::after{content:"";width:7px;height:7px;border-radius:var(--radius-pill);background:var(--blue-500);flex:none;margin-top:var(--space-15);box-shadow:var(--glow-sm)}

/* ---------------------------- TOOLTIP ---------------------------- */
.om-tip{position:relative;display:inline-flex}
/* see kit-port-notes.md: om-tip translate */
.om-tip__bubble{position:absolute;z-index:var(--z-tooltip);inset-inline-start:50%;translate:50% 0;width:max-content;max-width:min(38ch,80vw);bottom:calc(100% + 9px);transform:translateY(4px);
  padding:var(--space-15) var(--space-3);border-radius:var(--radius-sm);background:var(--n-900);color:var(--text-inverse);
  font:var(--fw-medium) var(--fs-2xs)/1.4 var(--font-sans);white-space:nowrap;box-shadow:var(--shadow-lg);
  opacity:0;pointer-events:none;transition:opacity var(--motion-fast) var(--ease-out),transform var(--motion-base) var(--ease-out)}
:where([dir="ltr"]) .om-tip__bubble{translate:-50% 0}
.om-tip__bubble::after{content:"";position:absolute;top:100%;inset-inline:0;margin-inline:auto;width:0;height:0;
  border:4px solid transparent;border-block-start-color:var(--n-900)}
.om-tip:hover .om-tip__bubble,.om-tip:focus-within .om-tip__bubble{opacity:1;transform:none}
.om-tip__bubble--inline-end{inset-inline-start:calc(100% + 9px);translate:none;bottom:auto;top:50%;transform:translateY(-50%) scale(.97)}
.om-tip__bubble--inline-end::after{top:50%;inset-inline-start:-8px;inset-inline-end:auto;margin-inline:0;transform:translateY(-50%);border-block-start-color:transparent;border-inline-end-color:var(--n-900)}
.om-tip:hover .om-tip__bubble--inline-end,.om-tip:focus-within .om-tip__bubble--inline-end{transform:translateY(-50%) scale(1)}
.om-tip__bubble--bottom{top:calc(100% + 9px);bottom:auto;transform:translateY(-4px)}
.om-tip__bubble--bottom::after{top:-8px;border-block-start-color:transparent;border-block-end-color:var(--n-900)}
/* WCAG 2.2 1.4.13 "Dismissible": a bubble that covers content must be
   dismissable without moving the pointer or the focus. CSS has no expression for
   "the user pressed Escape", so OM.overlay.bindTooltips stamps this class and
   removes it on focusout/mouseleave. It must sit AFTER the :hover/:focus-within
   rule above — both are (0,2,0), so position is the only tiebreaker. */
.om-tip--hushed .om-tip__bubble{opacity:0}

/* ----------------------- MODAL / BOTTOM SHEET ----------------------- */
.om-scrim{position:fixed;inset:0;z-index:var(--z-modal);display:grid;place-items:center;padding:var(--space-4);
  background:var(--ink-a48);backdrop-filter:blur(6px);animation:om-fade-in var(--motion-base) var(--ease-out)}
.om-modal{position:relative;width:100%;max-width:520px;max-height:88vh;display:flex;flex-direction:column;
  background:var(--surface);border-radius:var(--radius-xl);box-shadow:var(--shadow-2xl);overflow:hidden;
  animation:om-pop var(--motion-slow) var(--ease-spring)}
.om-modal--sm{max-width:420px}
.om-modal--lg{max-width:760px}
.om-modal--xl{max-width:1040px}
.om-modal__head{display:flex;align-items:flex-start;gap:var(--space-4);padding:var(--pad-card-lg) var(--pad-card-lg) var(--space-4)}
.om-modal__glyph{display:grid;place-items:center;flex:none;width:44px;height:44px;border-radius:var(--radius-md);
  background:var(--blue-50);color:var(--blue-600);font-size:var(--icon-glyph)}
.om-modal__glyph--danger{background:var(--danger-soft);color:var(--danger)}
.om-modal__glyph--success{background:var(--success-soft);color:var(--success)}
.om-modal__title{font:var(--fw-bold) var(--fs-lg)/1.3 var(--font-sans);letter-spacing:var(--ls-tight);color:var(--text-primary)}
.om-modal__sub{font:var(--type-body);color:var(--text-secondary);margin-top:var(--space-1)}
.om-modal__body{padding:0 var(--pad-card-lg) var(--pad-card-lg);overflow-y:auto}
.om-modal__foot{display:flex;align-items:center;justify-content:flex-end;gap:var(--space-3);
  padding:var(--space-4) var(--pad-card-lg);border-top:1px solid var(--border-light);background:var(--surface-sunken)}
.om-modal__close{position:absolute;top:var(--space-3);inset-inline-end:var(--space-3)}
@media (max-width:767px){
  .om-scrim{padding:0;place-items:end stretch}
  .om-modal{max-width:none;max-height:92vh;border-radius:var(--radius-sheet);animation:om-sheet-up var(--motion-slow) var(--ease-out)}
  .om-modal::before{content:"";position:absolute;top:var(--space-2);inset-inline:0;margin-inline:auto;width:40px;height:4px;border-radius:var(--radius-pill);background:var(--n-200)}
  .om-modal__head{padding-top:var(--space-5)}
  /* D14 case (2), om-tokens.css: this is a SHEET footer, not a fixed-height bar,
     so it takes the sheet token — the static max inset on the engine that
     animates the inset, the exact current inset on the engines that have no max
     keyword. --safe-max-inset-bottom is wrong here: nothing cancels it, so its
     36px fallback would ship as dead space under every sheet on a chinless
     WebKit/Gecko viewport. An inset change reflows this flex column to the
     correct height rather than squeezing a clamped row. */
  .om-modal__foot{flex-direction:column-reverse;padding-bottom:calc(var(--space-4) + var(--safe-inset-bottom-sheet))} /* D14-EXEMPT: sheet footer, not a fixed-height bar */
  .om-modal__foot>.om-btn{width:100%}
}

.om-drawer{position:fixed;top:0;inset-inline-end:0;bottom:0;z-index:var(--z-modal);width:min(520px,100vw);display:flex;flex-direction:column;
  background:var(--surface);box-shadow:var(--shadow-2xl);animation:om-drawer-in var(--motion-slow) var(--ease-out)}
@keyframes om-drawer-in{from{inset-inline-end:calc(var(--space-5) * -1);opacity:.4}to{inset-inline-end:0;opacity:1}}

/* ----------------------------- TOAST ----------------------------- */
.om-toast{display:flex;align-items:flex-start;gap:var(--space-3);padding:var(--space-4) var(--space-4);min-width:300px;
  background:var(--n-900);color:var(--text-inverse);border-radius:var(--radius-md);box-shadow:var(--shadow-2xl);
  animation:om-rise var(--motion-slow) var(--ease-spring)}
.om-toast__icon{font-size:var(--icon-tile);flex:none;color:var(--blue-300)}
/* The kit inked these two with the app's DARK-theme --success-ink/--danger-ink
   literals. The toast surface is --n-900 in BOTH themes, so a themed ink token
   cannot serve it: the light --success-ink is a deep green and lands at 1.9:1
   there. The theme-invariant semantic pair is the only existing token that
   fits, and it clears the 3:1 non-text floor on --n-900 (5.29:1 / 3.95:1). */
.om-toast--success .om-toast__icon{color:var(--success)}
.om-toast--danger .om-toast__icon{color:var(--danger)}
/* `warning` was reachable from day one — Variants allows it and a live call site
   passes it — but had no rule, so it rendered a warning GLYPH in info INK
   (logged in docs/redesign/behaviour-changes.md under T3, "fix is one CSS rule").
   It was NOT one rule. This one is necessary — --warning clears the 3:1 non-text
   floor on --n-900 — but it was inert on arrival: toast.blade.php emitted a
   modifier for success and danger only, so nothing carried this class. That half
   is fixed in the component. Do not trust "the fix is one CSS rule" again without
   checking who emits the class. */
.om-toast--warning .om-toast__icon{color:var(--warning)}
.om-toast__title{font:var(--type-body-strong);color:var(--text-inverse)}
.om-toast__text{font:var(--type-caption);color:var(--text-inverse);opacity:.7;margin-top:var(--space-05)}
.om-toast-stack{position:fixed;z-index:var(--z-toast);inset-inline-end:var(--space-4);bottom:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)}
@media (max-width:767px){.om-toast-stack{inset-inline:var(--space-3);bottom:calc(var(--bottomnav-h) + var(--space-3))}}

/* ============================================================================
   PHASE-2 DERIVED - primitives COMPONENTS.md mandates that no kit file ships.
   Each block names its authority.
   ========================================================================= */

/* ---- ICON (spec 3.2 icon-sizing family) ---- */
.om-icon{display:inline-block;font-size:var(--icon-btn);line-height:1;flex:none}
.om-icon--btn{font-size:var(--icon-btn)}
.om-icon--tab{font-size:var(--icon-tab)}
.om-icon--tile{font-size:var(--icon-tile)}
.om-icon--hero{font-size:var(--icon-hero)}
.om-icon--nav{font-size:var(--icon-nav);width:var(--icon-nav-track);text-align:center}

/* ---- SKELETON (universal state contract: never a spinner over content) ---- */
.om-skel{position:relative;overflow:hidden;background:var(--surface-sunken);border-radius:var(--radius-sm);
  background-image:linear-gradient(90deg,transparent 0%,var(--blue-a04) 40%,transparent 80%);
  background-size:220% 100%;animation:om-shimmer 1600ms var(--ease-linear) infinite}
.om-skel--text{height:12px;width:100%}
.om-skel--line-sm{height:9px;width:56%}
.om-skel--circle{width:38px;height:38px;border-radius:var(--radius-pill);flex:none}
/* Sized to the thing it stands in for: .om-stat is --space-4 padding around a
   52px medallion = 84px, at --radius-xl. It was 78px/--radius-lg, which made the
   grid twitch on arrival — the one defect a skeleton exists to prevent. */
.om-skel--tile{height:84px;border-radius:var(--radius-xl)}
.om-skel--row{display:flex;align-items:center;gap:var(--space-3);padding:var(--pad-cell-y) 0;background:none;background-image:none;animation:none}
@media (prefers-reduced-motion:reduce){.om-skel{animation:none}}

/* ---- CHIP (Matrix B row 11; distinct from .filter-pill and from .st-chip) ---- */
/* THE CHIP MAY NOT PAINT OUTSIDE ITS OWN RECTANGLE (T29). It did, everywhere the
   design feedback looked: مراجعة and حضور مباشر measured the SAME 71.67px used
   width on the doctor's board, because .live-table / .om-tablecard--live set
   table-layout:fixed and clamp `td > *` to max-width:100%. The chip is sized by
   the column, not by its content — and with white-space:nowrap, no min-width:0
   and nothing able to clip it, the surplus Arabic simply rendered past the pill.
   The escape hatch is that a flex container's loose text is an ANONYMOUS flex
   item, which text-overflow cannot address; hence .om-chip__label, an element.
   max-width:100% here rather than only in the two table rules, so the invariant
   holds in a card, a filter bar and a hero as well as in a starved column. */
.om-chip{display:inline-flex;align-items:center;gap:var(--space-15);max-width:100%;min-width:0;height:var(--chip-h);padding:0 var(--space-3);border-radius:var(--radius-pill);
  background:var(--surface-sunken);border:1px solid var(--border);color:var(--ink-muted);
  font:var(--fw-bold) var(--fs-xs)/1 var(--font-sans);white-space:nowrap;transition:var(--t-all)}
/* The label yields first: an amount, a removal control and the tone glyph are all
   worth more than the tail of a word that is still legible truncated. */
.om-chip__label{min-width:0;overflow:hidden;text-overflow:ellipsis;line-height:1.6}
.om-chip--action{cursor:pointer}
.om-chip--action:hover{border-color:var(--blue-300);background:var(--primary-soft);color:var(--primary)}
.om-chip--action:focus-visible{outline:none;box-shadow:var(--shadow-ring)}
.om-chip--selected{background:var(--gradient-primary);border-color:transparent;color:var(--text-inverse);box-shadow:var(--glow-sm)}
.om-chip--danger{border-color:var(--danger-soft);color:var(--danger)}
/* Both tints below are currentColor-RELATIVE: a chip's ink is --ink-muted by
   default, --primary while hovered, --danger on --danger and --text-inverse on
   --selected, so no fixed token can stand in for them. currentColor + opacity is
   the token-free equivalent of the colour-mixing function the kit used
   (identical sRGB composite) and leaves no colour notation behind.
   The divider's 1px moved from the border box into the padding, so the inset had
   to absorb it. T2.5 then put both halves on the scale: margin 3px -> --space-05
   (2px) and padding 7px -> --space-2 (8px), which preserves the 10px total the
   text has always sat at. The 6px -> 7px this sentence used to quote is no longer
   in the code — the invariant is the TOTAL, not either number. */
.om-chip__value{position:relative;flex:none;margin-inline-start:var(--space-05);padding-inline-start:var(--space-2);font-weight:var(--fw-black)}
.om-chip__value::before{content:"";position:absolute;inset-block:0;inset-inline-start:0;width:var(--hairline);background:currentColor;opacity:.28}
.om-chip__x{position:relative;display:grid;flex:none;place-items:center;width:16px;height:16px;margin-inline-end:calc(var(--space-05) * -1);border-radius:var(--radius-pill);opacity:.55;transition:var(--t-all)}
/* The wash is the same colour as the glyph it overlays, so painting above the
   in-flow icon is a no-op on the icon and the intended tint off it. */
.om-chip__x::before{content:"";position:absolute;inset:0;border-radius:inherit;background:currentColor;opacity:0;transition:opacity var(--motion-fast) var(--ease-out)}
.om-chip__x:hover{opacity:1}
.om-chip__x:hover::before{opacity:.14}

/* ---- CHIP TONES (T8) — the nine domain chips collapse onto this one contract.
   They used to be four private families in om-clinic.css (.st-chip, .vt-chip--*,
   .pay-chip--*, .gender-chip) plus five Bootstrap `.badge bg-*` components, so the
   SAME domain rendered two different ways: x-status-chip painted a solid pill and
   x-appointment-status-badge a subtle Bootstrap badge, for the identical column.
   One tone enum, one geometry, one place a tone is defined.

   THREE TIERS, and the tier itself carries meaning — this is the hierarchy the
   source view establishes and it must not be flattened:
     الحالة  status  → SOLID   (filled, inverse text)  — what you scan for
     النوع   type    → OUTLINE (surface, hairline, coloured text)
     الدفع   payment → SOFT    (tinted bg, coloured text, tinted border)
   The tier is a property of the TONE, never a choice at the call site, so
   config/vocabulary.php stays the only enum→tone lookup.

   Every tone states border-color. The base ships `1px solid var(--border)` for the
   neutral filter chip, so a tone that only set a background would keep the neutral
   sheet's edge and read as a fourth, unintended tier. */
/* --chip-h / --chip-h-sm are named tokens because nine domain components now
   share them: the 26px .st-chip and the 22px .gender-chip were separate literals
   in om-clinic.css, and merging them onto one contract made the number a design
   decision rather than a per-family accident. */
.om-chip--sm{height:var(--chip-h-sm);padding:0 var(--space-2);gap:var(--space-1);font-size:var(--fs-2xs)}
.om-chip__icon{flex:none;font-size:var(--icon-micro)}
/* The dot and its breath are .om-badge__dot's rules, shared by selector rather
   than copied: a chip's live dot and a badge's are the same 6px currentColor
   circle, and a second declaration of it is a second place for them to drift. */

/* الحالة — appointment status. SOLID. */
.om-chip--scheduled{background:var(--st-scheduled-deep);border-color:transparent;color:var(--text-inverse);box-shadow:0 1px 3px var(--st-scheduled-glow)}
.om-chip--confirmed{background:var(--st-confirmed-deep);border-color:transparent;color:var(--text-inverse);box-shadow:0 1px 3px var(--st-confirmed-glow)}
.om-chip--arrived{background:var(--st-arrived-deep);border-color:transparent;color:var(--text-inverse);box-shadow:0 1px 3px var(--st-arrived-glow)}
.om-chip--in_progress{background:var(--st-in-progress-deep);border-color:transparent;color:var(--text-inverse);box-shadow:0 1px 3px var(--st-in-progress-glow)}
.om-chip--completed{background:var(--st-completed-deep);border-color:transparent;color:var(--text-inverse);box-shadow:0 1px 3px var(--st-completed-glow)}
.om-chip--cancelled{background:var(--st-cancelled-deep);border-color:transparent;color:var(--text-inverse);box-shadow:0 1px 3px var(--st-cancelled-glow)}
.om-chip--no_show{background:var(--st-no-show-deep);border-color:transparent;color:var(--text-inverse);box-shadow:0 1px 3px var(--st-no-show-glow)}
.om-chip--scheduled>.om-chip__icon,.om-chip--confirmed>.om-chip__icon,.om-chip--arrived>.om-chip__icon,.om-chip--in_progress>.om-chip__icon,.om-chip--completed>.om-chip__icon,.om-chip--cancelled>.om-chip__icon,.om-chip--no_show>.om-chip__icon{opacity:.92}
/* Settled states must not shout — the work on them is finished. */
.om-chip--completed,.om-chip--no_show{opacity:.88}

/* النوع — visit type. OUTLINE. */
.om-chip--new{background:var(--surface);border-color:var(--vt-new-line);color:var(--vt-new-deep)}
.om-chip--follow_up{background:var(--surface);border-color:var(--vt-follow-up-line);color:var(--vt-follow-up-deep)}
.om-chip--walk_in{background:var(--surface);border-color:var(--vt-walk-in-line);color:var(--vt-walk-in-deep)}
/* طوارئ is the one visit type allowed to break the quiet tier. */
.om-chip--emergency{background:var(--vt-emergency-tint);border-color:var(--vt-emergency-line);color:var(--vt-emergency-deep);font-weight:var(--fw-black)}

/* الدفع — payment. SOFT. */
.om-chip--paid{background:var(--pay-paid-tint);border-color:var(--pay-paid-line);color:var(--pay-paid-deep)}
.om-chip--partial{background:var(--pay-partial-tint);border-color:var(--pay-partial-line);color:var(--pay-partial-deep)}
.om-chip--insured{background:var(--pay-insured-tint);border-color:var(--pay-insured-line);color:var(--pay-insured-deep)}
/* An unpaid balance blocks the patient at the desk — give it a visible edge. */
.om-chip--due{background:var(--pay-due-tint);border-color:var(--pay-due-line);color:var(--pay-due-deep);font-weight:var(--fw-black)}

/* Dark theme: lift the ink on the two tiers that use -deep as text. */
[data-theme="dark"] .om-chip--new{background:transparent;color:var(--vt-new-lift)}
[data-theme="dark"] .om-chip--follow_up{background:transparent;color:var(--vt-follow-up-lift)}
[data-theme="dark"] .om-chip--walk_in{background:transparent;color:var(--vt-walk-in-lift)}
[data-theme="dark"] .om-chip--emergency{color:var(--vt-emergency-lift)}
[data-theme="dark"] .om-chip--paid{color:var(--pay-paid-lift)}
[data-theme="dark"] .om-chip--due{color:var(--pay-due-lift)}
[data-theme="dark"] .om-chip--partial{color:var(--pay-partial-lift)}
[data-theme="dark"] .om-chip--insured{color:var(--pay-insured-lift)}

/* الجنس — gender. The app ships dedicated tokens for it. Label + glyph always
   accompany the tint; colour never carries the meaning alone (WCAG 1.4.1). */
.om-chip--male{background:var(--gender-male-soft);border-color:transparent;color:var(--gender-male)}
.om-chip--female{background:var(--gender-female-soft);border-color:transparent;color:var(--gender-female)}
/* `other` is the fallback for a null or unrecognised value, so it renders more
   often than either of the two above. */
.om-chip--other{background:var(--gender-other-soft);border-color:transparent;color:var(--gender-other)}

/* الدور — staff role (JR-05), and the booking source flag. SOFT tier: neither
   changes a clinical decision, so neither may outrank the status column. */
.om-chip--junior{background:var(--info-soft);border-color:transparent;color:var(--info-ink)}
.om-chip--senior{background:var(--primary-soft);border-color:transparent;color:var(--primary-ink)}
.om-chip--staff{background:var(--n-100);border-color:transparent;color:var(--n-600)}
.om-chip--source{background:var(--info-soft);border-color:transparent;color:var(--info-ink)}
/* An online booking nobody has acknowledged yet — the one chip that shouts. */
.om-chip--flag{background:var(--danger);border-color:transparent;color:var(--text-inverse)}

/* ---- BREADCRUMB — see .app-crumbs in om-clinic.css. D5 makes clinic.css the
   authority, so the kit's second `.om-crumbs` family (and the x-breadcrumb
   component that carried it) was deleted in Phase 3 Task 7. ---- */

/* ---- TABS (Matrix B row 12 - serves BOTH the JS mode and the server ?tab= mode) ---- */
.om-tabs{display:flex;align-items:center;gap:var(--space-1);border-bottom:1px solid var(--border);overflow-x:auto;scrollbar-width:none}
.om-tabs::-webkit-scrollbar{display:none}
.om-tab{position:relative;display:inline-flex;align-items:center;gap:var(--space-2);height:40px;padding:0 var(--space-4);
  border-radius:var(--radius-md) var(--radius-md) 0 0;font:var(--fw-bold) var(--fs-sm)/1 var(--font-sans);
  color:var(--text-secondary);white-space:nowrap;text-decoration:none;transition:var(--t-color)}
.om-tab:hover{color:var(--text-brand);background:var(--blue-50);text-decoration:none}
.om-tab:focus-visible{outline:none;box-shadow:var(--shadow-ring)}
.om-tab--active{color:var(--text-brand)}
.om-tab--active::after{content:"";position:absolute;inset-inline:var(--space-3);bottom:calc(var(--hairline) * -1);height:var(--rail-h);border-radius:var(--radius-2xs) var(--radius-2xs) 0 0;background:var(--gradient-primary);box-shadow:var(--glow-sm)}

/* ---- TAB SHELL + RAIL NAV (T13) — the settings-tabbed archetype's frame.
   The two settings god-files each hand-roll this, and the class family they share
   is declared THREE times: om-clinic.css tokenised it, design-tokens.css repeats
   it in rem, and doctor-mobile.css corrects the result with sixteen !important
   rules under .device-mobile. One definition here, and the mobile form is a media
   query rather than a device class, so it needs no !important to win.
   The rail is a <nav> of grouped links; the shell renders exactly ONE panel. ---- */
.om-tabshell{display:grid;grid-template-columns:var(--railnav-w) minmax(0,1fr);gap:var(--space-4);align-items:start}
.om-tabshell--strip{display:block}
.om-tabshell__panel{min-width:0}
.om-tabshell--strip .om-tabshell__panel{padding-top:var(--space-4)}
.om-railnav{position:sticky;top:var(--space-4);align-self:start;padding:var(--space-2);background:var(--surface);
  border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm)}
.om-railnav__search{position:relative;margin-bottom:var(--space-2)}
.om-railnav__search>i{position:absolute;inset-inline-end:var(--space-3);top:50%;transform:translateY(-50%);
  color:var(--text-muted);font-size:var(--icon-micro);pointer-events:none}
.om-railnav__search input{width:100%;height:var(--control-h);padding-inline:var(--space-3) var(--space-6);
  background:var(--surface-sunken);border:1px solid var(--border);border-radius:var(--radius-md);
  color:var(--text-primary);font:var(--fw-medium) var(--fs-sm)/1 var(--font-sans);transition:var(--t-color)}
.om-railnav__search input:focus{outline:none;border-color:var(--primary);box-shadow:var(--shadow-ring);background:var(--surface)}
.om-railnav__group{margin-bottom:var(--space-2)}
.om-railnav__group:last-child{margin-bottom:0}
.om-railnav__grouptitle{padding:var(--space-3) var(--space-2) var(--space-1);color:var(--text-muted);
  font:var(--fw-bold) var(--fs-2xs)/1 var(--font-sans);letter-spacing:var(--ls-caps)}
.om-railnav__item{position:relative;display:flex;align-items:center;gap:var(--space-2);min-height:var(--tap-min);
  padding:var(--space-2);border-radius:var(--radius-md);border:1px solid transparent;color:var(--text-primary);
  text-decoration:none;font:var(--fw-medium) var(--fs-sm)/1.25 var(--font-sans);
  transition:background var(--motion-base) var(--ease-out),border-color var(--motion-base) var(--ease-out)}
.om-railnav__item:hover{text-decoration:none}
.om-railnav__icon{display:grid;place-items:center;flex:none;width:var(--railnav-icon);height:var(--railnav-icon);border-radius:var(--radius-md);
  background:var(--primary-soft);color:var(--primary);font-size:var(--icon-tile);
  transition:transform var(--motion-base) var(--ease-spring),box-shadow var(--motion-base) var(--ease-out)}
.om-railnav__label{display:flex;flex-direction:column;min-width:0;flex:1}
.om-railnav__label small{color:var(--text-muted);font:var(--fw-regular) var(--fs-2xs)/1.3 var(--font-sans);margin-top:var(--hairline)}
.om-railnav__item:hover:not(.om-railnav__item--active){background:var(--primary-soft);border-color:var(--blue-200)}
.om-railnav__item:hover:not(.om-railnav__item--active) .om-railnav__icon{transform:scale(1.06) rotate(-2deg)}
.om-railnav__item--active{background:var(--primary-soft);border-color:var(--blue-300);box-shadow:var(--shadow-sm)}
.om-railnav__item--active .om-railnav__icon{background:var(--gradient-primary);color:var(--text-inverse);box-shadow:var(--glow-md)}
.om-railnav__dot{position:absolute;inset-inline-start:0;top:50%;transform:translateY(-50%);width:var(--railnav-dot-w);height:var(--railnav-dot-h);
  border-radius:var(--radius-pill);background:var(--primary);box-shadow:var(--glow-sm)}
/* A group whose every item was filtered out must not leave its heading behind. */
.om-railnav__group[hidden],.om-railnav__item[hidden]{display:none}

/* ---- STEPPER (COMPONENTS.md derived: numbered steps, active = gradient + glow) ---- */
.om-stepper{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:var(--space-3)}
.om-stepper__step{display:flex;align-items:center;gap:var(--space-3);min-width:0;padding:var(--space-3);
  border-radius:var(--radius-md);border:1px solid var(--border);background:var(--surface);transition:var(--t-all)}
.om-stepper__num{display:grid;place-items:center;flex:none;width:28px;height:28px;border-radius:var(--radius-pill);
  background:var(--surface-sunken);color:var(--text-muted);font:var(--fw-black) var(--fs-xs)/1 var(--font-sans);
  font-variant-numeric:tabular-nums;transition:var(--t-all)}
.om-stepper__label{font:var(--type-body-strong);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.om-stepper__step--active{border-color:transparent;background:var(--gradient-primary);box-shadow:var(--glow-md)}
/* A fill cannot carry its alpha as opacity without washing the numeral, so this
   takes --white-a22 from the om-tokens.css additions layer -- the kit's alpha. */
.om-stepper__step--active .om-stepper__num{background:var(--white-a22);color:var(--text-inverse)}
.om-stepper__step--active .om-stepper__label{color:var(--text-inverse)}
.om-stepper__step--done .om-stepper__num{background:var(--success-soft);color:var(--success)}
.om-stepper__step--disabled{opacity:var(--opacity-disabled);pointer-events:none}
/* The wizard frame around it (T13): steps, then the step's body, then the row
   that moves you. The action row is `wrap-reverse` so that at 360px the primary
   (which is last in the source, because it is last in the reading order) lands
   on the FIRST line rather than below the secondary. */
.om-wizard{display:flex;flex-direction:column;gap:var(--space-4)}
.om-wizard__panel{min-width:0}
.om-wizard__actions{display:flex;flex-wrap:wrap-reverse;align-items:center;justify-content:space-between;gap:var(--space-3)}

/* ---- FUNNEL (COMPONENTS.md derived: read/delivered/sent/failed/suppressed) ---- */
.om-funnel{display:flex;width:100%;height:14px;border-radius:var(--radius-pill);overflow:hidden;background:var(--surface-sunken)}
.om-funnel__seg{height:100%;transition:width var(--motion-slow) var(--ease-out)}
.om-funnel__seg--read{background:var(--success)}
.om-funnel__seg--delivered{background:var(--blue-500)}
.om-funnel__seg--sent{background:var(--blue-200)}
.om-funnel__seg--failed{background:var(--danger)}
.om-funnel__seg--suppressed{background:var(--n-300)}
.om-funnel__legend{display:flex;flex-wrap:wrap;gap:var(--space-2) var(--space-4);margin-top:var(--space-3)}
.om-funnel__legend li{display:flex;align-items:center;gap:var(--space-15);font:var(--type-caption);color:var(--text-secondary)}
.om-funnel__legend i{width:8px;height:8px;border-radius:var(--radius-2xs);flex:none}

/* ---- DROP ZONE (COMPONENTS.md derived: dashed --border-strong, hover -> --primary) ---- */
.om-dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-2);
  padding:var(--space-5) var(--space-5);border:2px dashed var(--border-strong);border-radius:var(--radius-lg);
  background:var(--surface);text-align:center;cursor:pointer;transition:var(--t-all)}
.om-dropzone:hover,.om-dropzone--over{border-color:var(--primary);background:var(--primary-soft)}
.om-dropzone:focus-within{outline:none;box-shadow:var(--shadow-ring)}
.om-dropzone__glyph{display:grid;place-items:center;width:52px;height:52px;border-radius:var(--radius-pill);background:var(--primary-soft);color:var(--primary);font-size:var(--icon-hero)}
.om-dropzone__hint{font:var(--type-caption);color:var(--text-muted)}

/* ---- NUMBER STEPPER (COMPONENTS.md forms: +/- buttons + unit cap) ---- */
.om-numstep{display:inline-flex;align-items:stretch;height:var(--control-h);border:1px solid var(--border);
  border-radius:var(--radius-md);background:var(--surface);overflow:hidden;transition:var(--t-all)}
.om-numstep:focus-within{border-color:var(--primary);box-shadow:var(--glow-focus)}
.om-numstep__btn{display:grid;place-items:center;width:44px;color:var(--text-secondary);background:var(--surface-sunken);transition:var(--t-color)}
.om-numstep__btn:hover{background:var(--blue-50);color:var(--text-brand)}
.om-numstep__btn:disabled{opacity:var(--opacity-disabled);cursor:not-allowed}
.om-numstep__input{width:78px;min-width:0;border:0;outline:none;background:transparent;text-align:center;direction:ltr;
  font:var(--fw-bold) var(--fs-sm)/1 var(--font-mono);font-variant-numeric:tabular-nums;color:var(--text-primary)}
.om-numstep__unit{display:grid;place-items:center;padding:0 var(--space-3);background:var(--surface-sunken);
  border-inline-start:1px solid var(--border);font:var(--fw-medium) var(--fs-2xs)/1 var(--font-sans);color:var(--text-muted);white-space:nowrap}

/* ---- ALERT (Matrix B: 122 hand-rolled .alert collapse here) ---- */
/* flex-wrap + a flex:1 body means the action slot only drops to its own line when
   the text genuinely cannot share the row — 360px with a long sentence — so
   nothing moves on the surfaces that already fit. */
.om-alert{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--space-3);padding:var(--space-4);border-radius:var(--radius-md);
  background:var(--surface);border:1px solid var(--border);box-shadow:var(--shadow-sm)}
.om-alert__icon{flex:none;font-size:var(--icon-tile);line-height:1.2}
.om-alert__body{min-width:0;flex:1}
.om-alert__title{font:var(--type-body-strong);color:var(--text-primary)}
.om-alert__action{flex:none;align-self:center;display:flex;flex-wrap:wrap;gap:var(--space-2)}
/* The validation summary is a list; the kit owns its indent so the 15 call sites
   do not each pick a utility for it. */
.om-alert__list{margin:0;padding-inline-start:var(--space-5)}
.om-alert--success{background:var(--success-soft);border-color:transparent}
.om-alert--success .om-alert__icon{color:var(--success)}
.om-alert--danger{background:var(--danger-soft);border-color:transparent}
.om-alert--danger .om-alert__icon{color:var(--danger)}
.om-alert--warning{background:var(--warning-soft);border-color:transparent}
.om-alert--warning .om-alert__icon{color:var(--warning)}
.om-alert--info{background:var(--info-soft);border-color:transparent}
.om-alert--info .om-alert__icon{color:var(--info)}

/* ---- PERMISSION DENIED (COMPONENTS.md derived from empty-state) ---- */
.om-denied{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-3);
  padding:var(--space-7) var(--space-5);text-align:center;background:var(--surface-soft);border-radius:var(--radius-lg)}
.om-denied__glyph{display:grid;place-items:center;width:64px;height:64px;border-radius:var(--radius-pill);
  background:var(--surface);color:var(--text-muted);font-size:var(--icon-hero);box-shadow:var(--shadow-sm)}
.om-denied__role{font:var(--fw-bold) var(--fs-sm)/1.5 var(--font-sans);color:var(--text-secondary)}

/* ---- KPI STRIP + CARD RAIL TONES (COMPONENTS.md derived) ---- */
.om-kpi-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:var(--space-3)}
/* The side-by-side pair every module dashboard reaches for — two boards that
   must become one column before either is too narrow to read. It is `auto-fit`
   with a floor, not `.row > .col-lg-6`: the Bootstrap pair breaks at a viewport
   width, which is the wrong question inside a grid whose main column is already
   narrowed by a sidebar. 320px is the floor a two-column clinical table needs. */
.om-duo{display:grid;grid-template-columns:repeat(auto-fit,minmax(20rem,1fr));gap:var(--space-3);align-items:start}
.om-card--rail-primary{--om-rail:var(--primary)}
.om-card--rail-success{--om-rail:var(--success)}
.om-card--rail-warning{--om-rail:var(--warning)}
.om-card--rail-danger{--om-rail:var(--danger)}
.om-card--rail-info{--om-rail:var(--info)}

/* ---- READ-ONLY FIELD VALUE (universal state contract: values as text) ---- */
.om-field__value{display:flex;align-items:center;gap:var(--space-2);min-height:var(--control-h);padding:0 var(--space-3);
  border-radius:var(--radius-md);background:var(--surface-sunken);color:var(--text-primary);
  font:var(--fw-bold) var(--fs-sm)/1.4 var(--font-sans)}
/* T11: a read-only ANSWER that is not a field's value — a checkbox's yes/no, a
   stepper's quantity — sits inline in a definition list or a card body, where the
   sunken pill of .om-field__value would read as a second, empty input. Same type
   and rhythm, no box. */
.om-field__value--flat{min-height:0;padding:0;background:transparent;border-radius:0}
.om-check__mark{font-size:var(--icon-btn);color:var(--text-brand)}
.om-field__counter{font:var(--type-caption);color:var(--text-muted);text-align:end;padding-inline-end:var(--space-05)}

/* ---- FORM SECTION HEADER (T10) — one section rhythm.
   Was .form-section-header / .step-badge, declared by TWO sheets with different
   geometry AND different intent: om-clinic drew a bottom hairline with a 28px
   pill, style.css a tinted gradient panel with a 4px `border-right` — physical,
   so on this Arabic-first page the accent landed on the wrong edge — and a 32px
   pill. Which one a form got was decided by link order. One declaration now, and
   the accent is a logical inline-start edge. */
.om-formsection{display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-4);
  padding-bottom:var(--space-3);border-bottom:1px solid var(--border-light)}
/* --step-h is named for the same reason: this pill was 28px in om-clinic.css and
   32px in style.css, for the same element. */
.om-formsection__step{display:grid;place-items:center;flex:none;width:var(--step-h);height:var(--step-h);border-radius:var(--radius-pill);
  background:var(--gradient-primary);color:var(--text-inverse);font:var(--fw-black) var(--fs-xs)/1 var(--font-sans);
  box-shadow:var(--glow-sm)}
.om-formsection__title{font:var(--type-section);color:var(--ink);margin:0}
/* The far-side control (an "add a row" button on a repeater section). margin
   rather than justify-content, so the title keeps sitting beside its badge. */
.om-formsection__actions{margin-inline-start:auto;display:flex;align-items:center;gap:var(--space-2)}

/* ============================================================================
   TOUCH TARGETS - DESIGN-LANGUAGE.md requires >=44px under 768px. The kit
   shipped ONE size ladder, drawn for a pointer, and never implemented the
   mobile uplift. Every later phase composes on this layer, so the floor is
   raised here rather than screen by screen.
   Desktop keeps the dense sizes deliberately: clinical density is a stated
   requirement and 40px is fine with a mouse. Only this branch changes.
   Two kinds of fix. Most controls simply grow, because nothing downstream
   measures them. Where the visual box IS the design - a 40x20 switch pill, an
   avatar whose diameter is its variant - the box is left alone and a centred
   44x44 HIT overlay is added instead (inset:0 + margin:auto + a fixed size
   centres on both axes with no physical offset).
   ========================================================================= */
/* ERGONOMICS REACH THE TABLET — 1024, not 767, because a tablet is a touch
   device and the middleware puts 768-1024 in `tablet`. Rules that COLLAPSE
   LAYOUT stay at 767 (next block): a tablet has room for two columns. */
@media (max-width:1024px){
  .om-btn--sm{height:var(--control-h)}
  .om-input--sm{height:var(--control-h)}
  .om-iconbtn,.om-iconbtn--sm{width:var(--tap-min);height:var(--tap-min)}
  .om-seg__btn,.om-seg--lg .om-seg__btn{height:var(--tap-min)}
  .om-tab{height:var(--tap-min)}
  .om-chip--action{height:var(--tap-min)}
  .om-cal__day{min-height:var(--tap-min)}
  .om-menuitem{min-height:var(--tap-min)}
  .om-check,.om-radio{min-height:var(--tap-min)}
  .om-switch::after,.om-avatar--interactive::after{content:"";position:absolute;inset:0;margin:auto;
    width:var(--tap-min);height:var(--tap-min)}
  /* iOS zooms a focused control under 16px and never zooms back. 1rem, not
     var(--fs-base): density="compact" remaps that token to 14.7px. Element
     selectors reach raw controls; .om-input>* outranks the kit's font shorthand. */
  input:not([type=checkbox]):not([type=radio]):not([type=range]),select,textarea,
  .om-input>input,.om-input>select,.om-input>textarea{font-size:1rem}
}

@media (max-width:767px){
  /* THE RAIL BECOMES A SCROLLING PILL STRIP. This is one media query where
     doctor-mobile.css needed sixteen !important rules under .device-mobile —
     because it was correcting a sheet rather than owning the definition. The
     search goes: a 7-item list needs no filter, and it cost the strip its height. */
  .om-tabshell{grid-template-columns:minmax(0,1fr);gap:var(--space-3)}
  .om-railnav{position:static;padding:var(--space-1);overflow:hidden}
  .om-railnav__search{display:none}
  .om-railnav__nav{display:flex;flex-wrap:nowrap;gap:var(--space-1);overflow-x:auto;scrollbar-width:none;padding-bottom:var(--space-1)}
  .om-railnav__nav::-webkit-scrollbar{display:none}
  .om-railnav__group{display:flex;flex-wrap:nowrap;align-items:center;gap:var(--space-1);margin-bottom:0}
  .om-railnav__grouptitle{display:none}
  .om-railnav__item{flex:none;padding:var(--space-15) var(--space-3);border-color:var(--border);
    border-radius:var(--radius-pill);white-space:nowrap}
  .om-railnav__item--active{background:var(--gradient-primary);border-color:transparent;color:var(--text-inverse)}
  .om-railnav__item--active .om-railnav__icon{background:var(--white-a22);color:var(--text-inverse)}
  .om-railnav__icon{width:var(--railnav-icon-sm);height:var(--railnav-icon-sm);font-size:var(--icon-micro);border-radius:var(--radius-pill)}
  .om-railnav__label small{display:none}
  .om-railnav__dot{display:none}
}

/* ---- METER GEOMETRY: width comes from a custom property, never inline style ----
   spec §4.5 puts inline style="" at 0 bytes, and the 13 hand-rolled bars in the
   app all set their width that way. The percentage therefore travels as a class
   that sets --om-pct (+ --om-pctf for the 1-4% remainder); the two meter fills
   read both. They are set by a modifier class in this file only — never in a
   :root and never in a style attribute. */
.om-progress__fill{width:calc((var(--om-pct,0) + var(--om-pctf,0)) * 1%)}
.om-funnel__seg{width:calc((var(--om-pct,0) + var(--om-pctf,0)) * 1%)}

/* 0-100 in steps of 1 would be 101 rules. The coarse ladder below is 21 rules of
   5%, and the four fine rules after it add the remaining 1-4% — so any whole
   percentage is exactly two classes and the rendered width never rounds away from
   the announced aria-valuenow. That matters most at the ends: 98% must not paint
   a full bar and 2% must not paint an empty track. */
.om-pct-0{--om-pct:0}.om-pct-5{--om-pct:5}.om-pct-10{--om-pct:10}.om-pct-15{--om-pct:15}
.om-pct-20{--om-pct:20}.om-pct-25{--om-pct:25}.om-pct-30{--om-pct:30}.om-pct-35{--om-pct:35}
.om-pct-40{--om-pct:40}.om-pct-45{--om-pct:45}.om-pct-50{--om-pct:50}.om-pct-55{--om-pct:55}
.om-pct-60{--om-pct:60}.om-pct-65{--om-pct:65}.om-pct-70{--om-pct:70}.om-pct-75{--om-pct:75}
.om-pct-80{--om-pct:80}.om-pct-85{--om-pct:85}.om-pct-90{--om-pct:90}.om-pct-95{--om-pct:95}
.om-pct-100{--om-pct:100}
.om-pctf-1{--om-pctf:1}.om-pctf-2{--om-pctf:2}.om-pctf-3{--om-pctf:3}.om-pctf-4{--om-pctf:4}

/* ---- VITAL HELPER LINE + ROW-CARD QUEUE (clinical safety: the flag is
        border + figure + helper line together, never colour alone) ---- */
.om-vital__range{font:var(--type-caption);color:var(--text-muted)}
.om-vital--alert .om-vital__range{color:var(--danger)}
/* The editable tile reuses the .om-input shell, so the flag reaches the FIELD
   BORDER too (EMR-CONSULTATION-MODULE.md: border + figure + helper line move
   together). No second input skin is introduced. */
.om-vital--alert .om-input{border-color:var(--danger)}
.om-rowcard__queue{display:flex;align-items:center;gap:var(--space-3);min-width:0;flex:1}
.om-chips{display:flex;flex-wrap:wrap;gap:var(--space-2)}
/* Was .om-rowcard__chips. Same rule, one call site, and every module index needs
   a chip list in a table cell — a group of chips is not a rowcard part. */

/* استجابة اللمس | Press feedback. Derived: this sheet had 63 :hover bases and
   10 :active ones, and hover means nothing to a finger — rich feedback for a
   mouse, almost none for the tablets this system runs on. Population is the tap
   targets OmKitTouchTargetsTest derives, plus the row-shaped controls; inner
   parts are excluded because their parent carries the press.
   TWO scales: .97 reads as a press on a small control and a lurch on a wide row,
   which is why .om-rowcard already used the subtle one.
   tap-highlight is paired with a press state and never shipped alone — removing
   the platform's grey flash from something with no press of its own leaves a
   control that answers a tap with nothing. */
.om-seg__btn,.om-tab,.om-chip--action,.om-cal__day,.om-pager__link,
.om-avatar--interactive,.om-menuitem,.om-radio-card,a.om-stat,.om-vital{
  -webkit-tap-highlight-color:transparent;touch-action:manipulation}

.om-seg__btn:active,.om-tab:active,.om-chip--action:active,.om-cal__day:active,
.om-pager__link:active,.om-avatar--interactive:active{
  transform:scale(var(--press-scale));transition-duration:var(--motion-fast)}

.om-menuitem:active,.om-radio-card:active,a.om-stat:active,.om-vital:active{
  transform:scale(var(--press-scale-subtle));transition-duration:var(--motion-fast)}

/* The two button variants the press rule at :90 missed. Every other variant
   has one, so these two alone felt dead under a finger. */
.om-btn--attention:active,.om-btn--danger-ghost:active{
  transform:translateY(0) scale(var(--press-scale));transition-duration:var(--motion-fast)}
