/*
 * Realty Reach CRM — Design System
 *
 * Layer order matters: this file is loaded AFTER tailwind.css, so component
 * rules here win over Tailwind utilities of equal specificity.
 *
 * Contents
 *   1. Theme tokens          dark is the default, [data-theme=light] overrides
 *   2. Base surface          page canvas, scrollbars, selection
 *   3. Components            surface / card / button / pill / input / table
 *   4. Motion                keyframes + reduced-motion escape hatch
 *   5. Dark accent remap     keeps bg-*-100 pills legible on a dark canvas
 *   6. App-specific rules    sidebar, timeline, modals, print, watermark
 */

/* ══ 1. Theme tokens ═══════════════════════════════════════════════════
 * Every value is a bare "R G B" triplet so Tailwind's <alpha-value> works
 * (see tailwind.config.js). Dark is the default because it is the intended
 * look; light is an explicit opt-in, not a system preference. */

:root,
[data-theme="dark"] {
  color-scheme: dark;

  /* Canvas + surfaces */
  --c-bg:              8 12 24;      /* #080c18  page */
  --c-surface:        16 22 38;      /* #101626  cards, panels */
  --c-surface-2:      24 32 52;      /* #182034  nested panels, table head */
  --c-border:         39 50 76;      /* #27324c  hairlines */
  --c-border-strong:  56 70 104;     /* #38466c  emphasised hairlines */

  /* Ink */
  --c-text:          226 232 240;    /* #e2e8f0  body */
  --c-text-strong:   241 245 249;    /* #f1f5f9  headings */
  --c-text-soft:     203 213 225;    /* #cbd5e1  secondary */
  --c-muted:         148 163 184;    /* #94a3b8  meta */
  --c-faint:         100 116 139;    /* #64748b  placeholders */

  /* Accent — the "futuristic" part */
  --c-accent:         99 102 241;    /* indigo-500 */
  --c-accent-2:       34 211 238;    /* cyan-400   */
  --c-accent-soft:   129 140 248;    /* indigo-400 */

  /* Effects */
  --c-glow:          99 102 241;
  --c-shadow:         0 0 0;         /* black */
  --c-shadow-a:       0.45;
  --c-glass:         255 255 255;   /* glass fill source */
  --c-grid-line:     99 102 241;    /* ambient background grid */

  /* The sidebar rail deliberately has NO tokens of its own. It reuses
   * --c-glass-tint / --c-glass-a, so it follows the active theme exactly like
   * every other surface: white and airy in light, deep blue in dark.
   *
   * Two earlier passes each invented a rail-specific token and both were
   * wrong. Pinning it to a dark gradient broke the light theme's contrast, and
   * a separate "light blue" token set drifted out of sync with the palette.
   * The rail is not a special case; it is a surface like any other, and its
   * inks are theme-aware for the same reason (see the ink rules below). */

  /* Glass tokens — DARK THEME
   *
   * Dark glass is the case that most often gets faked. On a dark canvas a
   * flat white film does NOT read as glass — it reads as a washed-out grey
   * card, because the eye expects a reflection to travel ACROSS the panel and
   * a uniform white has no direction. So the tint is a deep blue rather than
   * white: the pane is mostly a dark translucent base, which lets the
   * background genuinely show through, plus a thin white specular edge. That
   * edge is what separates a dark pane from the background behind it; drop it
   * and the layout goes flat and unreadable.
   *
   * The base alpha is low (0.42) on purpose. This is genuine transparency —
   * the colour blobs behind it stay visible and move, which is the entire
   * point of the effect. Legibility is protected by the saturate() lift and
   * the edge highlight rather than by making the fill opaque. */
  --c-glass-tint:    30  38  64;
  --c-glass-a:        0.42;
  --c-glass-edge:     0.16;
  --c-glass-blur:    20px;
  --c-glass-sat:      160%;

  /* No --c-rail-a any more. The rail used to carry its own alpha, raised to
   * 0.82 to stop the ambient decor behind it showing through. That token is
   * gone because the rail is opaque now: --c-glass-tint at full strength, with
   * no alpha and no backdrop-filter. Every version of this surface that used an
   * alpha produced a visible seam somewhere down its full height, and a rail
   * that spans the screen cannot hide one. The full history is written up on
   * #sidebar below, which is where the next person will be looking. */

  --rr-radius:        1rem;
  --rr-radius-sm:     0.75rem;
  --rr-ease:          cubic-bezier(0.22, 1, 0.36, 1);

  /* Motion tokens.
   *
   * Durations are deliberately short. Enterprise UI is used for hours at a
   * time; anything past ~500ms of decorative motion becomes a tax the user
   * pays on every single page load. Anything under ~120ms reads as a glitch
   * rather than an animation, because there is no time for the eye to track
   * the interpolation. The range below is the usable middle.
   *
   * The easings are named by feel, not by math, so a value is chosen for the
   * job rather than by muscle memory:
   *   ease-out  — something entering. Fast start, long settle. The default.
   *   ease-in   — something leaving. Accelerates away, no slow tail.
   *   spring    — something responding to a direct user action (press, drag).
   *              Slight overshoot, which reads as physical weight.
   *   smooth    — two-dimensional movement where a curve would look wrong.
   *              Neither end is emphasised, because either end reads as a
   *              hitch when the element travels a long distance. */
  --rr-dur-instant:  90ms;
  --rr-dur-fast:     160ms;
  --rr-dur-base:     240ms;
  --rr-dur-slow:     380ms;
  --rr-dur-deliberate: 520ms;   /* hero / page-level only */

  --rr-ease-out:     cubic-bezier(0.22, 1, 0.36, 1);
  --rr-ease-in:      cubic-bezier(0.55, 0, 1, 0.45);
  --rr-ease-spring:  cubic-bezier(0.34, 1.56, 0.64, 1);
  --rr-ease-smooth:  cubic-bezier(0.4, 0, 0.2, 1);

  /* Stagger step. Kept under ~50ms because past that, later items in a long
   * list feel broken rather than choreographed — the user is waiting on the
   * last card, not watching a parade. Capped in the JS, not here. */
  --rr-stagger-step: 38ms;
}

[data-theme="light"] {
  color-scheme: light;

  --c-bg:            244 246 251;
  --c-surface:        255 255 255;
  --c-surface-2:      238 242 249;
  --c-border:        224 230 239;
  --c-border-strong: 203 213 225;

  --c-text:           30  41  59;
  --c-text-strong:    15  23  42;
  --c-text-soft:      51  65  85;
  --c-muted:         100 116 139;
  --c-faint:         148 163 184;

  --c-accent:         79  70 229;    /* indigo-600 */
  --c-accent-2:        8 145 178;    /* cyan-600   */
  --c-accent-soft:    67  56 202;

  --c-glow:           79  70 229;
  --c-shadow:         15  23  42;
  --c-shadow-a:       0.10;
  --c-glass:         255 255 255;
  --c-grid-line:      99 102 241;

  /* Glass tokens — LIGHT THEME
   *
   * These four values are what separate a real glass surface from a
   * translucent grey rectangle, so they are worth being precise about.
   *
   * --c-glass-tint is not white. It is a very slightly blue white, because a
   * neutral grey glass reads as "disabled" against a blue-tinted canvas; the
   * tint is what makes the panel feel like it belongs to the palette.
   *
   * The alphas are a stack, not one value. Real glass on a real screen is
   * built from three contributions: a near-opaque base so text stays legible
   * over a busy background, a light wash that catches the light, and a
   * specular highlight along the top edge. Folding all three into one
   * semi-transparent white is the usual mistake, and it is why most
   * "glassmorphism" looks like a grey smudge.
   *
   * --c-glass-a is the BASE and is deliberately high (0.72). Dropping it
   * lower looks better in a screenshot on one flat background and becomes
   * unreadable over a photo. 0.72 keeps 4.5:1 contrast against the grid
   * pattern while still showing the blur.
   */
  --c-glass-tint:    250 252 255;
  --c-glass-a:        0.72;
  --c-glass-edge:     1.0;   /* top-edge specular highlight opacity */
  --c-glass-blur:    16px;
  --c-glass-sat:      175%;  /* saturation lift; >100% is what sells "glass" */
}

/* -- 1b. Stacking order ---------------------------------------------------------
 *
 * This app has roughly twenty modals at z-50, a toast at z-60, the top nav at
 * z-40, and the sidebar rail at z-40. Those numbers were all chosen in
 * isolation, and the collision is structural rather than accidental: a modal
 * that outranks the rail means the rail's contents - labels, icons, the active
 * marker - are painted UNDER a dialog on every page, at every viewport.
 *
 * It first presented as a mobile-drawer bug, so the first fix here was scoped
 * to a max-width media query. That was wrong twice over. It was wrong about
 * scope, because the desktop rail sits at z-40 against the same z-50 modals and
 * has the same problem; and it was wrong about cause, because the trigger is
 * the rail losing a comparison it never wins, and that comparison is identical
 * on both sides of the breakpoint.
 *
 * THE RAIL OUTRANKS IN-PAGE OVERLAYS, AT EVERY WIDTH. The rail is the one
 * surface that is present on every page and is never dismissed, so it is the
 * wrong thing to lose a paint order. Modals are transient and scoped to the
 * page that opened them; losing to the rail costs nothing, because a modal that
 * spans the viewport can simply render beneath it without anything being
 * obscured that the user needs. The reverse - the rail under a modal - loses the
 * navigation permanently, with no way to recover it in view.
 *
 * The rule has to be enforced here rather than by renumbering the views,
 * because the modals are declared in ~20 templates and a future template will
 * ship another z-50 that nobody thinks to check. A rule that any later markup
 * cannot violate is worth more than a correct number today.
 *
 * The ordering, low to high:
 *   0        ambient decor (body::before/::after)
 *   1        page content, via `body > *`
 *   30       page chrome: top nav, dropdowns
 *   40-100   in-page overlays: modals, toasts, the leads popup
 *   900      mobile drawer backdrop
 *   1000     sidebar rail, at every viewport
 *
 * The backdrop sits below the rail for the same reason it always did: a
 * backdrop above the rail greys out the menu the user opened it to read. The
 * wide gap is headroom, so a modal can never land between them.
 *
 * The !important here is load-bearing and not a shortcut. Both sides are
 * Tailwind utilities of identical specificity - `z-40` in the markup, `z-50`
 * in twenty views - so without it the winner would be decided by which
 * stylesheet happened to load last, which is not a design anyone can rely on.
 * The number in the markup stays at z-40 deliberately: if this stylesheet ever
 * fails to load, the rail is still under the modals and the user can close
 * them, rather than a rail at 1000 trapping them behind a menu they cannot
 * dismiss. */
#sidebar {
  z-index: 1000 !important;
}
#sidebar-backdrop {
  z-index: 900 !important;
}

/* -- 2. Base surface ---------------------------------------------------------- */

html { -webkit-text-size-adjust: 100%; }

body {
  background-color: rgb(var(--c-bg));
  color: rgb(var(--c-text));
  transition: background-color 0.35s var(--rr-ease), color 0.35s var(--rr-ease);
}

/* Ambient grid + accent bloom. Fixed and non-interactive so it never
 * intercepts clicks, and cheap enough to sit under every page. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(60rem 40rem at 12% -10%, rgb(var(--c-accent) / 0.10), transparent 60%),
    radial-gradient(48rem 32rem at 92% 0%, rgb(var(--c-accent-2) / 0.07), transparent 60%),
    linear-gradient(rgb(var(--c-grid-line) / 0.045) 1px, transparent 1px) 0 0 / 100% 4rem,
    linear-gradient(90deg, rgb(var(--c-grid-line) / 0.045) 1px, transparent 1px) 0 0 / 4rem 100%;
}
[data-theme="light"] body::before { opacity: 0.5; }

/* Content must sit above the ambient layer. The blobs (body::after) are at
 * z-index 0, and content at 1, so the grid pattern and blobs never cover
 * text. */
body > * { position: relative; z-index: 1; }

/* The blobs are decorative and never interactive; keeping them out of the
 * tab order and off the pointer avoids a full-viewport element swallowing
 * clicks meant for the UI behind it. */
body::after { pointer-events: none; }

::selection { background: rgb(var(--c-accent) / 0.30); color: rgb(var(--c-text-strong)); }

* { scrollbar-width: thin; scrollbar-color: rgb(var(--c-border-strong)) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: rgb(var(--c-border-strong) / 0.9);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover { background: rgb(var(--c-faint) / 0.9); background-clip: content-box; }

/* Focus ring: visible on both themes, keyboard-only. */
:focus-visible {
  outline: 2px solid rgb(var(--c-accent-2));
  outline-offset: 2px;
  border-radius: 6px;
}

/* ══ 3. Components ════════════════════════════════════════════════════ */

/* The card surface. Replaces `bg-white` so panels follow the theme. */
/* The card surface. Replaces `bg-white` so panels follow the theme.
 *
 * Cards are the surface that benefits most from glass, because a dashboard is
 * mostly a grid of them stacked over one another: opaque cards make the blobs
 * behind invisible and flatten the whole page, whereas translucent ones let
 * the background move through the gaps and between the rows.
 *
 * The alpha here is lower than .rr-glass. A card is a larger area than a
 * sticky bar, and a large translucent region accumulates more background, so
 * it needs a slightly more opaque base to keep body text readable over a
 * bright blob. */
.rr-surface {
  position: relative;
  background: rgb(var(--c-glass-tint) / calc(var(--c-glass-a) + 0.10));
  -webkit-backdrop-filter: blur(calc(var(--c-glass-blur) * 0.7)) saturate(var(--c-glass-sat));
  backdrop-filter: blur(calc(var(--c-glass-blur) * 0.7)) saturate(var(--c-glass-sat));
  border: 1px solid rgb(var(--c-glass) / calc(var(--c-glass-edge) + 0.10));
  border-radius: var(--rr-radius);
  box-shadow:
    inset 0 1px 0 0 rgb(var(--c-glass) / var(--c-glass-edge)),
    0 1px 2px rgb(var(--c-shadow) / calc(var(--c-shadow-a) * 0.5)),
    0 8px 24px -12px rgb(var(--c-shadow) / var(--c-shadow-a));
  transition:
    background-color 0.35s var(--rr-ease),
    border-color 0.35s var(--rr-ease),
    box-shadow 0.25s var(--rr-ease),
    transform 0.25s var(--rr-ease);
}

/* Hover lift for cards that are actually interactive. */
.rr-interactive { cursor: pointer; }
.rr-interactive:hover {
  transform: translateY(-3px);
  border-color: rgb(var(--c-accent) / 0.45);
  box-shadow:
    0 2px 4px rgb(var(--c-shadow) / calc(var(--c-shadow-a) * 0.6)),
    0 18px 40px -18px rgb(var(--c-glow) / 0.55);
}

/* Glass — used by sticky bars, the sidebar and floating modals. */
/* Glass — the transparent surface used by sticky bars, the sidebar, floating
 * panels, modals and cards.
 *
 * HOW A REAL GLASS SURFACE IS BUILT
 * Flat white at 4% opacity is the version everyone ships first, and it is the
 * version that does not look like glass — it looks like a grey smudge. The
 * depth comes from four separate layers, each doing one job:
 *
 *   1. backdrop-filter  blurs and saturates what is BEHIND the panel. This is
 *      the entire effect. It is why the panel must not sit on a flat
 *      background: with nothing behind it, there is nothing to blur and the
 *      panel reads as plain transparency.
 *   2. a base fill       gives the pane enough body for text to stay legible
 *      over a busy background.
 *   3. a top-edge        specular highlight — a bright hairline on the top
 *      highlight          border only, because light lands on the upper edge
 *                        of a physical pane. This single detail does more for
 *      the "thickness"    realism than any amount of shadow.
 *   4. an inset shadow   implies the pane has a body with depth behind the
 *                        surface, rather than being a flat cutout.
 *
 * The saturation value is above 100% on purpose. Blurring alone desaturates
 * what passes behind the panel and makes everything look grey and dead; the
 * saturate() lift is what keeps the colour blobs vivid as they pass under
 * the glass. Removing it is the difference between glass and frosted plastic.
 */
.rr-glass {
  position: relative;
  background: rgb(var(--c-glass-tint) / var(--c-glass-a));
  -webkit-backdrop-filter: blur(var(--c-glass-blur)) saturate(var(--c-glass-sat));
  backdrop-filter: blur(var(--c-glass-blur)) saturate(var(--c-glass-sat));
  border: 1px solid rgb(var(--c-glass) / calc(var(--c-glass-edge) + 0.14));
  box-shadow:
    inset 0 1px 0 0 rgb(var(--c-glass) / var(--c-glass-edge)),
    inset 0 -1px 0 0 rgb(var(--c-shadow) / 0.06),
    0 8px 32px -8px rgb(var(--c-shadow) / calc(var(--c-shadow-a) * 0.8));
}

/* The specular highlight, drawn as a pseudo-element rather than a second
 * border so it can fade independently of the border colour. Kept on its own
 * layer because it must not be clipped by a parent's overflow. */
.rr-glass::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    160deg,
    rgb(var(--c-glass) / var(--c-glass-edge)) 0%,
    rgb(var(--c-glass) / 0) 38%,
    rgb(var(--c-glass) / 0) 100%
  );
}

/* Ambient colour blobs.
 *
 * These exist for one reason: backdrop-filter has nothing to work with on a
 * flat background. The grid pattern is too regular to read through a blur, so
 * without these the glass is visually inert no matter how the surface is
 * styled. They are fixed, blurred, and sit behind all content at z-index 0
 * so nothing depends on them being painted first.
 *
 * Fixed rather than absolute so they do not scroll away, and so a long page
 * does not build up a paint cost as the backdrop-filtered regions scroll. */
body::after {
  content: '';
  position: fixed;
  inset: -20%;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(38rem 30rem at 12% 8%,  rgb(var(--c-accent)     / 0.20), transparent 62%),
    radial-gradient(32rem 26rem at 88% 14%, rgb(var(--c-accent-2)   / 0.18), transparent 60%),
    radial-gradient(40rem 32rem at 72% 88%, rgb(var(--c-glow)       / 0.16), transparent 64%),
    radial-gradient(28rem 24rem at 22% 78%, rgb(168 85 247)        / 0.14), transparent 60%);
  filter: blur(20px);
  opacity: 0.9;
  /* Slow drift keeps the glass from looking like a static texture. This is
   * transform-only, so it stays on the compositor and never triggers the
   * repaint that animating a filter or background-position would cause on a
   * full-viewport layer. */
  animation: rrBlobDrift 44s var(--rr-ease-smooth) infinite alternate;
}
[data-theme="light"] body::after { opacity: 0.62; }

@keyframes rrBlobDrift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(2.5%, -2%, 0) scale(1.08); }
}

/* Fallback for browsers without backdrop-filter (older Firefox, some
 * embedded webviews).
 *
 * Without this the panel is only a translucent tint, and text over a moving
 * blob drops below the 4.5:1 contrast floor. The `@supports` guard means
 * browsers that DO support it never see this, so there is no second paint.
 * The fallback deliberately uses a higher alpha rather than the glass value:
 * legibility matters more than the effect when the effect is unavailable. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .rr-glass,
  .rr-surface {
    background: rgb(var(--c-glass-tint) / 0.94);
    box-shadow:
      inset 0 1px 0 0 rgb(var(--c-glass) / var(--c-glass-edge)),
      0 8px 32px -8px rgb(var(--c-shadow) / calc(var(--c-shadow-a) * 0.8));
  }
  .rr-glass::before { display: none; }
}

/* Accent gradient text, for hero numbers and page titles. */
.rr-gradient-text {
  background: linear-gradient(120deg, rgb(var(--c-accent-soft)), rgb(var(--c-accent-2)));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* Buttons ------------------------------------------------------------- */
.rr-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-weight: 600;
  border-radius: var(--rr-radius-sm);
  transition:
    transform 0.18s var(--rr-ease),
    box-shadow 0.18s var(--rr-ease),
    filter 0.18s var(--rr-ease),
    background-color 0.18s var(--rr-ease);
  will-change: transform;
}
.rr-btn:hover { transform: translateY(-1px); }
.rr-btn:active { transform: translateY(0) scale(0.985); }

.rr-btn-primary {
  background-image: linear-gradient(120deg, rgb(var(--c-accent)), rgb(var(--c-accent-2)));
  color: #fff;
  box-shadow: 0 6px 20px -8px rgb(var(--c-glow) / 0.85);
}
.rr-btn-primary:hover { filter: brightness(1.08); box-shadow: 0 10px 28px -8px rgb(var(--c-glow) / 0.95); }

.rr-btn-ghost {
  background-color: rgb(var(--c-surface-2));
  color: rgb(var(--c-text));
  border: 1px solid rgb(var(--c-border));
}
.rr-btn-ghost:hover { border-color: rgb(var(--c-accent) / 0.6); color: rgb(var(--c-accent-soft)); }

/* Pills / badges ------------------------------------------------------ */
.rr-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

/* Form controls ------------------------------------------------------- */
.rr-input {
  background-color: rgb(var(--c-surface-2));
  border: 1px solid rgb(var(--c-border));
  border-radius: var(--rr-radius-sm);
  color: rgb(var(--c-text));
  transition: border-color 0.2s, box-shadow 0.2s, background-color 0.35s var(--rr-ease);
}
.rr-input::placeholder { color: rgb(var(--c-faint)); }
.rr-input:hover { border-color: rgb(var(--c-border-strong)); }
.rr-input:focus {
  outline: none;
  border-color: rgb(var(--c-accent) / 0.75);
  box-shadow: 0 0 0 3px rgb(var(--c-accent) / 0.20);
}
select.rr-input option { background-color: rgb(var(--c-surface)); color: rgb(var(--c-text)); }

/* Tables -------------------------------------------------------------- */
.rr-table { border-collapse: separate; border-spacing: 0; width: 100%; }
.rr-table thead th {
  background-color: rgb(var(--c-surface-2));
  color: rgb(var(--c-muted));
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  text-align: left;
  padding: 0.7rem 1rem;
  border-bottom: 1px solid rgb(var(--c-border));
  position: sticky;
  top: 0;
  z-index: 1;
}
.rr-table tbody td { padding: 0.7rem 1rem; border-bottom: 1px solid rgb(var(--c-border) / 0.6); }
.rr-table tbody tr { transition: background-color 0.18s; }
.rr-table tbody tr:hover { background-color: rgb(var(--c-accent) / 0.07); }

/* KPI / stat tiles ---------------------------------------------------- */
.rr-kpi { position: relative; overflow: hidden; }
.rr-kpi::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(20rem 10rem at 100% 0%, rgb(var(--c-glow) / 0.14), transparent 60%);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s var(--rr-ease);
}
.rr-kpi:hover::after { opacity: 1; }

/* Sidebar ------------------------------------------------------------- */
/* The rail follows the active theme like every other surface: it is built from
 * --c-glass-tint / --c-glass-a, so it is white and airy in light and deep blue
 * in dark.
 *
 * What it must NOT do is carry hardcoded light inks. This rail used to be
 * pinned to a fixed dark brand gradient, and its inks were pinned light to
 * match (#cbd5e1 links, `hover:text-white`, #94a3b8 headings). Those two
 * decisions were a matched pair. Once the rail became theme-aware, the inks
 * stayed light while the backing turned white, and the result was white text
 * on a white panel — the menu items disappeared on hover in the light theme.
 *
 * So the inks are now derived from the same theme tokens as the panel. That
 * keeps the pair consistent by construction: change the theme and both move
 * together, and no ink can be left stranded on the wrong background. */
#sidebar {
  /* THE FILL LIVES HERE, ON THE SCROLL CONTAINER ITSELF.
   *
   * It used to live on a child element, `.sidebar-glass`, which was
   * `position: fixed; inset: 0` inside this rail. The reasoning was that
   * backdrop-filter cannot go on a scrolling fixed element — it turns the
   * element into a backdrop root and the scroller then clips its own children —
   * so the fill and the blur had to live on a separate, non-scrolling child.
   * That reasoning was sound WHILE THERE WAS A BLUR TO KEEP.
   *
   * There is not any more, and the pane turned out not to be non-scrolling at
   * all. Measured in the browser: the rail's own rect was y=64 h=892, and the
   * pane's was y=-357 h=892. Same height, correct fill, but the pane was 421px
   * too high — it had travelled WITH the menu, because a fixed child of a
   * scroller rides the scroller's content. So the pane covered the top of the
   * rail and its bottom edge cut across the middle of the visible area, and
   * everything under that line showed the bare page canvas: 250 252 255 above,
   * 244 246 251 below, on the light theme. A hard horizontal seam, white over
   * off-white, that moved every time the menu was scrolled.
   *
   * A background on the element paints on that element's own box, not on its
   * scrolled content, so it stays put however far the menu scrolls. No child, no
   * positioning trick, and no dependence on scroll position. The rail is its
   * own surface.
   *
   * height is still left to `top` + `bottom`. Setting a height or a min-height
   * here would fight the fixed positioning: `min-h-screen` in particular
   * resolved to a full viewport inside a box that only had 100vh - 4rem
   * available, leaving 64px of rail hanging below the fold. */
  background: rgb(var(--c-glass-tint));
  /* The border and shadow used to hang off the pane. They are the rail's own
   * edge, so they belong to the rail. The transparent 1px placeholder existed
   * only to reserve the width the pane was inset against. */
  border-right: 1px solid rgb(var(--c-glass) / calc(var(--c-glass-edge) + 0.10));
  box-shadow:
    inset -1px 0 0 0 rgb(var(--c-glass) / calc(var(--c-glass-edge) * 0.6)),
    2px 0 24px -12px rgb(var(--c-shadow) / var(--c-shadow-a));
  scrollbar-width: thin;
  scrollbar-color: rgb(var(--c-faint) / 0.5) transparent;
}

/* The sticky close row, which sits over the links as the menu scrolls.
 * It needs its own fill for exactly that reason — the links pass underneath it,
 * and without an opaque row they read through the button. Behind it is now the
 * rail's own background rather than a pane, so this is a plain match, not a
 * second layer. No alpha: a translucent row here is the same seam as the one
 * this file spent five versions removing, in miniature. */
.sidebar-close {
  background: rgb(var(--c-glass-tint));
}
#sidebar::-webkit-scrollbar { width: 4px; }
/* Was rgb(255 255 255 / 0.16) — a fixed white alpha, which is invisible on the
   light theme's near-white rail. Theme-derived like everything else in the rail,
   so the thumb is visible on both. */
#sidebar::-webkit-scrollbar-thumb { background: rgb(var(--c-faint) / 0.55); border-radius: 4px; }

/* The fixed watermark in templates/footer.php.
 *
 * It floats over the page rather than participating in flow, so two things have
 * to be true and neither is obvious from the markup:
 *
 *  1. THE CONTENT COLUMN MUST CLEAR IT, or the last rows of every table sit
 *     underneath the bar. --rr-watermark-h sizes the bar, and .rr-main reserves
 *     the same height plus the page's own bottom padding.
 *  2. IT MUST BE CENTRED ON THE CONTENT, NOT ON THE VIEWPORT. The rail is a
 *     fixed 18rem column down the left, so a bar spanning left:0-right:0 is
 *     centred against the viewport and its text reads as pushed to the right.
 *     Below lg there is no rail to clear, so the offset is desktop-only and
 *     keyed to the same 18rem the rail uses.
 *
 * Both numbers come from tokens rather than utilities, because the alternative
 * is two copies of 18rem in two files that drift apart unnoticed the day the
 * rail is resized. `left-72` is not in the built Tailwind stylesheet, so
 * expressing the offset as a utility would have failed open on desktop. */
:root {
  --rr-rail-w: 18rem;          /* the fixed rail's width; matches w-72 in sidebar.php */
  --rr-watermark-h: 1.75rem;   /* py-1.5 (0.375rem x2) around a 10px line */
}
.rr-watermark {
  left: 0;
  right: 0;
  height: var(--rr-watermark-h);
  display: flex;
  align-items: center;
  justify-content: center;
}
@media (min-width: 1024px) {
  .rr-watermark { left: var(--rr-rail-w); }
}
.rr-main { padding-bottom: calc(var(--rr-watermark-h) + 1.5rem); }

/* Menu + meta inks, pinned off the neutral ramp (see note above). The active
 * link carries `bg-blue-600 text-white` and no slate class, so it is untouched. */
/* Menu + meta inks, derived from the theme tokens.
 *
 * These used to be pinned literals (#cbd5e1 / #94a3b8) because the rail was a
 * fixed dark panel. A pinned ink is only correct against a fixed background;
 * once the rail follows the theme, a pinned light ink lands on a white panel
 * and disappears. Every one of these is now a token, so the ink and the panel
 * can never disagree about which theme they are in. */
#sidebar a                     { color: rgb(var(--c-text-soft)); }
#sidebar a:hover               { color: rgb(var(--c-accent)); }
#sidebar a.text-slate-300      { color: rgb(var(--c-text-soft)); }
#sidebar a.text-slate-300:hover{ color: rgb(var(--c-accent)); }
#sidebar .text-slate-400,
#sidebar .text-slate-500       { color: rgb(var(--c-muted)); }
#sidebar .text-slate-300:not(a) { color: rgb(var(--c-text-soft)); }

/* Nav item: the accent bar grows from the left on hover/active. */
#sidebar a {
  position: relative;
  transition: color 0.2s, background-color 0.2s;
}
/* ─── Active nav label: gradient text ────────────────────
 *
 * CONTRAST IS THE CONSTRAINT HERE, and it dictated the whole palette.
 *
 * A gradient label is three colours, not one, and the eye only checks the
 * average. Against the solid blue-600 pill (#2563eb) the measured ratios are:
 *
 *   #ffffff  5.12:1      #e6f6ff  4.63:1      #cffafe  4.57:1
 *
 * Every stop clears 4.5:1, so the *least* legible part of the gradient still
 * passes WCAG AA for body text. The nav is text-sm (14px) at weight 600,
 * which is below the 18.66px/14pt-bold threshold for "large text", so the
 * full 4.5:1 applies and no exemption is available.
 *
 * The obvious alternative — cyan #22d3ee, which is what the accent tokens use
 * — was rejected: it lands at roughly 2.5:1 on blue-600 and fails outright.
 * The stops are pale on purpose; the gradient has to be built out of tints,
 * not the brand colours.
 *
 * The tint also has to stay LIGHT in both themes. The pill is a literal
 * Tailwind `bg-blue-600` that does not invert, so a dark-theme gradient would
 * put dark text on a mid-blue fill and vanish.
 */
.rr-nav-active-text { color: #cffafe; }   /* fallback, see @supports below */

/* @supports, not a plain rule: `color: transparent` is a one-way door. Without
 * background-clip: text the text would be invisible rather than merely
 * unstyled, so the gradient is only enabled where it actually renders. The
 * solid fallback above is what shows in a browser that lacks the property. */
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .rr-nav-active-text {
    background-image: linear-gradient(120deg, #ffffff 0%, #e6f6ff 50%, #cffafe 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    /* Safari honours background-clip:text inconsistently unless the fill is
     * also cleared explicitly. */
    -webkit-text-fill-color: transparent;
  }
}

/* The icon keeps a solid ink. It cannot inherit the gradient: it is a
 * separate element with no background box of its own, so the transparent fill
 * would leave it invisible. A flat tint next to the gradient label is
 * visually consistent and stays above the contrast floor. */
.rr-nav-active-icon { color: #dff3fe; }

/* The hover wash is an accent tint, not a white one.
 *
 * It was `rgb(255 255 255 / 0.06)`, which only ever worked on a dark rail —
 * on the light theme that put a white link label on a white panel. Tinting with
 * the accent instead means one declaration is legible in both themes: a pale
 * blue wash behind a blue label on light, and a faint blue lift behind a light
 * label on dark. Same rule, no theme branch.
 *
 * `:not(.is-active)` is load-bearing. This is an ID + pseudo-class selector,
 * so it outranks Tailwind's `bg-blue-600` utility (0,1,0) on the active link;
 * without the exclusion, hovering the current page replaced its solid fill
 * and the label lost its contrast. `.active` is also excluded in case another
 * template still emits the older class name. */
#sidebar a:hover:not(.is-active):not(.active) {
  background-color: rgb(var(--c-accent) / 0.10);
}
/* The active link keeps its fill on hover. brightness() rather than a
 * background change so the pinned blue is never replaced, only lifted. */
#sidebar a.is-active:hover { filter: brightness(1.1); }
#sidebar a::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  height: 0;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: linear-gradient(180deg, rgb(var(--c-accent)), rgb(var(--c-accent-2)));
  transform: translateY(-50%);
  transition: height 0.25s var(--rr-ease);
}
#sidebar a:hover::before,
#sidebar a.is-active::before,
#sidebar a.active::before { height: 60%; }

/* Theme toggle — the icon always shows the theme you'd switch TO. */
#rr-theme-toggle {
  transition: transform 0.4s var(--rr-ease), background-color 0.25s, border-color 0.25s;
}
#rr-theme-toggle:hover { transform: rotate(18deg) scale(1.06); }
#rr-theme-toggle .rr-icon-sun  { display: inline-flex; }
#rr-theme-toggle .rr-icon-moon { display: none; }
[data-theme="light"] #rr-theme-toggle .rr-icon-sun  { display: none; }
[data-theme="light"] #rr-theme-toggle .rr-icon-moon { display: inline-flex; }

/* ══ 4. Motion ════════════════════════════════════════════════════════
 *
 * All motion is opt-in through a class or a data attribute, and everything
 * that animates is limited to `transform` and `opacity`.
 *
 * That restriction is not a style preference. Animating width, height, top,
 * left or box-shadow forces the browser to re-run layout and paint on every
 * frame. On a page with a 200-row lead table that turns a smooth interaction
 * into visible jank on modest hardware. Transform and opacity are handled on
 * the compositor and never touch layout, so they stay at 60fps regardless of
 * how heavy the document is. Where a shadow must change, the layer below
 * animates a pseudo-element's opacity rather than the shadow itself.
 *
 * js/motion.js decides which elements get which class; this file defines the
 * behaviour. Keeping them apart means the visual language can be retuned
 * without re-reading the detection logic, and the detection can be fixed
 * without touching a single animation.
 */

@keyframes rrFadeUp {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes rrFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes rrScaleIn {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes rrSlideRight {
  from { opacity: 0; transform: translate3d(-18px, 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes rrSlideUp {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes rrPulseGlow {
  0%, 100% { box-shadow: 0 0 0 0 rgb(var(--c-glow) / 0.45); }
  50%      { box-shadow: 0 0 0 7px rgb(var(--c-glow) / 0); }
}
@keyframes rrSpin { to { transform: rotate(360deg); } }

/* Toast entrance. Comes from the top-right, and overshoots very slightly so
 * it reads as arriving rather than materialising. */
@keyframes rrToastIn {
  from { opacity: 0; transform: translate3d(0, -14px, 0) scale(0.96); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}
@keyframes rrToastOut {
  from { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
  to   { opacity: 0; transform: translate3d(0, -10px, 0) scale(0.97); }
}

/* Dropdown / popover. Scales from the top edge, which is the edge it grows
 * out of, so the motion explains where the panel came from. */
@keyframes rrDropIn {
  from { opacity: 0; transform: translate3d(0, -8px, 0) scale(0.97); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}

/* Modal backdrop and panel. The panel travels further than the backdrop and
 * over a longer duration, which layers the motion: the scrim arrives first,
 * then the content settles on top of it. */
@keyframes rrScrimIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes rrScrimOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes rrModalIn {
  from { opacity: 0; transform: translate3d(0, 16px, 0) scale(0.97); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}
@keyframes rrModalOut {
  from { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
  to   { opacity: 0; transform: translate3d(0, 10px, 0) scale(0.98); }
}

/* Skeletons. A sweeping highlight reads as "working"; a pulsing block reads
 * as "broken". */
@keyframes rrSkeleton {
  0%   { background-position: -180% 0; }
  100% { background-position: 180% 0; }
}

/* Inline validation. A colour shift alone is missed by users with the most
 * common forms of colour blindness, so the message also shakes, and a valid
 * field shows a drawn tick rather than only turning green. */
.rr-field-bad  { animation: rrShake 340ms var(--rr-ease-smooth) both; }
.rr-field-good { border-color: rgb(16 185 129 / 0.6) !important; }

/* Inline validation, applied by adding a class. Uses a colour shift plus a
 * short horizontal shake, so the message is noticed even by a user who does
 * not read the text. */
@keyframes rrShake {
  0%, 100%      { transform: translate3d(0, 0, 0); }
  20%, 60%       { transform: translate3d(-4px, 0, 0); }
  40%, 80%       { transform: translate3d(4px, 0, 0); }
}
.rr-shake { animation: rrShake 340ms var(--rr-ease-smooth) both; }

/* Success tick. Drawn as a border on a rotated square, so it needs no
 * SVG asset and no icon font. */
@keyframes rrTickDraw {
  from { opacity: 0; transform: rotate(-45deg) scale(0.5); }
  to   { opacity: 1; transform: rotate(-45deg) scale(1); }
}
.rr-tick {
  display: inline-block;
  width: 0.9em;
  height: 0.45em;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  animation: rrTickDraw var(--rr-dur-base) var(--rr-ease-spring) both;
}

/* Skeleton loading state. */
.rr-skeleton {
  background: linear-gradient(
    90deg,
    rgb(var(--c-surface-2)) 25%,
    rgb(var(--c-border)) 37%,
    rgb(var(--c-surface-2)) 63%
  );
  background-size: 200% 100%;
  animation: rrSkeleton 1.4s linear infinite;
  border-radius: 6px;
}

/* ── Enter animations ─────────────────────────────────────────────────── */

.rr-fade-up   { animation: rrFadeUp var(--rr-dur-slow) var(--rr-ease-out) both; }
.rr-fade-in   { animation: rrFadeIn var(--rr-dur-base) var(--rr-ease-out) both; }
.rr-scale-in  { animation: rrScaleIn var(--rr-dur-base) var(--rr-ease-out) both; }
.rr-slide-in  { animation: rrSlideRight var(--rr-dur-slow) var(--rr-ease-out) both; }
.rr-rise-in   { animation: rrSlideUp var(--rr-dur-base) var(--rr-ease-out) both; }
.rr-pulse     { animation: rrPulseGlow 2.2s ease-in-out infinite; }

/* Choreographed entrance for a group. Each child reads its own index from
 * --rr-i, which js/motion.js assigns, so a long list costs one custom
 * property per item rather than N generated stylesheet rules.
 *
 * The cap on the delay is not a performance concern — the browser handles
 * hundreds of delayed animations fine. It is a pacing one: past ~12 steps the
 * tail of the list is still animating long after the user has read the head,
 * so it reads as lag instead of choreography. */
.rr-stagger > * {
  animation: rrFadeUp var(--rr-dur-slow) var(--rr-ease-out) both;
  animation-delay: calc(
    min(var(--rr-i, 0), 12) * var(--rr-stagger-step)
  );
}

/* ── Interactive surfaces ─────────────────────────────────────────────── */

/* Hover lift for cards and clickable panels.
 *
 * The elevation is done with a dedicated ::after layer whose opacity is
 * animated, rather than transitioning box-shadow on the element. Transitioning
 * a shadow repaints the element every frame; transitioning an opacity on a
 * composited pseudo-element does not. The pseudo-element carries the shadow
 * and is a no-op until the card is hovered, so non-hovered cards cost nothing.
 *
 * `will-change` is set only on the state that needs it, and released on the
 * way out. Leaving it on permanently promotes hundreds of cards to their own
 * compositor layer and exhausts GPU memory — a common way motion ends up
 * making a page slower. */
.rr-lift {
  position: relative;
  transition:
    transform var(--rr-dur-fast) var(--rr-ease-out),
    border-color var(--rr-dur-fast) var(--rr-ease-out);
}
.rr-lift::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 10px 28px -8px rgb(var(--c-shadow) / calc(var(--c-shadow-a) * 1.6));
  opacity: 0;
  transition: opacity var(--rr-dur-fast) var(--rr-ease-out);
}
.rr-lift:hover {
  transform: translate3d(0, -2px, 0);
  border-color: rgb(var(--c-accent) / 0.45);
}
.rr-lift:hover::after { opacity: 1; }
.rr-lift:active {
  transform: translate3d(0, 0, 0) scale(0.985);
  transition-duration: var(--rr-dur-instant);
}

/* Press feedback for buttons. Scaled down, not darkened: scaling is a
 * compositor-only property, so it stays responsive even when the button
 * carries a gradient and a shadow. */
.rr-press {
  transition:
    transform var(--rr-dur-fast) var(--rr-ease-out),
    background-color var(--rr-dur-fast) var(--rr-ease-out),
    border-color var(--rr-dur-fast) var(--rr-ease-out),
    color var(--rr-dur-fast) var(--rr-ease-out);
}
.rr-press:active { transform: scale(0.96); }

/* Keyboard focus. The ring is animated so it grows into place rather than
 * snapping, and it is drawn with a negative offset plus a padding so it
 * clears the element's own border without shifting layout. */
.rr-focus { transition: box-shadow var(--rr-dur-fast) var(--rr-ease-out); }
.rr-focus:focus-visible {
  outline: 2px solid rgb(var(--c-accent) / 0.75);
  outline-offset: 2px;
}

/* Row reorder. The lead-swipe gesture in app.js moves DOM nodes; this gives
 * the survivors a short settle so the list does not snap to its new order. */
.rr-reorder { transition: transform var(--rr-dur-base) var(--rr-ease-out); }

/* ── Scroll reveal ────────────────────────────────────────────────────── */

/* Elements below the fold start hidden and are released by the
 * IntersectionObserver in js/motion.js. The observer unobserves each element
 * after it fires, so scrolling back up does not replay the animation — which
 * would make a long page feel like it was flickering at the user. */
.rr-reveal {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition:
    opacity var(--rr-dur-slow) var(--rr-ease-out),
    transform var(--rr-dur-slow) var(--rr-ease-out);
  transition-delay: calc(min(var(--rr-i, 0), 8) * var(--rr-stagger-step));
}
.rr-reveal.rr-revealed {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* ── Toast, dropdown, modal ───────────────────────────────────────────── */

.rr-toast      { animation: rrToastIn var(--rr-dur-base) var(--rr-ease-spring) both; }
.rr-toast-out  { animation: rrToastOut var(--rr-dur-fast) var(--rr-ease-in) both; }
.rr-drop       { animation: rrDropIn var(--rr-dur-fast) var(--rr-ease-out) both; }
.rr-scrim      { animation: rrScrimIn var(--rr-dur-fast) var(--rr-ease-out) both; }
.rr-scrim-in   { animation: rrScrimIn var(--rr-dur-fast) var(--rr-ease-out) both; }
.rr-scrim-out  { animation: rrScrimOut var(--rr-dur-fast) var(--rr-ease-in) both; }
.rr-modal      { animation: rrModalIn var(--rr-dur-base) var(--rr-ease-out) both; }
.rr-modal-out  { animation: rrModalOut var(--rr-dur-fast) var(--rr-ease-in) both; }

/* ── Count-up numbers ─────────────────────────────────────────────────── */

/* The JS writes the final value; a class swap fades between the two. The
 * font-variant-numeric keeps digits from changing width as they tick, which
 * would otherwise make the whole card jitter horizontally. */
.rr-count {
  font-variant-numeric: tabular-nums;
  transition: opacity var(--rr-dur-fast) var(--rr-ease-out);
}

.rr-spin { animation: rrSpin 0.8s linear infinite; }

/* Inline spinner for a pending button. The track is a ring drawn from a
 * border, and only the transparent side is rotated — animating `background`
 * or a conic-gradient would repaint every frame. */
.rr-spinner {
  display: inline-block;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: rrSpin 0.7s linear infinite;
  vertical-align: -0.125em;
}

/* Pending buttons keep their width so the row cannot reflow under the
   pointer, and dim slightly to read as "not available right now" rather
   than as a hover state. */
.rr-pending {
  cursor: progress;
  opacity: 0.75;
  pointer-events: none;
}

/* Page-level enter. Applied to the <main> wrapper once, by js/motion.js. */
.rr-page { animation: rrFadeIn var(--rr-dur-base) var(--rr-ease-out) both; }

/* Respect the OS setting. Motion here is decorative, so turning it off must
 * not hide anything — the end state of every animation is the resting state.
 *
 * Two things matter here.
 *
 * First, the durations are forced to ~0 rather than 0. A literal 0ms still
 * dispatches animationstart/animationend and still runs the compositor for a
 * frame, so a page full of 0ms animations still costs main-thread work. A
 * non-zero but imperceptible duration is the conventional way to make the
 * browser treat it as "no motion".
 *
 * Second, .rr-reveal is explicitly reset. It is the one class whose *default*
 * state is invisible, so a naive blanket override would leave every
 * below-the-fold element permanently hidden with no way to reveal it. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .rr-stagger > * { animation: none !important; }
  /* The blob drift is a full-viewport transform on a blurred layer. Even
   though it is compositor-only, on a low-power device the continuously
   animating blur is the single most expensive thing on the page, so reduced
   motion stops it entirely rather than leaving it running. */
  body::after { animation: none !important; }
  .rr-reveal {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  .rr-lift:hover { transform: none !important; }
  .rr-lift:active,
  .rr-press:active { transform: none !important; }
  .rr-shake { animation: none !important; }
}

/* ══ 5. Dark accent remap ═════════════════════════════════════════════
 * The accent families stay literal in Tailwind (see tailwind.config.js), so
 * `bg-blue-100` is a near-white fill — unreadable on a dark canvas. These
 * rules re-point the two shades the UI actually leans on: the -100 tints
 * (badge/pill fills) and the -700 text that sits on them.
 *
 * !important is required and deliberate: the alternative is threading
 * `dark:` variants through ~40 badge sites and the PHP helpers that emit
 * badge classes, which is exactly the maintenance burden this layer avoids.
 * Scope is narrow (two shades, dark theme only) and the values are the same
 * hues, re-tuned for a dark backdrop. */

[data-theme="dark"] .bg-blue-100    { background-color: rgb(var(--c-accent) / 0.20) !important; }
[data-theme="dark"] .text-blue-700  { color: rgb(147 197 253) !important; }
[data-theme="dark"] .bg-blue-50     { background-color: rgb(var(--c-accent) / 0.10) !important; }

[data-theme="dark"] .bg-emerald-100 { background-color: rgb(16 185 129 / 0.18) !important; }
[data-theme="dark"] .text-emerald-700 { color: rgb(110 231 183) !important; }
[data-theme="dark"] .bg-emerald-50  { background-color: rgb(16 185 129 / 0.09) !important; }

[data-theme="dark"] .bg-green-100   { background-color: rgb(34 197 94 / 0.18) !important; }
[data-theme="dark"] .text-green-700 { color: rgb(134 239 172) !important; }
[data-theme="dark"] .bg-green-50    { background-color: rgb(34 197 94 / 0.09) !important; }

[data-theme="dark"] .bg-red-100     { background-color: rgb(239 68 68 / 0.18) !important; }
[data-theme="dark"] .text-red-700   { color: rgb(252 165 165) !important; }
[data-theme="dark"] .bg-red-50      { background-color: rgb(239 68 68 / 0.09) !important; }

[data-theme="dark"] .bg-amber-100   { background-color: rgb(245 158 11 / 0.18) !important; }
[data-theme="dark"] .text-amber-700 { color: rgb(252 211 77) !important; }
[data-theme="dark"] .bg-amber-50    { background-color: rgb(245 158 11 / 0.09) !important; }

[data-theme="dark"] .bg-yellow-100  { background-color: rgb(234 179 8 / 0.18) !important; }
[data-theme="dark"] .text-yellow-700 { color: rgb(253 224 71) !important; }

[data-theme="dark"] .bg-purple-100  { background-color: rgb(168 85 247 / 0.20) !important; }
[data-theme="dark"] .text-purple-700 { color: rgb(216 180 254) !important; }
[data-theme="dark"] .bg-purple-50  { background-color: rgb(168 85 247 / 0.10) !important; }

[data-theme="dark"] .bg-orange-100  { background-color: rgb(249 115 22 / 0.18) !important; }
[data-theme="dark"] .text-orange-700 { color: rgb(253 186 116) !important; }

[data-theme="dark"] .bg-indigo-100  { background-color: rgb(var(--c-accent) / 0.20) !important; }
[data-theme="dark"] .text-indigo-700 { color: rgb(165 180 252) !important; }

[data-theme="dark"] .bg-cyan-100    { background-color: rgb(var(--c-accent-2) / 0.18) !important; }
[data-theme="dark"] .text-cyan-700  { color: rgb(103 232 249) !important; }

[data-theme="dark"] .bg-pink-100    { background-color: rgb(236 72 153 / 0.18) !important; }
[data-theme="dark"] .text-pink-700  { color: rgb(251 182 206) !important; }

[data-theme="dark"] .bg-sky-100     { background-color: rgb(14 165 233 / 0.18) !important; }
[data-theme="dark"] .text-sky-700   { color: rgb(125 211 252) !important; }
[data-theme="dark"] .bg-sky-50      { background-color: rgb(14 165 233 / 0.09) !important; }

[data-theme="dark"] .bg-rose-100    { background-color: rgb(244 63 94 / 0.18) !important; }
[data-theme="dark"] .text-rose-700  { color: rgb(253 164 175) !important; }
[data-theme="dark"] .bg-rose-50     { background-color: rgb(244 63 94 / 0.09) !important; }

[data-theme="dark"] .bg-violet-100  { background-color: rgb(139 92 246 / 0.20) !important; }
[data-theme="dark"] .text-violet-700 { color: rgb(196 181 253) !important; }
[data-theme="dark"] .bg-violet-50   { background-color: rgb(139 92 246 / 0.10) !important; }

/* Dark inputs: placeholder/datalist chrome follows the theme. */
[data-theme="dark"] input:-webkit-autofill,
[data-theme="dark"] input:-webkit-autofill:hover,
[data-theme="dark"] input:-webkit-autofill:focus {
  -webkit-text-fill-color: rgb(var(--c-text));
  -webkit-box-shadow: 0 0 0 1000px rgb(var(--c-surface-2)) inset;
  caret-color: rgb(var(--c-text));
}
[data-theme="dark"] option { background-color: rgb(var(--c-surface)); color: rgb(var(--c-text)); }

/* ══ 6. App-specific rules ════════════════════════════════════════════ */

/* Legacy animation classes kept so older markup still animates. */
@keyframes fadeIn  { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@keyframes slideUp { from { opacity: 0; transform: translateY(12px); }  to { opacity: 1; transform: none; } }
.fade-in  { animation: fadeIn 0.25s ease; }
.slide-up { animation: slideUp 0.3s ease; }

.kpi-card { transition: box-shadow 0.25s var(--rr-ease), transform 0.25s var(--rr-ease); }
.kpi-card:hover { transform: translateY(-2px); box-shadow: 0 12px 36px -14px rgb(var(--c-glow) / 0.6); }

.flash-message { animation: rrFadeUp 0.35s var(--rr-ease) both; }

.timeline-dot::before {
  content: '';
  position: absolute;
  left: -13px;
  top: 50%;
  transform: translateY(-50%);
  width: 8px;
  height: 8px;
  background: rgb(var(--c-accent));
  border-radius: 50%;
  border: 2px solid rgb(var(--c-surface));
  box-shadow: 0 0 0 3px rgb(var(--c-accent) / 0.18);
}

tr.clickable { cursor: pointer; }
tr.clickable:active { background-color: rgb(var(--c-accent) / 0.12); }

.modal-backdrop {
  backdrop-filter: blur(6px);
  background-color: rgb(var(--c-shadow) / 0.6);
}

/* Modal enter: the panel scales in from the backdrop. */
.rr-modal-panel { animation: rrScaleIn 0.28s var(--rr-ease) both; }

/* Leads popup modal — bulletproof scroll (online + mobile) */
#modal-leads-popup {
  overflow-y: auto !important;
  -webkit-overflow-scrolling: touch !important;
}
#modal-leads-popup .popup-modal-card {
  max-height: 80vh !important;
  max-height: 80dvh !important;
  display: flex !important;
  flex-direction: column !important;
  overflow: hidden !important;
  background-color: rgb(var(--c-surface));
  border: 1px solid rgb(var(--c-border));
}
#modal-leads-popup .popup-modal-card .popup-body {
  flex: 1 1 0% !important;
  min-height: 0 !important;
  overflow-y: auto !important;
  overflow-x: hidden !important;
  -webkit-overflow-scrolling: touch !important;
  overscroll-behavior: contain !important;
  touch-action: pan-y !important;
  scroll-behavior: smooth !important;
}
@media (max-width: 768px) {
  #modal-leads-popup .popup-modal-card { max-height: 70vh; max-height: 70dvh; margin: 8px; }
}
#modal-leads-popup .popup-modal-card .popup-body a {
  display: flex;
  align-items: center;
  padding: 12px;
}
#modal-leads-popup .popup-modal-card .popup-body a:hover {
  background-color: rgb(var(--c-accent) / 0.08);
}

/* ─── Mandatory watermark — DO NOT REMOVE ────────────── */
.powered-by-watermark {
  font-size: 10px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: rgb(var(--c-faint));
  pointer-events: none;
  user-select: none;
}

/* ─── Stage pills ────────────────────────────────────── */
.stage-pill {
  display: inline-flex;
  align-items: center;
  padding: 2px 10px;
  border-radius: 9999px;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* ─── Read-only banner ───────────────────────────────── */
#readonly-banner {
  background: linear-gradient(90deg, #f59e0b, #d97706);
  color: white;
  text-align: center;
  font-size: 0.8rem;
  font-weight: 600;
  padding: 6px 16px;
  letter-spacing: 0.03em;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 9999;
}

/* ─── Print tweaks ───────────────────────────────────── */
@media print {
  #sidebar, #topnav, .no-print { display: none !important; }
  /* Both ambient layers are removed, not just the grid: a full-viewport
     blurred layer prints as a muddy grey wash over every page and burns
     toner for nothing. */
  body::before, body::after { display: none !important; }
  /* Glass is dropped in favour of flat white. A translucent panel over white
     paper still paints the tint, and the backdrop-filter is discarded by the
     print pipeline anyway, so it would cost ink and buy nothing. */
  .rr-glass, .rr-surface {
    background: #fff !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    box-shadow: none !important;
    border: 1px solid #d4d4d8 !important;
  }
  .rr-glass::before { display: none !important; }
  main { margin: 0 !important; }
  body { background: #fff !important; color: #000 !important; }
}

/* Lead "View full details" disclosure. A native <details> keeps the requirement
   fields reachable with JavaScript disabled. Hiding the default marker and
   rotating the chevron are not in the compiled Tailwind build (which has no
   group-open/rotate-180/list-none utilities), so they live here instead. */
.rr-details > summary { list-style: none; cursor: pointer; }
.rr-details > summary::-webkit-details-marker { display: none; }
.rr-details > summary .rr-chev { transition: transform 0.2s ease; }
.rr-details[open] > summary .rr-chev { transform: rotate(180deg); }

