/* Cultum web — the Coming Soon page
   ---------------------------------------------------------------------------
   One screen, one job: say what Cultum is and take an email address.

   The shape of the page: the lockup sits at the top on its own, and the
   sentence, the field and the promise are one block centred in what is left,
   lit by two soft blooms of brand green pooling at the foot of the screen.

   Three things are worth knowing before reading on:

   1. The intro is not a separate screen. The sheet that covers the page is the
      page's own colour, and the lockup that draws itself in is the real lockup
      from the top of the page, travelling to its place rather than being
      swapped for a copy. So the page never flashes and never jumps.
   2. Nothing is hidden unless script is present to unhide it. Every initial
      hidden state is written under `.js`, which only exists because a script in
      the head put it there.
   3. Sizes are fluid, not stepped. The clamps in tokens.css hit their Figma
      values at desktop width and walk down from there; the media queries below
      are only for the one thing a clamp cannot do — splitting the field into
      two controls on a phone. */

/* ============ reset ============ */
*, *::before, *::after { box-sizing: border-box; }
html { height: 100%; }
body, h1, p, figure { margin: 0; }
img, svg { display: block; max-width: 100%; }
button, input { font: inherit; color: inherit; }

/* The horizontal clip lives on html only: html's overflow propagates to the
   viewport, so this is the whole clip. Repeating it on body would quietly make
   body the page's scroll container — the document would then never scroll,
   window.scrollY would stay 0, and every window scroll listener would go dead. */
html { overflow-x: hidden; -webkit-text-size-adjust: 100%; }

/* The page is one vertical stack — the lockup, the block, the small print —
   and every part of it is in the flow rather than floating over it. The hero
   takes whatever height is left and centres its block in that, so on a short
   window the sentence gives ground to the lockup instead of being landed on
   by it. */
body {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  font-family: var(--font-ui);
  font-weight: var(--weight-low);
  font-size: var(--size-body);
  color: var(--ink);
  background: var(--page);
  /* Light falling from the top of the frame — the ground is one flat token in
     Figma, and a flat fill behind a photographed room reads as a sticker. */
  background-image:
    radial-gradient(120% 86% at 50% -10%, #EEFDE8 0%, rgba(238, 253, 232, 0) 62%),
    radial-gradient(90% 60% at 50% 112%, #CFF3C0 0%, rgba(207, 243, 192, 0) 70%);
  background-attachment: fixed;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* A honeypot has to be reachable by a robot reading the DOM and unreachable by
   everyone else — off-screen rather than display:none, which some bots skip. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

:where(a, button, input):focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: var(--radius-8);
}

/* ============ the lockup, at the top and on its own ============ */
.topbar {
  flex: none;
  z-index: 3;
  display: flex;
  justify-content: center;
  padding: clamp(28px, 5.2vh, 54px) var(--gutter-page) 0;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--tile-gap);
  text-decoration: none;
  color: var(--ink);
  transform-origin: 50% 50%;
}

.brand-tile {
  position: relative;
  flex: none;
  width: var(--tile); height: var(--tile);
  border-radius: var(--tile-radius);
  background: var(--green);
  color: var(--green-ink);
  overflow: hidden;
}

/* Placed as the frame places it — a hair left of centre, and sized off the
   export box (30.345 x 27.2127) rather than the stroke's bounding box, so the
   round caps are not clipped at the corners. */
.mark {
  position: absolute;
  left: 12.594%; top: 14.573%;
  width: 79.024%; height: 70.866%;
  overflow: visible;
}

.brand-word {
  font-family: var(--font-display);
  font-weight: var(--weight-high);
  font-size: var(--size-brand);
  line-height: var(--leading-snug);
  letter-spacing: -.01em;
}

/* ============ the page ============ */
/* the page's side gutter — 96 on desktop, stepped to 32 and 16 below */
:root { --gutter-page: 96px; }

.hero {
  position: relative;
  z-index: 2;
  /* the leftover, whatever the lockup and the small print did not take */
  flex: 1;
  display: grid;
  place-items: center;
  /* Equal above and below, so the block is centred in what it was given. */
  padding: clamp(48px, 8vh, 96px) var(--gutter-page);
}

.col {
  width: min(760px, 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  --col-gap: var(--space-21);   /* named so the note can pull back against it */
  gap: var(--col-gap);
}

/* ============ the sentence ============
   Ten words in a wrapping row, exactly as the frame builds it. Letting them
   wrap rather than hard-coding the three lines is what makes the headline
   adaptive for free — and gives the reveal something to stagger over. */
.headline {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-end;
  gap: var(--space-2) var(--space-12);
  /* The measure is in em, not px, so the sentence keeps the three-line shape it
     was written for at every size rather than only at 1440. 13.85em is the
     frame's 720px of text at its 52px — the Literata Google serves today sets a
     few percent tighter than the one the frame was drawn with, and a measure in
     px would have let "so" creep up onto the first line. */
  width: 100%;
  max-width: 13.85em;
  margin-inline: auto;
  font-family: var(--font-display);
  font-weight: var(--weight-high);
  font-size: var(--size-h1);
  line-height: var(--leading-tight);
}

.headline .w { display: inline-block; }

/* The line the splash used to carry, standing over the sentence as its kicker.
   Small, and in the ramp's darker green rather than in ink, so it reads as a
   label on the headline rather than as a first line of it. */
.eyebrow {
  margin: 0;
  font-size: var(--size-body);
  font-weight: var(--weight-med);
  line-height: var(--leading-normal);
  letter-spacing: .01em;
  color: var(--green-deep);
  text-align: center;
}

/* the one handwritten word, and the stroke under it */
.w--script { position: relative; display: inline-flex; align-items: flex-end; }

.script {
  position: relative;
  display: inline-block;
  font-family: var(--font-script);
  font-weight: var(--weight-high);
  font-size: var(--size-script);
  line-height: 1;
  /* Caveat's baseline sits high inside its box; this drops it onto the same
     line as the Literata beside it rather than floating above it. */
  transform: translateY(-.055em);
  padding-inline: .06em;
}

.rule {
  position: absolute;
  left: 0; right: 0;
  bottom: -.16em;
  width: 100%;
  height: .19em;
  color: var(--green);
  overflow: visible;
  transform: rotate(-1.62deg);
  transform-origin: 0 50%;
}

.comma {
  font-family: var(--font-display);
  letter-spacing: -1px;
  margin-left: -.06em;
}

/* ============ the lead ============ */
.lead {
  max-width: 598px;
  font-size: var(--size-lead);
  line-height: var(--leading-normal);
  color: var(--ink-2);
  text-align: center;
  text-wrap: pretty;
  transition: color var(--dur-fast) ease;
}

/* ============ the field ============ */
.signup {
  position: relative;
  width: min(400px, 100%);
  display: grid;
}

/* form and confirmation occupy the same cell, so the swap does not move the
   page under the note beneath it */
.wl, .done { grid-area: 1 / 1; }

.field {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  width: 100%;
  min-height: 64px;
  /* Figma draws the stroke inside the 400x64 frame, so its 8/8/8/24 padding is
     measured from the outer edge. Under border-box the border sits outside the
     padding, so the padding gives the border its pixel back — 1+7+48+7+1 = 64,
     and the button still lands 8px off every edge. */
  padding: 7px 7px 7px 23px;
  background: var(--paper);
  border: var(--divider) solid var(--hairline);
  border-radius: var(--radius-full);
  box-shadow: 0 1px 2px rgba(16, 16, 16, .04);
  transition:
    border-color var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-out);
}

/* Active is ink, not brand: the green ring competed with the green button
   sitting inside the same pill, and on the green ground it read as a glow
   rather than as focus. */
.field:focus-within {
  border-color: var(--ink);
  box-shadow: 0 0 0 4px rgba(var(--ink-rgb), .12), 0 1px 2px rgba(16, 16, 16, .04);
}

/* something in the field that could actually be posted firms the rule up */
.field.is-valid { border-color: rgba(var(--ink-rgb), .55); }

.field-input {
  flex: 1 0 0;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  font-size: 16px;                     /* below 16 and iOS zooms the page in */
  line-height: var(--leading-normal);
  color: var(--ink);
  text-overflow: ellipsis;
}
.field-input::placeholder { color: var(--ink-3); opacity: 1; }
.field-input:focus { outline: none; }

.is-invalid.field { border-color: var(--danger); }
.is-invalid.field:focus-within { box-shadow: 0 0 0 4px rgba(218, 55, 55, .22); }

.btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-8);
  height: 48px;
  padding: 0 var(--space-16);
  border: 0;
  border-radius: var(--radius-full);
  background: var(--green);
  color: var(--green-ink);
  font-family: var(--font-ui);
  font-weight: var(--weight-med);
  font-size: 16px;
  line-height: var(--leading-tight);
  white-space: nowrap;
  cursor: pointer;
  transition:
    background var(--dur-fast) ease,
    transform var(--dur-fast) var(--ease-out),
    box-shadow var(--dur) var(--ease-out);
}

.btn:hover { background: #86e46c; box-shadow: 0 4px 14px rgba(58, 120, 34, .26); }
.btn:active { transform: scale(.97); }
.btn[disabled] { cursor: default; }

/* The two labels share a cell, so the button's width is the wider of them and
   never changes. visibility, not display, on the one that is not showing: it
   has to keep taking up space to hold the width, and has to leave the
   accessibility tree so the button is not announced as both at once. */
.btn-label { display: grid; }
.btn-label > span {
  grid-area: 1 / 1;
  transition: opacity var(--dur-fast) var(--ease-out), visibility var(--dur-fast);
}
.btn-label-busy { opacity: 0; visibility: hidden; }
.is-busy .btn-label-idle { opacity: 0; visibility: hidden; }
.is-busy .btn-label-busy { opacity: 1; visibility: visible; }

.btn-ico {
  position: relative;
  flex: none;
  width: 20px; height: 20px;
  display: grid;
  place-items: center;
}
.btn-ico > svg { grid-area: 1 / 1; }
.ico-arrow {
  width: 12.9166px; height: 12.9166px;
  transition: transform var(--dur) var(--ease-out);
}
.btn:hover .ico-arrow { transform: translateX(2px); }

.ico-spin {
  width: 20px; height: 20px;
  opacity: 0;
  stroke-dasharray: 14 33;
}

/* submitting: the arrow gives way to a ring, the label says what is happening */
.is-busy .btn { pointer-events: none; }
.is-busy .ico-arrow { opacity: 0; transform: scale(.6); }
.is-busy .ico-spin { opacity: 1; animation: spin .8s linear infinite; }
.is-busy .field-input { opacity: .6; }

/* ============ the confirmation ============ */
.done {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-12);
  min-height: 64px;
  padding: var(--space-8) var(--space-20);
  background: var(--success-secondary);
  border: var(--divider) solid rgba(56, 125, 65, .34);
  border-radius: var(--radius-full);
}
.done[hidden] { display: none; }
.done:focus { outline: none; }
.done:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

.done-tick {
  flex: none;
  display: grid; place-items: center;
  width: 32px; height: 32px;
  border-radius: var(--radius-full);
  background: var(--green);
  color: var(--green-ink);
}
.done-tick svg { width: 20px; height: 20px; }

.done-text {
  font-size: var(--size-body);
  font-weight: var(--weight-med);
  line-height: var(--leading-normal);
  color: #26512B;
}

/* Someone who has already joined gets the receipt rather than the field — with
   a way back to it, because the address they used may not be the one they want
   this time. */
.done-again {
  display: none;
  margin-left: auto;
  padding: 6px 2px;                 /* keeps the target at 24px, per 2.5.8 */
  min-height: 24px;
  border: 0;
  background: none;
  font-size: var(--size-body);
  font-weight: var(--weight-med);
  color: #26512B;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  white-space: nowrap;
}
.done-again:hover { color: #17351B; }
.is-done-quiet .done-again { display: block; }

/* the confirmation arrives the way the logo does — the tick draws itself */
.is-done .done { animation: doneIn .5s var(--ease-spring) both; }
.is-done .done-tick { animation: tickPop .48s var(--ease-spring) .08s both; }
.is-done .done-tick path { animation: draw .42s var(--ease-out) .26s both; }
.is-done .wl { animation: formOut .32s var(--ease-out) both; pointer-events: none; }

.is-done-quiet .done,
.is-done-quiet .done-tick,
.is-done-quiet .done-tick path,
.is-done-quiet .wl { animation: none; }
/* no animation means nothing will ever draw the stroke back in */
.js .is-done-quiet .done-tick path { stroke-dasharray: none; stroke-dashoffset: 0; }

/* ============ the promise, and anything that went wrong ============ */
/* A footnote to the field, not the next paragraph down: it is set smaller than
   the page's body size and pulled back against the column gap so it sits under
   the control it is talking about. When it turns into an error, being close is
   the whole point — a complaint 21px adrift reads as unrelated small print.

   The gap it settles at is the line's own size: one text-height below the
   button is near enough to belong to it and far enough not to touch it. */
.eyebrow { margin-bottom: calc(var(--space-4) - var(--col-gap)); }

.note {
  max-width: 34em;
  margin-top: calc(var(--size-note) - var(--col-gap));
  font-size: var(--size-note);
  line-height: var(--leading-normal);
  color: var(--ink-2);
  text-align: center;
  text-wrap: pretty;
}

/* the same line, saying something that went wrong */
.note--bad { color: var(--danger); }

/* ============ the small print ============ */
.footer {
  flex: none;
  z-index: 3;
  padding: 0 var(--gutter-page) clamp(20px, 3.4vh, 36px);
  font-size: var(--size-note);
  line-height: var(--leading-normal);
  color: var(--ink-3);
  text-align: center;
}

/* ============ the light ============ */
.scene {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

/* Two blooms of brand green pooling at the foot of the screen. The ground is
   one flat token in Figma; light falling into it is what keeps the page from
   reading as a swatch behind the sentence. */
.glow {
  position: absolute;
  width: 46vw; height: 46vw;
  max-width: 660px; max-height: 660px;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, rgba(var(--green-rgb), .55), rgba(var(--green-rgb), 0) 68%);
  filter: blur(60px);
  /* a token, so the bloom keyframes lift whatever this breakpoint set rather
     than snapping to a hard-coded desktop value */
  --glow-o: .5;
  opacity: var(--glow-o);
}
.glow--l { left: -15vw; bottom: -14vw; }
.glow--r { right: -15vw; bottom: -14vw; }

/* ============ the intro ============ */
/* The sheet exists only where there is script to take it away again. Without
   that guard a browser with JS off gets a full screen of green and nothing
   else — the one failure mode a splash must never have. */
.intro {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
}
.js .intro { display: block; }
.is-gone .intro { display: none; }

.intro-sheet {
  position: absolute;
  inset: 0;
  background: var(--page);
  background-image: radial-gradient(120% 86% at 50% -10%, #EEFDE8 0%, rgba(238, 253, 232, 0) 62%);
}
.is-leaving .intro-sheet { animation: sheetOut .62s var(--ease-out) both; }

/* One mark on an empty sheet, and nothing to read while it draws. It is its
   own artwork, not the header lockup borrowed — the header's arrives later with
   everything else. Centred outright rather than optically: with the wordmark
   and the tagline gone there is no column below it to balance against. */
.intro-lockup {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* The tile is a span, and width and height mean nothing on an inline box.
     It used to be a flex item and got its size that way; the flex stays for
     exactly that reason now that it is the only thing left in here. */
  display: flex;
}

.intro-tile {
  position: relative;
  width: 96px; height: 96px;
  border-radius: 24px;
  background: var(--green);
  color: var(--green-ink);
  overflow: hidden;
}

.is-leaving .intro-lockup { animation: lockupOut .5s var(--ease-out) both; }

/* ============ what waits for the script ============
   Only the intro's own mark is stroke-drawn; the header's arrives whole, as a
   block in the reveal like everything else. */
.js .rise,
.js .headline .w { opacity: 0; visibility: hidden; }
.js .topbar .brand { opacity: 0; visibility: hidden; }
.js .intro-mark path,
.js .rule path,
.js .done-tick path { stroke-dasharray: var(--len); stroke-dashoffset: var(--len); }

/* --- the splash's own beats, timed as the app times them --- */
.is-drawing .intro-tile { animation: tilePop .58s var(--ease-spring) both; }
.is-drawing .mark-frame { animation: draw .78s var(--ease-out) .12s both; }
.is-drawing .mark-leaf  { animation: draw .42s var(--ease-out) .70s both; }
.is-drawing .mark-check { animation: draw .46s var(--ease-out) .95s both; }

/* --- and the page's, as the sheet lifts --- */
.is-ready .topbar .brand { animation: rise .7s var(--ease-out) both; }
.is-ready .headline .w { animation: rise .8s var(--ease-out) calc(80ms + var(--i) * 42ms) both; }
.is-ready .rule path { animation: draw .5s var(--ease-out) 640ms both; }
.is-ready .rise { animation: rise .78s var(--ease-out) var(--d, 0ms) both; }
/* ============ motion ============ */
@keyframes draw { from { stroke-dashoffset: var(--len, 200px); } to { stroke-dashoffset: 0; } }
@keyframes tilePop {
  from { opacity: 0; transform: scale(.6); }
  to   { opacity: 1; transform: none; }
}
/* `visibility` interpolates to visible at any progress above zero, so with
   fill-mode both each block stays out of the tab order through its whole delay
   — invisible controls were reachable by Tab for about three seconds without
   this — and rejoins it exactly as its fade begins. */
@keyframes rise {
  from { opacity: 0; transform: translate3d(0, 16px, 0); }
  to   { opacity: 1; transform: none; visibility: visible; }
}
/* how the splash leaves: the whole lockup lifts and fades as the sheet goes */
@keyframes lockupOut {
  to { opacity: 0; transform: translate(-50%, calc(-50% - 18px)); }
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes sheetOut { to { opacity: 0; } }
@keyframes formOut { to { opacity: 0; transform: scale(.96); } }
@keyframes doneIn {
  from { opacity: 0; transform: scale(.94); }
  to   { opacity: 1; transform: none; }
}
@keyframes tickPop {
  from { transform: scale(.4); }
  to   { transform: none; }
}
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20%      { transform: translateX(-6px); }
  40%      { transform: translateX(5px); }
  60%      { transform: translateX(-3px); }
  80%      { transform: translateX(2px); }
}
.field.is-shaking { animation: shake .38s var(--ease-out); }

/* The room brightens for a moment when someone joins. It rides on the two glows
   because nothing else animates them. */
.is-celebrating .glow { animation: bloom 1.7s var(--ease-out); }
@keyframes bloom {
  0%   { transform: scale(1);    opacity: var(--glow-o, .5); }
  32%  { transform: scale(1.16); opacity: calc(var(--glow-o, .5) * 1.56); }
  100% { transform: scale(1);    opacity: var(--glow-o, .5); }
}

/* ============ narrower ============ */

/* the tablet gutter */
@media (max-width: 1024px) {
  :root { --gutter-page: 32px; }
}

/* The block has the width to itself here, so it is given more room to breathe
   and the light is pulled in behind it. */
@media (max-width: 900px) {
  .hero { padding-block: clamp(40px, 7vh, 80px); }
  .glow { width: 70vw; height: 70vw; filter: blur(46px); --glow-o: .42; }
}

/* On a phone the pill becomes two controls, one under the other: at this width
   "Join waitlist" and a readable email field cannot share a line without one of
   them being cramped, and a full-width button is the better thing to reach for
   with a thumb anyway. */
@media (max-width: 640px) {
  :root { --gutter-page: 16px; }

  /* symmetric, so the block sits in the middle of the screen and the lockup
     and the small print float over the ends rather than pushing it down */
  .hero { padding-block: clamp(32px, 6vh, 64px); }
  .topbar { padding-top: clamp(24px, 4vh, 40px); }

  .col { --col-gap: var(--space-20); }
  .headline { gap: 0 10px; }

  .field {
    display: block;
    min-height: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }
  .field:focus-within { box-shadow: none; }

  .field-input {
    width: 100%;
    min-height: 60px;
    padding: 0 22px;
    background: var(--paper);
    border: var(--divider) solid var(--hairline);
    border-radius: var(--radius-full);
    box-shadow: 0 1px 2px rgba(16, 16, 16, .04);
    transition:
      border-color var(--dur) var(--ease-out),
      box-shadow var(--dur) var(--ease-out);
  }
  .field-input:focus {
    border-color: var(--ink);
    box-shadow: 0 0 0 4px rgba(var(--ink-rgb), .12);
  }
  .field.is-valid .field-input { border-color: rgba(var(--ink-rgb), .55); }
  .field.is-invalid .field-input { border-color: var(--danger); }

  /* On a phone the wrapper is only a stack — it has no edge of its own, and the
     button has stepped out of the pill to sit below the field. So everything
     that says "this is wrong" is drawn on the input and nowhere else: the ring
     the pill wears on a wide screen would box the button in red here too, and
     the button is not the thing that is wrong. Same for the shake — the field
     is what jumps, not the control underneath it. */
  .field.is-invalid:focus-within { box-shadow: none; }
  .field.is-invalid .field-input:focus {
    border-color: var(--danger);
    box-shadow: 0 0 0 4px rgba(218, 55, 55, .22);
  }
  .field.is-shaking { animation: none; }
  .field.is-shaking .field-input { animation: shake .38s var(--ease-out); }

  .btn {
    width: 100%;
    height: 60px;
    margin-top: var(--space-12);
  }

  /* The pill squares off and lets its parts wrap, but the receipt stays centred
     — it is one line about one thing, and the tick belongs beside the words
     rather than in a margin of its own. When "Use another email" wraps onto a
     second line it centres under them for the same reason. */
  .done { border-radius: var(--radius-24); flex-wrap: wrap; padding: var(--space-16); }
  .done-again { margin-inline: auto; }
}

/* A phone on its side has almost no height to give — every band of it goes to
   the sentence. */
@media (max-height: 540px) and (orientation: landscape) {
  .hero { padding-block: 24px; }
  .topbar { padding-top: 18px; }
}

/* ============ when motion is not wanted ============
   The choreography goes; the content still arrives, and the intro still hands
   over — it just does it in a single quiet fade. */
@media (prefers-reduced-motion: reduce) {
  .js .rise, .js .headline .w,
  .js .topbar .brand { opacity: 0; }
  .js .intro-mark path, .js .rule path, .js .done-tick path { stroke-dasharray: none; stroke-dashoffset: 0; }

  .is-drawing .intro-tile,
  .is-drawing .mark-frame, .is-drawing .mark-leaf, .is-drawing .mark-check { animation: none; }
  .is-leaving .intro-lockup { animation: fade .4s ease reverse both; }

  .is-ready .topbar .brand { animation: fade .5s ease both; }
  .is-ready .headline .w { animation: fade .5s ease calc(var(--i) * 12ms) both; }
  .is-ready .rule path { animation: none; }
  .is-ready .rise { animation: fade .5s ease calc(var(--d, 0ms) / 3) both; }

  .is-done .done, .is-done .done-tick, .is-done .done-tick path { animation: fade .3s ease both; }
  .is-done .wl { animation: fade .2s ease reverse both; }
  .field.is-shaking, .is-celebrating .glow { animation: none; }
  .is-busy .ico-spin { animation: none; opacity: .55; }
  .btn:active { transform: none; }

  @keyframes fade { from { opacity: 0; } to { opacity: 1; visibility: visible; } }
}
