/* Self-hosted so the first paint does not wait on a third party, nothing about
   a visitor is sent to another host, and the app renders correctly offline.
   Both are variable fonts, latin subset - one file per family, all weights.
   `swap` because a blocked font must never hide the text. */
@font-face {
  font-family: "Bricolage Grotesque";
  src: url("/fonts/bricolage-grotesque.woff2") format("woff2");
  font-weight: 500 800;
  font-stretch: 100%;
  font-display: swap;
}
@font-face {
  font-family: "Archivo";
  src: url("/fonts/archivo.woff2") format("woff2");
  font-weight: 400 700;
  font-stretch: 100%;
  font-display: swap;
}

/* GemSpot design system.

   Cascade contract: this file loads AFTER tailwind.css on every page, so an
   authored rule here beats a Tailwind utility of equal specificity by order,
   and any multi-class selector beats one outright. Design overrides INTO the
   cascade (specificity + order); never force them. `!important` is reserved
   for layers outside our cascade - here, Leaflet's runtime-injected chrome.

   Hover contract: touch browsers latch :hover on the last element a finger
   touched, so a scrolled card stays highlighted. Tailwind utilities are gated
   by `future.hoverOnlyWhenSupported`; authored rules below carry an explicit
   `@media (hover: hover) and (pointer: fine)`. Never author a bare :hover. */

:root {
  --gs-ink: #1c1726;
  --gs-ink-soft: #453d57;
  --gs-muted: #6e6880;
  --gs-line: #e8e4f2;
  --gs-line-strong: #d6cfe8;
  --gs-surface: #ffffff;
  --gs-canvas: #f6f4fb;

  --gs-brand: #6d3fd4;
  --gs-brand-hover: #5a2fb8;
  --gs-brand-soft: #efe9fd;
  --gs-brand-ink: #4a2a95;
  /* The only warm note in the system, so it always reads as "this one is
     urgent". Two values on purpose: the fill is the bright one, the ink is
     the readable one - #d946a6 as small text is 3.9:1 on white and fails. */
  --gs-accent: #d946a6;
  --gs-accent-ink: #b42a86;
  /* Placeholders are text and answer to the same 4.5:1 floor as body copy;
     the usual near-silver (#9a93ab) is 2.9:1 and is not readable. */
  --gs-placeholder: #75708a;

  --gs-radius-sm: 0.625rem;
  --gs-radius-md: 0.875rem;
  --gs-radius-lg: 1.125rem;
  --gs-radius-xl: 1.5rem;

  --gs-shadow-card: 0 1px 2px rgba(28, 23, 38, 0.05), 0 1px 1px rgba(28, 23, 38, 0.03);
  --gs-shadow-lift: 0 12px 34px -20px rgba(28, 23, 38, 0.4), 0 2px 5px rgba(28, 23, 38, 0.05);
  --gs-shadow-panel: 0 18px 48px -24px rgba(45, 25, 90, 0.35), 0 2px 8px rgba(28, 23, 38, 0.06);
  --gs-shadow-brand: 0 10px 26px -12px rgba(109, 63, 212, 0.55);
  --gs-ring-brand: 0 0 0 4px rgba(109, 63, 212, 0.16);

  /* Exponential ease-out - natural deceleration, no bounce. -quint for
     interaction feedback, -expo for entrances. */
  --gs-ease-quint: cubic-bezier(0.22, 1, 0.36, 1);
  --gs-ease-expo: cubic-bezier(0.16, 1, 0.3, 1);

  /* Two faces, one job each: Bricolage Grotesque has optical sizing and enough
     character to carry a headline, Archivo is a sturdy grotesque that stays
     legible at 12px under dense UI. Both are self-hosted - see the @font-face
     rules at the top of this file. */
  --gs-font-display: "Bricolage Grotesque", Archivo, system-ui, sans-serif;
  --gs-font-ui: Archivo, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

  /* One authority for the app's fixed chrome. The map sheet, both scrolling
     views and the FAB all size against these rather than restating a magic
     number, so changing the nav's height moves everything that clears it. */
  --gs-header-h: 3.25rem;
  --gs-island-h: 3.5rem;
  --gs-island-gap: 0.75rem;
  /* Everything that must clear the floating nav uses this, including the
     device's own home-indicator inset. */
  --gs-nav-clear: calc(var(--gs-island-h) + var(--gs-island-gap) * 2 + env(safe-area-inset-bottom));
}

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

body {
  font-family: var(--gs-font-ui);
  color: var(--gs-ink);
  background: var(--gs-canvas);
}

/* Display face on anything that behaves as a headline, including the wordmark
   and the marketing page's own heading utilities. */
h1, h2, h3, .gs-wordmark, .gs-step-n {
  font-family: var(--gs-font-display);
  font-optical-sizing: auto;
}

/* Kill WebKit's double-tap zoom (it fights double-tap-to-wow) while keeping
   accessibility pinch-zoom. touch-action doesn't inherit, hence the universal. */
* { touch-action: manipulation; }

/* Restore [hidden] as a real hide authority. Tailwind Preflight ships
   `[hidden]:where(...)` at zero specificity, so any .flex/.grid utility beats it
   and the element stays visible. Two attribute selectors (0,2,0) outrank every
   display utility; keep [hidden] elements free of inline display styles. */
[hidden]:not([hidden="until-found"]) { display: none; }

/* ---------- marketing page ----------
   The app shell pins the body; the marketing page scrolls normally, so it opts
   out rather than inheriting a height it does not want. */
.gs-marketing { min-height: 100dvh; }
.gs-marketing .gs-header { position: sticky; }

/* Hero wash: a single soft bloom behind the headline, anchored above the fold
   so it reads as light falling on the page rather than as a gradient panel. */
.gs-hero-wash {
  position: absolute;
  inset: -6rem 0 auto 0;
  height: 32rem;
  background: radial-gradient(52rem 26rem at 28% 0%, #ddcdff 0%, transparent 66%);
  pointer-events: none;
}

/* The one authored moment on the page: the evidence cards rise in sequence
   once, from an already-visible default so a failed animation still reads. */
.gs-stack { display: grid; gap: 0.75rem; max-width: 34rem; width: 100%; }
.gs-stack > li { animation: gs-rise-in 0.7s var(--gs-ease-expo) backwards; animation-delay: calc(var(--i) * 90ms + 120ms); }
@keyframes gs-rise-in { from { opacity: 0; transform: translateY(14px); } }

.gs-thumb {
  width: 3rem;
  height: 3rem;
  flex: none;
  border-radius: var(--gs-radius-md);
  display: grid;
  place-items: center;
  font-size: 1.375rem;
  background: var(--gs-brand-soft);
}

/* Numbered sequence. A rail on wide screens, a stacked list on a phone; the
   rule on the left is what makes it read as one ordered thing rather than
   three interchangeable cards. */
.gs-steps { display: grid; gap: 2.25rem; counter-reset: step; }
.gs-steps > li { padding-left: 3.25rem; position: relative; max-width: 34rem; }
.gs-step-n {
  position: absolute;
  left: 0;
  top: -0.125rem;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--gs-radius-md);
  font-weight: 800;
  background: var(--gs-brand-soft);
  color: var(--gs-brand-ink);
}
@media (min-width: 768px) {
  .gs-steps { grid-template-columns: repeat(3, 1fr); gap: 2rem; }
  .gs-steps > li { padding-left: 0; padding-top: 3.25rem; }
  .gs-step-n { top: 0; }
}

.gs-badge {
  display: inline-block;
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  font-size: 0.65rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  white-space: nowrap;
}

/* Status legend. A definition list, because that is what it is - the badge
   names the state and the line next to it says what the state promises. */
.gs-legend { display: grid; gap: 0.875rem; }
.gs-legend > div { display: grid; grid-template-columns: 7.25rem 1fr; gap: 0.75rem; align-items: baseline; }
.gs-legend dd { color: var(--gs-muted); line-height: 1.55; font-size: 0.9rem; }
@media (max-width: 420px) {
  .gs-legend > div { grid-template-columns: 1fr; gap: 0.25rem; }
}

.gs-cta {
  border-radius: var(--gs-radius-xl);
  padding: 2.5rem 1.75rem;
  background: linear-gradient(135deg, #7a4ade 0%, #4d269e 100%);
  box-shadow: 0 24px 60px -30px rgba(45, 25, 90, 0.8);
}
@media (min-width: 640px) { .gs-cta { padding: 4rem 3.5rem; } }

/* ---------- app shell ----------
   The body never scrolls. The header is a flex child pinned by ordinary layout
   and the map fills the rest. A `position: fixed` bar is at the mercy of each
   mobile browser's idea of "the viewport" as its URL bar slides; layout is not. */
.gs-shell {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh; /* progressive enhancement: dvh tracks browser chrome */
  overflow: hidden;
}

.gs-header {
  height: var(--gs-header-h);
  flex: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0 0.875rem;
  padding-top: env(safe-area-inset-top);
  box-sizing: content-box; /* the notch inset adds to the height, never eats it */
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(16px) saturate(1.6);
  border-bottom: 1px solid var(--gs-line);
  z-index: 1000; /* over Leaflet panes, which top out at 800 */
}

.gs-icon-btn {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 999px;
  color: var(--gs-ink-soft);
  cursor: pointer;
  transition: background-color 0.18s var(--gs-ease-quint), color 0.18s var(--gs-ease-quint),
    transform 0.18s var(--gs-ease-quint);
}
.gs-icon-btn:active { transform: scale(0.92); }
.gs-icon-btn:focus-visible { outline: none; box-shadow: var(--gs-ring-brand); }
@media (hover: hover) and (pointer: fine) {
  .gs-icon-btn:hover { background: var(--gs-brand-soft); color: var(--gs-brand-ink); }
}

.gs-wordmark {
  font-weight: 800;
  font-size: 1.0625rem;
  letter-spacing: -0.035em;
  color: var(--gs-brand-ink);
}

.gs-main { flex: 1; position: relative; min-height: 0; }

/* One view visible at a time, each filling the main area. Hidden tabs stay in
   the DOM (the map keeps its state and its tiles) rather than being torn down. */
.gs-view { position: absolute; inset: 0; }

/* Tabs that scroll their own content, clearing the floating nav so the last
   row is never trapped underneath it. */
.gs-view-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.gs-page {
  /* Wider than a pure reading measure: these rows are a vote control, a badge
     and two lines of body, and at 34rem the body wrapped every third word. */
  max-width: 40rem;
  margin: 0 auto;
  padding: 1.25rem 1rem calc(1.5rem + var(--gs-nav-clear));
}

/* ---------- island nav ----------
   A floating, translucent pill rather than a flat edge-to-edge bar: it reads as
   an object above the content, keeps the map visible behind it, and its rounded
   ends line up with the device's own corner radius. */
.gs-island {
  position: fixed;
  left: 50%;
  bottom: calc(var(--gs-island-gap) + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 1500;
  display: flex;
  align-items: center;
  gap: 0.25rem;
  height: var(--gs-island-h);
  padding: 0.3125rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.72);
  backdrop-filter: blur(22px) saturate(1.8);
  -webkit-backdrop-filter: blur(22px) saturate(1.8);
  border: 1px solid rgba(255, 255, 255, 0.7);
  box-shadow: 0 10px 34px -12px rgba(45, 25, 90, 0.34), 0 2px 8px rgba(28, 23, 38, 0.06);
}

.gs-island-tab {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  height: 100%;
  padding: 0 0.75rem;
  border-radius: 999px;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--gs-muted);
  cursor: pointer;
  transition: color 0.22s var(--gs-ease-quint), background-color 0.22s var(--gs-ease-quint);
}
.gs-island-tab svg { width: 1.25rem; height: 1.25rem; flex: none; }
.gs-island-tab { justify-content: center; }
/* Labels are permanent. Revealing one on the active tab animated max-width -
   layout work on every tab change - and three short labels fit a 360px screen
   outright, so the pill background alone carries the state change. */
.gs-island-tab span { white-space: nowrap; }
.gs-island-tab[aria-current="true"] {
  background: var(--gs-brand);
  color: #fff;
  box-shadow: var(--gs-shadow-brand);
}
.gs-island-tab:focus-visible { outline: none; box-shadow: var(--gs-ring-brand); }
@media (hover: hover) and (pointer: fine) {
  .gs-island-tab:hover { color: var(--gs-brand-ink); }
  .gs-island-tab[aria-current="true"]:hover { color: #fff; background: var(--gs-brand-hover); }
}

/* Primary action: a circle floating clear of the island, on the thumb side. */
.gs-fab {
  position: fixed;
  right: 1rem;
  bottom: calc(var(--gs-nav-clear) + 0.25rem);
  z-index: 1500;
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 999px;
  background: var(--gs-brand);
  color: #fff;
  box-shadow: 0 12px 28px -10px rgba(109, 63, 212, 0.7), 0 2px 6px rgba(28, 23, 38, 0.12);
  cursor: pointer;
  transition: transform 0.2s var(--gs-ease-quint), background-color 0.2s var(--gs-ease-quint);
}
.gs-fab:active { transform: scale(0.93); }
.gs-fab:focus-visible { outline: none; box-shadow: var(--gs-ring-brand), 0 12px 28px -10px rgba(109, 63, 212, 0.7); }
@media (hover: hover) and (pointer: fine) {
  .gs-fab:hover { background: var(--gs-brand-hover); transform: translateY(-2px); }
}

/* Category filter rail over the map. */
.gs-filters {
  position: absolute;
  z-index: 850;
  top: 0.625rem;
  left: 0.625rem;
  right: 0.625rem;
  display: flex;
  gap: 0.375rem;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 0.25rem;
}
.gs-filters::-webkit-scrollbar { display: none; }

/* ---------- buttons ---------- */
.gs-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  font-weight: 600;
  font-size: 0.875rem;
  line-height: 1;
  padding: 0.65rem 1rem;
  border-radius: var(--gs-radius-md);
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform 0.18s var(--gs-ease-quint), background-color 0.18s var(--gs-ease-quint),
    box-shadow 0.18s var(--gs-ease-quint), color 0.18s var(--gs-ease-quint);
}
.gs-btn:active { transform: scale(0.97); }
.gs-btn:focus-visible { outline: none; box-shadow: var(--gs-ring-brand); }
.gs-btn[disabled] { opacity: 0.55; cursor: not-allowed; }

.gs-btn-primary { background: var(--gs-brand); color: #fff; box-shadow: var(--gs-shadow-brand); }
.gs-btn-ghost { background: transparent; color: var(--gs-ink-soft); }
.gs-btn-soft { background: var(--gs-brand-soft); color: var(--gs-brand-ink); }
.gs-btn-outline { background: var(--gs-surface); color: var(--gs-ink-soft); border-color: var(--gs-line-strong); }

@media (hover: hover) and (pointer: fine) {
  .gs-btn-primary:hover { background: var(--gs-brand-hover); }
  .gs-btn-ghost:hover { background: var(--gs-brand-soft); color: var(--gs-brand-ink); }
  .gs-btn-soft:hover { background: #e4d9fc; }
  .gs-btn-outline:hover { border-color: var(--gs-brand); color: var(--gs-brand-ink); }
}

/* ---------- inputs ---------- */
.gs-field {
  width: 100%;
  font-size: 0.9375rem;
  padding: 0.7rem 0.85rem;
  border-radius: var(--gs-radius-md);
  border: 1px solid var(--gs-line-strong);
  background: var(--gs-surface);
  color: var(--gs-ink);
  transition: border-color 0.18s var(--gs-ease-quint), box-shadow 0.18s var(--gs-ease-quint);
}
.gs-field::placeholder { color: var(--gs-placeholder); }
.gs-field:focus { outline: none; border-color: var(--gs-brand); box-shadow: var(--gs-ring-brand); }
textarea.gs-field { resize: vertical; min-height: 5.5rem; line-height: 1.5; }

.gs-label {
  display: block;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--gs-muted);
  margin-bottom: 0.4rem;
}

/* ---------- category chips ---------- */
.gs-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  min-height: 2.25rem; /* with the 0.375rem rail gap this clears the 44px target */
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
  padding: 0.4rem 0.8rem;
  border-radius: 999px;
  border: 1px solid var(--gs-line-strong);
  background: rgba(255, 255, 255, 0.92);
  color: var(--gs-ink-soft);
  cursor: pointer;
  transition: all 0.18s var(--gs-ease-quint);
}
.gs-chip[aria-pressed="true"] {
  background: var(--gs-brand);
  border-color: var(--gs-brand);
  color: #fff;
  box-shadow: var(--gs-shadow-brand);
}
@media (hover: hover) and (pointer: fine) {
  .gs-chip:hover { border-color: var(--gs-brand); color: var(--gs-brand-ink); }
  .gs-chip[aria-pressed="true"]:hover { color: #fff; }
}

/* ---------- cards ---------- */
.gs-card {
  background: var(--gs-surface);
  border: 1px solid var(--gs-line);
  border-radius: var(--gs-radius-lg);
  box-shadow: var(--gs-shadow-card);
  transition: box-shadow 0.2s var(--gs-ease-quint), transform 0.2s var(--gs-ease-quint),
    border-color 0.2s var(--gs-ease-quint);
}
@media (hover: hover) and (pointer: fine) {
  .gs-card-link:hover { box-shadow: var(--gs-shadow-lift); border-color: var(--gs-line-strong); transform: translateY(-1px); }
}
.gs-card-link[aria-current="true"] { border-color: var(--gs-brand); box-shadow: var(--gs-ring-brand); }
/* Cards are divs with tabindex (they contain their own buttons, which cannot
   nest inside a button), so the focus ring has to be authored here. */
.gs-card-link:focus-visible { outline: none; border-color: var(--gs-brand); box-shadow: var(--gs-ring-brand); }
.gs-grip:focus-visible { outline: none; box-shadow: var(--gs-ring-brand); border-radius: var(--gs-radius-md); }

/* ---------- map ---------- */
#map { position: absolute; inset: 0; background: #e9e5f4; }

/* Leaflet injects its own chrome at runtime, outside this cascade - the one
   sanctioned place to out-specify rather than re-author. */
.leaflet-container { font: inherit !important; background: #e9e5f4 !important; }
.leaflet-control-attribution {
  font-size: 10px !important;
  background: rgba(255, 255, 255, 0.78) !important;
  color: var(--gs-muted) !important;
}
.leaflet-control-zoom a {
  border-radius: 10px !important;
  border: 1px solid var(--gs-line) !important;
  color: var(--gs-ink-soft) !important;
  box-shadow: var(--gs-shadow-card);
}

/* Pinch zooms on touch, so the +/- buttons are redundant there and they collide
   with the sheet's primary action. */
@media (max-width: 639px) {
  .leaflet-control-zoom { display: none !important; }
}

/* Pins are DOM (divIcon), not images: one element themed by a modifier class,
   so a category colour change is a token edit and never a sprite rebuild. */
.gs-pin {
  width: 30px;
  height: 30px;
  border-radius: 50% 50% 50% 4px;
  transform: rotate(-45deg);
  background: var(--gs-brand);
  border: 2.5px solid #fff;
  box-shadow: 0 4px 10px -2px rgba(45, 25, 90, 0.5);
  display: grid;
  place-items: center;
  transition: transform 0.22s var(--gs-ease-quint);
}
.gs-pin > span { transform: rotate(45deg); font-size: 14px; line-height: 1; }
.gs-pin-active { transform: rotate(-45deg) scale(1.28); }
/* The device's own position, not a gem: a dot, never a teardrop. */
.gs-here {
  width: 16px; height: 16px; border-radius: 50%;
  background: #2563eb; border: 3px solid #fff;
  box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.22);
}

/* ---------- bottom sheet / side rail ----------
   One panel element. On a phone it is a bottom sheet the thumb can reach; from
   `sm` up it becomes a floating left rail. Both are ordinary layout, so neither
   fights the browser chrome. */
.gs-panel {
  position: absolute;
  z-index: 800;
  background: rgba(255, 255, 255, 0.94);
  backdrop-filter: blur(18px);
  box-shadow: var(--gs-shadow-panel);
  display: flex;
  flex-direction: column;
  transition: transform 0.36s var(--gs-ease-expo);
}
@media (max-width: 639px) {
  .gs-panel {
    left: 0; right: 0; bottom: 0;
    max-height: 68dvh;
    border-radius: var(--gs-radius-xl) var(--gs-radius-xl) 0 0;
    /* The sheet runs to the screen edge (its blur under the nav is the point),
       so its CONTENT is what clears the floating island. */
    padding-bottom: var(--gs-nav-clear);
  }
  /* Peeked height = the nav it must clear, plus the sheet header and exactly
     one whole card. A half-clipped card reads as a rendering bug; one whole
     card plus the grip reads as "there is more, pull me up". */
  .gs-panel[data-collapsed="true"] {
    /* max(0px, ...) because 100% is the sheet's own height: a short sheet (the
       empty state) would otherwise translate by a negative amount and float up
       off the screen edge. */
    transform: translateY(max(0px, calc(100% - var(--gs-nav-clear) - 12.5rem)));
  }
}
@media (min-width: 640px) {
  /* Leaflet's zoom control only exists from `sm` up (see below), so only the
     rail layout has to leave room for it. */
  .gs-filters { right: 3.25rem; }
  .gs-panel {
    left: 1rem; top: 1rem;
    bottom: calc(var(--gs-nav-clear) + 0.25rem);
    width: 23rem;
    border-radius: var(--gs-radius-xl);
    padding-bottom: 0.75rem;
  }
  /* Collapse is a phone gesture; on the rail there is nothing to collapse into. */
  .gs-panel[data-collapsed="true"] { transform: none; }
  .gs-filters { left: 25rem; }
}

/* Grab handle: phone affordance only - on desktop the panel is a rail, not a
   sheet, and a handle there would promise a drag that does nothing. */
.gs-grip { display: flex; justify-content: center; padding: 0.55rem 0 0.35rem; cursor: grab; }
.gs-grip::before { content: ""; width: 2.25rem; height: 0.25rem; border-radius: 999px; background: var(--gs-line-strong); }
@media (min-width: 640px) { .gs-grip { display: none; } }

.gs-scroll { overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }

/* ---------- modal ----------
   One authority for modal geometry on phones: every dialog shares the
   `.gs-backdrop` + first-child-panel shape and becomes a full-width bottom
   sheet. This (0,2,0) selector outranks per-modal utilities, so panels must not
   carry their own width/radius/padding or they would fight it. */
.gs-backdrop {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgba(28, 23, 38, 0.42);
  backdrop-filter: blur(4px);
  animation: gs-fade 0.24s var(--gs-ease-quint);
}
.gs-backdrop > .gs-modal {
  width: 100%;
  max-width: 26rem;
  max-height: 90dvh;
  overflow-y: auto;
  background: var(--gs-surface);
  border-radius: var(--gs-radius-xl);
  box-shadow: var(--gs-shadow-panel);
  padding: 1.5rem;
  animation: gs-pop 0.3s var(--gs-ease-expo);
}
/* The dialog takes focus on open so Escape reaches it and a screen reader
   announces it; it is not a focus target a person navigated to, so no ring. */
.gs-backdrop > .gs-modal:focus { outline: none; }
@media (max-width: 639px) {
  .gs-backdrop { padding: 0; align-items: flex-end; }
  .gs-backdrop > .gs-modal {
    max-width: 100%;
    max-height: 92dvh;
    border-radius: var(--gs-radius-xl) var(--gs-radius-xl) 0 0;
    padding: 1.25rem 1.25rem calc(1.25rem + env(safe-area-inset-bottom));
    animation: gs-sheet 0.34s var(--gs-ease-expo);
  }
}

/* Close affordance. Sticky rather than absolute: a long dialog scrolls, and a
   close button that scrolls out of reach is worse than none on a phone sheet
   where there is no backdrop left to tap. */
.gs-modal-x {
  position: sticky;
  top: 0;
  float: right;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  margin: -0.5rem -0.5rem 0 0;
  border-radius: 999px;
  color: var(--gs-muted);
  background: var(--gs-surface);
  cursor: pointer;
  transition: background-color 0.18s var(--gs-ease-quint), color 0.18s var(--gs-ease-quint);
}
@media (hover: hover) and (pointer: fine) {
  .gs-modal-x:hover { background: var(--gs-brand-soft); color: var(--gs-brand-ink); }
}

/* ---------- wow ---------- */
.gs-wow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-width: 3rem;
  min-height: 2.25rem;
  font-weight: 700;
  font-size: 0.8125rem;
  padding: 0.4rem 0.7rem;
  border-radius: 999px;
  border: 1px solid var(--gs-line-strong);
  background: var(--gs-surface);
  color: var(--gs-muted);
  cursor: pointer;
  transition: all 0.18s var(--gs-ease-quint);
}
.gs-wow[aria-pressed="true"] {
  background: #fdeaf7;
  border-color: #f5bde3;
  color: var(--gs-accent-ink);
}
.gs-wow[aria-pressed="true"] .gs-wow-mark { animation: gs-pop-mark 0.4s var(--gs-ease-expo); }

/* Upvote control for the feedback board. A tall pill rather than a heart: the
   count IS the argument for the feature, so it carries the emphasis. */
.gs-vote {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.05rem;
  width: 2.75rem;
  min-height: 2.75rem;
  padding: 0.4rem 0;
  border-radius: var(--gs-radius-md);
  border: 1px solid var(--gs-line-strong);
  background: var(--gs-surface);
  color: var(--gs-muted);
  font-weight: 800;
  cursor: pointer;
  transition: all 0.18s var(--gs-ease-quint);
}
.gs-vote[aria-pressed="true"] {
  background: var(--gs-brand);
  border-color: var(--gs-brand);
  color: #fff;
  box-shadow: var(--gs-shadow-brand);
}
.gs-vote:focus-visible { outline: none; box-shadow: var(--gs-ring-brand); }
@media (hover: hover) and (pointer: fine) {
  .gs-vote:hover { border-color: var(--gs-brand); color: var(--gs-brand-ink); }
  .gs-vote[aria-pressed="true"]:hover { color: #fff; background: var(--gs-brand-hover); }
}

.gs-btn-danger { background: #d92d20; color: #fff; }
@media (hover: hover) and (pointer: fine) {
  .gs-btn-danger:hover { background: #b42318; }
}

.gs-toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--gs-nav-clear) + 0.75rem);
  transform: translateX(-50%);
  z-index: 3000;
  max-width: min(26rem, calc(100vw - 2rem));
  padding: 0.7rem 1rem;
  border-radius: var(--gs-radius-md);
  background: var(--gs-ink);
  color: #fff;
  font-size: 0.875rem;
  font-weight: 600;
  box-shadow: var(--gs-shadow-panel);
  animation: gs-rise 0.28s var(--gs-ease-expo);
}
.gs-toast[data-tone="error"] { background: #b42318; }

.gs-skeleton {
  background: linear-gradient(90deg, #efecf7 25%, #e4dff2 37%, #efecf7 63%);
  background-size: 400% 100%;
  animation: gs-shimmer 1.4s ease infinite;
  border-radius: var(--gs-radius-md);
}

@keyframes gs-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@keyframes gs-fade { from { opacity: 0; } }
@keyframes gs-pop { from { opacity: 0; transform: translateY(8px) scale(0.98); } }
@keyframes gs-sheet { from { transform: translateY(100%); } }
@keyframes gs-rise { from { opacity: 0; transform: translate(-50%, 10px); } }
@keyframes gs-pop-mark { 40% { transform: scale(1.35); } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
