/* ==========================================================================
   Mortaga — reverse logistics for Egyptian e-commerce
   مرتجع: the word Egyptian merchants use for a returned item.
   See the palette block below for the colour reasoning.
   ========================================================================== */

/* The browser's own `[hidden] { display: none }` is only an element-level rule,
   so any class that sets a display — .btn, .banner-app, .choice, every flex
   thing here — silently outranks it and the element stays on screen. That is
   how the dashboard came to show "No store connected yet" directly above
   "350 orders synced": the JS had set .hidden correctly and the CSS ignored it.
   Must stay first, and must keep !important — later sheets set display too. */
[hidden] { display: none !important; }

/* ==========================================================================
   Palette — Mortaga

   Green and white — sage for the brand, a light green paper underneath, one
   warm amber kept back for contrast.

   Three rules held the whole way through:

   1. Nothing is pure. The page is #F4F8F5, not #FFFFFF; the ink is #16201B,
      not #000. Pure white against pure black is the highest-contrast pairing a
      screen can produce, and staring at it all day is what makes dashboards
      tiring. Tinted paper under near-black is the same legibility with the
      glare taken out — that is what makes it restful rather than merely light.
   2. The green is desaturated. A vivid green over a whole interface reads as
      loud within about a minute; sage carries the same identity and can be
      looked at for an hour.
   3. Cards are true white. Against a green-tinted page that separation is what
      gives the layout depth without needing a heavier shadow.

   Everything hangs off --brand. Green/amber/red below are the only colours
   allowed to mean something — and since the brand is now also green, see the
   note on --ok.
   ========================================================================== */
:root {
  --ink:        #16201B;   /* near-black with a green cast — never #000 */
  --ink-2:      #202C26;
  --brand:      #2F6B54;   /* sage — low chroma on purpose, see the note below */
  --brand-lift: #3F8A6D;
  --brand-deep: #234F3F;
  --brand-glow: #63AC8E;   /* hover accents only, never a surface */

  --ember:      #A2601D;   /* the one warm note, for contrast against all the green */
  --ember-lift: #E09A4C;

  --concrete:   #E7EFE9;
  --paper:      #F4F8F5;   /* light green paper — the resting surface */
  --surface:    #FFFFFF;
  --saffron:    #A2601D;
  --saffron-hi: #E09A4C;
  --muted:      #55635C;
  --faint:      #66746D;
  --line:       #DDE7E0;
  --line-hi:    #C4D2C9;

  /* Status. Approved / needs review / rejected — data, never decoration.

     --ok is green and so is the brand, which is the one real hazard in a green
     palette. They are kept apart by chroma, not hue: --brand is a muted sage
     that sits back, --ok is vivid and reads as a signal. Do not "harmonise"
     --ok toward the brand — the moment approved-green looks like button-green,
     a merchant scanning the queue can no longer tell state from furniture. */
  --ok:         #0E8145;
  --warn:       #A16814;
  --err:        #C4453B;

  --bg:      var(--paper);
  --fg:      var(--ink);
  --card:    var(--surface);
  --accent:  var(--brand);
  --accent-t:var(--brand-lift);

  --disp: "Bricolage Grotesque", "Segoe UI Variable Display", system-ui, sans-serif;
  --body: "IBM Plex Sans Arabic", "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, Consolas, monospace;

  /* ---- type scale ----
     The thing that separates the reference sites from a default admin panel is
     the size of the jump: 16px body under a 90px heading. A timid 28px hero is
     what makes a page read like a form. Tracking tightens as size grows, since
     letter-spacing that looks right at 16px looks loose and airy at 90px. */
  --fs-display: clamp(40px, 7.4vw, 92px);
  --fs-h1:      clamp(31px, 4.4vw, 54px);
  --fs-h2:      clamp(24px, 2.8vw, 36px);
  --fs-h3:      20px;
  --fs-body:    16px;
  --fs-sm:      14px;
  --fs-xs:      12.5px;

  --track-display: -.038em;
  --track-h1:      -.028em;
  --track-h2:      -.02em;
  --lh-display:    .94;
  --lh-head:       1.08;

  --measure: 62ch;
  --gutter: clamp(20px, 5vw, 64px);
  --maxw: 1160px;

  /* ---- rhythm ----
     One scale for vertical space, so sections cannot drift apart by 4px at a
     time. The section step is the biggest single difference between a page
     that reads calm and one that reads cramped — generous air is most of what
     "clean" means. */
  --sp-1: 6px;
  --sp-2: 12px;
  --sp-3: 20px;
  --sp-4: 32px;
  --sp-5: 52px;
  --sp-6: 84px;
  --sp-section: clamp(72px, 11vw, 132px);

  /* ---- shape ----
     3px read as a 2012 admin panel. Surfaces get real curvature; controls stay
     slightly tighter than the cards that hold them so nesting reads correctly. */
  --r-xs: 8px;
  --r:    11px;   /* inputs, small controls */
  --r-md: 16px;   /* cards, panels */
  --r-lg: 22px;   /* big containers, dialogs */
  --r-full: 100px;

  /* ---- elevation ----
     Soft and wide, not tight and dark. A shadow you can point to is a shadow
     that is too strong: at these alphas a card reads as lifted without the eye
     ever resolving an edge, which is what keeps a light interface calm.
     Dark mode swaps to a rim-light, since a shadow on near-black is invisible. */
  --sh-1: 0 1px 2px rgba(22,32,27,.04), 0 1px 1px rgba(22,32,27,.03);
  --sh-2: 0 1px 3px rgba(22,32,27,.05), 0 8px 20px -8px rgba(22,32,27,.09);
  --sh-3: 0 2px 6px rgba(22,32,27,.05), 0 24px 48px -16px rgba(22,32,27,.14);
  --rim: inset 0 1px 0 rgba(255,255,255,.6);

  /* ---- tints ----
     Mixed off --brand rather than baked as hex, so they re-derive in dark mode
     and follow the brand if it is ever swapped. Flat washes for surfaces. */
  --tint-brand: color-mix(in srgb, var(--brand) 7%, var(--card));

  /* ---- motion ----
     One family of curves, four speeds. Consistency is what reads as "smooth";
     a page where every element eases differently reads as janky even when fast.

     Every curve here ends flat (y=1 approached from below) except --ease-pop,
     which overshoots to 1.56. Overshoot is a garnish: it belongs on things that
     *appear* — a chip being added, a badge landing — and never on hover or on
     anything the user triggers dozens of times a day, where the extra 100ms of
     wobble is just latency they have to wait through. */
  --ease: cubic-bezier(.22,.72,.28,1);
  --ease-out: cubic-bezier(.16,1,.3,1);
  --ease-soft: cubic-bezier(.32,.72,0,1);      /* long, gliding — panels, reveals */
  --ease-pop: cubic-bezier(.34,1.56,.64,1);    /* overshoot — entrances only */
  --t-fast: 120ms;
  --t: 200ms;
  --t-slow: 340ms;
  --t-glide: 620ms;
}

/* Arabic mode: Bricolage has no Arabic coverage, so display collapses into
   the Plex Arabic superfamily rather than silently falling back mid-heading. */
:root[lang="ar"] {
  --disp: "IBM Plex Sans Arabic", "Segoe UI", Tahoma, sans-serif;
}

/* Light is the default and it is not conditional. There is deliberately no
   `@media (prefers-color-scheme: dark)` block here: the :root values above are
   already the light theme, so dark applies only when someone asks for it by
   toggling. That also removes the dark flash a machine set to dark used to get
   in the moment before the theme script ran.

   Dark keeps the same green cast the neutrals have in light mode, so switching
   themes reads as the lights going down rather than as a different product.
   Sage is too dark to survive on near-black, so dark promotes --brand-glow to
   brand — and --ok goes brighter still, to hold the same gap it has in light. */
:root[data-theme="dark"] {
  --bg:#111714; --fg:#E5EBE7; --card:#19211D; --paper:#111714; --surface:#19211D;
  --concrete:#202A25; --muted:#96A39C; --faint:#7F8C86; --line:#27322C; --line-hi:#36443D;
  --accent:#63AC8E; --accent-t:#86C6AC; --saffron:#E09A4C; --saffron-hi:#EDB877;
  --brand:#63AC8E; --brand-lift:#86C6AC; --brand-deep:#3F8A6D;
  --brand-glow:#86C6AC; --ember:#E09A4C; --ember-lift:#EDB877;
  --ok:#4ADE80; --warn:#FBBF24; --err:#FB7185;
  --sh-1: 0 1px 2px rgba(0,0,0,.4);
  --sh-2: 0 2px 6px rgba(0,0,0,.45), 0 8px 20px -8px rgba(0,0,0,.55);
  --sh-3: 0 6px 14px rgba(0,0,0,.5), 0 20px 44px -14px rgba(0,0,0,.65);
  --rim: inset 0 1px 0 rgba(255,255,255,.055);
}
:root[data-theme="light"] {
  --bg:#F4F8F5; --fg:#16201B; --card:#FFFFFF; --paper:#F4F8F5; --surface:#FFFFFF;
  --concrete:#E7EFE9; --muted:#55635C; --faint:#66746D; --line:#DDE7E0; --line-hi:#C4D2C9;
  --accent:#2F6B54; --accent-t:#234F3F; --saffron:#A2601D; --saffron-hi:#E09A4C;
  --brand:#2F6B54; --brand-lift:#3F8A6D; --brand-deep:#234F3F;
  --brand-glow:#63AC8E; --ember:#A2601D; --ember-lift:#E09A4C;
  --ok:#0E8145; --warn:#A16814; --err:#C4453B;
  --sh-1: 0 1px 2px rgba(22,32,27,.04), 0 1px 1px rgba(22,32,27,.03);
  --sh-2: 0 1px 3px rgba(22,32,27,.05), 0 8px 20px -8px rgba(22,32,27,.09);
  --sh-3: 0 2px 6px rgba(22,32,27,.05), 0 24px 48px -16px rgba(22,32,27,.14);
  --rim: inset 0 1px 0 rgba(255,255,255,.6);
}

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--body);
  font-size: 16.5px;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  transition: background-color var(--t-slow) var(--ease), color var(--t-slow) var(--ease);
}

img { max-width: 100%; display: block; }

:where(a):focus-visible,
:where(button):focus-visible,
:where(input):focus-visible {
  outline: 2px solid var(--accent-t);
  outline-offset: 3px;
  border-radius: var(--r);
}

.shell { max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }

/* ---------- typography ---------- */
h1, h2, h3 { font-family: var(--disp); margin: 0; text-wrap: balance; }
h1 {
  font-size: var(--fs-h1);
  line-height: var(--lh-head);
  letter-spacing: var(--track-h1);
  font-weight: 800;
}
h2 {
  font-size: var(--fs-h2);
  line-height: 1.1;
  letter-spacing: var(--track-h2);
  font-weight: 800;
}
h3 {
  font-size: var(--fs-h3);
  line-height: 1.25;
  letter-spacing: -.012em;
  font-weight: 700;
}

/* Opt-in hero size. Only the marketing page should shout — a dashboard with a
   90px heading over a data table is a poster, not a tool. */
.h-display {
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  font-weight: 800;
}
:root[lang="ar"] .h-display { letter-spacing: 0; line-height: 1.16; }
:root[lang="ar"] h1 { letter-spacing: 0; line-height: 1.18; }
:root[lang="ar"] h2 { letter-spacing: 0; line-height: 1.3; }
:root[lang="ar"] h3 { letter-spacing: 0; }

p { margin: 0; max-width: var(--measure); }

.eyebrow {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent-t);
  display: flex;
  align-items: center;
  gap: 10px;
}
:root[lang="ar"] .eyebrow { letter-spacing: .04em; }

.lede { font-size: clamp(17px, 2.1vw, 20.5px); color: var(--muted); line-height: 1.56; }
.tnum { font-variant-numeric: tabular-nums; }

/* ---------- nav ---------- */
/* The rule only appears once the page has actually moved. A hairline under a
   bar sitting at the very top of an unscrolled page is drawing a boundary
   between nothing and nothing; letting it fade in is the cheapest way to make
   a header feel considered rather than stuck on. */
.nav {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid transparent;
  transition: border-color var(--t-slow) var(--ease),
              background var(--t-slow) var(--ease);
}
.nav.stuck {
  border-bottom-color: var(--line);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
}
.nav-in {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; height: 68px;
}
.brand {
  display: flex; align-items: center; gap: 9px;
  font-family: var(--disp); font-weight: 800; font-size: 19px;
  letter-spacing: -.03em; color: var(--fg); text-decoration: none;
}
:root[lang="ar"] .brand { letter-spacing: 0; }
/* ---------- the logo ----------

   One file — /assets/img/logo.svg — drawn everywhere through this class. It
   used to be an inline <svg> pasted into eleven places, which is the same
   problem shell.js exists to solve for the nav: every change was an eleven
   file edit, and copies drift.

   It is a mask rather than an <img> because the mark has to inherit `color`.
   It renders sage in the sidebar, ink in the nav, and white on the inverted
   band — an <img> is opaque to CSS and would come out one fixed colour, and
   an external <use> does not reliably inherit styles from the host document
   in Chrome or Safari. A mask reads only the alpha channel, so the shape
   comes from the file and the colour comes from `currentColor` here.

   > **A mask is a silhouette.** If you replace logo.svg with a multi-colour
   > mark it will render as one flat shape. To keep the artwork's own colours,
   > swap the two mask lines below for:
   >
   >     background: url("/assets/img/logo.svg") center / contain no-repeat;
   >
   > and delete `background-color`. You lose the tinting, so check the mark
   > against the dark theme and the inverted `.stakes` band before shipping. */
.mark {
  /* The artwork's own width/height. The mask is sized `contain`, so a box that
     does not match the file's proportions letterboxes it — a portrait logo in
     a square box renders narrow and floats off the wordmark's baseline gap.
     Height is the fixed dimension because that is what has to line up with the
     text beside it; width follows from this ratio.

     Set it from the <svg> element's own viewBox and nothing else needs to
     change when the logo does. */
  --logo-aspect: 1 / 1;

  height: 21px;
  width: auto;
  aspect-ratio: var(--logo-aspect);
  flex: none;
  display: inline-block;
  color: var(--accent-t);
  background-color: currentColor;
  -webkit-mask: url("/assets/img/logo.svg") center / contain no-repeat;
          mask: url("/assets/img/logo.svg") center / contain no-repeat;
}
.nav-actions { display: flex; align-items: center; gap: 8px; }

/* These sit in a 68px bar and have to stay small to belong there, but at
   ~29px tall they were well under the 44px minimum tap target. Rather than
   inflate the pill, the hit area is extended past the visual bounds with a
   pseudo-element — the button looks the same and the thumb gets the full
   44px. position:relative is load-bearing for that. */
.ghost {
  position: relative;
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .1em;
  text-transform: uppercase;
  background: none; border: 1px solid var(--line-hi); color: var(--muted);
  padding: 8px 14px; border-radius: 100px; cursor: pointer;
  transition: color .18s ease, border-color .18s ease;
}
.ghost::after {
  content: ''; position: absolute; inset: 50% 0 0 0;
  height: 44px; width: 100%;
  transform: translateY(-50%);
}
.ghost:hover { color: var(--fg); border-color: var(--fg); background: color-mix(in srgb, var(--fg) 5%, transparent); }
.ghost:active { transform: scale(.96); }
.ghost { transition: color var(--t) var(--ease), border-color var(--t) var(--ease),
                     background var(--t) var(--ease), transform var(--t-fast) var(--ease); }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  font-family: var(--body); font-weight: 600; font-size: 15px;
  padding: 12px 22px; border-radius: var(--r-full);
  text-decoration: none; cursor: pointer; border: 1px solid transparent;
  transition: background var(--t) var(--ease), transform var(--t-fast) var(--ease),
              box-shadow var(--t) var(--ease), border-color var(--t) var(--ease),
              color var(--t) var(--ease);
  will-change: transform;
}
.btn:active { transform: translateY(1px) scale(.985); transition-duration: 60ms; }
.btn-primary { background: var(--accent); color: #fff; box-shadow: var(--sh-1); }
.btn-primary:hover { background: var(--accent-t); transform: translateY(-1px); box-shadow: var(--sh-2); }
/* background:none matters — without it the UA button colour shows through and
   light text lands on a light button. */
.btn-line { background: none; border-color: var(--line-hi); color: var(--fg); }
.btn-line:hover { border-color: var(--fg); background: color-mix(in srgb, var(--fg) 6%, transparent); }
.btn svg { width: 15px; height: 15px; flex: none; }
:root[dir="rtl"] .btn svg { transform: scaleX(-1); }

/* ==========================================================================
   Marketing components

   Rebuilt around one idea: the page should argue, not list. The old layout was
   a split hero beside a widget, then a stat strip, then a grid of eight cards
   — the default SaaS stack, where every section carries the same visual weight
   and none of them lands. This version alternates: a centred statement, a
   full-bleed line of argument, a horizontal journey, the calculator given a
   whole section of its own, then the comparison the product actually turns on.
   ========================================================================== */

/* ---------- section rhythm ----------

   The single most load-bearing rule on the page. Without it every section
   stacks flush against the next and the whole thing reads as one wall of
   text — no amount of padding inside the components makes up for missing
   space between them. */
section { padding: var(--sp-section) 0; }

.sec-head {
  display: grid; gap: var(--sp-2);
  margin-bottom: var(--sp-5);
  max-width: 640px;
}

/* ---------- hero ---------- */

/* One column, centred, capped at a readable measure. A split hero forces the
   headline to compete with whatever is beside it; alone it can be enormous. */
.hero-v2 {
  padding: clamp(72px, 12vw, 150px) 0 var(--sp-6);
  text-align: center;
  /* The containing block for the floating layer, and the clip that lets a
     photograph sit half off the edge instead of stopping short of it. */
  position: relative;
  overflow: hidden;
}

/* The ring is sized as a percentage of this box, so the hero has to be tall
   enough to contain it. Sized off the copy alone it came out around 900px,
   which left the twelve- and six-o'clock photographs hanging ~30px past the
   top and bottom edges — and `overflow: hidden` sliced them in half. Give the
   ring room instead of shrinking it. */
@media (min-width: 1100px) {
  .hero-v2 { min-height: clamp(820px, 92vh, 1040px); display: grid; align-content: center; }
}
.hero-v2-in { display: grid; gap: var(--sp-3); justify-items: center; position: relative; z-index: 2; }
.hero-v2-in .eyebrow { justify-self: center; }
.hero-v2-in h1 { max-width: 16ch; }
.hero-v2-lede { max-width: 60ch; margin-top: var(--sp-1); }
.hero-v2-cta {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  justify-content: center; margin-top: var(--sp-2);
}
.btn-lg { padding: 15px 30px; font-size: 16px; }

/* ---------- floating photographs ----------

   A parallax layer behind the hero. floating.js drives the transform on
   .floaty-el; everything here is position, size and the fade-in.

   The split matters: the script owns `transform` on the outer element and
   nothing else may touch it, so the entrance animates opacity only and the
   hover scale lives on the inner .floaty-shot. An inline transform written
   every frame silently wins over any keyframe on the same element, which is
   how a parallax layer ends up with no entrance at all. */

.floaty {
  position: absolute; inset: 0; z-index: 0;
  /* The layer is decoration and must not intercept a click meant for the
     headline's call to action; the photographs opt back in individually. */
  pointer-events: none;
}

/* `translate` rather than a transform, and that is the whole trick: it is its
   own property in the individual-transform model, so it composes with the
   `transform` floating.js writes every frame instead of being overwritten by
   it. That lets top/left address the centre of each photograph, which is the
   only way a ring stays a ring when the eight sizes differ. */
.floaty-el { position: absolute; pointer-events: auto; translate: -50% -50%; }

/* The ring passes through the text, because at these proportions any ring
   that clears a centred 693px measure at the diagonals would have to sit
   outside the hero. So the photographs are allowed behind the copy and this
   veil buys the legibility back: the page colour at full strength behind the
   words, faded out well before the ring.

   It is a separate element rather than a pseudo-element on .hero-v2 so the
   stacking is explicit — photographs at 0, veil at 1, copy at 2. As ::after
   it would paint last and sit on top of the text it is meant to protect. */
.floaty-veil {
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none;
  /* Sized off the text, not off the hero: the copy runs x ±347 and y ±234
     about a centre at 42% of the hero height — above the geometric middle,
     because the flightpath below it is decoration nothing needs to protect.

     An ellipse cannot cover a rectangle of similar proportions without
     growing by about √2 on both axes, because the corners of the text are the
     furthest points from the centre and an inscribed ellipse misses them. So
     this is deliberately bigger than the text and does overlap the ring: the
     photographs soften on the side facing the copy and stay full strength on
     the side facing the edge. That reads as depth rather than as a mistake,
     and it is the only way a ring this close to the middle can sit behind
     dark text on light paper at all.

     If you want the photographs sharper, widen the ring in index.html and
     these two radii together; the veil must always end outside the text and
     the ring must always start outside the veil. */
  /* Percentages alone are wrong at the narrow end. The copy is capped in `ch`,
     so its width barely changes across the desktop range while the hero's
     does — a purely proportional veil shrinks out from under the lede at
     around 1000px even though the lede is the same size it was at 1400. The
     px floor holds the veil to the text instead of to the viewport.

     That floor used to be written as `max()` INSIDE the gradient's radius:

       radial-gradient(max(46%, 600px) max(48%, 430px) at 50% 42%, …)

     Chrome rejects a math function in that position, and an invalid value
     drops the whole declaration — so this element painted nothing at all and
     the veil silently did not exist. Verified with getComputedStyle: the
     computed backgroundImage was `none`. Nothing looked broken only because
     the ring was wide enough that the photographs missed the text anyway.

     The floor now lives on the element's own box, where max() is valid, and
     the gradient uses plain percentages of that box. Same result, and it
     actually renders. Keep math functions out of the radius. */
  inset: auto;
  left: 50%;
  top: 42%;
  translate: -50% -50%;
  /* Sized to the copy, not to the hero. At 94%/98% it covered almost the
     whole section and flattened the entire ring to a grey smudge — the
     photographs are meant to stay vivid out at the edges and soften only
     where they pass behind the words.

     Narrow but tall, and those two are set independently on purpose. The
     copy is a tall column — eyebrow through to "Works with Shopify" is a
     little over 500px — while its width is capped in `ch`. A veil wide
     enough to matter reaches the side photographs; one short enough to spare
     them leaves the eyebrow and the button sitting out in the falloff, over
     bare photograph. Width stays off the ring, height covers the column.

     The solid stop does the real work: at 62% the core ended above the
     eyebrow. 70% keeps the whole text block on flat colour and still leaves
     the outer third of the ellipse to fade through. */
  width: max(880px, 50%);
  height: max(820px, 94%);
  background: radial-gradient(50% 50% at 50% 50%,
    var(--bg) 0%,
    var(--bg) 70%,
    color-mix(in srgb, var(--bg) 52%, transparent) 88%,
    transparent 100%);
}

/* Promoted only while the layer is genuinely live — floating.js adds .is-live
   when it binds and drops it when the hero leaves the viewport or the pointer
   turns out to be coarse. Eight compositor layers held for the life of the
   page is the same waste as `will-change` sitting on every button: the hint
   costs memory continuously and buys nothing outside the gesture. */
@media (prefers-reduced-motion: no-preference) {
  .floaty.is-live .floaty-el { will-change: transform; }
}

.floaty-shot {
  position: relative; overflow: hidden;
  border-radius: var(--r-md);
  background: var(--concrete);
  border: 1px solid var(--line);
  box-shadow: var(--sh-2);
  opacity: 0;
  animation: floaty-in var(--t-glide) var(--ease-soft) both;
  animation-delay: calc(var(--i, 0) * 90ms);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.floaty-shot:hover { transform: scale(1.045); box-shadow: var(--sh-3); }
.floaty-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
@keyframes floaty-in { to { opacity: 1; } }

/* Same contract as .shot-empty on the screenshot band: the label sits behind
   the image and is simply covered once a real file loads. The <img> carries
   onerror="this.remove()", so a missing file leaves this rather than a
   broken-image icon. */
.floaty-empty {
  position: absolute; inset: 0; display: grid; place-content: center;
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--faint); text-align: center;
}
:root[lang="ar"] .floaty-empty { letter-spacing: 0; }

/* Sizes are viewport-relative on purpose, and the middle term matters more
   than the maximum: the photographs live in the gutter beside the centred
   text, and that gutter closes faster than the viewport does. Scaling on vw
   is what keeps a photograph and the space it has to sit in shrinking at
   roughly the same rate. Raise the vw coefficients and the low end of the
   desktop range is where it breaks, not the high end. */
/* Scaled up roughly 25% — at the old sizes the ring read as a scattering of
   thumbnails rather than as photographs. Safe to grow now that .hero-v2
   carries a min-height tall enough to hold the ring (see above). */
.fs-s { width: clamp(92px, 10vw, 166px);   height: clamp(92px, 10vw, 166px); }
.fs-m { width: clamp(120px, 13.5vw, 226px); height: clamp(120px, 13.5vw, 226px); }
.fs-t { width: clamp(142px, 14.5vw, 250px); height: clamp(194px, 19.5vw, 340px); }
.fs-l { width: clamp(178px, 17.5vw, 316px); height: clamp(134px, 13.5vw, 240px); }

/* The cutoff used to be about gutters — how much clear space there was beside
   the measure — and it moved twice as the photographs grew. The veil retired
   that question: the ring is allowed over the text now, so a narrow viewport
   costs legibility nothing.

   What is left is composition, and the veil's px floor. Below this the ring's
   radius drops under the veil's, so every photograph sits inside the wash and
   the whole layer greys out — a ring of ghosts is worse than no ring. */
@media (max-width: 1100px) { .floaty, .floaty-veil { display: none; } }

/* Reduced motion keeps the photographs and drops the drift and the stagger.
   They are content, not an effect — removing them would change what the page
   says, which is not what the preference asks for. */
@media (prefers-reduced-motion: reduce) {
  .floaty-shot { opacity: 1; animation: none; }
}

/* ---------- flight path ---------- */

/* The brand argument as a drawing: out, and back. It is decoration, so it is
   aria-hidden and it is the first thing reduced-motion switches off. */
.flightpath { width: 100%; max-width: 780px; margin-top: var(--sp-5); color: var(--line-hi); }
.flightpath svg { width: 100%; height: auto; display: block; overflow: visible; }
.fp-track {
  stroke-dasharray: 5 9;
  animation: fp-travel 2.4s linear infinite;
}
.fp-dot { color: var(--accent); opacity: .55; }
@keyframes fp-travel { to { stroke-dashoffset: -28; } }
@media (prefers-reduced-motion: reduce) { .fp-track { animation: none; } }

/* ---------- stakes ---------- */

/* Full-bleed, inverted, one sentence set large. It is the only inverted band
   on the page — that is what makes it read as the turn in the argument
   rather than as another section. */
.stakes {
  background: var(--ink);
  color: #fff;
  padding: var(--sp-6) 0;
}
.stakes-in { display: grid; gap: var(--sp-3); max-width: 900px; }
.stakes .eyebrow { color: color-mix(in srgb, #fff 62%, transparent); }
.stakes-line {
  font-family: var(--disp);
  font-size: clamp(23px, 3.1vw, 38px);
  line-height: 1.28;
  letter-spacing: -.02em;
  font-weight: 600;
  color: color-mix(in srgb, #fff 86%, transparent);
}
.stakes-line b { color: #fff; font-weight: 800; }
:root[lang="ar"] .stakes-line { letter-spacing: 0; line-height: 1.6; }

/* ---------- journey ---------- */

/* Four stops reading across, not down. The connecting rule sits behind the
   numbers so the row reads as one route rather than four unrelated cards. */
.journey {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--sp-4);
  grid-template-columns: 1fr;
  position: relative;
}
@media (min-width: 760px) {
  .journey { grid-template-columns: repeat(4, 1fr); gap: var(--sp-3); }
  /* The line only makes sense once the stops are side by side. */
  .journey::before {
    content: ''; position: absolute;
    top: 15px; left: 0; right: 0; height: 1px;
    background: linear-gradient(90deg,
      transparent, var(--line-hi) 12%, var(--line-hi) 88%, transparent);
  }
}
.stop { position: relative; display: grid; gap: var(--sp-1); align-content: start; }
.stop-n {
  font-family: var(--mono); font-size: 11.5px; font-weight: 500;
  letter-spacing: .12em;
  width: 31px; height: 31px; border-radius: 100px;
  display: grid; place-items: center;
  background: var(--card); color: var(--accent);
  border: 1px solid var(--line-hi);
  margin-bottom: var(--sp-1);
}
:root[lang="ar"] .stop-n { letter-spacing: 0; }
.stop b { font-size: 16px; font-weight: 700; line-height: 1.35; }
.stop p { font-size: 14.5px; color: var(--muted); line-height: 1.6; }

/* ---------- calculator ---------- */

.calc-band { background: var(--concrete); border-block: 1px solid var(--line); }
.sec-head-center { max-width: 620px; margin-inline: auto; text-align: center; justify-items: center; }

.calc-v2 {
  display: grid; gap: 0;
  grid-template-columns: 1fr;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--sh-2);
}
@media (min-width: 860px) { .calc-v2 { grid-template-columns: 1.25fr .75fr; } }

.calc-controls { padding: clamp(28px, 3.4vw, 44px); display: grid; gap: var(--sp-5); align-content: start; }

/* ---- slider fields ----
   .field-top must be flex with space-between or the label and its output run
   together as one sentence: "Refunds you pay out now EGP 400,000". A number
   that is meant to be read as an answer has to sit apart from its question. */
.field { display: grid; gap: var(--sp-2); }
.field-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.field label { font-size: 14.5px; font-weight: 600; color: var(--muted); }
.field output {
  font-family: var(--mono); font-size: 15px; font-weight: 500;
  color: var(--fg); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 4px; border-radius: 100px;
  background: var(--line-hi); cursor: pointer; margin: 4px 0;
}
/* The thumb is 22px so the grab target clears the 44px minimum once the
   invisible padding around a range input is counted. */
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--accent); border: 4px solid var(--card);
  box-shadow: 0 0 0 1px var(--accent), var(--sh-1);
  cursor: grab;
  transition: transform var(--t-fast) var(--ease);
}
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.12); }
input[type="range"]:active::-webkit-slider-thumb { cursor: grabbing; transform: scale(1.05); }
input[type="range"]::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--accent); border: 4px solid var(--card);
  box-shadow: 0 0 0 1px var(--accent); cursor: grab;
}
input[type="range"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 6px;
}

/* The answer panel is tinted and separated by a real border, so the sliders
   read as the question and this reads as the result. */
.calc-result {
  padding: clamp(28px, 3.4vw, 44px);
  background: var(--tint-brand);
  border-top: 1px solid var(--line);
  display: grid; gap: var(--sp-2); align-content: center;
}
@media (min-width: 860px) {
  .calc-result { border-top: 0; border-inline-start: 1px solid var(--line); }
}
.calc-label {
  font-family: var(--mono); font-size: 11px; letter-spacing: .13em;
  text-transform: uppercase; color: var(--muted);
}
:root[lang="ar"] .calc-label { letter-spacing: 0; }
.calc-big {
  font-family: var(--disp); font-weight: 800;
  font-size: clamp(31px, 4.2vw, 46px);
  letter-spacing: -.03em; line-height: 1.05;
  color: var(--accent);
}
:root[lang="ar"] .calc-big { letter-spacing: 0; }
.calc-split {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3);
  margin-top: var(--sp-4); padding-top: var(--sp-4);
  border-top: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
}
.calc-split > div { display: grid; gap: var(--sp-1); }
.calc-k { font-size: 12.5px; color: var(--muted); }
.calc-v { font-family: var(--mono); font-size: 14.5px; color: var(--fg); font-variant-numeric: tabular-nums; }

/* ---------- versus ---------- */

.versus { display: grid; gap: var(--sp-4); grid-template-columns: 1fr; }
@media (min-width: 760px) { .versus { grid-template-columns: 1fr 1fr; } }

.vs-card {
  padding: clamp(28px, 3.4vw, 42px);
  border-radius: var(--r-lg);
  border: 1px solid var(--line);
  background: var(--card);
  display: grid; gap: var(--sp-2); align-content: start;
}
/* The rule separates the headline number from its reasons. Without it the
   amount and the first bullet read as one block and the number stops landing. */
.vs-card ul {
  list-style: none; margin: var(--sp-3) 0 0; padding: var(--sp-3) 0 0;
  border-top: 1px solid var(--line);
  display: grid; gap: var(--sp-3);
}
.vs-card li { font-size: 15px; color: var(--muted); line-height: 1.55; padding-inline-start: 22px; position: relative; }
.vs-card li::before {
  content: ''; position: absolute; inset-inline-start: 0; top: .5em;
  width: 7px; height: 7px; border-radius: 100px; background: currentColor; opacity: .35;
}
.vs-tag {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .13em;
  text-transform: uppercase; color: var(--muted);
}
:root[lang="ar"] .vs-tag { letter-spacing: 0; }
.vs-amount {
  font-family: var(--disp); font-weight: 800;
  font-size: clamp(26px, 3.2vw, 34px); letter-spacing: -.03em;
}
:root[lang="ar"] .vs-amount { letter-spacing: 0; }

/* The losing side stays quiet. Colouring it red would make it look like an
   error state; it is simply the outcome nobody wants. */
.vs-bad .vs-amount { color: var(--muted); }
.vs-good {
  border-color: color-mix(in srgb, var(--accent) 36%, var(--line));
  background: var(--tint-brand);
}
.vs-good .vs-amount { color: var(--accent); }

/* ---------- screenshots ----------

   Drop-in slots for real product photography. Each frame reserves its aspect
   ratio up front, so the page never jumps when the image decodes — an image
   without declared dimensions is the most common cause of layout shift.

   Until a file exists the frame shows a labelled placeholder rather than a
   broken-image icon, so the layout is honest about what is missing. Replace
   the <img src> and the placeholder disappears on its own. */
/* ---------- the two screenshots ----------

   Wider than the reading shell on purpose. Body copy wants ~62ch; a dashboard
   screenshot wants every pixel it can get, and at the shell's 1160px the desk
   was rendering at ~615px — a quarter of the image's real width, with the
   table illegible. Breaking out is what makes the screenshot worth showing. */
.shots-band .shots { display: grid; gap: var(--sp-4); grid-template-columns: 1fr; }

@media (min-width: 900px) {
  .shots-band .shots {
    width: min(1480px, calc(100vw - 2 * var(--gutter)));
    margin-inline: auto;
    /* Pull back out of .shell's own max-width without changing the shell. */
    margin-left: calc(50% - min(740px, 50vw - var(--gutter)));
    margin-right: calc(50% - min(740px, 50vw - var(--gutter)));
  }

  /* Side by side and level, not overlapping. An earlier version floated the
     phone over the desk's bottom-right corner: it used the space well, but
     the two screenshots ran into each other and the captions sat at
     different heights, so the band read as cluttered rather than composed.

     Two rows, not two columns of figures: the frames share row 1 and the
     captions share row 2. Bottom-aligning the FIGURES instead lines up the
     bottoms of the captions, which — since one caption wraps to two lines and
     the other does not — leaves the frames themselves a line out of step.
     Splitting the rows is what actually puts the frames on one baseline and
     both captions on another. `display: contents` dissolves each <figure> so
     its frame and caption become grid items in their own right. */
  .shot-stage {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto auto;
    /* Read back by the phone's width calc below — the two must agree. */
    --shot-gap: clamp(24px, 2.6vw, 48px);
    column-gap: var(--shot-gap);
    /* So the phone can size itself off the band's real width in cqw. The band
       is not simply `100vw - 2 * gutter`: it starts from the shell and is
       pulled back out with negative margins, which at narrow widths comes out
       ~30px short of that. Measuring it is exact; re-deriving it was not. */
    container-type: inline-size;
    row-gap: var(--sp-2);
    align-items: end;
  }

  .shot-main, .shot-inset { display: contents; }

  .shot-main .shot-frame { grid-column: 1; grid-row: 1; }
  .shot-main .shot-cap   { grid-column: 1; grid-row: 2; }

  /* `align-items: end` above is meant for the frames in row 1. Left to apply
     to row 2 as well it bottom-aligns the captions, so the one-line caption
     drops a whole line to meet the bottom of the two-line one — the exact
     misalignment the row split was added to remove. */
  .shot-stage .shot-cap { align-self: start; }

  /* The phone is sized so its frame ends at exactly the same height as the
     desk's, with neither image cropped or stretched. That cannot be done by
     stretching it: the desk's height is whatever its own image works out to
     at the width left over, and the phone only matches that at one width.
     So the width is solved for rather than picked.

     Writing B for the stage's own width, G for the column gap and W for the
     phone — B measured with a container query, not re-derived from the
     viewport, since the band is pulled out of the shell with negative margins
     and does not equal `100vw - 2 * gutter`:

       desk outer height  = (B - G - W - 2) / (1920/1080) + 34 + 2
       phone outer height = (W - 2) * (1440/900) + 2

     the 2s being the 1px borders and the 34 the desk's chrome bar. Setting
     them equal and solving gives W = 0.2601 (B - G) + 16.68px, which is the
     calc below. At the band's 1480px cap that lands on ~391px and stops
     moving, so on any full-width desktop this is a fixed width; below the cap
     it tracks the band, because the desk does too and a constant width there
     would break the match it exists to keep.

     The two constants are the image aspect ratios. Swap in screenshots of a
     different shape and the heights drift apart — recompute from the two
     lines above, or drop back to a plain `width` and accept a short phone. */
  /* `.is-phone` is in the selector to outrank the plain `.shot-frame.is-phone`
     rule further down, which caps the width at 300px and, being later in the
     file at equal specificity, was silently winning — invisibly, until this
     rule started asking for more than 300px. */
  /* Declared here rather than on .shot-stage because the stage is the query
     container and an element cannot query itself. .shot-inset is `display:
     contents`, so it lays nothing out, but it still passes the property down
     to the frame and the caption — and being a custom property, the cqw is
     resolved where it is used, against the stage. */
  .shot-inset { --phone-w: calc(0.260116 * (100cqw - var(--shot-gap)) + 16.68px); }

  /* `.is-phone` is in the selector to outrank the plain `.shot-frame.is-phone`
     rule further down, which caps the width at 300px and, being later in the
     file at equal specificity, was silently winning — invisibly, until this
     rule started asking for more than 300px. */
  .shot-inset .shot-frame.is-phone {
    grid-column: 2; grid-row: 1;
    width: var(--phone-w);
    max-width: none;
  }
  /* The caption is capped to the frame rather than to a measure of its own.
     Column 2 is an `auto` track, so it takes the width of its widest item —
     and a 34ch caption is wider than the phone below about 1200px, which put
     the extra width into the track, took it away from the desk, and left the
     desk shorter than the phone by whatever the difference came to. */
  .shot-inset .shot-cap { grid-column: 2; grid-row: 2; max-width: var(--phone-w); }
}

/* Under 900px the two stack, each full width and centred. */
@media (max-width: 899px) {
  .shot-main, .shot-inset { margin: 0; }
  .shot-stage { display: grid; gap: var(--sp-5); }
}

.shot-frame {
  position: relative;
  border-radius: var(--r-lg);
  border: 1px solid var(--line);
  background: var(--concrete);
  box-shadow: var(--sh-2);
  overflow: hidden;
}
/* These track the real files in assets/img — a frame that disagrees with its
   image letterboxes it (see the note in that folder's README). desk.png is
   2560×1600, portal.png is 1170×2532. */
/* Neither frame carries an aspect-ratio of its own, and that is what makes a
   screenshot drop-in. The image sets the proportions and the frame grows to
   fit: swap a 2560×1600 for a 1920×1080 or a 1440×900 and it scales down to
   the available width, keeps its shape, and nothing is cropped or letterboxed.

   Both used to be pinned. The wide frame was pinned to the image ratio while
   also carrying a 34px chrome bar, so it came out 34px short and object-fit
   ate the bottom row of the table; the phone was pinned to 1170/2532, so any
   other screenshot would have been cropped to match. */
.shot-frame.is-wide  { aspect-ratio: auto; }
.shot-frame.is-phone { aspect-ratio: auto; max-width: 300px; margin-inline: auto; }

/* A short bar of window chrome so a flat screenshot reads as a screen. */
.shot-bar {
  height: 34px; display: flex; align-items: center; gap: 6px;
  padding-inline: 13px;
  background: var(--card); border-bottom: 1px solid var(--line);
}
.shot-bar i { width: 9px; height: 9px; border-radius: 100px; background: var(--line-hi); }

/* `height: auto` is the whole mechanism: the image fills the frame's width and
   derives its own height, so the frame ends up exactly the image's shape at
   whatever size it has room for. `cover` with a fixed height is what used to
   crop them. */
.shot-frame img {
  display: block; width: 100%; height: auto;
  object-fit: contain;
}

/* One screenshot per theme. Light is the visible default so that a page whose
   JS has not run — data-theme is set on boot by i18n.js — still shows an
   image rather than an empty frame. */
.shot-img.on-dark { display: none; }
:root[data-theme="dark"] .shot-img.on-light { display: none; }
:root[data-theme="dark"] .shot-img.on-dark  { display: block; }

/* Sits behind the image. Once a real file loads it is simply covered. */
.shot-empty {
  position: absolute; inset: 0; display: grid; place-content: center;
  gap: var(--sp-1); text-align: center; padding: var(--sp-3);
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--faint);
}
:root[lang="ar"] .shot-empty { letter-spacing: 0; }
.shot-empty span { font-family: var(--body); font-size: 13px; letter-spacing: 0; text-transform: none; }
.shot-cap { font-size: 13.5px; color: var(--muted); margin-top: var(--sp-2); }

/* ---------- egypt ---------- */

.reasons-v2 { display: grid; gap: var(--sp-3); grid-template-columns: 1fr; }
@media (min-width: 700px) { .reasons-v2 { grid-template-columns: 1fr 1fr; gap: var(--sp-4); } }
.reason {
  display: grid; gap: var(--sp-1); align-content: start;
  padding-inline-start: var(--sp-3);
  border-inline-start: 2px solid var(--line-hi);
  transition: border-color var(--t) var(--ease);
}
.reason:hover { border-inline-start-color: var(--accent); }
.reason b { font-size: 16px; font-weight: 700; }
.reason p { font-size: 14.5px; color: var(--muted); line-height: 1.6; }

/* ---------- faq ---------- */
/* Native <details>/<summary> — free keyboard support and no JS, which a
   disclosure list this simple doesn't need. */
.faq-list { display: grid; gap: 10px; max-width: 760px; margin-inline: auto; }
.faq-item {
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--card); overflow: hidden;
  transition: border-color var(--t) var(--ease);
}
.faq-item[open] { border-color: var(--line-hi); }
.faq-item summary {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 16px 20px; cursor: pointer; list-style: none;
  font-size: 15.5px; font-weight: 600;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary svg {
  width: 18px; height: 18px; flex: none; color: var(--faint);
  transition: transform var(--t) var(--ease);
}
.faq-item[open] summary svg { transform: rotate(180deg); }
.faq-item summary:hover { color: var(--accent-t); }
.faq-item p {
  padding: 0 20px 18px; margin: 0; max-width: none;
  font-size: 14.5px; color: var(--muted); line-height: 1.65;
}
.faq-item p a { color: var(--accent-t); text-decoration: underline; text-underline-offset: 2px; }

/* ---------- sticky mobile cta ---------- */
/* A phone visitor who has scrolled past the hero has lost the top CTA and
   won't scroll back up to find it — this stays pinned instead. Desktop
   already has the full closing section in easy reach, so this never shows
   there. */
.sticky-cta {
  display: none;
  position: fixed; inset-inline: 0; bottom: 0; z-index: 40;
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(10px);
  border-top: 1px solid var(--line);
}
.sticky-cta .btn { width: 100%; }
@media (max-width: 700px) {
  .sticky-cta { display: block; }
  /* Room for the bar so it never covers the footer's own links. */
  .foot { padding-bottom: 72px; }
}

/* ---------- close ---------- */

.cta-v2 {
  text-align: center; display: grid; gap: var(--sp-3); justify-items: center;
  padding: clamp(40px, 6vw, 68px) clamp(20px, 4vw, 48px);
  background: var(--concrete);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
.cta-v2 h2 { max-width: 18ch; }
.cta-v2 p { color: var(--muted); max-width: 48ch; font-size: 15.5px; }

/* ---------- footer ---------- */
footer { border-top: 1px solid var(--line); padding: 40px 0 52px; }
.foot-in { display: flex; flex-wrap: wrap; gap: 20px; justify-content: space-between; align-items: center; }
.foot-in p, .foot-in a { font-size: 13.5px; color: var(--faint); }
.foot-in a { text-decoration: none; }
.foot-in a:hover { color: var(--fg); }
.foot-links { display: flex; gap: 18px; flex-wrap: wrap; }
/* Card network marks. Paymob's online annex (3.2.2) requires the accepted
   payment brands to be shown clearly on the website. Each file is its own
   white badge, so it reads the same on the light and dark themes. */
.pay-logos { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.pay-logos img { display: block; height: 26px; width: auto; }
.legal-in .pay-logos { margin: 6px 0 4px; }

/* ---------- reveal ---------- */
/* Reveals run as an animation, not a transition, and that is structural rather
   than stylistic. A card both reveals on scroll and lifts on hover; `transition`
   is a single property, so whichever rule sits later in the file wins outright
   and the other effect vanishes. .rv used to land after .card and silently
   deleted the entire hover. Animations occupy a different channel, so the two
   coexist and neither has to know about the other.

   The easing matters too: the old curve was plain `ease` — ease-in-out — so
   every element crept for its first 150ms before committing. That hesitation
   is what made scrolling feel heavy. --ease-soft leaves at once and coasts. */
@keyframes rv-in {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: none; }
}

/* `both`, never `backwards` alone: the base state is opacity 0, so without a
   forwards fill the element snaps back to invisible the instant the animation
   finishes. */
.rv { opacity: 0; }
.rv.in { animation: rv-in var(--t-glide) var(--ease-soft) both; }

/* Children arrive in sequence rather than as one slab. --i is set inline by
   the reveal script; the cap stops a long list taking three seconds. `backwards`
   holds the from-state during the delay, otherwise each child flashes at full
   opacity before its turn. */
.rv-stagger > * { opacity: 0; }
.rv-stagger.in > * {
  animation: rv-in var(--t-glide) var(--ease-soft) both;
  animation-delay: calc(min(var(--i, 0) * 70ms, 420ms));
}
/* Reveals start invisible, so reduced-motion cannot merely disable the
   transition — it has to land them in the finished state or the content never
   appears at all. Stagger delays are cleared for the same reason. */
@media (prefers-reduced-motion: reduce) {
  .rv, .rv-stagger > * { opacity: 1; transform: none; animation: none; }
}


/* ==========================================================================
   Shared form + banner primitives.
   These live here, not in auth.css, because the dashboard pages use them too
   and only load styles.css + app-shell.css. Leaving them in auth.css meant an
   unstyled .banner rendered its icon at full size.
   ========================================================================== */

form { display: grid; gap: 15px; }

.f { display: grid; gap: 6px; }
.f-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.f label { font-size: 13.5px; font-weight: 600; color: var(--fg); }
.f-link { font-size: 12.5px; color: var(--accent-t); text-decoration: none; }
.f-link:hover { text-decoration: underline; }

.input-wrap { position: relative; display: flex; }
/* 16px is not a style choice. Safari on iOS zooms the whole page in when a
   focused field is under 16px, and then leaves it zoomed — so a 15px input
   means the layout lurches on every tap through a signup form. */
.f input {
  width: 100%;
  font-family: var(--body); font-size: 16px;
  padding: 11px 13px;
  min-height: 44px;
  color: var(--fg);
  background: var(--card);
  border: 1px solid var(--line-hi);
  border-radius: var(--r);
  transition: border-color var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.f input::placeholder { color: var(--faint); }
.f input:hover { border-color: var(--muted); }
.f input:focus {
  outline: none;
  border-color: var(--accent-t);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-t) 18%, transparent);
}
.f input[aria-invalid="true"] { border-color: var(--err); }
.f input[aria-invalid="true"]:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--err) 18%, transparent);
}
.f input[type="password"] { padding-inline-end: 44px; }

.peek {
  position: absolute; inset-block: 0; inset-inline-end: 0;
  width: 42px; display: grid; place-items: center;
  background: none; border: 0; cursor: pointer; color: var(--faint);
  border-radius: var(--r);
  transition: color var(--t) var(--ease), transform var(--t-fast) var(--ease);
}
.peek:hover { color: var(--fg); }
.peek:active { transform: scale(.88); transition-duration: 60ms; }
.peek svg { width: 17px; height: 17px; }

.err {
  font-size: 12.5px; color: var(--err);
  display: none; align-items: center; gap: 5px;
}
.err.on { display: flex; }
.err svg { width: 13px; height: 13px; flex: none; }

.hint { font-size: 12.5px; color: var(--faint); }

.check { display: flex; align-items: center; gap: 9px; }
.check label { font-size: 13.5px; color: var(--muted); cursor: pointer; }

/* Custom checkbox, everywhere. `accent-color` alone only recolours the
   checked fill — the unchecked box stays the browser's native white/grey
   square regardless of theme, which is exactly the "light in dark mode"
   look this replaces. appearance:none plus a hand-drawn check mark keeps it
   themed in both states. */
input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 16px; height: 16px; flex: none; margin: 0;
  border: 1.5px solid var(--line-hi); border-radius: 4px;
  background: var(--card); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--t) var(--ease), border-color var(--t) var(--ease);
}
input[type="checkbox"]:hover { border-color: var(--muted); }
input[type="checkbox"]:checked,
input[type="checkbox"]:indeterminate {
  background: var(--accent-t); border-color: var(--accent-t);
}
input[type="checkbox"]:checked::after {
  content: ""; width: 4px; height: 8px;
  border: solid #fff; border-width: 0 2px 2px 0;
  transform: rotate(45deg) translate(-1px, -1px);
}
input[type="checkbox"]:indeterminate::after {
  content: ""; width: 8px; height: 2px; background: #fff; border-radius: 1px;
}
input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent-t); outline-offset: 2px; }
input[type="checkbox"]:disabled { opacity: .5; cursor: default; }

.btn-full { width: 100%; min-height: 46px; }
.btn[aria-busy="true"] { pointer-events: none; opacity: .72; }

.spin {
  width: 15px; height: 15px; flex: none;
  border: 2px solid color-mix(in srgb, currentColor 30%, transparent);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: spin .6s linear infinite;
  display: none;
}
.btn[aria-busy="true"] .spin { display: block; }
.btn[aria-busy="true"] svg { display: none; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spin { animation-duration: 1.6s; } }

.divider {
  display: flex; align-items: center; gap: 13px;
  font-family: var(--mono); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--faint);
}
.divider::before, .divider::after {
  content: ""; height: 1px; background: var(--line); flex: 1;
}
:root[lang="ar"] .divider { letter-spacing: 0; }

.btn-oauth {
  width: 100%; min-height: 46px;
  background: var(--card); border: 1px solid var(--line-hi); color: var(--fg);
  font-weight: 500;
}
.btn-oauth:hover { border-color: var(--muted); background: var(--card); transform: none; }
.btn-oauth svg { width: 17px; height: 17px; }

.swap { font-size: 14px; color: var(--muted); text-align: center; }
.swap a { color: var(--accent-t); font-weight: 600; text-decoration: none; }
.swap a:hover { text-decoration: underline; }

.note {
  font-size: 12.5px; color: var(--faint); text-align: center;
  line-height: 1.5; max-width: 36ch; margin-inline: auto;
}
.note a { color: var(--muted); }

/* status banner (stub backend, errors, success) */
.banner {
  display: none; gap: 10px; align-items: flex-start;
  padding: 12px 14px; border-radius: var(--r);
  font-size: 13.5px; line-height: 1.5;
  border: 1px solid transparent;
}
.banner.on { display: flex; }
.banner svg { width: 16px; height: 16px; flex: none; margin-top: 2px; }
.banner.info { background: color-mix(in srgb, var(--accent-t) 10%, transparent); border-color: color-mix(in srgb, var(--accent-t) 30%, transparent); color: var(--fg); }
.banner.bad  { background: color-mix(in srgb, var(--err) 10%, transparent); border-color: color-mix(in srgb, var(--err) 30%, transparent); color: var(--fg); }
.banner.good { background: color-mix(in srgb, var(--ok) 12%, transparent); border-color: color-mix(in srgb, var(--ok) 32%, transparent); color: var(--fg); }
.banner.warn { background: color-mix(in srgb, var(--warn) 12%, transparent); border-color: color-mix(in srgb, var(--warn) 32%, transparent); color: var(--fg); }

/* select, matching the text inputs */
.f select {
  width: 100%; font-family: var(--body); font-size: 15px; min-height: 44px;
  padding: 10px 12px; border-radius: var(--r);
  border: 1px solid var(--line-hi); background: var(--card); color: var(--fg);
}
.f select:focus {
  outline: none; border-color: var(--accent-t);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-t) 18%, transparent);
}

/* ==========================================================================
   Polish layer.
   Everything above sets structure; this makes it feel built rather than
   assembled. One easing curve, real elevation, and a response to every
   interaction — that consistency is what actually reads as "smooth".
   ========================================================================== */

/* form controls pick up the new shape + depth */
.f input, .f select, .dlg-in input, .dlg-in select, #swapSearch, .reason-slot select {
  border-radius: var(--r);
  transition: border-color var(--t) var(--ease), box-shadow var(--t) var(--ease),
              background var(--t) var(--ease);
}
.f input:hover, .f select:hover { border-color: var(--muted); }

/* Everything that can be clicked says so on hover and answers on press. */
button, [role="button"], .tile, .alt, .swap-item, .size-chip, .choice, .res-opt, .item {
  -webkit-tap-highlight-color: transparent;
}
/* .card is deliberately absent: it declares its own longer, softer transition
   above, and this rule sits later in the file so it would silently win. */
.tile, .alt, .swap-item, .choice, .res-opt, .item, .kpi, .proof {
  transition:
    border-color var(--t) var(--ease),
    box-shadow   var(--t) var(--ease),
    background   var(--t) var(--ease),
    transform    var(--t-fast) var(--ease);
}
.tile:active, .alt:active, .swap-item:active, .choice:active, .res-opt:active {
  transform: scale(.99); transition-duration: 60ms;
}
/* `.item` and `.proof` are missing from that list on purpose, and it is the same
   cascade trap the comment above describes in reverse: both declare a :hover
   transform in a *later* sheet (portal.css, app-shell.css), so an :active
   transform written here is outranked the moment a mouse is also hovering. Their
   press states live beside their hover states instead. `.kpi` is a plain div
   with no click handler — it lifts on hover as polish and has nothing to press.

   This matters more than it looks: the rule above deletes the native tap
   highlight, so a control listed there without an :active is completely silent
   on touch. Anything added to that list needs a press state in the same pass. */

/* Rows: never `transition: all` — it animates layout and reads as lag. */
table.grid tbody tr {
  transition: background var(--t-fast) var(--ease);
}
table.grid tbody tr:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }
tr.rowlink:active { background: color-mix(in srgb, var(--fg) 7%, transparent); }


/* Theme swap now crossfades instead of hard-cutting: body's background/color
   transition above, plus each component's own already-authored transition
   (.nav's background, .ghost's border/color, box-shadow tokens, etc.) all
   fire together when data-theme flips. No more freeze-and-swap-behind-a-
   curtain — that hid the whole page for the swap and looked like a glitch
   instead of an animation.

   The one place a freeze still applies is boot: a returning dark-mode
   visitor's page must not paint light and visibly crossfade to dark on
   every load. i18n.js adds .theme-swapping only for that first, instant
   applyTheme() call — never for a click. */
:root.theme-swapping,
:root.theme-swapping *,
:root.theme-swapping *::before,
:root.theme-swapping *::after {
  transition: none !important;
  animation-duration: 0s !important;
}

/* Icon variant of the theme toggle — pages that only load styles.css
   (auth screens, the marketing page) still need this; app-shell.css and
   portal.css repeat it for the dashboard shell, which loads after this file
   and simply redeclares the same values. */
.icon-btn { display: grid; place-items: center; width: 34px; height: 34px; padding: 0; flex: none; }
.icon-btn svg { width: 17px; height: 17px; }
.i-moon { display: none; }
:root[data-theme="dark"] .i-sun  { display: none; }
:root[data-theme="dark"] .i-moon { display: block; }

/* ==========================================================================
   Dialog motion.

   Every dialog in the product is opened with showModal() and, until now, had
   no entrance at all — it appeared at full opacity, at full size, from
   nowhere. That is the one transition on screen with no continuity between
   the before and the after state, and it is the moment a page most needs one:
   a modal is a context switch, and an instant cut gives the eye nothing to
   follow to where the content went.

   It lives in styles.css rather than beside each dialog because app-shell.css
   and portal.css both declare .dlg but neither declares a `transition` on it,
   so there is nothing later in the cascade to outrank this.

   The mechanism is `allow-discrete`: a <dialog> is display:none when shut, and
   without it the exit is skipped entirely — the element is simply gone on the
   frame `close()` lands, so only the entrance would ever be visible. `overlay`
   is listed for the same reason, otherwise the top layer is dropped on frame
   one and the outgoing dialog animates *behind* the page.

   Scale plus a small rise, not a plain fade. A surface that arrives at final
   size reads as a picture of a dialog being cross-faded in; one that grows
   the last 3% reads as an object coming forward. No overshoot: --ease-pop is
   reserved for things a gesture threw, and a click carries no momentum to
   inherit — a bounce here is 100ms of wobble in front of a decision.
   ========================================================================== */
.dlg {
  opacity: 1;
  transform: scale(1) translateY(0);
  transition: opacity var(--t) var(--ease-out),
              transform var(--t) var(--ease-out),
              overlay var(--t) allow-discrete,
              display var(--t) allow-discrete;
}
.dlg:not([open]) { opacity: 0; transform: scale(.97) translateY(6px); }
/* Enter and exit share the curve and the offset, so the path in and the path
   out are the same line walked in opposite directions. */
@starting-style {
  .dlg[open] { opacity: 0; transform: scale(.97) translateY(6px); }
}

.dlg::backdrop {
  opacity: 1;
  transition: opacity var(--t) var(--ease-out),
              overlay var(--t) allow-discrete,
              display var(--t) allow-discrete;
}
.dlg:not([open])::backdrop { opacity: 0; }
@starting-style { .dlg[open]::backdrop { opacity: 0; } }

/* The global reduced-motion rule collapses these durations to .01ms, which
   lands both states instantly and is the correct outcome — the dialog still
   opens and closes, it just does not travel. allow-discrete stays in force,
   so nothing is left stranded in the top layer. */

/* ==========================================================================
   Accessibility preferences beyond reduced-motion.

   Two signals the OS exposes entirely independently of prefers-reduced-motion,
   and both were unhandled. They are not shades of the same setting: somebody
   can be fine with motion and still unable to read text sitting on a blur, or
   fine with translucency and still need more separation than a deliberately
   soft palette offers. Handling only reduced-motion covers neither.
   ========================================================================== */

/* ---- translucency off ----
   Surfaces go fully opaque rather than merely less blurred. A half-measure is
   the worst outcome available here: the text is still competing with whatever
   is scrolling underneath, just less obviously. The blur is dropped alongside
   the transparency because behind an opaque background it costs a compositor
   pass and buys nothing.

   .nav and .nav.stuck are both listed — .stuck raises the background to 92%
   on scroll, so setting only the base state leaves the scrolled bar
   translucent, which is the state it spends most of its life in. */
@media (prefers-reduced-transparency: reduce) {
  .nav, .nav.stuck { background: var(--bg); backdrop-filter: none; }
  .dlg::backdrop { background: rgba(0,0,0,.78); backdrop-filter: none; }
}

/* ---- more contrast ----
   The palette is low-contrast on purpose: tinted paper, near-black rather than
   black, hairline borders. That reasoning still stands as a default and is
   exactly why it needs an escape hatch rather than a defence — the people this
   setting exists for cannot opt into "restful".

   Both theme attributes are listed because :root[data-theme="light"] outranks
   a bare :root, so a token override here has to match its specificity or the
   explicit theme silently wins. */
@media (prefers-contrast: more) {
  :root, :root[data-theme="light"] {
    --bg:#FFFFFF; --card:#FFFFFF; --paper:#FFFFFF; --surface:#FFFFFF;
    --fg:#000000; --ink:#000000; --ink-2:#000000;
    --muted:#2E3B34; --faint:#414E47;
    --line:#5C6B64; --line-hi:#3A4842;
    --concrete:#E4EDE7;
    --accent:#1C4A38; --accent-t:#1C4A38;
    --brand:#1C4A38; --brand-lift:#1C4A38; --brand-deep:#123326;
    --ok:#04602D; --warn:#6B4207; --err:#97281F;
  }
  :root[data-theme="dark"] {
    --bg:#000000; --card:#0B100D; --paper:#000000; --surface:#0B100D;
    --fg:#FFFFFF;
    --muted:#D6DFDA; --faint:#BCC7C1;
    --line:#78877F; --line-hi:#98A69F;
    --concrete:#171F1A;
    --accent:#7FCBA9; --accent-t:#7FCBA9;
    --brand:#7FCBA9; --brand-lift:#7FCBA9;
    --ok:#5CEB92; --warn:#FFCE45; --err:#FF97A4;
  }

  /* A filled button hard-codes #fff on --accent. That clears AA in the light
     palette and does not in the dark one, where the accent has to be light
     enough to read as text on near-black. Dark ink on the light accent is the
     pairing that works in both directions. */
  :root[data-theme="dark"] .btn-primary { color: #04120B; }

  /* Shadow carries no information at this setting and is invisible against a
     hard white or a hard black regardless. Borders do the separating.

     Only the marketing surfaces are listed here, because they are the only
     ones whose shadow is declared in this file. app-shell.css and portal.css
     both re-declare box-shadow on .panel / .tile / .kpi / .dlg / .item and
     load later, so suppressing them from here does nothing — each sheet has
     to answer for its own surfaces at the bottom of the file. */
  .calc-v2, .shot-frame { box-shadow: none; border-color: var(--line-hi); }
  :where(a):focus-visible,
  :where(button):focus-visible,
  :where(input):focus-visible { outline-width: 3px; }
}

/* ==========================================================================
   2026-10 redesign — "lime on ink".
   Same token names, new values, so every page and component that already
   reads them follows without its own edit. Lime is a FILL colour only
   (--fill / --on-fill); as text it is unreadable on white, so the light
   theme's text accent (--accent / --brand) is a deep olive and dark mode
   promotes lime itself to text.

   Scoped to html:not([data-portal]): the customer-facing returns portal is
   painted in the merchant's own colours (portal.js), not Mortaga's.
   ========================================================================== */
html:not([data-portal]),
html:not([data-portal])[data-theme="light"] {
  --ink:#0F1511; --ink-2:#1B231E;
  --paper:#F3F5EF; --surface:#FFFFFF; --concrete:#E8ECE2;
  --bg:#F3F5EF; --fg:#0F1511; --card:#FFFFFF;
  --muted:#56625A; --faint:#6F7B73; --line:#E1E6DB; --line-hi:#CBD3C4;
  --brand:#2F6B54; --brand-lift:#3F8A6D; --brand-deep:#234F3F; --brand-glow:#63AC8E;
  --accent:#2F6B54; --accent-t:#234F3F;
  --ember:#C4561F; --ember-lift:#F0875A; --saffron:#C4561F; --saffron-hi:#F0875A;
  --ok:#0E8145; --warn:#A16814; --err:#C4453B;
  --fill:#2F6B54; --on-fill:#FFFFFF; --fill-glow:rgba(47,107,84,.2);
  --body:"Readex Pro","IBM Plex Sans Arabic","Segoe UI",system-ui,sans-serif;
  --r: 12px; --r-md: 18px; --r-lg: 24px;
  --sh-2: 0 1px 2px rgba(15,21,17,.04), 0 10px 26px -12px rgba(20,40,10,.16);
  --sh-3: 0 2px 6px rgba(15,21,17,.05), 0 28px 56px -20px rgba(20,40,10,.24);
}
html:not([data-portal])[data-theme="dark"] {
  --paper:#0A0C0B; --surface:#121715; --concrete:#1A201D;
  --bg:#0A0C0B; --fg:#EEF2EE; --card:#121715;
  --muted:#97A39C; --faint:#75817A; --line:#222A26; --line-hi:#313B36;
  --brand:#63AC8E; --brand-lift:#86C6AC; --brand-deep:#3F8A6D; --brand-glow:#86C6AC;
  --accent:#63AC8E; --accent-t:#86C6AC;
  --ember:#FF8A65; --ember-lift:#FFB199; --saffron:#FF8A65; --saffron-hi:#FFB199;
  --ok:#4ADE80; --warn:#FBBF24; --err:#FB7185;
  --fill:#63AC8E; --on-fill:#04120B; --fill-glow:rgba(99,172,142,.2);
  --sh-1: 0 1px 2px rgba(0,0,0,.5);
  --sh-2: 0 1px 0 rgba(255,255,255,.04) inset, 0 12px 28px -14px rgba(0,0,0,.8);
  --sh-3: 0 1px 0 rgba(255,255,255,.05) inset, 0 30px 60px -24px rgba(0,0,0,.9);
}
html:not([data-portal])[lang="ar"] { --disp: "Readex Pro", "IBM Plex Sans Arabic", system-ui, sans-serif; }

html:not([data-portal]) .btn-primary { background: var(--fill); color: var(--on-fill); border-color: transparent; }
html:not([data-portal]) .btn-primary:hover { background: var(--fill); box-shadow: 0 10px 30px -8px var(--fill-glow), 0 0 0 5px var(--fill-glow); }
html:not([data-portal])[data-theme="dark"] .btn-primary { color: var(--on-fill); }
html:not([data-portal]) ::selection { background: var(--fill); color: var(--on-fill); }
