/* ──────────────────────────────────────────────────────────────────────────
   Critical, render-blocking font faces — keep at the TOP of this file and do
   NOT move them to an async stylesheet.

   styles.css is render-blocking on every full-document layout (base.html,
   petitions/donate.html, moderation/base_mod.html), so these faces are
   guaranteed parsed before first paint. Two kinds live here:

   1. The metric-matched fallback faces ('Inter Fallback' / 'Satoshi Fallback'):
      src: local('Arial') + size-adjust / *-override remap Arial onto the real
      font's box so the font-display:swap causes NO reflow (kills the font-swap
      slice of the CLS tail). Values are computed (@capsizecss/unpack + the
      fontaine override formula), NOT hand-tuned — do not edit.
   2. The PRELOADED heading family ('Satoshi' 700 — the h1/LCP font).
      base.html <link rel=preload>s its woff2; the @font-face must be
      discovered early (render-blocking) or the preloaded bytes can't bind
      until the late, low-priority async fonts.css loads — re-introducing a
      FOUT and a "preloaded but unused" warning.

   The real BODY font (Inter) is deliberately NOT here and NOT preloaded: its
   unicode-range subset slices live in async static/css/fonts.css, so on cold
   loads its bytes don't compete with the LCP hero image for pre-paint
   bandwidth. First paint renders body text in the metric-matched
   'Inter Fallback' above, and the late swap reflows nothing. Do not re-add
   a render-blocking Inter face or an Inter preload —
   tests/unit/test_critical_font_fallbacks.py guards both directions.

   The remaining, non-above-the-fold faces (Inter slices, 'Satoshi' 900,
   Merriweather, Noto Sans, Plus Jakarta Sans) live in static/css/fonts.css,
   which loads ASYNC (see templates/base.html). Paired file: static/css/fonts.css.
   ────────────────────────────────────────────────────────────────────────── */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107.12%;
  ascent-override: 90.44%;
  descent-override: 22.52%;
  line-gap-override: 0.00%;
}
@font-face {
  font-family: 'Satoshi Fallback';
  src: local('Arial');
  size-adjust: 105.20%;
  ascent-override: 96.00%;
  descent-override: 22.81%;
  line-gap-override: 9.51%;
}
@font-face {
  font-family: 'Satoshi';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/satoshi/Satoshi-Bold.d4db658f4dd6.woff2") format('woff2');
}

/* ── Cross-browser compatibility baseline ──
   Addresses: iOS landscape text inflation, iOS tap flash, form font inheritance,
   Chrome autofill background, number input spinners.
   Sources: Normalize.css, modern-normalize, CSS Remedy. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

*,
*::before,
*::after {
  -webkit-tap-highlight-color: transparent;
}

button,
input,
select,
textarea {
  font: inherit;
  letter-spacing: inherit;
}

/* Chrome/Edge autofill: override the default yellow/blue background that
   clashes with our slate-50 input backgrounds. Uses inset box-shadow trick
   because Chrome ignores background-color on autofilled inputs. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
select:-webkit-autofill {
  -webkit-box-shadow: 0 0 0 1000px #f8fafc inset; /* slate-50 */
  -webkit-text-fill-color: #1e293b; /* slate-800 */
  transition: background-color 5000s ease-in-out 0s;
}

/* Hide number input spinners for consistent appearance across browsers.
   Chrome/Edge show up/down arrows; Firefox/Safari do not. */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
input[type="number"] {
  -moz-appearance: textfield;
}

/* ── iPetitions Design System: Color Palette ──
   Source: Figma Design System → Palette
   Convention: --color-{family}-{shade}
   Usage:  var(--color-blue-gray-800)  or  Tailwind: text-blue-gray-800 */
:root {
  /* Blue Gray */
  --color-blue-gray-50: #f8fafc;
  --color-blue-gray-100: #f1f5f9;
  --color-blue-gray-200: #e2e8f0;
  --color-blue-gray-300: #cbd5e1;
  --color-blue-gray-400: #94a3b8;
  --color-blue-gray-500: #64748b;
  --color-blue-gray-600: #475569;
  --color-blue-gray-700: #334155;
  --color-blue-gray-800: #1e293b;
  --color-blue-gray-900: #0f172a;

  /* Orange */
  --color-orange-50: #fff7ed;
  --color-orange-100: #ffedd5;
  --color-orange-200: #fed7aa;
  --color-orange-300: #fdba74;
  --color-orange-400: #fb923c;
  --color-orange-500: #f97316;
  --color-orange-600: #ea580c;
  --color-orange-700: #c2410c;
  --color-orange-800: #9a3412;
  --color-orange-900: #7c2d12;

  /* Yellow */
  --color-yellow-50: #fffbeb;
  --color-yellow-100: #fef3c7;
  --color-yellow-200: #fde68a;
  --color-yellow-300: #fcd34d;
  --color-yellow-400: #fbbf24;
  --color-yellow-500: #f59e0b;
  --color-yellow-600: #d97706;
  --color-yellow-700: #b45309;
  --color-yellow-800: #92400e;
  --color-yellow-900: #78350f;

  /* Red */
  --color-red-50: #fef2f2;
  --color-red-100: #fee2e2;
  --color-red-200: #fecaca;
  --color-red-300: #fca5a5;
  --color-red-400: #f87171;
  --color-red-500: #ef4444;
  --color-red-600: #dc2626;
  --color-red-700: #b91c1c;
  --color-red-800: #991b1b;
  --color-red-900: #7f1d1d;

  /* Green */
  --color-green-50: #ecfdf5;
  --color-green-100: #d1fae5;
  --color-green-200: #a7f3d0;
  --color-green-300: #6ee7b7;
  --color-green-400: #34d399;
  --color-green-500: #10b981;
  --color-green-600: #059669;
  --color-green-700: #047857;
  --color-green-800: #065f46;
  --color-green-900: #064e3b;

  /* Teal — stored as space-separated RGB triples (not hex) so Tailwind's
     `bg-teal-N/<alpha>` slash-opacity modifier can wrap them as
     `rgb(<r> <g> <b> / <alpha>)`. Direct usages outside Tailwind classes
     must wrap with `rgb(var(--color-teal-N))` — see styles.css gradient
     and _donation_card.html. */
  --color-teal-50: 240 253 250;
  --color-teal-100: 204 251 241;
  --color-teal-200: 153 246 228;
  --color-teal-300: 94 234 212;
  --color-teal-400: 45 212 191;
  --color-teal-500: 20 184 166;
  --color-teal-600: 13 148 136;
  --color-teal-700: 15 118 110;
  --color-teal-800: 17 94 89;
  --color-teal-900: 19 78 74;

  /* Blue */
  --color-blue-50: #eff6ff;
  --color-blue-100: #dbeafe;
  --color-blue-200: #bfdbfe;
  --color-blue-300: #93c5fd;
  --color-blue-400: #60a5fa;
  --color-blue-500: #3b82f6;
  --color-blue-600: #2563eb;
  --color-blue-700: #1d4ed8;
  --color-blue-800: #1e40af;
  --color-blue-900: #1e3a8a;

  /* Indigo — RGB triples for the same slash-opacity reason as teal above. */
  --color-indigo-50: 238 242 255;
  --color-indigo-100: 224 231 255;
  --color-indigo-200: 199 210 254;
  --color-indigo-300: 165 180 252;
  --color-indigo-400: 129 140 248;
  --color-indigo-500: 99 102 241;
  --color-indigo-600: 79 70 229;
  --color-indigo-700: 67 56 202;
  --color-indigo-800: 55 48 163;
  --color-indigo-900: 49 46 129;


  /* Navbar clearance - padding-top needed to push content below the
     fixed glassmorphic navbar.  Use in Tailwind as:
       * pt-[var(--pt-main-navbar-m)]
       * lg:pt-[var(--pt-main-navbar-d)] */
  --pt-main-navbar-m: 98px; /* Mobile */
  --pt-main-navbar-d: 122px; /* Desktop */
}


/* 3D Gradient CTA Button
   Note: Hover changes background-image (gradients), which CSS cannot
   transition smoothly. The color shift is intentionally instant. */
.btn-cta-3d {
  background:
    radial-gradient(
      ellipse at 86% 88%,
      rgba(124, 45, 18, 0.2) 0%,
      rgba(124, 45, 18, 0) 86%
    ),
    radial-gradient(
      ellipse at 26% 21%,
      rgba(255, 247, 237, 0.35) 0%,
      rgba(255, 255, 255, 0) 77%
    ),
    linear-gradient(90deg, var(--color-orange-500) 0%, var(--color-orange-500) 100%);
  box-shadow:
    3px 3px 5px 0px rgba(124, 45, 18, 0.15), /* orange-900 base / 15% */
    inset -2px -4px 7px 0px rgba(255, 247, 237, 0.24); /* orange-50 base / 24% */
}

.btn-cta-3d:hover {
  background:
    radial-gradient(
      ellipse at 86% 88%,
      rgba(124, 45, 18, 0.25) 0%,
      rgba(124, 45, 18, 0) 86%
    ),
    radial-gradient(
      ellipse at 26% 21%,
      rgba(255, 247, 237, 0.4) 0%,
      rgba(255, 255, 255, 0) 77%
    ),
    linear-gradient(90deg, var(--color-orange-600) 0%, var(--color-orange-600) 100%);
}

/* 3D Gradient CTA Button — Teal variant */
.btn-cta-3d-teal {
  background:
    radial-gradient(
      ellipse at 86% 88%,
      rgba(17, 94, 89, 0.2) 0%,
      rgba(17, 94, 89, 0) 86%
    ),
    radial-gradient(
      ellipse at 26% 21%,
      rgba(255, 247, 237, 0.5) 0%,
      rgba(255, 255, 255, 0) 77%
    ),
    linear-gradient(90deg, rgb(var(--color-teal-700)) 0%, rgb(var(--color-teal-700)) 100%);
  box-shadow:
    3px 3px 5px 0px rgba(17, 94, 89, 0.15), /* teal-800 base / 15% */
    inset -2px -4px 7px 0px rgba(255, 247, 237, 0.24); /* orange-50 base / 24% */
}

.btn-cta-3d-teal:hover {
  background:
    radial-gradient(
      ellipse at 86% 88%,
      rgba(17, 94, 89, 0.25) 0%,
      rgba(17, 94, 89, 0) 86%
    ),
    radial-gradient(
      ellipse at 26% 21%,
      rgba(255, 247, 237, 0.55) 0%,
      rgba(255, 255, 255, 0) 77%
    ),
    linear-gradient(90deg, #0d6963 0%, #0d6963 100%);
}

/* 3D Gradient CTA Button — Green variant (constructive secondary, e.g. Edit) */
.btn-cta-3d-green {
  background:
    radial-gradient(
      ellipse at 86% 88%,
      rgba(6, 78, 59, 0.2) 0%,
      rgba(6, 78, 59, 0) 86%
    ),
    radial-gradient(
      ellipse at 26% 21%,
      rgba(236, 253, 245, 0.4) 0%,
      rgba(255, 255, 255, 0) 77%
    ),
    linear-gradient(90deg, var(--color-green-600) 0%, var(--color-green-600) 100%);
  box-shadow:
    3px 3px 5px 0px rgba(6, 78, 59, 0.15), /* green-900 base / 15% */
    inset -2px -4px 7px 0px rgba(236, 253, 245, 0.24); /* green-50 base / 24% */
}

.btn-cta-3d-green:hover {
  background:
    radial-gradient(
      ellipse at 86% 88%,
      rgba(6, 78, 59, 0.25) 0%,
      rgba(6, 78, 59, 0) 86%
    ),
    radial-gradient(
      ellipse at 26% 21%,
      rgba(236, 253, 245, 0.45) 0%,
      rgba(255, 255, 255, 0) 77%
    ),
    linear-gradient(90deg, var(--color-green-700) 0%, var(--color-green-700) 100%);
}

/* 3D Gradient CTA Button — Red variant (destructive, e.g. Delete) */
.btn-cta-3d-red {
  background:
    radial-gradient(
      ellipse at 86% 88%,
      rgba(127, 29, 29, 0.2) 0%,
      rgba(127, 29, 29, 0) 86%
    ),
    radial-gradient(
      ellipse at 26% 21%,
      rgba(254, 242, 242, 0.4) 0%,
      rgba(255, 255, 255, 0) 77%
    ),
    linear-gradient(90deg, var(--color-red-600) 0%, var(--color-red-600) 100%);
  box-shadow:
    3px 3px 5px 0px rgba(127, 29, 29, 0.15), /* red-900 base / 15% */
    inset -2px -4px 7px 0px rgba(254, 242, 242, 0.24); /* red-50 base / 24% */
}

.btn-cta-3d-red:hover {
  background:
    radial-gradient(
      ellipse at 86% 88%,
      rgba(127, 29, 29, 0.25) 0%,
      rgba(127, 29, 29, 0) 86%
    ),
    radial-gradient(
      ellipse at 26% 21%,
      rgba(254, 242, 242, 0.45) 0%,
      rgba(255, 255, 255, 0) 77%
    ),
    linear-gradient(90deg, var(--color-red-700) 0%, var(--color-red-700) 100%);
}

@media (max-width: 1023px) {
  .btn-cta-3d-sm,
  .btn-cta-3d-green-sm,
  .btn-cta-3d-red-sm {
    min-height: 30px;
    padding: 0 12px;
    border-radius: 9999px;
    font-size: 12px;
    line-height: 1;
    white-space: nowrap;
    box-shadow:
      1px 1px 3px 0px rgba(71, 85, 105, 0.14),
      inset -1px -2px 4px 0px rgba(255, 247, 237, 0.24);
  }

  .btn-cta-3d-sm:hover,
  .btn-cta-3d-green-sm:hover,
  .btn-cta-3d-red-sm:hover {
    box-shadow:
      1px 1px 3px 0px rgba(71, 85, 105, 0.18),
      inset -1px -2px 4px 0px rgba(255, 247, 237, 0.3);
  }
}

/* Glass-effect preview button */
.btn-preview:hover:not(:disabled),
.btn-preview:active:not(:disabled) {
  background:
    radial-gradient(ellipse at 30% 20%, rgba(255, 255, 255, 0.7) 0%, transparent 60%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(241, 245, 249, 0.3) 100%); /* blue-gray-100 base / 30% */
  box-shadow:
    2px 2px 6px 0px rgba(0, 0, 0, 0.06),
    -1px -1px 4px 0px rgba(0, 0, 0, 0.04),
    inset -1px -2px 5px 0px rgba(255, 255, 255, 0.4);
}

/* Design System: card shadow — reusable drop shadow for card containers.
   Figma token: "card shadow" = 1px 1px 10px rgba(71,85,105,0.1) */
.shadow-card {
  box-shadow: 1px 1px 10px 0px rgba(71, 85, 105, 0.1); /* blue-gray-600 / 10% */
}

/* "Improve My Draft" shimmer animation — uses a pseudo-element so the
   gradient fades in via opacity instead of snapping (CSS can't transition
   between a solid bg and a gradient background-image). */
.improve-draft-shimmer {
  position: relative;
  overflow: hidden;
}

.improve-draft-shimmer > * {
  position: relative;
  z-index: 1;
}

.improve-draft-shimmer::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(
    82deg,
    rgba(255, 237, 213, 0.4) 0%,  /* orange-100 / 40% */
    rgba(254, 215, 170, 0.8) 50%, /* orange-200 / 80% */
    rgba(255, 237, 213, 0.4) 100% /* orange-100 / 40% */
  );
  background-size: 200% 100%;
  background-position: 100% 0;
  opacity: 0;
  animation: draft-shimmer-in 0.4s ease-out forwards,
             draft-shimmer 3s ease-in-out 0.4s infinite;
}

@keyframes draft-shimmer-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes draft-shimmer {
  0% { background-position: 100% 0; }
  50% { background-position: 0% 0; }
  100% { background-position: 100% 0; }
}

/* IMD button activation glow ring */
.imd-activate {
  animation: imd-glow 0.6s ease-out;
}

@keyframes imd-glow {
  0%   { outline: 2px solid rgba(249, 115, 22, 0.5); outline-offset: 0px; } /* orange-500 / 50% */
  100% { outline: 2px solid rgba(249, 115, 22, 0);   outline-offset: 8px; } /* orange-500 / 0% */
}

/* AI suggestion loading skeleton */
.ai-skeleton-line {
  height: 14px;
  border-radius: 4px;
  background: linear-gradient(90deg, var(--color-blue-gray-200) 25%, var(--color-blue-gray-100) 50%, var(--color-blue-gray-200) 75%);
  background-size: 200% 100%;
  animation: skeleton-pulse 1.5s ease-in-out infinite;
}

@keyframes skeleton-pulse {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .improve-draft-shimmer::before { animation: none; opacity: 1; }
  .imd-activate { animation: none; }
  .ai-skeleton-line { animation: none; }
}


/* Donation marquee — animate the wrapper so all copies move as one unit.
   3 copies in the track; translateX(-33.33%) shifts by exactly one copy's width
   for a seamless loop with buffer to prevent gap flashes. */
@keyframes donation-marquee {
  0% { transform: translateX(0); }
  100% { transform: translateX(-33.33%); }
}
.donation-marquee-track {
  animation: donation-marquee 60s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .donation-marquee-track { animation: none; }
  /* The recent-signers marquee on the petition detail page. Its keyframes live
     in tailwind.config.js (`animation.marquee`), which has no place to express a
     media query, so the guard belongs here next to its sibling. Without it this
     was the only infinite animation in the codebase that ignored the
     preference — above the fold on the site's most-trafficked page. Stopping it
     costs nothing: the signer list stays legible unscrolled, and the second
     (aria-hidden) copy is only there to hide the loop seam. */
  .animate-marquee { animation: none; }
}



/* Alpine.js cloak — hide elements until Alpine initializes */
[x-cloak] { display: none !important; }

/* Alpine's collapse plugin writes the height transition as an inline style, so
   a stylesheet rule only wins with !important. Disclosures then open and close
   instantly under a reduced-motion preference; Alpine's own bookkeeping timer
   still runs, which is harmless because the panel is already at full height. */
@media (prefers-reduced-motion: reduce) {
  [x-collapse] { transition-duration: 0s !important; }
}


/* Mobile petition progress bar — inset shadow matching Figma */
.progress-bar-inset {
  box-shadow: inset 0px 1px 5px 0px rgba(198, 96, 0, 0.19); /* ~orange-700 / 19% */
}

/* Fade-out for collapsed/truncated text blocks.
   Apply .text-gradient-fade when collapsed; remove when expanded.
   Set --fade-length on the element to change how deep the fade reaches
   (default 2.25rem ~= 1.5 lines of body copy).

   Implemented as a mask, not background-clip:text, on purpose. The previous
   version set `color: transparent` and painted the text with a gradient that
   began decaying at 0% — the block's FIRST line — so the petition body fell
   below WCAG AA contrast from roughly line 5 of 11.7 and reached 1.41:1 at the
   bottom. It also meant the text colour was a gradient stop rather than the
   element's own colour, so expanding shifted the whole block's hue and every
   caller had to restate its colour via --fade-color (three of the four named
   tokens that do not exist in this codebase, and silently fell back).
   A mask leaves the real text colour alone and fades only the tail. */
.text-gradient-fade {
  --fade-length: 2.25rem;
  -webkit-mask-image: linear-gradient(
    to bottom,
    #000 0%,
    #000 calc(100% - var(--fade-length)),
    transparent 100%
  );
  mask-image: linear-gradient(
    to bottom,
    #000 0%,
    #000 calc(100% - var(--fade-length)),
    transparent 100%
  );
}

/* Subtle boundary for avatar images, so a light or white-cornered photo does not
   dissolve into the card behind it. Pure black at low alpha on purpose: a tinted
   neutral picks up the surface underneath and reads as dirt on the image edge.
   Applied to the wrapper, not the <img> — the wrapper is what carries the
   rounding and the overflow clip, so an outline on the image itself is clipped
   away entirely.

   Deliberately NOT applied to the petition hero or the browse-card thumbnails:
   those are full-bleed and fade into their card through a scrim, so an outline
   would draw a hard line across the middle of the card rather than bound an
   object. */
.avatar-ring {
  outline: 1px solid oklch(0 0 0 / 0.1);
  outline-offset: -1px;
}

/* Sticky bar frosted glass */
.sticky-bar-glass {
  backdrop-filter: blur(7.5px);
  -webkit-backdrop-filter: blur(7.5px);
  background: rgba(250, 250, 250, 0.7);
  box-shadow: 0px -2px 4px 0px rgba(71, 85, 105, 0.05), /* blue-gray-600 / 5% */
              0px -10px 15px 0px rgba(71, 85, 105, 0.05); /* blue-gray-600 / 5% */
}



/* Hide scrollbar while preserving scroll — used by horizontal scroll strips
   (trending carousel on home, category chips on browse). */
.scrollbar-hide {
  scrollbar-width: none;       /* Firefox */
  -ms-overflow-style: none;    /* IE/Edge */
}
.scrollbar-hide::-webkit-scrollbar {
  display: none;                /* Chrome/Safari */
}


/* Progressive blur overlay for petition card image — from petition_editor.html */
.preview-progressive-blur {
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
  -webkit-mask: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,0.2) 20%, rgba(0,0,0,0.5) 40%, rgba(0,0,0,1) 100%);
  mask: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,0.2) 20%, rgba(0,0,0,0.5) 40%, rgba(0,0,0,1) 100%);
}


/* ── Trix editor content (IPE-127) ──
   Trix 2.1.8's trix.css ships no list/link styling for editor content, so
   bullet/numbered lists render markerless and links render as plain text
   *inside* the <trix-editor> — Tailwind Preflight's resets (`ul, ol {
   list-style: none }` and `a { color: inherit; text-decoration: inherit }`)
   win unopposed. Restore them for the editor, mirroring the .rich-text body. */
.trix-content ul,
.trix-content ol { padding-left: 1.5rem; }
.trix-content ul { list-style-type: disc; }
.trix-content ol { list-style-type: decimal; }
.trix-content a {
  color: var(--color-orange-600);
  text-decoration: underline;
}
/* Headings inside the editor (mirrors .rich-text sizing). h1 kept so pasted
   <h1> renders before the sanitizer drops it on save. */
.trix-content h1,
.trix-content h2,
.trix-content h3,
.trix-content h4 {
  font-weight: 600;
  line-height: 1.3;
  margin: 1.5rem 0 0.5rem;
}
.trix-content h1 { font-size: 1.5rem; }
.trix-content h2 { font-size: 1.25rem; }
.trix-content h3 { font-size: 1.125rem; }
.trix-content h4 { font-size: 1rem; }
.trix-content u { text-decoration: underline; }
.trix-content blockquote {
  margin: 0 0 1rem;
  padding-left: 1rem;
  border-left: 3px solid var(--color-blue-gray-200);
  color: var(--color-blue-gray-600);
}
.trix-content pre {
  margin: 0 0 1rem;
  padding: 0.75rem 1rem;
  background: var(--color-blue-gray-100);
  border-radius: 0.375rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.875rem;
  white-space: pre-wrap;
}
.trix-content hr {
  margin: 1.5rem 0;
  border: 0;
  border-top: 1px solid var(--color-blue-gray-200);
}
/* Trix renders the HR content attachment as a <figure> — make it visible
   inside the editor. The sanitizer drops the <figure> wrapper on save. */
.trix-content figure[data-trix-content-type="application/vnd.trix.horizontal-rule.html"] {
  display: block;
  margin: 1.5rem 0;
}

/* ── Unified 5-state input field (IPE-233) ──────────────────────────
   Pseudo-class driven, zero JS. Compose with sizing/padding utilities
   (h-12, rounded-2xl, bg-slate-50, px-4, etc.) in templates — this class
   only owns the state-dependent border + ring colors.

     State                                         Border + ring
     1. Default (empty + unfocused)                slate (grey)
     2. Typed but not validated yet                slate (same as default)
     3. Focused / active                           orange (always wins)
     4. Filled + validation passed (post-blur)     green
     5. Filled + validation failed                 red
        - :user-invalid → client-side HTML5 failure (post-interaction)
        - [aria-invalid="true"] → Django server-side form error

   :user-valid / :user-invalid (CSS spec; not :valid / :invalid) fire only
   after the user has interacted, so empty required fields don't turn red
   on page load. Safari 16+ floor per project policy; Safari 16.0–16.4
   gracefully degrade to states 1 + 3 (no green / red without focus). */
.input-field {
  border-color: var(--color-blue-gray-300);
  outline: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.input-field:not(:focus):user-valid {
  border-color: var(--color-green-500);
  box-shadow: 0 0 0 1px var(--color-green-500);
}
.input-field:not(:focus):user-invalid,
.input-field:not(:focus)[aria-invalid="true"] {
  border-color: var(--color-red-500);
  box-shadow: 0 0 0 1px var(--color-red-500);
}
.input-field:focus {
  border-color: var(--color-orange-500);
  box-shadow: 0 0 0 1px var(--color-orange-500);
}

/* QR share code — make_qr_svg() emits a viewBox (omitsize) and no fixed
   width/height, so the SVG scales to its bounded container instead of
   overflowing the narrow share/success modal and clipping on the right
   (IPE-255). The wrapping element caps the max width. */
.qr-code svg {
  display: block;
  width: 100%;
  height: auto;
}

/* Adaptive donation wall themes (spec §3.3). Server-selected via
   data-wall-theme; the artifact never supplies raw colors.
   Contrast check (WCAG AA, spec §11): --wall-muted on --wall-panel is
   4.76:1 (light) / 6.93:1 (dark) — both pass the 4.5:1 text bar as-is. */
[data-wall-theme="light"] {
  --wall-page: #faf9f7;
  --wall-panel: #ffffff;
  --wall-text: #1e293b;
  --wall-muted: #64748b;
  --wall-border: #e2e8f0;
  --wall-hero-overlay: linear-gradient(180deg, rgba(15, 23, 42, 0) 35%, rgba(15, 23, 42, 0.78) 100%);
  --wall-desktop-overlay:
    linear-gradient(90deg, rgba(15, 23, 42, 0.08) 0%, rgba(15, 23, 42, 0.18) 34%, rgba(250, 249, 247, 0.82) 56%, rgba(250, 249, 247, 0.98) 100%),
    linear-gradient(180deg, rgba(15, 23, 42, 0) 42%, rgba(15, 23, 42, 0.76) 100%);
}
[data-wall-theme="dark"] {
  --wall-page: #0c0a09;
  --wall-panel: #1c1917;
  --wall-text: #ffffff;
  --wall-muted: #a8a29e;
  --wall-border: rgba(255, 255, 255, 0.14);
  --wall-hero-overlay: linear-gradient(180deg, rgba(12, 10, 9, 0) 30%, rgba(28, 18, 12, 0.85) 100%);
  --wall-desktop-overlay:
    linear-gradient(90deg, rgba(12, 10, 9, 0.1) 0%, rgba(12, 10, 9, 0.3) 38%, rgba(12, 10, 9, 0.86) 60%, rgba(12, 10, 9, 0.98) 100%),
    linear-gradient(180deg, rgba(12, 10, 9, 0) 38%, rgba(12, 10, 9, 0.82) 100%);
}

.wall-payment-panel {
  background: var(--wall-page);
}

.wall-desktop-overlay {
  background: var(--wall-desktop-overlay);
}

@media (min-width: 1024px) {
  .wall-payment-panel {
    background: transparent;
  }
}

/* Unselected amount-preset button on the wall variant of _donation_card.html
   (card_variant="wall") — swaps the fixed slate Tailwind classes used by the
   other variants for the theme tokens above so the grid reads correctly on
   both light and dark wall themes. */
.wall-preset-unselected {
  background: var(--wall-panel);
  border: 1px solid var(--wall-border);
  color: var(--wall-text);
}
