/* ===========================================================================
   GG UI — design tokens + primitive styles.
   Plain CSS on purpose: no build step, no preprocessing. Loaded with a normal
   <link>, so it paints before the Tailwind browser runtime finishes compiling.
   =========================================================================== */

/* ===========================================================================
   ⬇  CHANGE YOUR BRAND HERE  ⬇
   Edit the --brand-* ramp below and the ENTIRE kit re-themes: every button,
   badge, focus ring, chart tint, border beam, header and footer. Nothing else
   hardcodes the colour — components only ever reference --primary/--accent,
   which are mapped into Tailwind as bg-primary / text-accent / etc.

   Values are R G B channel triplets (not hex) so any rule can apply alpha:
   rgb(var(--primary) / 0.3)
   =========================================================================== */
:root {
    --brand-25: 254 240 235;
    --brand-50: 254 231 222;
    --brand-100: 253 208 191;
    --brand-200: 251 176 148;
    --brand-300: 248 137 100;
    --brand-400: 244 105 62;
    --brand-500: 240 80 37;
    --brand-600: 211 70 33;
    --brand-700: 173 58 27;
    --brand-800: 139 46 21;
    --brand-900: 110 37 17;
    --brand-950: 67 22 10;

    /* Primary is the canonical brand colour. Accent aliases it, so the whole
       component kit (which references --accent) follows automatically. Point
       --accent at a different ramp only if you want a true second colour. */
    --primary: var(--brand-500);
    --primary-hover: var(--brand-400);
    --primary-press: var(--brand-700);

    --accent: var(--primary);
    --accent-hover: var(--primary-hover);
    --accent-press: var(--primary-press);

    --success: 22 163 74;
    --danger: 239 68 68;
    --warning: 245 158 11;
    --info: 37 99 235;

    /* Light surfaces.

       The page is pure white and cards sit on it at the same white, separated by
       a hairline and a soft shadow rather than by a tint — a grey page behind
       white cards is what made this theme look dull.

       Sunken keeps the cool slate tint: inputs, wells and section bands need to
       read as recessed, and against a white page a tint is the only thing that
       does that. It is slate rather than neutral grey so light and dark stay one
       family — the dark theme is navy (#0c111d / #101828). */
    --bg: #ffffff;
    --surface: #ffffff;
    --surface-raised: #ffffff;
    --surface-sunken: #f1f5f9;
    --surface-high: #e2e8f0;

    --text: 15 23 42;
    /* Measured, not picked by eye: 5.19:1 on --bg, where the neutral grey it
       replaces managed 4.76 — cooler *and* easier to read at 11px. */
    --muted: #5b6b82;
    --border: rgb(15 23 42 / 0.11);
    --border-soft: rgb(15 23 42 / 0.07);

    --popover-bg: rgb(255 255 255 / 0.94);
    --popover-border: rgb(15 23 42 / 0.08);
    --popover-shadow: 0 24px 60px rgb(15 23 42 / 0.14);
    --overlay: rgb(15 23 42 / 0.35);

    /* A hairline plus one soft ambient pool. The old glossy white inset was
       invisible on white surfaces and only muddied the edge. */
    --card-shadow: 0 1px 2px rgb(15 23 42 / 0.04), 0 12px 28px -18px rgb(15 23 42 / 0.26);
    --card-shadow-hover: 0 1px 2px rgb(15 23 42 / 0.05), 0 18px 38px -20px rgb(15 23 42 / 0.30);

    /* Radii — tightened one step from the source design for a denser feel. */
    --r-control: 7px;
    --r-input: 9px;
    --r-card: 11px;
    --r-card-lg: 13px;
    --r-panel: 16px;
    --r-pill: 999px;

    --spring: cubic-bezier(0.34, 1.56, 0.64, 1);

    color-scheme: light;
}

.dark {
    --bg: #0c111d;
    --surface: #101828;
    --surface-raised: #1a2233;
    --surface-sunken: #080d16;
    --surface-high: #242e42;

    --text: 242 244 247;
    --muted: #94a3b8;
    --border: rgb(242 244 247 / 0.16);
    --border-soft: rgb(242 244 247 / 0.11);

    --popover-bg: rgb(10 11 26 / 0.9);
    --popover-border: rgb(255 255 255 / 0.1);
    --popover-shadow: 0 32px 80px rgb(0 0 0 / 0.5);
    --overlay: rgb(3 6 14 / 0.7);

    --card-shadow: none;
    --card-shadow-hover: none;

    color-scheme: dark;
}

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

body {
    margin: 0;
    background-color: var(--bg);
    color: rgb(var(--text));
    font-family: 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px rgb(var(--accent) / 0.35);
}

/* Browsers default <button> to cursor:default. Anything clickable gets a hand. */
button,
summary,
label[for],
select,
[role='button'],
[role='option'],
[role='tab'],
[role='menuitem'],
a[href] {
    cursor: pointer;
}

button:disabled,
select:disabled,
[aria-disabled='true'],
[data-disabled] {
    cursor: not-allowed;
}

::selection {
    background: rgb(var(--accent) / 0.25);
}

/* Alpine hasn't booted yet — keep popovers from flashing open. */
[x-cloak] {
    display: none !important;
}

/* iOS Safari auto-zooms when a focused field's font-size is below 16px. Bump
   real text fields to 16px on small screens — !important because the Tailwind
   utilities that set 13/14px are class-specificity and would otherwise win.
   (Don't "fix" this with maximum-scale=1; that blocks pinch-zoom entirely.) */
@media screen and (max-width: 767px) {
    input:not([type='checkbox']):not([type='radio']):not([type='hidden']),
    select,
    textarea {
        font-size: 16px !important;
    }
}

/* --- Primitives ---------------------------------------------------------- */
.ui-card-shadow {
    box-shadow: var(--card-shadow);
}

.ui-popover {
    background: var(--popover-bg);
    border: 1px solid var(--popover-border);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    backdrop-filter: blur(20px) saturate(180%);
    box-shadow: var(--popover-shadow);
}

/* Shared field chrome: input, textarea, select, combobox trigger, date trigger */
.ui-field {
    width: 100%;
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--r-input);
    color: rgb(var(--text));
    font: inherit;
    transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
}

.ui-field::placeholder {
    color: var(--muted);
}

/* The :focus exclusions matter: clicking a field leaves the pointer on it, so
   hover and focus are both live. This selector outweighs the focus rule below,
   so without them the grey hover border would hide the focus colour. */
.ui-field:hover:not(:disabled):not([data-invalid]):not([data-disabled]):not(:focus):not(:focus-within) {
    border-color: rgb(var(--text) / 0.28);
}

.ui-field:focus,
.ui-field:focus-within {
    outline: none;
    border-color: rgb(var(--primary));
    /* No ring — the border alone marks focus. Set explicitly rather than
       omitted, or the global :focus-visible rule paints one back. */
    box-shadow: none;
}

/* When .ui-field is a wrapper (prefix/suffix, combobox, date picker), the
   wrapper draws the focus ring via :focus-within. The inner control must not
   draw its own from the global :focus-visible rule, or the two rings overlap.
   Scoped to form controls so buttons inside keep their focus indicator. */
.ui-field input:focus,
.ui-field input:focus-visible,
.ui-field textarea:focus,
.ui-field textarea:focus-visible,
.ui-field select:focus,
.ui-field select:focus-visible {
    outline: none;
    box-shadow: none;
}

/* Chrome paints its own background on an autofilled control. That background
   sits on the inner <input>, which has no radius of its own, so it squares off
   the wrapper's rounded corners until the value is edited. It can't be unset
   directly, so an inset shadow large enough to flood the box stands in for it.
   Kept in a separate rule from the standard :autofill below — an unknown
   selector invalidates the whole list, and each browser only knows its own.
   `.ui-field` is the control itself on textarea/select and the wrapper on
   input, hence both forms. */
.ui-field:-webkit-autofill,
.ui-field:-webkit-autofill:hover,
.ui-field:-webkit-autofill:focus,
.ui-field input:-webkit-autofill,
.ui-field input:-webkit-autofill:hover,
.ui-field input:-webkit-autofill:focus {
    -webkit-box-shadow: 0 0 0 100px var(--surface-sunken) inset;
    box-shadow: 0 0 0 100px var(--surface-sunken) inset;
    -webkit-text-fill-color: rgb(var(--text));
    caret-color: rgb(var(--text));
}

.ui-field:autofill,
.ui-field input:autofill {
    box-shadow: 0 0 0 100px var(--surface-sunken) inset;
    -webkit-text-fill-color: rgb(var(--text));
    caret-color: rgb(var(--text));
}

/* An inset shadow is clipped to its own element's box. The inner <input> fills
   the wrapper as a square-cornered rectangle, so without a radius of its own
   the fill squares off the wrapper's rounded corners. `inherit` takes the
   wrapper's radius, so it tucks inside the border instead. Only the wrapper
   case — where .ui-field is the control itself it already has --r-input, and
   inheriting there would pull the radius off the field's parent. */
.ui-field input:-webkit-autofill,
.ui-field input:-webkit-autofill:hover,
.ui-field input:-webkit-autofill:focus {
    border-radius: inherit;
}

.ui-field input:autofill {
    border-radius: inherit;
}

.ui-field[data-invalid] {
    border-color: rgb(var(--danger) / 0.6);
}

.ui-field[data-invalid]:focus,
.ui-field[data-invalid]:focus-within {
    border-color: rgb(var(--danger));
    box-shadow: none;
}

.ui-field:disabled,
.ui-field[data-disabled] {
    opacity: 0.55;
    cursor: not-allowed;
}

/* Readonly still submits its value, so it stays legible — just visibly
   not-editable, unlike the faded disabled state. */
.ui-field[data-readonly] {
    background: rgb(var(--text) / 0.04);
    border-style: dashed;
}

.ui-field[data-readonly] input {
    cursor: default;
}

/* Native select needs its own arrow since we strip the OS one. */
.ui-select {
    -webkit-appearance: none;
    appearance: none;
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
        linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 20px) calc(50% + 2px), calc(100% - 15px) calc(50% + 2px);
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

/* --- Tones ---------------------------------------------------------------
   Soft fills for badges and trend pills. The text colour is a *deeper* shade
   than the fill in light mode and a *lighter* one in dark mode — using the raw
   500-level colour on a 12%-alpha fill fails contrast badly for amber & green.
   ------------------------------------------------------------------------- */
:root {
    --tone-neutral-bg: rgb(15 23 42 / 0.06);
    --tone-neutral-fg: #334155;
    --tone-neutral-bd: rgb(15 23 42 / 0.14);

    /* Derived from the brand ramp, so accent badges re-theme with --primary.
       The semantic tones below stay fixed — success should read green whatever
       the brand colour is. */
    --tone-accent-bg: rgb(var(--primary) / 0.11);
    --tone-accent-fg: rgb(var(--brand-700));
    --tone-accent-bd: rgb(var(--primary) / 0.3);

    --tone-success-bg: rgb(22 163 74 / 0.11);
    --tone-success-fg: #15803d;
    --tone-success-bd: rgb(22 163 74 / 0.3);

    --tone-danger-bg: rgb(239 68 68 / 0.11);
    --tone-danger-fg: #b91c1c;
    --tone-danger-bd: rgb(239 68 68 / 0.3);

    --tone-warning-bg: rgb(245 158 11 / 0.14);
    --tone-warning-fg: #b45309;
    --tone-warning-bd: rgb(245 158 11 / 0.38);

    --tone-info-bg: rgb(37 99 235 / 0.11);
    --tone-info-fg: #1d4ed8;
    --tone-info-bd: rgb(37 99 235 / 0.3);
}

.dark {
    --tone-neutral-bg: rgb(242 244 247 / 0.09);
    --tone-neutral-fg: #cbd5e1;
    --tone-neutral-bd: rgb(242 244 247 / 0.18);

    --tone-accent-bg: rgb(var(--primary) / 0.18);
    --tone-accent-fg: rgb(var(--brand-300));
    --tone-accent-bd: rgb(var(--primary) / 0.38);

    --tone-success-bg: rgb(34 197 94 / 0.16);
    --tone-success-fg: #4ade80;
    --tone-success-bd: rgb(34 197 94 / 0.35);

    --tone-danger-bg: rgb(239 68 68 / 0.16);
    --tone-danger-fg: #fca5a5;
    --tone-danger-bd: rgb(239 68 68 / 0.35);

    --tone-warning-bg: rgb(245 158 11 / 0.16);
    --tone-warning-fg: #fcd34d;
    --tone-warning-bd: rgb(245 158 11 / 0.35);

    --tone-info-bg: rgb(59 130 246 / 0.16);
    --tone-info-fg: #93c5fd;
    --tone-info-bd: rgb(59 130 246 / 0.35);
}

.ui-tone-neutral,
.ui-tone-accent,
.ui-tone-success,
.ui-tone-danger,
.ui-tone-warning,
.ui-tone-info {
    background: var(--tone-bg);
    color: var(--tone-fg);
    border-color: var(--tone-bd);
}

.ui-tone-neutral { --tone-bg: var(--tone-neutral-bg); --tone-fg: var(--tone-neutral-fg); --tone-bd: var(--tone-neutral-bd); }
.ui-tone-accent  { --tone-bg: var(--tone-accent-bg);  --tone-fg: var(--tone-accent-fg);  --tone-bd: var(--tone-accent-bd); }
.ui-tone-success { --tone-bg: var(--tone-success-bg); --tone-fg: var(--tone-success-fg); --tone-bd: var(--tone-success-bd); }
.ui-tone-danger  { --tone-bg: var(--tone-danger-bg);  --tone-fg: var(--tone-danger-fg);  --tone-bd: var(--tone-danger-bd); }
.ui-tone-warning { --tone-bg: var(--tone-warning-bg); --tone-fg: var(--tone-warning-fg); --tone-bd: var(--tone-warning-bd); }
.ui-tone-info    { --tone-bg: var(--tone-info-bg);    --tone-fg: var(--tone-info-fg);    --tone-bd: var(--tone-info-bd); }

/* The status dot always uses the full-strength colour, not the text shade. */
.ui-tone-dot { background: var(--tone-dot, currentColor); }
.ui-tone-neutral .ui-tone-dot { --tone-dot: #94a3b8; }
.ui-tone-accent .ui-tone-dot  { --tone-dot: rgb(var(--accent)); }
.ui-tone-success .ui-tone-dot { --tone-dot: rgb(var(--success)); }
.ui-tone-danger .ui-tone-dot  { --tone-dot: rgb(var(--danger)); }
.ui-tone-warning .ui-tone-dot { --tone-dot: rgb(var(--warning)); }
.ui-tone-info .ui-tone-dot    { --tone-dot: rgb(var(--info)); }

/* --- Checkbox / radio ----------------------------------------------------
   Native inputs with the OS chrome stripped, repainted via background-image so
   checked state needs no extra markup and no JS.
   ------------------------------------------------------------------------- */
.ui-check,
.ui-radio {
    -webkit-appearance: none;
    appearance: none;
    flex-shrink: 0;
    width: 16px;
    height: 16px;
    margin: 0;
    background-color: var(--surface-sunken);
    border: 1px solid var(--border);
    background-position: center;
    background-repeat: no-repeat;
    cursor: pointer;
    transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.ui-check {
    border-radius: 6px;
}

.ui-radio {
    border-radius: 50%;
}

.ui-check:hover:not(:disabled),
.ui-radio:hover:not(:disabled) {
    border-color: rgb(var(--accent) / 0.5);
}

.ui-check:checked,
.ui-check:indeterminate,
.ui-radio:checked {
    background-color: rgb(var(--accent));
    border-color: rgb(var(--accent));
}

.ui-check:checked {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3 8.5 3.5 3.5L13 5'/%3E%3C/svg%3E");
    background-size: 74%;
}

.ui-check:indeterminate {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M4 8h8'/%3E%3C/svg%3E");
    background-size: 74%;
}

.ui-radio:checked {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3.25' fill='white'/%3E%3C/svg%3E");
    background-size: 100%;
}

.ui-check:disabled,
.ui-radio:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.ui-scroll {
    scrollbar-width: thin;
    scrollbar-color: rgb(var(--text) / 0.2) transparent;
}

.ui-scroll::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}

.ui-scroll::-webkit-scrollbar-thumb {
    background: rgb(var(--text) / 0.2);
    border-radius: 999px;
}

.ui-scroll::-webkit-scrollbar-track {
    background: transparent;
}

/* Accent glow used on primary buttons and the hero panel. */
.ui-glow {
    box-shadow: 0 4px 20px rgb(var(--accent) / 0.3);
}

.ui-glow-lg {
    box-shadow: 0 8px 28px rgb(var(--accent) / 0.4);
}

/* Concentric rings radiating from a single point — reads as "many things, one
   centre", which is the platform's whole pitch. Masked so it fades out instead
   of terminating in a hard edge. Position with --rings-x / --rings-y. */
.ui-rings-bg {
    background-image: repeating-radial-gradient(
        circle at var(--rings-x, 78%) var(--rings-y, 22%),
        transparent 0 71px,
        var(--border-soft) 71px 72px
    );
    -webkit-mask-image: radial-gradient(
        circle at var(--rings-x, 78%) var(--rings-y, 22%),
        #000 0%,
        rgb(0 0 0 / 0.35) 55%,
        transparent 85%
    );
    mask-image: radial-gradient(
        circle at var(--rings-x, 78%) var(--rings-y, 22%),
        #000 0%,
        rgb(0 0 0 / 0.35) 55%,
        transparent 85%
    );
}

/* Starfield for a centred hero. Two layers of dots at different sizes and
   offsets so the scatter reads as random rather than as a lattice, drawn in the
   text colour at low alpha so it inverts with the theme instead of assuming a
   dark page. The mask fades it out at the edges so it never fights the copy. */
.ui-stars-bg {
    background-image:
        radial-gradient(1.5px 1.5px at 12% 18%, rgb(var(--text) / 0.55) 50%, transparent 50%),
        radial-gradient(1px 1px at 32% 62%, rgb(var(--text) / 0.4) 50%, transparent 50%),
        radial-gradient(1.5px 1.5px at 58% 28%, rgb(var(--text) / 0.5) 50%, transparent 50%),
        radial-gradient(1px 1px at 76% 72%, rgb(var(--text) / 0.35) 50%, transparent 50%),
        radial-gradient(1.5px 1.5px at 88% 38%, rgb(var(--text) / 0.45) 50%, transparent 50%),
        radial-gradient(1px 1px at 46% 88%, rgb(var(--text) / 0.3) 50%, transparent 50%),
        radial-gradient(1px 1px at 68% 8%, rgb(var(--text) / 0.4) 50%, transparent 50%),
        radial-gradient(1px 1px at 22% 44%, rgb(var(--text) / 0.3) 50%, transparent 50%);
    background-size: 420px 320px, 300px 260px, 520px 400px, 360px 300px,
        480px 360px, 340px 280px, 400px 340px, 280px 240px;
    -webkit-mask-image: radial-gradient(ellipse 90% 70% at 50% 30%, #000 10%, transparent 75%);
    mask-image: radial-gradient(ellipse 90% 70% at 50% 30%, #000 10%, transparent 75%);
}

/* The brand glow behind a centred hero: one soft pool of primary, sized in the
   element rather than with a blur filter so it costs nothing to paint. */
.ui-hero-glow {
    /* Two pools: a wide wash that lifts the whole block off the page, and a
       tighter core behind the badge and headline. Anchored at 28% down rather
       than the top edge — at 0% the light is mostly outside the section and
       reads as a smudge above the fold instead of as a glow behind the copy. */
    background-image:
        radial-gradient(ellipse 70% 55% at 50% 28%, rgb(var(--primary) / 0.16) 0%, transparent 70%),
        radial-gradient(ellipse 32% 26% at 50% 18%, rgb(var(--primary) / 0.22) 0%, transparent 72%);
}

/* A light page needs more of it: the same alpha that glows on near-black barely
   tints near-white. */
:root:not(.dark) .ui-hero-glow {
    background-image:
        radial-gradient(ellipse 70% 55% at 50% 28%, rgb(var(--primary) / 0.13) 0%, transparent 70%),
        radial-gradient(ellipse 32% 26% at 50% 18%, rgb(var(--primary) / 0.18) 0%, transparent 72%);
}

/* --- Marquee -------------------------------------------------------------
   A row that scrolls itself, for a strip with more in it than fits on a line.
   The track holds the same group twice and slides exactly one group width, so
   the loop closes on itself with no seam and no JS. Duration is a variable so a
   longer strip can be slowed to keep the same apparent speed.
   ------------------------------------------------------------------------- */
.ui-marquee {
    overflow: hidden;
    /* Items fade out at both ends rather than being clipped mid-word. */
    -webkit-mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
    mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
}

.ui-marquee-track {
    display: flex;
    width: max-content;
    animation: ui-marquee-scroll var(--marquee-duration, 45s) linear infinite;
}

/* Let someone stop it to read or click an item. */
.ui-marquee:hover .ui-marquee-track,
.ui-marquee:focus-within .ui-marquee-track {
    animation-play-state: paused;
}

.ui-marquee-group {
    display: flex;
    flex-shrink: 0;
    align-items: center;
}

@keyframes ui-marquee-scroll {
    from { transform: translateX(0); }
    to { transform: translateX(-50%); }
}

/* Motion off: hand the row back as something to swipe, rather than hiding
   half of it behind an animation that no longer runs. */
@media (prefers-reduced-motion: reduce) {
    .ui-marquee-track { animation: none; }
    .ui-marquee { overflow-x: auto; }
}

/* Faint grid backdrop for empty states / hero panels. */
.ui-grid-bg {
    background-image: linear-gradient(to right, var(--border-soft) 1px, transparent 1px),
        linear-gradient(to bottom, var(--border-soft) 1px, transparent 1px);
    background-size: 56px 56px;
}

/* --- Border beam ---------------------------------------------------------
   A light that sweeps around the card's edge. Drawn as a conic gradient on a
   pseudo-element, masked to just the 1px border ring so the card's own
   background stays untouched. Registering --beam-angle as an <angle> is what
   makes it animatable — a plain custom property can't be tweened.
   ------------------------------------------------------------------------- */
@property --beam-angle {
    syntax: '<angle>';
    initial-value: 0deg;
    inherits: false;
}

@keyframes ui-beam-spin {
    to {
        --beam-angle: 360deg;
    }
}

.ui-beam {
    position: relative;
    isolation: isolate;
}

.ui-beam::before {
    content: '';
    position: absolute;
    /* An absolutely-positioned pseudo-element anchors to the PADDING box, so
       inset:0 would draw the ring just inside the border as a second line.
       Pulling out by the border width sits it on the border itself. */
    inset: calc(-1 * var(--beam-inset, 1px));
    z-index: 1;
    border-radius: inherit;
    padding: var(--beam-width, 2px);
    background: conic-gradient(
        from var(--beam-angle),
        transparent 0deg 190deg,
        rgb(var(--beam-color, var(--accent)) / 0.25) 260deg,
        rgb(var(--beam-color, var(--accent)) / 0.7) 320deg,
        rgb(var(--beam-color, var(--accent))) 350deg,
        transparent 360deg
    );
    /* Punch out the middle, leaving only the ring painted. */
    -webkit-mask:
        linear-gradient(#000 0 0) content-box,
        linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask:
        linear-gradient(#000 0 0) content-box,
        linear-gradient(#000 0 0);
    mask-composite: exclude;
    opacity: 0;
    transition: opacity 0.25s;
    pointer-events: none;
}

/* Runs on hover, when explicitly marked active, or when it wraps a checked
   input — the same trigger the radio/checkbox cards already use. */
.ui-beam:hover::before,
.ui-beam[data-active]::before,
.ui-beam:has(:checked)::before,
.ui-beam:focus-within::before {
    opacity: 1;
    animation: ui-beam-spin var(--beam-duration, 3s) linear infinite;
}

@media (prefers-reduced-motion: reduce) {
    .ui-beam:hover::before,
    .ui-beam[data-active]::before,
    .ui-beam:has(:checked)::before,
    .ui-beam:focus-within::before {
        animation: none;
        background: rgb(var(--beam-color, var(--accent)) / 0.55);
    }
}

/* Indeterminate progress sweep. */
@keyframes ui-indeterminate {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(400%); }
}

.ui-indeterminate {
    animation: ui-indeterminate 1.4s ease-in-out infinite;
}

@keyframes ui-shimmer {
    100% { transform: translateX(100%); }
}

.ui-skeleton {
    position: relative;
    overflow: hidden;
    background: rgb(var(--text) / 0.07);
}

.ui-skeleton::after {
    content: '';
    position: absolute;
    inset: 0;
    transform: translateX(-100%);
    background: linear-gradient(90deg, transparent, rgb(var(--text) / 0.08), transparent);
    animation: ui-shimmer 1.6s infinite;
}

@media (prefers-reduced-motion: reduce) {
    .ui-skeleton::after,
    .ui-indeterminate {
        animation: none;
    }
}

/* ─────────────────────────────────────────────────────────────
   Rich text output (.ui-prose)
   Admin-authored HTML rendered on customer pages. Kept to the
   same tag set the sanitiser allows, so nothing can arrive here
   unstyled.
   ───────────────────────────────────────────────────────────── */
.ui-prose {
    font-size: 13px;
    line-height: 1.65;
    color: rgb(var(--text));
}
.ui-prose > :first-child { margin-top: 0; }
.ui-prose > :last-child  { margin-bottom: 0; }
.ui-prose p { margin: 0 0 0.75em; }
.ui-prose h2 { font-size: 16px; font-weight: 700; margin: 1.2em 0 0.45em; }
.ui-prose h3 { font-size: 14.5px; font-weight: 700; margin: 1.1em 0 0.4em; }
.ui-prose h4 { font-size: 13.5px; font-weight: 650; margin: 1em 0 0.35em; }
.ui-prose ul,
.ui-prose ol { margin: 0 0 0.75em; padding-left: 1.25em; }
.ui-prose ul { list-style: disc; }
.ui-prose ol { list-style: decimal; }
.ui-prose li { margin: 0.15em 0; }
.ui-prose strong, .ui-prose b { font-weight: 650; color: rgb(var(--text)); }
.ui-prose a {
    color: rgb(var(--primary));
    text-decoration: underline;
    text-underline-offset: 2px;
}
.ui-prose a:hover { color: rgb(var(--primary-hover)); }
.ui-prose blockquote {
    margin: 0 0 0.75em;
    padding: 0.35em 0 0.35em 0.9em;
    border-left: 2px solid var(--border);
    color: var(--muted);
}
.ui-prose code {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.9em;
    background: var(--surface-sunken);
    border-radius: var(--r-control);
    padding: 0.1em 0.35em;
}
.ui-prose pre {
    margin: 0 0 0.75em;
    padding: 0.7em 0.85em;
    background: var(--surface-sunken);
    border-radius: var(--r-card);
    overflow-x: auto;
}
.ui-prose pre code { background: none; padding: 0; }
.ui-prose hr { margin: 1em 0; border: 0; border-top: 1px solid var(--border-soft); }

/* The editor writes alignment and size as inline styles; a list that has been
   centred needs its markers to follow the text rather than stay hard left. */
.ui-prose [style*="text-align: center"],
.ui-prose [style*="text-align:center"] { list-style-position: inside; }
.ui-prose [style*="text-align: right"],
.ui-prose [style*="text-align:right"]  { list-style-position: inside; }

/* Inline colours must survive the dark theme, but the *default* text colour
   still has to flip — so only elements without an explicit colour inherit. */
.ui-prose :where(p, li, h2, h3, h4, blockquote):not([style*="color"]) { color: inherit; }

.ui-prose s, .ui-prose strike { text-decoration: line-through; }
.ui-prose u { text-decoration: underline; text-underline-offset: 2px; }

/* ─────────────────────────────────────────────────────────────
   Joined control (.ui-join)
   An input and its action welded into one control, so they read
   as a single thing rather than two that happen to sit together.
   ───────────────────────────────────────────────────────────── */
.ui-join {
    display: flex;
    align-items: stretch;
    width: 100%;
}
.ui-join > .ui-field {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
    flex: 1 1 auto;
    min-width: 0;
}
.ui-join > .ui-join-action {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
    /* Collapse the doubled border down the seam. */
    margin-left: -1px;
    flex: 0 0 auto;
    white-space: nowrap;
}
/* Whichever side is focused owns the seam. */
.ui-join > .ui-field:focus,
.ui-join > .ui-join-action:focus-visible {
    position: relative;
    z-index: 1;
}

/* The action half of a joined control: primary, and visibly inert when the
   field beside it is empty. */
.ui-btn-join {
    background: rgb(var(--primary));
    border: 1px solid rgb(var(--primary));
    color: #fff;
    transition: background 0.18s, border-color 0.18s, opacity 0.18s;
}
.ui-btn-join:hover:not(:disabled) { background: rgb(var(--primary-hover)); border-color: rgb(var(--primary-hover)); }
.ui-btn-join:active:not(:disabled) { background: rgb(var(--primary-press)); border-color: rgb(var(--primary-press)); }
.ui-btn-join:disabled { cursor: not-allowed; opacity: 0.45; }

/* ─────────────────────────────────────────────────────────────
   Stepper (.ui-step-*)
   Minus / value / plus welded into one control, built on .ui-join.
   ───────────────────────────────────────────────────────────── */
.ui-step-btn {
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    color: var(--muted);
    transition: background 0.15s, color 0.15s, border-color 0.15s, opacity 0.15s;
}
/* Tinted with an alpha on the token rather than color-mix: the ramp is stored
   as raw rgb components, which color-mix cannot take. */
.ui-step-btn:hover:not(:disabled) {
    background: rgb(var(--primary) / 0.12);
    color: rgb(var(--primary));
    border-color: rgb(var(--primary) / 0.35);
}
.ui-step-btn:active:not(:disabled) {
    background: rgb(var(--primary) / 0.2);
}
/* At a bound the button is inert — say so rather than let it look clickable. */
.ui-step-btn:disabled { opacity: 0.4; cursor: not-allowed; }

.ui-step-start {
    border-radius: var(--r-input) 0 0 var(--r-input);
}
.ui-step-end {
    border-radius: 0 var(--r-input) var(--r-input) 0;
    margin-left: -1px;
}
/* The value sits between the two buttons, so it owns no rounding at all. */
.ui-join > .ui-step-input {
    border-radius: 0;
    margin-left: -1px;
    min-width: 0;
    flex: 1 1 auto;
}
.ui-step-suffix {
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    margin-left: -1px;
}
/* Focus lifts above its neighbours so the ring is never clipped by the seam. */
.ui-step-btn:focus-visible,
.ui-step-input:focus { position: relative; z-index: 1; }
