/* Motion: the reveals, the pointer highlight, and the `prefers-reduced-motion`
 * opt out that turns all of it off.
 *
 * Split out of app.css on 2026-08-01, and then trimmed the same day: the form
 * and state rules under this file's own banner were never motion and are now
 * forms.css. Linked after app and pages so an animation can win on equal
 * specificity against the layout it animates. */
/* motion -------------------------------------------------------------------- */

.rise { opacity: 0; transform: translateY(16px); animation: rise 660ms cubic-bezier(.16,.84,.44,1) forwards; }
@keyframes rise { to { opacity: 1; transform: none; } }

.steps-4 { grid-template-columns: repeat(4, 1fr); }

@media (max-width: 1100px) {
  .steps-4 { grid-template-columns: repeat(2, 1fr); row-gap: var(--step-10); }
  .steps-4::before { display: none; }
}

@media (max-width: 980px) {
  .bento, .steps, .steps-4, .stack, .split, .split-even { grid-template-columns: 1fr; }
  .tile, .tile-lead { grid-column: 1; grid-row: auto; }
  .tile-lead h3 { font-size: 22px; }
  .tile-lead p { font-size: 15px; }
  .tile-lead .meta .mono { font-size: 24px; }
  .stack { gap: var(--step-10); }
  .stack-head { position: static; }
  .steps, .steps-4 { row-gap: var(--step-8); }
  /* `.steps-4` is hidden at 1100 because at two columns the horizontal spine
     would run between the wrong pair. At one column it is a vertical line again
     and wants to come back, and until now it did not: the home page's three
     step rail got its spine on a phone while /rent's and /gifting's four step
     rails - the same component, the same width - were three loose circles. */
  .steps::before, .steps-4::before {
    display: block;
    left: 17px; right: auto; top: 17px; bottom: 17px; width: 1px; height: auto;
    background: linear-gradient(180deg, var(--border-str), rgb(var(--accent-rgb) / .5)); }
  .step { padding-right: 0; }
  .sticky { position: static; }
  .section-head, .page-head .wrap, .hero .wrap { grid-template-columns: 1fr; row-gap: var(--step-4); }
  .hero .wrap { gap: var(--step-10); }
  /* Flush with the headline once the hero is one column. Centring is what fills
     the empty right of the art cell on a wide screen; stacked there is no such
     cell, so the same rule reads as an arbitrary indent beside copy that starts
     at the wrap edge. */
  .rail { justify-content: start; }
  .hero h1 { max-width: 18ch; }
  .section-head h2, .section-head p, .page-head h1, .page-head .wrap > p:not(.notice) { grid-column: 1; }
  .section-head p, .page-head .wrap > p:not(.notice) { grid-row: 3; padding-bottom: 0; }
  /* One column, so the lede takes row three and the date follows it. */
  .page-head .wrap > p.doc-date { grid-row: 4; margin-top: var(--step-2); }
  .tile-wide { flex-direction: column; align-items: flex-start; }
  .hero { padding-top: var(--step-16); }
  /* Less air above the fold on a phone: the buy card is the point of the page,
     and eighty pixels of padding was pushing it under the fold on a small one. */
  .page-head { padding-top: var(--step-10); }
}

@media (max-width: 640px) {
  .stack-item { grid-template-columns: 1fr; gap: var(--step-2); }
  .stack-key { padding-top: 0; }
  /* auto-fit already drops these to one column below its own minmax floor, but
     stated outright so the intent survives somebody widening the floor later,
     and so two comparison cards never sit side by side at 500px. */
  .choice, .mins { grid-template-columns: 1fr; }
  .tile-wide { padding: var(--step-6); }
  /* `.panel` is a box and sheds padding here. `.band` is NOT a box - it has no
     padding at any other width, and on the home page it IS the `.wrap`, so this
     inset the closing "Start with a top up" section 24px on both sides while
     every section above it started flush at the wrap edge: a jog in the left
     margin at the foot of the front page. `.quote` is a box and was missing:
     it kept 32px at 320px, the narrowest and most important container on the
     site, in the one place a field has no width to spare. */
  .panel, .quote { padding: var(--step-6); }
}

/* ---- arrival, and the light on the glass ------------------------------- */

/* Panels arrive as you reach them, on the hero's own curve so the page moves
   one way rather than two. Script adds this class, and only to something still
   below the fold: written into the markup it would hide the whole page behind a
   request that might never arrive, and fading in what is already on screen
   reads as a flicker rather than an entrance. */
.will-rise {
  opacity: 0; transform: translateY(18px);
  transition: opacity 620ms ease-out, transform 620ms cubic-bezier(.16,.84,.44,1);
}
.will-rise.is-in { opacity: 1; transform: none; }

/* The one indulgence: a highlight that tracks the pointer, so the glass reads
   as a surface with a light above it instead of a rectangle with a blur on it.
   Absent entirely where there is no pointer to track, since on a phone it is
   just a bright patch stuck in one corner. */
@media (hover: hover) {
  /* The light itself, behind everything that carries text. The card highlight
     below only ever fired while the pointer was inside a card, so three
     quarters of the page did not answer the mouse at all; this is the layer
     that makes moving it feel like moving a light. Two stops from one point: a
     wide 660px wash that carries the colour, and a tight 260px core that gives
     it a centre to be moving. One stop alone reads either as a torch beam or as
     a vignette that happens to drift. It starts centred, so a page that is
     never pointed at still looks lit. */
  /* Moved with a transform rather than by animating the gradient's own centre.
     Both look identical; only one is cheap. A full viewport gradient whose
     position changes every frame has to be re-rastered every frame, whereas a
     fixed size layer that only ever gets translated is handed to the
     compositor and costs nothing to move. The element is 1400 square with its
     own margin pulling its centre onto the origin, so translating by the
     pointer puts the centre of the light exactly under the pointer. */
  /* On `html`, not on `body`. Both of body's pseudo-elements are already
     spoken for by app.css - the document-length gradient and the fixed top
     wash - and an element has only one `::before`. Declaring this one there
     did not layer over the gradient, it REPLACED it: from whenever the lamp
     was written until 2026-08-02 the site's signature background simply did
     not render on any device with a pointer, and nothing could report that.
     Behind the text at -1 for the second half of the same bug: at z-index 0 a
     positioned layer paints after the in-flow text, so the light was tinting
     the words rather than the page, and 15px grey on the swap hue measured
     3.80:1 against the 4.5:1 it needs. */
  html::before {
    content: ""; position: fixed; top: 0; left: 0; z-index: -1; pointer-events: none;
    width: 1400px; height: 1400px; margin: -700px 0 0 -700px;
    transform: translate3d(var(--px, 50vw), var(--py, 34vh), 0);
    background:
      radial-gradient(260px circle at 700px 700px, rgb(var(--accent-rgb) / .06), transparent 68%),
      radial-gradient(660px circle at 700px 700px, rgb(var(--accent-rgb) / .05), transparent 72%);
  }
  .tile::after, .panel::after {
    content: ""; position: absolute; inset: 0; pointer-events: none; z-index: -1;
    border-radius: inherit; opacity: 0; transition: opacity 260ms ease-out;
    background: radial-gradient(340px circle at var(--mx, 50%) var(--my, 0%),
                rgb(var(--accent-rgb) / .16), transparent 68%);
  }
  .tile:hover::after, .panel:hover::after { opacity: 1; }
  /* The edge answers too. Without it the sheen floats inside a border that
     stayed the same grey, and the card reads as lit rather than as glass. */
  /* `isolation` is load bearing, not tidying: it is what keeps the sheen's
     `z-index: -1` inside the card. Without a stacking context here the
     sheen escapes to the root one and paints behind the card instead of
     behind its words, which is the effect disappearing rather than moving. */
  .tile, .panel { transition: border-color 260ms ease-out; isolation: isolate; }
  .tile:hover, .panel:hover { border-color: rgb(var(--accent-rgb) / .28); }
  /* The icon plate is the one place the service hue is allowed to be solid. */
  .tile:hover .tile-icon, .panel:hover .p-ic {
    border-color: rgb(var(--accent-rgb) / .45);
    box-shadow: 0 0 0 1px rgb(var(--accent-rgb) / .12), 0 6px 18px rgb(var(--accent-rgb) / .16);
  }
  .tile-icon, .p-ic { transition: border-color 260ms ease-out, box-shadow 260ms ease-out; }
}

/* Last, so it outranks both of the above at equal specificity. `.will-rise`
   sets opacity to zero and waits for a transition, and the rule below switches
   transitions off, so leaving it out would hide half the page permanently for
   exactly the people who asked for less movement. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition: none !important; animation: none !important; }
  .rise, .will-rise { opacity: 1; transform: none; }
}
