//! Centralised UI design tokens — colours, typography, spacing, radius,
//! z-index hierarchy, and motion durations.
//!
//! Every UI surface (HUD, modals, popovers, toasts) reads from these
//! tokens instead of hardcoding hex codes or magic numbers. The audit
//! that produced this module found 40+ scattered colour literals, 12+
//! distinct font sizes, and 8+ hardcoded z-index values across the
//! engine; collapsing them into one source of truth keeps the visual
//! system coherent and makes future palette swaps a single-file change.
//!
//! Palette is "Terminal" (base16-eighties) — see
//! `docs/ui-mockups/design-system.md` for the full token spec, mockup
//! library, and rationale. The tokens are exposed as `pub const` so
//! static contexts (default colours on Sprite components, etc.) can use
//! them; a future `UiTheme` resource can layer runtime switching on top
//! without changing the constant API.

use bevy::color::Color;
use bevy::math::Vec2;
use bevy::prelude::Val;
use solitaire_data::AnimSpeed;

// ---------------------------------------------------------------------------
// Colours — Terminal (base16-eighties): near-black surface ramp with a brick-
// red primary accent and lime/lavender/gold/teal/pink semantic accents.
// ---------------------------------------------------------------------------

/// Window backstop and the default text colour on top of `ACCENT_PRIMARY`.
/// Near-black terminal background. `#151515`.
pub const BG_BASE: Color = Color::srgb(0.082, 0.082, 0.082);

/// Elevated surface — modal cards, popover panels, button backgrounds.
/// One step lighter than `BG_BASE` so cards read as a separate plane
/// without needing drop shadows. `#202020`.
pub const BG_ELEVATED: Color = Color::srgb(0.125, 0.125, 0.125);

/// Hovered/highlighted surface — used on button hover and on the
/// currently-active row of a popover. `#2a2a2a`.
pub const BG_ELEVATED_HI: Color = Color::srgb(0.165, 0.165, 0.165);

/// Top elevation step — Secondary button hover, popover currently-
/// hovered row. One rung above `BG_ELEVATED_HI`. `#353535`.
pub const BG_ELEVATED_TOP: Color = Color::srgb(0.208, 0.208, 0.208);

/// Pressed-button surface — sits below `BG_ELEVATED` so a press reads
/// as the surface receding rather than rising. `#1a1a1a`.
pub const BG_ELEVATED_PRESSED: Color = Color::srgb(0.102, 0.102, 0.102);

/// Uniform scrim under every modal. Per the Terminal design system,
/// modals dim the table aggressively (95% opacity) without blurring,
/// to maintain the crisp synthwave-flat aesthetic. `rgba(21, 21, 21, 0.95)`.
pub const SCRIM: Color = Color::srgba(0.082, 0.082, 0.082, 0.95);

/// Solid fill for the top-of-window HUD band painted by
/// `hud_plugin::spawn_hud_band`. Terminal HUD chips are opaque
/// `surface-container` panels — no transparency — so the chrome reads
/// as a status-line strip rather than a glassy overlay. `#202020`.
pub const BG_HUD_BAND: Color = Color::srgba(0.125, 0.125, 0.125, 1.0);

/// Primary text — warm off-white. The base16-eighties foreground.
/// `#d0d0d0`.
pub const TEXT_PRIMARY: Color = Color::srgb(0.816, 0.816, 0.816);

/// High-contrast variant of [`TEXT_PRIMARY`] — `#f5f5f5`. Boosted
/// luminance for the Settings → Accessibility → High-contrast mode
/// toggle. Spec at `design-system.md` §Accessibility (#2).
pub const TEXT_PRIMARY_HC: Color = Color::srgb(0.961, 0.961, 0.961);

/// Secondary text — captions, hints, muted labels. `#a0a0a0`.
pub const TEXT_SECONDARY: Color = Color::srgb(0.627, 0.627, 0.627);

/// Disabled text — greyed-out buttons, locked items. `#505050`.
pub const TEXT_DISABLED: Color = Color::srgb(0.314, 0.314, 0.314);

/// Brick-red primary accent — the CTA colour of the system. Reserved
/// for primary actions (Play, Resume, Save), focus rings, and
/// selection. `#a54242` (base16-eighties `base08`). Pre-2026-05-08
/// this slot was cyan `#6fc2ef`; the swap was a project-wide
/// palette decision recorded in `design-system.md` and the
/// SESSION_HANDOFF entry that followed Option D.
pub const ACCENT_PRIMARY: Color = Color::srgb(0.647, 0.259, 0.259);

/// Brightened `ACCENT_PRIMARY` for hover states on primary buttons.
/// Picks up luminance while keeping the same hue. `#c25e5e`.
pub const ACCENT_PRIMARY_HOVER: Color = Color::srgb(0.761, 0.369, 0.369);

/// Lavender secondary accent — celebratory states (level-up,
/// achievement unlocked, streak milestones). Used sparingly so it stays
/// special. `#e1a3ee`.
pub const ACCENT_SECONDARY: Color = Color::srgb(0.882, 0.639, 0.933);

/// Success — foundation completion, valid drop tint, sync OK. Lime
/// from base16-eighties. `#acc267`.
pub const STATE_SUCCESS: Color = Color::srgb(0.675, 0.761, 0.404);

/// High-contrast variant of [`STATE_SUCCESS`] — `#c8e862`. Brighter
/// lime that maintains the success hue while lifting luminance from
/// ~0.51 → ~0.73 so the WIN MOVE scrub-bar marker stands out from
/// the bumped notch ticks (`BORDER_SUBTLE_HC` `#a0a0a0`, L≈0.60) in
/// high-contrast mode.
pub const STATE_SUCCESS_HC: Color = Color::srgb(0.784, 0.910, 0.384);

/// Warning — penalty signal, daily-seed expiry countdown, sync-pending
/// status. Gold from base16-eighties. **Both** Undo and Recycle
/// counters use this when non-zero. `#ddb26f`.
pub const STATE_WARNING: Color = Color::srgb(0.867, 0.698, 0.435);

/// Danger — rejection shake, illegal placement, sync error. Pink from
/// base16-eighties (also doubles as `suit-red` per the design.md
/// rationale). `#fb9fb1`.
pub const STATE_DANGER: Color = Color::srgb(0.984, 0.624, 0.694);

/// Info — neutral system toasts, sync-connected indicator. Teal from
/// base16-eighties. `#12cfc0`.
pub const STATE_INFO: Color = Color::srgb(0.071, 0.812, 0.753);

/// Soft fill colour for the drop-target overlay shown over every legal
/// destination pile while the player is dragging a card. Same lime hue
/// as `STATE_SUCCESS` (`#acc267`) so the visual language stays
/// consistent, but at 10 % alpha so the underlying card faces remain
/// fully readable through the wash.
pub const DROP_TARGET_FILL: Color = Color::srgba(0.675, 0.761, 0.404, 0.10);

/// Outline colour for the drop-target overlay. Matches the
/// `STATE_SUCCESS` hue at 75 % alpha so the rectangle border reads
/// unmistakably against both the felt and stacked card faces without
/// drowning the cards themselves.
pub const DROP_TARGET_OUTLINE: Color = Color::srgba(0.675, 0.761, 0.404, 0.75);

/// Thickness of the drop-target outline edges, in world-space pixels.
pub const DROP_TARGET_OUTLINE_PX: f32 = 3.0;

/// Sprite-space `Transform.z` for drop-target overlay entities. Sits
/// well above any static card (top stack z is `~1.04`) but well below
/// the lifted dragged stack (`DRAG_Z = 500.0` in `input_plugin`) so the
/// overlay never occludes the card the player is holding. Distinct from
/// the i32 `Z_*` UI-Node tokens above — those are `ZIndex` values for
/// `bevy::ui`, while this is a 2D `Sprite` z coordinate.
pub const Z_DROP_OVERLAY: f32 = 50.0;

/// Background colour of the stock-pile remaining-count chip.
///
/// Reuses `BG_ELEVATED_HI` so the chip reads as one rung above the
/// translucent stock pile marker without introducing a new palette
/// value. The badge sits on the stock corner so the player knows how
/// many cards remain before a recycle.
pub const STOCK_BADGE_BG: Color = BG_ELEVATED_HI;

/// Foreground (text) colour of the stock-pile remaining-count chip.
///
/// `ACCENT_PRIMARY` keeps the chip readable against the elevated
/// surface background and matches the primary accent already used
/// for other "look here" callouts.
pub const STOCK_BADGE_FG: Color = ACCENT_PRIMARY;

/// Sprite-space `Transform.z` for the stock-pile remaining-count chip.
///
/// Sits above the stock pile marker (`Z_PILE_MARKER` = `-1`) and any
/// face-down stock cards (which start at `0`), but well below
/// [`Z_DROP_OVERLAY`] (`50.0`) so the green drop-target wash always
/// renders on top while a card is being dragged. Like `Z_DROP_OVERLAY`,
/// this is a 2D `Sprite` z coordinate, not a `bevy::ui` `ZIndex`.
pub const Z_STOCK_BADGE: f32 = 30.0;

// ---------------------------------------------------------------------------
// Card drop-shadow — the subtle dark halo painted beneath every card so the
// play surface reads as physical instead of a flat collage of stickers. Idle
// values are deliberately low-contrast (small offset, ~25% alpha) so resting
// cards feel grounded without competing with focus rings or drop overlays.
// Drag values are slightly stronger (further offset, ~40% alpha, larger
// halo) so the dragged stack visually "lifts" off the felt.
// ---------------------------------------------------------------------------

/// RGB base for the per-card drop shadow. Always neutral black — never
/// suit-tinted — so the shadow never carries colour information that a
/// colour-blind player would rely on to identify a card. Alpha is applied
/// separately via [`CARD_SHADOW_ALPHA_IDLE`] / [`CARD_SHADOW_ALPHA_DRAG`].
pub const CARD_SHADOW_COLOR: Color = Color::srgb(0.0, 0.0, 0.0);

/// Alpha for the resting-state card shadow. Set to 0 under the Terminal
/// design system: depth is achieved through 1px suit-color borders and
/// tonal layering, not blur shadows ("no `box-shadow` anywhere" is a
/// hard constraint of `docs/ui-mockups/design-system.md`). The shadow
/// rendering code path is left in place so a future palette swap can
/// re-enable it without touching consumers.
pub const CARD_SHADOW_ALPHA_IDLE: f32 = 0.0;

/// Alpha for the lifted/dragged card shadow. Set to 0 for the same
/// reason as [`CARD_SHADOW_ALPHA_IDLE`]. Drag affordance under the
/// Terminal system is the primary-accent focus glow + z-index lift,
/// not a deeper shadow.
pub const CARD_SHADOW_ALPHA_DRAG: f32 = 0.0;

/// World-space pixel offset of the resting-state card shadow relative to
/// its parent card centre. Down-and-right matches a soft top-left light
/// source — the same convention used by the elevated-surface tones in the
/// rest of the palette.
pub const CARD_SHADOW_OFFSET_IDLE: Vec2 = Vec2::new(2.0, -3.0);

/// World-space pixel offset of the lifted/dragged card shadow. Roughly
/// double the idle offset so the parallax reads as "the card is further
/// from the table".
pub const CARD_SHADOW_OFFSET_DRAG: Vec2 = Vec2::new(4.0, -6.0);

/// Padding in pixels added to each axis of the card size when sizing the
/// resting-state shadow sprite. The shadow extends slightly past every
/// edge of the card so the dark border reads as a halo rather than a
/// matte rectangle behind the card.
pub const CARD_SHADOW_PADDING_IDLE: Vec2 = Vec2::new(4.0, 4.0);

/// Padding added to the card size when sizing the lifted/dragged shadow.
/// A slightly larger halo at the drag state reinforces the "lifted off
/// the felt" cue alongside the deeper offset and higher alpha.
pub const CARD_SHADOW_PADDING_DRAG: Vec2 = Vec2::new(8.0, 8.0);

/// Local `Transform.z` for the shadow child sprite, relative to its
/// parent `CardEntity`. Slightly negative so the shadow always renders
/// below the card itself even though it shares the parent's world z.
pub const CARD_SHADOW_LOCAL_Z: f32 = -0.05;

/// Subtle border — default popover, card, and idle button outline.
/// `outline-variant` from the design system at full alpha; the Terminal
/// aesthetic uses solid 1px borders rather than translucent washes.
/// `#353535`.
pub const BORDER_SUBTLE: Color = Color::srgba(0.208, 0.208, 0.208, 1.0);

/// High-contrast variant of [`BORDER_SUBTLE`] — `#a0a0a0`. Lifts
/// outlines from near-invisible to clearly visible for the
/// Settings → Accessibility → High-contrast mode toggle. Spec at
/// `design-system.md` §Accessibility (#2): outline jumps from
/// `#505050` to `#a0a0a0` so card borders, popover edges, and
/// focus rings are legible on low-quality displays / for low-
/// vision users.
pub const BORDER_SUBTLE_HC: Color = Color::srgba(0.627, 0.627, 0.627, 1.0);

/// Marker for entities whose [`BorderColor`] should swap to
/// [`BORDER_SUBTLE_HC`] when `Settings::high_contrast_mode` is on.
/// Tag any UI node where border legibility is accessibility-critical
/// — modal panels, popovers, settings rows, focus-ring carriers —
/// then add the `apply_high_contrast_borders` system to react to
/// settings changes.
///
/// `default_color` records the off-state colour the entity was
/// spawned with so the system can revert when HC is toggled back
/// off. Different sites use different defaults (`BORDER_SUBTLE` for
/// idle popover edges, `BORDER_STRONG` for active modal cards) — the
/// marker captures whichever one applies at this entity.
#[derive(bevy::prelude::Component, Debug, Clone, Copy)]
pub struct HighContrastBorder {
    /// Border colour to use when high-contrast mode is *off* — the
    /// site's normal idle / active-state colour.
    pub default_color: Color,
}

impl HighContrastBorder {
    /// Convenience constructor — `HighContrastBorder::with_default(BORDER_SUBTLE)`.
    pub const fn with_default(default_color: Color) -> Self {
        Self { default_color }
    }
}

/// Marker for entities whose [`BackgroundColor`] should swap to
/// [`BORDER_SUBTLE_HC`] when `Settings::high_contrast_mode` is on.
/// Parallel to [`HighContrastBorder`] but for sites that paint their
/// shape via `BackgroundColor` rather than `BorderColor` —
/// `bevy::ui` 1 px decorative strips, tick marks, fine separators
/// often render as tiny full-bleed `Node`s, not as borders, so the
/// border-marker pattern doesn't apply.
///
/// `default_color` records the off-state colour; `hc_color` the on-
/// state colour. [`with_default`] fills `hc_color` with
/// [`BORDER_SUBTLE_HC`] so the 90 % of sites that just need the
/// standard subtle-border bump can continue using a one-argument
/// constructor. [`with_hc`] overrides the HC colour for the rare
/// site (currently only the WIN MOVE scrub-bar marker) that needs a
/// domain-specific HC variant (`STATE_SUCCESS_HC` instead of a gray).
///
/// [`with_default`]: HighContrastBackground::with_default
/// [`with_hc`]: HighContrastBackground::with_hc
/// [`BackgroundColor`]: bevy::prelude::BackgroundColor
#[derive(bevy::prelude::Component, Debug, Clone, Copy)]
pub struct HighContrastBackground {
    /// Background colour to use when high-contrast mode is *off* —
    /// the site's normal idle / active-state colour.
    pub default_color: Color,
    /// Background colour to use when high-contrast mode is *on*.
    /// Defaults to [`BORDER_SUBTLE_HC`] via [`with_default`].
    ///
    /// [`with_default`]: HighContrastBackground::with_default
    pub hc_color: Color,
}

impl HighContrastBackground {
    /// Convenience constructor — HC colour defaults to
    /// [`BORDER_SUBTLE_HC`].
    pub const fn with_default(default_color: Color) -> Self {
        Self {
            default_color,
            hc_color: BORDER_SUBTLE_HC,
        }
    }

    /// Constructor for sites whose HC colour differs from the standard
    /// [`BORDER_SUBTLE_HC`]. Currently used by the WIN MOVE scrub-bar
    /// marker which bumps `STATE_SUCCESS` → `STATE_SUCCESS_HC` rather
    /// than to a neutral gray.
    pub const fn with_hc(
        default_color: Color,
        hc_color: Color,
    ) -> Self {
        Self {
            default_color,
            hc_color,
        }
    }
}

/// Strong border — hover outline, focused button, active popover.
/// `outline` from the design system. `#505050`.
pub const BORDER_STRONG: Color = Color::srgba(0.314, 0.314, 0.314, 1.0);

/// 2 px ring drawn around the focused interactive element. Brick-red
/// (matches `ACCENT_PRIMARY`) at 85% alpha so the ring stays legible
/// against both elevated surfaces and the modal scrim backdrop.
/// `rgba(165, 66, 66, 0.85)`.
pub const FOCUS_RING: Color = Color::srgba(0.647, 0.259, 0.259, 0.85);

// ---------------------------------------------------------------------------
// Typography scale (px) — 5 rungs replace the prior
// 14/15/16/17/18/22/26/28/30/32/40/48 jungle. All UI uses FiraMono via
// `FontResource`; sizes carry the hierarchy.
// ---------------------------------------------------------------------------

/// Display titles — Home, Win Summary, Onboarding header. 40 px.
pub const TYPE_DISPLAY: f32 = 40.0;

/// Modal / overlay headers. 26 px.
pub const TYPE_HEADLINE: f32 = 26.0;

/// Primary HUD numbers, button labels, body copy that needs weight.
/// 18 px.
pub const TYPE_BODY_LG: f32 = 18.0;

/// Secondary HUD, body copy, list items. 14 px.
pub const TYPE_BODY: f32 = 14.0;

/// Hotkey-hint chips, microcopy, dates. 11 px.
pub const TYPE_CAPTION: f32 = 11.0;

// ---------------------------------------------------------------------------
// Spacing scale (px) — 4-multiple rungs. Every padding, margin, and gap
// in the engine snaps to one of these.
// ---------------------------------------------------------------------------

/// 4 px — inline padding, chip gap.
pub const SPACE_1: f32 = 4.0;
/// 8 px — default gap between row items.
pub const SPACE_2: f32 = 8.0;
/// 12 px — standard button padding-X, action row gap.
pub const SPACE_3: f32 = 12.0;
/// 16 px — section gap inside a modal.
pub const SPACE_4: f32 = 16.0;
/// 24 px — modal card outer padding.
pub const SPACE_5: f32 = 24.0;
/// 32 px — block separator inside large overlays.
pub const SPACE_6: f32 = 32.0;
/// 48 px — outer modal margin from the window edge.
pub const SPACE_7: f32 = 48.0;

/// `Val::Px` form of `SPACE_1`, for ergonomic Node construction.
pub const VAL_SPACE_1: Val = Val::Px(SPACE_1);
/// `Val::Px` form of `SPACE_2`.
pub const VAL_SPACE_2: Val = Val::Px(SPACE_2);
/// `Val::Px` form of `SPACE_3`.
pub const VAL_SPACE_3: Val = Val::Px(SPACE_3);
/// `Val::Px` form of `SPACE_4`.
pub const VAL_SPACE_4: Val = Val::Px(SPACE_4);
/// `Val::Px` form of `SPACE_5`.
pub const VAL_SPACE_5: Val = Val::Px(SPACE_5);
/// `Val::Px` form of `SPACE_6`.
pub const VAL_SPACE_6: Val = Val::Px(SPACE_6);
/// `Val::Px` form of `SPACE_7`.
pub const VAL_SPACE_7: Val = Val::Px(SPACE_7);

// ---------------------------------------------------------------------------
// Border radius (px)
// ---------------------------------------------------------------------------

/// 4 px — hotkey chip, inline pill.
pub const RADIUS_SM: f32 = 4.0;
/// 8 px — buttons, popover panels.
pub const RADIUS_MD: f32 = 8.0;
/// 16 px — modal cards.
pub const RADIUS_LG: f32 = 16.0;

// ---------------------------------------------------------------------------
// Z-index hierarchy — replaces 8+ scattered magic numbers across plugins
// (background, pile markers, HUD, several overlay tiers, win cascade,
// toasts). Documented order:
//
//   background  →  pile markers  →  cards  →  HUD  →  HUD popovers  →
//   modal scrim → modal panel  →  pause  →  onboarding  →
//   win cascade  →  toasts (always on top)
// ---------------------------------------------------------------------------

pub const Z_BACKGROUND: i32 = -10;
pub const Z_PILE_MARKER: i32 = -1;
/// Base layer for HUD readouts (top-left).
pub const Z_HUD: i32 = 50;
/// Fullscreen transparent dismiss-backdrop spawned behind a HUD popover so
/// tapping outside it light-dismisses the panel without blocking other input.
pub const Z_HUD_POPOVER_BACKDROP: i32 = Z_HUD + 4;
/// HUD popovers (Modes dropdown, etc.) — above the dismiss backdrop.
pub const Z_HUD_POPOVER: i32 = Z_HUD + 5;
/// Transient HUD annotations (score-delta floaters) — above popovers.
pub const Z_HUD_TOP: i32 = Z_HUD + 10;
pub const Z_MODAL_SCRIM: i32 = 200;
pub const Z_MODAL_PANEL: i32 = 210;
/// Pause overlay outranks normal modals — pausing should always be on top.
pub const Z_PAUSE: i32 = 220;
/// Confirmation dialog stacked on top of the pause overlay (e.g. the
/// forfeit-confirm modal launched from the pause modal). Sits above
/// `Z_PAUSE` so the dialog is always visible over the paused state.
pub const Z_PAUSE_DIALOG: i32 = 225;
pub const Z_ONBOARDING: i32 = 230;
/// Z-layer for the keyboard focus indicator. Sits one rung above the
/// topmost modal layer (`Z_ONBOARDING`) so the ring is never occluded by
/// a modal card's hover state, while staying below the win cascade and
/// transient toasts that are allowed to overlay everything else.
pub const Z_FOCUS_RING: i32 = 240;
/// Win cascade sits between modals and toasts so the celebration plays
/// over a paused / mid-modal screen.
pub const Z_WIN_CASCADE: i32 = 300;
/// Toasts always render above everything else.
pub const Z_TOAST: i32 = 400;

// ---------------------------------------------------------------------------
// Motion — durations in seconds at `AnimSpeed::Normal`. `Fast` halves
// every value, `Instant` zeroes them. Use `scaled_duration` to apply.
// ---------------------------------------------------------------------------

/// Card slide during gameplay — tweened with `MotionCurve::SmoothSnap`
/// (ease-out-cubic). 180 ms; bumped from the old 150 ms because cubic
/// feels slower at endpoints — the perceived speed is unchanged.
pub const MOTION_SLIDE_SECS: f32 = 0.18;

/// Settle bounce on placement — only the moved card, not every top
/// card on every state change. 180 ms.
pub const MOTION_SETTLE_SECS: f32 = 0.18;

/// Shake on rejected drop — tightened from 300 ms; frequency drops to
/// 35 rad/s to match the new settle bounce so the two feedback signals
/// no longer feel discordant. 250 ms.
pub const MOTION_SHAKE_SECS: f32 = 0.25;

/// Shake angular frequency in rad/s.
pub const MOTION_SHAKE_OMEGA: f32 = 35.0;

/// Duration of the smooth return tween when a drag is rejected by an
/// invalid drop target. Short enough to feel snappy but long enough to
/// read as motion rather than a teleport.
pub const MOTION_DRAG_REJECT_SECS: f32 = 0.15;

/// Card flip — half-time per phase (squash + grow). 100 ms each =
/// 200 ms total. Pair with a ±8° Z-rotation at the midpoint for a 3D
/// feel without 3D rendering.
pub const MOTION_FLIP_HALF_SECS: f32 = 0.10;

/// Per-card stagger on the new-game deal animation — centre value;
/// each card gets ±10% jitter applied at deal time so the deal feels
/// organic instead of mechanical. 40 ms.
pub const MOTION_DEAL_STAGGER_SECS: f32 = 0.04;

/// Deal slide duration with an `ease-out` curve and a 40 ms
/// scale-pop on land so cards "arrive" instead of just stopping.
/// 280 ms.
pub const MOTION_DEAL_SLIDE_SECS: f32 = 0.28;

/// Win cascade per-card stagger — slightly slower than the prior
/// 50 ms for a more theatrical feel. 60 ms.
pub const MOTION_CASCADE_STAGGER_SECS: f32 = 0.06;

/// Win cascade per-card slide — uses `MotionCurve::Expressive`
/// (overshoot) plus ±15° Z-rotation. 500 ms.
pub const MOTION_CASCADE_SLIDE_SECS: f32 = 0.50;

/// Per-line stagger between score-breakdown rows during the win modal
/// reveal animation, in seconds.
pub const MOTION_SCORE_BREAKDOWN_STAGGER_SECS: f32 = 0.15;

/// Per-line fade-in duration during the win modal score reveal, in
/// seconds.
pub const MOTION_SCORE_BREAKDOWN_FADE_SECS: f32 = 0.12;

/// Screen shake on win — wider and longer than the old 0.6 s / 8 px.
/// 800 ms.
pub const MOTION_WIN_SHAKE_SECS: f32 = 0.80;

/// Peak displacement of the win screen shake. 12 px.
pub const MOTION_WIN_SHAKE_AMPLITUDE: f32 = 12.0;

/// Toast in — scale-from-0.92-to-1.0 fade-in. 200 ms.
pub const MOTION_TOAST_IN_SECS: f32 = 0.20;

/// Toast out — fade. 250 ms.
pub const MOTION_TOAST_OUT_SECS: f32 = 0.25;

/// Modal in/out — scale-from-0.96-to-1.0 + scrim fade. 220 ms.
pub const MOTION_MODAL_SECS: f32 = 0.22;

/// Button hover/press colour blend — short, snappy. 100 ms.
pub const MOTION_BUTTON_BLEND_SECS: f32 = 0.10;

/// Score-pulse — when score increases by ≥ 50, briefly scale the
/// readout 1.0 → 1.1 → 1.0. 250 ms.
pub const MOTION_SCORE_PULSE_SECS: f32 = 0.25;

/// Foundation-completion flourish — when a King lands on a foundation
/// pile (Ace → King, 13 cards), briefly scale the King card 1.0 →
/// [`FOUNDATION_FLOURISH_PEAK_SCALE`] → 1.0 and tint the matching
/// `PileMarker` gold. 400 ms.
pub const MOTION_FOUNDATION_FLOURISH_SECS: f32 = 0.4;

/// Peak scale magnification reached at the midpoint of the
/// foundation-completion flourish. The triangular curve climbs from
/// 1.0 at `t=0` to this value at `t=0.5` and back to 1.0 at `t=1.0`.
pub const FOUNDATION_FLOURISH_PEAK_SCALE: f32 = 1.15;

/// Total duration of the streak-milestone flourish on the HUD score
/// readout, in seconds. Mirrors the foundation flourish in feel — a
/// brief celebratory pulse that does not block subsequent gameplay.
pub const MOTION_STREAK_FLOURISH_SECS: f32 = 0.6;

/// Peak scale magnification reached at the midpoint of the streak
/// flourish (1.0 → this → 1.0). Larger than the foundation flourish
/// peak so the lifetime-streak celebration reads as a bigger deal than
/// the per-suit completion.
pub const STREAK_FLOURISH_PEAK_SCALE: f32 = 1.20;

/// Win-streak counts that trigger the flourish. The flourish fires
/// only when the streak crosses a threshold from below — never at
/// every win past the highest threshold. Static for now; could become
/// a `Settings`-tunable list later if play-testing surfaces it.
pub const STREAK_MILESTONES: &[u32] = &[3, 5, 10];

/// Loading-ellipsis cycle — `.`/`..`/`...` toggles every step.
/// 400 ms.
pub const MOTION_LOADING_TICK_SECS: f32 = 0.40;

/// Period of the focus-ring breathing pulse, in seconds.
///
/// The keyboard focus ring's alpha is modulated by a sin-curve over this
/// interval so the indicator gently "breathes" instead of presenting as
/// a flat outline. 1.4 s reads as a calm heartbeat — slow enough that
/// the motion is in the player's peripheral vision rather than competing
/// for attention, fast enough that a focus change still draws the eye.
/// Not run through [`scaled_duration`]: the pulse is an accessibility
/// affordance, not gameplay motion. `AnimSpeed::Instant` is honoured at
/// the system level by skipping the pulse entirely (see
/// `pulse_focus_overlay` in `ui_focus`).
pub const MOTION_FOCUS_PULSE_SECS: f32 = 1.4;

/// Hover delay before a tooltip appears, in seconds. Long enough that
/// players gliding the cursor across the HUD don't see flicker; short
/// enough that "stop and read" feels responsive. Not run through
/// [`scaled_duration`] — `AnimSpeed` controls gameplay motion, not the
/// hover-discoverability budget for help text.
pub const MOTION_TOOLTIP_DELAY_SECS: f32 = 0.5;

/// Total visible duration of the splash screen overlay, in seconds.
/// Composed of a fade-in, a hold, and a fade-out — see
/// [`MOTION_SPLASH_FADE_SECS`] for the per-edge fade budget. Not run
/// through [`scaled_duration`]: the splash is a one-shot brand beat at
/// app start, not gameplay motion that should track `AnimSpeed`.
pub const MOTION_SPLASH_TOTAL_SECS: f32 = 1.6;

/// Fade-in and fade-out duration of the splash overlay, in seconds.
/// The hold time is `MOTION_SPLASH_TOTAL_SECS - 2 * MOTION_SPLASH_FADE_SECS`.
/// Mirroring fade-in and fade-out keeps the curve symmetric so the brand
/// beat reads as a single dissolve instead of two separate animations.
pub const MOTION_SPLASH_FADE_SECS: f32 = 0.3;

// ---------------------------------------------------------------------------
// Z-index — tooltip layer
// ---------------------------------------------------------------------------

/// Z-layer for tooltips. Sits one rung above the focus ring so a
/// tooltip rendered over a focused button is never occluded by the
/// button's outline. Still below `Z_WIN_CASCADE` and `Z_TOAST` so the
/// celebration and notification layers stay on top.
pub const Z_TOOLTIP: i32 = Z_FOCUS_RING + 10;

/// Z-layer for the launch splash overlay. The splash owns the entire
/// viewport for ~1.6 s before fading out, so it sits above every other
/// UI rung — including `Z_TOAST` — to guarantee the brand beat is
/// never occluded by a stray toast or tooltip. Neither toasts nor the
/// win cascade can fire during the splash window in practice (no game
/// has run yet, no toast queue has dispatched), but the relative order
/// is kept tidy in case a future feature schedules either at startup.
pub const Z_SPLASH: i32 = Z_TOAST + 100;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Scales a `MOTION_*_SECS` value by the player's animation-speed
/// preference. `Normal` × 1.0, `Fast` × 0.5, `Instant` → 0.
///
/// Pass any duration constant from this module through this helper
/// before handing it to a tween. The audit found that only slide and
/// cascade respected `AnimSpeed`; toasts, deal stagger, shake, and
/// settle were hardcoded. Routing every duration through this function
/// fixes that.
pub fn scaled_duration(secs: f32, speed: AnimSpeed) -> f32 {
    match speed {
        AnimSpeed::Normal => secs,
        AnimSpeed::Fast => secs * 0.5,
        AnimSpeed::Instant => 0.0,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Every spacing rung is a positive multiple of 4 — keeps the scale
    /// honest if someone tweaks values later.
    #[test]
    fn spacing_scale_is_a_4_multiple_geometric_progression() {
        for v in [
            SPACE_1, SPACE_2, SPACE_3, SPACE_4, SPACE_5, SPACE_6, SPACE_7,
        ] {
            assert!(v > 0.0, "spacing tokens must be positive");
            assert!(
                (v.rem_euclid(4.0)).abs() < f32::EPSILON,
                "spacing token {v} must be a 4-multiple"
            );
        }
    }

    /// Type scale is monotonically decreasing display → caption.
    #[test]
    fn type_scale_is_monotonically_decreasing() {
        let scale = [
            TYPE_DISPLAY,
            TYPE_HEADLINE,
            TYPE_BODY_LG,
            TYPE_BODY,
            TYPE_CAPTION,
        ];
        for window in scale.windows(2) {
            assert!(
                window[0] > window[1],
                "type scale must be monotonically decreasing: {} should be > {}",
                window[0],
                window[1]
            );
        }
    }

    /// Z-index hierarchy is monotonically increasing through documented
    /// layers, so a future add-a-layer change can't accidentally land
    /// in the wrong slot.
    #[test]
    fn z_index_hierarchy_is_monotonically_increasing() {
        let layers = [
            Z_BACKGROUND,
            Z_PILE_MARKER,
            Z_HUD,
            Z_HUD_POPOVER_BACKDROP,
            Z_HUD_POPOVER,
            Z_HUD_TOP,
            Z_MODAL_SCRIM,
            Z_MODAL_PANEL,
            Z_PAUSE,
            Z_PAUSE_DIALOG,
            Z_ONBOARDING,
            Z_FOCUS_RING,
            Z_TOOLTIP,
            Z_WIN_CASCADE,
            Z_TOAST,
            Z_SPLASH,
        ];
        for window in layers.windows(2) {
            assert!(
                window[0] < window[1],
                "z-index hierarchy must be monotonically increasing: {} should be < {}",
                window[0],
                window[1]
            );
        }
    }

    #[test]
    fn scaled_duration_matches_anim_speed() {
        assert!((scaled_duration(0.18, AnimSpeed::Normal) - 0.18).abs() < f32::EPSILON);
        assert!((scaled_duration(0.18, AnimSpeed::Fast) - 0.09).abs() < f32::EPSILON);
        assert_eq!(scaled_duration(0.18, AnimSpeed::Instant), 0.0);
    }
}

/// System set for text/UI visual effects that animate `Transform`/`Sprite`
/// on chrome entities (HUD score pulse, streak flourish, modal enter, focus
/// ring). These never touch board entities, so members are declared
/// `.ambiguous_with(BoardVisuals)` and `.ambiguous_with(UiTextFx)` — the
/// entity domains are disjoint by construction and relative order within a
/// frame is invisible (#143).
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct UiTextFx;
