/*!**********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ../../node_modules/.pnpm/next@14.2.35_@babel+core@7.29.7_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[12].use[2]!../../node_modules/.pnpm/next@14.2.35_@babel+core@7.29.7_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[14].oneOf[12].use[3]!../../packages/ui/src/tokens.css ***!
  \**********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
/**
 * # The token layer
 *
 * Every value the look and feel is made of, in one place. Nothing here paints anything: this
 * file only *names* decisions, and `globals.css` plus the components spend them. Repainting the
 * app should mean editing this file and nothing else — that is the whole point of it existing.
 *
 * ## Why `light-dark()` and not two blocks
 *
 * The obvious shape for a themed app is a dark `:root` and a light override, and it is the wrong
 * one here. It writes every token's name three times — once for the default, once for
 * `[data-theme="light"]`, once inside `@media (prefers-color-scheme: light)` for the automatic
 * case — and the three copies drift. A token added to one and forgotten in the others is a bug
 * you only see in one theme, on one setting, which is exactly the bug nobody finds.
 *
 * `light-dark(a, b)` states both values in one declaration and lets the browser pick, reading the
 * element's own `color-scheme`. **One line per token, and the two themes cannot fall out of sync
 * because they are the same line.**
 *
 * So the theme switch below is not a palette swap: it swaps `color-scheme`, and every
 * `light-dark()` in the app follows. That is also why the automatic case needs no code at all —
 * `color-scheme: light dark` means "whatever the system says", which is the default state.
 *
 * The cost is browser support: `light-dark()` is Chrome 123, Safari 17.5 and Firefox 120, all of
 * 2024. On anything older every token falls back to nothing and the page renders unstyled, which
 * is why `@supports` guards it — see the fallback block at the end.
 *
 * ## Where a palette is changed, and why it is one block
 *
 * The app runs **Grafito** (`/lab/paletas.html`, variant 06), and it will not be the last one:
 * the palette is expected to keep moving. So the file is split in two halves that read very
 * differently.
 *
 * **The palette** — the first block below — is the whole set of free choices: one hue, the
 * accent, and the cyan. Roughly a dozen values, and swapping a palette means editing those and
 * nothing else.
 *
 * **The roles** — everything after it — are what the app actually spends, and most of them are
 * *derived*. The neutrals are `hsl()` off `--palette-hue`, so moving that one number moves every
 * surface, border and grey together and they cannot drift apart. That derivation is not a trick:
 * it is the same formula the lab page uses to generate its six candidates, so what is on screen
 * here and what is on screen there are the same palette, not two hand-copies of it.
 *
 * Three values live outside CSS because the platform will not read a custom property there —
 * `theme-color`, the manifest, and the standalone `/auth` page. They are mirrored in
 * `lib/palette.ts`, and `lib/palette.test.ts` reads *this file* and fails if the mirror drifts.
 *
 * ## Why `color-scheme` matters beyond our own colours
 *
 * It is not just the mechanism for `light-dark()`. It is also what tells the browser to paint the
 * things **we do not control**: scrollbars, the background behind an overscroll bounce, date
 * pickers, the default look of a `<select>`. Without it, a light theme still gets a dark
 * scrollbar and a black flash at the top of a rubber-band scroll on iOS.
 */

:root {
  /**
   * The default: follow the system. `data-theme` on `<html>` overrides it, and the inline script
   * in `layout.tsx` sets that attribute before first paint — see `ThemeScript`.
   */
  color-scheme: light dark;

  /* ============================== THE PALETTE ==============================
   *
   * Everything in this block is a free choice. Everything below it is derived from these, or is
   * a signal colour that answers to somebody else (WhatsApp's green, the yellow of a warning).
   *
   * To try another palette: change these, and nothing else in this file.
   * ======================================================================== */

  /**
   * **The hue the whole app is made of**, and the reason the neutrals below are `hsl()` and not
   * hexes. Every surface, border and grey is this hue at a fixed lightness, which is what makes a
   * palette feel like one piece instead of an accent glued onto somebody else's greys.
   *
   * 225 is Grafito's. It is also, to within a degree, what the app was already using by hand —
   * so the neutrals barely moved when the palette changed, and the whole visible difference is
   * the accent. That is Grafito's actual claim: *almost no colour*.
   */
  --palette-hue: 225;

  /**
   * **The accent, and in Grafito it is ink rather than a colour.** The primary button is the
   * darkest thing on a light page and the lightest thing on a dark one, and the only saturated
   * colour left on screen is the cyan that means *you* — plus WhatsApp's green when it shows up.
   *
   * Two consequences that are easy to trip over, both of them the point rather than side effects:
   *
   * - **`--accent-ink` is no longer white.** It used to be, for both themes, because a violet
   *   button carries white ink either way. An ink-coloured button cannot: in the dark the accent
   *   *is* nearly white. Anything sitting on `--accent` has to spend `--accent-ink` — writing
   *   `#fff` by hand is now a bug that only shows in one theme.
   * - **`--accent-weak` lands on top of `--surface-2`.** With no hue to tint with, "the accent as
   *   a background" and "a raised grey" are the same grey. It reads fine — an active tab is still
   *   a chip with ink-coloured text against muted text — but it means a tinted panel no longer
   *   says *brand* on its own. **This is the seam to cut** if the tint has to come back: push
   *   `--accent-weak` a step darker than `--surface-2` rather than reaching for a hue.
   */
  --accent: light-dark(#171a21, #eceef3);
  --accent-weak: light-dark(#e9ebf1, #23262f);
  --accent-ink: light-dark(#ffffff, #14161c);

  /**
   * The cyan means one thing in this app and it is worth keeping straight: **"this is you"** —
   * your marker, your group's circle, the 🫵 chip. Grafito raises the stakes on it: with the
   * accent gone quiet, this is the only colour left that carries meaning, so anything that
   * borrows it steals from it.
   *
   * It is written in the **signal register** —same lightness and chroma as the red, the amber and
   * the green, only the hue changes— because *you* is one more thing the app says with colour and
   * it has to sound like the others. See the block further down for the numbers and why they are
   * those. The two values are not one colour lightened and darkened: a cyan that carries text on
   * near-black cannot carry it on white.
   */
  --cyan: light-dark(oklch(0.45 0.15 205), oklch(0.78 0.19 205));
  /* The muted background of the cyan, analogous to `--accent-weak` against `--accent`. The solid
     cyan is a 5 px chip, not a block, and a full-width notice needs the block. */
  --cyan-weak: color-mix(in oklab, var(--cyan) 10%, var(--bg));
  /**
   * **The cyan as a solid fill, and it is the same in both themes** — same argument as
   * `--warn-solid` further down. `--cyan` above is the cyan *as text*, which is why its light
   * value is a dark teal: nothing brighter is readable on white. That makes it useless where the
   * cyan has to be a **block on the map** — the selected marker — because there a dark teal reads
   * as a dead grey-blue instead of *this one is you*.
   *
   * A bright cyan block with near-black ink works on a white page and on a black one, because
   * what it sits on is its own fill.
   *
   * **It is for the map and not for the page.** The "you" chip on a member card used to spend it
   * and does not any more: on a white card a block of bright cyan was the loudest thing on screen,
   * louder than the primary button, which is ink. On the page the cyan says *you* as a tint
   * (`--cyan-weak` + `--cyan`); the solid fill is what a marker needs to survive the tiles.
   */
  --cyan-solid: #34d3ee;
  --cyan-solid-ink: #04252b;
  /* The lighter edge of the same cyan: the ring around a selected chip, where a border in
     `--cyan-solid` would melt into the fill it surrounds. */
  --cyan-edge: #67e8f9;

  /**
   * # El registro de los colores GENERADOS
   *
   * The hue of a group or a person is derived from its id (`groupAvatarHue`, `avatarHue`) — the
   * app has no say in *which* colour anybody gets. What it does have a say in is **how loud they
   * all are**, and that is these two numbers: saturation and lightness, fixed, hue free.
   *
   * They exist because that pair was written by hand in five places and **had five different
   * values** — `55% 32%` on a group's square, `45% 42%` on a person's circle, `55% 40%` on the map
   * marker, `70% 60%` on the circle around it. The same group came out a different colour
   * depending on which file drew it, which is the exact opposite of what a generated identity is
   * for.
   *
   * The saturation came down from 45–55 % to 32 %. Not for taste: an accent made of ink leaves
   * generated colours as the loudest thing on the screen, and six shouting circles are six
   * colours nobody can name. At 32 % the six are still told apart —that is what the hue is for—
   * and they stop competing with the one colour that means something.
   *
   * ⚠️ **What this does NOT fix, because it is somebody else's decision:** nothing stops a
   * generated hue from landing on a colour that already means something — cyan is *you*, amber is
   * *verified*, red is *alarm*, and there is WhatsApp's green. The lab measured the cost of
   * reserving all five: 113° of 360 survive. That is decision 1 of
   * `el-look-nuevo-tiene-cuatro-caminos-y-ninguna-decision`, still open.
   */
  --gen-s: 32%;
  --gen-l: 42%;
  /* The same identity drawn as a **line** on the map — a circle around a group — where a fill's
     lightness disappears against the tiles. */
  --gen-line-s: 45%;
  --gen-line-l: 55%;

  /* ---------------------------------------------------------------- surfaces */

  /**
   * The elevation ramp, and it does not mean the same thing in both themes.
   *
   * In the dark it is literal: further from the page means lighter, so `--bg` is the darkest
   * thing on screen and every raised box climbs from there. In the light it inverts — a raised
   * card is **whiter than its background**, which is why `--bg` is a soft grey and `--surface` is
   * pure white. Same names, same order, opposite direction. Getting this backwards is the single
   * most common way a light theme ends up looking like a dark theme with the colours flipped.
   *
   * The lightnesses are what changes here, never the hue: all of them read `--palette-hue`, so a
   * new palette moves the whole ramp at once and no surface can be left behind on the old family.
   * `--surface` in the light theme is the exception and is pure white on purpose — a raised card
   * has nowhere left to climb.
   */
  --bg: light-dark(hsl(var(--palette-hue) 30% 97%), hsl(var(--palette-hue) 20% 6%));
  --surface: light-dark(#ffffff, hsl(var(--palette-hue) 16% 11%));
  --surface-2: light-dark(hsl(var(--palette-hue) 26% 93%), hsl(var(--palette-hue) 15% 15%));

  /**
   * `--surface-2` carries two jobs that dark mode let us conflate and light mode does not:
   * **raised** (a popup, the bell's circle) and **sunken** (an input, a chip). Both are "lighter
   * than the surface" in the dark, so one token covered them.
   *
   * They stay one token on purpose. Splitting them means auditing ~40 call sites to decide which
   * is which, and the light values chosen above read acceptably as both: a grey chip is right,
   * and a grey popup with a border and a shadow is fine. **If the new look and feel wants a real
   * distinction, this is the seam to cut** — add `--surface-raised` and move the popups to it.
   */

  --border: light-dark(hsl(var(--palette-hue) 22% 87%), hsl(var(--palette-hue) 14% 22%));
  /* For the edge that has to be seen rather than felt: a focused input, a selected card. */
  --border-strong: light-dark(hsl(var(--palette-hue) 18% 74%), hsl(var(--palette-hue) 13% 30%));

  /* ------------------------------------------------------------------- text */

  --text: light-dark(hsl(var(--palette-hue) 26% 11%), hsl(var(--palette-hue) 22% 94%));
  /* Secondary text. Both values clear 4.5:1 on their own `--bg`, which `--text-faint` does not.
     The lightnesses are the floor here: dropping the light one below 40% or raising the dark one
     above ~66% breaks that, whatever the hue ends up being. */
  --muted: light-dark(hsl(var(--palette-hue) 12% 40%), hsl(var(--palette-hue) 10% 64%));
  /**
   * Placeholder-grade only — deliberately below the contrast floor, because a placeholder that
   * reads as strongly as the value typed over it is a placeholder people try to delete. Never use
   * it for anything a person has to read.
   */
  --text-faint: light-dark(hsl(var(--palette-hue) 10% 58%), hsl(var(--palette-hue) 8% 45%));

  /* ---------------------------------------------------------------- signals */

  /**
   * # Los cuatro avisos, en un solo registro
   *
   * They are written in `oklch()` and they all share the same two numbers — lightness and
   * chroma — with only the hue changing. That is the whole point: in OKLCH those two numbers are
   * *perceptual*, so four colours that share them read as one set instead of four stickers stuck
   * on the same page. In HSL they would not: `hsl(… 50% 50%)` is a punchy red and a muddy olive.
   *
   * The register is **measured, not guessed**, and the light one is measured **on the worst
   * ground it lands on, not on `--bg`** — which is what moved it from `0.50` to `0.45`.
   *
   * The old number cleared 4.79:1 on the base `--bg` and nothing else was checked. Two grounds
   * are darker than that one and both are ordinary: the pink edition's `--bg` (its light theme
   * takes more colour, `55% 94%` against `30% 97%`) and **`--surface-2` in either edition**, which
   * is what a chip, a field or an incoming bubble is made of. There `--ok` fell to 3.99:1 and the
   * cyan to 3.92:1 — under the floor, as text, on a card anybody can open.
   *
   * | | rojo 25 | ámbar 80 | ok 165 | cyan 205 |
   * |---|---|---|---|---|
   * | claro `0.45 0.15` | `#972527` 6.10 | `#7f4600` 5.73 | `#006d3f` 4.89 | `#00697c` 4.81 |
   * | oscuro `0.78 0.19` | `#ff7e77` 7.82 | `#f5a600` 9.51 | `#00dc98` 10.75 | `#00d7f0` 11.02 |
   *
   * The light row is the pink `--surface-2`, the worst of the four grounds; on the base `--bg`
   * the same four read 7.45, 7.00, 5.97 and 5.87. The dark row is `--bg`, where nothing else is
   * darker. **Los cuatro se mueven juntos o no se mueve ninguno**: lo que los hace sonar a familia
   * es compartir los dos números, así que subir sólo los dos que fallaban los habría partido en
   * dos pares.
   *
   * ⚠️ Where these are a **dot, a ring or a border** and not text, the floor is 3:1 and the old
   * register already cleared it. Nothing here was broken; what was under the bar is the writing.
   *
   * **To retune every signal at once, move the two numbers.** They are repeated per token instead
   * of living in a variable because `light-dark()` only takes colours, not numbers — and inventing
   * a second theme mechanism for two constants would cost more than it saves.
   *
   * ## `--ok` es nuevo, y existe porque le estábamos robando el verde a WhatsApp
   *
   * "Everything is fine" had no colour of its own, so the code reached for `--wa` — the group is
   * visible on the map, your position is fresh. That breaks the rule this file already states:
   * **`--wa` only goes on something that leads to WhatsApp.** A green box that does not open
   * WhatsApp is a promise nobody meant to make, and next to a real WhatsApp button the two greens
   * argue.
   *
   * So `--ok` is deliberately **not** WhatsApp's green: hue 165 leans to emerald where WhatsApp's
   * sits at ~150, far enough that the two never read as the same green.
   *
   * ## Los `-weak` ya no se eligen a mano
   *
   * Each one is its own signal mixed into the page, so it cannot drift out of family and it
   * follows the theme for free — `--danger` and `--bg` both flip, so one expression covers both.
   * `oklab` and not `srgb` for the mix: mixing through sRGB darkens and greys the midpoint, which
   * is exactly the muddy tint these are meant to avoid.
   */
  --danger: light-dark(oklch(0.45 0.15 25), oklch(0.78 0.19 25));
  --danger-weak: color-mix(in oklab, var(--danger) 10%, var(--bg));
  /**
   * What goes *on* a red button, and it needed a name of its own the moment `--accent-ink` stopped
   * being white. The two buttons filled with `--danger` were spending `--accent-ink` — which was
   * correct only for as long as it meant "white". Under Grafito it still lands right by luck (a
   * dark ink is what the light red of the dark theme wants), and the next palette would take that
   * luck away without a single line changing here.
   */
  --danger-ink: light-dark(#ffffff, #2a0d0d);
  --warn: light-dark(oklch(0.45 0.15 80), oklch(0.78 0.19 80));
  --warn-weak: color-mix(in oklab, var(--warn) 10%, var(--bg));

  /** "Anda bien": el estado que no tenía color propio. Ver el bloque de arriba. */
  --ok: light-dark(oklch(0.45 0.15 165), oklch(0.78 0.19 165));
  --ok-weak: color-mix(in oklab, var(--ok) 10%, var(--bg));
  /* Lo que va **encima** de un `--ok` lleno (el tilde de un paso terminado). Sigue al tema por el
     mismo motivo que `--accent-ink`: en claro el verde es oscuro y en oscuro es brillante. */
  --ok-ink: light-dark(#ffffff, #06231a);

  /**
   * **The yellow as a solid block, and it is the same in both themes.**
   *
   * `--warn` above is the yellow *as text and as a border*, which is why its light value is a
   * dark brown: nothing else is readable on white. That makes it useless for the two places that
   * need warning yellow as a **fill** — the full-width banner over the map, and the verified
   * badge — where a brown block would read as mud and stop meaning "look at this".
   *
   * A yellow alert bar with dark ink is legible on a white page and on a black one, the same way
   * a strip of hazard tape is. So this pair does not follow the theme, on the same argument as
   * `--wa`: it is a signal, and its job is to be the colour people already know.
   */
  --warn-solid: #f0b429;
  --warn-solid-ink: #1a1200;

  /* ------------------------------------------------------------- WhatsApp */

  /**
   * **`--wa` is a brand colour and it does not follow the theme.** A button that goes to WhatsApp
   * has to be WhatsApp's green in both themes, or it stops saying where it leads.
   *
   * `--wa-ink` is the dark green that goes on top of it — the value was already written by hand as
   * `#04210f` in five components; it is a token now. It works in both themes for the same reason
   * the green does: the surface underneath it never changes.
   *
   * `--wa-text` is the other job, and it needs its own value: the same green used as **text or an
   * icon on the page background**, where 1.9:1 on white is unreadable. Dark mode keeps the brand
   * green; light mode gets a green dark enough to be read.
   */
  --wa: #25d366;
  --wa-ink: #04210f;
  --wa-text: light-dark(#0d7c3c, #25d366);

  /* -------------------------------------------------------------------- map */

  /**
   * What shows through before the tiles land, and the correction laid over them.
   *
   * The tiles themselves are two different image sets, swapped in `BaseTiles.tsx`. What stays
   * here is the treatment: at night the base is dark and the streets are pushed up so they read
   * as city lights (`brightness(1.35) saturate(1.9)`). **By day that correction has to be off** —
   * the same push on a light tile set blows the whole map out to white.
   *
   * `--map-bg` has to be close to the tiles it precedes, or every pan flashes the wrong colour at
   * the edges before the new tiles paint.
   */
  --map-bg: light-dark(hsl(var(--palette-hue) 18% 91%), hsl(var(--palette-hue) 18% 9%));
  --map-tint: light-dark(none, brightness(1.35) saturate(1.9) contrast(0.95));

  /* ---------------------------------------------------------------- markers */

  /**
   * **A marker does not sit on the page, it sits on the tiles** — so these are chosen against the
   * map and not against `--bg`, and that is why they are their own tokens instead of the app's
   * colours reused. The map is mid-toned in both themes: `light_all`'s streets are not white and
   * `dark_all`'s are not black.
   *
   * They used to be a dozen hand-written hexes in `lib/mapIcons.ts`, deliberately fixed, on the
   * argument that one violet with a white ring reads on both tile sets. **That argument died with
   * the violet.** Grafito's accent is ink: near-black by day, near-white by night. Near-black on
   * the night map is a hole, so the marker has to follow the theme now — the one thing the old
   * comment there said would never happen.
   *
   * Following the theme costs nothing at runtime: the markers are `divIcon`s, real HTML inside
   * the map pane, so they inherit these properties and `var()` re-resolves on its own when the
   * theme flips. The icons can still be built once and cached — a cached *string* holding
   * `var(--marker)` is not a cached colour.
   */
  --marker: var(--accent);
  --marker-ink: var(--accent-ink);
  /**
   * "You", and the one you are looking at. Fixed in both themes, unlike `--marker`: it answers to
   * the tiles, and `--cyan`'s light value is a dark teal picked to be *read on white* — on a pale
   * map that is a dull dot, exactly the opposite of what this marker is for.
   */
  --marker-you: var(--cyan-solid);
  --marker-you-ink: var(--cyan-solid-ink);
  /**
   * The ring around a marker, and it is white in both themes because its job is not to match the
   * app: it is the gap that keeps a marker from melting into whatever it lands on. The dark one
   * is for badges pinned to a *light* marker — a photo chip, a near-white pin — where a white
   * ring would have nothing to separate from.
   */
  --marker-ring: #ffffff;
  --marker-ring-dark: #101014;
  /**
   * **El filo**: un pelo de negro por fuera del aro, y la última red de contención.
   *
   * El aro de un marcador es la tinta de lo que tiene dibujado adentro (ver `lib/markerSkin.ts`),
   * así que un marcador claro lleva aro oscuro — y sobre una zona oscura del mapa ese aro se
   * mimetiza. Un píxel translúcido por fuera cubre ese caso sin pesar en ningún otro: sobre fondo
   * claro se ve como un filo, sobre fondo oscuro no se ve y no molesta.
   *
   * Negro translúcido y no un color del tema **a propósito**: no está separando el marcador de la
   * página, lo está separando de una foto satelital de una ciudad. Su compañero es el mapa.
   */
  --marker-edge: rgba(0, 0, 0, 0.45);

  /* ---------------------------------------------------------------- shadows */

  /**
   * **Shadow is not one colour scaled by opacity.** Black at 30% over a dark surface is nearly
   * invisible — dark mode gets depth from the elevation ramp above, not from shadow — while the
   * same black over white is the whole effect. So the light values are stronger *and* the dark
   * ones lean on a deeper black to stay visible at all.
   */
  --shadow-1: light-dark(0 1px 3px rgba(15, 18, 30, 0.1), 0 1px 3px rgba(0, 0, 0, 0.4));
  --shadow-2: light-dark(0 4px 14px rgba(15, 18, 30, 0.12), 0 5px 16px rgba(0, 0, 0, 0.45));
  --shadow-3: light-dark(0 8px 30px rgba(15, 18, 30, 0.16), 0 6px 24px rgba(0, 0, 0, 0.55));

  /* ------------------------------------------------------------------ focus */

  /**
   * One ring for the whole app, so keyboard focus looks the same everywhere.
   *
   * It used to be its own violet, on the argument that the ring must be seen *on top of* an
   * accent-coloured button. Under an ink accent no single colour can do that: in the light theme
   * the button is near-black and needs a light ring, in the dark theme it is near-white and needs
   * a dark one, and the ring would have to be both.
   *
   * So the rule is stated instead of fudged: **the ring is drawn outside the control** — that is
   * what `--focus-offset` is for — and its contrast partner is the page, not the button. Against
   * the page, `--accent` is by construction the highest contrast thing there is, in both themes.
   * If some control ever has to carry the ring *inside* itself, that one spends `--accent-ink`.
   *
   * The cyan was the other candidate and lost on purpose: it is the one colour left that still
   * means *you*, and a ring that lands on every focusable thing on the page would spend it.
   */
  --focus-ring: var(--accent);
  --focus-width: 2px;
  --focus-offset: 2px;

  /* ------------------------------------------------------------- typography */

  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;

  /**
   * The scale is **measured, not invented**: these are the sizes the app already uses, counted.
   * 13 px is the body of this app (104 uses), 14 next (81), 12 for chips and labels (64). A
   * clean 1.25 ratio would have replaced all three with numbers nobody wrote, and moved every
   * screen for no reason.
   *
   * The names are roles, not sizes, so the new look and feel can retune them without a rename.
   */
  --font-2xs: 11px; /* chips, timestamps, the tab bar's second line */
  --font-xs: 12px;
  --font-sm: 13px; /* the body of the app */
  --font-md: 14px;
  --font-lg: 15px;
  --font-xl: 17px;
  --font-2xl: 20px;
  --font-3xl: 28px; /* the landing's hero, and nothing else */

  --leading-tight: 1.2;
  --leading-normal: 1.45;
  --leading-loose: 1.6;

  /* 400 exists but is rare (5 uses): this UI is mostly bold. */
  --weight-normal: 400;
  --weight-medium: 600;
  --weight-bold: 700;
  --weight-heavy: 800;

  /* --------------------------------------------------------------- spacing */

  /* A 4 px grid, which is what the hand-written values already round to. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 22px; /* the gap between blocks, already used as `margin-bottom: 22px` */
  --space-6: 32px;
  --space-7: 48px;

  /* ---------------------------------------------------------------- radii */

  --radius-sm: 6px;
  --radius-md: 8px; /* buttons — the most used radius in the app */
  --radius-lg: 10px; /* the wide CTAs */
  --radius-xl: 12px;
  --radius-2xl: 16px; /* cards and panels */
  --radius-pill: 999px;

  /* --------------------------------------------------------------- motion */

  --dur-fast: 80ms; /* the press of a button */
  --dur-base: 120ms; /* hover, colour changes */
  --dur-slow: 240ms; /* something entering or leaving */
  --ease: ease;
}

/**
 * The two explicit choices. They set `color-scheme` and nothing else — every token above follows
 * on its own, and so do the scrollbars and form controls the browser draws for us.
 *
 * The attribute goes on `<html>`, written by the inline script before first paint.
 */
:root[data-theme="light"] {
  color-scheme: light;
}

:root[data-theme="dark"] {
  color-scheme: dark;
}

/**
 * **The pink edition, and it is a palette rather than a theme.** It rides on its own attribute so
 * it multiplies with light and dark instead of replacing them, and every value here is still a
 * `light-dark()` pair. See `lib/edition.ts`.
 *
 * Only the free choices are restated: the neutrals are `hsl(var(--palette-hue) …)`, so moving the
 * hue moves every surface, border and grey with it. Nothing else in this file knows the edition
 * exists, and turning it off is deleting this block.
 */
:root[data-edition="pink"] {
  --palette-hue: 342;

  /**
   * **El tema claro pide más color que el oscuro, y no es un capricho.** Con las saturaciones de
   * la paleta base, mover sólo el tono deja una página que se lee como blanca: en oscuro basta un
   * 20 % para que el fondo se vea ciruela, y en claro ese mismo 30 % a 97 % de luz es papel.
   *
   * Los tres suben juntos porque son una rampa: si sólo subiera `--bg`, un chip de `--surface-2`
   * quedaría **más gris que la página que tiene detrás**. Los brazos oscuros son los de la paleta
   * base, sin tocar.
   *
   * Medido contra el fondo nuevo: `--muted` 5.26:1, `--text` 14.78:1 y `--accent` 5.48:1 — todos
   * por encima del piso. Y las tarjetas blancas despegan más que antes (1.19:1 contra 1.08:1),
   * que es la otra mitad de lo que se veía lavado.
   */
  --bg: light-dark(hsl(var(--palette-hue) 55% 94%), hsl(var(--palette-hue) 20% 6%));
  --surface-2: light-dark(hsl(var(--palette-hue) 40% 90%), hsl(var(--palette-hue) 15% 15%));
  --border: light-dark(hsl(var(--palette-hue) 30% 84%), hsl(var(--palette-hue) 14% 22%));
  /**
   * Unlike Grafito's, this accent is a colour and not ink, so it gets measured like the signals
   * above: 5.48:1 on `--bg` in light, 4.95:1 on `--surface-2` —where it is the translate switch
   * under a bubble— and 7.06:1 in dark. `--focus-ring` and `--marker` still carry it.
   *
   * **Era `44%` y sobre `--surface-2` daba 4.45:1**, o sea por debajo del piso justo donde más se
   * usa como letra chica. Tres puntos de luz lo arreglan sin mover el tono.
   */
  --accent: light-dark(hsl(var(--palette-hue) 72% 41%), hsl(var(--palette-hue) 85% 70%));
  --accent-weak: light-dark(hsl(var(--palette-hue) 60% 94%), hsl(var(--palette-hue) 40% 18%));
  --accent-ink: light-dark(#ffffff, hsl(var(--palette-hue) 60% 10%));
}

/**
 * **The fallback, for browsers without `light-dark()`** (pre-2024: Safari < 17.5, Chrome < 123,
 * Firefox < 120). There, every token above computes to nothing and the app renders as unstyled
 * HTML — a blank white page with blue links, which is far worse than being stuck in one theme.
 *
 * So the guard restates the dark palette, flat. **Dark and not light**, because that is what the
 * app looked like before this file existed: an old browser keeps exactly what it had, and nobody
 * gets a regression out of a feature they cannot run.
 *
 * Only the colours are restated, and only the ones written with `light-dark()`. The neutrals stay
 * `hsl(var(--palette-hue) …)` — that syntax is from 2020 and `var()` is older still, so the hue
 * knob keeps working here too and this block cannot be left behind on the previous palette.
 * Everything defined as `var(--accent)` and friends (the markers, the focus ring) follows on its
 * own. The sizes, radii and durations are plain values any browser already understands.
 *
 * **The four signals come back as hexes and their `-weak` companions do not come back at all.**
 * The hexes are the dark arm of the `oklch()` register above, resolved — a browser this old may
 * not parse `oklch()` either, and there is no third chance after this block. The `-weak` ones need
 * nothing: they are a `color-mix()` of a signal into `--bg`, and both of those are restated here,
 * so they recompute on their own. That is the argument for deriving them in the first place.
 */
@supports not (color: light-dark(#000, #fff)) {
  :root {
    --bg: hsl(var(--palette-hue) 20% 6%);
    --surface: hsl(var(--palette-hue) 16% 11%);
    --surface-2: hsl(var(--palette-hue) 15% 15%);
    --border: hsl(var(--palette-hue) 14% 22%);
    --border-strong: hsl(var(--palette-hue) 13% 30%);
    --text: hsl(var(--palette-hue) 22% 94%);
    --muted: hsl(var(--palette-hue) 10% 64%);
    --text-faint: hsl(var(--palette-hue) 8% 45%);
    --accent: #eceef3;
    --accent-weak: #23262f;
    --accent-ink: #14161c;
    --danger: #ff7e77;
    --danger-ink: #2a0d0d;
    --warn: #f5a600;
    --ok: #00dc98;
    --ok-ink: #06231a;
    --cyan: #00d7f0;
    --wa-text: #25d366;
    --map-bg: hsl(var(--palette-hue) 18% 9%);
    --map-tint: brightness(1.35) saturate(1.9) contrast(0.95);
    --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.4);
    --shadow-2: 0 5px 16px rgba(0, 0, 0, 0.45);
    --shadow-3: 0 6px 24px rgba(0, 0, 0, 0.55);
  }

  /**
   * The edition needs its own restatement, and it is not optional: `[data-edition]` outranks the
   * `:root` above, so without this the edition's `light-dark()` accent would shadow the hex here
   * and resolve to nothing — an invisible primary button, on the browsers that have no second
   * chance.
   *
   * The neutrals are missing on purpose: they read `--palette-hue`, which the block above already
   * spends and the edition already moved.
   */
  :root[data-edition="pink"] {
    --accent: #f47198;
    --accent-weak: #401c27;
    --accent-ink: #290a13;
  }
}

/*!**********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ../../node_modules/.pnpm/next@14.2.35_@babel+core@7.29.7_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[12].use[2]!../../node_modules/.pnpm/next@14.2.35_@babel+core@7.29.7_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[14].oneOf[12].use[3]!./src/app/tokens.css ***!
  \**********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/


/*!***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ../../node_modules/.pnpm/next@14.2.35_@babel+core@7.29.7_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[12].use[2]!../../node_modules/.pnpm/next@14.2.35_@babel+core@7.29.7_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[14].oneOf[12].use[3]!./src/app/globals.css ***!
  \***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
/**
 * Every colour, size, radius and duration now lives in `tokens.css`, and both themes with them.
 * The import has to be the first thing in the file — CSS requires `@import` before any rule —
 * which also makes the load order self-evident: tokens exist before anything spends them.
 */

:root {
  /* How wide a drawn phone may get, and how big it reads. See `.at-draw`. */
  --at-draw-max: 420px;
  --at-draw-zoom: 1;
  /**
   * How much colour a drawing keeps. See `.at-draw`.
   *
   * **It is not the same number in both themes, and the reason is that the argument does not
   * survive the move.** Half the saturation reads as *calm* against a dark page — the drawing
   * steps back and the button keeps its shout. Against white the same value reads as *faded*:
   * washed-out colour on a light background is what disabled looks like, so a screenshot at 0.5
   * stops looking like a quieter WhatsApp and starts looking like a broken one.
   *
   * 0.8 in the light keeps the hierarchy — the drawing is still a step below the button that
   * sits under it — without crossing into looking switched off.
   */
  --at-calm: light-dark(0.8, 0.5);
  /**
   * The ice: what a group that has not earned the rest of the map gets to see instead of it.
   * Cyan and not grey, so it reads as frozen —something that thaws— and not as broken.
   */
  --at-frost: light-dark(oklch(0.93 0.05 205 / 0.72), oklch(0.55 0.08 205 / 0.5));
  --at-frost-line: var(--cyan);
  /**
   * The fog past the edge. **Never a flat black**: over night tiles that reads as a map that
   * failed to load, and the shape of the world is what makes it read as fog of war.
   */
  --at-fog: light-dark(oklch(0.24 0.02 250 / 0.66), oklch(0.12 0.02 250 / 0.74));
}

/* Same guard as `tokens.css`, and it has to be repeated here rather than live over there: this
   `:root` is imported *after* that one, so a fallback declared there would lose to this block.
   Without it an old browser gets `saturate()` with nothing inside — an invalid filter, so the
   drawings would come back at full colour. */
@supports not (color: light-dark(#000, #fff)) {
  :root {
    --at-calm: 0.5;
  }
}

/**
 * **The drawn phones: one width and one scale for all of them.**
 *
 * Three drawings on this page stand for the same phone — the groups story's frame, the "no
 * signup" card's scene and the WhatsApp how-to modal — and each carried its own cap: 320, 300 and
 * 380. On any screen wider than 380 px you saw the three widths in a single scroll, which reads
 * as carelessness even though each number made sense on its own.
 *
 * ## Why there is a cap, and why 420
 *
 * The cap is not an oversight, it has a written argument (`GroupsStory.tsx`, `NoLoginCard.tsx`):
 * on a laptop the landing column is 600 px of content, and a drawing that wide stops reading as a
 * phone screen and starts reading as a real map that will not respond to being touched.
 *
 * 420 px is where that line actually is: **it is about the width of the largest real phone**
 * (an iPhone Pro Max is 430 CSS px). Up to there the drawing is a phone at life size; past it, it
 * is a picture of nothing.
 *
 * ## Why `zoom` and not simply a bigger box
 *
 * Because on a laptop the problem is not that the drawing is narrow, it is that it is **small** —
 * and its insides are pixel values written by hand (`canvas` 190 tall, `avatarSize: 44`,
 * `fontSize: 15`…). Widening the frame alone just pours air around the same tiny drawing.
 *
 * `zoom` scales the whole subtree **and reserves the room it takes**, which is exactly what
 * `transform: scale()` does not do: a transformed wrapper keeps its old height and the page jumps
 * around it. It also leaves every internal ratio intact, and that matters more than it looks —
 * the groups story scrolls its member list by fractions of `scrollHeight - clientHeight`, and
 * since `zoom` scales both, that box's calibration still holds. Growing the box for real would
 * have let the whole list fit, leaving nothing to scroll and the animation dead.
 *
 * So the width fills whatever is there and what changes with the screen is the scale: on a small
 * phone the drawing is life size and edge to edge, on a big one it fills the column and reads a
 * notch larger, and on a laptop it keeps its phone shape but reads as if held closer.
 *
 * `max-width` is divided by the zoom because it is measured **inside** the scaled box: without
 * that the cap would grow along with everything else and the drawing would stretch to the full
 * column — exactly what the cap exists to prevent.
 *
 * **It goes on the drawing and never on what surrounds it.** The green button under the scene,
 * and the modal's title and note, are our own copy at the size the rest of the app uses; sweeping
 * them into the zoom would leave a laptop showing a button and a heading bigger than anything
 * else on screen. For those, `--at-draw-max` alone is enough to line them up with the drawing.
 */
/* The landing's hero. */
.at-hero {
  background: linear-gradient(160deg, var(--accent-weak), var(--surface));
  border: 1px solid var(--border);
  border-radius: var(--radius-2xl);
  padding: 28px 22px;
  margin-bottom: 24px;
}

.at-draw {
  width: 100%;
  max-width: var(--at-draw-max);
  margin-inline: auto;
  /*
   * **Todo dibujo va con la mitad del color, y es una regla de jerarquía.**
   *
   * Los dibujos son capturas de WhatsApp, así que traen el verde de WhatsApp a fondo lleno:
   * la barra de arriba, el globo del mensaje, el botón de enviar. Y lo que hay que apretar
   * en esas pantallas —el botón de abajo, el de la app— es del mismo verde. Dos cosas igual
   * de saturadas y la más grande gana, así que el que manda parecía la ilustración.
   *
   * Bajarle el color al dibujo lo arregla **sin tocar los botones**, que es lo correcto: un
   * botón tiene que quedarse con todo su color, porque de eso vive. A 0.5 el verde sigue
   * siendo verde y la escena se sigue leyendo como WhatsApp — pero un paso atrás, que es
   * donde va un argumento.
   *
   * Va acá y no en `WaPhone` porque `.at-draw` es lo que ya comparten los tres lugares donde
   * aparece un teléfono dibujado: la tarjeta de la landing, el modal del "cómo se le escribe
   * al bot" y la historia de grupos. Quien quiera otro nivel pisa `--at-calm` en su caja.
   */
  filter: saturate(var(--at-calm));
}

/* Firefox only got `zoom` in 126. Without the guard, an older one would keep the wide cap and
   none of the scale — a big box around a small drawing, which is worse than either on its own. */
@supports (zoom: 1.5) {
  .at-draw {
    max-width: calc(var(--at-draw-max) / var(--at-draw-zoom));
    zoom: var(--at-draw-zoom);
  }
}

/**
 * The steps. The widths are the **window's**, while what the drawing gets is the 640 px column
 * minus 2.5 rem of padding — so 400 px of window is 360 px of drawing.
 *
 * Steps and not a smooth curve because there is no way to write one: `zoom` takes a number, and
 * CSS cannot divide a length by a length to produce one. Four of them, so no single jump is large
 * enough to notice while resizing.
 */
@media (min-width: 400px) {
  :root {
    --at-draw-zoom: 1.1;
  }
}
@media (min-width: 560px) {
  :root {
    --at-draw-zoom: 1.2;
  }
}
@media (min-width: 720px) {
  :root {
    --at-draw-zoom: 1.35;
  }
}

/**
 * **The drawing that is a map, and may therefore grow.**
 *
 * The cap and the zoom above exist because a drawn screen stops reading as a phone the moment it
 * is wider than one. **A map has no natural width at all** — the wider it gets, the more of what
 * it is arguing actually fits on it. So it opts out of the zoom and raises its own cap instead:
 * the same box, bigger, rather than the same 420 px with larger contents inside it. It stays well
 * under the panel's 572 px of usable width, which is what keeps it from becoming a third left edge
 * on the page.
 *
 * **Only the map wears this**, and that was tried rather than assumed: the story's other two
 * blocks were given it, looked at, and taken off it again — see `Stage`'s `wide` for what each of
 * them did. They are screens of an app that is a phone.
 *
 * The two steps are the two the zoom already uses. A number of its own would be a fourth thing
 * that has to agree with the other three.
 */
@media (min-width: 560px) {
  .at-draw-wide {
    --at-draw-zoom: 1;
    --at-draw-max: 480px;
  }
}
@media (min-width: 720px) {
  .at-draw-wide {
    --at-draw-zoom: 1;
    --at-draw-max: 560px;
  }
}

/**
 * **The panel that gives a drawn block the column's width back.**
 *
 * `.at-draw` caps every drawing at 420 px no matter the screen — which is right, and is not what
 * this undoes. What it fixes is what surrounds it: the column is 600 px of content on a laptop, so
 * a capped drawing sits in the middle with **90 px of nothing on each side**, while the nav, the
 * hero and the block's own caption all start at the column's left edge. Three different left edges
 * on one page, and a caption floating 90 px away from the drawing it names.
 *
 * The panel is the column-wide box the drawing was missing: caption and drawing go inside it, the
 * gutters fill with `--surface` instead of reading as void, and every block lines up with the nav
 * again. **The symmetry comes from the frame, not from the drawing** — nothing here grows.
 *
 * This is the treatment `NoLoginCard` has had all along (its `card` spans the column and holds its
 * own label). Bringing the story's blocks in line is the whole change; no new pattern is invented.
 *
 * ## Why it only exists on wide screens
 *
 * Because narrow ones do not have the problem: below ~460 px of window the drawing already fills
 * the column, so caption and drawing share an edge and there is no gutter to fill. Painting a
 * panel there would put a second border exactly on top of the drawing's own, and its padding would
 * hand back the dead margins that `--at-draw-max` had just done away with.
 *
 * 560 px is the same step the zoom already uses. A fourth number would be one more thing that has
 * to agree with the other three.
 */
.at-draw-panel {
  margin-bottom: 22px;
}

@media (min-width: 560px) {
  .at-draw-panel {
    border: 1px solid var(--border);
    border-radius: 16px;
    background: var(--surface);
    /* Bottom-heavy like `NoLoginCard`'s card, so the two read as siblings. Horizontal padding is
       only ever spare room here: the drawing is capped well below the panel's width. */
    padding: 12px 14px 16px;
  }
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family:
    system-ui,
    -apple-system,
    "Segoe UI",
    sans-serif;
  -webkit-font-smoothing: antialiased;
}

a {
  color: var(--accent);
  text-decoration: none;
}
a:hover {
  text-decoration: underline;
}

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

input,
textarea {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
}
input::placeholder,
textarea::placeholder {
  color: var(--text-faint);
}

input[type="range"],
input[type="checkbox"] {
  accent-color: var(--accent);
}

/* The frozen stretch of the radius slider. It covers the track and lets every tap through, so
   the slider still answers — being refused is what explains the restriction on a phone. */
.at-frozen-track {
  position: absolute;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  height: 14px;
  border-left: 2px solid var(--at-frost-line);
  border-radius: 0 4px 4px 0;
  background: repeating-linear-gradient(-45deg, var(--at-frost) 0 4px, transparent 4px 8px);
  pointer-events: none;
  font-size: 10px;
  line-height: 14px;
  padding-left: 3px;
}
.at-frozen-note {
  margin: 0 0 12px;
  padding: 7px 9px;
  border-radius: 8px;
  border: 1px solid var(--at-frost-line);
  background: var(--at-frost);
  color: var(--text);
  font-size: 13px;
  transition: transform 120ms ease-out;
}
/*
 * La llave del final de la línea helada: el control que abre el panel del grupo.
 *
 * Pastilla y no un ícono suelto — adentro de un párrafo, un dibujo de 14 px se lee como
 * puntuación. `inline-flex` con el círculo alrededor lo separa del texto y le da el tamaño
 * tocable que un link de una línea no tiene.
 */
.at-frozen-fix {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  vertical-align: -8px;
  margin-left: 2px;
  border-radius: 999px;
  border: 1px solid var(--at-frost-line);
  background: var(--surface);
  color: var(--text);
}

/* Adentro del chip del mapa el texto es de 12 px: la pastilla de 26 lo desbordaría. */
.at-map-cap .at-frozen-fix {
  width: 20px;
  height: 20px;
  margin-left: 0;
  vertical-align: 0;
}

/* They just pushed past the ice. It is the same line, louder for a moment: a second element
   appearing under the finger would move the slider they are still holding. */
.at-frozen-note-hit {
  transform: scale(1.02);
  border-width: 2px;
  padding: 6px 8px;
}

/* --- Leaflet, en los dos temas --- */
.leaflet-container {
  background: var(--map-bg);
}
/**
 * The correction over the tiles, and **it only exists at night.**
 *
 * Dark: the base is dark and the streets get pushed up so they read as city lights. Other
 * combinations worth trying live in `--map-tint` in `tokens.css`.
 *
 * Light: `none`. The tile set itself changes (`BaseTiles.tsx` swaps `dark_all` for `light_all`),
 * and pushing brightness on an already-light tile blows the map out to a white sheet with the
 * streets gone. The filter was never a style choice, it was a fix for one particular tile set.
 */
.leaflet-tile-pane {
  filter: var(--map-tint);
}
/* !important: el leaflet.css se carga después y pinta el popup blanco; ganamos igual */
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
  background: var(--surface-2) !important;
  color: var(--text) !important;
  box-shadow: var(--shadow-3);
}
.leaflet-popup-content {
  color: var(--text) !important;
}
.leaflet-popup-content-wrapper a {
  color: var(--accent) !important;
}
.leaflet-popup-close-button {
  color: var(--muted) !important;
}
/* The attribution sits **on the tiles**, not on the page, so it cannot use `--bg`: it needs a
   scrim of whatever the map is under it. Hence the surface at 62% rather than a flat colour —
   the tiles have to keep showing through, it is a credit line and not a panel. */
.leaflet-control-attribution {
  background: color-mix(in srgb, var(--bg) 62%, transparent) !important;
  color: var(--text-faint) !important;
}
.leaflet-control-attribution a {
  color: var(--muted) !important;
}
.leaflet-bar a {
  background: var(--surface-2) !important;
  color: var(--text) !important;
  border-color: var(--border) !important;
}

/**
 * # Los círculos del mapa, y por qué su color vive acá y no en `pathOptions`
 *
 * Un `Circle` de Leaflet es un `<path>` de SVG, y Leaflet le escribe el color como **atributo**
 * (`stroke="…"`, `fill="…"`). Un atributo no entiende `var()`: `fill="var(--marker)"` no es un
 * color inválido que el navegador arregla, es un `fill` que no pinta nada.
 *
 * Una regla CSS sí lo entiende, y además **le gana al atributo** —los atributos de presentación
 * están abajo de todo en la cascada—, así que alcanza con pasarle un `className` al círculo y
 * pintarlo desde acá. Es la misma jugada que hacen los marcadores, que son HTML de verdad y
 * heredan las custom properties solos.
 *
 * Las opacidades siguen en `pathOptions`: ésas son números, viajan bien, y varían por estado
 * (un círculo seleccionado se llena más). Acá va sólo el color.
 */
/*
 * **Cada clase declara exactamente las propiedades que su círculo dibuja, y no una más.** Una
 * regla le gana al atributo *siempre*, también cuando el atributo dice `stroke="none"`: poner el
 * borde acá "por las dudas" le dibujaría un aro duro a los círculos que Leaflet tiene orden de
 * no bordear (`stroke: false` en `MapView`, `color: "transparent"` en `GroupsMapView`). Por eso
 * la zona de la home lleva relleno solo, y la de `/mygroup` —que sí tiene borde— es otra clase.
 */
.at-zone-circle {
  fill: var(--marker);
}
.at-zone-mine {
  fill: var(--marker);
  stroke: var(--marker);
}
/* La zona de alguien con bandera en `/mygroup`: el mismo ámbar de la insignia de verificado. */
.at-zone-mine-flag {
  fill: var(--warn-solid);
  stroke: var(--warn-solid);
}
/**
 * El círculo de UN grupo, en los mini-mapas: el mismo tono que su avatar y su marcador.
 *
 * Es el único de estos círculos cuyo color **no está en la hoja**: sale del id del grupo, o sea
 * de JavaScript. Y como el color de un `path` de Leaflet es un atributo, tampoco puede llevar el
 * `var()`. Así que el tono viaja por la otra puerta: quien dibuja el mapa le pone `--group-hue` al
 * contenedor, y la regla lo lee de ahí junto con el registro compartido. El `path` hereda las
 * custom properties del contenedor como cualquier otro nodo del DOM.
 */
.at-group-circle {
  fill: hsl(var(--group-hue) var(--gen-line-s) var(--gen-line-l));
  stroke: hsl(var(--group-hue) var(--gen-line-s) var(--gen-line-l));
}
/* El área de búsqueda: el aro punteado alrededor tuyo. */
.at-radius-circle {
  fill: var(--marker-you);
  stroke: var(--marker-you);
}
/* Everything past what this group earned. The inner ring of the same polygon is the ice edge,
   so the line and the fill can never drift apart. */
.at-fog {
  fill: var(--at-fog);
  stroke: var(--at-frost-line);
}
/* The halo under the edge, so that pulling the zoom out keeps the line reading as a limit and
   not as the tiles simply going dark. */
.at-fog-halo {
  stroke: var(--at-frost-line);
}
/* The label welded to the ice line. See `frostEdgeIcon`. */
.at-frost-edge-label {
  display: inline-block;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid var(--at-frost-line);
  background: var(--bg);
  color: var(--text);
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.18);
}
/* El punto "acá estás vos" del mini-mapa de una solicitud, que es un `CircleMarker` y no un
   `divIcon` — mismo color que el marcador de vos, con el aro blanco de todos los marcadores. */
.at-you-circle {
  fill: var(--marker-you);
  stroke: var(--marker-ring);
}

/* --- Nav: fila arriba en desktop, barra de íconos abajo en mobile --- */

/**
 * El nav queda FIJO arriba.
 *
 * Antes era un bloque normal en el flujo y se iba con el scroll: bastaba bajar un poco
 * —el mapa, la lista de cercanos, /chats— para que desapareciera, y con él el único acceso
 * a la navegación. Hoy los destinos viven abajo (`.at-tabbar`), pero acá siguen la marca y
 * la campanita, y esos también tienen que quedarse a la vista.
 *
 * `sticky` y no `fixed`: `html`/`body` no tienen `overflow` seteado, que es lo que suele
 * romper el sticky, y así el nav sigue ocupando su lugar en el flujo — con `fixed` habría
 * que compensar su alto a mano en cada página.
 *
 * ## Los dos agregados que el sticky obliga
 *
 * **Fondo propio.** Transparente, el contenido se le vería pasar por debajo.
 *
 * **`z-index: 2000`, y el número no es al azar.** Tiene que ganarle al mapa y perder contra
 * los overlays:
 *
 * - Leaflet llega a 1000 (`.leaflet-top`) y nuestras chapitas sobre el mapa a 1200
 *   (`MapView.tsx`, `GroupsMapView.tsx`). `.leaflet-container` no crea contexto de
 *   apilamiento propio, así que esos números compiten de igual a igual contra el nav: con
 *   menos de 1200, el mapa le pasaría por encima.
 * - Los overlays de pantalla completa van en 3000 y 4000, y tienen que seguir tapándolo:
 *   un nav flotando sobre una ficha abierta a pantalla completa se ve roto.
 *
 * ⚠️ Al ponerle `z-index`, el nav pasa a ser un **contexto de apilamiento**: lo que vive
 * adentro queda encerrado en estos 2000. El panel de la campanita pide 3500 y ahora vale
 * como "arriba de todo lo del nav", no como 3500 global — sigue tapando el mapa, que es lo
 * que importa, pero un overlay de 3000 abierto encima lo taparía. Es aceptable porque para
 * abrir un overlay hay que tocar el mapa, que está detrás del panel.
 */
.at-nav {
  position: -webkit-sticky;
  position: sticky;
  top: 0;
  z-index: 2000;
  background: var(--bg);
  display: flex;
  align-items: center;
  gap: 6px;
  /* Los negativos de los costados sacan al nav del padding del contenedor de `layout.tsx`
     (1.25rem) y el padding se lo devuelve adentro: sin esto el fondo llegaría sólo hasta el
     borde del contenedor y por los costados se vería pasar el contenido. El negativo de
     arriba hace lo mismo con el padding superior, para que pegado al borde no quede una
     franja transparente. */
  margin: -1.25rem -1.25rem 22px;
  padding: 1.25rem 1.25rem 14px;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}
.at-nav > a:first-child {
  margin-right: auto; /* la marca empuja el resto a la derecha */
}
/**
 * La chapita de buddy: "estás entrando como buddy", en todas las pantallas.
 *
 * **Sobrevive al colapso del teléfono, y por eso está acá y no en una página.** Con sesión
 * y en móvil esta barra se reduce a la campanita (abajo, `.at-nav.logged`): la marca y la
 * fila de links se esconden. La chapita queda en esa misma caja absoluta, o sea **a la
 * vista sin ocupar una sola fila** — igual que la campanita, y por la misma cuenta.
 *
 * `--accent-weak` y no un color propio: es un estado de la cuenta, no una alerta. Si
 * gritara, competiría con la campanita, que sí tiene algo que pedirte.
 */
/**
 * El reloj chico de la barra: cuánto te queda de vida, en todas las pantallas.
 *
 * Calcado de `.at-buddy-pill` en medidas —misma altura de 22 px, mismo radio, misma
 * separación de la campanita— porque comparten fila y son el mismo tipo de objeto: una
 * chapita de estado pegada al borde de arriba. Lo que cambia es que ésta **es un botón**
 * (abre el modal de seguir vivo), así que agrega el reset del `<button>` y un `cursor`.
 *
 * Tono neutro mientras sobra tiempo: es un dato, no una alarma. `--warn` recién en el
 * último tramo, que es el mismo corte con el que la app cambia de comportamiento — ver
 * `LifeChip`.
 */
.at-life {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 24px;
  padding: 0 10px;
  /* **The border is half the affordance**, and it is what this chip has instead of a fill:
     with no background of its own it would read as plain text sitting in the row. The other
     half is that the label is NOT `--muted` — muted says "background detail, nothing to do
     here". (The buddy chip solves the same problem the other way, with a solid fill.) */
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  /* Cifras de ancho fijo: sin esto el chip se ensancha y se angosta solo cada vez que el
     número cambia de dígitos, y como está anclado a la derecha, empuja lo que tiene al lado. */
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  vertical-align: middle;
  cursor: pointer;
  transition:
    border-color 120ms ease,
    color 120ms ease,
    transform 80ms ease;
}
/**
 * El área tocable, de 44 px, **sin agrandar el dibujo**.
 *
 * La chapita mide 24 px porque comparte línea con la campanita y con la de buddy; un botón
 * de 24 px en un teléfono es un blanco que se falla. El pseudo-elemento estira la zona
 * sensible hacia afuera sin mover un píxel de lo que se ve, que es el mismo truco que usan
 * los íconos chicos de la barra.
 */
button.at-life::before {
  content: "";
  position: absolute;
  inset: -10px -6px;
}
button.at-life:hover {
  border-color: var(--accent);
  color: var(--accent);
}
/* Que hunda al tocar: en el teléfono no hay hover, así que éste es el único acuse de
   recibo antes de que se abra el modal. */
button.at-life:active {
  transform: scale(0.94);
}
/**
 * Y el `<span>`, que es la misma chapita **sin nada que tocar**.
 *
 * La usa la fila de una conversación en la bandeja: ahí el reloj informa y no ofrece, porque
 * lo que renueva una sala es escribir, y abrir la sala ya es lo que hace la fila entera. Por
 * eso las tres reglas de arriba se escriben con `button.` adelante en vez de sueltas: el área
 * tocable de 44 px, el hover y el hundido son del botón, y heredarlos acá prometería un gesto
 * que no existe — encima adentro de otro botón, que se lo lleva el clic igual.
 *
 * El `cursor` es lo único que hay que deshacer, porque vive en la base compartida.
 */
span.at-life {
  cursor: inherit;
}
.at-life.low {
  background: var(--warn-weak);
  border-color: var(--warn);
  color: var(--warn);
}

/**
 * The buddy chip. **It is a `<button>` now**, so the reset lives here: `<button>` brings its
 * own font, border and background, and without clearing them the chip would stop matching the
 * clock it shares the row with.
 *
 * Unlike `.at-life` below, this one does NOT need a border to read as touchable: the filled
 * accent already sets it apart from everything around it, and a border on top of a solid fill
 * is a second outline saying the same thing. `cursor: pointer` does the rest.
 */
.at-buddy-pill {
  display: inline-flex;
  align-items: center;
  /* 22 px, la misma altura del reloj: comparten fila y son el mismo tipo de objeto. Los
     centros los alinea el `align-items: center` de `.at-header`, no una constante. */
  height: 22px;
  padding: 0 9px;
  border: none;
  border-radius: 999px;
  background: var(--accent-weak);
  color: var(--accent);
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
  vertical-align: middle;
  cursor: pointer;
}
/**
 * La fila de arriba de cada pantalla (`PageHeader`): controles de la página a la izquierda,
 * chapita de buddy + reloj + campanita a la derecha.
 *
 * **Los 44 px son los de la campanita**, que es el mínimo tocable y el elemento más alto de
 * la fila: fijarlos acá hace que la altura no cambie según qué haya a la izquierda, o sea que
 * el contenido de la página empieza siempre a la misma altura.
 *
 * `margin-right: auto` en el hueco de la izquierda es lo único que hace falta para anclar la
 * esquina a la derecha — antes eso lo hacía `position: absolute` y costaba una variable
 * medida con un `ResizeObserver` más un `paddingRight` en cada página.
 *
 * **Sin `position` y sin `z-index`.** Está en el flujo, así que se va con el scroll — que es
 * lo que ya hacía en el teléfono, donde colgaba del contenedor de la página y no de la
 * pantalla. Fijarla pediría un `z-index`, y eso la convertiría en contexto de apilamiento
 * encerrando adentro al panel de avisos de la campanita, que pide 2500 para quedar arriba
 * del mapa.
 */
.at-header {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  margin-bottom: 6px;
}
.at-header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-right: auto;
  min-width: 0;
}
/**
 * El rótulo de "Escanear" (`ScanQrButton`), que se va en las pantallas angostas.
 *
 * La fila ya lleva "Compartir la app", el reloj y la campanita, y en un teléfono de 360 px las
 * dos palabras juntas la desbordan. El marco de la cámara se entiende solo y el nombre sigue en
 * el `aria-label`, así que lo que se pierde es la palabra, no el botón.
 */
@media (max-width: 420px) {
  .at-scan-label {
    display: none;
  }
}
/**
 * La barra de abajo: los cuatro destinos, siempre a la vista, **sólo en el teléfono**.
 *
 * En desktop no hace falta —el nav de arriba ya es una fila cómoda— así que la barra vive
 * entera adentro del media query y arriba de 600 px ni existe. Y al revés: la fila de links
 * del nav se esconde en el teléfono, porque ahí el trabajo lo hace la barra. Son la misma
 * navegación en dos formas, no dos navegaciones que haya que mantener sincronizadas.
 *
 * **`z-index: 2000`, el mismo del nav**, y por la misma cuenta: le gana a Leaflet (sus
 * controles llegan a 1000 y nuestras chapitas sobre el mapa a 1200) y pierde contra los
 * overlays de pantalla completa (3000 y 4000). O sea que abrir una ficha te deja sin
 * navegación, con la ✕ como única salida — que es lo correcto: un overlay **es** una
 * pantalla entera, y por eso el `SheetNav` fijo de "nuestra ubicación" (3001, adentro de un
 * overlay de 3000) tampoco choca con esto.
 *
 * `env(safe-area-inset-bottom)` no es opcional: sin eso, en un iPhone la fila de íconos
 * queda debajo de la barra del sistema.
 */
.at-tabbar {
  display: none;
}
.at-tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  flex: 1 1;
  /**
   * 52 px, que antes eran 44 (el mínimo tocable) con íconos de 22 y etiqueta de 11.
   *
   * Se agrandó "un poco" a pedido, y el poco es literal: ícono 24, etiqueta 12, y el aire de
   * arriba y abajo de 6 a 8. La barra pasa de ~49 px de alto real a ~57, o sea que **entra
   * justo en los 58 que `.at-page` ya venía reservando** — la reserva estaba calculada para
   * una barra más grande que la que había, así que ninguna página necesita cambiar.
   *
   * Los 8 px de padding hacen además un trabajo que no es de tamaño: despegan los íconos del
   * borde de abajo de la pantalla, que en el navegador (no instalado) es la franja donde
   * Safari y Chrome se quedan con el toque para mostrar su propia barra.
   */
  min-height: 52px;
  padding: 8px 4px;
  /* Para la barra indicadora del estado activo, que se posiciona contra la pestaña. */
  position: relative;
  color: var(--muted);
  text-decoration: none;
  font-size: 12px;
  font-weight: 600;
  /**
   * `manipulation` apaga el doble-toque para hacer zoom, y con él la espera de ~300 ms que el
   * navegador se toma antes de disparar el click por si viene un segundo toque.
   *
   * **Probablemente ya no cambiaba nada**: los navegadores de hoy se saltean esa espera cuando
   * el viewport declara `width=device-width`, que es lo que pone Next por su cuenta. Va igual
   * porque es explícito y no cuesta nada — pero no es el arreglo, es un por-las-dudas. El
   * arreglo es el estado `pending` de acá abajo.
   */
  touch-action: manipulation;
  /* El resaltado del sistema se tiñe de acento en vez de apagarse: es la primera respuesta al
     dedo, llega antes que cualquier cosa nuestra, y no se le puede pedir a `:active` que la
     reemplace porque en iOS `:active` sobre un `<a>` no siempre se aplica. */
  -webkit-tap-highlight-color: color-mix(in srgb, var(--accent) 25%, transparent);
  transition:
    color 0.12s ease,
    background-color 0.12s ease;
}
.at-tab:hover {
  text-decoration: none;
}
/**
 * # "Estás acá", y por qué ya no alcanza con el color
 *
 * Esto era una línea: `color: var(--accent)`. Con un acento de color funcionaba —violeta contra
 * gris se ve de reojo— y **con el acento en tinta dejó de funcionar**: `--accent` y `--muted` son
 * los dos grises oscuros de la misma familia, así que la pestaña donde estás parado se veía
 * apenas un poco más negra que las otras tres. O sea que el problema no es el tamaño del cambio,
 * es que la señal estaba puesta **en el único canal que Grafito vació**.
 *
 * La regla que lo reemplaza vale para toda la app: **acá lo seleccionado se marca con relleno y
 * con peso, no con tinte.** Son tres cosas a la vez, y ninguna sobra:
 *
 * - **La barra.** Es la que se ve de reojo, porque es lo único que aparece donde antes no había
 *   nada. Va sobre el borde de arriba de la barra —`top: -1px`, tapándolo— así que se lee como
 *   que ese borde se engrosó abajo de una pestaña, y no como un objeto nuevo flotando.
 * - **El trazo del ícono, de 2 a 2.6.** Los cinco íconos son de trazo y con `currentColor`, así
 *   que engordan sin dibujos nuevos. **Rellenarlos —el idioma de iOS— se probó y se descartó**:
 *   el de "Mapa" es un pliegue con dos líneas abiertas adentro, y relleno se convierte en una
 *   mancha que ya no se lee como un mapa. Los otros cuatro quedaban bien; uno de cinco alcanza
 *   para tirar la idea, porque la barra tiene que tratarlos a todos igual.
 * - **La etiqueta en 800.** Es la parte que sigue funcionando en la pestaña de "Perfil", donde el
 *   ícono es tu foto y no un trazo que se pueda engordar.
 *
 * Y deja separado lo que antes se pisaba: `:active`/`.pending` —el toque y el viaje al
 * servidor— siguen siendo el lavado gris de acá abajo, **sin barra**. Uno dice "esto se está
 * yendo a buscar", la otra dice "ya estás acá"; con el acento en tinta las dos habían quedado
 * pintadas casi igual.
 */
.at-tab.active {
  color: var(--accent);
}
.at-tab.active::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--accent);
  border-radius: 0 0 3px 3px;
}
/* Le gana al atributo `stroke-width` del SVG, que es lo que hace que no haga falta un segundo
   juego de dibujos. La foto de perfil no tiene trazo y no se entera: para ésa están la barra y
   la etiqueta. */
.at-tab.active svg {
  stroke-width: 2.6;
}
.at-tab.active .at-tab-label {
  font-weight: var(--weight-heavy);
}
/**
 * **La respuesta al toque, que es lo que faltaba.**
 *
 * Todas las páginas son `force-dynamic` y ninguna tiene `loading.tsx`, así que entre el toque
 * y el primer píxel nuevo hay una ida y vuelta al servidor en la que **no se pinta nada**: la
 * pantalla se queda idéntica. Un toque que sí registró se ve exactamente igual que uno que no,
 * y de ahí sale el "a veces hago clic y no responde".
 *
 * `:active` cubre el rato que el dedo está apoyado; `.pending` —que pone `TabLink` al tocar—
 * cubre el que falta, desde que el dedo se levanta hasta que la página nueva aparece.
 */
.at-tab:active,
.at-tab.pending {
  color: var(--accent);
  background: var(--accent-weak);
}
.at-tab-label {
  line-height: 1;
}

@media (max-width: 600px) {
  /* La fila de links del nav se va: abajo están los mismos cuatro. */
  .at-navrow {
    display: none;
  }

  /**
   * **Con sesión, en el teléfono, la barra de arriba no se dibuja.**
   *
   * Medía ~87 px para llevar dos cosas —la marca y la campanita— contra los 58 px de la
   * barra de abajo, que lleva cuatro destinos con ícono y etiqueta. Y su motivo para ser
   * fija **se venció**: era para que no se fuera la hamburguesa con el scroll, y esa
   * hamburguesa ya no existe. La barra de abajo ya dice dónde estás; ésta no hacía ese
   * trabajo, lo duplicaba.
   *
   * **Antes esto no era `display: none` sino `position: absolute`**, con el nav reducido a
   * una caja colgada arriba a la derecha: era la única forma de que la campanita —la única
   * que de verdad tiene que estar siempre— sobreviviera al colapso. Y salía caro, porque
   * fuera de flujo no le corre el contenido a nadie: cada página tenía que reservarle el
   * hueco a mano (`--at-corner`, medido con un `ResizeObserver` porque el ancho cambia solo)
   * contra un `top` calculado para un título de 17 px. En `/perfil`, cuyo `<h1>` es el nombre
   * de la persona a 32 px, la esquina caía encima y lo partía en dos renglones.
   *
   * Ahora las tres cosas viven en `.at-header`, una fila de verdad que pone el layout arriba
   * de cada pantalla, y acá no queda nada que colgar.
   *
   * **La marca es para el que todavía no entró**, y ése conserva su barra entera (con el
   * idioma y "Entrar"). Al logueado no le hace falta que le recuerden en qué app está.
   */
  .at-nav.logged {
    display: none;
  }

  .at-tabbar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2000;
    display: flex;
    align-items: stretch;
    /**
     * **The bar gets a plate of its own, and in the light theme that is the whole fix.**
     *
     * It used to be `--bg`, the page's exact colour, so the only thing separating the bar from
     * what it sits on was a hairline border. That works in the dark, where `--border` reads
     * clearly against a near-black page. In the light theme both are near-white — 87 % against
     * 97 % — and the four tabs stopped looking like a bar at all: they read as text floating at
     * the foot of the page, which is what "no se ven" was.
     *
     * `--surface` is the token that already means raised, and it says it in both themes without
     * a single conditional: pure white over a grey page by day, a lighter grey over a darker one
     * by night. The border goes up to `--border-strong` for the same reason — it is the seam
     * between two surfaces now, not a rule drawn on one.
     */
    background: var(--surface);
    border-top: 1px solid var(--border-strong);
    padding-bottom: env(safe-area-inset-bottom);
  }
  /**
   * Lugar para la barra abajo de todo, o el último elemento de cada página queda tapado.
   * Va sobre el contenedor de `layout.tsx`, que ya traía `2.5rem` de aire: se le suman los
   * ~58 px de la barra más la safe area.
   */
  .at-page {
    padding-bottom: calc(3.5rem + 58px + env(safe-area-inset-bottom)) !important;
  }
}

/* --- El botón flotante de invitar (`InviteQr` variant="fab") --- */

/*
 * Sumar gente es lo que traba el onboarding, así que la acción no puede depender de que
 * alguien scrollee hasta encontrarla: flota abajo a la derecha y se ve entre desde arriba
 * de todo hasta el final de la pantalla.
 *
 * `z-index: 1500`: le gana a las chapitas del mapa (1200) y pierde contra la barra de abajo
 * y el nav (2000) y contra los overlays (3000/4000), así que abrir la hoja del QR lo tapa
 * en vez de dejarlo flotando arriba.
 *
 * En CSS y no en un `style` inline porque el alto de la barra de pestañas depende de un
 * media query, igual que el strip de staging.
 */
.at-invite-fab,
.at-global-scan-fab,
.at-chat-contact-fab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border: none;
  border-radius: 999px;
  padding: 0;
  background: color-mix(in srgb, var(--accent) 88%, transparent);
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
  color: var(--accent-ink);
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  /* Sombra y no borde: es lo único de la pantalla que está por encima del papel. */
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
  cursor: pointer;
  opacity: 0.82;
  transition: opacity 160ms ease;
}

.at-invite-fab:hover,
.at-invite-fab:focus-visible,
.at-invite-fab:active,
.at-global-scan-fab:hover,
.at-global-scan-fab:focus-visible,
.at-global-scan-fab:active,
.at-chat-contact-fab:hover,
.at-chat-contact-fab:focus-visible,
.at-chat-contact-fab:active {
  opacity: 1;
}

.at-fab-label {
  display: none;
}

.at-invite-fab {
  position: fixed;
  right: 16px;
  bottom: calc(16px + env(safe-area-inset-bottom));
  z-index: 1500;
}

.at-global-scan-fab {
  position: fixed;
  right: 16px;
  bottom: calc(16px + env(safe-area-inset-bottom));
  z-index: 1500;
}

.at-invite-fab:disabled,
.at-global-scan-fab:disabled,
.at-chat-contact-fab:disabled {
  opacity: 0.58;
  cursor: default;
}

.at-chat-contact-actions,
.at-map-contact-actions {
  position: fixed;
  right: 16px;
  bottom: calc(74px + env(safe-area-inset-bottom));
  z-index: 1500;
}

.at-map-contact-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
}

/* A FAB inside a stack lets the stack own its fixed position. */
.at-chat-contact-actions .at-invite-fab,
.at-map-contact-actions .at-invite-fab {
  position: static;
}

@keyframes at-live-story-ring {
  from {
    transform: rotate(-2deg);
    filter: hue-rotate(-7deg) saturate(1);
  }
  to {
    transform: rotate(2deg);
    filter: hue-rotate(7deg) saturate(1.08);
  }
}

.at-live-story-ring {
  animation: at-live-story-ring 3.8s ease-in-out infinite alternate;
  will-change: transform, filter;
}

@media (prefers-reduced-motion: reduce) {
  .at-live-story-ring {
    animation: none;
  }
}

@media (max-width: 600px) {
  /* Arriba de la barra de pestañas (~58 px), que en este ancho sí existe. */
  .at-invite-fab,
  .at-global-scan-fab {
    bottom: calc(70px + env(safe-area-inset-bottom));
  }
  .at-chat-contact-actions,
  .at-map-contact-actions {
    bottom: calc(128px + env(safe-area-inset-bottom));
  }
}

/* --- El guiño de las flechas de los overlays (`SheetNav`) --- */

/*
 * Tres veces y se termina: `animation-iteration-count` es lo que la mata, así que no hay
 * ningún timer en JS que pueda quedar colgado. Quién lo enciende y cuándo deja de aparecer
 * está en `useHint`, en `SheetNav.tsx`.
 *
 * El medio segundo de espera no es decorativo: el overlay entra, la ficha se acomoda, y
 * recién entonces se mueve algo. Animar mientras la pantalla todavía se está armando es
 * pedirle atención a alguien que está mirando otra cosa.
 *
 * ## Son dos cosas a la vez, y cada una hace un trabajo distinto
 *
 * **La flecha se enciende y se corre.** No sólo se mueve: en el pico se llena del color de
 * acento con la cabeza en blanco y crece un 14 %. El movimiento solo se le escapa a
 * cualquiera que en ese momento esté leyendo el nombre o mirando la foto; un botón que
 * *cambia de color* se ve por el rabillo del ojo, que es exactamente lo que hay disponible.
 *
 * **Y suelta un ping.** Un aro que sale de la flecha y se desvanece, como el radar de un
 * mapa. Es lo que más levanta la mirada porque **crece hacia afuera del control**: ocupa
 * lugar donde no había nada, en vez de moverse adentro de su propio círculo. Va en un
 * `::after` para que no le toque el layout a la barra ni tape nada (`pointer-events: none`).
 *
 * Ah, y **la flecha de la izquierda arranca un poco después**. El ojo detecta antes lo
 * desparejo que lo simétrico: las dos juntas se leen como una decoración, una atrás de la
 * otra se lee como algo que está pasando.
 *
 * `transform`, `opacity`, `background` y `color`: nada de esto recalcula el layout de la
 * barra en cada cuadro.
 */
@keyframes at-hint-prev {
  0%,
  70%,
  100% {
    transform: translateX(0) scale(1);
    background: var(--accent-weak);
    color: var(--accent);
  }
  35% {
    transform: translateX(-7px) scale(1.14);
    background: var(--accent);
    color: var(--accent-ink);
  }
}
@keyframes at-hint-next {
  0%,
  70%,
  100% {
    transform: translateX(0) scale(1);
    background: var(--accent-weak);
    color: var(--accent);
  }
  35% {
    transform: translateX(7px) scale(1.14);
    background: var(--accent);
    color: var(--accent-ink);
  }
}
/* El aro del ping: sale del borde de la flecha y se apaga antes de llegar lejos. */
@keyframes at-hint-ping {
  0% {
    transform: scale(1);
    opacity: 0.75;
  }
  55%,
  100% {
    transform: scale(2.1);
    opacity: 0;
  }
}
.at-hint-prev,
.at-hint-next,
.at-hint-prev-once,
.at-hint-next-once {
  /* Para que el `::after` se acomode contra la flecha y no contra la barra. */
  position: relative;
}
.at-hint-prev::after,
.at-hint-next::after,
.at-hint-prev-once::after,
.at-hint-next-once::after {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: 50%;
  border: 2px solid var(--accent);
  pointer-events: none;
  /* Invisible por defecto, y no sólo durante el retardo: cuando la animación termina, el
     elemento vuelve a ESTE estado (`animation-fill-mode` es `none`). Sin el 0, al apagarse
     el guiño quedaría un aro de acento fijo alrededor de la flecha para siempre. */
  opacity: 0;
}
.at-hint-prev {
  animation: at-hint-prev 1.1s ease-in-out 0.65s 3;
}
.at-hint-next {
  animation: at-hint-next 1.1s ease-in-out 0.5s 3;
}
.at-hint-prev::after {
  animation: at-hint-ping 1.1s ease-out 0.65s 3;
}
.at-hint-next::after {
  animation: at-hint-ping 1.1s ease-out 0.5s 3;
}

/*
 * The `-once` pair: the same choreography, blinked ONE time and with no delay.
 *
 * It is for the landing's groups story, where the bar lives inside a loop that steps every couple
 * of seconds: the three blinks of the real hint would still be running when the step already
 * changed, and a blink cut in half reads as a broken drawing. No delay for the same reason — the
 * whole step is barely twice one blink.
 *
 * `both` and not the default `none`: the arrow has to hold its resting look until the animation
 * starts and after it ends, or the last frame would leave it lit.
 */
.at-hint-prev-once {
  animation: at-hint-prev 1.1s ease-in-out both;
}
.at-hint-next-once {
  animation: at-hint-next 1.1s ease-in-out both;
}
.at-hint-prev-once::after {
  animation: at-hint-ping 1.1s ease-out both;
}
.at-hint-next-once::after {
  animation: at-hint-ping 1.1s ease-out both;
}

/*
 * Quien pidió menos movimiento no recibe ninguno — pero sí recibe la señal: la flecha se
 * queda con un halo quieto mientras el guiño está activo, y el aro del ping no sale.
 * "Reducir movimiento" es una preferencia sobre cómo se muestran las cosas, no un pedido de
 * que le escondamos los controles. Y de paso saca a esa preferencia —que en un teléfono
 * está prendida más seguido de lo que uno cree— de la lista de motivos por los que esto
 * podría no verse.
 */
@media (prefers-reduced-motion: reduce) {
  .at-hint-prev,
  .at-hint-next,
  .at-hint-prev-once,
  .at-hint-next-once {
    animation: none;
    box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 22%, transparent);
  }
  .at-hint-prev::after,
  .at-hint-next::after,
  .at-hint-prev-once::after,
  .at-hint-next-once::after {
    display: none;
  }
}

/* --- La demostración de "enviar tu ubicación" (`SendLocationHowTo.tsx`) --- */

/*
 * **Una demostración que termina en un botón**, contada entera en CSS.
 *
 * El modal abre con una captura dibujada de WhatsApp. Entra un puntero, toca el clip, se
 * abre la bandeja de adjuntar, y el renglón de "Enviar tu ubicación actual" —que es un
 * link de verdad— empieza a latir. Un rato después la demostración **se termina**: la
 * captura se va a blanco y negro, el botón deja el verde de WhatsApp por nuestro violeta,
 * y por último todo el dibujo se pliega y queda **sólo nuestro CTA**.
 *
 * ## La línea de tiempo, que hay que leer junta porque todo se encadena
 *
 * | cuándo     | qué                                                                  |
 * |------------|----------------------------------------------------------------------|
 * | 0–0,55s    | el puntero entra y viaja hasta el clip                                |
 * | 0,55s      | el clic: el puntero se achica y el clip suelta la onda                 |
 * | 0,6s       | la bandeja sube y aparece                                             |
 * | 0,86s      | el renglón de ubicación **empieza a latir**, todavía en verde          |
 * | 0,86–4s    | la captura se queda quieta, entera y en color                          |
 * | 4s         | **se va sola a WhatsApp** (el temporizador vive en `SendLocationHowTo.tsx`) |
 * | 4s         | **y por las dudas**: la captura pasa a blanco y negro y el botón a violeta |
 *
 * ## Los dos últimos pasan al mismo tiempo, y eso es a propósito
 *
 * A los 4 segundos la página **se va sola a WhatsApp**, así que en el caso normal nadie
 * llega a ver el cambio de color: la navegación se lleva la pantalla puesta. El cambio
 * está para **cuando la redirección no ocurre** — el navegador la frenó, la conexión
 * tardó, la persona volvió atrás. Ahí queda un dibujo apagado con un botón violeta que
 * late, o sea algo que se toca, en vez de una captura muerta esperando algo que no va a
 * pasar.
 *
 * Es una red, no un acto: si se ve, es porque algo no salió.
 * | 3,05s      | **el cierre**: el dibujo se pliega y sube nuestro CTA en su lugar       |
 * | 3,5s       | el CTA late y suelta su aro                                            |
 *
 * ## Por qué son tres actos y no uno
 *
 * La secuencia dice tres cosas distintas, y juntas no se entiende ninguna:
 *
 * 1. **Cómo se manda la ubicación.** Para esto la captura tiene que quedarse quieta,
 *    entera y **en color** un rato largo: es la pantalla de otra app y la persona tiene
 *    que reconocerla, y reconocer lleva más tiempo que ver moverse algo.
 * 2. **Que eso también se toca.** Es el latido, y arranca en cuanto la bandeja termina de
 *    abrirse: nada en una captura invita a tocarla, así que si el latido llegara tarde,
 *    todo ese rato la pantalla sería una foto.
 * 3. **Que el botón es NUESTRO y la demostración terminó.** Ésta sólo tiene sentido
 *    después de las otras dos, y sólo para quien sigue mirando a los tres segundos — que
 *    es justamente quien no se fue a WhatsApp.
 *
 * Hubo una versión que hacía la 3 encima de la 1, y el resultado fue que el dibujo nunca
 * llegaba a leerse: las dos cosas pasaban en el mismo instante y se fundían en una sola.
 *
 * ## Blanco y negro, no desenfoque
 *
 * También se probó desenfocar, y estaba de más: el gris ya dice "esto era una referencia,
 * ya está" **y encima deja el dibujo legible**, que no molesta. Y hace algo que el
 * desenfoque no hacía — con la captura sin color, **el violeta del botón queda como el
 * único color de la pantalla**. Ahí no hay nada que explicar: lo que tiene color es lo que
 * se toca.
 *
 * ## Nada de esto usa un `setTimeout`
 *
 * Los tiempos viven acá, en un solo lugar, y no hay ningún temporizador que pueda quedar
 * colgado si el modal se cierra en la mitad: se desmonta el nodo y se acabó.
 */

/**
 * ⚠️ **Everything from here down is WhatsApp's palette, written by hand on purpose, and it must
 * not be turned into tokens.**
 *
 * `#008069`, `#efeae2`, `#e8e8e8` and the black shadows below are not our colours: they are what
 * WhatsApp looks like. These rules paint a **simulated screenshot of somebody else's app**, and
 * the drawing's whole job is to be recognised — the person is about to go do this for real, in
 * that app, on this phone. A mock that followed Fugaz's theme would go dark next to a WhatsApp
 * that did not, and stop teaching the thing it exists to teach.
 *
 * So they stay fixed in both themes. The one thing to watch is the **frame around them**: a light
 * mock on a light page needs its border to keep the drawing from bleeding into the background,
 * and that border is ours (`--border`) and does follow the theme.
 *
 * Same rule applies in `WaMock.tsx`, `FakeWhatsApp.tsx` and `SendLocationHowTo.tsx`.
 */

/* ---------- Acto 1: el puntero toca el clip y la bandeja se abre ---------- */

.at-wa-stage {
  position: relative;
}

.at-wa-tray {
  animation: at-wa-tray 0.24s ease-out 0.6s both;
}

@keyframes at-wa-tray {
  0% {
    opacity: 0;
    visibility: hidden;
    transform: translateY(12px);
  }
  1% {
    visibility: visible;
  }
  100% {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
}

/*
 * El puntero. `pointer-events: none` porque un cursor dibujado que se coma el toque del
 * cursor de verdad sería exactamente el chiste que no queremos hacer.
 *
 * **Sale con `ease-out`, no `ease-in-out`.** Arranca a toda velocidad y frena llegando al
 * clip. Con la curva simétrica el primer tercio del viaje era casi imperceptible y la
 * animación *parecía* tardar en empezar aunque ya estuviera corriendo: lo que se siente
 * como demora no es el total, es cuánto pasa hasta que algo se mueve de verdad.
 *
 * Se va al terminar y no vuelve: ya dijo lo que tenía que decir, y un mouse fantasma dando
 * vueltas encima de un botón que late compite con el botón.
 */
/*
 * `--at-wa-speed` multiplica **todos** los tiempos de esta familia.
 *
 * Nació porque el cuadrito de la landing (`NoLoginCard`) tenía que ir más rápido que el
 * modal, y los retardos estaban escritos a mano acá: cambiarlos aceleraba las dos cosas,
 * porque el cuadrito reusa la misma escena.
 *
 * Es **un** multiplicador y no un número por pieza a propósito. Lo que hace que la escena se
 * lea como una conversación no son los valores absolutos sino **el orden y las distancias**
 * entre ellos —el tilde azul antes del "escribiendo…", la respuesta después—, y un factor
 * único los conserva por construcción. Con un tiempo por pieza, acelerar sería la
 * oportunidad de romper ese orden sin darse cuenta.
 *
 * El default es `1`, así que todo lo que no la declare queda **exactamente** como estaba.
 */
.at-wa-pointer {
  position: absolute;
  right: 50px;
  bottom: 4px;
  pointer-events: none;
  animation: at-wa-pointer calc(0.9s * var(--at-wa-speed, 1)) ease-out both;
}

@keyframes at-wa-pointer {
  0% {
    transform: translate(34px, 44px) scale(1);
    opacity: 0;
  }
  10% {
    opacity: 1;
  }
  55% {
    transform: translate(0, 0) scale(1);
    opacity: 1;
  }
  62% {
    transform: translate(0, 0) scale(0.78);
  }
  72% {
    transform: translate(0, 0) scale(1);
  }
  88%,
  100% {
    transform: translate(0, 0) scale(1);
    opacity: 0;
  }
}

/* La onda del toque, en el clip, justo cuando el puntero se achica. */
.at-wa-clip {
  position: relative;
}

.at-wa-clip::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  animation: at-wa-tap 0.45s ease-out 0.54s both;
}

@keyframes at-wa-tap {
  0% {
    box-shadow: 0 0 0 0 rgba(0, 128, 105, 0.5);
  }
  100% {
    box-shadow: 0 0 0 15px rgba(0, 128, 105, 0);
  }
}

/* ---------- Acto 2: el renglón late, y a los 2,6s deja de ser de WhatsApp ---------- */

.at-wa-cta {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 10px;
  border-radius: 8px;
  background: #008069;
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.3;
  text-decoration: none;
  /*
   * Dos animaciones, y **cada una es dueña de propiedades distintas**: el latido tiene el
   * `transform` y el `box-shadow`, el traspaso tiene sólo el `background`. Si el traspaso
   * también tocara la sombra, con su `both` la estaría fijando desde el segundo cero
   * —antes de su propio retardo— y le apagaría el latido a los dos primeros segundos.
   */
  animation:
    at-wa-beat 1.7s ease-in-out 0.86s infinite,
    at-wa-handoff 0.45s ease-out var(--at-wa-end, 4s) both;
}

/*
 * El latido, con **sombra neutra a propósito**.
 *
 * Este mismo latido corre con el botón verde y después con el botón violeta, así que la
 * sombra no puede llevar ninguno de los dos colores: teñida de verde se vería sucia sobre
 * el violeta, y al revés. Negra sirve para los dos.
 */
@keyframes at-wa-beat {
  0%,
  38%,
  100% {
    transform: scale(1);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22);
  }
  16% {
    transform: scale(1.045);
    box-shadow: 0 5px 16px rgba(0, 0, 0, 0.35);
  }
}

/*
 * Deja de ser el verde de WhatsApp y pasa a ser nuestro. Sólo el color: ver arriba.
 *
 * **La tinta viaja con el fondo, y antes no hacía falta.** Sobre el verde de WhatsApp y sobre el
 * violeta de la marca, la misma tinta blanca servía para los dos. Con el acento en tinta ya no:
 * en el tema oscuro el botón termina casi blanco, así que una flecha blanca desaparecería justo
 * al final de la demostración. Por eso el traspaso ahora es de las dos cosas a la vez.
 */
@keyframes at-wa-handoff {
  0% {
    background: #008069;
    color: #fff;
  }
  100% {
    background: var(--accent);
    color: var(--accent-ink);
  }
}

/*
 * **El blanco y negro.**
 *
 * La clase va en las piezas de la captura una por una —encabezado, burbuja, la fila de
 * íconos, el rótulo, la opción tachada y el cajón de escribir— y **no en el teléfono
 * entero**, porque `filter` se hereda a todos los descendientes: puesto en un ancestro
 * habría dejado gris también al botón, que es el único que tiene que conservar su color.
 *
 * La tarjeta blanca de la bandeja no la lleva y no hace falta: ya es blanca. El beige del
 * chat se apaga aparte, con un cambio de `background` en `at-wa-stage`, porque ése SÍ es
 * ancestro del botón y un `filter` ahí lo habría arrastrado.
 */
.at-wa-gray {
  animation: at-wa-gray 0.45s ease-out var(--at-wa-end, 4s) both;
}

@keyframes at-wa-gray {
  0% {
    filter: grayscale(0);
  }
  100% {
    filter: grayscale(1);
  }
}

.at-wa-stage {
  animation: at-wa-stage-gray 0.45s ease-out var(--at-wa-end, 4s) both;
}

@keyframes at-wa-stage-gray {
  0% {
    background: #efeae2;
  }
  100% {
    background: #e8e8e8;
  }
}

/* ========================================================================================
 * La segunda demostración: **escribirle al bot** (`JoinChatHowTo.tsx`)
 * ========================================================================================
 *
 * Misma familia, otra escena: en vez de una bandeja de adjuntar que se abre, una
 * **conversación que ocurre**. El cascarón, el teléfono y los íconos son los mismos
 * (`WaMock.tsx`); acá abajo está sólo lo que esta escena agrega.
 *
 * ## Por qué el final es una variable y ya no un 4s escrito a mano
 *
 * El blanco y negro y el traspaso a violeta tienen que caer **exactamente** cuando la página
 * se va a WhatsApp, y ese instante ya no es el mismo para las dos demostraciones: la de
 * ubicación redirige a los 4 s y ésta a los 5,4 s. Duplicar `.at-wa-gray` y `.at-wa-stage`
 * con otro retardo habría dejado dos copias de la misma máquina para arreglar por separado,
 * que es justo lo que se evitó partiendo el componente.
 *
 * Entonces las clases compartidas leen `var(--at-wa-end, 4s)` y **la escena la declara**: la
 * de ubicación no declara nada y cae al default, la de entrada pone su `REDIRECT_MS` en un
 * `style` inline. El número vive en el TSX, que es donde está el `setTimeout` que tiene que
 * coincidir con él.
 *
 * ## La línea de tiempo
 *
 *   0,00 – 0,90  el puntero entra y toca ENVIAR (la onda del toque, a los 0,54)
 *   0,62         la burbuja verde sube: el mensaje salió
 *   1,20         doble tilde AZUL — el bot lo leyó
 *   1,50         "escribiendo…" debajo del nombre
 *   2,45         se va el "escribiendo…" y **entra la respuesta del bot**
 *   3,00         el botón de enviar empieza a latir
 *   5,40         blanco y negro + violeta, y la página se va a WhatsApp
 *
 * **El orden de los tres primeros no es decorativo, es lo único que hace que se lea como una
 * conversación de verdad.** El tilde azul va ANTES del "escribiendo…" porque abrir un chat
 * para escribir es justamente lo que lo marca como leído: al revés sería un chat que contesta
 * algo que no leyó, que ningún cliente de WhatsApp puede hacer. Es la misma regla que el bot
 * ya respeta al responder (ver `humanTiming` en `packages/messaging`).
 *
 * **Y el latido llega tarde a propósito.** En la escena de ubicación arranca en cuanto la
 * bandeja termina de abrirse, porque ahí no hay nada más que mirar. Acá hay una conversación
 * ocurriendo, y un botón latiendo mientras el bot contesta compite con lo único que la
 * persona tiene que leer. Empieza cuando la respuesta ya está puesta.
 */

/* ---------- Acto 1: el toque en ENVIAR y el mensaje que sale ---------- */

/*
 * El puntero apunta al botón de enviar, que está pegado al borde derecho — el de la otra
 * escena apunta al clip, que está más adentro. Sólo cambia dónde termina: el viaje, la curva
 * y el achique son los mismos `@keyframes`.
 */
.at-wa-pointer--send {
  right: 12px;
  bottom: 6px;
}

/*
 * El botón de enviar: la onda del toque, el latido y el traspaso a violeta.
 *
 * Es el equivalente exacto de `.at-wa-cta` en la otra escena — el control dibujado que además
 * es el botón de verdad—, con la diferencia de que acá es un círculo y allá un renglón. Las
 * mismas dos animaciones y el mismo reparto de propiedades: el latido es dueño del
 * `transform` y la sombra, el traspaso sólo del `background`.
 */
.at-wa-send {
  /* El verde vive acá y no en el `style` del componente: una declaración inline le gana a la
     regla de `prefers-reduced-motion` y dejaba este botón verde en una captura ya gris. Ver
     la nota de `roundBtn` en `WaMock.tsx`. */
  background: #008069;
  /* La flecha de adentro (`IconSend`) trae `fill="#fff"` como ATRIBUTO, y un atributo pierde
     contra cualquier regla: pasándola a `currentColor` sigue al `color` que el traspaso anima,
     que es lo único que la salva de quedar blanca sobre un botón casi blanco. */
  color: #fff;
  animation:
    at-wa-beat calc(1.7s * var(--at-wa-speed, 1)) ease-in-out calc(3s * var(--at-wa-speed, 1))
      infinite,
    at-wa-handoff 0.45s ease-out var(--at-wa-end, 4s) both;
}

.at-wa-send path {
  fill: currentColor;
}

.at-wa-send::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  animation: at-wa-tap calc(0.45s * var(--at-wa-speed, 1)) ease-out
    calc(0.54s * var(--at-wa-speed, 1)) both;
}

/*
 * La burbuja saliente. Sube un poco al aparecer, como cuando mandás uno de verdad.
 *
 * `visibility` además de `opacity`, igual que la bandeja: sin eso la burbuja está ahí desde el
 * primer cuadro, transparente pero presente, y en el traspaso a blanco y negro se la ve
 * aparecer un instante antes de tiempo en algunos navegadores. El lugar **sí** lo ocupa desde
 * el principio —por eso no hay `height` animada— para que la respuesta del bot y el cajón no
 * den un salto cuando llega.
 */
.at-wa-out {
  animation: at-wa-rise calc(0.28s * var(--at-wa-speed, 1)) ease-out
    calc(0.62s * var(--at-wa-speed, 1)) both;
}

@keyframes at-wa-rise {
  0% {
    opacity: 0;
    visibility: hidden;
    transform: translateY(8px);
  }
  1% {
    visibility: visible;
  }
  100% {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
}

/*
 * **El cajón se vacía al enviarse** (sólo el loop: ver `clearOnSend` en `JoinChatHowTo.tsx`).
 *
 * El retardo es **el mismo que el de la burbuja que sube**: el texto tiene que irse del cajón
 * en el instante exacto en que aparece arriba, porque es la misma cosa moviéndose de lugar.
 * Un cuadro de diferencia y se ve dos veces el mensaje, o ninguna.
 *
 * Más corto que el ascenso (0,18 s contra 0,28 s) a propósito: lo que hay que mirar es la
 * burbuja llegando, no el hueco que deja.
 */
.at-wa-sent-hide {
  animation: at-wa-fade-out calc(0.18s * var(--at-wa-speed, 1)) ease-out
    calc(0.62s * var(--at-wa-speed, 1)) both;
}

.at-wa-sent-show {
  animation: at-wa-fade-in calc(0.18s * var(--at-wa-speed, 1)) ease-out
    calc(0.62s * var(--at-wa-speed, 1)) both;
}

@keyframes at-wa-fade-out {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/* ---------- Acto 2: lo leyó, lo está escribiendo, contesta ---------- */

/* El tilde azul se enciende encima del gris. Ver `IconTicks`. */
.at-wa-read {
  animation: at-wa-fade-in calc(0.2s * var(--at-wa-speed, 1)) ease-out
    calc(1.2s * var(--at-wa-speed, 1)) both;
}

@keyframes at-wa-fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

/*
 * "escribiendo…" entra y se va. Ocupa su renglón siempre (nace transparente, no escondido)
 * para que el encabezado no crezca al aparecer: un nombre que salta hacia arriba en medio de
 * la escena es lo primero que delata que esto es un dibujo.
 */
.at-wa-typing {
  animation: at-wa-typing calc(0.95s * var(--at-wa-speed, 1)) ease-out
    calc(1.5s * var(--at-wa-speed, 1)) both;
}

@keyframes at-wa-typing {
  0% {
    opacity: 0;
  }
  20%,
  80% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/* La respuesta del bot, justo cuando se apaga el "escribiendo…". */
.at-wa-reply {
  animation:
    at-wa-rise calc(0.28s * var(--at-wa-speed, 1)) ease-out calc(2.45s * var(--at-wa-speed, 1)) both,
    at-wa-gray 0.45s ease-out var(--at-wa-end, 4s) both;
}

/* ========================================================================================
 * The second door: the same demos drawn as Telegram (`docs/telegram-ui.md`)
 * ========================================================================================
 *
 * **Only the gesture is new here.** The pointer, the bubble that rises, the read tick, the
 * "typing…" and the beat are the same machine and get reused as they are — copying them under
 * another prefix would be two of everything to fix. What Telegram does not have is a tray that
 * opens; what it has instead is a **native keyboard that comes up** with a button on it, and
 * that button is one tap, not three. So these classes replace the tray, not the family.
 *
 * Same contract as above: `--at-wa-end` is when the drawing goes grey and hands the colour over
 * to us, `--at-wa-speed` multiplies the timings, and the scene declares both.
 */

/*
 * Telegram's chat is blue where WhatsApp's is beige, and it fades to the same dead grey. A
 * `background` swap and not a `filter` for the reason `.at-wa-stage` gives: this is an ancestor
 * of the button, and a filter here would drag its colour along.
 */
.at-tg-stage {
  position: relative;
  animation: at-tg-stage-gray 0.45s ease-out var(--at-wa-end, 4s) both;
}

@keyframes at-tg-stage-gray {
  0% {
    background: #a8c8e8;
  }
  100% {
    background: #e8e8e8;
  }
}

/*
 * The native keyboard coming up: Telegram's answer to `.at-wa-tray`, and it arrives at the same
 * 0,6 s so the two demos keep the same rhythm.
 *
 * `visibility` on top of `opacity` for the same reason as the tray: transparent-but-present, the
 * hand-off to grey shows it a frame too early in some browsers.
 */
.at-tg-kbd {
  animation: at-tg-kbd calc(0.3s * var(--at-wa-speed, 1)) ease-out
    calc(0.6s * var(--at-wa-speed, 1)) both;
}

@keyframes at-tg-kbd {
  0% {
    opacity: 0;
    visibility: hidden;
    transform: translateY(14px);
  }
  1% {
    visibility: visible;
  }
  100% {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
}

/*
 * The button the bot itself drew — *Enviar mi ubicación*, START — beating and then handing the
 * colour over to us. It is `.at-wa-cta`'s twin, with two differences.
 *
 * **Colour and animation only, no layout**: the two Telegram scenes draw it in different shapes
 * (a key on the keyboard, a bar under the bot's card) and a padding written here would be fought
 * from both. And **the beat's delay is a variable**, because it is the one number that differs:
 * the location scene starts as soon as the keyboard is up, the join scene waits for the reply.
 *
 * The blue lives here and not inline for the same reason as `.at-wa-send`: an inline declaration
 * beats the `prefers-reduced-motion` rule and would leave a blue button on a grey screenshot.
 */
.at-tg-btn {
  position: relative;
  background: #2aabee;
  color: #fff;
  animation:
    at-wa-beat calc(1.7s * var(--at-wa-speed, 1)) ease-in-out var(--at-tg-beat, 0.86s) infinite,
    at-tg-handoff 0.45s ease-out var(--at-wa-end, 4s) both;
}

@keyframes at-tg-handoff {
  0% {
    background: #2aabee;
    color: #fff;
  }
  100% {
    background: var(--accent);
    color: var(--accent-ink);
  }
}

/*
 * The tap ring, in Telegram's blue. `border-radius: inherit` because here it lands on a wide
 * button and not on a round icon, and `--at-tg-tap-at` because the tap is not always at 0,54 s.
 */
.at-tg-tap {
  position: relative;
}

.at-tg-tap::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  animation: at-tg-tap calc(0.45s * var(--at-wa-speed, 1)) ease-out var(--at-tg-tap-at, 0.54s) both;
}

@keyframes at-tg-tap {
  0% {
    box-shadow: 0 0 0 0 rgba(42, 171, 238, 0.5);
  }
  100% {
    box-shadow: 0 0 0 15px rgba(42, 171, 238, 0);
  }
}

/*
 * Where the pointer lands. Goes together with `.at-wa-pointer` — same travel, same curve, same
 * shrink — because what changed is the target: a button that spans the width, so it is aimed at
 * the middle instead of at an icon by the edge.
 */
.at-tg-pointer--btn {
  right: auto;
  left: 50%;
  bottom: 10px;
}

/* START is Telegram's whole first interaction: the bar disappears and the command it sends
 * rises into the chat. No bot reply or onboarding step belongs in this landing drawing. */
.at-tg-start-message {
  animation: at-tg-start-message calc(0.28s * var(--at-wa-speed, 1)) ease-out
    calc(0.72s * var(--at-wa-speed, 1)) both;
}

@keyframes at-tg-start-message {
  0% {
    opacity: 0;
    transform: translateY(12px);
  }
  100% {
    opacity: 1;
    transform: none;
  }
}

.at-tg-start-bar {
  animation:
    at-tg-start-bar-away calc(0.18s * var(--at-wa-speed, 1)) ease-out
      calc(0.62s * var(--at-wa-speed, 1)) both,
    at-tg-handoff 0.45s ease-out var(--at-wa-end, 4s) both;
}

@keyframes at-tg-start-bar-away {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    visibility: hidden;
  }
}

/*
 * ## Sin movimiento
 *
 * Se cae directo en **el estado final y quieto**: la bandeja abierta, la captura en blanco
 * y negro y el botón violeta. Es el mismo cuadro en el que termina todo el mundo, sólo que
 * sin el camino hasta ahí.
 *
 * Quedarse en el inicial sería peor de las dos formas posibles: la bandeja escondida
 * esperando un clic que nadie va a ver, y el botón con el verde de otra app para siempre.
 * En blanco y negro se pierde el verde del pin de Ubicación, y se acepta: la captura sigue
 * siendo legible entera, y a cambio el violeta del botón queda —también acá— como el único
 * color de la pantalla.
 */
@media (prefers-reduced-motion: reduce) {
  .at-wa-pointer,
  .at-wa-clip::after,
  .at-wa-send::after {
    display: none;
  }
  .at-wa-tray,
  .at-wa-cta,
  .at-wa-send,
  .at-wa-out,
  .at-wa-read,
  .at-wa-reply {
    animation: none;
  }
  .at-wa-cta,
  .at-wa-send {
    background: var(--accent);
    color: var(--accent-ink);
  }
  .at-wa-gray,
  .at-wa-reply {
    animation: none;
    filter: grayscale(1);
  }
  .at-wa-stage {
    animation: none;
    background: #e8e8e8;
  }
  /*
   * El "escribiendo…" **se va del todo**, y es la única pieza que no tiene equivalente
   * quieto. Las otras dos escenas terminan en un estado que existe (la bandeja abierta, la
   * respuesta puesta); "escribiendo…" es un estado *de paso* — congelarlo dejaría un chat
   * eternamente a punto de contestar, al lado de la respuesta que ya llegó.
   */
  .at-wa-typing {
    animation: none;
    opacity: 0;
  }

  /*
   * **El cuadrito de la landing no termina, así que no se apaga.**
   *
   * Todo lo de arriba da por hecho que la escena acaba yéndose a WhatsApp: por eso el estado
   * final es la captura en blanco y negro con el botón violeta, que es el traspaso ya
   * consumado. En `NoLoginCard` no se va nadie —es un bloque que se repite y no navega por su
   * cuenta—, así que ese mismo cuadro contaría algo que no pasa: una pantalla apagada por una
   * transición que nunca ocurrió.
   *
   * Entonces acá se deshace, y sólo acá. El resto del apagado sí vale igual y no se toca: sin
   * puntero, sin latido y sin "escribiendo…", o sea el dibujo quieto en el instante en que el
   * bot ya contestó.
   *
   * (Con movimiento no hace falta ninguna de estas reglas: la escena recibe `endMs =
   * NO_HANDOFF_MS`, un retardo que no llega nunca, y el `both` de las dos animaciones las deja
   * fijadas en su cuadro inicial. Esto es sólo para el camino sin movimiento, donde las reglas
   * de arriba saltean el retardo y fijan el final a mano.)
   */
  .at-wa-loop .at-wa-gray,
  .at-wa-loop .at-wa-reply {
    filter: none;
  }
  .at-wa-loop .at-wa-send {
    background: #008069;
  }
  .at-wa-loop .at-wa-stage {
    background: #efeae2;
  }
  /*
   * El cuadro quieto del loop es **después** de enviar —el mensaje arriba y la respuesta
   * puesta—, así que el cajón tiene que estar vacío, con el `placeholder`. Sin esto los dos
   * textos quedarían encimados en la misma celda, ilegibles los dos.
   */
  .at-wa-sent-hide {
    animation: none;
    opacity: 0;
  }
  .at-wa-sent-show {
    animation: none;
    opacity: 1;
  }

  /*
   * Telegram, same still frame: the keyboard already up, the screenshot grey and the button in
   * our accent. And the same exception for the looping card, which never hands anything over.
   */
  .at-tg-tap::after {
    display: none;
  }
  .at-tg-start-message {
    animation: none;
    opacity: 1;
    transform: none;
  }
  .at-tg-start-bar {
    display: none;
  }
  .at-tg-kbd,
  .at-tg-btn {
    animation: none;
  }
  .at-tg-btn {
    background: var(--accent);
    color: var(--accent-ink);
  }
  .at-tg-stage {
    animation: none;
    background: #e8e8e8;
  }
  .at-wa-loop .at-tg-btn {
    background: #2aabee;
    color: #fff;
  }
  .at-wa-loop .at-tg-stage {
    background: #a8c8e8;
  }
}

/* --- El latido del botón principal (`NoLoginCard`) --- */

/*
 * **Dos golpes seguidos y después descansa**, que es lo que lo hace un latido y no un
 * parpadeo. Un pulso parejo cada tanto se lee como un cartel que titila y el ojo lo apaga en
 * cinco segundos; el ritmo de corazón —fuerte, flojo, silencio largo— se sigue notando sin
 * pedir nada. De los 3,6 s, más de dos son quietos.
 *
 * El aro sale del borde y se apaga antes de llegar lejos: es lo mismo que hace `at-hint-ping`
 * con la flecha. El `scale` acompaña apenas (2%), lo justo para que el aro no parezca pegado
 * a un botón muerto.
 *
 * **Tiene el verde del botón adentro** y ahí se separa de `at-wa-beat`, el del dibujo: aquél
 * corre debajo de dos botones de colores distintos y por eso lleva sombra negra. Este aro es
 * de un solo botón que siempre es verde, así que teñirlo es gratis y se ve mejor.
 *
 * Sólo late **cuando se puede apretar**: ver `NoLoginCard`. Un botón apagado que late estaría
 * llamando a hacer algo que no deja hacer.
 */
@keyframes at-cta-beat {
  0% {
    transform: scale(1);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--wa) 45%, transparent);
  }
  8% {
    transform: scale(1.02);
    box-shadow: 0 0 0 7px color-mix(in srgb, var(--wa) 20%, transparent);
  }
  17% {
    transform: scale(1);
    box-shadow: 0 0 0 13px color-mix(in srgb, var(--wa) 0%, transparent);
  }
  25% {
    transform: scale(1.014);
    box-shadow: 0 0 0 5px color-mix(in srgb, var(--wa) 16%, transparent);
  }
  36%,
  100% {
    transform: scale(1);
    box-shadow: 0 0 0 13px color-mix(in srgb, var(--wa) 0%, transparent);
  }
}

.at-cta-beat {
  animation: at-cta-beat 3.6s ease-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  /*
   * Sin movimiento no queda nada: el botón ya es lo más grande y lo más verde de la tarjeta,
   * o sea que el latido siempre fue el refuerzo y nunca la única señal.
   */
  .at-cta-beat {
    animation: none;
  }
}

/* --- Esqueletos de carga (`Skeleton.tsx`) y giradorcito (`Spinner.tsx`) --- */

/*
 * El brillo es un gradiente que SE DESPLAZA, no una opacidad que pulsa.
 *
 * No es un gusto: algo que aparece y desaparece se lee como una alarma —"esto está
 * fallando"— mientras que un reflejo que recorre la pieza se lee como "esto se está
 * llenando". Es la diferencia entre parecer un error y parecer una espera.
 *
 * El gris de base sale de los tokens que ya existen, sin inventar ninguno nuevo, así que si
 * mañana cambia la paleta los esqueletos la siguen solos.
 */
@keyframes at-shimmer {
  from {
    background-position: 200% 0;
  }
  to {
    background-position: -200% 0;
  }
}
/* The sweep is `--text` at 7%, which is what makes it work in both themes without a second
   value: in the dark that is a pale band over a dark block, in the light a dark band over a pale
   one. A fixed near-white would have been invisible on the light skeleton. */
.at-skeleton {
  background-color: var(--surface-2);
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    color-mix(in srgb, var(--text) 7%, transparent) 45%,
    color-mix(in srgb, var(--text) 7%, transparent) 55%,
    transparent 100%
  );
  background-size: 200% 100%;
  animation: at-shimmer 1.4s linear infinite;
}

@keyframes at-spin {
  to {
    transform: rotate(360deg);
  }
}
.at-spinner {
  display: inline-block;
  flex-shrink: 0;
  border-style: solid;
  /* Hereda el color del botón: el aro apagado y un cuarto encendido, que es lo que hace
     ver que gira. Sin `currentColor` haría falta una variante por cada fondo. */
  border-color: currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  opacity: 0.75;
  animation: at-spin 0.7s linear infinite;
  vertical-align: -0.15em;
}

/*
 * Con "reducir movimiento" **no desaparecen, sólo se quedan quietos**: el esqueleto sigue
 * ocupando el lugar del contenido (que es la mitad de para qué está) y el aro del spinner
 * sigue diciendo que algo está en curso. Esconderlos sería sacarle información a quien pidió
 * menos animación, que no es lo que pidió.
 */
@media (prefers-reduced-motion: reduce) {
  .at-skeleton {
    background-image: none;
    animation: none;
  }
  .at-spinner {
    animation: none;
  }
}

/*
 * El `<h1>` de la home, que se achica cuando el mapa termina de cargar (ver `Feed.tsx`).
 *
 * La transición corta es lo que hace que se lea como algo intencional y no como un tirón.
 * Con "reducir movimiento" el cambio de tamaño pasa de golpe, que es exactamente lo que
 * corresponde: el encabezado igual queda chico, sólo que sin el recorrido.
 */
/**
 * # Fuera de la pantalla, adentro del documento
 *
 * Para el `<h1>` de la home, que **se saca de la vista pero no del DOM**. Son dos cosas
 * distintas: en pantalla el título competía con los tres controles de la esquina y lo que se leía
 * era un choque; en el documento sigue siendo el único encabezado de nivel 1 de la página, y una
 * página sin `<h1>` es una página que un lector de pantalla no sabe nombrar.
 *
 * La receta es la de siempre y cada línea hace falta: `clip-path` lo recorta a nada, el `1px` con
 * `overflow: hidden` evita que un navegador viejo lo reflote, `white-space: nowrap` impide que
 * una palabra larga estire el layout invisible, y `position: absolute` lo saca del flujo para que
 * no ocupe ni un píxel de alto. **No se usa `display: none` ni `visibility: hidden`**: los dos lo
 * sacan también del árbol de accesibilidad, que es justo lo que queremos conservar.
 */
.at-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  -webkit-clip-path: inset(50%);
          clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/**
 * # La arena cayendo
 *
 * El reloj del chip de vida no se queda quieto: la arena baja. Es la única animación de la app
 * que no responde a un toque, y se la gana **porque el dato que muestra también se mueve** — un
 * número que baja solo, dibujado por algo que se mueve solo.
 *
 * Son dos animaciones sobre el mismo `<svg>` y hacen cosas distintas:
 *
 * - **La arena** (`.at-sand`) es un `<path>` que crece de arriba hacia abajo con `scaleY` desde
 *   el vértice del reloj. Ocho segundos, `steps(4)`: en un reloj de arena de verdad el chorro es
 *   continuo, pero a 12 px un movimiento continuo se lee como un temblor. Cuatro escalones se
 *   leen como que algo pasó.
 * - **La vuelta** (`.at-life svg`) gira el reloj entero medio giro al final del ciclo, que es lo
 *   que hace que la arena pueda volver a empezar sin que se vea un salto: cuando termina de caer,
 *   se da vuelta.
 *
 * `transform` y nada más: no toca el layout, así que el chip no se mueve ni un píxel mientras
 * esto pasa, y el navegador lo resuelve en el compositor.
 *
 * ⚠️ **Ocho segundos, y no uno.** Esto vive en la barra, en todas las pantallas, al lado de la
 * campanita. Una animación rápida y permanente en la periferia de la vista es exactamente lo que
 * la gente aprende a ignorar — o peor, a tapar con el pulgar. Lenta, se nota cuando la mirás y
 * desaparece cuando no.
 */
@keyframes at-sand-fall {
  0%,
  6% {
    transform: scaleY(0);
  }
  70%,
  100% {
    transform: scaleY(1);
  }
}
@keyframes at-sand-flip {
  0%,
  84% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(180deg);
  }
}
.at-life svg {
  animation: at-sand-flip 8s ease-in-out infinite;
  transform-origin: 50% 50%;
}
.at-sand {
  animation: at-sand-fall 8s steps(4, end) infinite;
  /* Desde el vértice del reloj y hacia abajo: es de donde cae la arena. El `transform-box` es
     obligatorio en SVG — sin él el origen se mide contra el viewBox del `<svg>` y no contra la
     caja del `<path>`, y el montoncito crece desde cualquier lado menos el correcto. */
  transform-box: fill-box;
  transform-origin: 50% 0%;
}
@media (prefers-reduced-motion: reduce) {
  .at-life svg,
  .at-sand {
    animation: none;
  }
  /* Sin movimiento, la arena se queda abajo: el estado en reposo de un reloj es el que ya cayó. */
  .at-sand {
    transform: scaleY(1);
    transform-box: fill-box;
    transform-origin: 50% 0%;
  }
}

/*
 * El botón de ubicarte, arriba del mapa, en pantallas angostas: **sólo el ícono**.
 *
 * La fila que comparte con las dos tabs es un `flex` sin `flex-wrap`, con el botón en
 * `white-space: nowrap`: cuando la suma no entra, el último se sale de la pantalla. Pasaba
 * en alemán, pero **el alemán no era el peor, era el que se vio**: contando la fila entera,
 * español y francés son más largos, y en español el desborde caía justo en la tab de grupos,
 * que es la que abre por defecto.
 *
 * Se esconde la etiqueta y no se deja que la fila crezca a dos líneas: envolver empujaría el
 * mapa hacia abajo, que es un problema que esta pantalla ya tuvo con los avisos de ubicación.
 *
 * **430 px y no 600**: a 600 la fila entra de sobra y esconder el texto ahí sería regalar
 * información sin motivo. 430 cubre los teléfonos donde de verdad no entra (360–430) y deja
 * la etiqueta en todo lo demás.
 */
@media (max-width: 430px) {
  .at-locate-label {
    display: none;
  }
}

/* The map modes can grow without squeezing the locate action or overflowing the page. */
.at-map-filter-row {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 12px;
}

.at-map-filter-rail-wrap {
  position: relative;
  flex: 1 1;
  min-width: 0;
}

.at-map-filter-rail-wrap[data-more]::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 28px;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--bg));
  opacity: 0.78;
}

.at-map-filter-rail {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  padding: 6px 28px 6px 0;
}

.at-map-filter-rail::-webkit-scrollbar {
  display: none;
}

/*
 * The card of the landing's groups story, when the step changes.
 *
 * One shot, and the component remounts it with a `key` on every step — see `useLoopTick` for why
 * that beats `infinite` in general. Here there is only one piece, so it could have been
 * `infinite`; it is remounted anyway so the fade cannot outlive the state that caused it. When
 * the loop pauses off screen, no remount means no fade, and the card just stays where it is.
 *
 * The rise is 4 px and not more: the card sits right under a map full of markers, and anything
 * bigger reads as the map moving.
 */
@keyframes at-story-card {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
.at-story-card {
  animation: at-story-card 0.3s ease-out both;
}

/*
 * No motion, no fade. The card is already in its final state — name, counter and description
 * all present — so there is nothing to reveal, and animating it would be repainting to leave
 * everything exactly as it was.
 */
@media (prefers-reduced-motion: reduce) {
  .at-story-card {
    animation: none;
  }
}

/*
 * The "message this group" button of the landing story, being pressed.
 *
 * A press and not a blink: it is a full-width block, and the lateral nudge that works on a 36 px
 * round arrow (`at-hint-next`) reads as the layout shifting at this size. So it sinks — down a
 * pixel and slightly smaller — which is what a finger on a button actually looks like.
 *
 * One shot; the component remounts it when the press step comes round.
 */
@keyframes at-story-press {
  0%,
  100% {
    transform: scale(1) translateY(0);
    filter: none;
  }
  45% {
    transform: scale(0.975) translateY(1px);
    filter: brightness(1.18);
  }
}
.at-story-press {
  animation: at-story-press 0.62s ease-in-out both;
}

/*
 * No motion: the button keeps its resting look. It is already legible on its own — it says what it
 * does — and this block's meaning does not depend on seeing it pressed.
 */
@media (prefers-reduced-motion: reduce) {
  .at-story-press {
    animation: none;
  }
}

/*
 * The mouse that presses the story's "message this group".
 *
 * Reuses `at-wa-pointer`'s keyframes — the travel is written as a `translate` back to zero, so it
 * is position-agnostic and only the anchor changes. What does not carry over is the timing: that
 * family is multiplied by `--at-wa-speed`, which belongs to the WhatsApp scenes, and this one has
 * to agree with a different partner.
 *
 * **The two durations below are a pair, do not move one alone.** The pointer shrinks at 62 % of
 * 1.15 s ≈ 713 ms; the button sinks at 45 % of 0.62 s ≈ 279 ms, so it is delayed 430 ms to land at
 * ≈ 709 ms. Miss it and you get a cursor pressing nothing, followed by a button pressing itself.
 */
.at-story-pointer {
  position: absolute;
  right: 26px;
  top: 6px;
  pointer-events: none;
  animation: at-wa-pointer 1.15s ease-out both;
}
.at-story-press {
  animation: at-story-press 0.62s ease-in-out 0.43s both;
}

@media (prefers-reduced-motion: reduce) {
  .at-story-pointer {
    animation: none;
  }
  /* No pointer to see, so nothing should be waiting for it: the cursor is hidden rather than parked
     on top of the button. */
  .at-story-pointer {
    display: none;
  }
}

/*
 * The play line of a looping drawing: along the top edge, like a video's.
 *
 * `width` and not `transform: scaleX()`, which is the usual advice — with `scaleX` the growth is
 * anchored at the centre unless you also set `transform-origin`, and a 2 px line that grows from
 * the middle outwards does not read as progress. At 2 px tall and one element there is no layout
 * cost worth the trick.
 *
 * `linear` because it is a clock. Any easing would make the same lap look like it speeds up or
 * drags, which is the one thing a progress line must not do.
 *
 * The duration and the play state come from the component: each drawing's lap is a different
 * length, and it pauses when another block takes the turn.
 */
@keyframes at-story-line {
  from {
    width: 0%;
  }
  to {
    width: 100%;
  }
}
.at-story-line {
  position: absolute;
  top: 0;
  left: 0;
  height: 2px;
  background: var(--accent);
  border-radius: 0 2px 2px 0;
  pointer-events: none;
  z-index: 3;
  animation-name: at-story-line;
  animation-timing-function: linear;
  animation-iteration-count: 1;
  animation-fill-mode: both;
}

/*
 * No motion: no line. It would be a bar sitting at 0 % forever, which reads as something that
 * failed to start — and with this preference nothing IS running, so there is no progress to report.
 */
@media (prefers-reduced-motion: reduce) {
  .at-story-line {
    display: none;
  }
}

/* ---------- La portada del grupo: girá el teléfono, y queda 4:3 ---------- */

/*
 * El dibujo que reemplaza a la instrucción escrita (`StoryHowTo`). Enseña las dos cosas que
 * hay que hacer ANTES de abrir la cámara —girar el teléfono, y que la portada es apaisada—
 * en el único momento en que podemos decirlas: `<input capture>` le pasa la posta a la app
 * del sistema y no volvemos a tener control hasta que el archivo vuelve.
 *
 * El escenario tiene alto fijo porque el teléfono gira adentro: sin esto la tarjeta del modal
 * cambia de alto en cada vuelta y salta todo lo de abajo.
 */
.at-story-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 128px;
  margin-top: 10px;
  border-radius: 12px;
  background: var(--surface-2);
}

.at-story-phone {
  position: relative;
  width: 62px;
  height: 112px;
  border: 2px solid var(--fg);
  border-radius: 12px;
  background: var(--surface);
  animation: at-story-turn 3.4s ease-in-out infinite;
}

/* La cámara, para que se lea como un teléfono y no como una tarjeta. */
.at-story-lens {
  position: absolute;
  top: 6px;
  left: 50%;
  width: 6px;
  height: 6px;
  margin-left: -3px;
  border-radius: 50%;
  background: var(--muted);
}

/*
 * El marco 4:3, **medido al revés a propósito**: acá está en las coordenadas del teléfono, que
 * está girado −90°, así que lo que en pantalla se ve como ANCHO es este `height`. De ahí que
 * sea más alto que ancho (50 × 67 ≈ 3:4 local) para aparecer 67 × 50 ≈ 4:3 apaisado.
 *
 * Aparece recién cuando el teléfono terminó de girar: mostrarlo desde el principio contaría
 * las dos cosas a la vez y no se leería ninguna.
 */
.at-story-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 50px;
  height: 67px;
  margin: -33.5px 0 0 -25px;
  border: 2px dashed var(--accent);
  border-radius: 4px;
  opacity: 0;
  animation: at-story-frame 3.4s ease-in-out infinite;
}

@keyframes at-story-turn {
  0%,
  22% {
    transform: rotate(0deg);
  }
  40%,
  90% {
    transform: rotate(-90deg);
  }
  100% {
    transform: rotate(0deg);
  }
}

@keyframes at-story-frame {
  0%,
  42% {
    opacity: 0;
  }
  55%,
  88% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/*
 * Con el movimiento reducido, el teléfono se queda **acostado y con el marco puesto** — o sea
 * en el estado que el dibujo existe para enseñar. Congelarlo en vertical dejaría a esa persona
 * mirando exactamente lo contrario de lo que hay que hacer.
 */
@media (prefers-reduced-motion: reduce) {
  .at-story-phone {
    animation: none;
    transform: rotate(-90deg);
  }
  .at-story-frame {
    animation: none;
    opacity: 1;
  }
}

/* --- El strip de staging (`StagingStrip.tsx`) --- */

/*
 * Nunca se dibuja en producción: el componente devuelve `null` antes de llegar acá. Estos
 * estilos igual viven en la hoja compartida porque el alto de la barra de abajo depende de
 * un media query, y desde un `style` inline eso no se puede escribir.
 *
 * El `z-index` es el punto delicado: arriba de la barra (2000) y de las hojas (4000), pero
 * **debajo de los modales (5000)**. Un cartel de depuración no puede taparle un botón a un
 * diálogo que está esperando una respuesta.
 */
.at-staging-strip {
  position: fixed;
  left: 8px;
  bottom: calc(8px + env(safe-area-inset-bottom));
  z-index: 4500;
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: calc(100vw - 16px);
  padding: 3px 7px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  -webkit-backdrop-filter: blur(2px);
          backdrop-filter: blur(2px);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  line-height: 1.4;
  color: var(--muted);
  opacity: 0.85;
}
.at-staging-strip:hover {
  opacity: 1;
}
.at-staging-fields {
  display: flex;
  align-items: center;
  gap: 8px;
  overflow: hidden;
  white-space: nowrap;
}
.at-staging-strip[data-collapsed="1"] .at-staging-fields {
  display: none;
}
/* Set from the client only, and never restored: it comes back on the next load. */
.at-staging-strip[data-hidden="1"] {
  display: none;
}
.at-staging-controls {
  display: flex;
  align-items: center;
  gap: 6px;
}
.at-staging-controls button {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  color: var(--accent);
  cursor: pointer;
}
.at-staging-warn {
  color: var(--danger);
}
.at-staging-dim {
  opacity: 0.6;
}

@media (max-width: 600px) {
  /* Arriba de la barra de pestañas (~58 px), que en este ancho sí existe. */
  .at-staging-strip {
    bottom: calc(62px + env(safe-area-inset-bottom));
  }
}

/* --- The refresh countdowns (`AutoRefresh.tsx`) --- */

/*
 * Same story as the strip above and the same guard: nothing reaches this in production. The
 * stack is on the opposite corner so the two debug instruments never overlap, and it grows
 * upwards because several loops can be alive at once and the newest one is the interesting one.
 */
#at-refresh-pills {
  position: fixed;
  right: 8px;
  bottom: calc(8px + env(safe-area-inset-bottom));
  z-index: 4500;
  display: flex;
  flex-direction: column-reverse;
  align-items: flex-end;
  gap: 6px;
  pointer-events: none; /* the gaps stay clickable for whatever is underneath */
}
.at-refresh-pill {
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  -webkit-backdrop-filter: blur(2px);
          backdrop-filter: blur(2px);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  line-height: 1.4;
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  cursor: pointer;
  opacity: 0.85;
}
.at-refresh-pill:hover {
  opacity: 1;
}
/* The pill is a box with two buttons in it, so the `×` is not a button inside a button. */
.at-refresh-body,
.at-refresh-close {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.at-refresh-body {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.at-refresh-close {
  padding: 0 2px;
  opacity: 0.55;
}
.at-refresh-close:hover {
  opacity: 1;
}
.at-refresh-pill[data-paused="1"] {
  opacity: 0.45;
}
.at-refresh-mark {
  display: inline-block;
  transition: transform 0.5s ease;
}
.at-refresh-mark[data-spinning="1"] {
  transform: rotate(360deg);
}

@media (max-width: 600px) {
  #at-refresh-pills {
    bottom: calc(62px + env(safe-area-inset-bottom));
  }
}

/* La entrada de cada foto del carrusel del grupo. Ver `StoryCarousel.fade`. */
@keyframes at-story-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Quien pidió que no se mueva nada ve el cambio seco, que acá no pierde información: lo
     que dice cuál estás mirando es el contador, no el fundido. */
  @keyframes at-story-in {
    from {
      opacity: 1;
    }
  }
}

/* --- The typing dots in a room (`ChatSheet.typingBubble`) --- */

/*
 * The one animation in the app that nobody triggered — and it earns it the same way the
 * hourglass sand does: what it draws is someone moving right now, on the other side.
 */
.at-typing-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--muted);
  opacity: 0.4;
  animation: at-typing-dot 1.2s ease-in-out infinite;
}
.at-typing-dot:nth-child(2) {
  animation-delay: 0.16s;
}
.at-typing-dot:nth-child(3) {
  animation-delay: 0.32s;
}

/* The wave has to end before it starts again, hence the flat stretch from 60 % on: without
   it the three dots read as one blinking blob instead of a left-to-right ripple. */
@keyframes at-typing-dot {
  0%,
  60%,
  100% {
    opacity: 0.4;
    transform: translateY(0);
  }
  30% {
    opacity: 1;
    transform: translateY(-2px);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Still but visible, unlike the drawn scene's "escribiendo…" further up: there the row is a
     step of an animation, here it is a state that exists on its own — somebody is typing. */
  .at-typing-dot {
    animation: none;
    opacity: 0.6;
  }
}

/* La barrita que se llena mientras dura una foto del carrusel. Ver `StoryCarousel.barFill`.
   No lleva variante para `prefers-reduced-motion` porque ahí no llega a correr: sin avance
   automático la barra de la foto actual se dibuja llena y quieta. */
@keyframes at-story-bar {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

/* One live region owns every transient confirmation, above the highest sheet tier (5000+). */
.at-toast-region {
  position: fixed;
  z-index: 6000;
  top: max(16px, env(safe-area-inset-top));
  left: 50%;
  width: min(calc(100% - 32px), 460px);
  translate: -50% 0;
  display: grid;
  grid-gap: 8px;
  gap: 8px;
  pointer-events: none;
}

.at-toast {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 48px;
  padding: 10px 38px 10px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 8px 28px color-mix(in srgb, var(--text) 18%, transparent);
  pointer-events: auto;
  animation: at-toast-in 180ms ease-out;
}

.at-toast-success {
  border-color: color-mix(in srgb, var(--ok) 42%, var(--border));
  background: color-mix(in srgb, var(--ok) 10%, var(--surface));
}
.at-toast-error {
  border-color: color-mix(in srgb, var(--danger) 46%, var(--border));
  background: color-mix(in srgb, var(--danger) 10%, var(--surface));
}
.at-toast-info {
  border-color: color-mix(in srgb, var(--accent) 38%, var(--border));
  background: color-mix(in srgb, var(--accent) 9%, var(--surface));
}
.at-toast-mark {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  flex: 0 0 22px;
  border-radius: 50%;
  font-size: 13px;
  font-weight: 800;
}
.at-toast-success .at-toast-mark {
  background: color-mix(in srgb, var(--ok) 18%, transparent);
  color: var(--ok);
}
.at-toast-error .at-toast-mark {
  background: color-mix(in srgb, var(--danger) 18%, transparent);
  color: var(--danger);
}
.at-toast-info .at-toast-mark {
  background: var(--accent-weak);
  color: var(--accent);
}
.at-toast-message {
  flex: 1 1;
  font-size: 14px;
  line-height: 1.35;
}
.at-toast-action {
  border: 0;
  padding: 4px;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
}
.at-toast-close {
  position: absolute;
  right: 6px;
  width: 30px;
  height: 30px;
  border: 0;
  padding: 0;
  background: none;
  color: var(--muted);
  font-size: 21px;
  cursor: pointer;
}

@keyframes at-toast-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .at-toast {
    animation: none;
  }
}

