:root {
  /* DESIGN.md color tokens */
  --bg-base: #150c2e;
  --bg-elev: #201338;
  --surface-glass: rgba(38, 24, 63, 0.65);
  --surface-glass-border: rgba(255, 255, 255, 0.08);
  --accent-violet: #7c5cff;
  --accent-pink: #ff5f8f;
  --text-primary: #f4f1fb;
  --text-secondary: #b0a6c9;
  --text-disabled: #8a84a8;
  --danger: #ff5f5f;

  /* Count-number gradient text (Hero Card + Compact Row): defaults to the
     brand gradient in dark mode. Overridden to deeper, contrast-safe
     values in light mode below, since the brand gradient itself doesn't
     hold WCAG contrast as *text* against a light page background. */
  --count-grad-1: var(--accent-violet);
  --count-grad-2: var(--accent-pink);

  /* DESIGN.md rounded scale */
  --radius-sm: 10px;
  --radius-md: 16px;
  --radius-lg: 24px;

  /* DESIGN.md spacing scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 28px;
  --space-7: 40px;

  color-scheme: light dark;
}

/* Story 1.9: light mode, follows OS/browser preference. Brand gradient
   (accent-violet/accent-pink) is unchanged — it's only ever used as a
   background fill behind forced-white text (header, FAB, primary button),
   never as text color, so it doesn't need a contrast-driven variant. */
@media (prefers-color-scheme: light) {
  :root {
    --bg-base: #f3f0fa;
    --bg-elev: #ffffff;
    --surface-glass: rgba(255, 255, 255, 0.65);
    --surface-glass-border: rgba(36, 26, 61, 0.1);
    --text-primary: #241a3d;
    --text-secondary: #4f4470;
    --text-disabled: #6b6088;
    --danger: #c8102e;
    --count-grad-1: #6a3ff0;
    --count-grad-2: #c91f66;
  }
}

* { box-sizing: border-box; }

.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;
}

/* `html`'s background paints the canvas -- the whole scrollable/visible
   area, not just this element's own box -- which is what actually
   matters here. `body`'s own min-height (below) is layout-only, no
   background of its own: on iOS Safari, a page shorter than the
   *largest* possible viewport (address bar hidden) can reveal area
   below body's `100dvh`-computed box before/while `dvh` re-resolves as
   the chrome collapses on scroll -- confirmed live on the empty-state
   screen (short content, nothing to scroll to normally). A background
   confined to body's box leaves that strip showing the UA's own
   `color-scheme` canvas default (paints white in light mode) instead
   of --bg-base. Canvas-level painting via `html` isn't bounded by any
   box's computed height, so it holds regardless of the exact dvh
   timing quirk. */
html {
  margin: 0;
  padding: 0;
  background: var(--bg-base);
  min-height: 100%;
}

body {
  margin: 0;
  padding: 0;
  padding-bottom: env(safe-area-inset-bottom);
  color: var(--text-primary);
  font-family: "Heebo", -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
  min-height: 100vh;
  min-height: 100dvh;
}

.app-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: calc(env(safe-area-inset-top) + 16px) 20px 16px;
  /* Story 1.4: a flat white foreground can't hit 4.5:1 against the whole
     brand gradient -- white-on-accent-pink alone is ~2.9:1 (confirmed by
     WCAG relative-luminance calc, see DESIGN.md's corrected note on this).
     Rather than changing the shared --accent-violet/--accent-pink token
     values (used unscrimmed elsewhere: buttons, Hero Card halo), a
     header-local dark scrim is layered on top -- this does visibly deepen
     the header's own gradient, but leaves the tokens themselves, and every
     other place they're used, untouched. White text/icons now clear
     5.38:1+ at every point across the header, measured worst-case. */
  background:
    linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)),
    linear-gradient(135deg, var(--accent-violet), var(--accent-pink));
  position: sticky;
  top: 0;
  z-index: 5;
}

.app-header h1 {
  font-size: 1.3rem;
  margin: 0;
  color: #fff;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.btn-icon {
  /* 48px: minimum 48dp touch target (Story 1.5), not the 40px original */
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: none;
  background: rgba(255,255,255,0.2);
  font-size: 1.2rem;
  cursor: pointer;
}

.fab {
  /* 48px: minimum 48dp touch target (Story 1.5), not the 42px original */
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: none;
  background: #fff;
  color: var(--accent-violet);
  font-size: 1.6rem;
  line-height: 1;
  font-weight: bold;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0,0,0,0.35);
}

.hero-card {
  /* Elevated glass treatment reserved for the single nearest-event tile
     (see .event-card's backdrop-filter note) -- stronger blur than the
     dialog's (16px) to lead the visual hierarchy, plus a subtle brand-
     gradient glow (not a heavy shadow, per DESIGN.md depth rules). */
  width: calc(100% - 32px);
  max-width: 560px;
  margin: 16px auto 0;
  background: var(--surface-glass);
  border: 1px solid var(--surface-glass-border);
  border-radius: var(--radius-lg);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  box-shadow: 0 0 40px rgba(124, 92, 255, 0.25);
  padding: 24px 20px;
  display: flex;
  align-items: center;
  gap: 16px;
  cursor: pointer;
  min-height: 48px;
  transition: transform 0.15s ease;
}

.hero-card[hidden] {
  /* [hidden] and .hero-card have equal CSS specificity, and this
     author-stylesheet rule for the latter would otherwise win over the
     UA default (display: none) -- state it explicitly so hiding actually
     hides. */
  display: none;
}

.hero-card:active { transform: scale(0.98); }

.hero-card:focus-visible,
.event-card:focus-visible,
.presets-list li:focus-visible {
  outline: 2px solid var(--accent-pink);
  outline-offset: 2px;
}

/* Story 1.5: a past row's opacity:0.55 dimming (Story 1.3, unchanged)
   also dims its own focus ring, since opacity applies to everything the
   element renders. Rather than restructuring the dimming itself, pop the
   row to full opacity only while it holds keyboard focus -- the default
   (unfocused) look is untouched. At opacity:1 this row's focus ring is
   pixel-identical to every other row's already-accepted focus treatment
   (same outline color/width against the same surface-glass background),
   so no separate contrast measurement is needed here. */
.event-card.past:focus-visible {
  opacity: 1;
}

.hero-emoji {
  font-size: 2.5rem;
  flex-shrink: 0;
}

.hero-info {
  flex: 1;
  min-width: 0;
}

.hero-name {
  font-weight: 700;
  font-size: 1.2rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.hero-date {
  color: var(--text-secondary);
  font-size: 0.95rem;
  margin-top: 4px;
}

.hero-count {
  text-align: center;
  flex-shrink: 0;
  min-width: 84px;
}

.hero-count .num {
  font-size: clamp(3.5rem, 12vw, 4.5rem);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(135deg, var(--count-grad-1), var(--count-grad-2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  display: block;
  line-height: 1;
}

.hero-count .label {
  font-size: 0.85rem;
  color: var(--text-secondary);
}

.event-list {
  padding: 16px;
  /* Story 3.1: room for the fixed Ad Banner so it never overlaps the
     last row. Static (not toggled when ads are removed) -- a little
     extra empty space at the bottom post-purchase is a minor cosmetic
     nit, not worth the added complexity of syncing this to billing.js's
     display decision. Matches .ad-banner's now-fixed 56px height
     exactly -- no guesswork, since the banner can no longer grow. */
  padding-bottom: calc(56px + 16px + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 560px;
  margin: 0 auto;
  overflow-x: hidden; /* Story 1.7: defense-in-depth against a dragged row (clamped in JS) still widening the page */
}

.event-card {
  /* no backdrop-filter here by design: blur is reserved for the elevated
     Hero Card treatment (Story 1.2) to avoid stacking blur across many
     simultaneously-rendered scrolling rows. rounded-md, not -lg (Story
     1.3) -- Hero/dialog keep the larger radius, Compact Row doesn't. */
  background: var(--surface-glass);
  border-radius: var(--radius-md);
  padding: 16px 18px;
  display: flex;
  align-items: center;
  gap: 14px;
  cursor: pointer;
  border: 1px solid var(--surface-glass-border);
  min-height: 48px;
  transition: transform 0.15s ease, border-color 0.15s ease;
  /* Story 1.7: hint the browser that vertical panning (page scroll) is
     native/expected on this element, while horizontal movement is ours
     (swipe-to-delete) to interpret via pointer events in app.js. */
  touch-action: pan-y;
}

/* Story 1.7: swipe-to-delete drag state. While actively dragging, the
   transition is off so translateX tracks the pointer with no lag; the
   border recolors once the drag has crossed the delete threshold, as
   feedback before the confirmation dialog opens on release. */
.event-card.swiping {
  transition: none;
  /* A mouse/pointer drag over text naturally starts a text selection
     unless suppressed -- distracting during a swipe gesture. */
  user-select: none;
}

.event-card.swipe-armed {
  border-color: var(--danger);
}

/* Same reasoning as .event-card.past:focus-visible (Story 1.5): the
   armed-state warning border would itself be dimmed by opacity:0.55 on
   exactly the rows most likely to be deleted. */
.event-card.past.swipe-armed {
  opacity: 1;
}

.event-card:active { transform: scale(0.98); }

.event-card.past {
  opacity: 0.55;
}

.event-emoji {
  font-size: 1.8rem;
  flex-shrink: 0;
}

.event-info {
  flex: 1;
  min-width: 0;
}

.event-name {
  font-weight: 600;
  font-size: 1.05rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.event-date {
  color: var(--text-secondary);
  font-size: 0.85rem;
  margin-top: 2px;
}

.event-count {
  text-align: center;
  flex-shrink: 0;
  min-width: 64px;
}

.event-count .num {
  font-size: 1.6rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(135deg, var(--count-grad-1), var(--count-grad-2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  display: block;
  line-height: 1;
}

.event-count .label {
  font-size: 0.7rem;
  color: var(--text-secondary);
}

.empty-state {
  text-align: center;
  color: var(--text-secondary);
  margin-top: 60px;
  line-height: 1.8;
  padding: 0 20px;
}

dialog {
  border: none;
  border-radius: var(--radius-lg);
  padding: 0;
  background: var(--surface-glass);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  color: var(--text-primary);
  width: min(420px, 92vw);
}

dialog::backdrop {
  background: rgba(0,0,0,0.55);
}

#eventForm, .presets-content {
  padding: 22px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

dialog h2 {
  margin: 0 0 8px;
  font-size: 1.2rem;
}

label {
  font-size: 0.85rem;
  color: var(--text-secondary);
  margin-top: 6px;
}

input, select {
  background: var(--surface-glass);
  border: 1px solid var(--surface-glass-border);
  color: var(--text-primary);
  border-radius: var(--radius-sm);
  padding: 12px;
  font-size: 1rem;
  min-height: 48px; /* Story 1.5: 48dp touch target */
  width: 100%;
}

.dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 16px;
  flex-wrap: wrap;
}

button {
  font-family: inherit;
}

.btn-primary, .btn-secondary, .btn-danger {
  border: none;
  border-radius: var(--radius-sm);
  padding: 11px 18px;
  font-size: 0.95rem;
  min-height: 48px; /* Story 1.5: 48dp touch target */
  cursor: pointer;
}

.btn-primary {
  /* Story 3.2: same white-on-gradient contrast failure Story 1.4 found
     and fixed for .app-header (white on raw accent-pink measures ~2.9:1,
     below even the 3:1 large-text floor) turns out to also apply to this
     button -- Story 1.4 scoped its scrim fix to the header only and
     explicitly left buttons unaddressed. Confirmed failing again here
     while building the purchase button (arguably the highest-stakes
     button in the app for legibility), so applying the same fix broadly
     to .btn-primary -- this also corrects the pre-existing "שמירה"
     button, not just the new one. Same 30% scrim, same measured
     worst-case >=5.38:1 as the header. */
  background:
    linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)),
    linear-gradient(135deg, var(--accent-violet), var(--accent-pink));
  color: #fff;
  font-weight: 600;
}

.btn-secondary {
  background: rgba(255,255,255,0.08);
  color: var(--text-primary);
}

.btn-danger {
  background: transparent;
  color: var(--danger);
  margin-inline-end: auto;
}

.presets-hint {
  color: var(--text-secondary);
  font-size: 0.85rem;
  margin: -4px 0 4px;
}

.presets-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 50vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.presets-list li {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--surface-glass);
  border: 1px solid var(--surface-glass-border);
  border-radius: var(--radius-md);
  padding: 10px 14px;
  min-height: 48px; /* Story 1.5: 48dp touch target */
  cursor: pointer;
}

.presets-list li .p-name {
  flex: 1;
  font-weight: 500;
}

.presets-list li .p-date {
  color: var(--text-secondary);
  font-size: 0.8rem;
}

/* Story 2.3: Settings Row -- label at the reading-start side (right in
   RTL, via source order: label markup comes first), switch at the end
   (left), matching the same source-order convention as .app-header. */
.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--surface-glass);
  border: 1px solid var(--surface-glass-border);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  min-height: 48px;
}

.settings-row-label {
  /* Overrides the generic label rule's size (0.85rem->1rem), color
     (--text-secondary->--text-primary), and margin-top (->0) -- this is
     a Settings Row's own label, not a form-field label. */
  font-size: 1rem;
  color: var(--text-primary);
  margin: 0;
}

.settings-toggle {
  appearance: none;
  -webkit-appearance: none;
  width: 46px;
  height: 26px;
  min-width: 46px; /* keep the switch from being squeezed by flexbox */
  border-radius: 999px;
  /* A fixed neutral fill, not --surface-glass-border (a subtle *border*
     token, too faint to double as a track fill -- was nearly invisible
     against the light-mode dialog background). */
  background: rgba(128, 128, 128, 0.4);
  border: 1px solid var(--surface-glass-border);
  position: relative;
  cursor: pointer;
  margin: 0;
  transition: background 0.15s ease;
}

.settings-toggle::before {
  content: "";
  position: absolute;
  top: 2px;
  /* Logical inset (not `right` + `transform: translateX`) animates
     directly and stays correct if the app ever supports LTR -- matches
     the `margin-inline-end` convention already used elsewhere. */
  inset-inline-end: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #fff;
  transition: inset-inline-end 0.15s ease;
}

.settings-toggle:checked {
  background: linear-gradient(135deg, var(--accent-violet), var(--accent-pink));
  border-color: transparent;
}

.settings-toggle:checked::before {
  inset-inline-end: 22px; /* 46px track - 2px inset - 20px thumb - 2px inset */
}

.settings-toggle:focus-visible {
  outline: 2px solid var(--accent-pink);
  outline-offset: 2px;
}

.settings-toggle:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

@media (prefers-reduced-motion: reduce) {
  .settings-toggle,
  .settings-toggle::before {
    transition: none;
  }
}

/* Story 1.4: micro-interaction on a newly-saved event only (app.js decides
   *when* to add/skip this -- prefers-reduced-motion is checked there so
   the animation never even starts; these rules are the reduced-motion
   fallback in case that check is ever bypassed). */
.just-saved {
  animation: card-bounce 0.4s ease-out;
}

@keyframes card-bounce {
  0% { transform: scale(0.9); }
  55% { transform: scale(1.04); }
  100% { transform: scale(1); }
}

.confetti-piece {
  position: fixed;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  pointer-events: none;
  z-index: 50;
  animation: confetti-burst 0.6s ease-out forwards;
}

@keyframes confetti-burst {
  0% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(0.4); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .just-saved { animation: none; }
  .confetti-piece { display: none; }
}

/* Story 3.1: Ad Banner -- fixed strip, main screen only (never rendered
   inside a <dialog>; native dialogs sit in the browser's own top layer
   above any z-index here regardless, so this needs no special handling
   for that). Deliberately bg-elev, not surface-glass -- quieter, doesn't
   compete visually the way another glass surface would (DESIGN.md). */
.ad-banner {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  width: 100%;
  /* Fixed 50px of ad + 6px breathing room, NOT min-height. A real demo
     showed why: given any freedom, the AdSense unit sized itself to
     ~390px -- roughly half a phone screen -- which buries the actual
     countdown the app exists for. A countdown list is short content;
     an ad that outweighs it is worse than no ad revenue at all.
     56px is ~8% of a typical phone viewport, the standard mobile
     banner footprint. overflow:hidden is the hard backstop: paired
     with the <ins>'s own height:50px and format="horizontal" (see
     index.html), nothing taller can push into the app's space.
     Revisit alongside deferred-work.md's Anchor-ad-format note once a
     real AdSense account exists -- Google's Anchor format is built
     for exactly this placement and self-limits its height. */
  height: calc(56px + env(safe-area-inset-bottom));
  overflow: hidden;
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--bg-elev);
  border-top: 1px solid var(--surface-glass-border);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 4;
}

.ad-banner[hidden] {
  /* Same reasoning as Story 1.2's Hero Card: [hidden] and a class rule
     that sets `display` have equal specificity, and this author-
     stylesheet rule would otherwise win the tie -- state it explicitly
     so hasRemovedAds() actually hides the banner, not just leaves an
     empty bar. */
  display: none;
}

/* Story 3.2: purchase button -- reuses .btn-primary's existing gradient
   style (same visual language as "שמירה"), full width in the Settings
   dialog. */
.purchase-btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.purchase-btn:disabled {
  cursor: default;
}

/* "pending" (mid-purchase) is .purchase-btn:disabled without .done --
   keeps the brand gradient + spinner. "done" (already owned) gets a
   flat, muted surface instead -- at a glance (or in a screenshot) the
   two are otherwise nearly identical, telling "in progress" and
   "already removed" apart shouldn't depend on spotting a small spinner. */
.purchase-btn.done {
  background: var(--surface-glass);
  border: 1px solid var(--surface-glass-border);
  color: var(--text-secondary);
}

.purchase-spinner {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: #fff;
  animation: purchase-spin 0.7s linear infinite;
  flex-shrink: 0;
}

@keyframes purchase-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .purchase-spinner {
    animation: none;
    /* Still communicates "busy" without a spin -- a half-opacity ring
       instead of just disappearing (which would drop the loading
       indication for reduced-motion users, not just the animation). */
    opacity: 0.6;
  }
}
