@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 { --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-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-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); --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; /* 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; /* * 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; /* 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; --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 `` 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; } /* ── 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.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: ""; 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) ); } .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 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; } }