mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
The Vorsorge "Jetzt messen" / mark-done button hard-coded white text on a solid green fill. In the dark theme the fill is the bright Dracula green, so white-on-green reads about 1.37:1 — below the WCAG AA floor. Add a theme-aware `--success-foreground` token (near-black glyph on the dark fill, white on the darker light-mode green) mirroring the existing `--primary-foreground` pattern, and use `text-success-foreground` on both the card and list variants. Green stays only on the action button; the card surface remains neutral.
938 lines
33 KiB
CSS
938 lines
33 KiB
CSS
@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 `<repo-root>/.planning` and
|
||
* `<repo-root>/docs`.
|
||
*/
|
||
@source not "../../.planning";
|
||
@source not "../../docs";
|
||
|
||
@custom-variant dark (&:is(.dark *));
|
||
|
||
@theme inline {
|
||
--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);
|
||
|
||
/*
|
||
* 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;
|
||
|
||
/*
|
||
* 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);
|
||
|
||
/*
|
||
* 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.14.0 — wellness-ring per-metric gradients (Dracula dark).
|
||
* Five DISTINCT single-hue two-stop arcs (from = darker, to = lighter):
|
||
* readiness green · recovery cyan · sleep purple · stress orange ·
|
||
* strain pink. Every `to` stop IS the matching Dracula palette colour
|
||
* and the `from` stop is a hand-darkened tone of the same hue, so the
|
||
* arcs read as Dracula rather than generic chart colours (the v1.14.0
|
||
* set leaned indigo/teal/violet, which sat next to the palette instead
|
||
* of on it). Each `to` doubles as the ring's bloom colour. The Alucard
|
||
* light set lives on `:root.light`.
|
||
*/
|
||
--ring-readiness-from: #1fa854;
|
||
--ring-readiness-to: #50fa7b;
|
||
--tile-readiness: #50fa7b;
|
||
--ring-recovery-from: #2fa8c4;
|
||
--ring-recovery-to: #8be9fd;
|
||
--tile-recovery: #8be9fd;
|
||
--ring-sleep-from: #8655d6;
|
||
--ring-sleep-to: #bd93f9;
|
||
--tile-sleep: #bd93f9;
|
||
--ring-stress-from: #d9842e;
|
||
--ring-stress-to: #ffb86c;
|
||
--tile-stress: #ffb86c;
|
||
--ring-strain-from: #d6429b;
|
||
--ring-strain-to: #ff79c6;
|
||
--tile-strain: #ff79c6;
|
||
/* Band fallback (anatomy detail view, no per-metric hue). */
|
||
--ring-green-from: #1f9d57;
|
||
--ring-green-to: #5cf08f;
|
||
--ring-yellow-from: #e0922f;
|
||
--ring-yellow-to: #ffce6b;
|
||
--ring-red-from: #c93a2a;
|
||
--ring-red-to: #ff7a6b;
|
||
|
||
/*
|
||
* 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 {
|
||
--background: #f5f5f5;
|
||
--foreground: #1f1f1f;
|
||
--card: #ffffff;
|
||
--card-foreground: #1f1f1f;
|
||
--popover: #ffffff;
|
||
--popover-foreground: #1f1f1f;
|
||
--primary: #644ac9;
|
||
--primary-foreground: #ffffff;
|
||
--secondary: #dcdeef;
|
||
--secondary-foreground: #1f1f1f;
|
||
--muted: #dcdeef;
|
||
--muted-foreground: #5b6273;
|
||
/* Inactive tab-trigger text — the light muted-foreground already clears
|
||
* AA (4.58:1) on the light muted strip, so the token mirrors it. */
|
||
--tab-inactive: #5b6273;
|
||
/* Light-theme counterpart of the NativeSelect chevron (see dark block). */
|
||
--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='%235b6273' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||
--accent: #dcdeef;
|
||
--accent-foreground: #1f1f1f;
|
||
--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: #cfcfde;
|
||
--input: #cfcfde;
|
||
--ring: #644ac9;
|
||
--chart-1: #644ac9;
|
||
--chart-2: #14720a;
|
||
--chart-3: #a3134d;
|
||
--chart-4: #036a96;
|
||
--chart-5: #a34d14;
|
||
--sidebar: #ffffff;
|
||
--sidebar-foreground: #1f1f1f;
|
||
--sidebar-primary: #644ac9;
|
||
--sidebar-primary-foreground: #ffffff;
|
||
--sidebar-accent: #dcdeef;
|
||
--sidebar-accent-foreground: #1f1f1f;
|
||
--sidebar-border: #cfcfde;
|
||
--sidebar-ring: #644ac9;
|
||
|
||
/*
|
||
* v1.14.0 — wellness-ring per-metric gradients (Alucard light). DEEPER +
|
||
* more saturated than the dark set so each hue holds AA-grade presence on
|
||
* the white `--card` (raw Dracula cyan/green wash out on white). Same five
|
||
* Dracula hue families as the dark set: readiness green · recovery cyan ·
|
||
* sleep purple · stress orange · strain pink.
|
||
*/
|
||
--ring-readiness-from: #0f7a3a;
|
||
--ring-readiness-to: #18a34e;
|
||
--tile-readiness: #14720a;
|
||
--ring-recovery-from: #067f96;
|
||
--ring-recovery-to: #0d95b0;
|
||
--tile-recovery: #08869e;
|
||
--ring-sleep-from: #5b2db8;
|
||
--ring-sleep-to: #7a45d6;
|
||
--tile-sleep: #6233c0;
|
||
--ring-stress-from: #b06a16;
|
||
--ring-stress-to: #c47a1e;
|
||
--tile-stress: #a34d14;
|
||
--ring-strain-from: #ad1e6f;
|
||
--ring-strain-to: #cc2e88;
|
||
--tile-strain: #b0277d;
|
||
--ring-green-from: #0f7a3a;
|
||
--ring-green-to: #18a34e;
|
||
--ring-yellow-from: #b06a16;
|
||
--ring-yellow-to: #d68a2a;
|
||
--ring-red-from: #a3271a;
|
||
--ring-red-to: #cb3a2a;
|
||
|
||
/*
|
||
* 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 `<html>` for exactly those branches.
|
||
*
|
||
* The authenticated app shell is height-locked (`flex h-dvh`) and scrolls
|
||
* inside its own `<main>`, 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 `<AdminShell>` and
|
||
* `<SettingsShell>` 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;
|
||
}
|
||
|
||
/* ── 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 <HeroStrip> 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.14.0 — the wellness-strip "signature reveal" motion system.
|
||
*
|
||
* The whole moment plays ONCE per session (the strip toggles
|
||
* `data-revealed="true"` once, gated on a sessionStorage flag) so a
|
||
* background Apple-Health-sync refetch never re-triggers it. Three layers:
|
||
* 1. tile rise+fade (staggered left-to-right by an inline animation-delay),
|
||
* 2. the ring's spring-fill arc sweep (in score-ring.tsx, --ring-spring),
|
||
* 3. a conic SHEEN that sweeps the arc once (the "expensive" specular touch).
|
||
* A single prefers-reduced-motion block at the bottom zeroes all of it.
|
||
*/
|
||
@property --shimmer-angle {
|
||
syntax: "<angle>";
|
||
inherits: false;
|
||
initial-value: 0deg;
|
||
}
|
||
|
||
: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);
|
||
}
|
||
}
|
||
|
||
/* The conic sheen overlay sits over the ring; opacity animates from 0 so a
|
||
browser without @property support (Safari <16.4 / FF <128) shows nothing
|
||
rather than a frozen highlight band. */
|
||
.wellness-ring-sheen {
|
||
position: absolute;
|
||
inset: 6%;
|
||
border-radius: 50%;
|
||
opacity: 0;
|
||
/* A specular highlight must always LIGHTEN. Under mix-blend overlay a
|
||
near-black light-mode `--card-foreground` would darken the tile into a
|
||
grey band, so the sheen rides a fixed white in both themes. */
|
||
background: radial-gradient(
|
||
circle at 50% 50%,
|
||
rgba(255, 255, 255, 0.72) 0%,
|
||
transparent 70%
|
||
);
|
||
mix-blend-mode: overlay;
|
||
pointer-events: none;
|
||
-webkit-mask: conic-gradient(
|
||
from var(--shimmer-angle),
|
||
transparent 0deg,
|
||
transparent 38deg,
|
||
#000 58deg,
|
||
transparent 80deg,
|
||
transparent 360deg
|
||
);
|
||
mask: conic-gradient(
|
||
from var(--shimmer-angle),
|
||
transparent 0deg,
|
||
transparent 38deg,
|
||
#000 58deg,
|
||
transparent 80deg,
|
||
transparent 360deg
|
||
);
|
||
}
|
||
[data-revealed="true"] .wellness-ring-sheen {
|
||
animation: ringSheen 1500ms var(--ring-ease) forwards;
|
||
animation-delay: calc(var(--reveal-delay, 0ms) + 550ms);
|
||
}
|
||
@keyframes ringSheen {
|
||
0% {
|
||
--shimmer-angle: 0deg;
|
||
opacity: 0;
|
||
}
|
||
15% {
|
||
opacity: 0.5;
|
||
}
|
||
85% {
|
||
opacity: 0.5;
|
||
}
|
||
100% {
|
||
--shimmer-angle: 360deg;
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
/* Token-tinted bloom behind the progress arc. The breathing pulse runs only
|
||
on the strongest (green-band) ring so the strip celebrates a good score
|
||
rather than jittering all five. `--ring-glow` is set inline per ring. */
|
||
.wellness-ring-arc {
|
||
filter: drop-shadow(
|
||
0 0 3px color-mix(in srgb, var(--ring-glow) 45%, transparent)
|
||
);
|
||
}
|
||
/* Flat 2-D arc — no bloom, no pulse. The dashboard hero ring uses this so it
|
||
reads as calmly as the chart + tile surfaces around it. */
|
||
.wellness-ring-arc--flat {
|
||
filter: none;
|
||
}
|
||
.wellness-ring-arc--pulse {
|
||
/* Breathe a few times to celebrate a strong score, then settle — an
|
||
infinite loop would repaint the (non-compositable) drop-shadow filter
|
||
forever. Motion happens, then the surface goes calm. */
|
||
animation: ringGlow 3.4s ease-in-out 2s 3;
|
||
}
|
||
@keyframes ringGlow {
|
||
0%,
|
||
100% {
|
||
filter: drop-shadow(
|
||
0 0 2px color-mix(in srgb, var(--ring-glow) 40%, transparent)
|
||
);
|
||
}
|
||
50% {
|
||
filter: drop-shadow(
|
||
0 0 8px color-mix(in srgb, var(--ring-glow) 65%, transparent)
|
||
);
|
||
}
|
||
}
|
||
|
||
@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;
|
||
}
|
||
[data-revealed="true"] .wellness-ring-sheen {
|
||
animation: none;
|
||
opacity: 0;
|
||
}
|
||
.wellness-ring-arc,
|
||
.wellness-ring-arc--pulse {
|
||
filter: 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 <svg> 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: "<angle>";
|
||
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;
|
||
}
|
||
}
|
||
|
||
/* ── 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;
|
||
}
|
||
}
|