@font-face {
  font-family: 'IBM Plex Sans';
  src:
    url('/_shared/webfonts/IBMPlexSans-VariableFont_wdth,wght.woff2') format('woff2-variations'),
    url('/_shared/webfonts/IBMPlexSans-VariableFont_wdth,wght.woff2') format('woff2');
  font-weight: 100 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Sans';
  src:
    url('/_shared/webfonts/IBMPlexSans-Italic-VariableFont_wdth,wght.woff2')
      format('woff2-variations'),
    url('/_shared/webfonts/IBMPlexSans-Italic-VariableFont_wdth,wght.woff2') format('woff2');
  font-weight: 100 700;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Sans Arabic';
  src: url('/_shared/webfonts/IBMPlexSansArabic-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Sans Arabic';
  src: url('/_shared/webfonts/IBMPlexSansArabic-Medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Sans Arabic';
  src: url('/_shared/webfonts/IBMPlexSansArabic-SemiBold.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Sans Arabic';
  src: url('/_shared/webfonts/IBMPlexSansArabic-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Sans Arabic';
  src: url('/_shared/webfonts/IBMPlexSansArabic-Light.woff2') format('woff2');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src:
    url('/_shared/webfonts/JetBrainsMono-VariableFont_wght.woff2') format('woff2-variations'),
    url('/_shared/webfonts/JetBrainsMono-VariableFont_wght.woff2') format('woff2');
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* =====================================================================
     PRIMITIVE TOKENS — raw scales with no meaning attached. Nothing in
     the app should reference these directly except the semantic/component
     tokens below; this indirection is what lets the whole app re-theme or
     re-scale from one place instead of hunting through every component.
     ===================================================================== */

  /* Primitive spacing scale (rem-based so it follows the user's browser
     font-size/zoom setting, not just fixed-px "Ctrl +"). 1rem = 16px. */
  --space-1: 0.25rem; /* 4px  */
  --space-2: 0.5rem; /* 8px  */
  --space-3: 0.75rem; /* 12px */
  --space-4: 1rem; /* 16px */
  --space-5: 1.25rem; /* 20px */
  --space-6: 1.5rem; /* 24px */
  --space-8: 2rem; /* 32px */
  --space-10: 2.5rem; /* 40px */

  /* Primitive type scale (rem-based). Body copy stays close to 1rem —
     fluid clamp() scaling is reserved for headings/display text per the
     "don't shrink body text" rule; see --text-heading-lg below. */
  --text-xs: 0.75rem; /* 12px */
  --text-sm: 0.8125rem; /* 13px */
  --text-base: 0.9375rem; /* 15px — the app's existing base size */
  --text-md: 1rem; /* 16px */
  --text-lg: 1.0625rem; /* 17px */
  --text-xl: 1.25rem; /* 20px */
  --text-2xl: clamp(1.375rem, 1.2rem + 0.8vw, 1.625rem); /* headings scale fluidly */
  /* Labels on icon+label controls (.qa-action-btn, .hub-tab): 12px -> 13px.
     Scales down only slightly -- a control label must stay readable -- and
     is paired with single-line labels (white-space: nowrap + ellipsis) so a
     long label can never wrap a button taller than its neighbours. */
  --text-control: clamp(0.75rem, 0.7rem + 0.2vw, 0.8125rem);

  /* Primitive brand palette */
  --color-primary: #12315a;
  --color-primary-light: #1c4a82;
  --color-primary-dark: #0a2038;
  /* --color-accent: #b08d57; */
  /* --color-accent-light: #d4af37; */
  --color-accent: #c49a6c; /* soft bronze-gold */
  --color-accent-light: #e6c27a; /* muted champagne-gold */

  /* =====================================================================
     SEMANTIC TOKENS — what a primitive *means* in this app. Components
     should reach for these, not the primitives above, so a future retheme
     only touches this block.
     ===================================================================== */

  --surface: #ffffff;
  --surface-sunken: #f4f6f9;
  /* A hair off pure white — used for hover/focus-adjacent surfaces (inputs,
     table-row hover, cards-within-cards) that need to read as "raised but
     inactive" without the stronger contrast of --surface-sunken. Reused in
     4+ places, which is what earns it a token instead of a repeated literal. */
  --surface-hover: #fbfcfe;
  --border: #d7dce3;

  /* Text (WCAG AA: >=4.5:1 on --surface / --surface-sunken) */
  --text: #16202f;
  --text-muted: #4f5a6b;
  --text-on-dark: #ffffff;
  --text-on-dark-muted: rgba(255, 255, 255, 0.8);

  /* Status (foreground darkened for AA text contrast on its own -bg) */
  --success: #157a3f;
  --success-bg: #e5f5eb;
  --warning: #8a5a12;
  --warning-bg: #fbedd3;
  --danger: #a52f22;
  --danger-bg: #fae2df;
  --info: #1f5a94;
  --info-bg: #e3edf9;

  /* Light tints of the two brand colors — the status set above (success/
     warning/danger/info) has no light variant for "primary" or "accent",
     but role-identity avatars need one (Admin/Lawyer use the brand colors
     themselves, not a status color). Reused wherever a light brand tint is
     needed, not just avatars. */
  --primary-bg: #e7ecf3;
  --accent-bg: #f6eee0;

  /* Legacy aliases: kept so every existing component rule below keeps
     working unchanged. New rules should prefer the semantic names above. */
  --color-bg: var(--surface-sunken);
  --color-surface: var(--surface);
  --color-border: var(--border);
  --color-text: var(--text);
  --color-text-muted: var(--text-muted);
  --color-success: var(--success);
  --color-success-bg: var(--success-bg);
  --color-warning: var(--warning);
  --color-warning-bg: var(--warning-bg);
  --color-danger: var(--danger);
  --color-danger-bg: var(--danger-bg);
  --color-info: var(--info);
  --color-info-bg: var(--info-bg);

  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;
  --shadow-sm: 0 1px 2px rgba(18, 49, 90, 0.06);
  --shadow-md: 0 4px 16px rgba(18, 49, 90, 0.08);
  --shadow-lg: 0 12px 32px rgba(18, 49, 90, 0.14);

  /* =====================================================================
     COMPONENT TOKENS — the few values components should actually consume
     directly when nothing more specific exists yet. Kept deliberately
     small; component-specific one-off values stay local to that
     component's rule instead of becoming a global variable (section 18).
     ===================================================================== */

  --control-height: var(--space-10); /* 44px — WCAG/HIG minimum touch target */
  --gutter: clamp(1rem, 4vw, 1.625rem); /* page-edge padding, fluid across all widths */
  --content-max: 1440px; /* keeps dashboards/tables from over-stretching on 1920px+ monitors */
  --content-max-lg: 1800px; /* --content-max's counterpart for >=2560px screens (see
    the responsive breakpoint reference below) -- 1440px alone would leave huge
    bare margins on a wide-desktop/4K display; applied at that breakpoint only,
    not blanketed across every element. */

  /* ---- Dashboard: stat cards (Sept 2026 overhaul, Task 4) ----
     Both size tokens are clamp()s that already cover the old mobile-vs-desktop
     split with ONE fluid value, so the 768px breakpoint no longer needs its
     own override for either -- see the removed rules in section 20. */
  --stat-card-padding: clamp(
    0.625rem,
    0.5rem + 0.3vw,
    0.75rem
  ); /* 10px -> 12px, was 18px 20px desktop / 14px mobile */
  --stat-icon-size: 1.5rem; /* 24px, was 40px */
  --stat-value-size: clamp(
    1.125rem,
    1rem + 0.4vw,
    1.375rem
  ); /* 18px -> 22px, was 26px desktop / 21px mobile */

  /* ---- Dashboard: Quick Case Access panel (Sub-phase 3.2, ground-up rewrite,
     Task 5) -- new concept, no prior equivalent existed for either token. ---- */
  --qcp-avatar-size: 1.75rem; /* 28px */
  --qcp-tile-radius: var(--radius-md);

  /* ---- RESPONSIVE BREAKPOINT REFERENCE (documented literals, not tokens) ----
     CSS custom properties cannot be substituted into an @media condition --
     no browser resolves var() there, and this app has no build step to fake
     it with a preprocessor. Every @media rule in this file should use one of
     these exact pixel values so "which breakpoint is this" stays answerable
     by a single search, even though they can't be var()s.
       375px   small-mobile floor (implicit; nothing narrower is special-cased)
       640px   mobile / mobile-landscape ceiling               [existing]
       768px   sidebar -> bottom-nav swap                      [existing]
       1024px  tablet ceiling                                  [existing, as "641-1024px"]
       1366px  common-laptop ceiling                           [new, not yet used]
       1680px  wide-laptop ceiling / desktop floor              [new, not yet used]
       2560px  wide-desktop ceiling / 4K floor -- see --content-max-lg above [new] */

  /* ---- Layout ---- */
  --sidebar-width: 264px;
  --topbar-height: 68px;

  /* ---- Mobile (native-app-style bottom navigation) ---- */
  --bottom-nav-height: 60px;
  --mobile-topbar-height: 56px;
  --safe-area-bottom: env(safe-area-inset-bottom, 0px);
  --safe-area-top: env(safe-area-inset-top, 0px);

  /* ---- Typography ---- */
  --font-ar: 'IBM Plex Sans Arabic', 'Segoe UI', sans-serif;
  --font-en: 'IBM Plex Sans', 'Segoe UI', sans-serif;

  /* ---- Glassmorphism (currently only the login card uses this; kept as
     named tokens rather than inline literals so any future glass surface
     reads from the same values). Tuned against a dark background image —
     re-check contrast on .login-form-wrap's own near-opaque panel (not
     these tokens) if this is ever pointed at a light background instead. */
  --glass-bg: rgba(255, 255, 255, 0.12);
  --glass-border: rgba(255, 255, 255, 0.25);
  --glass-blur: 1.375rem; /* 22px */
  --glass-saturate: 160%;
}

/* Canonical breakpoints (hardcoded in @media rules — custom properties
   can't drive media-query conditions). Every responsive override in this
   file lives in ONE consolidated section at the very end, ordered by
   breakpoint, specifically so a later base-style rule can never win a
   cascade tie against an earlier mobile override again (see the "Mobile
   breakpoints" section at the bottom of this file for the full story). */

* {
  box-sizing: border-box;
  min-width: 0;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-ar);
  font-size: 15px;
  line-height: 1.6;
  overflow-wrap: break-word;
}

html[lang='en'] body {
  font-family: var(--font-en);
}

h1,
h2,
h3,
h4 {
  margin: 0 0 8px;
  font-weight: 600;
}

button,
input,
select,
textarea {
  font-family: inherit;
  font-size: inherit;
}

a {
  color: inherit;
}

::selection {
  background: var(--color-accent-light);
}

/* ---------- Login ---------- */
.login-screen {
  /* 100vh first as the universal fallback, then 100dvh where supported.
     On mobile Safari/Chrome, 100vh is the viewport height with the URL bar
     RETRACTED, so at rest the element overflows by the bar's height and the
     photograph's bottom edge sits off-screen (and the layout shifts as the
     bar hides on scroll). 100dvh tracks the *actual* visible height, which
     is what a full-bleed background needs. Older browsers simply never
     apply the second declaration. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
  /* Three stacked layers (first listed paints on top): a brand-colored tint
     (keeps the card and orb colors legible no matter how bright/busy the
     photo is), the photo itself, then the original solid gradient as a
     fallback — if background.jpg 404s, the image layer simply doesn't paint
     and the gradient shows through underneath, so this never depends on
     the file existing (same "never a broken-image icon" rule brandMark()
     already follows elsewhere in this file).
     `cover` rescales live on every resize, so the image always fills the
     viewport edge-to-edge with no letterboxing (no white/black bars) at any
     aspect ratio, and `no-repeat` means a tiling seam is impossible even if
     a future image were smaller than the viewport. `background-attachment`
     is left at its default (scroll) rather than `fixed`, which is unreliable
     on mobile Safari and unnecessary here since .login-screen has no
     internal scrolling content of its own.

     The DEFAULT position is `center center`; portrait phones override it
     below — see the comment on that media query for why the subject's
     off-centre placement in this particular photograph makes that
     necessary. */
  background-image:
    linear-gradient(
      135deg,
      rgba(10, 32, 56, 0.6),
      rgba(18, 49, 90, 0.4) 55%,
      rgba(28, 74, 130, 0.3)
    ),
    url('/assets/background.jpg'),
    linear-gradient(
      135deg,
      var(--color-primary-dark),
      var(--color-primary) 55%,
      var(--color-primary-light)
    );
  background-size: cover, cover, cover;
  background-position:
    center center,
    center center,
    center center;
  background-repeat: no-repeat, no-repeat, no-repeat;
  padding: var(--gutter);
}

/* ---------- Login background: keeping the subject in frame ----------
   `background.jpg` is a 2440x1440 photograph — aspect 1.694, i.e. roughly
   5:3, NOT the 16:9 the earlier 1249x700 source was. Measured against the
   file itself, its subject (the brass scales, sampled as the warm pixels
   that are the only saturated thing in an otherwise blue-grey scene)
   occupies x 58.3%-82.9% and y 24.4%-84%. So the subject is both
   off-centre horizontally and a full quarter of the frame wide.

   With `cover`, a viewport narrower than the image is filled by matching
   the image's HEIGHT and cropping its WIDTH, and the surviving slice is
   (viewport aspect / image aspect) of the original width:

     375x667  -> 33.2% slice      768x1024 -> 44.3% slice
     393x852  -> 27.2% slice      414x896  -> 27.3% slice

   Centred, that slice lands on the empty glass partition around x=44-56%
   and crops the scales out of frame entirely, leaving a login screen
   backed by featureless grey. So the portrait override shifts HORIZONTALLY
   toward the subject, rather than the vertical `center top` that a
   top-weighted photograph would want. Vertical stays centred because the
   scales already span most of the image's height.

   Why 78% and not the 72% this rule used against the old source: the
   subject is 24.6% of the image wide, and the narrowest slice above is
   27.2%, so there is only ~2.6% of total slack. Solving
   `position x (1 - slice)` to keep 58.3%-82.9% inside the window gives
   [76.5%, 80.1%] at 393x852 and wider ranges everywhere else; 78% is the
   midpoint of that intersection. At 72% the right-hand pan and chain fell
   outside the window by ~3% of the image width and were visibly clipped.

   Deliberately a constant rather than a `clamp()` on viewport width: the
   variable that drives the crop is the viewport's ASPECT RATIO, which no
   width-based `clamp()` can track, and the feasible band above is narrow
   enough that a single value is already at its optimum for every portrait
   size in range.

   Written as `orientation: portrait` AND a width ceiling, not width alone:
   a tablet held in portrait (768x1024, 0.75) crops enough to need this too,
   while a small laptop window at 800px wide is landscape and does not. */
@media (orientation: portrait) and (max-width: 900px) {
  .login-screen {
    background-position:
      78% center,
      78% center,
      78% center;
  }
}

/* There is deliberately NO mobile-landscape override here.
   An earlier `@media (orientation: landscape) and (max-height: 500px)` rule
   re-asserted `center center` for phones held sideways. It was removed
   because it was doing nothing it claimed to do:

     - Where it DID win (landscape, short, aspect under 2:1) it set exactly
       the value the default rule already sets, so it was a no-op.
     - Where it looked like it mattered it did not win at all. A phone in
       landscape is typically 2.1-2.2 wide (iPhone 14 Pro is 852x393 =
       2.168, iPhone XR 896x414 = 2.164), which matches the `min-aspect-
       ratio: 2/1` rule below. Equal specificity, later in the source, so
       the ultra-wide rule takes it — as verified by reading the computed
       `background-position` at 852x393, which is 50% 58%, not 50% 50%.

   That outcome is the correct one, so only the misleading rule is gone:
   at 2.168 the viewport crops 22% of the image's height, which is the
   ultra-wide rule's problem, not the "nothing is cropped" case the removed
   comment described. And across every landscape viewport where the
   ultra-wide rule does NOT apply (aspect under 2:1), at most 15% of either
   axis is cropped and the subject stays inside the frame at plain centre —
   so no override is needed there either. */

/* Ultra-wide (2:1 and beyond — which includes phones in landscape, see
   above): now the WIDTH matches and the top/bottom are cropped instead.
   At 2560x1080 only 71.5% of the image's height survives (1.694 / 2.370),
   against a subject spanning y 24.4%-84%.

   `center 40%` — correct for the old 16:9 source, which lost less height
   here — now places the window at y 11.4%-82.9% and slices the foot of the
   scales' base off the bottom. Keeping 24.4%-84% inside needs a position in
   [50.9%, 78.5%]; 58% sits inside that with margin at both edges and still
   crops away the brightest part of the reflective foreground, which is what
   the upward bias was for. */
@media (min-aspect-ratio: 2/1) {
  .login-screen {
    background-position:
      center 58%,
      center 58%,
      center 58%;
  }
}

.login-orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(60px);
  opacity: 0.55;
  pointer-events: none;
  animation: orb-float 16s ease-in-out infinite;
}
.login-orb.orb-1 {
  width: 460px;
  height: 460px;
  background: var(--color-accent);
  top: -140px;
  inset-inline-start: -120px;
  animation-duration: 18s;
}
.login-orb.orb-2 {
  width: 380px;
  height: 380px;
  background: #2b6cb0;
  bottom: -160px;
  inset-inline-end: -100px;
  animation-duration: 22s;
  animation-delay: -6s;
}
.login-orb.orb-3 {
  width: 300px;
  height: 300px;
  background: #1f8a4c;
  top: 40%;
  inset-inline-start: 45%;
  opacity: 0.35;
  animation-duration: 26s;
  animation-delay: -12s;
}

@keyframes orb-float {
  0%,
  100% {
    transform: translate(0, 0) scale(1);
  }
  33% {
    transform: translate(40px, -30px) scale(1.08);
  }
  66% {
    transform: translate(-30px, 25px) scale(0.95);
  }
}

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

.login-card {
  /* This is the ONLY glassed surface on the page — deliberately. Everything
     inside it (.login-form-wrap's inputs/labels/title, the submit button,
     the error box) stays on its own near-opaque or fully solid background
     so legibility never depends on what happens to be behind the card. */
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 58.75rem; /* 940px */
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  display: grid;
  grid-template-columns: 1fr 1fr;
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
}

/* Mobile override for .login-card/.login-brand lives in the consolidated
   responsive section at the end of this file (see "Mobile breakpoints") —
   NOT here. Placing it here previously caused a real bug: this rule sat
   *before* the base .login-brand rule below, and because both are simple
   class selectors of equal specificity, the base rule's `display: flex`
   (declared later in the file) silently won the cascade at every width,
   so the mobile-only "hide the brand panel" override never actually
   applied. Every responsive rule in this app must live after every base
   rule it touches — the consolidated end-of-file section guarantees that
   by construction instead of relying on developers remembering it. */

.login-brand {
  background: rgba(10, 32, 56, 0.35);
  color: #fff;
  padding: 48px 36px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  border-inline-end: 1px solid rgba(255, 255, 255, 0.15);
}

.login-brand .brand-mark {
  width: 56px;
  height: 56px;
  border-radius: 14px;
  background: var(--color-accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 22px;
  color: var(--color-primary-dark);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
}

.login-brand h1 {
  font-size: 24px;
  margin-top: 20px;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.login-brand p {
  color: rgba(255, 255, 255, 0.85);
  font-size: 14px;
}

.login-form-wrap {
  /* Ported from the same login treatment on alseif-connect
     (mobile-app/alseif-connect/public/css/app.css, .login__card): glass
     panel + white text + a blurred dark halo for guaranteed contrast,
     instead of the opaque backing this used before. Everything below is
     scoped under .login-form-wrap specifically so none of it leaks into
     the shared .form-group/input rules the rest of the app relies on —
     same reasoning the reference file documents for its own .login__card
     scoping. */
  position: relative;
  z-index: 1;
  padding: clamp(1.25rem, 3vw + 0.5rem, 2.75rem) clamp(1rem, 3vw, 2.25rem);
  background: var(--glass-bg);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
}
/* A soft blurred dark halo confined behind this panel's own content, not a
   box — it stays invisible as a shape, but boosts contrast for the
   title/labels/inputs no matter how light the photo is at that spot.
   z-index -1 keeps it inside .login-form-wrap's own stacking context
   (established by position:relative + z-index:1 above), so it sits behind
   the text but still above .login-screen's photo. */
.login-form-wrap::before {
  content: '';
  position: absolute;
  inset: -10% -8%;
  z-index: -1;
  background: radial-gradient(
    60% 68% at 50% 42%,
    rgba(3, 6, 14, 0.55) 0%,
    rgba(3, 6, 14, 0.28) 55%,
    transparent 82%
  );
  filter: blur(1rem);
}
.login-form-wrap h2,
.login-form-wrap p.text-muted {
  color: #fff;
  text-shadow:
    0 0.125rem 0.25rem rgba(0, 0, 0, 0.6),
    0 0.25rem 1.25rem rgba(0, 0, 0, 0.5);
}
.login-form-wrap .form-group label {
  color: rgba(255, 255, 255, 0.95);
  text-shadow:
    0 0.0625rem 0.1875rem rgba(0, 0, 0, 0.6),
    0 0.125rem 0.5rem rgba(0, 0, 0, 0.45);
}
.login-form-wrap .form-group input {
  background: rgba(255, 255, 255, 0.14);
  border-color: rgba(255, 255, 255, 0.38);
  color: #fff;
  backdrop-filter: blur(0.625rem);
  -webkit-backdrop-filter: blur(0.625rem);
}
.login-form-wrap .form-group input::placeholder {
  color: rgba(255, 255, 255, 0.55);
}
.login-form-wrap .form-group input:focus {
  border-color: var(--color-accent-light);
  background: rgba(255, 255, 255, 0.22);
  box-shadow: 0 0 0 3px rgba(230, 194, 122, 0.28);
}
/* Matches the base .form-group.invalid .field-error selector's own
   specificity/condition (only visible once actually invalid) — a plain
   `.login-form-wrap .field-error` rule would have overridden the base
   `display: none` unconditionally and shown the chip on every fresh load. */
.login-form-wrap .form-group.invalid .field-error {
  color: #fff;
  background: var(--color-danger);
  display: inline-block;
  padding: 0.125rem 0.5rem;
  border-radius: var(--radius-sm);
  margin-top: 0.375rem;
}
/* Browsers without backdrop-filter would otherwise show barely-there glass
   over a busy photo — fall back to solid-enough tinted panels instead,
   same defensive rule the reference file uses for its own inputs. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .login-form-wrap {
    background: rgba(10, 32, 56, 0.72);
  }
  .login-form-wrap .form-group input {
    background: rgba(10, 32, 56, 0.55);
  }
}

.form-group {
  margin-bottom: 18px;
}
.form-group label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 6px;
  color: var(--color-text-muted);
}
.form-group input,
.form-group select,
.form-group textarea {
  width: 100%;
  min-height: var(--control-height);
  padding: 11px 14px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--surface-hover);
  transition:
    border-color 0.15s,
    box-shadow 0.15s;
}
.form-group textarea {
  min-height: calc(var(--control-height) * 1.6);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
  outline: none;
  border-color: var(--color-primary-light);
  box-shadow: 0 0 0 3px rgba(28, 74, 130, 0.12);
}
.field-error {
  color: var(--color-danger);
  font-size: 12px;
  margin-top: 4px;
  display: none;
}
.form-group.invalid input,
.form-group.invalid select {
  border-color: var(--color-danger);
}
.form-group.invalid .field-error {
  display: block;
}
/* A neutral note under a field -- not an error, so it stays visible at all
   times rather than only in the .invalid state. Used by the New Case
   modal's Automated Number field to show the year being derived from the
   digits typed so far, so the value the server will infer is visible
   BEFORE saving. `margin-block-start` rather than `margin-top` for the
   same reason as everything else in this file: it mirrors under dir=rtl. */
.field-hint {
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  margin-block-start: var(--space-1);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-height);
  padding: 0.6875rem 1.25rem; /* 11px 20px */
  border-radius: var(--radius-sm);
  border: none;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.875rem; /* 14px */
  transition:
    transform 0.1s,
    box-shadow 0.15s,
    background 0.15s;
}
.btn:active {
  transform: translateY(1px);
}
.btn:focus-visible {
  /* Shared across every .btn variant. A solid primary-light outline (~8:1
     on the page background), not the former 3px 25%-alpha box-shadow, which
     blended to ~1.5:1 -- under WCAG 1.4.11's 3:1 for a focus indicator. The
     same outline every interactive component in the app now uses (KI-4). */
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
}
/* ---- The app's two required button variants: Primary and Secondary ----
   Every :hover/:focus-visible/:active/:disabled state either variant needs
   is defined once: :active/:focus-visible/:disabled above are shared by
   every .btn (including these two), so only :hover differs per variant
   below. Both read their colors from the semantic tokens in :root, so a
   retheme only ever touches this file's one variable block. */
.btn-primary {
  background: var(--color-primary);
  color: #fff;
}
.btn-primary:hover {
  background: var(--color-primary-light);
}
.btn-secondary,
.btn-outline {
  /* .btn-outline is this variant's original class name and stays fully
     supported (every page still uses it) — .btn-secondary is a same-rule
     alias so new markup can spell out "secondary" explicitly. */
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text);
}
.btn-secondary:hover,
.btn-outline:hover {
  border-color: var(--color-primary-light);
}
/* ---- Additional intentional variants beyond the required pair ----
   Accent marks a special-emphasis CTA (e.g. "create reminder") and Danger
   marks a destructive action (e.g. delete) — distinct real semantics the
   primary/secondary pair can't express, so they're kept rather than folded
   in and losing that meaning. */
.btn-accent {
  background: var(--color-accent);
  color: var(--color-primary-dark);
}
.btn-accent:hover {
  background: var(--color-accent-light);
}
.btn-danger {
  background: var(--color-danger-bg);
  color: var(--color-danger);
}
.btn-sm {
  /* Intentionally below --control-height: this variant is used for dense,
     repeated inline actions (table rows, document/case-list actions) where
     a full 44px row-height per action would make already-dense data views
     unusably tall. 36px (2.25rem) still clears the ~32px "secondary
     control" floor most touch guidelines allow, and siblings keep >=6px
     gap to avoid mis-taps. */
  min-height: 2.25rem; /* 36px */
  padding: 0.4375rem 0.75rem; /* 7px 12px */
  font-size: 0.78125rem; /* 12.5px */
}
.btn-block {
  width: 100%;
}
.btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.quick-avatar {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: var(--color-primary);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 13px;
  flex-shrink: 0;
}
/* Role identity: one consistent icon + color per role, everywhere a user
   avatar appears (sidebar, topbar, users table) — see roleAvatar() in
   ui.js. Contrast checked against WCAG 1.4.11 (3:1 for non-text/icon
   content), not the stricter 4.5:1 text threshold. */
.role-avatar-admin {
  background: var(--primary-bg);
  color: var(--color-primary);
}
.role-avatar-lawyer {
  background: var(--accent-bg);
  color: #8a6b3d;
}
.role-avatar-consultant {
  background: var(--info-bg);
  color: var(--info);
}
.role-avatar-delegate {
  background: var(--warning-bg);
  color: var(--warning);
}
.role-avatar-client {
  background: var(--surface-sunken);
  color: var(--text-muted);
}
.brand-logo-img {
  /* The logo artwork already IS a self-contained dark rounded-square
     badge with the gold scale icon baked in — inheriting .brand-mark's
     own flat gold fill/box behind it (meant for the plain-text "AS"
     fallback, which has no artwork of its own and needs a background for
     contrast) produced a clashing "double frame": a solid gold square
     with a smaller dark rounded image floating inside it. Dropping the
     background/padding lets the artwork read as one clean badge; the
     soft shadow + thin translucent gold ring add depth/polish without
     reintroducing a competing fill color. */
  background: transparent;
  padding: 0;
  object-fit: contain;
  box-shadow:
    0 6px 16px rgba(0, 0, 0, 0.35),
    0 0 0 1.5px rgba(176, 141, 87, 0.55);
}
.login-error-box {
  background: var(--color-danger-bg);
  color: var(--color-danger);
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  margin-bottom: 16px;
  display: none;
}
.login-error-box.show {
  display: block;
}

/* ---------- App Shell ---------- */
.app-shell {
  display: flex;
  min-height: 100vh;
}

.sidebar {
  width: var(--sidebar-width);
  background: var(--color-primary-dark);
  color: #fff;
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  flex-shrink: 0;
}
.sidebar-brand {
  padding: 22px 20px;
  display: flex;
  align-items: center;
  gap: 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.sidebar-brand .brand-mark {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  background: var(--color-accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  color: var(--color-primary-dark);
}
.sidebar-brand .brand-text {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.3;
}
.sidebar-brand .brand-sub {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.6);
}

.sidebar-nav {
  flex: 1;
  padding: 14px 10px;
  overflow-y: auto;
}
.nav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  color: rgba(255, 255, 255, 0.78);
  cursor: pointer;
  margin-bottom: 4px;
  font-size: 13.5px;
  font-weight: 500;
  /* Now an <a href> (was a <div>: not keyboard-reachable at all). */
  text-decoration: none;
  transition:
    background 0.15s,
    color 0.15s;
}
/* On the dark sidebar the app's usual primary-light outline would be nearly
   invisible (dark blue on dark blue); the light accent is high-contrast. */
.nav-item:focus-visible {
  outline: 2px solid var(--color-accent-light);
  outline-offset: 2px;
}
.nav-item .nav-icon {
  width: 18px;
  text-align: center;
  flex-shrink: 0;
}
.nav-item:hover {
  background: rgba(255, 255, 255, 0.06);
  color: #fff;
}
.nav-item.active {
  background: var(--color-accent);
  color: var(--color-primary-dark);
  font-weight: 700;
}
.nav-badge {
  margin-inline-start: auto;
  background: var(--color-danger);
  color: #fff;
  font-size: 10.5px;
  font-weight: 700;
  padding: 1px 7px;
  border-radius: 20px;
}

.sidebar-footer {
  padding: 14px 16px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.sidebar-user {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
.sidebar-user .quick-avatar {
  margin: 0;
}
.sidebar-user .u-name {
  font-size: 12.5px;
  font-weight: 700;
}
.sidebar-user .u-role {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.6);
}

.sidebar-footer .btn-outline {
  background: rgba(255, 255, 255, 0.04);
  border-color: rgba(255, 255, 255, 0.25);
  color: #fff;
}
.sidebar-footer .btn-outline:hover {
  background: rgb(255 0 0 / 10%);
  border-color: rgb(255 0 0 / 40%);
}

.main-area {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.topbar {
  height: var(--topbar-height);
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 26px;
  position: sticky;
  top: 0;
  z-index: 20;
}
.topbar-title {
  font-size: 17px;
  font-weight: 700;
  /* This is navigation chrome, not the page's actual content — every page
     already repeats its full descriptive heading in its own <h2> (e.g.
     "Users & Permissions Management"), so truncating the short nav label
     here loses nothing. Without this, a long label plus the fixed-height
     topbar's lack of vertical overflow room would let text wrap and get
     silently clipped instead. */
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.topbar-actions {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-shrink: 0;
}
.icon-btn {
  width: var(--control-height);
  height: var(--control-height);
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  position: relative;
  font-size: 1.1rem;
  flex-shrink: 0;
}
.icon-btn:hover {
  background: var(--color-bg);
}
.icon-badge {
  position: absolute;
  top: -3px;
  inset-inline-end: -3px;
  background: var(--color-danger);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  border-radius: 20px;
  padding: 1px 5px;
  min-width: 16px;
  text-align: center;
}

.page-content {
  /* Fluid gutter (clamp) replaces a hard desktop/mobile padding jump, so
     the page reads correctly at every width in between, not just at the
     two sizes someone happened to test (section 2B: "avoid arbitrary
     fixed widths ... prefer fluid sizing"). */
  padding: var(--gutter);
  flex: 1;
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
}

/* ---------- Cards & Stats ---------- */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}
.stat-card {
  background: var(--color-surface);
  border-radius: var(--radius-md);
  padding: var(--stat-card-padding);
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--color-border);
  position: relative;
  overflow: hidden;
}
.stat-card .stat-icon {
  width: var(--stat-icon-size);
  height: var(--stat-icon-size);
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(
    var(--stat-icon-size) * 0.45
  ); /* same 18px:40px ratio as before,
    genuinely derived via calc() so it keeps tracking the box if --stat-icon-size
    ever changes -- an em here would resolve against the inherited text size, not
    the icon box, and silently stop tracking it */
  margin-bottom: 10px;
}
.stat-card .stat-value {
  font-size: var(--stat-value-size);
  font-weight: 700;
}
.stat-card .stat-label {
  font-size: var(
    --text-xs
  ); /* was a bespoke 12.5px -- close enough to the existing
    primitive that a dedicated token would just duplicate it (see the Sept 2026
    Foundation report: only add a token for a genuinely new, reused value). */
  color: var(--color-text-muted);
  font-weight: 600;
}

.stat-section-label {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin: 4px 0 10px;
}

/* ---------- Procedural Intelligence: Case Details "Procedure" block ----------
   Logical (inline-start/end) properties throughout, so this mirrors correctly
   under [dir="rtl"] with no separate rule, matching every other panel here. */
.proc-latest {
  background: var(--color-info-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-3);
  font-size: var(--text-sm);
}
.proc-latest-label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-muted);
  margin-bottom: 2px;
}
.proc-next-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.proc-action-card {
  border: 1px solid var(--color-border);
  border-inline-start: 3px solid var(--color-accent);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
}
.proc-history {
  margin-top: var(--space-3);
}
.proc-history > summary {
  cursor: pointer;
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  padding: var(--space-2) 0;
}
.proc-history[open] > summary {
  color: var(--color-text);
}

/* ---------- Official Search Engine: source-of-truth disclaimer + sync panel --- */
.search-source-notice {
  /* Sits below the results (last thing in the search panel), so it needs
     its own clearance from the results table above it. */
  margin-block: var(--space-5) 0;
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  background: var(--color-warning-bg);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* ---------- Dashboard: Quick Case Access panel ----------
   Sub-phase 3.2 (Sept 2026 overhaul, Task 5) -- ground-up rewrite, not a
   restyle. The prior version (.qa-chip / .qa-case-pill, filled pill-shaped
   chips over a vertically-stacked text pill) is gone entirely; nothing here
   reuses its rule bodies. The `qcp-` prefix is deliberately different from
   the old `qa-` prefix so a future diff can never confuse the two.
   Interaction pattern is intentionally UNCHANGED (click a category chip to
   filter, click a tile to open the case) -- that's a proven UX decision from
   a prior batch, not styling, and this overhaul's brief asked for new
   styling, not a new interaction model. Logical (inline-start/end)
   properties throughout, so RTL mirrors with no separate rule. All sizes on
   the shared rem/em token scale. */
.qcp-panel {
  margin-bottom: var(--space-6);
}

/* Segmented-control chip row, not filled pill buttons: the active state is
   an underline + color shift rather than a solid fill, which is the visual
   change that actually makes this read as a different component at a
   glance, not just a recolored one. */
.qcp-chiprow {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: var(--space-3) 0 var(--space-4);
  border-bottom: 1px solid var(--color-border);
}
.qcp-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border: none;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: var(--color-text-muted);
  border-radius: 0;
  padding: var(--space-2) var(--space-3);
  margin-bottom: -1px; /* sits on top of .qcp-chiprow's border */
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  transition:
    color 0.15s,
    border-color 0.15s;
}
.qcp-chip:hover {
  color: var(--color-text);
}
.qcp-chip:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
}
.qcp-chip.active {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
}
.qcp-chip-count {
  background: var(--surface-sunken);
  color: inherit;
  border-radius: 999em;
  padding: 0.05em 0.6em;
  font-size: var(--text-xs);
  font-weight: 700;
}
.qcp-chip.active .qcp-chip-count {
  background: var(--primary-bg);
  color: var(--color-primary);
}

/* Tile grid with an avatar, not a text-only pill: a person-facing identity
   marker (initials) reads faster than a bare case number when scanning a
   list of a dozen entries, which is the actual usability problem this
   redesign is solving, not just a fresh coat of paint. */
.qcp-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
  gap: var(--space-2);
}
.qcp-tile {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  text-align: start;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--qcp-tile-radius);
  padding: var(--space-2) var(--space-3);
  min-height: var(--control-height);
  cursor: pointer;
  transition:
    border-color 0.15s,
    box-shadow 0.15s,
    transform 0.15s;
}
.qcp-tile:hover {
  border-color: var(--color-accent);
  box-shadow: var(--shadow-sm);
  transform: translateY(-1px);
}
.qcp-tile:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
}
.qcp-avatar {
  flex-shrink: 0;
  width: var(--qcp-avatar-size);
  height: var(--qcp-avatar-size);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-bg);
  color: var(--color-primary-dark);
  font-size: var(--text-xs);
  font-weight: 700;
}
.qcp-tile-body {
  /* flex (not the bare block the min-width:0 fix alone would give) so
     -webkit-line-clamp below has the block-level box it requires --
     .qcp-tile-num/.qcp-tile-party are <span>s, and overflow/text-overflow/
     line-clamp on an inline box are spec'd to do nothing at all, which was
     the actual cause of the party name spilling past the tile: every
     truncation property was already present but silently inert. */
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.qcp-tile-num {
  display: block;
  font-weight: 700;
  font-size: var(--text-sm);
  color: var(--color-primary-dark);
}
.qcp-tile-party {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  overflow: hidden;
  overflow-wrap: break-word;
  max-width: 100%;
  /* Two lines, not one-line ellipsis: the opposing party is real context a
     lawyer needs at a glance, and a long name is common in this dataset
     ("Heirs of Khalid Al-Anzi vs United Insurance Co."). */
}

@media (prefers-reduced-motion: reduce) {
  .qcp-tile {
    transition: none;
  }
  .qcp-tile:hover {
    transform: none;
  }
}

/* =====================================================================
   Dashboard panel rows (Sub-phase 3.4, restructured on review). Replaces
   the earlier 2fr/1fr two-column grid: every panel below the stat grids
   is now a full-width row, except My Week + Watched Cases, which share one
   row 50/50. Rows stack in a single-column grid so every gap -- between
   rows and inside the split row -- is the same token, and no row can be
   stretched to match another's height.
   ===================================================================== */
.dash-rows,
.dash-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
  align-items: start;
}
.dash-row-split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
  /* stretch, not start: two cards sharing one row read as a pair only if
     they end together. With start, the shorter card (usually My Week)
     left a hole of bare page background under it before the next
     full-width row -- the "empty gap" the layout review ruled out. */
  align-items: stretch;
}
@media (max-width: 768px) {
  .dash-row-split {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---------- Quick Actions row (Gap C) ----------
   A distinct feature from the pre-existing "Quick Case Access" (.qcp-*)
   browse panel above it -- six one-click shortcuts, not a case browser. */
.qa-actions-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
}
/* Reused inside the Useful Kuwait Websites panel body (kuwait-websites.js),
   where it's the last thing in the panel rather than a standalone row with
   more page content below it -- the space-5 margin above was doubling up
   with .panel-body's own bottom padding there, exactly the kind of
   inconsistent trailing gap flagged on review. */
.panel-body > .qa-actions-row:last-child {
  margin-bottom: 0;
}
.qa-action-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-4);
  min-height: var(--control-height);
  max-inline-size: 100%;
  font-size: var(--text-control);
  font-weight: 600;
  line-height: 1.2;
  color: var(--text);
  /* The Kuwait links are <a> elements; without this they kept the browser's
     link underline and never truly matched the Quick Actions buttons. */
  text-decoration: none;
  cursor: pointer;
  transition:
    border-color 0.15s,
    box-shadow 0.15s,
    transform 0.15s;
}
/* Single-line labels: a label too long for the space ellipsizes (full text
   stays in aria-label / title) instead of wrapping the button taller. */
.qa-action-btn .btn-label,
.hub-tab .btn-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.qa-action-btn:hover {
  border-color: var(--color-accent);
  box-shadow: var(--shadow-sm);
  transform: translateY(-1px);
}
/* A 2px outline, not the former 25%-alpha box-shadow: blended onto white
   that ring was ~1.5:1, under WCAG 1.4.11's 3:1. Same fix as .hub-tab. */
.qa-action-btn:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
}
.qa-action-btn i {
  color: var(--color-accent);
}
@media (prefers-reduced-motion: reduce) {
  .qa-action-btn {
    transition: none;
  }
  .qa-action-btn:hover {
    transform: none;
  }
}
/* Icon + label -> icon only below 768px (Item 3). 767.98px, not the
   file's usual 768px: at exactly 768px the labels must still show. The
   label is visually hidden, not display:none, so it stays in the
   accessibility tree; every such button also carries aria-label (the
   accessible name) and title (the pointer tooltip). The button becomes a
   fixed square the size of a touch target, so a row of them can't wrap
   into uneven heights the way long labels did. */
@media (max-width: 767.98px) {
  .qa-action-btn {
    position: relative;
    justify-content: center;
    inline-size: var(--control-height);
    padding-inline: 0;
    gap: 0;
  }
  .qa-action-btn .btn-label,
  .hub-tab:not([aria-selected='true']) .btn-label {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  /* The Dashboard's Quick search trigger: its "Quick search" label already
     hides at this width (.label-full), and a "Ctrl K" hint is meaningless
     on the touch devices this width serves -- icon only. Its accessible
     name comes from aria-label, not the hidden text. */
  #cmdk-trigger .cmdk-kbd {
    display: none;
  }
  /* Hub tabs keep the SELECTED tab's label visible, so the page always says
     which tab you are on; only the other tabs collapse to their icon. */
  .hub-tab {
    position: relative;
  }
  .hub-tab:not([aria-selected='true']) {
    padding-inline: var(--space-3);
    gap: 0;
  }
}

/* ---------- Action Stream (merged notifications + open tasks) ---------- */
.astream-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.astream-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  width: 100%;
  text-align: start;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-2);
  font: inherit;
  color: inherit;
  cursor: default;
}
.astream-row.astream-clickable {
  cursor: pointer;
}
.astream-row.astream-clickable:hover {
  background: var(--surface-sunken);
}
.astream-row.astream-clickable:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
}
.astream-row.astream-overdue {
  background: var(--danger-bg);
}
.astream-row.astream-soon {
  background: var(--warning-bg);
}
.astream-icon {
  flex-shrink: 0;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-xs);
  background: var(--surface-sunken);
  color: var(--text-muted);
}
.astream-icon-alert {
  background: var(--info-bg);
  color: var(--info);
}
.astream-icon-system {
  background: var(--accent-bg);
  color: var(--color-accent);
}
.astream-icon-task {
  background: var(--primary-bg);
  color: var(--color-primary);
}
.astream-body-text {
  min-width: 0;
  flex: 1;
}
.astream-title-line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.35;
}
.astream-dot {
  flex-shrink: 0;
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  background: var(--info);
}
.astream-meta {
  display: block;
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: 2px;
}
.astream-overdue-tag {
  color: var(--danger);
}

/* ---------- My Week widget ---------- */
.myweek-stats {
  display: flex;
  gap: var(--space-5);
  margin-bottom: var(--space-3);
}
.myweek-stat {
  display: flex;
  flex-direction: column;
}
.myweek-num {
  font-size: var(--text-xl);
  font-weight: 700;
  color: var(--text);
}
.myweek-num-success {
  color: var(--success);
}
.myweek-num-danger {
  color: var(--danger);
}
.myweek-label {
  font-size: var(--text-xs);
  color: var(--text-muted);
  font-weight: 600;
}
.myweek-progress {
  position: relative;
  height: 0.5rem;
  border-radius: 999em;
  background: var(--surface-sunken);
  overflow: hidden;
}
.myweek-progress-fill {
  position: absolute;
  inset-inline-start: 0;
  inset-block: 0;
  background: var(--success);
  border-radius: 999em;
  transition: inline-size 0.2s;
}
@media (prefers-reduced-motion: reduce) {
  .myweek-progress-fill {
    transition: none;
  }
}

/* ---------- Watched Cases rail ---------- */
.watched-rail-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.watched-rail-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  width: 100%;
  text-align: start;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  cursor: pointer;
  font: inherit;
  color: inherit;
}
.watched-rail-item:hover {
  background: var(--surface-sunken);
}
.watched-rail-item:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
}
.watched-rail-num {
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--color-primary-dark);
}
.watched-rail-party {
  font-size: var(--text-xs);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}
.watched-rail-hearing {
  font-size: var(--text-xs);
  color: var(--warning);
}
.rail-empty {
  margin: 0;
}
.rail-more {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-primary);
  text-decoration: none;
}
.rail-more:hover {
  text-decoration: underline;
}

/* ---------- Command Palette (Ctrl+K) ---------- */
.cmdk-kbd {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  background: var(--surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: 4px;
  padding: 1px 5px;
  color: var(--text-muted);
}
.cmdk-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 22, 38, 0.5);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: min(14vh, 7rem);
  padding-inline: var(--gutter);
  z-index: 300;
  opacity: 0;
  transition: opacity 0.12s ease;
}
.cmdk-overlay.open {
  opacity: 1;
}
.cmdk-box {
  width: 100%;
  max-width: 34rem;
  max-height: min(60vh, 28rem);
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.cmdk-input-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}
.cmdk-search-icon {
  color: var(--text-muted);
}
.cmdk-input {
  flex: 1;
  border: none;
  outline: none;
  font-size: var(--text-md);
  background: transparent;
  color: var(--text);
  min-height: auto;
  padding: 0;
}
.cmdk-list {
  overflow-y: auto;
  padding: var(--space-2);
}
.cmdk-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  text-align: start;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.cmdk-item.active,
.cmdk-item:hover {
  background: var(--primary-bg);
}
.cmdk-item-icon {
  flex-shrink: 0;
  width: 1.75rem;
  text-align: center;
  color: var(--color-primary);
}
.cmdk-item-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}
.cmdk-item-label {
  font-size: var(--text-sm);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cmdk-item-hint {
  display: none;
}
.cmdk-item-group {
  flex-shrink: 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.cmdk-empty {
  text-align: center;
  color: var(--text-muted);
  padding: var(--space-6) 0;
  margin: 0;
}
@media (max-width: 640px) {
  .cmdk-overlay {
    padding-top: var(--safe-area-top);
    align-items: stretch;
  }
  .cmdk-box {
    max-width: 100%;
    max-height: 100%;
    height: 100%;
    border-radius: 0;
  }
}

/* ---------- Upload access grants (client document uploads) ---------- */
.upload-grant-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--color-success-bg);
  border: 1px solid #bfe3cc;
  border-radius: var(--radius-md);
  padding: 14px 16px;
  margin-bottom: 20px;
  font-size: 13px;
}
.upload-grant-banner i {
  color: var(--color-success);
  font-size: 18px;
}
.upload-grant-banner .ug-expiry {
  color: var(--color-text-muted);
  font-size: 12px;
}
.upload-blocked-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--color-bg);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  margin-bottom: 20px;
  font-size: 13px;
  color: var(--color-text-muted);
}
.upload-blocked-banner i {
  font-size: 18px;
}
.grant-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 12.5px;
}
.grant-row:last-child {
  border-bottom: none;
}

.panel {
  background: var(--color-surface);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-sm);
}
/* One header treatment for every panel in the app.
   `--surface-sunken` is the design system's neutral recessed tone, so the
   strip reads as a header against the white panel body without claiming a
   semantic colour: the previous `--color-info-bg` is the *info* tint, which
   made every panel — hearings, documents, users — look like an information
   notice. The accent hairline ties it to the brand without adding a second
   border weight, and the rounded top corners stop the fill bleeding past the
   panel's own radius. */
.panel-header {
  padding: 11px 16px;
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--color-border);
  border-top: 2px solid var(--color-accent);
  /* Matches the panel's own radius minus its 1px border, so the fill follows
     the card's corners instead of squaring them off. Logical-property safe:
     the top corners are the same two in LTR and RTL. */
  border-radius: calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px) 0 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.panel-header h3 {
  font-size: var(--text-base);
  font-weight: 700;
  color: var(--color-primary);
  margin: 0;
}
.panel-body {
  padding: 16px 18px;
}
/* A <p> (empty-state text, error placeholders) left as a panel-body's last
   child still carries the browser's default UA margin-bottom on top of the
   padding above -- an inconsistent trailing gap that varies by what
   happens to be the last rendered element, not by design. app-wide, not
   scoped to the Dashboard, since every panel in the app can end in a <p>. */
.panel-body > p:last-child {
  margin-bottom: 0;
}

.hearing-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 12px;
  padding: 12px 0;
  margin: 0.5rem 0;
  border-bottom: 1px dashed var(--color-border);
}
.hearing-row:last-child {
  border-bottom: none;
}
.hearing-info {
  /* Case/party names are real legal content and can run long (section 11)
     — this claims priority space and wraps its own text rather than being
     crushed by the countdown pill's fixed width. If the row is too narrow
     for both, flex-wrap on .hearing-row above drops the pill to its own
     line instead of shrinking the case text to near-nothing. */
  flex: 1 1 220px;
  min-width: 0;
}
.hearing-meta {
  font-size: 12.5px;
  color: var(--color-text-muted);
}
/* Three units (days / hours / minutes), sized down a step from the original
   four-unit pill so the row reads as supporting detail rather than competing
   with the case name beside it. */
.countdown-pill {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}
.countdown-unit {
  background: var(--success);
  color: #fff;
  border-radius: var(--radius-sm);
  padding: 4px 6px;
  text-align: center;
  min-width: 38px;
}
.countdown-unit b {
  display: block;
  font-size: 12.5px;
  line-height: 1.25;
}
.countdown-unit span {
  font-size: 8.5px;
  opacity: 0.8;
}
.countdown-unit.urgent {
  background: var(--color-danger);
}

/* ---------- Tabs ---------- */
.tabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--color-border);
  margin-bottom: 20px;
  overflow-x: auto;
}
.tab-btn {
  /* flex-shrink:0 is load-bearing, not decorative: the global `* {
     min-width: 0 }` reset (added for flex text-truncation elsewhere) also
     removes the implicit `min-width: auto` flex items get by default, so
     without this a tab button is free to shrink its box below its own
     nowrap label's width — the label then overflows the shrunk box and
     visually collides with the next tab instead of the row simply
     scrolling (`.tabs` already has overflow-x: auto for exactly that). */
  flex-shrink: 0;
  padding: 12px 18px;
  background: none;
  border: none;
  cursor: pointer;
  font-weight: 600;
  color: var(--color-text-muted);
  border-bottom: 3px solid transparent;
  white-space: nowrap;
  font-size: 13.5px;
}
.tab-btn.active {
  color: var(--color-primary);
  border-bottom-color: var(--color-accent);
}

/* One form-grid for every search/filter form in the app (search.js, cases.js,
   reminders.js, users.js). min(100%, ...) lets a column shrink below its
   12.5rem floor on a narrow screen instead of overflowing, so no separate
   mobile override is needed. `.search-actions` is the grid's LAST CHILD, not
   a sibling after it: spanning 1 / -1 puts it on its own full row, aligned to
   the inline end, sharing the grid's own row gap instead of a second margin. */
.search-form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12.5rem), 1fr));
  gap: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
}
.search-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.search-form-grid > .search-actions {
  grid-column: 1 / -1;
  justify-content: flex-end;
}

/* ---------- Cases page: contained tabs (Merge A) ----------
   A segmented control, deliberately unlike `.tabs`/`.tab-btn` below it: the
   Official Search Engine tab has its own five sub-tabs in that style, and
   two stacked rows of identical-looking tabs would read as one confusing
   nav. Logical properties throughout; RTL mirrors with no separate rule. */
.hub-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.hub-tabs {
  display: inline-flex;
  gap: var(--space-1);
  padding: var(--space-1);
  max-width: 100%;
  overflow-x: auto;
  background: var(--surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.hub-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-height);
  padding: var(--space-2) var(--space-4);
  border: none;
  border-radius: calc(var(--radius-md) - 2px);
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--text-control);
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color 0.15s,
    color 0.15s;
}
.hub-tab:hover {
  color: var(--text);
}
/* Filled primary, not white-on-sunken: white against --surface-sunken is
   ~1.1:1, far under WCAG 1.4.11's 3:1 for a state indicator, and live
   testing showed a focus ring on the *unselected* tab reading as the
   selection. White text on --color-primary is well above 4.5:1. */
.hub-tab[aria-selected='true'],
.hub-tab[aria-selected='true']:hover {
  background: var(--color-primary);
  color: var(--text-on-dark);
  box-shadow: var(--shadow-sm);
}
/* Tab-click refresh in progress (js/hub.js swaps the tab's icon for a
   spinner). Deliberately no dimming: fading the selected tab would drop its
   white-on-primary contrast mid-load. fa-spin already stops under
   prefers-reduced-motion in Font Awesome's own stylesheet. */
.hub-tab.is-loading {
  cursor: progress;
}
.hub-tab-icon {
  display: inline-flex;
  inline-size: 1.1em;
  justify-content: center;
}
/* An outline offset clear of the tab, rather than a box-shadow, so it can't
   be confused with (or hidden by) the selected tab's own fill and shadow.
   primary-light, not the accent gold: gold on --surface-sunken is ~2.3:1. */
.hub-tab:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .hub-tab {
    transition: none;
  }
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}
thead th {
  text-align: start;
  padding: 10px 12px;
  background: var(--color-bg);
  color: var(--color-text-muted);
  font-weight: 700;
  font-size: 12px;
  border-bottom: 1px solid var(--color-border);
}
tbody td {
  padding: 12px;
  border-bottom: 1px solid var(--color-border);
}
tbody tr:hover {
  background: var(--surface-hover);
}
.table-wrap {
  overflow-x: auto;
}

.badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 20px;
  font-size: 11.5px;
  font-weight: 700;
}
.badge-success {
  background: var(--color-success-bg);
  color: var(--color-success);
}
.badge-warning {
  background: var(--color-warning-bg);
  color: var(--color-warning);
}
.badge-danger {
  background: var(--color-danger-bg);
  color: var(--color-danger);
}
.badge-info {
  background: var(--color-info-bg);
  color: var(--color-info);
}
.badge-muted {
  background: var(--color-bg);
  color: var(--color-text-muted);
}

.empty-state {
  text-align: center;
  padding: 40px 20px;
  color: var(--color-text-muted);
}
.empty-state .empty-icon {
  font-size: 34px;
  margin-bottom: 10px;
}

.filters-row {
  display: flex;
  gap: 12px;
  margin-bottom: 18px;
  flex-wrap: wrap;
}
.filters-row .form-group {
  margin-bottom: 0;
  min-width: 180px;
}

/* ---------- Modal ---------- */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 20, 40, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  padding: var(--space-5);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
}
.modal-overlay.open {
  opacity: 1;
  pointer-events: auto;
}
.modal-box {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  width: 100%;
  max-width: 45rem; /* 720px */
  max-height: 88vh;
  overflow-y: auto;
  box-shadow: var(--shadow-lg);
  transform: translateY(10px);
  transition: transform 0.15s;
}
.modal-overlay.open .modal-box {
  transform: translateY(0);
}
.modal-header {
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: sticky;
  top: 0;
  background: var(--color-primary-dark);
  color: var(--color-accent-light);
  /* Load-bearing: .modal-box gets its own stacking context from its
     `transform` (used for the open/close slide animation), and within
     that context a `position: sticky` element with no z-index paints
     according to DOM tree order, not "is currently stuck" — so any
     absolutely-positioned content further down modal-body (e.g.
     .timeline-item's dot markers) painted AFTER it in the tree, which
     visually put the timeline on top of the sticky header once scrolled
     under it. An explicit z-index removes the ambiguity. */
  z-index: 2;
}
.modal-header h3 {
  font-size: 1rem;
}
.modal-close {
  /* Sits on .modal-header's dark navy background, so it needs its own
     light-on-dark color rather than --color-text-muted (tuned for light
     surfaces elsewhere in the app, which read as nearly invisible here).
     Sized to a full 44x44px (2.75rem) hit area per WCAG 2.5.5, independent
     of --control-height (2.5rem/40px) which targets normal-density controls,
     not a lone icon-only close affordance in a header corner. */
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  min-height: 2.75rem;
  margin-inline-end: -0.5rem;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-on-dark-muted);
  font-size: 1.375rem;
  line-height: 1;
  cursor: pointer;
  transition:
    background 0.15s,
    color 0.15s;
}
.modal-close:hover {
  background: rgba(255, 255, 255, 0.12);
  color: var(--text-on-dark);
}
.modal-close:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.4);
}
.modal-close:active {
  background: rgba(255, 255, 255, 0.2);
}
.modal-body {
  padding: var(--space-5);
}

/* ---------- Timeline ---------- */
.timeline-list {
  position: relative;
  padding-inline-start: 26px;
}
.timeline-list::before {
  content: '';
  position: absolute;
  inset-inline-start: 8px;
  top: 4px;
  bottom: 4px;
  width: 2px;
  background: var(--color-border);
}
.timeline-item {
  position: relative;
  padding-bottom: 22px;
}
.timeline-item::before {
  content: '';
  position: absolute;
  inset-inline-start: -26px;
  top: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--color-border);
  border: 3px solid #fff;
  box-shadow: 0 0 0 2px var(--color-border);
}
.timeline-item.done::before {
  background: var(--color-success);
  box-shadow: 0 0 0 2px var(--color-success);
}
.timeline-item .ti-title {
  font-weight: 700;
  font-size: 13.5px;
}
.timeline-item .ti-date {
  font-size: 12px;
  color: var(--color-text-muted);
}

/* ---------- Upload ---------- */
.dropzone {
  border: 2px dashed var(--color-border);
  border-radius: var(--radius-md);
  padding: 40px 20px;
  text-align: center;
  cursor: pointer;
  transition:
    border-color 0.15s,
    background 0.15s;
  margin-bottom: 22px;
  background: var(--surface-sunken);
}
.dropzone.dragover {
  border-color: var(--color-primary-light);
  background: var(--color-info-bg);
}
.dropzone .dz-icon {
  font-size: 32px;
  margin-bottom: 8px;
}
.upload-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--color-border);
}
.upload-item:last-child {
  border-bottom: none;
}
.progress-bar {
  flex: 1;
  height: 8px;
  background: var(--color-border);
  border-radius: 10px;
  overflow: hidden;
}
.progress-bar-fill {
  height: 100%;
  background: var(--color-primary);
  width: 0%;
  transition: width 0.2s;
}

/* ---------- Document Center: case accordion ---------- */
.doc-case-group {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  margin-bottom: 10px;
  overflow: hidden;
}
.doc-case-group:last-child {
  margin-bottom: 0;
}
.doc-case-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 12px 14px;
  background: var(--surface-sunken);
}
.doc-case-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1 1 200px;
  min-width: 0;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: start;
  cursor: pointer;
}
.doc-case-toggle:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.doc-case-chevron {
  flex-shrink: 0;
  color: var(--text-muted);
  transition: transform 0.15s;
}
.doc-case-group.open .doc-case-chevron {
  transform: rotate(90deg);
}
/* The icon itself (fa-chevron-right) is a physical glyph — it always points
   right, [dir] or not, unless mirrored explicitly. At rest it should point
   toward the reading direction's "expand" side: right under LTR (no change
   needed), left under RTL (rotate 180deg, which turns ">" into "<"). The
   `.open` rule above has higher specificity (3 classes vs. 1 attribute + 1
   class) so it still wins in both directions, meaning "open" always reads
   as pointing down — no separate RTL override of the open state is needed. */
[dir='rtl'] .doc-case-chevron {
  transform: rotate(180deg);
}
/* Header: the case number only -- the compact identifier to scan a closed
   list by. The parties (.doc-case-title) live at the top of the body. */
.doc-case-number {
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 700;
  font-size: var(--text-base);
}
/* Body: the case's parties/title as the context line above its documents,
   divided from the list so it doesn't read as one of the documents. */
.doc-case-title {
  margin: var(--space-2) 0 var(--space-2);
  padding-block-end: var(--space-2);
  border-block-end: 1px dashed var(--border);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.doc-case-count {
  flex-shrink: 0;
}
.doc-case-body {
  padding: 4px 14px 14px;
}
.doc-item .doc-item-name {
  font-weight: 600;
  font-size: 13.5px;
  overflow-wrap: break-word;
}
.doc-item-actions {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}

/* The notification bell had a preview dropdown here (.notif-dropdown /
   .notif-item). It was removed along with its markup and handlers: the bell
   now navigates straight to the Notifications page, so there is exactly one
   place notifications are read and no second, truncated copy of the list to
   keep in sync. `.notif-smart` and `.smart-tag` further down are NOT part of
   that dropdown — the Notifications page still uses them. */

/* ---------- Toast ---------- */
.toast-container {
  position: fixed;
  bottom: 20px;
  inset-inline-end: 20px;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.toast {
  background: var(--color-primary-dark);
  color: #fff;
  padding: 12px 18px;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  font-size: 13.5px;
  display: flex;
  align-items: center;
  gap: 10px;
  animation: toast-in 0.2s ease;
}
.toast.success {
  background: var(--color-success);
}
.toast.error {
  background: var(--color-danger);
}
@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.text-muted {
  color: var(--color-text-muted);
}
.mt-0 {
  margin-top: 0;
}
.flex-between {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.flex-between > h1,
.flex-between > h2,
.flex-between > h3 {
  /* Page-header + action-button pattern (Cases, Documents, Notifications,
     Users). Wrapping the heading's own text is fine and expected; this
     just stops a very long heading from refusing to shrink at all and
     pushing the action button off the row before wrapping kicks in. */
  min-width: 0;
  flex: 1 1 auto;
}
.gap-8 {
  gap: 8px;
}

/* ---------- Responsive label swap ----------
   Shared utility pair: a component renders BOTH a full descriptive label
   and a concise one (e.g. topbar title, search tabs); which one is visible
   is a pure CSS breakpoint flip, not a JS viewport check, so the same
   markup and event wiring serves both modes (see .topbar-title and
   .tab-btn usage). Mobile override lives in the consolidated <=768px
   section below, next to every other mobile rule. */
.label-short {
  display: none;
}

/* ---------- Bulk selection toolbar (Users & Permissions) ---------- */
.bulk-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  background: var(--color-info-bg);
  border: 1px solid #bcd7ef;
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  margin-bottom: 14px;
  font-size: 13px;
  display: none;
}
.bulk-toolbar.show {
  display: flex;
}
.bulk-toolbar .btn {
  /* Let each action button wrap to a new row as a whole unit instead of
     compressing below its own text's natural width, which otherwise wraps
     multi-word Arabic labels internally into uneven, cramped multi-line
     buttons (see engineering report). */
  flex: 0 1 auto;
  white-space: nowrap;
}
.bulk-toolbar .count {
  font-weight: 700;
  color: var(--color-primary);
}
.row-checkbox,
.select-all-checkbox {
  width: 16px;
  height: 16px;
  cursor: pointer;
}

/* ---------- Reminders ---------- */
.reminder-item {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin-bottom: 10px;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}
.reminder-item.overdue {
  border-color: var(--color-danger);
  background: var(--color-danger-bg);
}
.reminder-item .r-note {
  font-weight: 600;
  font-size: 13.5px;
}
.reminder-item .r-meta {
  font-size: 11.5px;
  color: var(--color-text-muted);
  margin-top: 3px;
}
.reminder-actions {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}

.watch-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Now a <button> (was a <span>: not keyboard-reachable). */
  padding: 0;
  background: none;
  border: 0;
  font-family: inherit;
  color: inherit;
  cursor: pointer;
  font-size: 12.5px;
  font-weight: 600;
}
.watch-toggle:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
/* Mouse targets made keyboard-operable via ui.js::makeKeyboardActivatable
   (notification rows, upload dropzones). */
[role="button"]:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
}
.watch-toggle i {
  color: var(--color-text-muted);
}
.watch-toggle.active i {
  color: var(--color-accent);
}

/* ---------- Icon-only buttons used across the app ---------- */
.btn i {
  font-size: 0.95em;
}
.stat-icon i,
.icon-btn i,
.nav-icon i,
.empty-icon i,
.dz-icon i {
  line-height: 1;
}

/* Desktop default; the mobile section below is the only place this
   flips to `flex`, keeping both states of this component next to each
   other's real trigger instead of split across the file. */
.bottom-nav {
  display: none;
}

/* =======================================================================
   Mobile-first overhaul (<=768px) — the sidebar is replaced entirely by a
   native-app-style bottom tab bar + a slide-up "More" sheet for the rest.
   ======================================================================= */
@media (max-width: 768px) {
  .sidebar {
    display: none;
  }

  .main-area {
    width: 100%;
  }

  .topbar {
    padding: 0 var(--gutter);
    padding-top: var(--safe-area-top);
    height: calc(var(--mobile-topbar-height) + var(--safe-area-top));
  }
  .topbar-title {
    font-size: 15px;
  }
  .topbar-actions {
    gap: 8px;
  }
  /* Swap every full/short label pair to its concise form: the topbar title
     (section 7) and search tabs (section 8) both render a full descriptive
     span and a short one in the same markup — only visibility toggles. */
  .label-full {
    display: none;
  }
  .label-short {
    display: inline;
  }
  /* Icon buttons deliberately do NOT shrink below --control-height here —
     they're primary touch targets on the exact device class most likely
     to be touch-only. Compactness must never cost tap-ability (section 7). */

  .page-content {
    padding-bottom: calc(var(--bottom-nav-height) + var(--safe-area-bottom) + 20px);
  }

  /* Was a hardcoded repeat(2, 1fr) — fine for today's short Arabic labels,
     but a future card with a longer label (English "Pending Status
     Requests" is already close) would wrap awkwardly at a forced 2-up
     width. auto-fit + minmax lets it drop to 1 column on its own if a
     card ever needs more room, instead of only fitting the specific
     labels that exist today. */
  .stat-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 8.5rem), 1fr));
    gap: 10px;
  }
  /* .stat-card padding and .stat-value font-size no longer need an override
     here (Sept 2026 overhaul, Task 4) -- --stat-card-padding and
     --stat-value-size are now clamp()s that already shrink smoothly down to
     this width, one fluid value instead of a second hardcoded tier. */

  .filters-row {
    flex-direction: column;
  }
  .filters-row .form-group {
    width: 100%;
  }

  .dropzone {
    /* Drag-and-drop is desktop-mouse behavior; touch users only ever use
       the tap-to-browse affordance, so the generous drag target padding
       is pure unused whitespace here (section 15) rather than functional. */
    padding: 26px 16px;
  }

  .tabs {
    gap: 0;
  }
  .tab-btn {
    padding: 10px 12px;
    font-size: 12.5px;
  }

  /* ---------- Tables -> stacked records (section 12) ----------
     Applies only to row-based/record tables (search results, cases,
     documents, users) via the .data-table modifier — NOT the Users
     permissions matrix, which stays a plain horizontally-scrollable
     table. A role x module permission grid is a genuinely two-dimensional
     comparison (compare a role across all modules at a glance); collapsing
     it into stacked key/value cards would destroy exactly the comparison
     it exists to show, so contained horizontal scroll is the intentional,
     documented exception here (section 13's "last resort" carve-out). */
  .data-table thead {
    /* Visually hidden but still in the accessibility tree, so a screen
       reader navigating by table semantics still gets column headers —
       plain `display:none` would remove them entirely. Sighted mobile
       users get the header's meaning instead from each cell's own
       data-label (below), which is the point of the record layout. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  .data-table,
  .data-table tbody,
  .data-table tr,
  .data-table td {
    display: block;
    width: 100%;
  }
  .data-table tr {
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: 10px 12px;
    margin-bottom: 10px;
  }
  .data-table tr:last-child {
    margin-bottom: 0;
  }
  .data-table td {
    padding: 6px 0;
    border-bottom: 1px dashed var(--border);
  }
  .data-table td:last-child {
    border-bottom: none;
  }
  .data-table td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 10.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--text-muted);
    margin-bottom: 2px;
  }

  .modal-overlay {
    align-items: flex-end;
    padding: 0;
  }
  .modal-box {
    max-width: 100%;
    width: 100%;
    max-height: 92vh;
    border-radius: 1.125rem 1.125rem 0 0; /* 18px */
  }

  /* .btn/.form-group input etc. already carry --control-height (44px)
     from their base rules — deliberately NOT reduced here. Shrinking
     touch targets specifically on mobile would fix nothing and cost
     tap-accuracy on exactly the device class that needs it most. */

  .bottom-nav {
    display: flex;
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    height: calc(var(--bottom-nav-height) + var(--safe-area-bottom));
    padding-bottom: var(--safe-area-bottom);
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
    box-shadow: 0 -4px 16px rgba(18, 49, 90, 0.08);
    z-index: 80;
  }
  .bn-item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.2rem;
    background: var(--color-primary-dark);
    border: none;
    color: var(--color-surface);
    font-family: inherit;
    font-size: 0.6rem;
    font-weight: 600;
    cursor: pointer;
    padding: 0.5rem 0.25rem;
    position: relative;
  }
  .bn-item i {
    font-size: 1rem;
  }
  .bn-item.active {
    color: var(--color-accent-light);
  }
  .bn-item.active::after {
    /* Text/icon color alone is a subtle active-state signal on a small
       touch target glanced at quickly; a small accent bar gives the
       selected tab a clearer, more intentional visual anchor (section 14)
       without adding a new color — reuses the existing accent token. */
    content: '';
    position: absolute;
    bottom: 0.1rem;
    inset-inline: 10%;
    height: 0.3rem;
    border-radius: 0.3rem;
    background: var(--color-accent-light);
  }
  .bn-item .bn-badge {
    position: absolute;
    top: 0.1rem;
    inset-inline-end: 22%;
    background: var(--color-danger);
    color: #fff;
    font-size: 9px;
    font-weight: 700;
    border-radius: 20px;
    padding: 0 4px;
    min-width: 14px;
    line-height: 14px;
    text-align: center;
  }
}

/* =======================================================================
   Login card responsiveness — deliberately its OWN breakpoints, separate
   from the app-shell's 768px sidebar/bottom-nav switch above. The login
   screen has no sidebar and isn't part of the authenticated shell, so
   there is no reason its card has to fold at the exact same width as
   that unrelated component; tying it to 768px was hiding .login-brand
   on genuinely roomy tablet/small-laptop widths (up to 1024px) that have
   plenty of space for two columns.

   Three tiers, using non-overlapping min/max ranges (not cascade order)
   so which rule applies is a fact about the viewport, not about where
   these blocks happen to sit in the file:
     >1024px            desktop  — untouched, base .login-card/.login-brand
                                    rules already give the full two-column
                                    layout with .login-brand visible.
     641px – 1024px      tablet  — still two columns, .login-brand still
                                    visible, just narrower so the form
                                    column gets a bit more of the width.
     <=640px              phone  — only width genuinely too narrow for two
                                    columns side by side; collapses to one
                                    column with .login-brand restyled into a
                                    compact horizontal header strip above
                                    the form instead of a side column.
   ======================================================================= */
@media (min-width: 641px) and (max-width: 1024px) {
  .login-card {
    grid-template-columns: 0.85fr 1.15fr;
  }
  .login-brand {
    padding: clamp(1.25rem, 3vw, 3rem) clamp(1rem, 2.5vw, 2.25rem);
  }
}
@media (max-width: 640px) {
  .login-card {
    grid-template-columns: 1fr;
  }
  /* No room for a side panel at this width, but the branding shouldn't
     just vanish — .login-brand is the FIRST child of .login-card, so a
     single-column grid already places it above .login-form-wrap with no
     markup change; it's restyled here from a tall side column into a
     compact horizontal header strip (logo beside the wordmark) instead of
     being hidden. The copyright line (the login-brand's second child div)
     is dropped at this width — it's not essential on a phone header and
     would otherwise force the strip taller than the logo+name really need. */
  .login-brand {
    flex-direction: row;
    justify-content: center;
    align-items: center;
    padding: var(--space-4) var(--space-5);
    border-inline-end: none;
    border-bottom: 1px solid rgba(255, 255, 255, 0.15);
  }
  .login-brand > div:last-child {
    display: none;
  }
  .login-brand > div:first-child {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    column-gap: var(--space-3);
  }
  .login-brand .brand-mark {
    grid-row: 1 / 3;
    width: 2.75rem; /* 44px — down from the desktop side-panel's 56px */
    height: 2.75rem;
    font-size: 1rem;
    /* Same fill the desktop side-panel badge uses — restated explicitly
       here (rather than left to inherit) so the mobile badge is
       guaranteed to match it rather than depending on cascade order. */
    background: var(--color-accent);
  }
  .login-brand h1 {
    grid-column: 2;
    margin-top: 0;
    margin-bottom: 0.25rem;
    font-size: 1.0625rem; /* --text-lg */
  }
  .login-brand p {
    grid-column: 2;
    margin: 0;
    font-size: 0.8125rem; /* --text-sm */
  }
}

/* ---------- "More" bottom sheet (mobile) ---------- */
.bottom-sheet-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 20, 40, 0.45);
  z-index: 90;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
}
.bottom-sheet-overlay.open {
  opacity: 1;
  pointer-events: auto;
}
.bottom-sheet {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  background: var(--color-surface);
  border-radius: 20px 20px 0 0;
  padding: 10px 16px calc(20px + var(--safe-area-bottom));
  transform: translateY(100%);
  transition: transform 0.25s ease;
  max-height: 78vh;
  overflow-y: auto;
}
.bottom-sheet-overlay.open .bottom-sheet {
  transform: translateY(0);
}
.bottom-sheet-handle {
  width: 40px;
  height: 4px;
  border-radius: 4px;
  background: var(--color-border);
  margin: 6px auto 14px;
}
/* Route entries are <a href>, language/Logout are <button> (all were <div>s,
   unreachable by keyboard): reset both to the same row look. */
.bottom-sheet-item {
  display: flex;
  align-items: center;
  gap: 14px;
  inline-size: 100%;
  padding: 13px 6px;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--color-border);
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  text-align: start;
  text-decoration: none;
  color: var(--color-text);
  cursor: pointer;
}
.bottom-sheet-item:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: -2px;
}
.bottom-sheet-item:last-child {
  border-bottom: none;
}
.bottom-sheet-item i {
  width: 20px;
  text-align: center;
  color: var(--color-text-muted);
}
.bottom-sheet-item.danger {
  color: var(--color-danger);
}
.bottom-sheet-item.danger i {
  color: var(--color-danger);
}

/* ---------- Interactive dashboard stat cards ---------- */
/* Every stat card is a real control that opens the list its number counted.
   Previously they were inert numbers with no affordance, so users had no way
   to know a figure was actionable — hence the hover lift, the pointer, the
   focus ring and the explicit "View ..." line. */
.stat-card-clickable {
  cursor: pointer;
  transition:
    transform 0.12s ease,
    box-shadow 0.12s ease,
    border-color 0.12s ease;
}
.stat-card-clickable:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md, 0 6px 18px rgba(0, 0, 0, 0.1));
  border-color: var(--color-accent);
}
.stat-card-clickable:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}
.stat-card .stat-action {
  margin-top: 10px;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--color-accent);
  display: flex;
  align-items: center;
  gap: 6px;
  opacity: 0.85;
}
.stat-card-clickable:hover .stat-action {
  opacity: 1;
}
/* The arrow points "forward" in reading order, so it must flip under RTL. */
[dir='rtl'] .stat-action-arrow {
  transform: scaleX(-1);
}

/* ---------- Row action clusters (hearings, search results) ---------- */
.hearing-actions,
.result-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  flex-shrink: 0;
}

/* ---------- Active-filter chip (arriving from a stat card) ---------- */
.filter-chip-row {
  margin-bottom: 14px;
}
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px 6px 12px;
  border-radius: 999px;
  background: var(--color-info-bg);
  color: var(--color-info);
  font-size: 12.5px;
  font-weight: 700;
}
.filter-chip-clear {
  border: none;
  background: transparent;
  color: inherit;
  cursor: pointer;
  font-size: 12px;
  padding: 2px 4px;
  border-radius: 50%;
  line-height: 1;
}
.filter-chip-clear:hover {
  background: rgba(0, 0, 0, 0.08);
}

/* ---------- Smart (system-generated) notifications ---------- */
/* Deliberately unlike a normal notification: an automatic nudge must never be
   mistaken for a colleague personally chasing you, and the lawyer's FYI copy
   must not read as though they sent it themselves. */
/* The tint fades from the row's inline-start edge (where the accent border
   is). A gradient angle is physical -- 90deg is left-to-right whatever the
   text direction -- and browsers don't implement logical gradient keywords
   (`to inline-end`), so the angle is a custom property flipped under RTL;
   the color stops stay defined once. */
.notif-smart {
  --notif-smart-angle: 90deg;
  border-inline-start: 3px solid var(--color-accent);
  padding-inline-start: var(--space-4);
  background: linear-gradient(var(--notif-smart-angle), #fbf3e8 0%, transparent 60%);
}
[dir="rtl"] .notif-smart {
  --notif-smart-angle: 270deg;
}
.smart-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-accent);
  margin-bottom: 4px;
}

/* ---------- Timeline inline action ---------- */
.btn-xs {
  padding: 3px 8px;
  font-size: 11px;
  line-height: 1.5;
  height: auto;
}
.ti-action {
  margin-inline-start: 10px;
  vertical-align: middle;
}

/* ---------- Useful Kuwait government websites ----------
   Sub-phase 3.4 correction: the Sub-phase 3.3 cover-image card (`.ul-card`/
   `.ul-cover`/etc.) is superseded -- it read as visually inconsistent next
   to the Quick Actions row directly above it on the Dashboard. The four
   links now render as `.qa-action-btn` buttons (see the Quick Actions row
   rules above), reusing that exact style rather than a parallel one, so the
   two can never drift apart again. Nothing below this comment is dead: the
   old rule bodies were deleted outright, not merely unreferenced. */

/* ---------- Mandatory field indicators ---------- */
/* A single convention used by every form in the app: put `required` on the
   label and the asterisk is generated. Using ::after rather than typing "*"
   into each label keeps it out of the translated strings, so Arabic and
   English stay identical and no translator can accidentally drop it. */
.form-group > label.required::after {
  content: ' *';
  color: var(--color-danger);
  font-weight: 700;
}
/* Screen readers get the real signal from aria-required on the input; the
   asterisk is decorative, so it is not announced twice. */
.required-note {
  font-size: 11.5px;
  color: var(--color-text-muted);
  margin: 2px 0 12px;
}
.required-note .req-star {
  color: var(--color-danger);
  font-weight: 700;
}

/* ==========================================================================
   PRINTING
   --------------------------------------------------------------------------
   The application never prints the live page. js/print.js composes a
   self-contained sheet into #print-root; that element is the ONLY thing
   visible on paper, and is never visible on screen. Two consequences:

     - Navigation, the top bar, the bottom nav, buttons, forms, toasts and any
       open modal are excluded from the printout structurally, not by hiding
       them one class at a time — nothing interactive is ever composed into
       the sheet in the first place.
     - Nothing is clipped. On screen, wide tables live inside `.table-wrap`
       (overflow-x: auto) and document lists inside collapsible bodies; a
       browser prints only the visible slice of an overflow box, which is why
       "just call window.print()" loses columns and rows. The composed sheet
       has no scrolling containers at all.
   ========================================================================== */

/* ---------- Idle session warning (js/idle.js) ----------
   z-index sits one above .modal-overlay (100): this can appear while the
   user has a case or document modal open, and a "you are about to be signed
   out" notice that renders *behind* the dialog it is warning about would be
   worse than not showing it at all.

   Deliberately NOT reusing .modal-overlay: that class is driven by an .open
   class for its fade-in and is closed by Escape and backdrop clicks, neither
   of which should dismiss this. Sized entirely in tokens/rem so it tracks
   the user's font-size setting, and laid out with logical properties so it
   mirrors correctly in Arabic. */
.idle-warning-overlay {
  position: fixed;
  inset: 0;
  z-index: 101;
  background: rgba(10, 20, 40, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
}
.idle-warning-box {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-6);
  max-width: 26rem;
  width: 100%;
  text-align: center;
}
.idle-warning-icon {
  font-size: var(--text-2xl);
  color: var(--color-warning);
  margin-bottom: var(--space-2);
}
.idle-warning-box h3 {
  margin: 0 0 var(--space-2);
}
.idle-warning-countdown {
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--color-primary-dark);
  /* Tabular figures so the seconds ticking down do not make the whole
     dialog jitter as glyph widths change. */
  font-variant-numeric: tabular-nums;
  margin: var(--space-3) 0 var(--space-4);
}
.idle-warning-actions {
  display: flex;
  gap: var(--space-2);
  justify-content: center;
  flex-wrap: wrap;
}

#print-root {
  display: none;
}

@media print {
  /* Everything the app draws on screen, gone. `#app-root` covers the shell
     (sidebar, top bar, page content, bottom nav and its sheet); the other two
     mount directly on <body> and so need naming separately. */
  #app-root,
  .toast-container,
  .modal-overlay,
  .idle-warning-overlay {
    display: none !important;
  }

  html,
  body {
    background: #fff !important;
    margin: 0;
    padding: 0;
  }

  #print-root {
    display: block;
    color: #000;
    background: #fff;
    font-size: 11pt;
    line-height: 1.45;
    /* Latin and Arabic both render well here, and the sheet must not depend
       on the icon/webfont files being reachable from the print renderer. */
    font-family: 'Segoe UI', Tahoma, Arial, sans-serif;
  }

  @page {
    margin: 14mm 12mm;
  }

  /* Repeat the table head on every page of a long list. */
  #print-root thead {
    display: table-header-group;
  }
  #print-root tr,
  #print-root li,
  #print-root .print-section {
    page-break-inside: avoid;
  }
  #print-root h1,
  #print-root h2 {
    page-break-after: avoid;
  }
}

/* Sheet layout. Defined outside the print query so the rules are readable and
   maintainable as ordinary CSS; they only ever apply because #print-root is
   display:none on screen. */
.print-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
  border-bottom: 2px solid var(--color-primary);
  padding-bottom: 8px;
  margin-bottom: 14px;
}
.print-head .pb-name {
  font-size: 15pt;
  font-weight: 700;
  color: var(--color-primary);
}
.print-head .pb-sub {
  font-size: 9pt;
  color: #444;
}
.print-issued {
  font-size: 8.5pt;
  color: #333;
  text-align: end;
  line-height: 1.6;
}
.print-issued span {
  color: #666;
}

.print-title {
  font-size: 14pt;
  margin: 0 0 2px;
  color: var(--color-primary);
}
.print-subtitle {
  margin: 0 0 12px;
  font-size: 10pt;
  color: #444;
}

.print-meta,
.print-pairs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px 22px;
  margin: 0 0 14px;
  padding: 8px 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: #f7f8fa;
}
.print-pairs {
  background: none;
  border: none;
  padding: 0;
}
.print-meta > div,
.print-pairs > div {
  display: flex;
  gap: 6px;
  font-size: 9.5pt;
  break-inside: avoid;
}
.print-meta dt,
.print-pairs dt {
  color: #555;
  font-weight: 600;
  white-space: nowrap;
}
.print-meta dd,
.print-pairs dd {
  margin: 0;
  font-weight: 500;
  overflow-wrap: anywhere;
}

.print-section {
  margin-bottom: 16px;
}
.print-section h2 {
  font-size: 11pt;
  margin: 0 0 6px;
  padding-bottom: 3px;
  border-bottom: 1px solid #ccc;
  color: var(--color-primary);
}
.print-count {
  font-weight: 400;
  color: #666;
  font-size: 9pt;
}

.print-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 9.5pt;
}
.print-table th,
.print-table td {
  border: 1px solid #bbb;
  padding: 5px 7px;
  text-align: start;
  vertical-align: top;
  overflow-wrap: anywhere;
}
.print-table th {
  background: #eef1f6;
  font-weight: 700;
  color: var(--color-primary);
}

.print-list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.print-list li {
  border-bottom: 1px solid #ddd;
  padding: 5px 0;
}
.print-list .pl-title {
  font-size: 10pt;
  font-weight: 600;
}
.print-list .pl-meta {
  font-size: 9pt;
  color: #444;
}

.print-text {
  margin: 0;
  font-size: 10pt;
  white-space: pre-wrap;
}
.print-empty {
  font-size: 9.5pt;
  color: #666;
  font-style: italic;
  margin: 0;
}

.print-foot {
  margin-top: 18px;
  padding-top: 6px;
  border-top: 1px solid #ccc;
  font-size: 8.5pt;
  color: #555;
}
