/*
  The public site's stylesheet. Hand-written, no Tailwind, no build.

  DARK, AND NOT A NEW DARK. Every colour below is copied from
  `packages/shared/src/tokens.css`, `:root[data-theme="dark"]` — "Rose Orchid
  Night", the palette both apps shipped through Phase 10 and still offer as
  their dark theme. Deriving a second dark palette from the light mockup would
  put a face on the front door that the client has never seen, which is the
  same reasoning that file gives for reviving these values rather than
  inventing them.

  There is no light branch here. The apps resolve three theme states in
  JavaScript, and this page has no JavaScript at all; one look, one set of
  values, nothing to keep in sync with itself. The trade is written down: a
  visitor on a light device gets a dark page, and the app they sign in to
  opens light.

  WHY IT IS NOT THE APPS' STYLESHEET. `packages/shared/src/tokens.css` starts
  with `@import "tailwindcss"` and deletes the default palette, so it only means
  anything inside a Vite build with the Tailwind plugin. This page has no build
  worth the name and no JavaScript at all. What is shared is the *values*, and
  they are copied below with their source named so a drift is findable.

  Kept in sync with packages/shared/src/tokens.css. If a colour changes there
  and not here, the landing page and the app stop looking like one product.

  Logical properties only -- `padding-inline`, `margin-inline-start`, `border-
  inline-start`. `/ar/` is `dir="rtl"` and a physical `padding-left` here breaks
  it silently, which is the project's oldest standing rule. The two physical
  properties that survive are shapes rather than positions and are commented
  where they appear.

  NOTHING HERE MAY REQUIRE JAVASCRIPT TO BECOME VISIBLE. The scroll reveals are
  guarded by `@supports`, and a browser without `animation-timeline` shows every
  section in full. A reveal that fails closed is a blank website in Firefox.
*/

:root {
  /* ground & surfaces — tokens.css, :root[data-theme="dark"] */
  --ground: #17131a;
  --ground-lift: #1c1721;
  --card: #221a27;
  --card-hi: #2a2032;
  --edge: #3a2c41;
  --rule: #33283a;

  /*
    accent

    On dark, `--rose-hi` means what it originally meant: a *lighter* rose,
    because emphasis on a near-black ground is brighter rather than deeper.
    #f8b3c2 measures 9.4:1 on `--ground`.

    The contrast problem the light palette documents at #e5819f (Decisions Log
    #52) does not exist here, so the call to action is filled with `--rose`
    itself and carries `--on-rose` — a near-black plum — on top. The light
    site had to reach for #be3d66 to pass AA; this one does not.
  */
  --rose: #f2889f;
  --rose-hi: #f8b3c2;
  --orchid: #c084d8;
  --on-rose: #2a1220;

  /* text */
  --ink: #f6eef3;
  --ink-soft: #e7dae2;
  --ink-body: #bfb0c0;
  --ink-muted: #a294a5;

  /*
    Shadow. On a near-black ground a shadow is nearly free, so it goes deep and
    neutral-black rather than plum-tinted — a tinted shadow on #17131a reads as
    a smudge of colour instead of as depth. tokens.css says the same.
  */
  --shadow-float: 0 18px 40px -12px rgb(0 0 0 / 0.75);
  --shadow-soft: 0 8px 24px -14px rgb(0 0 0 / 0.8);
  --glow-rose: 0 12px 34px -10px rgb(242 136 159 / 0.45);

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

  /* Type scale, from the design file's own chrome via tokens.css. */
  --text-hero: 52px;
  --text-title: 28px;
  --text-lead: 15px;
  --text-body: 13.5px;
  --text-meta: 11.5px;

  --radius-card: 20px;
}

/*
  Two families, one stack, split by unicode-range so the browser picks Manrope
  for Latin and Cairo for Arabic per character -- including inside one mixed
  string. Manrope has no Arabic glyphs at all. Ranges copied from tokens.css.
*/
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("./fonts/manrope-latin-var.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Cairo";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("./fonts/cairo-arabic-var.woff2") format("woff2");
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0898-08E1,
    U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF,
    U+FE70-FE74, U+FE76-FEFC;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  /* Also on <html>, so an overscroll bounce past the end of the document
     shows the page's colour rather than white. */
  background: var(--ground);
  color-scheme: dark;
}

/*
  The ground stays on <body>.

  It was briefly moved to <html> so the backdrop layers below could sit at
  `z-index: -1` — a negative-z child paints behind its own parent's background,
  which is the usual way to do this. It cost a check: dev/verify-seo.mjs proves
  the stylesheet loaded at all by reading body's computed background, and a
  transparent body is indistinguishable to it from a 404 on styles.css.

  The check is right and the trick was unnecessary. The layers are `z-index: 0`
  instead and `.wrap` is `z-index: 1`, so the paint order is body's background,
  then the glow and the grain, then everything readable — with no dependence on
  how a canvas background propagates.
*/
body {
  margin: 0;
  background: var(--ground);
  position: relative;
  color: var(--ink-body);
  font-family: var(--font-sans);
  font-size: var(--text-lead);
  line-height: 1.65;
  /* Cairo sits smaller than Manrope at the same px. tokens.css corrects this
     for the apps; the same correction, applied once, here. */
  font-synthesis-weight: none;
  /*
    DELIBERATELY NO `overflow-x: hidden`.

    It was here, as insurance against a stray wide child. It is gone because it
    is not insurance, it is a blindfold: the way a sideways overflow is caught
    is `document.scrollingElement.scrollWidth`, and `overflow-x: hidden` clamps
    exactly that number. The page would have measured clean at every width
    whether it was clean or not.

    It was removed and the ten measurements retaken, and the layout is clean on
    its own -- which is the only reason it is safe to leave off. Lessons
    Learned, 2026-08-27: a tool that trusts a proxy instead of the fact is worse
    than no tool.
  */
}

/*
  The glow. Three radial pools in rose and orchid, at an alpha low enough that
  they read as light on a wall rather than as coloured shapes.

  Fixed, so it stays behind the page as it scrolls rather than scrolling off
  the top. Fixed also means it cannot widen the document however far it drifts:
  an out-of-flow element contributes nothing to scrollWidth.
*/
body::before {
  content: "";
  position: fixed;
  inset: -20%;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(38% 32% at 78% 8%, rgb(242 136 159 / 0.22), transparent 70%),
    radial-gradient(42% 38% at 12% 22%, rgb(192 132 216 / 0.16), transparent 72%),
    radial-gradient(50% 40% at 50% 96%, rgb(242 136 159 / 0.09), transparent 75%);
}

/*
  Grain. A single fractal-noise tile as a data: URI — allowed by the page's own
  `img-src 'self' data:`, and a CSS background is governed by img-src rather
  than by style-src, so this needs no loosening of the policy.

  It is here because large flat areas of near-black band badly on 8-bit panels;
  a few percent of noise breaks the banding up. At 0.04 it is invisible as
  texture and visible as the absence of stripes.
*/
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* The skip link: visible only when it has focus. A page with one nav and one
   main does not need it for navigation, but a keyboard user landing on the
   language switch should be able to leave it in one key. */
.skip {
  position: absolute;
  inset-inline-start: -9999px;
  top: 0;
  padding: 0.5rem 1rem;
  background: var(--ink);
  color: var(--ground);
  font-weight: 700;
  z-index: 40;
}

.skip:focus {
  inset-inline-start: 0;
}

.wrap {
  position: relative;
  z-index: 1;
  max-width: 68rem;
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 5vw, 2.5rem);
}

/* ---- header --------------------------------------------------------------- */

/*
  A floating pill rather than a full-width bar, because `.top` lives inside
  `.wrap` and a full-bleed background would need to escape the container's
  padding. The pill wants no such trick and reads as more deliberate.
*/
.top {
  position: sticky;
  top: 0.75rem;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-block-start: 1rem;
  padding-block: 0.5rem;
  padding-inline: 0.9rem 0.5rem;
  border: 1px solid var(--edge);
  border-radius: 999px;
  /* Opaque first. The translucent version is applied only where the blur that
     justifies it is supported -- without the blur, 72% opacity over moving
     content is just hard to read. */
  background: var(--card);
  box-shadow: var(--shadow-soft);
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .top {
    background: rgb(34 26 39 / 0.72);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
  }
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  color: var(--ink);
  font-weight: 800;
  font-size: 1rem;
  letter-spacing: -0.02em;
  text-decoration: none;
}

.brand img {
  width: 32px;
  height: 32px;
  display: block;
}

.lang {
  color: var(--ink-soft);
  font-size: var(--text-body);
  font-weight: 700;
  text-decoration: none;
  border: 1px solid var(--edge);
  border-radius: 999px;
  padding: 0.4rem 0.9rem;
  background: var(--ground-lift);
}

.lang:hover,
.lang:focus-visible {
  border-color: var(--rose);
  color: var(--rose-hi);
  background: var(--card-hi);
}

/* ---- hero ----------------------------------------------------------------- */

.hero {
  display: grid;
  gap: clamp(2.25rem, 6vw, 3.5rem);
  align-items: center;
  padding-block: clamp(2.5rem, 9vw, 5rem) clamp(2.5rem, 7vw, 4.5rem);
}

/*
  Two columns only once there is room for the drawing to be worth its space.
  `minmax(0, ...)` on both tracks rather than `1.05fr .95fr` alone: a grid
  track's default minimum is min-content, and one long unbroken word in either
  language would push the column wider than the page.
*/
@media (min-width: 56rem) {
  .hero {
    grid-template-columns: minmax(0, 1.06fr) minmax(0, 0.94fr);
  }
}

.eyebrow {
  display: inline-block;
  margin: 0 0 1.1rem;
  padding: 0.3rem 0.8rem;
  border-radius: 999px;
  font-size: var(--text-meta);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--rose-hi);
  background: rgb(242 136 159 / 0.12);
  border: 1px solid rgb(242 136 159 / 0.28);
}

.hero h1 {
  margin: 0 0 1.15rem;
  color: var(--ink);
  font-size: clamp(32px, 6.4vw, var(--text-hero));
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.08;
  max-width: 15ch;
  text-wrap: balance;
}

/*
  The headline is painted with a gradient rather than a flat colour.

  Guarded, and the guard is the point: `color: transparent` is set only inside
  the @supports block, so a browser that cannot clip a background to text keeps
  `--ink` and shows the headline. Getting that the wrong way round is an
  invisible h1, which is also the page's only h1.

  Both ends are measured against `--ground`: #ffffff is 16.4:1 and #f4b8c8 is
  10.4:1. There is no dim end.
*/
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .hero h1 {
    background: linear-gradient(128deg, #ffffff 0%, #f6eef3 46%, #f4b8c8 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

/* Arabic does not take negative tracking -- it disconnects the joins. The same
   goes for the two uppercased labels: Arabic has no case, so the transform is
   a no-op, but the tracking that comes with it is not. */
html[lang="ar"] .hero h1,
html[lang="ar"] .card h3,
html[lang="ar"] .step h3,
html[lang="ar"] section h2 {
  letter-spacing: normal;
}

html[lang="ar"] .eyebrow,
html[lang="ar"] .ph-label {
  letter-spacing: normal;
  text-transform: none;
}

/* Arabic headlines run longer per idea; 15ch is an English measure. */
html[lang="ar"] .hero h1 {
  max-width: 18ch;
  line-height: 1.25;
}

.hero .lead {
  margin: 0 0 1.9rem;
  max-width: 46ch;
  color: var(--ink-body);
  font-size: clamp(15px, 2.1vw, 17.5px);
}

/*
  The two buttons on one line, and a <span> made into a flex row.

  `flex-wrap` rather than a media query: the Arabic labels are longer than the
  English ones and the point at which two buttons stop fitting is a function of
  the words, not of a width somebody picked.
*/
.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

.cta {
  /* `position` and `overflow` are for the sheen in the motion section, which
     is a ::after sweeping across and clipped to the pill. They are here rather
     than there because a positioned parent is layout, not motion: moving them
     inside the reduced-motion query would change where the ::after lands. */
  position: relative;
  overflow: hidden;
  display: inline-block;
  background: linear-gradient(132deg, var(--rose) 0%, #ea7fb4 100%);
  color: var(--on-rose);
  font-weight: 800;
  font-size: 1rem;
  text-decoration: none;
  padding: 0.85rem 1.75rem;
  border-radius: 999px;
  box-shadow: var(--glow-rose);
}

.cta:hover,
.cta:focus-visible {
  background: linear-gradient(132deg, #f79bae 0%, #f08fbe 100%);
  box-shadow: 0 16px 40px -10px rgb(242 136 159 / 0.6);
}

/*
  The WhatsApp button, and it is deliberately NOT WhatsApp green.

  #25D366 next to `--rose` is two brands arguing on one line, on a page whose
  whole character is that every colour in it came from one place. The mark is
  what makes the button recognisable -- it is the most recognised glyph on the
  internet -- so the colour does not have to do that job as well.

  Measured: `--rose-hi` on this fill composites to 9.2:1 against `--ground`.
  dev/verify-landing.mjs holds the arithmetic.
*/
.cta-wa {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  background: rgb(242 136 159 / 0.1);
  border: 1px solid rgb(242 136 159 / 0.35);
  color: var(--rose-hi);
  box-shadow: none;
  /* The pulse ring in the motion section grows past the pill's edge, and
     `.cta` clips its children. */
  overflow: visible;
}

.cta-wa:hover,
.cta-wa:focus-visible {
  background: rgb(242 136 159 / 0.17);
  border-color: rgb(242 136 159 / 0.55);
  color: var(--ink);
  box-shadow: none;
}

.wa-mark {
  flex: none;
}

.wa-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem 1.4rem;
  margin: 1.6rem 0 0;
}

.wa-number {
  color: var(--ink-soft);
  font-size: var(--text-body);
  /* The number is read digit by digit and often copied by hand. Tabular
     figures stop the glyphs shifting under the eye as it moves along. */
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

.note {
  margin: 1rem 0 0;
  color: var(--ink-muted);
  font-size: var(--text-body);
}

/* ---- the phone ------------------------------------------------------------ */

/*
  A drawing of the app, in HTML and CSS. See the comment on `phone()` in
  render.mjs for why it is not a screenshot: every quantity in here is a bar or
  an arc precisely so that nothing on the home page can be read as a real
  client's real numbers.

  No height is set. The screen is as tall as what is in it, which is why there
  is no empty band at the bottom and nothing to clip at the top.
*/
.hero-art {
  display: flex;
  justify-content: center;
}

.phone {
  position: relative;
  width: min(20rem, 78vw);
  padding: 0.55rem;
  border-radius: 2.4rem;
  background: linear-gradient(160deg, #4a3a52 0%, #241c2a 42%, #2f2438 100%);
  box-shadow:
    var(--shadow-float),
    inset 0 1px 0 rgb(255 255 255 / 0.09);
}

.phone-screen {
  border-radius: 1.95rem;
  background: var(--ground-lift);
  padding: 1rem 0.9rem 1.2rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  overflow: hidden;
}

.ph-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-block-end: 0.15rem;
}

.ph-avatar {
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 50%;
  flex: none;
  background: linear-gradient(140deg, var(--rose), var(--orchid));
}

.ph-lines {
  display: flex;
  flex-direction: column;
  gap: 0.32rem;
  flex: 1;
  min-width: 0;
}

.ph-lines i {
  display: block;
  height: 0.42rem;
  border-radius: 999px;
  background: var(--rule);
}

.ph-lines i:first-child {
  background: var(--edge);
}

.ph-days {
  display: flex;
  gap: 0.22rem;
}

.ph-day {
  flex: 1;
  min-width: 0;
  text-align: center;
  padding-block: 0.34rem;
  border-radius: 0.55rem;
  background: var(--card);
  color: var(--ink-muted);
  font-size: 0.58rem;
  font-weight: 700;
  line-height: 1;
}

.ph-day.is-on {
  background: var(--rose);
  color: var(--on-rose);
}

.ph-label {
  margin: 0.4rem 0 0;
  font-size: 0.58rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-muted);
  line-height: 1;
}

.ph-panel {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 0.9rem;
  padding: 0.7rem 0.75rem;
}

.ph-panel.is-list {
  flex-direction: column;
  align-items: stretch;
  gap: 0.6rem;
}

/*
  A custom property registered so that it can be INTERPOLATED.

  A plain `--x: 62%` is a string as far as animation is concerned: a browser
  will jump it from one keyframe to the next rather than sweep it, which is why
  animating a gradient stop normally does not work. Registering it with a
  syntax makes it a real percentage the engine can move through.

  THE FALLBACK IS WHY `--ph-arc` IS ALSO SET IN THE RULE BELOW. In an engine
  without `@property` the registration is ignored, but `--ph-arc: 62%` is still
  an ordinary custom property, so the gradient still resolves and the arc still
  draws -- it simply never animates. Relying on the initial-value alone would
  have left `var(--ph-arc)` unresolved there, which invalidates the whole
  `background` declaration and erases the arc completely.
*/
@property --ph-arc {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 62%;
}

.ph-arc {
  position: relative;
  width: 3.1rem;
  height: 3.1rem;
  flex: none;
  border-radius: 50%;
  /* An arc, not a figure. Nothing labels it and nothing reads off it. */
  --ph-arc: 62%;
  background: conic-gradient(
    var(--rose) 0 var(--ph-arc),
    var(--edge) var(--ph-arc) 100%
  );
}

.ph-arc::after {
  content: "";
  position: absolute;
  inset: 0.52rem;
  border-radius: 50%;
  background: var(--card);
}

.ph-meters {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  flex: 1;
  min-width: 0;
}

.ph-meter {
  display: block;
  height: 0.38rem;
  border-radius: 999px;
  background: var(--ground-lift);
  overflow: hidden;
}

.ph-meter b {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--rose), var(--orchid));
}

.ph-row,
.ph-task {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.ph-thumb {
  width: 2rem;
  height: 2rem;
  flex: none;
  border-radius: 0.6rem;
  background: linear-gradient(140deg, var(--rose), var(--orchid));
  opacity: 0.8;
}

.ph-bar {
  display: block;
  height: 0.42rem;
  border-radius: 999px;
  background: var(--edge);
}

.ph-tick {
  width: 1.15rem;
  height: 1.15rem;
  flex: none;
  border-radius: 50%;
  border: 1.5px solid var(--edge);
  display: grid;
  place-items: center;
}

.ph-tick.is-done {
  background: var(--rose);
  border-color: var(--rose);
}

/*
  EVERY tick carries the mark, and the undone one keeps it at zero opacity.

  It used to be drawn only for `.is-done`, which was right until the motion
  section needed a mark to bring in on the undone task. A pseudo-element cannot
  be created by an animation, so it has to exist first. Nothing about the
  resting page changed: without motion the undone tick is an empty ring exactly
  as before.
*/
.ph-tick::after {
  content: "";
  width: 0.24rem;
  height: 0.44rem;
  /*
    The one physical property in the file, and it is a SHAPE rather than a
    position: `border-width` draws the two strokes of a tick, and a tick points
    the same way in Arabic as in English. The element is centred by `grid` /
    `place-items`, so nothing here needs to know which way the page runs.
  */
  border: solid var(--on-rose);
  border-width: 0 1.6px 1.6px 0;
  transform: translateY(-1px) rotate(45deg);
  opacity: 0;
}

.ph-tick.is-done::after {
  opacity: 1;
}

/* Widths for the drawing's bars. They exist as classes because the page's own
   Content-Security-Policy has no 'unsafe-inline' in `style-src`, so a
   `style="width:70%"` attribute would simply be dropped by the browser. */
.w-80 { width: 80%; }
.w-75 { width: 75%; }
.w-70 { width: 70%; }
.w-65 { width: 65%; }
.w-55 { width: 55%; }
.w-45 { width: 45%; }
.w-40 { width: 40%; }
.w-35 { width: 35%; }

/* ---- sections ------------------------------------------------------------- */

section {
  padding-block: clamp(2.5rem, 7vw, 4rem);
  border-block-start: 1px solid var(--rule);
}

section h2 {
  margin: 0 0 1.75rem;
  color: var(--ink);
  font-size: clamp(22px, 3.6vw, var(--text-title));
  font-weight: 800;
  letter-spacing: -0.025em;
}

section > p {
  margin: 0;
  max-width: 62ch;
}

.cards {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}

.card {
  position: relative;
  overflow: hidden;
  background: linear-gradient(180deg, var(--card-hi) 0%, var(--card) 60%);
  border: 1px solid var(--edge);
  border-radius: var(--radius-card);
  padding: 1.4rem 1.4rem 1.5rem;
}

/* The hover glow, painted behind the words. Its own layer so the card's
   background gradient stays put underneath it. */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    120% 90% at 100% 0%,
    rgb(242 136 159 / 0.18),
    transparent 62%
  );
}

.card:hover,
.card:focus-within {
  border-color: rgb(242 136 159 / 0.4);
}

.card:hover::before,
.card:focus-within::before {
  opacity: 1;
}

.card > * {
  position: relative;
}

.card-badge {
  display: inline-grid;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  margin-block-end: 1rem;
  border-radius: 0.85rem;
  color: var(--rose-hi);
  background: rgb(242 136 159 / 0.13);
  border: 1px solid rgb(242 136 159 / 0.26);
}

.card-icon {
  display: block;
}

.card h3 {
  margin: 0 0 0.5rem;
  color: var(--ink);
  font-size: 1.0625rem;
  font-weight: 800;
  letter-spacing: -0.015em;
}

.card p {
  margin: 0;
  font-size: var(--text-body);
  line-height: 1.72;
}

/* ---- how it works --------------------------------------------------------- */

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: step;
  display: grid;
  gap: 1.5rem;
  grid-template-columns: 1fr;
}

.step {
  position: relative;
  padding-block-start: 3rem;
}

.step::before {
  counter-increment: step;
  content: counter(step);
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 0.8125rem;
  font-weight: 800;
  color: var(--on-rose);
  background: linear-gradient(140deg, var(--rose), var(--orchid));
}

.step h3 {
  margin: 0 0 0.4rem;
  color: var(--ink);
  font-size: 1rem;
  font-weight: 800;
  letter-spacing: -0.015em;
}

.step p {
  margin: 0;
  font-size: var(--text-body);
  line-height: 1.72;
  max-width: 34ch;
}

/*
  Three across, or one down. Deliberately not `auto-fit`: at two columns the
  connecting rule below would run off the end of the first row and start again
  under a step that is not first, which is worse than no rule at all.
*/
@media (min-width: 48rem) {
  .steps {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1.75rem;
  }

  .step:not(:last-child)::after {
    content: "";
    position: absolute;
    inset-block-start: 1.05rem;
    inset-inline-start: 2.6rem;
    inset-inline-end: -1.25rem;
    height: 1px;
    background: var(--edge);
  }
}

/* ---- the photograph band -------------------------------------------------- */

/*
  Four photographs, and the first thing to say about them is what they are not:
  they are not evidence. See render.mjs on `band()` and apps/site/img/CREDITS.md
  -- nothing pictured may read as Rim, as a client, or as her premises, and they
  carry `alt=""` because they say nothing a sentence has not already said.

  Two across on a phone, four across above 48rem. Deliberately not `auto-fit`:
  three-across would leave one photograph alone on a second row, which reads as
  a mistake rather than as a composition.
*/
.band {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.7rem;
  padding-block: clamp(2rem, 6vw, 3.25rem);
}

@media (min-width: 48rem) {
  .band {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 1rem;
  }

  /*
    The stagger. Every other frame drops by a third of a gap-and-a-bit, so the
    band reads as a composition rather than as a filmstrip.

    Only above 48rem: at two columns it would drop the right-hand column of
    every row and leave a ragged bottom edge on a phone, where there is no room
    for the effect to be read as intentional.
  */
  .shot:nth-child(even) {
    margin-block-start: 2.25rem;
  }
}

.shot {
  position: relative;
  margin: 0;
  overflow: hidden;
  border-radius: var(--radius-card);
  border: 1px solid var(--edge);
  background: var(--card);
  box-shadow: var(--shadow-soft);
}

/*
  `aspect-ratio` matches the `width`/`height` attributes on the element, which
  are 700x933. Both are here on purpose: the attributes reserve the space before
  the file arrives, and this keeps the box right if a future crop changes.
*/
.shot img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
}

/*
  The unifying wash, and it is doing real work rather than decoration.

  The four photographs came from four photographers with four different white
  balances, and side by side they looked like four photographs. A rose-to-orchid
  film at low alpha, plus a vignette into the page's own ground colour at the
  foot, pulls them into one palette and lets the band sit on the page instead of
  on top of it. It lifts on hover, so the photograph underneath is still there
  for anyone who wants to look at it.
*/
.shot::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgb(23 19 26 / 0) 42%, rgb(23 19 26 / 0.62) 100%),
    linear-gradient(155deg, rgb(242 136 159 / 0.17) 0%, rgb(192 132 216 / 0.15) 100%);
}

.shot:hover::after {
  opacity: 0.4;
}

/* ---- footer --------------------------------------------------------------- */

footer {
  margin-block-start: clamp(2rem, 6vw, 3rem);
  padding-block: 2rem 3rem;
  border-block-start: 1px solid var(--rule);
  color: var(--ink-muted);
  font-size: var(--text-body);
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  align-items: center;
}

footer a {
  color: var(--ink-soft);
  text-decoration: none;
  border-block-end: 1px solid var(--edge);
}

footer a:hover,
footer a:focus-visible {
  color: var(--rose-hi);
  border-color: var(--rose);
}

/*
  The alternate spelling of the name. Meta-sized and muted: it is there for a
  crawler and for the one visitor who typed the name the other way, and it must
  not read as a second heading in a footer of three links.
*/
footer .brand-note {
  font-size: var(--text-meta);
  color: var(--ink-muted);
}

footer .rights {
  margin-inline-start: auto;
  font-size: var(--text-meta);
}

:focus-visible {
  outline: 2px solid var(--rose-hi);
  outline-offset: 3px;
}

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

/*
  Everything below is inside `prefers-reduced-motion: no-preference`. Nothing
  below changes what is on the page -- only how it arrives.
*/
@media (prefers-reduced-motion: no-preference) {
  a,
  .card,
  .card::before,
  .lang,
  .shot,
  .shot::after,
  .step::before {
    transition:
      color 140ms ease,
      background-color 140ms ease,
      border-color 140ms ease,
      opacity 220ms ease,
      transform 220ms cubic-bezier(0.2, 0.7, 0.3, 1),
      box-shadow 220ms ease;
  }

  .card:hover,
  .card:focus-within {
    transform: translateY(-3px);
  }

  .cta:hover,
  .cta:focus-visible {
    transform: translateY(-2px);
  }

  .shot:hover {
    transform: translateY(-4px);
    border-color: rgb(242 136 159 / 0.4);
  }

  /* The counter answers the pointer too, so the whole step feels like one
     object rather than a number that happens to sit near some words. */
  .step:hover::before {
    transform: scale(1.12);
  }

  /* The glow breathes. 34 seconds is slow enough that it is never caught
     moving and only ever noticed as the page not being flat. */
  body::before {
    animation: drift 34s ease-in-out infinite;
  }

  /* The hero arrives on load. `both` fill, so the end state is the resting
     state -- an interrupted animation cannot leave anything invisible. */
  .hero-copy > *,
  .hero-art {
    animation: rise 760ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }

  .hero-copy > *:nth-child(1) { animation-delay: 40ms; }
  .hero-copy > *:nth-child(2) { animation-delay: 110ms; }
  .hero-copy > *:nth-child(3) { animation-delay: 180ms; }
  .hero-copy > *:nth-child(4) { animation-delay: 250ms; }
  .hero-copy > *:nth-child(5) { animation-delay: 320ms; }
  .hero-art { animation-delay: 240ms; animation-duration: 900ms; }

  /*
    The app draws itself.

    The phone arrives as one object above, and then its own contents come in
    behind it, top to bottom -- the week strip, then each panel. It is the same
    `rise` and the same `both` fill, so an interrupted load still ends with
    everything visible.

    Eight children, listed rather than computed, because there is no CSS way to
    say "index times seventy milliseconds" and a `:nth-child(n)` shorthand
    would not let each one be tuned.
  */
  .phone-screen > * {
    animation: rise 620ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }

  .phone-screen > *:nth-child(1) { animation-delay: 400ms; }
  .phone-screen > *:nth-child(2) { animation-delay: 470ms; }
  .phone-screen > *:nth-child(3) { animation-delay: 540ms; }
  .phone-screen > *:nth-child(4) { animation-delay: 590ms; }
  .phone-screen > *:nth-child(5) { animation-delay: 650ms; }
  .phone-screen > *:nth-child(6) { animation-delay: 700ms; }
  .phone-screen > *:nth-child(7) { animation-delay: 760ms; }
  .phone-screen > *:nth-child(8) { animation-delay: 810ms; }

  /*
    The arc fills once, on load, and then stops.

    Deliberately not a loop. An arc that empties and refills forever is a
    loading spinner, and a spinner on a home page says something is waiting on
    something. This says the day filled up. See `@property --ph-arc` above for
    why the gradient stop can move at all, and what happens where it cannot.
  */
  .ph-arc {
    animation: arcFill 1500ms cubic-bezier(0.3, 0.8, 0.3, 1) 900ms both;
  }

  /* The meters grow with it, each a beat behind the last. */
  .ph-meter b {
    transform-origin: 0 50%;
    animation: meterGrow 900ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }

  /* The bar grows from the side the language starts on. `transform-origin`
     takes no logical keyword, so the one physical value gets flipped by hand --
     the same treatment `.hero h1`'s tracking gets a few hundred lines up. */
  html[lang="ar"] .ph-meter b {
    transform-origin: 100% 50%;
  }

  .ph-meter:nth-child(1) b { animation-delay: 980ms; }
  .ph-meter:nth-child(2) b { animation-delay: 1080ms; }
  .ph-meter:nth-child(3) b { animation-delay: 1180ms; }

  /*
    The week walks, one day per second, forever.

    This and the tick below are the page's only permanent motion, and that is
    the whole design of it: everything else settles within a second of load, so
    what is left is small, slow and in one corner. A page where several things
    move at once is a page nobody can read.

    Every day gets the animation, INCLUDING the one marked `.is-on` in the
    markup, so the highlight is in one place at a time. With motion reduced no
    rule here applies and `.is-on` is simply the day that is lit.

    Direction takes care of itself: the strip is a flex row, so on the Arabic
    page it is laid out right-to-left and the walk goes with it.
  */
  .ph-day {
    animation: dayWalk 7s steps(1, end) infinite;
  }

  .ph-day:nth-child(1) { animation-delay: 0s; }
  .ph-day:nth-child(2) { animation-delay: 1s; }
  .ph-day:nth-child(3) { animation-delay: 2s; }
  .ph-day:nth-child(4) { animation-delay: 3s; }
  .ph-day:nth-child(5) { animation-delay: 4s; }
  .ph-day:nth-child(6) { animation-delay: 5s; }
  .ph-day:nth-child(7) { animation-delay: 6s; }

  /* The second workout ticks itself, holds, and clears. The first one is
     already done in the markup and stays done. */
  .ph-task:last-child .ph-tick:not(.is-done) {
    animation: tickRing 6s ease-in-out 2.2s infinite;
  }

  .ph-task:last-child .ph-tick:not(.is-done)::after {
    animation: tickMark 6s cubic-bezier(0.2, 0.9, 0.3, 1.4) 2.2s infinite;
  }

  /*
    The photographs drift.

    34 seconds is the glow's number and this is 30, deliberately not the same:
    two loops on one page at the same period beat together, and the eye finds
    the rhythm. The negative delays start each photograph part-way through its
    own cycle so the four are never in step.

    They sit at `scale(1.05)` at rest, so there is room to move without an edge
    ever showing. Without motion the transform never applies and the photograph
    fills its frame exactly.
  */
  .shot img {
    animation: kenburns 30s ease-in-out infinite;
  }

  .shot:nth-child(2) img { animation-delay: -7s; }
  .shot:nth-child(3) img { animation-delay: -15s; }
  .shot:nth-child(4) img { animation-delay: -22s; }

  /*
    A sheen across the primary call to action, once every seven seconds.

    Clipped by `overflow: hidden` on `.cta`, which is why that lives in the
    layout rules rather than here. Not on `.cta-wa`: two buttons glinting at
    each other on one line is a carnival, and the WhatsApp button has the ring
    below instead.
  */
  .cta:not(.cta-wa)::after {
    content: "";
    position: absolute;
    inset-block: -50%;
    inset-inline-start: 0;
    width: 38%;
    pointer-events: none;
    background: linear-gradient(
      100deg,
      transparent,
      rgb(255 255 255 / 0.34),
      transparent
    );
    animation: sheen 7s ease-in-out 1.6s infinite;
  }

  /*
    A ring leaving the WhatsApp button.

    It is the one thing on the page a stranger is meant to press, and it is
    below the fold on every screen, so it gets a heartbeat rather than a glint.
    `overflow: visible` on `.cta-wa` is what lets it grow past the pill.
  */
  .cta-wa::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    border: 1px solid var(--rose);
    pointer-events: none;
    animation: pulseRing 3.6s ease-out infinite;
  }
}

/*
  Scroll-linked reveals, and the two guards around them are not optional.

  `@supports (animation-timeline: view())` — Chrome and Edge have it; Firefox
  and Safari, at the time of writing, do not. Inside the guard, `.reveal`
  starts at `opacity: 0` and is brought back by the scroll. OUTSIDE it, no rule
  touches `.reveal` at all, so those browsers get a page whose every section is
  simply visible. The failure mode of getting this backwards is a blank site in
  two of the four engines, with nothing in the console to say so.

  The reduced-motion query is the second guard, for the same reason it is
  everywhere else here.
*/
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry 10% entry 85%;
    }

    /*
      The stagger across a row, and it is `animation-range` rather than
      `animation-delay` because there is no time here to delay.

      A scroll-linked animation's clock is the scrollbar: `animation-delay`
      measures milliseconds and means nothing on a timeline made of pixels.
      Pushing the RANGE later is the equivalent -- a card whose entry begins
      further into the viewport finishes later, so a row arrives left to right.

      Three columns is the widest the grid goes, so `3n` is the row.
    */
    .cards .card:nth-child(3n + 2) {
      animation-range: entry 15% entry 90%;
    }

    .cards .card:nth-child(3n) {
      animation-range: entry 20% entry 95%;
    }

    /* The band is four across, and its own stagger is already in the layout --
       every other photograph starts lower. Offsetting the reveal as well
       doubles it, so these two only trade places. */
    .band .shot:nth-child(even) {
      animation-range: entry 16% entry 91%;
    }
  }
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes drift {
  0% {
    transform: translate3d(0, 0, 0) scale(1);
  }
  50% {
    transform: translate3d(2.5%, -3%, 0) scale(1.12);
  }
  100% {
    transform: translate3d(0, 0, 0) scale(1);
  }
}

/* The arc's one fill. Starts at 6% rather than 0 so there is a mark to grow
   from and the ring never reads as empty. */
@keyframes arcFill {
  from {
    --ph-arc: 6%;
  }
  to {
    --ph-arc: 62%;
  }
}

@keyframes meterGrow {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

/*
  One day lit out of seven.

  `steps(1, end)` on the animation rather than an easing: the highlight should
  move from one day to the next, not fade between them, and a day is either
  today or it is not. The lit window is the first seventh of the cycle, and the
  seven delays are one second apart, so exactly one day is lit at any moment.
*/
@keyframes dayWalk {
  0%,
  14.2% {
    background-color: var(--rose);
    color: var(--on-rose);
  }
  14.3%,
  100% {
    background-color: var(--card);
    color: var(--ink-muted);
  }
}

@keyframes tickRing {
  0%,
  46% {
    background-color: transparent;
    border-color: var(--edge);
  }
  54%,
  88% {
    background-color: var(--rose);
    border-color: var(--rose);
  }
  96%,
  100% {
    background-color: transparent;
    border-color: var(--edge);
  }
}

/* The mark overshoots very slightly on the way in -- the easing on the rule
   carries it past 1 -- which is what makes it read as a tap rather than a
   fade. `rotate(45deg)` is the tick's shape and has to be repeated in every
   keyframe, because a transform list replaces rather than accumulates. */
@keyframes tickMark {
  0%,
  46% {
    opacity: 0;
    transform: translateY(-1px) rotate(45deg) scale(0.4);
  }
  56%,
  88% {
    opacity: 1;
    transform: translateY(-1px) rotate(45deg) scale(1);
  }
  96%,
  100% {
    opacity: 0;
    transform: translateY(-1px) rotate(45deg) scale(0.4);
  }
}

@keyframes kenburns {
  0% {
    transform: scale(1.05) translate3d(0, 0, 0);
  }
  50% {
    transform: scale(1.13) translate3d(-1.6%, -2.2%, 0);
  }
  100% {
    transform: scale(1.05) translate3d(0, 0, 0);
  }
}

/* Off-screen for most of the cycle, across in about two seconds. The two
   held keyframes are the pause; without them the sheen never stops crossing. */
@keyframes sheen {
  0%,
  64% {
    transform: translateX(-220%) skewX(-18deg);
  }
  92%,
  100% {
    transform: translateX(420%) skewX(-18deg);
  }
}

@keyframes pulseRing {
  0% {
    opacity: 0.5;
    transform: scale(1);
  }
  70%,
  100% {
    opacity: 0;
    transform: scale(1.17);
  }
}
