@import "tailwindcss" source("../.."); @import "tw-animate-css"; @import "shadcn/tailwind.css"; /* * Exclude planning + docs directories from Tailwind v4's content scan. * * v4's scanner walks the entire project root by default and picks any * class-shaped token out of every text file. Planning reports and * design docs often quote literal class-name fragments such as * `bg-[var(...)]/12` as examples, and the bracketed-CSS-variable form * combined with an opacity modifier generates an escaped selector that * Turbopack's CSS parser rejects with `Unexpected token Delim('.')`, * 500-ing every page. Constraining the scan to source code avoids * fixture-style strings poisoning the generated CSS. * * The `source("../..")` argument on the `@import "tailwindcss"` line * above pins auto-detection to the project root (resolved from this * stylesheet) instead of letting Tailwind walk up from whatever cwd * the build tool happens to be sitting at. That mirrors the * cwd-anchored fix the safety-contracts loader landed in * commit `dd44a0d` and stops the planning / docs directories from * leaking class-shaped fragments into the generated CSS regardless of * whether the build runs from the repo root or a tooling subdir. * * The `@source not` directives below are resolved relative to this * stylesheet, so they point at `/.planning` and * `/docs`. */ @source not "../../.planning"; @source not "../../docs"; @custom-variant dark (&:is(.dark *)); @theme inline { /* Extra-small breakpoint (30rem / 480px) for the narrowest phones — the default scale starts at sm (640px), which leaves a gap between an iPhone SE and a small tablet where two-up grids of fixed-width content overflow. */ --breakpoint-xs: 30rem; --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-sans); --font-mono: var(--font-mono); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent: var(--sidebar-accent); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar: var(--sidebar); --color-chart-5: var(--chart-5); --color-chart-4: var(--chart-4); --color-chart-3: var(--chart-3); --color-chart-2: var(--chart-2); --color-chart-1: var(--chart-1); --color-ring: var(--ring); --color-input: var(--input); --color-border: var(--border); --color-destructive: var(--destructive); --color-accent-foreground: var(--accent-foreground); --color-accent: var(--accent); --color-muted-foreground: var(--muted-foreground); --color-muted: var(--muted); --color-tab-inactive: var(--tab-inactive); --color-secondary-foreground: var(--secondary-foreground); --color-secondary: var(--secondary); --color-primary-foreground: var(--primary-foreground); --color-primary: var(--primary); --color-popover-foreground: var(--popover-foreground); --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --radius-2xl: calc(var(--radius) + 8px); --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); /* Dracula semantic colors for charts */ --color-dracula-purple: var(--dracula-purple); --color-dracula-pink: var(--dracula-pink); --color-dracula-green: var(--dracula-green); --color-dracula-cyan: var(--dracula-cyan); --color-dracula-orange: var(--dracula-orange); --color-dracula-red: var(--dracula-red); --color-dracula-yellow: var(--dracula-yellow); --color-dracula-lavender: var(--dracula-lavender); /* * Semantic feedback tokens — aliases over the Dracula palette so badges, * banners, and status pills can stop hard-coding `text-dracula-green` / * `bg-dracula-orange/15` and ask for the *meaning* instead. Each is a * straight alias of the existing colour at the time of introduction so * the visual stays pixel-identical to v1.4.6 — what changes is intent. * - success → dracula-green (positive outcome, e.g. published) * - warning → dracula-orange (caution / non-blocking attention) * - info → dracula-cyan (neutral informational note) */ --color-success: var(--success); --color-success-foreground: var(--success-foreground); --color-warning: var(--warning); --color-info: var(--info); /* * Brand accent tokens — identity colours with no semantic (status) * twin. `--brand-pink` is the Coach-gradient / Apple-Health accent * (`from-primary to-brand-pink`, the iOS-source badge, the recording * mic). Per-theme values ride the `--dracula-pink` primitive, which * the light theme overrides to its AA-tuned counterpart: * dark #ff79c6 — 6.86:1 card / 5.97:1 bg; light #a3134d — 7.32:1 * card / 6.83:1 bg (WCAG relative-luminance, computed). */ --color-brand-pink: var(--brand-pink); /* * Status-pill text tokens. The target status pill paints coloured text * on a 10% same-hue tint; the raw Dracula green/orange/red would fail * WCAG AA (4.5:1) for the "out" tone on the muted reference panel in * dark mode and for every tone on a white card in light mode. These * theme-aware tokens land each tone at or above AA on both `bg-card` * and `bg-muted/30` in both themes while keeping the Dracula feel. */ --color-status-pill-in: var(--status-pill-in); --color-status-pill-near: var(--status-pill-near); --color-status-pill-out: var(--status-pill-out); /* * Dose-accent token. The medication cards stamp the upcoming schedule * dose in purple. They previously hard-coded Tailwind stock * `text-purple-400` (`oklch(0.714 0.203 305.504)`), which reads ~3.1:1 on * the white Alucard card and fails WCAG AA (4.5:1) for body text. This * theme-aware token keeps the exact dark-mode purple and swaps to a darker * AA-safe purple on the light card — mirroring how the semantic feedback * tokens (`--success` / `--warning` / `--info`) land each tone above AA in * both themes. */ --color-dose-accent: var(--dose-accent); } /* * Dracula Theme * Based on https://draculatheme.com/contribute * Mapped to shadcn/ui CSS variable system */ /* ── Dracula palette (shared) ────────────────────── */ :root { --dracula-bg: #282a36; --dracula-current: #44475a; --dracula-fg: #f8f8f2; --dracula-comment: #9aa3b3; --dracula-cyan: #8be9fd; --dracula-green: #50fa7b; --dracula-orange: #ffb86c; --dracula-pink: #ff79c6; --dracula-purple: #bd93f9; --dracula-red: #ff5555; --dracula-yellow: #f1fa8c; --dracula-lavender: #d6acff; /* * v1.28.17 — the sleep-stage IN_BED fill. Distinct from `--dracula-comment` * above (an orphaned token — no `@theme` utility was ever generated for * it, per the `confidence-badge.ts` note — and its `#9aa3b3` value never * matched the classic Dracula "Comment" blue-grey this chart actually * paints). Minted here instead of retuning the unrelated token, so this * default carries the exact hex the stacked bar hardcoded before * (`#6272a4`) and the light override below (`:root.light`) is this * token's own AA-tuned counterpart. */ --chart-inbed: #6272a4; /* * Semantic feedback tokens (see @theme `--color-success` / `--color-warning` * / `--color-info` re-exports). Aliased over the Dracula palette so existing * dark-mode visuals stay identical; light mode (Alucard) below provides the * higher-contrast counterpart so AA contrast lands on the white card. */ --success: var(--dracula-green); --warning: var(--dracula-orange); --info: var(--dracula-cyan); /* * Brand accent (see the @theme re-export). Aliased over the pink * primitive so the light theme's AA-tuned `--dracula-pink` override * carries through automatically — pixel-identical to the raw utility * sites it replaced. */ --brand-pink: var(--dracula-pink); /* * Foreground for glyphs/labels painted on a solid `bg-success` fill. The * raw Dracula green (`#50fa7b`) is a bright tone, so white-on-green reads * ~1.37:1 in dark mode — well under AA. The dark theme below pairs it with * the near-black Dracula background (`#282a36`, ~10.4:1); the light theme * keeps white on the darker light-mode green (`#14720a`, ~6.1:1). Mirrors * the `--primary-foreground` pattern. Default here keeps the dark-theme * value so an un-themed root never falls back to an unreadable pairing. */ --success-foreground: #282a36; --radius: 0.625rem; } /* ── Dark mode (default) ─────────────────────────── */ :root.dark { --background: #282a36; --foreground: #f8f8f2; --card: #1e1f29; --card-foreground: #f8f8f2; --popover: #1e1f29; --popover-foreground: #f8f8f2; --primary: #bd93f9; --primary-foreground: #282a36; --secondary: #44475a; --secondary-foreground: #f8f8f2; --muted: #44475a; --muted-foreground: #9aa3b3; /* Inactive tab-trigger text. `--muted-foreground` (#9aa3b3) on the * muted (#44475a) tab-strip reads 3.6:1 — below WCAG AA. This brighter * tone clears 4.5:1 on the muted strip while staying within the Dracula * palette; the active trigger keeps `--foreground`. */ --tab-inactive: #b3bccd; /* NativeSelect chevron — a data-URI background-image cannot resolve * `currentColor` (it falls back to black per the SVG spec), so the * muted-foreground value is baked into the stroke per theme. Keep in * sync with `--muted-foreground` above. */ --native-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239aa3b3' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); --accent: #44475a; --accent-foreground: #f8f8f2; --destructive: #ff5555; --border: #44475a; --input: #44475a; --ring: #bd93f9; --chart-1: #bd93f9; --chart-2: #50fa7b; --chart-3: #ff79c6; --chart-4: #8be9fd; --chart-5: #ffb86c; --sidebar: #1e1f29; --sidebar-foreground: #f8f8f2; --sidebar-primary: #bd93f9; --sidebar-primary-foreground: #f8f8f2; --sidebar-accent: #44475a; --sidebar-accent-foreground: #f8f8f2; --sidebar-border: #44475a; --sidebar-ring: #bd93f9; /* * Status-pill text in dark mode. "in" / "near" keep the Dracula * green / orange (both clear AA on the muted panel); "out" lifts off * the raw `#ff5555`, which lands at 3.99:1 on `bg-muted/30`, to a * lighter Dracula red that clears 4.5:1 on both card and panel. */ --status-pill-in: #50fa7b; --status-pill-near: #ffb86c; --status-pill-out: #ff8a8a; /* Dark glyph on the bright Dracula-green fill — ~10.4:1 on `#50fa7b`. */ --success-foreground: #282a36; /* * Dose accent in dark mode — the exact Tailwind `text-purple-400` * (`oklch(0.714 0.203 305.504)`) the cards used before, so the dark card * stays pixel-identical. AA on the dark card at ~6.8:1. */ --dose-accent: oklch(0.714 0.203 305.504); /* * v1.18.7 — wellness-ring per-metric arc colours (Dracula dark), CALM pass. * Five DISTINCT but MUTED single-hue arcs: readiness green · recovery cyan · * sleep purple · stress orange · strain pink. The earlier set ran a * dark→neon two-stop sweep (`to` == the raw Dracula palette colour) plus a * bloom + sheen, which read as glowing/3-D against the flat Recharts charts. * The arc is now a near-flat single tone per hue: `from`/`to` sit a hair * apart for a whisper of depth, both pulled down from the neon palette and * desaturated toward the chart colours, so the ring reads as quiet as a * chart series. The Alucard light set lives on `:root.light`. */ --ring-readiness-from: #3fb86b; --ring-readiness-to: #46c476; --tile-readiness: #50fa7b; --ring-recovery-from: #4fb6cc; --ring-recovery-to: #58c0d6; --tile-recovery: #8be9fd; --ring-sleep-from: #9a73da; --ring-sleep-to: #a37fe0; --tile-sleep: #bd93f9; --ring-stress-from: #e09a52; --ring-stress-to: #e8a55e; --tile-stress: #ffb86c; --ring-strain-from: #e06aa8; --ring-strain-to: #ea74b1; --tile-strain: #ff79c6; /* Band fallback (anatomy detail view, no per-metric hue). Lands on the app-wide --success green (#50fa7b) so a "green" ring matches the green of every positive signal / trend delta beside it. */ --ring-green-from: #35d76a; --ring-green-to: #50fa7b; --ring-yellow-from: #e0a050; --ring-yellow-to: #e8ab5c; --ring-red-from: #e0594a; --ring-red-to: #e96456; /* * v1.15.0 — cycle-phase ring hues (Dracula dark). Four calm, distinct * tones for the cycle wheel: menstrual rose · follicular green · ovulatory * amber-gold · luteal violet. Tuned to read as a soft instrument, never a * gaudy rainbow — the same restraint as the wellness rings above. */ --cycle-phase-menstrual: #ff6e9c; --cycle-phase-follicular: #5cf08f; --cycle-phase-ovulatory: #ffce6b; --cycle-phase-luteal: #bd93f9; color-scheme: dark; } /* ── Light mode (Alucard — official Dracula light) ── */ :root.light { /* * Neutral spine (retuned v1.26.0). Softened off the pure-white / pure-black * high-contrast set — a faint mauve tint (hue ~295–300) runs through every * neutral so the light theme reads gentle rather than harsh. Foreground is a * tinted near-black (not #000), dropping fg-on-card from ~16.5:1 to a * comfortable ~13:1. Accent + semantic tokens below stay as the Alucard * AA-tuned set. oklch throughout so lightness steps are perceptually even. */ --background: oklch(0.964 0.004 300); --foreground: oklch(0.3 0.012 295); --card: oklch(0.988 0.003 300); --card-foreground: oklch(0.3 0.012 295); --popover: oklch(0.992 0.002 300); --popover-foreground: oklch(0.3 0.012 295); --primary: #644ac9; --primary-foreground: oklch(0.99 0.002 300); --secondary: oklch(0.95 0.008 300); --secondary-foreground: oklch(0.3 0.012 295); --muted: oklch(0.955 0.006 300); --muted-foreground: oklch(0.515 0.02 295); /* Inactive tab-trigger text — the light muted-foreground clears AA on the * light muted strip, so the token mirrors it. */ --tab-inactive: oklch(0.515 0.02 295); /* Light-theme counterpart of the NativeSelect chevron (see dark block). * Stroke tracks the muted-foreground hex (#686572). */ --native-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23686572' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); --accent: oklch(0.95 0.008 300); --accent-foreground: oklch(0.3 0.012 295); --destructive: #cb3a2a; /* * Alucard light-mode overrides for the semantic tokens — straight Dracula * green/orange/cyan would fail AA contrast on a white card. These match the * darker palette already used for `--destructive`/`--chart-*` in light mode. */ --success: #14720a; --warning: #a34d14; --info: #036a96; /* White glyph on the darker light-mode green — ~6.1:1 on `#14720a`. */ --success-foreground: #ffffff; --border: oklch(0.885 0.008 300); --input: oklch(0.87 0.009 300); --ring: oklch(0.6 0.13 292); --chart-1: #644ac9; --chart-2: #14720a; --chart-3: #a3134d; --chart-4: #036a96; --chart-5: #a34d14; /* * Light-mode (Alucard) overrides for the raw `--dracula-*` primitives. * The semantic sweep retired every `text-dracula-*` / `bg-dracula-*` * UTILITY site (lint-enforced via healthlog/no-dracula-utility), but * chart code still passes `var(--dracula-*)` token references * (Recharts stroke/fill maps) and the Coach gradient CSS below reads * the purple/pink primitives — without an override the dark neons * render on the white card at 1.2–1.6:1. Each value below is tuned to * the AA light palette already in this block (same hue family, * measured >=4.5:1 on both `--card` oklch(0.988 0.003 300) and * `--background` oklch(0.964 0.004 300)). */ --dracula-cyan: #036a96; /* shadows --info — 5.78:1 card / 5.39:1 bg */ --dracula-green: #14720a; /* shadows --success — 5.89:1 card / 5.49:1 bg */ --dracula-orange: #a34d14; /* shadows --warning — 5.59:1 card / 5.21:1 bg */ --dracula-pink: #a3134d; /* shadows --chart-3 — 7.35:1 card / 6.85:1 bg */ --dracula-purple: #644ac9; /* shadows --primary/chart-1 — 6.03:1 card / 5.62:1 bg */ --dracula-red: #cb3a2a; /* shadows --destructive — 4.85:1 card / 4.52:1 bg */ --dracula-yellow: #846e15; /* Lavender has no semantic twin; a violet one step lighter than purple, * AA-tuned — 4.97:1 card / 4.63:1 bg. */ --dracula-lavender: #7c50e0; /* IN_BED sleep-stage fill (see the `:root` default above) — same * blue-grey hue family, darkened for AA — 6.66:1 card / 6.03:1 bg. */ --chart-inbed: #4a5b8a; --sidebar: oklch(0.988 0.003 300); --sidebar-foreground: oklch(0.3 0.012 295); --sidebar-primary: #644ac9; --sidebar-primary-foreground: oklch(0.99 0.002 300); --sidebar-accent: oklch(0.95 0.008 300); --sidebar-accent-foreground: oklch(0.3 0.012 295); --sidebar-border: oklch(0.885 0.008 300); --sidebar-ring: oklch(0.6 0.13 292); /* * v1.18.7 — wellness-ring per-metric arc colours (Alucard light), CALM pass. * Deep + saturated enough to hold AA presence on the white `--card` (raw * Dracula cyan/green wash out on white), but the dark→light two-stop SWEEP * is collapsed to a near-flat single tone (`from`/`to` a hair apart) so the * arc reads as quiet as a chart series rather than a glowing gradient. Same * five hue families as the dark set: readiness green · recovery cyan · sleep * purple · stress orange · strain pink. */ --ring-readiness-from: #138043; --ring-readiness-to: #168a48; --tile-readiness: #14720a; --ring-recovery-from: #07849c; --ring-recovery-to: #098ba3; --tile-recovery: #08869e; --ring-sleep-from: #6638c2; --ring-sleep-to: #6c3fc8; --tile-sleep: #6233c0; --ring-stress-from: #b27018; --ring-stress-to: #b8771c; --tile-stress: #a34d14; --ring-strain-from: #b62478; --ring-strain-to: #bd2a7e; --tile-strain: #b0277d; /* Light: lands on the light-theme --success (#14720a) so ring green matches every positive signal beside it. */ --ring-green-from: #12760f; --ring-green-to: #14720a; --ring-yellow-from: #b27018; --ring-yellow-to: #bd7a1e; --ring-red-from: #b22e20; --ring-red-to: #ba3526; /* * v1.15.0 — cycle-phase ring hues (Alucard light). Deeper + more * saturated than the dark set so each phase holds AA-grade presence on the * white card. Same four distinct hues: menstrual rose · follicular green · * ovulatory amber · luteal violet. */ --cycle-phase-menstrual: #c8295e; --cycle-phase-follicular: #0f7a3a; --cycle-phase-ovulatory: #a34d14; --cycle-phase-luteal: #644ac9; /* * Status-pill text in light mode (Alucard). The bright Dracula * green / orange / red read 1.2–2.7:1 on a near-white tint; these * darker tones — the same family already used for `--success` / * `--warning` / the light-mode reds — clear AA on both the white card * and the muted reference panel. */ --status-pill-in: #14720a; --status-pill-near: #a34d14; --status-pill-out: #b32d1e; /* * Dose accent in light mode (Alucard). The bright `text-purple-400` reads * ~3.1:1 on the white card and fails AA; this darker purple — the same * family as the light-mode `--primary` (#644ac9) — clears 4.5:1 on both * the white card and the muted panel while keeping the purple identity. */ --dose-accent: #6f42c1; color-scheme: light; } @layer base { * { @apply border-border outline-ring/50; } /* * Reserve the scrollbar gutter on the document root ONLY for the * body-scrolled shells (login / register / onboarding / standalone legal * pages), where the document itself overflows. Those keep a fixed content * width whether or not the page paints the bar, so any `mx-auto` content * does not recentre a few px sideways on navigation. `AuthShell` sets * `data-scroll="body"` on `` for exactly those branches. * * The authenticated app shell is height-locked (`flex h-dvh`) and scrolls * inside its own `
`, which reserves its own gutter. Applying this * rule globally there reserved a SECOND, never-painted gutter on classic * (non-overlay) scrollbar platforms (Windows / Linux) — a ~15px dead band. * Scoping it to the body-scrolled branch leaves exactly one gutter per * branch. */ html[data-scroll="body"] { scrollbar-gutter: stable; } body { @apply bg-background text-foreground; } p { color: var(--foreground); } } /* * `.no-scrollbar` — keeps a container scrollable but hides the visible * bar. Used by the mobile section strips in `` and * `` so 13 admin / 10 settings entries can swipe * horizontally without painting an always-on scrollbar at the top of * every admin/settings page (a recurring maintainer complaint — most * recently mistaken for a `/admin/api-tokens` table scrollbar in * v1.4.16/17 because the strip sits right above the api-tokens card). * * Standard `scrollbar-width: none` (Firefox) + `::-webkit-scrollbar` * (Chromium/WebKit) covers every browser HealthLog targets. Touch * scrolling, keyboard arrow scrolling, and `Tab` focus traversal all * keep working — this is a purely cosmetic suppression, not an * `overflow: hidden`. */ .no-scrollbar { scrollbar-width: none; -ms-overflow-style: none; } .no-scrollbar::-webkit-scrollbar { display: none; } /* * `.scroll-touch` — momentum scrolling for horizontal scrollers on iOS * WebKit. `-webkit-overflow-scrolling: touch` restores the inertial * flick the default `overflow-x-auto` row loses on touch; `overscroll- * behavior: contain` keeps a flick that reaches the end of the row from * chaining into the document scroll (the "rubber-band drags the whole * page" paper-cut). Applied to the in-app horizontal pill / tab strips * (insights tab strip, cycle tab list) so the gesture matches the * native feel the bottom-sheet scrollers already get for free. */ .scroll-touch { -webkit-overflow-scrolling: touch; overscroll-behavior: contain; } /* ── Insight card animations ─────────────────────── */ @keyframes insight-fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .animate-insight-in { animation: insight-fade-in 0.4s ease-out forwards; } @media (prefers-reduced-motion: reduce) { .animate-insight-in { animation: none; } } /* ── v1.4.20 phase B1 — Insights redesign hero strip ─────────────── */ /* * `.hero-gradient` — purple → pink linear gradient at low opacity, for * the new band on /insights. Sits above `var(--card)` so * the band reads as a richer surface than the card it overlays. * * Matches the design handoff (`prototype/artboard-fullpage.jsx → * BriefingHero` background) — purple/pink at ~12% opacity over the * card colour, with a subtle border-tint to anchor it on the dark bg. */ .hero-gradient { background: linear-gradient( 135deg, color-mix(in srgb, var(--dracula-purple) 18%, var(--card)) 0%, color-mix(in srgb, var(--dracula-pink) 12%, var(--card)) 100% ); border: 1px solid color-mix(in srgb, var(--dracula-purple) 24%, transparent); } /* * `.glow-purple` — soft purple aura behind the hero card, painted via * `box-shadow` so it stays GPU-cheap and respects the existing card * border radius without an extra wrapper element. */ .glow-purple { box-shadow: 0 0 0 1px color-mix(in srgb, var(--dracula-purple) 16%, transparent), 0 18px 48px -28px color-mix(in srgb, var(--dracula-purple) 50%, transparent); } /* * `.wellness-tile` — the v1.13.x premium-redesign wellness-tile surface. * * Replaces the old saturated `.score-tile-gradient` dark slab. Mirrors the * gentle, calm `.hero-gradient` the maintainer loves: a LOW-opacity mix of * the metric's `--tile-hue` over the *theme* `var(--card)` (not the fixed * dark bg), so the surface stays luminous in both Dracula dark + Alucard * light. The mix is even softer than the hero's 18/12 (14/7) so a *row* of * five tinted tiles never overwhelms the page. Each `RingTile` sets * `--tile-hue` inline per metric; the per-metric `hue` arc on the ring is * the only saturated element, letting the dial be the hero. * * Text rides the theme `--foreground` (dark in light mode, light in dark * mode), so AA is trivial over the `--card`-based surface in both themes — * the old white-pinned copy + white arc are gone. */ .wellness-tile { --tile-hue: var(--dracula-purple); position: relative; overflow: hidden; background: linear-gradient( 140deg, color-mix(in srgb, var(--tile-hue) 14%, var(--card)) 0%, color-mix(in srgb, var(--tile-hue) 7%, var(--card)) 100% ); border: 1px solid color-mix(in srgb, var(--tile-hue) 22%, transparent); box-shadow: 0 10px 30px -26px color-mix(in srgb, var(--tile-hue) 40%, transparent); transition: transform 200ms cubic-bezier(0.22, 0.61, 0.36, 1), box-shadow 200ms cubic-bezier(0.22, 0.61, 0.36, 1), border-color 200ms cubic-bezier(0.22, 0.61, 0.36, 1); } /* * Faint static film-grain over the tile — lifts the flat CSS gradient to a * "material" surface (the Oura touch). Inline SVG fractalNoise data-URI, zero * network cost, one paint, no animation (reduced-motion-safe by construction). * Opacity kept <=3% so it never reads as dirty on the white Alucard card. */ .wellness-tile::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); opacity: 0.025; mix-blend-mode: overlay; pointer-events: none; } /* * v1.15.12 (F1/F3) — `.wellness-detail-card` is the score-anatomy detail * surface, deepened to match the dashboard ring tile the user tapped. * * F1 — carries the SAME per-metric `--tile-hue` mix over the theme `--card` * as `.wellness-tile`, so the tap-through reads as the same surface * rather than a plain bordered card. The hue is set inline per metric. * F3 — a SUBTLE vertical gradient that lightens toward the BOTTOM: the top * carries a touch more of the hue (16%) and it eases to 6% at the foot, * giving the card a premium, lit-from-below depth. Theme-aware by * construction — both stops mix over the theme `--card`, so it stays * luminous on the white Alucard card and rich on the dark Dracula one. * Kept minimal (a 10pp swing) so it never reads as a heavy slab. * * Static, no hover lift (it is a page surface, not a tap target) — so it * needs no reduced-motion handling. */ .wellness-detail-card { --tile-hue: var(--dracula-purple); position: relative; /* The bottom-leaning gradient below is the card's OWN `background`, which is * already clipped to `border-radius` — a painted background never bleeds past * the rounded corners regardless of `overflow`. We deliberately do NOT set * `overflow: hidden`: the ring's drop-shadow bloom (`.wellness-ring-arc`) is a * CHILD box, and `overflow: hidden` would clip that glow at the card edge. The * gradient stays corner-clipped; the glow renders uncut on all four sides. */ background: linear-gradient( 180deg, color-mix(in srgb, var(--tile-hue) 16%, var(--card)) 0%, color-mix(in srgb, var(--tile-hue) 6%, var(--card)) 100% ); border: 1px solid color-mix(in srgb, var(--tile-hue) 22%, transparent); box-shadow: 0 12px 34px -28px color-mix(in srgb, var(--tile-hue) 42%, transparent); } @media (hover: hover) { .wellness-tile:hover { transform: translateY(-2px); border-color: color-mix(in srgb, var(--tile-hue) 38%, transparent); box-shadow: 0 16px 40px -24px color-mix(in srgb, var(--tile-hue) 52%, transparent); } .wellness-tile:hover [data-slot="score-ring"] { transform: scale(1.015); } } .wellness-tile:active { transform: translateY(0) scale(0.985); } /* So the ring scales smoothly with the tile lift instead of snapping. */ .wellness-tile [data-slot="score-ring"] { transition: transform 200ms var(--ring-ease); } /* * v1.18.7 — the wellness-strip reveal, calmed. What remains is a quiet * left-to-right tile rise+fade (gated once per session via `data-revealed`) * and the arc's spring-fill sweep (in score-ring.tsx, `--ring-spring`). The * conic specular sheen + the bloom + the breathing pulse were removed so the * strip settles as flat as the surrounding charts. A single * prefers-reduced-motion block at the bottom zeroes the tile rise. */ :root { /* Sampled spring (stiffness ~120, damping ~14): overshoots ~6% then settles — the Oura/WHOOP fill feel. Falls back to the browser default on very old engines that ignore linear() (instant fill, not broken). */ --ring-spring: linear( 0, 0.06, 0.23, 0.46, 0.7, 0.89, 1.02, 1.06, 1.03, 1, 0.99, 1 ); --ring-ease: cubic-bezier(0.22, 0.61, 0.36, 1); } .wellness-tile-rise { opacity: 0; transform: translateY(10px); } [data-revealed="true"] .wellness-tile-rise { animation: wellnessTileRise 440ms var(--ring-ease) forwards; animation-delay: var(--reveal-delay, 0ms); } @keyframes wellnessTileRise { to { opacity: 1; transform: translateY(0); } } /* v1.18.7 — the conic specular SHEEN is retired. It swept a white highlight across the arc once on reveal, which was the most overtly "premium / 3-D" touch and clashed hardest with the flat charts. The class is kept as a no-op so the cycle-ring + score-ring call sites that still render the overlay div simply paint nothing. */ .wellness-ring-sheen { display: none; } /* v1.18.7 — the arc is FLAT by default. The earlier treatment painted a token-tinted drop-shadow bloom behind every arc plus a breathing pulse on the green-band ring; against the flat 2-D Recharts charts that read as a glowing 3-D dial. Both are gone — the arc is a plain stroke, the same weight a chart series carries. `.wellness-ring-arc--flat` is kept as an alias so the dashboard-hero call site (and any other) stays valid. */ .wellness-ring-arc, .wellness-ring-arc--flat, .wellness-ring-arc--pulse { filter: none; animation: none; } @media (prefers-reduced-motion: reduce) { .wellness-tile, .wellness-tile:hover, .wellness-tile:active, .wellness-tile [data-slot="score-ring"], .wellness-tile:hover [data-slot="score-ring"] { transform: none; transition: none; } .wellness-tile-rise, [data-revealed="true"] .wellness-tile-rise { opacity: 1; transform: none; animation: none; } } /* * `pulseDot` keyframes — the "live, refreshed N min ago" green dot in * the hero meta row. Used as `.pulse-dot { animation: pulseDot ... }`. * Reduced-motion users get a static dot via the prefers-reduced-motion * guard below. */ @keyframes pulseDot { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(0.85); } } .pulse-dot { animation: pulseDot 1.8s ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .pulse-dot { animation: none; } } /* * v1.15.0 — the cycle-ring "signature reveal". * * The phase dial earns the same premium signature as the wellness rings * (sweep + glow + sheen + count-up) without losing its own identity as a * four-phase dial: * 1. a clockwise CLOCK-WIPE draws the phase arcs on from the 12-o'clock * day-0 origin (the already carries `-rotate-90`, so the conic * origin lines up with both the arcs and the current-day marker); * 2. a phase-tinted bloom on the active arc (shared `.wellness-ring-arc`, * `--ring-glow` set inline to the active phase hue); * 3. the shared `.wellness-ring-sheen` specular pass + `.wellness-tile` * surface the strip already uses; * 4. a soft halo pulse on the current-day marker so "today" reads as alive. * All gated on the host's `data-revealed` (set once per session) so a * background calendar refetch never re-triggers the moment, and all zeroed * under prefers-reduced-motion. */ @property --cycle-wipe { syntax: ""; inherits: false; initial-value: 360deg; } .cycle-ring-reveal { -webkit-mask: conic-gradient(#000 var(--cycle-wipe), transparent 0); mask: conic-gradient(#000 var(--cycle-wipe), transparent 0); } [data-revealed="true"] .cycle-ring-reveal { animation: cycleRingWipe 1100ms var(--ring-ease) forwards; } @keyframes cycleRingWipe { from { --cycle-wipe: 0deg; } to { --cycle-wipe: 360deg; } } /* The current-day marker's halo — a few gentle pulses on reveal, then calm (a finite count, never an infinite repaint of the non-compositable ring). */ .cycle-ring-marker-halo { transform-box: fill-box; transform-origin: center; opacity: 0; } [data-revealed="true"] .cycle-ring-marker-halo { animation: cycleMarkerHalo 2.4s ease-in-out 950ms 3; } @keyframes cycleMarkerHalo { 0%, 100% { opacity: 0; transform: scale(1); } 50% { opacity: 0.45; transform: scale(1.9); } } @media (prefers-reduced-motion: reduce) { .cycle-ring-reveal, [data-revealed="true"] .cycle-ring-reveal { -webkit-mask: none; mask: none; animation: none; } .cycle-ring-marker-halo, [data-revealed="true"] .cycle-ring-marker-halo { animation: none; opacity: 0; } } /* * v1.15.1 — the cycle-length history chart reveal. * * The hand-rolled SVG bars grow up from the baseline on first paint, a calm * staggered rise (per-bar `--bar-delay`) that matches the wheel's 0-KB SVG * identity. Gated on the chart's own `data-animate="true"` (the view only sets * it when motion is allowed) so a background history refetch never replays it, * and zeroed entirely under prefers-reduced-motion (bars paint at full height). */ .cycle-history-bar { transform-box: fill-box; transform-origin: bottom; } [data-animate="true"] .cycle-history-bar { transform: scaleY(0); animation: cycleHistoryBarRise 520ms var(--ring-ease, ease-out) forwards; animation-delay: var(--bar-delay, 0ms); } @keyframes cycleHistoryBarRise { to { transform: scaleY(1); } } @media (prefers-reduced-motion: reduce) { [data-animate="true"] .cycle-history-bar { transform: none; animation: none; } } /* * ── v1.29.0 cohesion pass — the daily-value system's finishing layer ── * * Four small surface treatments, all expressed through the existing * token/motion vocabulary (no new palette, no new easing): * * `.today-hero-wash` — the ONE sanctioned Today atmosphere: a faint * `--primary` mix over the theme `--card` in the * `.wellness-tile` pattern, leaning toward the * ring corner. Softer than the insights * `.hero-gradient` (8% → 0 vs 18/12) so the * promoted hero reads as the calm first surface, * not a banner. Static — nothing to motion-gate. * `.metric-accent` — per-metric identity edge for rail cards and * cross-link pointers. Reads `--tile-hue` (set * inline from the SAME `--tile-*` vocabulary the * wellness tiles use; `--primary` for the med / * coach family) and paints a soft inset edge on * the reading side via a ::before overlay, so it * composes with the card's own `shadow-sm` and * the semantic status washes (which keep the * background). No layout width is added; the * inset shadow follows the border radius. * `.milestone-reached` — the S12 quiet celebration: a soft `--success` * halo over the theme `--card` plus a one-shot * spring reveal on the shared `--ring-spring` * curve. No confetti, no loop; reduced-motion * paints the settled card at once. * `.skeleton-shimmer` — the loading language upgrade: a single soft * `--foreground`-mix sweep across the muted * skeleton base instead of the whole-block * `animate-pulse` throb. Load-terminating by * construction (skeletons unmount with the data); * reduced-motion collapses to the static base. */ .today-hero-wash { background: radial-gradient( 120% 150% at 100% 0%, color-mix(in srgb, var(--primary) 8%, var(--card)) 0%, var(--card) 60% ); } .metric-accent { position: relative; } .metric-accent::before { content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; box-shadow: inset 3px 0 0 0 color-mix(in srgb, var(--tile-hue, var(--primary)) 55%, transparent); } .milestone-reached { background: radial-gradient( 130% 130% at 85% 0%, color-mix(in srgb, var(--success) 14%, var(--card)) 0%, color-mix(in srgb, var(--success) 4%, var(--card)) 60%, var(--card) 100% ); animation: milestone-reached-in 640ms var(--ring-spring) both; } @keyframes milestone-reached-in { from { opacity: 0; transform: translateY(6px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } @media (prefers-reduced-motion: reduce) { .milestone-reached { animation: none; } } @keyframes skeleton-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } } .skeleton-shimmer { background-image: linear-gradient( 100deg, transparent 36%, color-mix(in srgb, var(--foreground) 6%, transparent) 50%, transparent 64% ); background-size: 250% 100%; background-repeat: no-repeat; animation: skeleton-shimmer 1.8s ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .skeleton-shimmer { animation: none; background-image: none; } } /* ── Global reduced-motion backstop ────────────────────────────────── * Defensive catch-all so any animation/transition we forget to gate * per-component still collapses to near-instant when the user has asked * for reduced motion. The per-component blocks above stay — they carry * tailored static fallbacks (static dots, full-height bars). This is the * floor, not a replacement. Near-zero (not 0) keeps JS animation-end / * transition-end listeners firing so state machines don't stall. * * `animation-fill-mode: forwards` + `animation-delay: 0ms` are load-bearing: * several entrance animations rest at `opacity: 0` and only become visible * through their reveal keyframe. Without holding the end state (and without * zeroing the stagger delay) the near-zero animation completes and the * element snaps back to its hidden base style — leaving charts and list * rows permanently invisible under reduced motion. Forwards pins the final * (revealed) frame; zero delay reveals immediately. Per-component blocks * above that set `animation: none` are unaffected (no animation to fill). */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; animation-fill-mode: forwards !important; animation-delay: 0ms !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }