*, *::before, *::after {
    box-sizing: border-box;
}

:root {
    /* Design tokens — .planning/CLAUDE.md */
    --ink: #372F52;
    --muted: #7D7796;
    --faint: #B3ACC9;
    --primary: #8A7CD8;
    --primary-deep: #6A5BC2;
    --lilac: #C9BEF0;
    --pink: #E28FB0;
    --gold: #F6D06B;
    --star: #E8A94A;
    --star-off: #E9E5F2;
    --page: #FFFDFC;
    --surface: #FBFAFD;
    --surface-2: #F4F2FA;
    --border: #EDEAF4;
    --outside: #EDEAF4;
    --cta-gradient: linear-gradient(135deg, #8A7CD8, #E28FB0);
    --cta-disabled: #D9D3EC;

    --font-heading: 'Quicksand', sans-serif;
    --font-body: 'Nunito Sans', sans-serif;

    --radius-sm: 16px;
    --radius-lg: 20px;

    --safe-top: env(safe-area-inset-top);
    --safe-bottom: env(safe-area-inset-bottom);
}

body {
    margin: 0;
    /* Mobile: identical to .phone's --page so overscroll/bounce shows no seam. The
       lilac --outside frame is desktop-only (set on .phone-outside in the 483px query). */
    background: var(--page);
    font-family: var(--font-body);
    color: var(--ink);
    line-height: 1.5;
}

/* Shared "primary" button skin — issue #63. Each primary/disabled button used to duplicate this
   ~200-char CSS as a literal JS ternary string inside a double-quoted :style attribute, which is
   what kept reintroducing the font-family quoting bug (#49, #56, #57, #64: an unescaped double
   quote inside the ternary's CSS text prematurely closes the outer :style="...". Buttons now
   toggle these classes instead, so there's exactly one place to get quoting/values right. Per-button
   sizing (width/height/border-radius/font-size/margin-top) still varies, so it stays as a plain
   static style="..." attribute alongside class/:class — those values are never interpolated. */
.btn-primary {
    border: none;
    color: #fff;
    font-family: var(--font-heading);
    font-weight: 700;
    cursor: pointer;
    background: var(--cta-gradient);
}
/* Truly disabled (button also carries :disabled="!x") — dims and drops the pointer cursor. */
.btn-primary.is-disabled {
    background: var(--cta-disabled);
    cursor: default;
}
/* Not disabled, just visually dimmed while a selection/condition isn't met yet — stays clickable. */
.btn-primary.is-dimmed {
    background: var(--cta-disabled);
}
/* One-off "blocked" skin (add_wish.html) that also swaps the text color instead of just the background. */
.btn-primary.is-blocked {
    background: #EDE9F8;
    color: var(--muted);
    cursor: default;
}

/* Shared "secondary"/Cancel button skin — issue #67, same DRY motivation as .btn-primary above
   (#63): this exact border/background/color/font-family/font-weight/cursor combo was duplicated
   as a literal style="..." string across 9 Cancel/secondary buttons in 7 template files. Unlike
   .btn-primary's ternaries, these were always plain strings (no quoting-bug risk), so this is a
   maintainability-only extraction. Per-button sizing (flex/width/height/border-radius/font-size)
   still varies, so it stays as a plain static style="..." attribute alongside the class. */
.btn-secondary {
    border: 1.5px solid var(--border);
    background: #fff;
    color: var(--ink);
    font-family: var(--font-heading);
    font-weight: 700;
    cursor: pointer;
}

/* Scrollbar hiding — copied from design/Wish Journey.dc.html */
.wj-no-scrollbar::-webkit-scrollbar { display: none; }
/* Session 14: keeps a horizontal swipe at a carousel's scroll boundary from chaining into the
   WebView's own vertical bounce/pull-to-refresh gesture. Every horizontal scroll-snap carousel
   in this app (welcome.html, insights.html) already carries this class. */
.wj-no-scrollbar { overscroll-behavior: contain; }
*::-webkit-scrollbar { display: none; }
* { scrollbar-width: none; -ms-overflow-style: none; }

/* Keyframes — copied verbatim from design/Wish Journey.dc.html (lines 21-36) */
@keyframes wjPulse{0%{transform:scale(1);opacity:0.55}70%{transform:scale(2.2);opacity:0}100%{transform:scale(2.2);opacity:0}}
@keyframes wjBar{0%,100%{transform:scaleY(0.3)}50%{transform:scaleY(1)}}
@keyframes wjFloat{0%{transform:translateY(0) scale(1);opacity:0.8}100%{transform:translateY(-140px) scale(1.3);opacity:0}}
@keyframes wjPop{0%{transform:scale(0.4);opacity:0}60%{transform:scale(1.08);opacity:1}100%{transform:scale(1);opacity:1}}
@keyframes wjCheck{0%{stroke-dashoffset:40}100%{stroke-dashoffset:0}}
@keyframes wjFadeSlide{0%{opacity:0;transform:translateY(10px)}100%{opacity:1;transform:translateY(0)}}
@keyframes wjGentleFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}
@keyframes wjConfettiFloat{0%{opacity:0;transform:translate(0,0) rotate(0deg) scale(0.5)}16%{opacity:1;transform:translate(var(--wjMid)) rotate(calc(var(--wjRot) * 0.35)) scale(1)}100%{opacity:0;transform:translate(var(--wjEnd)) rotate(var(--wjRot)) scale(0.85)}}
@keyframes wjConfettiLayerFade{0%{opacity:1}100%{opacity:0}}
@keyframes wjMascotPeek{0%{opacity:0;transform:translateY(8px) scale(0.7)}100%{opacity:1;transform:translateY(0) scale(1)}}
@keyframes wjFlip{0%{transform:rotateY(0deg)}100%{transform:rotateY(180deg)}}
@keyframes wjSpin{to{transform:rotate(360deg)}}
@keyframes wjSplashLogo{0%{opacity:0;transform:scale(0.82)}65%{opacity:1;transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
@keyframes wjSplashGlow{0%{opacity:0;transform:scale(0.8)}60%{opacity:0.9}100%{opacity:0.55;transform:scale(1)}}
@keyframes wjSplashOut{0%{opacity:1}100%{opacity:0}}
@keyframes wjSparkle{0%{opacity:0;transform:scale(0.3) rotate(0deg)}30%{opacity:1;transform:scale(1) rotate(20deg)}60%{opacity:1;transform:scale(0.85) rotate(15deg)}100%{opacity:0;transform:scale(0.5) rotate(30deg)}}

/* App-launch splash — templates/_shell.html (#wj-splash). Static positioning lives here
   rather than an inline style="" because that element also carries an Alpine :style
   binding for the fade-out; a static style="" + :style="" pair on the same element makes
   Alpine's style merge clobber the static properties (see CLAUDE.md's recurring
   style/:style quoting-bug note, and #64/#68/#70) -- position/z-index/display/background
   silently vanish and the splash collapses to zero height instead of covering the screen. */
.wj-splash-overlay {
    position: absolute;
    inset: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(180deg, #F7F2FB 0%, #EFE7F7 100%);
}

/* Phone shell — templates/_shell.html
   Mobile-first: on a real device the app is full-bleed edge-to-edge and fills the
   viewport (100dvh, which tracks the mobile URL bar / toolbars — 100vh does not).
   The 402px / 874px phone-frame look is desktop/tablet-only (min-width: 483px). */
.phone-outside {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    align-items: stretch;
    justify-content: center;
}

.phone {
    width: 100%;
    max-width: 100%;
    height: 100vh;
    height: 100dvh;
    background: var(--page);
    position: relative;
    overflow: hidden;
    font-family: var(--font-body);
}

/* Wish card delete affordance (templates/partials/wish_card.html) — "revealed on hover/
   long-press" (session 08 task 2). ponytail: no JS long-press gesture; hover covers desktop/
   mouse, and touch devices (no hover capability) just get it always faintly visible so it's
   still discoverable/tappable. Add a real long-press directive later if user testing shows the
   faint always-on state isn't enough. */
.wish-card-delete { opacity: 0; transition: opacity 0.15s; }
.wish-card:hover .wish-card-delete { opacity: 1; }
/* Keyboard focus must reveal it too -- an invisible focused control is a real trap, not just a
   cosmetic miss (session 13 task 7). */
.wish-card-delete:focus-visible { opacity: 1; }
@media (hover: none) {
    .wish-card-delete { opacity: 0.55; }
}

/* Accessibility (SPEC.md session 13 task 7). */

/* iOS Safari auto-zooms the viewport on focus when a text input is <16px. Force every
   text-entry control to >=16px in one place; !important beats the per-input inline styles
   the same way prefers-reduced-motion below does. */
input:not([type="checkbox"]):not([type="file"]):not([type="hidden"]),
select,
textarea {
    font-size: 16px !important;
}

/* Visible focus rings -- one rule covers every real <a>/<button>/<input>/<select>/[tabindex] in
   the app; nothing here needs per-component focus styling. :focus-visible (not :focus) so a
   mouse click doesn't show a ring, only keyboard/AT navigation does. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--primary-deep);
    outline-offset: 2px;
    border-radius: 4px;
}

/* prefers-reduced-motion kills the splash/confetti/float animations (and everything else built
   from inline style="animation:...") in one place -- an !important rule in an external
   stylesheet beats a non-!important inline style, so this needs no per-template change. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }
}

/* Screen-reader-only text (e.g. an icon-only control's spoken label when a visible caption
   would be redundant/too tight to fit). */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Larger viewports (tablet/desktop browser preview): render the app as a centered
   phone frame. Height is capped to the viewport so the frame always fits — never
   overflows a short window (the old fixed 874px did). min() picks the smaller of
   the ideal phone height and the available viewport height. */
@media (min-width: 483px) {
    .phone-outside {
        align-items: center;
        padding: 20px;
        background: var(--outside);
    }
    .phone {
        width: min(402px, 100%);
        max-width: 402px;
        height: min(874px, calc(100dvh - 40px));
        max-height: calc(100dvh - 40px);
        border-radius: 40px;
        box-shadow: 0 20px 60px rgba(55, 47, 82, 0.18);
        overflow-y: auto;
    }
}
