/* =============================================================
   Treatlly Practice OS — Design Tokens (CSS variables only)
   Component classes are composed from Tailwind utilities; this
   file holds the design tokens for light + dark mode, fonts,
   and a few base resets that aren't easily expressed inline.
   ============================================================= */

:root {
  /* Brand — Treatlly Teal */
  --brand-50:  #E6F2EF;
  --brand-100: #C2DED7;
  --brand-200: #8EC1B5;
  --brand-300: #4F9F8E;
  --brand-400: #1F7E6B;
  --brand-500: #0E5F52;
  --brand-600: #0A4D43;
  --brand-700: #073B33;
  --brand-800: #062E28;
  --brand-900: #04201C;

  /* Neutrals (warm) */
  --ink-0:   #FFFFFF;
  --ink-25:  #FAFAF9;
  --ink-50:  #F5F5F4;
  --ink-100: #EDEDEB;
  --ink-150: #E2E2DF;
  --ink-200: #D4D4D0;
  --ink-300: #B6B5B0;
  --ink-400: #8E8D87;
  --ink-500: #6E6D67;
  --ink-600: #50504B;
  --ink-700: #3A3A36;
  --ink-800: #232320;
  --ink-900: #131311;

  /* Semantic (low-chroma) */
  --success:    #156D50;
  --success-bg: #E5F1EC;
  --warning:    #8A5A0B;
  --warning-bg: #FBF3DF;
  --danger:     #B33636;
  --danger-bg:  #FBE9E9;
  --info:       #1F4F94;
  --info-bg:    #E8EFFB;

  /* Brand foreground for soft pills (`bg-brand-50` text). Light mode uses
     brand-600; dark mode flips to brand-300 (see dark block). */
  --brand-fg: #0A4D43;

  /* Surfaces (light) */
  --bg-app:      #F7F7F5;
  --bg-card:     #FFFFFF;
  --bg-sunken:   #F2F2EF;
  --bg-elevated: #FFFFFF;
  --bg-inverted: #131311;

  --border-subtle: #ECECE9;
  --border:        #E2E2DE;
  --border-strong: #CFCFCA;

  --text-primary:   #131311;
  --text-secondary: #50504B;
  --text-tertiary:  #8E8D87;

  /* Type families */
  --font-sans: 'Geist', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;

  /* Motion */
  --ease:     cubic-bezier(0.2, 0.7, 0.2, 1);
  --dur-fast: 120ms;
  --dur:      180ms;
  --dur-slow: 320ms;
}

/* Tailwind class-based dark mode is `.dark` (already configured). The
   styleguide also accepts `[data-theme="dark"]` so we mirror both. */
.dark,
[data-theme="dark"] {
  --bg-app:      #0C0C0B;
  --bg-card:     #161614;
  --bg-sunken:   #0F0F0E;
  --bg-elevated: #1C1C1A;
  --bg-inverted: #FAFAF9;

  --border-subtle: #232320;
  --border:        #2D2D2A;
  --border-strong: #3A3A36;

  --text-primary:   #FAFAF9;
  --text-secondary: #B6B5B0;
  --text-tertiary:  #8E8D87;

  --ink-25:  #1C1C1A;
  --ink-50:  #232320;
  --ink-100: #2D2D2A;
  --ink-150: #3A3A36;

  --success-bg: #0F2A22;
  --warning-bg: #2A1F0A;
  --danger-bg:  #2A1414;
  --info-bg:    #0F1A2E;

  /* Lift the semantic foregrounds in dark mode so they read on the dark
     soft surfaces above. The light-mode values (#156D50 etc.) sit too close
     to their dark backgrounds — these lighter hues mirror the kpi_hero
     delta colours (#7BD3A8 / #F09A9A) that the styleguide already uses on
     dark surfaces. */
  --success: #7BD3A8;
  --warning: #E5B96A;
  --danger:  #F09A9A;
  --info:    #7DA8E5;

  /* Brand-foreground for soft pills (`bg-brand-50` text). brand-600 is too
     dark on the dark-mode brand-50 fill; brand-300 is the styleguide's
     "brand on dark" choice (see hero-eyebrow override). */
  --brand-fg: #4F9F8E;
}

/* Force light mode when [data-theme="light"] is set — public-facing pages
   (storefront, OTP login, doctor booking) opt out of theme switching so the
   tenant's brand renders consistently regardless of OS / dashboard preference.
   Placed after the .dark block so it wins on equal specificity. */
[data-theme="light"] {
  --bg-app:      #F7F7F5;
  --bg-card:     #FFFFFF;
  --bg-sunken:   #F2F2EF;
  --bg-elevated: #FFFFFF;
  --bg-inverted: #131311;

  --border-subtle: #ECECE9;
  --border:        #E2E2DE;
  --border-strong: #CFCFCA;

  --text-primary:   #131311;
  --text-secondary: #50504B;
  --text-tertiary:  #8E8D87;

  --ink-25:  #FAFAF9;
  --ink-50:  #F5F5F4;
  --ink-100: #EDEDEB;
  --ink-150: #E2E2DF;

  --success:    #156D50;
  --success-bg: #E5F1EC;
  --warning:    #8A5A0B;
  --warning-bg: #FBF3DF;
  --danger:     #B33636;
  --danger-bg:  #FBE9E9;
  --info:       #1F4F94;
  --info-bg:    #E8EFFB;

  --brand-fg: #0A4D43;
}

/* Anchor jump fix used by section/tab targets */
[id^="tt-sec-"] { scroll-margin-top: 80px; }

/* The body opt-in class — applied on layouts that should pick up the new
   design system. Existing pages still render against Tailwind defaults. */
body.tt-app {
  font-family: var(--font-sans);
  font-feature-settings: "ss01", "cv11";
  background: var(--bg-app);
  color: var(--text-primary);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* =============================================================
   Rx composer — dictation "recording" animation (Contract B).
   Decorative only (no audio analysis): expanding rings around the
   Dictate button + a small equalizer. Served here (this stylesheet
   is linked directly on the tenant layout) so the keyframes don't
   depend on the Tailwind build. Toggled from rx_composer_controller
   by showing/hiding the containers; gated under prefers-reduced-motion.
   ============================================================= */
@keyframes rx-ping {
  0%        { transform: scale(1);   opacity: 0.55; }
  80%, 100% { transform: scale(1.9); opacity: 0; }
}
.rx-pulse-ring {
  position: absolute;
  inset: 0;
  border-radius: 0.75rem;
  border: 2px solid var(--danger);
  animation: rx-ping 1.4s cubic-bezier(0, 0, 0.2, 1) infinite;
  pointer-events: none;
}
.rx-pulse-ring--delay { animation-delay: 0.7s; }

@keyframes rx-eq {
  0%, 100% { transform: scaleY(0.32); }
  50%      { transform: scaleY(1); }
}
.rx-eq-bar {
  width: 3px;
  height: 14px;
  border-radius: 9999px;
  background: currentColor;
  transform-origin: bottom;
  animation: rx-eq 0.9s ease-in-out infinite;
}
.rx-eq-bar:nth-child(1) { animation-delay: 0s; }
.rx-eq-bar:nth-child(2) { animation-delay: 0.15s; }
.rx-eq-bar:nth-child(3) { animation-delay: 0.30s; }
.rx-eq-bar:nth-child(4) { animation-delay: 0.45s; }
.rx-eq-bar:nth-child(5) { animation-delay: 0.60s; }

@media (prefers-reduced-motion: reduce) {
  .rx-pulse-ring { animation: none; opacity: 0; }
  .rx-eq-bar { animation: none; transform: scaleY(0.6); }
}

/* =============================================================
   Rx composer — numbered medicine rows (Task 7, screen 2a).
   The rows are cloned from a <template> at runtime, so the leading
   "1." "2." index is a CSS counter on the items container rather than
   a JS-set value (keeps the Stimulus row template free of index
   bookkeeping). rx-med-items resets, each rx-med-row increments, and
   the .rx-med-row-num span renders the current count.
   ============================================================= */
.rx-med-items { counter-reset: rx-med; }
.rx-med-row { counter-increment: rx-med; }
.rx-med-row-num::before { content: counter(rx-med) "."; }

/* Task 9: the investigations / lab-tests list uses the same CSS-counter numbering as the
   medicine rows (rows are cloned from a <template>, so the "1." "2." index is a container
   counter rather than a JS-set value). */
.rx-inv-items { counter-reset: rx-inv; }
.rx-inv-row { counter-increment: rx-inv; }
.rx-inv-row-num::before { content: counter(rx-inv) "."; }

/* =============================================================
   Scroll chaining -> browser history gesture.  (PARTIAL mitigation.)

   A horizontal scroll that reaches the end of a scroll container chains
   the remainder to the document by default, and Chrome/Safari on macOS
   turn a document-level horizontal overscroll into a Back/Forward
   navigation. `contain` keeps the scroll inside the container: the
   element still scrolls normally, it just stops chaining outward.

   Read the limits before trusting this:
     * It only covers scrolls that BEGIN inside a wrapped container. A
       two-finger swipe started over the sidebar, the header or a stat
       card reaches the root scroller directly and still fires the
       gesture.
     * Safari does not honour overscroll-behavior for its swipe-back
       gesture at all, so no CSS-only fix is complete there.
     * Coverage is incomplete by construction: 25 views under
       app/views/tenant/ use .overflow-x-auto, but 34 contain a <table>
       — clinic_visits/today, broadcasts/edit, abdm/counter/show and
       patient/appointments/reschedule_no_show have no wrapper.
     * treatlly_design.css is linked by five layouts (tenant, admin,
       embed, doctor_public, otp), so this lands on all of them. That is
       intended — containing a table's horizontal scroll is desirable
       everywhere — but it is not scoped to the dashboard.

   On the 2026-08-31 report: the Caddy access log proves the tab DID
   traverse its history (the document url changed with zero requests,
   which only a Turbo `restore` can do). It does NOT identify the gesture
   that triggered it — a trackpad swipe, a mouse side-button and Cmd-Left
   leave an identical trace. Treat this rule as removing one plausible
   contributor, not as the fix for that incident; the two proven causes
   were prefetch-served clicks and one history entry per keystroke.
   ============================================================= */
.overflow-x-auto,
.overflow-x-scroll {
  overscroll-behavior-x: contain;
}

/* =============================================================
   Hero background video — respect a reduced-motion preference.

   `prefers-reduced-motion: reduce` is set by people for whom moving
   imagery causes real symptoms (vestibular disorders, migraine
   triggers). A full-bleed autoplaying video behind a booking form is
   exactly the pattern the preference exists to suppress, and this is a
   healthcare site where those visitors are over-represented.

   The hero renders the background <img> ALWAYS and layers the video
   over it (see any theme's sections/hero.liquid), so hiding the video
   uncovers a real image — the still frame the clinic chose, not an
   empty box. That layering exists FOR this rule; an either/or render
   would leave these visitors with nothing.

   `display:none` rather than pausing, because a paused video still
   shows its first frame with a play affordance on some mobile
   browsers.
   ============================================================= */
@media (prefers-reduced-motion: reduce) {
  .hero-bg-video { display: none; }
}

/* ── Row action bar (2026-09-14) ────────────────────────────────────────────────────────────
   The appointment-shaped tables (staff appointments, doctor dashboard, check-in queue) carry up
   to eight actions per row. Behind a ⋯ menu every one of them was two clicks and their tones
   flattened to danger-or-nothing; as a row of buttons they pushed the table past the width of a
   clinic laptop. So they live in a bar that appears over the row, and the row's own content dims
   behind a scrim while it is open.

   DESKTOP IS PURE CSS — hover and focus-within. No JavaScript is involved on a laptop, so the bar
   cannot get stuck open if Stimulus fails to boot, and a keyboard reaches every button by tabbing
   into the row. `row_actions_controller.js` adds ONLY the touch case, which CSS cannot express.

   `position: relative` on the <tr> is what lets the scrim cover the whole row from inside one
   <td>; see the note in shared/_row_action_bar.html.erb. */
.tt-row-actions-host {
  position: relative;
}

/* The row is focusable (see ApplicationHelper#row_action_attrs) so `:focus-within` has something
   to fire from. Give it a visible ring, or a keyboard user cannot tell which row they are on. */
.tt-row-actions-host:focus-visible {
  outline: 2px solid var(--color-brand-500, #0E5F52);
  outline-offset: -2px;
}

/* The scrim + bar. Hidden by default; `flex` is what the states below switch on. */
.tt-row-actions {
  background: color-mix(in srgb, var(--bg-card) 86%, transparent);
  -webkit-backdrop-filter: blur(1px);
  backdrop-filter: blur(1px);
}

/* The scrim must be display:none here, not only via the partial's Tailwind `hidden` — otherwise a
   caller that forgets that utility gets a permanently open sheet over its row. */
.tt-row-actions {
  display: none;
}

/* ⛔ KEYBOARD: the row itself, or something INSIDE the bar — never an arbitrary control in the row.
   This was `:focus-within` on the whole host, which fires from ANY focusable descendant: focusing
   the reference-number link, the bulk-select checkbox or the Reassign token input opened the bar
   ON TOP of the very control that had focus, washing out its focus ring. That is a WCAG 2.4.7 /
   2.4.11 failure, and it hit the reference link this pattern relies on as its keyboard route to the
   appointment. Found by an adversarial pass measuring paint order in a browser, not by a spec.
     :focus              — the <tr> (tabindex=0) has focus: the deliberate "show me the actions".
     :has(… :focus)      — focus is inside the bar: keep it open while tabbing through the buttons.
     .tt-row-actions-open — the touch/JS path.
   `:has()` is the no-JS keyboard route; row_actions_controller also sets the class on focusin, so
   a browser without :has still works.

   ⛔ NESTING LEAK — `:not(.tt-row-actions *)` STOPS BAR-IN-BAR ONLY, NOT HOST-IN-HOST (C1 fix,
   2026-09-16 round 2 claimed this was closed generally; a round-3 adversarial pass, reproduced with
   CDP-dispatched hover against the real compiled stylesheet, proved it was not). Every selector
   below is an unscoped ANCESTOR → descendant rule — `.tt-row-actions-host:has(...) .tt-row-actions`
   reveals EVERY `.tt-row-actions` under that host, not only its own. `:not(.tt-row-actions *)`
   excludes a target that is itself a descendant of ANOTHER `.tt-row-actions` — i.e. a bar literally
   nested inside another bar's DOM — so it does stop THAT shape of leak. It does NOTHING for a HOST
   nested inside another HOST: two separate `.tt-row-actions-host` elements at different depths,
   each with its own (non-nested) bar. Round 2 moved the patient intake dialog to render as a
   SIBLING of `.tt-row-actions` instead of as its `content:` item — that fixed a real display:none /
   scroll-lock bug, but the dialog's own document rows (`_intake_dialog.html.erb`) were STILL each
   their own `.tt-row-actions-host`, nested inside the appointment row's host (the <tr> in
   index.html.erb) — a host-in-host shape this guard was never built for. Hovering the dialog's own
   heading, or a document's own label, revealed every document's Remove button superimposed and
   unlabelled: a patient could not tell which button deleted which upload.
   Fix (round 3): the leak is closed at the MARKUP, not the selector — `_intake_dialog.html.erb`'s
   document rows are no longer `.tt-row-actions-host` at all; View/Remove render as two
   always-visible buttons, not a hover-reveal bar. "Reveal only via the nearest host" cannot be
   expressed as a plain unscoped descendant selector — CSS has no "nearest ancestor" combinator —
   so the real invariant this whole file depends on is: a `.tt-row-actions-host` must never nest
   inside another `.tt-row-actions-host`, anywhere. That is what makes "reveal every
   `.tt-row-actions` under this host" equivalent to "reveal only this host's own bar" across the
   ~59 converted surfaces. Verified by walking the rendered DOM in
   spec/requests/tenant/patient/appointments_intake_modal_spec.rb, not by reasoning about the CSS.
   The `:not()` guard below is still correct for the bar-in-bar shape and stays for that reason. A
   `:not()` guard rather than a child combinator (`>`) because the bar sits at varying depths under
   its host across the ~60 converted files (direct child of an `<li>`, one level down inside a
   `<td>`, …) — `:not()` is depth-agnostic and only cares whether ANOTHER bar intervenes. */
.tt-row-actions-host:focus > * .tt-row-actions:not(.tt-row-actions *),
.tt-row-actions-host:focus .tt-row-actions:not(.tt-row-actions *),
.tt-row-actions-host:has(.tt-row-actions :focus) .tt-row-actions:not(.tt-row-actions *),
.tt-row-actions-host.tt-row-actions-open .tt-row-actions:not(.tt-row-actions *) {
  display: flex;
}

/* Mouse only. Guarded so a touch screen — where :hover either never fires or sticks after a tap —
   never opens the bar by hover; there it is opened by the controller's tap handler instead.
   Same nesting guard as above — see the comment there. */
@media (hover: hover) and (pointer: fine) {
  .tt-row-actions-host:hover .tt-row-actions:not(.tt-row-actions *) {
    display: flex;
  }
  .tt-row-actions-host:hover {
    cursor: default;
  }
}

/* A touch row advertises that tapping does something, now that the row click no longer navigates. */
@media (hover: none) {
  .tt-row-actions-host {
    cursor: pointer;
  }
}

/* No transition: the bar toggles `display`, which cannot animate. An `opacity` transition here was
   dead code whose own comment contradicted it. */
