/* app.css */
/* =========================================================
   أثر — Base
   Reset, bilingual typography, layout, utilities.
   Reads only layer-2 semantic tokens. Zero hardcoded colour.
   ========================================================= */
/* =========================================================
   أثر — Design Tokens
   ---------------------------------------------------------
   Three layers, in order. Components may ONLY read layer 2
   and 3. Never reference a layer-1 primitive outside this file.

     1. Primitives  --athar-surface-container   raw values
     2. Semantic    --color-surface             role, swaps per theme
     3. Component   defined in components/*.css

   Dark is the default: :root holds the dark values because the
   Athar logo is built for a dark ground, and because the design
   system is specified in dark. Light is an explicit opt-in via
   [data-theme="light"] — theme-init sets the attribute before
   first paint, so there is no flash.
   ========================================================= */

/* ---------------------------------------------------------
   LAYER 1a — The dark palette (Athar Commerce OS)

   The design system's own tokens, transcribed from the project's
   design file at the repository root.
   They are the authority for dark mode and nothing here is
   derived or eyeballed: if a value needs to change, it changes
   in the design file first and is copied across.

   The set is tonal rather than flat. Six ground steps carry the
   whole hierarchy — canvas, band, card, menu, and the two
   extremes — so a dashboard separates its panels by tone and not
   by drawing a border around everything.

   Every fill has an on-* partner: the ink that was measured
   against it. A fill used without its partner is how a bright
   teal button ends up with white text nobody can read.
   --------------------------------------------------------- */
:root {
  /* Grounds — container levels, deepest to brightest */
  --athar-surface-container-lowest: #0b1015;
  --athar-background: #0f1419;
  --athar-surface: #0f1419;
  --athar-surface-dim: #0f1419;
  --athar-surface-container-low: #171c21;
  --athar-surface-container: #1b2025;
  --athar-surface-container-high: #252a30;
  --athar-surface-container-highest: #30353b;
  --athar-surface-bright: #353a3f;
  --athar-surface-variant: #30353b;

  /* Ink on those grounds */
  --athar-on-background: #dee3ea;
  --athar-on-surface: #dee3ea;
  --athar-on-surface-variant: #bacac6;
  --athar-inverse-surface: #dee3ea;
  --athar-inverse-on-surface: #2c3136;

  /* Lines */
  --athar-outline: #859490;
  --athar-outline-variant: #3b4a47;

  /* Primary — the teal cyan of the mark. The operational core:
     conversion triggers, positive metrics, active navigation. */
  --athar-primary: #45e5d3;
  --athar-on-primary: #003732;
  --athar-primary-container: #00c9b7;
  --athar-on-primary-container: #004f47;
  --athar-inverse-primary: #006a60;
  --athar-surface-tint: #37ddca;
  --athar-primary-fixed: #5ffae6;
  --athar-primary-fixed-dim: #37ddca;
  --athar-on-primary-fixed: #00201c;
  --athar-on-primary-fixed-variant: #005048;

  /* Secondary — copper bronze. Tier badges, premium locks,
     revenue totals. Never a call to action. */
  --athar-secondary: #f8ba87;
  --athar-on-secondary: #4c2701;
  --athar-secondary-container: #6a3f17;
  --athar-on-secondary-container: #e8ac7a;
  --athar-secondary-fixed: #ffdcc2;
  --athar-secondary-fixed-dim: #f8ba87;
  --athar-on-secondary-fixed: #2e1500;
  --athar-on-secondary-fixed-variant: #673d15;

  /* Tertiary — deep cyan / petrol. Structural support: chart
     fills, category flags, hover gradients, secondary clusters. */
  --athar-tertiary: #84dbf6;
  --athar-on-tertiary: #003642;
  --athar-tertiary-container: #67bfda;
  --athar-on-tertiary-container: #004c5d;
  --athar-tertiary-fixed: #b3ebff;
  --athar-tertiary-fixed-dim: #7cd2ee;
  --athar-on-tertiary-fixed: #001f27;
  --athar-on-tertiary-fixed-variant: #004e5f;

  /* Error */
  --athar-error: #ffb4ab;
  --athar-on-error: #690005;
  --athar-error-container: #93000a;
  --athar-on-error-container: #ffdad6;
}

/* ---------------------------------------------------------
   LAYER 1b — Brand ramps

   The palette above is specified for dark only. Light mode is
   still the same brand, so the three brand hues are also written
   as ramps: the same colours mixed toward white and toward the
   ground, with the deep steps chosen so ink on paper clears AA
   rather than merely looking right.

     teal   the primary, anchored on #00c9b7
     cyan   the tertiary petrol, anchored on #1b829b
     brass  the secondary copper, anchored on #d49a6a

   -500 is the brand value in each. Steps above it are for dark
   grounds, steps below for paper.
   --------------------------------------------------------- */
:root {
  --athar-teal-200: #a8f2e9;
  --athar-teal-300: #5ce6d7;
  --athar-teal-400: #22d8c6;
  --athar-teal-500: #00c9b7;
  --athar-teal-600: #00a898;
  --athar-teal-700: #0a7d73;
  --athar-teal-800: #0a5c55;

  --athar-cyan-200: #b7e0ea;
  --athar-cyan-300: #7ec8da;
  --athar-cyan-400: #2ba3be;
  --athar-cyan-500: #1b829b;
  --athar-cyan-600: #136a80;
  --athar-cyan-700: #0e5265;
  --athar-cyan-800: #0a3a49;

  --athar-brass-200: #f0d5bc;
  --athar-brass-300: #e6bd97;
  --athar-brass-500: #d49a6a;
  --athar-brass-600: #ac7a4f;
  /* The reading step. A diamond mark and the word beside it do not
     need the same value: one is a shape, the other has to clear 4.5
     on all three paper grounds, the sunken one included. */
  --athar-brass-700: #7d5535;

  /* Paper — the light ground. A clean cool slate, a shade off white.
     50 is the container and sidebar surface, 100 the page canvas. */
  --athar-paper-50:  #ffffff;
  --athar-paper-100: #f8fafc;
  --athar-paper-200: #eaecef;
  --athar-paper-300: #dde0e4;
  --athar-paper-400: #c2c7ce;

  --athar-neutral-100: #e8eaec;
  --athar-neutral-300: #aab1b8;
  --athar-neutral-500: #79818a;
  --athar-neutral-700: #4a525b;
  --athar-neutral-900: #171a1d;

  /* Status outside the palette.

     Success is the primary teal by design — a finished task and a
     rising trendline are the same signal. Error is the palette's
     own. Warning is the one hue the system does not ship: amber
     sits a step off the copper secondary on purpose, because a
     warning that looks like a premium badge is a warning nobody
     reads. */
  --athar-amber-300: #f5c86b;
  --athar-amber-600: #a2681c;
  --athar-red-600: #ba1a1a;
}

/* ---------------------------------------------------------
   LAYER 1c — Type, space, shape, motion
   --------------------------------------------------------- */
:root {
  /* ---- Typography primitives ----
     Almarai everywhere — Arabic and Latin out of the same file,
     both drawn by Boutros, so a bilingual line does not change
     voice halfway across.

     It replaced IBM Plex Sans Arabic on the storefront and Cairo
     on the dashboard (owner's instruction, 2026-09-09). Plex and
     Cairo are the two faces every generated Arabic template
     reaches for first, and the platform had one of each.

     Almarai ships four cuts and no more: 300, 400, 700, 800. The
     weight tokens below name those and nothing else. A browser
     asked for 600 in a family that has none picks 700 by itself;
     writing 700 here only says out loud what was going to happen,
     and leaves 800 free for the one step above bold.

     --font-figures is the one thing Almarai cannot do, and it was
     measured rather than assumed. Its digits are proportional and
     it carries no tnum feature at all: at 40px the 1 is 14.20px
     wide against the 8 at 21.24px, and setting either
     font-variant-numeric: tabular-nums or "tnum" 1 moves the
     string not one hundredth of a pixel (measured in Chrome on
     the running app, 2026-09-09). A right-aligned column of
     riyals in that face does not line up — the decimal wanders by
     a digit-width per row, which is exactly the thing a merchant
     scanning revenue needs it not to do.

     So figures are set in the platform's monospace instead: every
     rule that already paired --font-latin with tabular-nums now
     reads this token, and that pairing was already the codebase's
     marker for "this is a number to be compared".

     Neither this nor --font-mono downloads anything. A second
     webfont for the sake of the digits is a network round trip
     and a second voice on the page; the system monospace is on
     every machine and its digits are equal-width by definition. */
  --font-display-ar: "Almarai", "Segoe UI", Tahoma, sans-serif;
  --font-body-ar: "Almarai", "Segoe UI", Tahoma, sans-serif;
  --font-latin: "Almarai", "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
  --font-figures: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, monospace;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, monospace;


  --fw-regular: 400;
  --fw-medium: 400;
  --fw-semibold: 700;
  --fw-bold: 700;
  --fw-black: 800;

  /* Fluid scale, matched to the design system's ramp. Each clamp
     runs from the mobile size to the desktop one:

       display-lg   34 -> 48    --fs-h1
       headline-xl  26 -> 32    --fs-h2
       headline-md  22          --fs-h3 / --fs-xl
       title-lg     18          --fs-h4 / --fs-lg
       body-lg      16          --fs-base
       body-md      14          --fs-sm
       label-md     13          --fs-xs
       label-sm     11          --fs-eyebrow */
  --fs-eyebrow: 0.6875rem;
  --fs-xs: 0.8125rem;
  --fs-sm: 0.875rem;
  --fs-base: 1rem;
  --fs-lg: 1.125rem;
  --fs-xl: 1.375rem;
  --fs-h4: 1.125rem;
  --fs-h3: clamp(1.25rem, 1.05rem + 0.7vw, 1.375rem);
  --fs-h2: clamp(1.625rem, 1.28rem + 1.5vw, 2rem);
  --fs-h1: clamp(2.125rem, 1.55rem + 2.5vw, 3rem);


  /* Generous by RTL necessity, not by taste: Arabic diacritics and
     ligatures clip against a tight baseline, so body sits at 1.6
     rather than the 1.45 a Latin-only system would take. */
  --lh-tight: 1.25;
  --lh-heading: 1.32;
  --lh-snug: 1.45;
  --lh-normal: 1.6;
  --lh-relaxed: 1.9;

  --tracking-tight: -0.02em;
  --tracking-normal: 0;
  --tracking-wide: 0.06em;
  --tracking-eyebrow: 0.04em;

  /* ---- Space — 8px architecture, 4px sub-rhythm ---- */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-20: 5rem;
  --space-24: 6rem;
  --space-32: 8rem;
  --space-48: 12rem; /* 192px — the largest step. Nothing on the
                        marketing pages spends it since the section
                        padding came back down to --space-24. */

  /* The same scale under the design system's own names, so a spec
     that says space-lg and a stylesheet that says --space-6 are
     provably the same measurement. */
  --space-xxs: var(--space-1);
  --space-xs: var(--space-2);
  --space-sm: var(--space-3);
  --space-md: var(--space-4);
  --space-lg: var(--space-6);
  --space-xl: var(--space-8);
  --space-2xl: var(--space-12);
  --space-3xl: var(--space-16);

  --grid-margin-desktop: 2rem;
  --grid-margin-mobile: 1rem;
  --grid-gutter: 1.25rem;

  /* ---- Density ----
     The space scale answers "how far apart", and every panel was
     reaching into it for the same answer: --space-6 inside a card,
     --space-4 when the card was in a table. Two numbers doing one
     job, chosen per file, is how a dashboard ends up with cards
     that breathe differently on adjacent screens.

     These three name the job instead. A panel is the unit a
     high-end shop is built out of — a card, a form well, a summary
     box — and it has one resting inside measure, one for a phone or
     a dense table, and one for the panel a page is built around. */
  --space-panel: 1.75rem;
  --space-panel-tight: 1.125rem;
  --space-panel-loose: 2.5rem;

  /* ---- Radius ----
     Rounded, not pill. Buttons, inputs and dropdowns hold 8px —
     corporate gravitas rather than a chat app. Cards and modules
     scale to 16px. Full radii are spent only on live status pills,
     which is what tells a status apart from a click target. */
  --radius-xs: 0.25rem;
  --radius-sm: 0.5rem;
  --radius-md: 0.75rem;
  --radius-lg: 1rem;
  --radius-xl: 1.5rem;
  --radius-pill: 9999px;
  --radius-premium: 1rem;

  /* ---- Motion ---- */
  --dur-fast: 160ms;
  --dur-base: 240ms;
  --dur-slow: 320ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);

  /* Indeterminate motion — a spinner and a progress bar that do not
     know how long they have. Linear, because an eased spinner reads
     as stuttering, and slow enough that it is not a distraction next
     to a form somebody is still reading. */
  --dur-spin: 760ms;
  --dur-crawl: 1400ms;
  --ease-linear: linear;
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

  /* ---- Layout ---- */
  --container-max: 1180px;
  --container-wide: 1400px;
  --header-height: 72px;
  --rail-width: 56px;

  /* ---- Grain ----
     A single tile of film grain, laid over the whole page at very
     low opacity. Flat digital colour is the tell of a generated
     interface; a little noise gives the surface a texture the eye
     reads as printed. */
  --texture-grain: 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.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
}

/* ---------------------------------------------------------
   LAYER 2 — Semantic · DARK (default)

   Where the palette becomes roles. Two rules hold the whole
   thing together.

   **Hierarchy is carried by the container levels, not by
   borders.** canvas -> band -> surface -> raised -> highest is
   five steps of one ground, and a panel is told apart from the
   page by the step it sits on. Lines are a refinement on top of
   that, never the thing doing the work.

   **Every fill names its own ink.** --color-primary is a bright
   teal and white on it is 1.6:1, unreadable. So --color-on-primary
   exists and is the only ink allowed on a primary fill. Same for
   secondary, tertiary, success and error. --color-text-on-brand
   stays white and now means one thing only: ink for a scrim, a
   photograph, or a fill dark enough to take it.
   --------------------------------------------------------- */
:root,
:root.dark,
:root[data-theme="dark"] {
  /* ---- Grounds ---- */
  --color-canvas: var(--athar-background);
  --color-canvas-deep: var(--athar-surface-container-lowest);
  --color-surface: var(--athar-surface-container);
  --color-surface-raised: var(--athar-surface-container-high);
  --color-surface-sunken: var(--athar-surface-container-lowest);

  /* One step above raised: a hovered row, a tooltip, the thing that
     has to sit on top of something already lifted. */
  --color-surface-highest: var(--athar-surface-container-highest);

  --color-line: var(--athar-outline-variant);
  --color-line-strong: var(--athar-outline);
  --color-line-gold: rgba(248, 186, 135, 0.34);

  /* ---- Ink ---- */
  --color-text: var(--athar-on-surface);
  --color-text-secondary: var(--athar-on-surface-variant);
  --color-text-muted: var(--athar-outline);
  --color-text-on-brand: #ffffff;

  /* ---- The on-* family ----
     One per fill, each measured against the fill it names:
     on-primary clears 8.4:1 on --color-primary, on-secondary 11.5:1
     on --color-gold, on-error 7.9:1 on --color-danger. */
  --color-on-primary: var(--athar-on-primary);
  --color-on-secondary: var(--athar-on-secondary);
  --color-on-tertiary: var(--athar-on-tertiary);
  --color-on-success: var(--athar-on-primary);
  --color-on-warning: var(--athar-on-secondary);
  --color-on-error: var(--athar-on-error);

  /* ---- Primary — the operational core ---- */
  --color-primary: var(--athar-primary);
  --color-primary-hover: var(--athar-primary-fixed);
  --color-primary-soft: rgba(0, 201, 183, 0.14);
  --color-primary-text: var(--athar-primary);

  /* The deep end of the brand, for a surface that carries brand
     colour without asking to be clicked: an avatar, a chat bubble
     the visitor wrote. Dark enough to take white. */
  --color-primary-deep: var(--athar-inverse-primary);
  --color-text-on-deep: #ffffff;

  /* ---- Growth — a rising number, a positive delta ---- */
  --color-growth: var(--athar-primary-container);
  --color-growth-soft: rgba(0, 201, 183, 0.14);
  --color-growth-text: var(--athar-primary);

  /* ---- Premium — the copper. Tier badges and nothing else. ---- */
  --color-gold: var(--athar-secondary);
  --color-gold-soft: rgba(248, 186, 135, 0.14);
  --color-gold-text: var(--athar-secondary);

  /* ---- Entry tier — the petrol tertiary ----
     The plan row wears the artwork: petrol for the tier you start
     on, teal for the one most stores end on, copper for the top. */
  --color-entry: var(--athar-tertiary-container);
  --color-entry-soft: rgba(132, 219, 246, 0.14);
  --color-entry-text: var(--athar-tertiary);
  --color-line-entry: rgba(132, 219, 246, 0.34);

  /* ---- Status ---- */
  --color-success: var(--athar-primary-container);
  --color-success-soft: rgba(0, 201, 183, 0.14);
  --color-warning: var(--athar-amber-300);
  --color-warning-soft: rgba(245, 200, 107, 0.16);
  --color-danger: var(--athar-error);
  --color-danger-soft: rgba(255, 180, 171, 0.16);

  /* ---- Delta — the two directions a KPI can move ----
     Status colours answer "did something break". A delta answers
     "is this up or down", and the top of the dashboard reads faster
     when the two questions do not share a palette. Emerald sits far
     enough off the brand teal that a rising figure is not read as a
     link; coral is warmer than --color-danger, which stays reserved
     for something that actually went wrong. */
  --color-delta-up: #10b981;
  --color-delta-up-soft: rgba(16, 185, 129, 0.1);
  --color-delta-down: #f43f5e;
  --color-delta-down-soft: rgba(244, 63, 94, 0.1);

  /* ---- Chart ----
     The lead series is electric violet #8b5cf6 by the owner's
     specification, and it is written here as well as in chart.js
     because the CSS bars the panel falls back to have to be the same
     colour as the line they stand in for. The petrol second series
     is still read from here at draw time. */
  --color-chart-primary: #8b5cf6;
  --color-chart-petrol: var(--athar-tertiary);
  --color-chart-glow: rgba(139, 92, 246, 0.55);

  --color-focus: var(--athar-primary-container);

  /* Scrim behind the mobile drawer. Not black: the canvas dimmed,
     so the page reads as pushed back rather than switched off. */
  --color-scrim: rgba(6, 10, 13, 0.66);

  /* ---- Hero wash ----
     The three stops a page's opening section is lit with. Kept here
     rather than in a page file because every marketing page opens
     the same way and they must not drift apart. Deliberately weaker
     than it looks written down: on a near-black ground even 10% of
     a saturated teal reads as a coloured panel. */
  --hero-wash-brand: rgba(0, 201, 183, 0.10);
  --hero-wash-deep: rgba(0, 106, 96, 0.30);
  --hero-wash-top: rgba(255, 255, 255, 0.02);

  /* ---- The alternating band ----
     A long page is a dozen sections of the same shape, and on one
     flat canvas they read as a single column with headings in it.
     This is the ground that breaks the run — one container step, no
     border. A band that announces itself competes with the content
     it is separating, which is the opposite of the job. */
  --color-canvas-band: var(--athar-surface-container-low);

  /* The anchor — the third ground, and the one that gives the page a
     pulse. The band above is a change of light; this is a change of
     room. It is the palette's deep petrol rather than more graphite,
     so a section wearing it reads as brand rather than as a darker
     rectangle, and it holds the same value in both themes. Spend it
     on two or three sections. A fourth turns the rhythm back into a
     texture. */
  --color-canvas-anchor: var(--athar-on-tertiary);
  --color-anchor-line: rgba(132, 219, 246, 0.18);

  --grain-opacity: 0.05;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.42);
  --shadow-lg: 0 24px 64px rgba(0, 0, 0, 0.5);
  --shadow-glow: 0 0 0 1px rgba(248, 186, 135, 0.16), 0 20px 56px rgba(0, 0, 0, 0.45);

  /* The ink a shadow is cast in, and how much of it this ground can
     take. Dark casts in black, because the canvas is already close
     to it; paper casts in the neutral the text is set in, because a
     black shadow on warm paper reads as a hole.

     --shadow-soft and --shadow-deep below are written once, off
     these two, so the light theme restates two values instead of
     eight — and so a shadow can never drift between themes. */
  --shadow-ink: 0 0 0;
  --shadow-strength: 1;

  /* Depth here is chromatic, not opaque: the ground does the lifting
     and these two put light on the edges. The rim is the inner
     highlight a lit card catches along its top edge; the bloom is
     the diffused teal a primary control throws under the cursor.
     Both are off by default and spent deliberately. */
  --rim-light: inset 0 1px 0 0 rgba(0, 201, 183, 0.25);
  --shadow-bloom: 0 0 20px rgba(0, 201, 183, 0.35);

  /* A focus ring, as a shadow rather than an outline: it follows the
     border-radius, and it can sit next to a shadow in the same
     property without either one clipping the other. */
  --color-focus-soft: rgba(0, 201, 183, 0.2);

  color-scheme: dark;
}

/* ---------------------------------------------------------
   LAYER 2 — Depth

   Two elevations, written once and tinted by the theme.

   Real depth is two things at once: a tight contact shadow that
   says the surface is touching the page, and a wide ambient one
   that says there is air under it. Stacked, they read as a lifted
   object; either alone reads as a filter.

   --shadow-soft  a panel at rest
   --shadow-deep  the same panel picked up: a hover, a menu, a dialog
   --------------------------------------------------------- */
:root {
  --shadow-soft:
    0 1px 2px rgb(var(--shadow-ink) / calc(0.22 * var(--shadow-strength))),
    0 4px 10px rgb(var(--shadow-ink) / calc(0.16 * var(--shadow-strength))),
    0 12px 28px rgb(var(--shadow-ink) / calc(0.14 * var(--shadow-strength)));

  --shadow-deep:
    0 2px 4px rgb(var(--shadow-ink) / calc(0.26 * var(--shadow-strength))),
    0 10px 24px rgb(var(--shadow-ink) / calc(0.26 * var(--shadow-strength))),
    0 28px 64px rgb(var(--shadow-ink) / calc(0.3 * var(--shadow-strength)));

  --ring-focus: 0 0 0 3px var(--color-focus-soft);

  /* ---- Gradients ----

     --gradient-sheen is light, not colour: a highlight along the top
     edge and a shade along the bottom, as if the surface had a
     thickness. White and black alpha only, so it lays over any fill
     — primary, copper, a merchant's own shop colour — without
     touching that fill's contrast against its label. It is an
     overlay, always the first layer of a two-layer background.

     --gradient-primary is the filled CTA. It runs primary into the
     brand teal and stops there: the design file's petrol end lands
     at 4.39:1 against --color-on-primary, a hair under AA at the
     14px bold a button is set in, and a gradient whose far corner
     fails is a gradient that fails. The petrol still gets its run in
     --gradient-brand below, which carries no small text. */
  --gradient-sheen: linear-gradient(
    180deg,
    rgb(255 255 255 / 0.16) 0%,
    rgb(255 255 255 / 0.03) 46%,
    rgb(0 0 0 / 0.07) 100%
  );

  --gradient-primary: linear-gradient(
    165deg,
    var(--color-primary-hover) 0%,
    var(--color-primary) 46%,
    var(--color-growth) 100%
  );

  --gradient-gold: linear-gradient(
    165deg,
    var(--athar-secondary-fixed) 0%,
    var(--athar-secondary) 100%
  );

  /* The emblem's own ramp — teal down to petrol — spent on the trace
     rail, the mark, and one word per page. Put it on a button as
     well and it stops being a signature. */
  --gradient-brand: linear-gradient(
    180deg,
    var(--athar-primary) 0%,
    var(--athar-primary-container) 46%,
    var(--athar-cyan-500) 100%
  );
  --gradient-trace: var(--gradient-brand);
  --gradient-word: linear-gradient(
    100deg,
    var(--athar-primary-fixed) 0%,
    var(--athar-primary-container) 50%,
    var(--athar-tertiary) 100%
  );

  /* A panel lit from above: the raised surface at the top edge
     falling to the flat one. Two steps of the same neutral, so it
     reads as light rather than as colour. */
  --gradient-surface: linear-gradient(
    180deg,
    var(--color-surface-raised) 0%,
    var(--color-surface) 100%
  );
}

/* ---------------------------------------------------------
   LAYER 2 — Semantic · LIGHT (explicit opt-in)

   The design system is specified for dark. Light is the same
   brand rebuilt on paper: the brand ramps from layer 1b taken at
   their deep steps, because a colour that reads as luminous on a
   #0f1419 ground reads as a highlighter on white.

   Every on-* token is restated. On paper the brand fills are dark
   and the ink is white — the exact inverse of the rule above —
   and a token left unset here would silently put dark ink on a
   dark fill.
   --------------------------------------------------------- */
:root[data-theme="light"] {
  --color-canvas: var(--athar-paper-100);
  --color-canvas-deep: var(--athar-paper-200);
  --color-surface: var(--athar-paper-50);
  --color-surface-raised: var(--athar-paper-50);
  --color-surface-sunken: var(--athar-paper-200);
  --color-surface-highest: var(--athar-paper-50);

  --color-line: var(--athar-paper-300);
  --color-line-strong: var(--athar-paper-400);
  --color-line-gold: rgba(172, 122, 79, 0.42);

  --color-text: var(--athar-neutral-900);
  --color-text-secondary: var(--athar-neutral-700);
  --color-text-muted: var(--athar-neutral-500);
  --color-text-on-brand: #ffffff;

  /* Inverted from dark: the fills are deep here, so the ink is white. */
  --color-on-primary: #ffffff;
  --color-on-secondary: #ffffff;
  --color-on-tertiary: #ffffff;
  --color-on-success: #ffffff;
  --color-on-warning: #ffffff;
  --color-on-error: #ffffff;

  --color-primary: var(--athar-teal-700);
  --color-primary-hover: var(--athar-teal-800);
  --color-primary-soft: rgba(0, 201, 183, 0.12);
  --color-primary-text: var(--athar-teal-800);

  --color-primary-deep: var(--athar-cyan-700);
  --color-text-on-deep: #ffffff;

  --color-growth: var(--athar-teal-700);
  --color-growth-soft: rgba(0, 201, 183, 0.12);
  --color-growth-text: var(--athar-teal-800);

  --color-gold: var(--athar-brass-600);
  --color-gold-soft: rgba(212, 154, 106, 0.18);
  /* Darker than the fill above it, the same way growth-text is darker
     than growth. A diamond mark and the word beside it do not need
     the same value: one is a shape and the other has to be read. */
  --color-gold-text: var(--athar-brass-700);

  --color-entry: var(--athar-cyan-600);
  --color-entry-soft: rgba(27, 130, 155, 0.10);
  --color-entry-text: var(--athar-cyan-600);
  --color-line-entry: rgba(27, 130, 155, 0.34);

  --color-success: var(--athar-teal-700);
  --color-success-soft: rgba(0, 201, 183, 0.12);
  --color-warning: var(--athar-amber-600);
  --color-warning-soft: rgba(162, 104, 28, 0.12);
  --color-danger: var(--athar-red-600);
  --color-danger-soft: rgba(186, 26, 26, 0.10);

  /* Delta on paper. The specified pair is tuned for a #111827 card and
     neither colour survives the trip to white — #10b981 reads 2.5:1
     there and #f43f5e 3.7:1, both under the 4.5:1 that eyebrow-sized
     text needs. Same two hues, taken to the depth paper wants, with
     the vibrance left in the tint behind them. */
  --color-delta-up: #047857;
  --color-delta-up-soft: rgba(4, 120, 87, 0.1);
  --color-delta-down: #be123c;
  --color-delta-down-soft: rgba(190, 18, 60, 0.1);

  --color-chart-primary: #6d28d9;
  --color-chart-petrol: var(--athar-cyan-600);
  --color-chart-glow: rgba(109, 40, 217, 0.28);

  --color-focus: var(--athar-teal-700);

  --color-scrim: rgba(23, 26, 29, 0.42);

  /* Paper takes the wash the other way round: the tint has to sit
     above the canvas rather than below it, so these are lighter and
     the top stop is a real colour instead of a white lift. */
  --hero-wash-brand: rgba(0, 201, 183, 0.10);
  --hero-wash-deep: rgba(27, 130, 155, 0.14);
  --hero-wash-top: rgba(255, 255, 255, 0.55);

  /* Paper's band. Cooler than the canvas rather than darker, so the
     step reads as a change of light and not as a grey rectangle. */
  --color-canvas-band: #f1f4f8;

  /* Same anchor, deliberately unchanged. A dark section on paper is
     the strongest move the page has; making it lighter here would
     spend the move and buy nothing. */
  --color-canvas-anchor: var(--athar-on-tertiary);
  --color-anchor-line: rgba(132, 219, 246, 0.20);

  --grain-opacity: 0.03;

  --shadow-sm: 0 1px 2px rgba(23, 26, 29, 0.06);
  --shadow-md: 0 8px 24px rgba(23, 26, 29, 0.07);
  --shadow-lg: 0 24px 64px rgba(23, 26, 29, 0.09);
  --shadow-glow: 0 0 0 1px rgba(172, 122, 79, 0.22), 0 20px 48px rgba(23, 26, 29, 0.08);

  /* Paper takes about a quarter of the ink the dark ground does, and
     takes it in the neutral the text is set in rather than in black.
     --shadow-soft and --shadow-deep are not restated: they read
     these two and re-tint themselves. */
  --shadow-ink: 23 26 29;
  --shadow-strength: 0.3;

  /* The rim and the bloom are dark-mode effects. Neon edge light on
     paper reads as a rendering artefact, so both are off. */
  --rim-light: none;
  --shadow-bloom: none;

  --color-focus-soft: rgba(10, 125, 115, 0.24);

  color-scheme: light;
}


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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  position: relative;
  margin: 0;
  background: var(--color-canvas);
  color: var(--color-text);
  font-family: var(--font-body-ar);
  font-size: var(--fs-base);
  line-height: var(--lh-normal);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Film grain over the whole page. Perfectly flat digital colour is
   the tell of a generated interface; a little tooth makes the
   surface read as printed. Never interactive, never in print. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  background-image: var(--texture-grain);
  opacity: var(--grain-opacity);
  mix-blend-mode: overlay;
}
@media print {
  body::after { display: none; }
}

img,
svg,
video { max-width: 100%; display: block; }

button,
input,
select,
textarea { font: inherit; color: inherit; }

ul, ol { margin: 0; padding: 0; list-style: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------------------------------------------------------
   Typography — bilingual
   Arabic and Latin do not share a rhythm. Latin display text
   gets negative tracking to close its wide sidebearings;
   Arabic gets none, because letter-spacing breaks the joins
   between glyphs. Every rule below is split on that fact.
   --------------------------------------------------------- */
:root[data-lang="en"] body { font-family: var(--font-latin); }

h1, h2, h3, h4, h5 {
  margin: 0 0 var(--space-4);
  font-family: var(--font-display-ar);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  color: var(--color-text);
}

:root[data-lang="en"] h1,
:root[data-lang="en"] h2,
:root[data-lang="en"] h3,
:root[data-lang="en"] h4,
:root[data-lang="en"] h5 {
  font-family: var(--font-latin);
  letter-spacing: var(--tracking-tight);
}

h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); line-height: var(--lh-snug); }
h4 { font-size: var(--fs-h4); font-weight: var(--fw-bold); line-height: var(--lh-snug); }

p {
  margin: 0 0 var(--space-4);
  color: var(--color-text-secondary);
}

a {
  color: var(--color-primary-text);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
a:hover { color: var(--color-growth-text); }

strong { font-weight: var(--fw-bold); color: var(--color-text); }

code {
  font-family: var(--font-mono);
  font-size: 0.875em;
  letter-spacing: 0;
}

/* Numerals stay Latin and tabular in both languages so figures in
   tables and KPI tiles align on the decimal. */
.u-figure {
  font-family: var(--font-figures);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  letter-spacing: var(--tracking-tight);
}

/* Emphasis inside a headline. Brass text over a brass rule, the way
   a copy editor would mark a word — not a gradient fill. The full
   logo ramp stays reserved for the trace rail and the wordmark. */
.u-word-mark {
  color: var(--color-gold-text);
  text-decoration: underline;
  text-decoration-color: var(--color-line-gold);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.2em;
  text-decoration-skip-ink: none;
}

/* ---------------------------------------------------------
   Eyebrow
   A section label, not decoration: it names the section for
   screen readers and for scanning. Latin gets caps + tracking;
   Arabic has no case, so it earns its rank from the gold node
   instead of from letterforms it does not have.
   --------------------------------------------------------- */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0 0 var(--space-4);
  font-family: var(--font-body-ar);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-bold);
  color: var(--color-gold-text);
}
.eyebrow::before {
  content: "";
  width: 5px;
  height: 5px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--color-gold);
}
:root[data-lang="en"] .eyebrow {
  font-family: var(--font-latin);
  text-transform: uppercase;
  letter-spacing: var(--tracking-eyebrow);
}

/* A gold hairline. The only decorative rule in the system, and
   only where a real boundary exists. */
.hairline {
  height: 1px;
  border: 0;
  margin: var(--space-10) 0;
  background: linear-gradient(
    to left,
    transparent,
    var(--color-line-gold) 20%,
    var(--color-line-gold) 80%,
    transparent
  );
}

/* ---------------------------------------------------------
   Layout
   --------------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-6);
}

/* One beat, and the ground carries the emphasis.

   Band and anchor used to take --space-32 while the canvas took
   --space-24, on the theory that a tinted section should also breathe
   wider. What that bought was arithmetic: three different silences for
   the same event, and no reader can hear a rhythm in that. So every
   full section is one number — .section, .section--band and
   .section--anchor all resolve to the same padding, and a tinted
   section says "this is a different thing" with its ground alone.

   That number is 96px (owner's spec, 2026-09-08). It was raised to
   192px on 2026-09-06 and measured badly: the padding came to about
   half the height of every section on the page — 384px of seam around
   a 300px card in the maker's note — so the page read as air with
   content in it. 96px is where it was before that, and the number the
   rest of this comment was written about.

   **The seam is twice the number.** Vertical padding does not collapse
   between siblings that carry a background, so the space a reader sees
   between two sections is the sum of the two that meet: 96px each read
   as a 192px seam, and 192px each reads as 384px. 192px of seam is a
   fifth of a 900px viewport, which reads as a break; 384px was a third
   of it, which reads as the page having ended. The phone step below
   exists for the same reason, one size down.

   The strip keeps half the section's value, as it always has. */
.section { padding-block: var(--space-24); }
.section--tight { padding-block: var(--space-16); }

/* The thin band — a marquee, a row of facts. It wears the same tint
   as a full band but is punctuation, not a section, so it takes the
   short step and never the section one. Before this existed the three
   strips on the home page carried 18px, 0px and 32px, all of them
   chosen by hand and none of them the same. */
.section--strip { padding-block: var(--space-12); }

/* Below the tablet break every step drops one rung: 96·64·48 becomes
   64·48·32. Remove this block to run the desktop ladder at every
   width. */
@media (max-width: 900px) {
  .section { padding-block: var(--space-16); }
  .section--tight { padding-block: var(--space-12); }
  .section--strip { padding-block: var(--space-8); }
}

/* Two sections on the same ground read as one long column, and the
   home page had two such seams. The tint separates wherever the
   ground changes; where it does not change, a hairline does — which
   is the complement of the rule below, not a contradiction of it. */
.section + .section:not(.section--band):not(.section--anchor),
.section--band + .section--band {
  border-block-start: 1px solid var(--color-line);
}
.section--anchor + .section:not(.section--band):not(.section--anchor),
.section--band + .section:not(.section--band):not(.section--anchor) {
  border-block-start: 0;
}

/* The alternating ground. No border: the tint is the separation, and
   a hairline on top of it turns one soft step into two hard ones. */
.section--band {
  background: var(--color-canvas-band);
}

/* The anchor — a section that changes the room rather than the light.
   It is dark in both themes, so it restates the local colour tokens
   instead of asking every component inside it to know where it is:
   a card, a heading and a muted caption keep their own rules and
   simply resolve against a different ground.

   Two per page, and far apart. Three was the old budget and the home
   page spent all three inside five sections of each other, with a
   single canvas section between each — which is how the loudest move
   the layout has turned into a flicker. */
.section--anchor {
  background: var(--color-canvas-anchor);

  --color-text: #eef4f6;
  --color-text-secondary: rgba(216, 232, 237, 0.76);
  /* 0.62, not the 0.56 this started at: on this ground that lands at
     4.39:1, which is under AA for anything not set large. 0.62 clears
     it at 5.06:1 and is still visibly the quietest text here. */
  --color-text-muted: rgba(199, 219, 226, 0.62);

  /* Surfaces lift out of the anchor, never off the page palette —
     a graphite card on a teal-navy ground reads as a mistake. The
     anchor holds one value in both themes, so these do too. */
  --color-surface: #00404e;
  --color-surface-raised: #0a4f5e;
  --color-surface-sunken: #002a34;
  --color-surface-highest: #125d6d;

  --color-line: var(--color-anchor-line);
  --color-line-strong: rgba(139, 197, 209, 0.3);

  /* The palette's own primary and secondary, not their container
     steps: a fill that reads as brand on the graphite canvas is a
     shade too deep to carry a link on this petrol ground. */
  --color-primary-text: var(--athar-primary);
  --color-gold-text: var(--athar-secondary);

  /* Casting in black over a dark ground would show as a smudge, so
     shadows here are nearly off and depth comes from the surface step. */
  --shadow-ink: 4 22 30;
  --shadow-strength: 0.85;
}

.stack > * + * { margin-top: var(--space-4); }
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}
.grid {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
}

/* ---------------------------------------------------------
   Focus — one visible ring everywhere, keyboard only
   --------------------------------------------------------- */
:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

.skip-link {
  position: absolute;
  inset-inline-start: var(--space-4);
  top: var(--space-4);
  z-index: 100;
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-sm);
  background: var(--color-surface-raised);
  color: var(--color-text);
  transform: translateY(-200%);
  transition: transform var(--dur-base) var(--ease-out);
}
.skip-link:focus-visible { transform: translateY(0); }

/* ---------------------------------------------------------
   Reveal on scroll
   One short fade with a few pixels of travel, once. shell.js adds
   .is-revealed as an element enters; with reduced motion, or with
   JavaScript off, the element is simply visible.
   --------------------------------------------------------- */
/* --reveal-delay lets a row of siblings arrive in order rather than
   all at once. They enter the viewport on the same frame, so without
   it a grid of cards is one block appearing, which is a flash and not
   a movement. Default zero: an element that does not set it behaves
   exactly as it did before. */
:root.js [data-reveal] {
  opacity: 0;
  translate: 0 14px;
  transition:
    opacity var(--dur-slow) var(--ease-out),
    translate var(--dur-slow) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
:root.js [data-reveal].is-revealed {
  opacity: 1;
  translate: 0 0;
}
@media (prefers-reduced-motion: reduce) {
  :root.js [data-reveal] { opacity: 1; translate: 0 0; }
}

/* ---------------------------------------------------------
   Utilities
   --------------------------------------------------------- */
.u-muted { color: var(--color-text-muted); }
.u-secondary { color: var(--color-text-secondary); }
.u-lead { font-size: var(--fs-lg); line-height: var(--lh-relaxed); }

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

/* components/button.css */
/* =========================================================
   Component — Button
   The brand gradient is deliberately absent here. It belongs to
   the trace, the logo, and one word per page. A solid button
   next to a gradient trace reads as restraint; two gradients
   read as a template.
   ========================================================= */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--color-text);
  --btn-border: transparent;
  --btn-pad-block: 0.7rem;
  --btn-pad-inline: 1.4rem;

  /* Two layers: an optional sheen over the fill. A flat button that
     is only its fill is the tell of a template; the same fill with
     a highlight on its top edge reads as an object with thickness.
     Off by default — the sheen is spent on filled buttons only,
     where there is a surface for the light to fall on. */
  --btn-sheen: none;
  --btn-shadow: none;

  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--btn-pad-block) var(--btn-pad-inline);
  border: 1px solid var(--btn-border);
  border-radius: var(--radius-sm);
  background: var(--btn-sheen), var(--btn-bg);
  box-shadow: var(--btn-shadow);
  color: var(--btn-fg);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  line-height: 1.2;
  cursor: pointer;
  white-space: nowrap;
  transition:
    background var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}
.btn:active { transform: translateY(1px); }
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  pointer-events: none;
}

/* The ring follows the radius, which an outline does not. */
.btn:focus-visible {
  outline: none;
  box-shadow: var(--ring-focus), var(--btn-shadow);
}

.btn__icon {
  width: 1.05em;
  height: 1.05em;
  flex: none;
}

/* Primary — the one action a screen is asking for.

   The fill is the brand ramp rather than a flat colour, which is
   what the design system asks a conversion trigger to look like.
   Gradients are safe here because both ends were measured against
   the same ink: --color-on-primary clears 8.4:1 on the light stop
   and 6.4:1 on the dark one, so there is no corner of the button
   where the label stops being readable.

   The sheen still lies over that fill rather than replacing it —
   white and black alpha only, so it moves the top and bottom
   edges without touching either contrast figure. */
.btn--primary {
  --btn-bg: var(--gradient-primary);
  --btn-fg: var(--color-on-primary);
  --btn-sheen: var(--gradient-sheen);
  --btn-shadow: var(--shadow-soft);
}
.btn--primary:hover {
  --btn-bg: var(--color-primary-hover);
  /* --shadow-bloom is `none` in light mode, and `box-shadow: <list>,
     none` is invalid as a whole — so on paper the primary button was
     losing its hover shadow entirely rather than losing the glow.
     The halo is written here instead, in a form that resolves in
     both themes. */
  --btn-shadow:
    var(--shadow-deep),
    0 0 18px color-mix(in srgb, var(--color-primary) 34%, transparent);
}

/* Secondary — a real action that is not the page's one ask.
   A frame in the brand colour over a faint wash of it: it reads as a
   button from across the screen without competing with the filled
   primary beside it. */
.btn--secondary {
  --btn-bg: color-mix(in srgb, var(--color-primary) 8%, transparent);
  --btn-fg: var(--color-text);
  --btn-border: color-mix(in srgb, var(--color-primary) 50%, transparent);
}
.btn--secondary:hover {
  --btn-bg: var(--color-primary-soft);
  --btn-border: var(--color-primary);
  --btn-fg: var(--color-primary-text);
}

/* Ghost — the quiet register: going back, a side option, a toolbar.
   It used to be bare grey text, which merchants read as a label rather
   than something to press (owner, 2026-09-16). Now a neutral frame on
   a surface one step up: still the quietest button, but a button. */
.btn--ghost {
  --btn-bg: var(--color-surface-raised);
  --btn-fg: var(--color-text);
  --btn-border: var(--color-line-strong);
}
.btn--ghost:hover {
  --btn-bg: var(--color-primary-soft);
  --btn-border: var(--color-growth);
}

/* Danger — what cannot be taken back: delete, remove, cancel a
   renewal. A red frame and wash at rest, solid red under the pointer,
   so it is never mistaken for the row's ordinary verb. */
.btn--danger {
  --btn-bg: var(--color-danger-soft);
  --btn-fg: var(--color-danger);
  --btn-border: color-mix(in srgb, var(--color-danger) 45%, transparent);
}
.btn--danger:hover {
  --btn-bg: var(--color-danger);
  --btn-fg: var(--color-on-error);
  --btn-border: var(--color-danger);
}

/* Gold — reserved for upgrade / premium. If it appears twice on
   one screen, one of them is wrong. */
.btn--gold {
  --btn-bg: var(--color-gold-soft);
  --btn-fg: var(--color-gold-text);
  --btn-border: var(--color-line-gold);
}
.btn--gold:hover {
  --btn-bg: var(--color-gold);
  --btn-fg: var(--color-on-secondary);
  --btn-border: var(--color-gold);
  --btn-sheen: var(--gradient-sheen);
  --btn-shadow: var(--shadow-soft);
}

.btn--lg {
  --btn-pad-block: 0.95rem;
  --btn-pad-inline: 1.9rem;
  font-size: var(--fs-base);
}
.btn--sm {
  --btn-pad-block: 0.45rem;
  --btn-pad-inline: 0.9rem;
  font-size: var(--fs-xs);
}
.btn--block { width: 100%; }

/* Icon-only — square, and it must still carry an aria-label */
.btn--icon {
  --btn-pad-block: 0.6rem;
  --btn-pad-inline: 0.6rem;
  --btn-border: var(--color-line);
}
.btn--icon:hover { --btn-border: var(--color-line-strong); }

/* ---------------------------------------------------------
   Action — the row's own verb

   For the one thing a table row exists to do: open it. It is
   louder than a ghost and quieter than the page's primary
   button, which is the register a list of rows needs — twenty
   filled blue buttons down a column is a list where nothing
   is more important than anything else.

   The soft primary wash rather than a solid fill for the same
   reason: it reads as a control at a glance without competing
   with the one filled button the screen actually has.
   --------------------------------------------------------- */
.btn--action {
  --btn-bg: var(--color-primary-soft);
  --btn-fg: var(--color-primary-text);
  --btn-border: transparent;
  font-weight: var(--fw-bold);
}
.btn--action:hover {
  --btn-bg: var(--color-primary);
  --btn-fg: var(--color-on-primary);
  --btn-shadow: var(--shadow-soft);
  /* 1.02, not more: a button that grows in a dense row pushes
     its neighbours in the eye even when the layout holds. */
  transform: scale(1.02);
}
.btn--action:active { transform: scale(0.99); }

/* A row action under a finger has nothing to hover from, and a
   scale that only fires on tap reads as a stutter. */
@media (hover: none) {
  .btn--action:hover { transform: none; }
}

/* ---------------------------------------------------------
   Touch floor

   A finger does not aim as well as a cursor. On a coarse
   pointer every button clears 48px of height, and the page's
   primary ask goes full width once the row can no longer hold
   two buttons side by side.
   --------------------------------------------------------- */
@media (pointer: coarse) {
  .btn { min-block-size: 48px; }
  .btn--sm { min-block-size: 44px; }
}

/* components/busy.css */
/* =========================================================
   Component — Busy

   What a slow request looks like.

   Until now it looked like nothing. A merchant uploading a 40 MB
   clip, or a shopper on a Syrian mobile connection pressing «أكمل
   الطلب», pressed a button that did not change, waited on a page
   that did not change, and pressed it again — which is how a
   checkout becomes two orders and an upload becomes two files.

   The whole system hangs off one attribute, `aria-busy="true"`,
   which is what the platform is already obliged to set for screen
   readers. js/busy.js sets it on the form and on the button that
   submitted it, and everything below follows from that: no state
   class to keep in sync, no second vocabulary, and a control that
   is announced as busy is also drawn as busy.

   Everything animated here is a transform or an opacity on a
   pseudo-element, so nothing below reflows the page or repaints a
   layer the browser cannot promote. The reduced-motion rule in
   app.css freezes every animation on the platform, so both
   indicators have a still form that still reads as «working» —
   otherwise reduced motion would mean no feedback at all, which is
   the bug this file exists to fix.
   ========================================================= */

/* ---------------------------------------------------------
   The button

   The label is not removed and the button is not resized: the
   label goes transparent in place and a ring is drawn over it.
   Swapping the text for a spinner re-measures the button, which
   moves the row it sits in at the exact moment the person is
   still looking at where they clicked.

   Not [disabled]: a disabled submit button is dropped from the
   payload it was about to send, so a form that reads which button
   was pressed loses that. It is taken out of reach with
   pointer-events instead, and js/busy.js refuses the second
   submit outright.
   --------------------------------------------------------- */
.btn[aria-busy="true"] {
  color: transparent;
  pointer-events: none;
  cursor: progress;
}

/* The icon inside a button paints in currentColor, which is now
   transparent — but an <svg> with its own fill would still show
   through the ring, so the whole label is hidden as one. */
.btn[aria-busy="true"] > * {
  opacity: 0;
}

.btn[aria-busy="true"]::after {
  content: "";
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  width: 1.15em;
  height: 1.15em;
  margin-block-start: -0.575em;
  margin-inline-start: -0.575em;
  border-radius: var(--radius-pill);

  /* Half a ring drawn, half left open — the gap is what makes the
     rotation legible; a closed ring spinning looks still.

     Two borders coloured and two left transparent, rather than one
     translucent ring under one opaque arc: no colour function is
     involved, so there is nothing here a browser can fail to parse
     and leave the merchant with an invisible spinner.

     --btn-fg rather than currentColor, because currentColor is the
     transparent the label was just set to. */
  border: 2px solid transparent;
  border-block-start-color: var(--btn-fg);
  border-inline-end-color: var(--btn-fg);

  animation: athar-spin var(--dur-spin) var(--ease-linear) infinite;
  will-change: transform;
}

@keyframes athar-spin {
  to { transform: rotate(1turn); }
}

/* ---------------------------------------------------------
   The form

   A thread of light along the top edge, crawling. It is drawn on
   the form itself rather than on the page, so the person's eye
   does not have to leave the thing they are waiting on — and it
   costs one composited pseudo-element rather than a fixed overlay
   that would have to sit above the grain layer.

   The fields go quiet but stay readable: a form that greys out
   entirely while it uploads hides the very data the person is
   waiting to see accepted.
   --------------------------------------------------------- */
form[aria-busy="true"] {
  position: relative;
  cursor: progress;
}

form[aria-busy="true"] .field__control,
form[aria-busy="true"] .field__check,
form[aria-busy="true"] .field__file {
  opacity: 0.62;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out);
}

form[aria-busy="true"]::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 2px;
  border-radius: var(--radius-pill);
  background: var(--color-primary-soft);
  pointer-events: none;
}

form[aria-busy="true"]::after {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  width: 34%;
  height: 2px;
  border-radius: var(--radius-pill);
  background: var(--color-primary);
  pointer-events: none;

  animation: athar-crawl var(--dur-crawl) var(--ease-in-out) infinite;
  will-change: transform;
}

/* Travels the width of the form and back, in transform only. The
   percentages are of the bar's own 34%, so the two ends land flush
   against the form's edges at any width. */
@keyframes athar-crawl {
  0% { transform: translateX(0); }
  50% { transform: translateX(194%); }
  100% { transform: translateX(0); }
}

/* RTL runs the other way: translateX is physical, and a progress
   bar that crawls away from the reading edge reads as rewinding. */
[dir="rtl"] form[aria-busy="true"]::after {
  animation-name: athar-crawl-rtl;
}

@keyframes athar-crawl-rtl {
  0% { transform: translateX(0); }
  50% { transform: translateX(-194%); }
  100% { transform: translateX(0); }
}

/* ---------------------------------------------------------
   A busy region that is neither

   A panel refreshing in place, a table reloading a page of rows.
   Same attribute, same vocabulary, no spinner: the content dims
   and stops taking clicks, which is all a region needs to say.
   --------------------------------------------------------- */
[data-busy-region][aria-busy="true"] {
  opacity: 0.55;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out);
}

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

   app.css cuts every animation on the platform to 0.01ms, so both
   indicators above stop dead — the spinner freezes at whatever
   angle it started on and the bar parks at the inline start. Left
   alone, that is a loading state that does not load.

   So under reduced motion they become still marks instead: a
   complete ring rather than a stalled three-quarter one, and a
   full-width bar rather than a stalled third of one. Neither
   moves, both say the same thing, and aria-busy is carrying the
   message to anybody not looking at either.
   --------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .btn[aria-busy="true"]::after {
    border-color: var(--btn-fg);
  }

  form[aria-busy="true"]::after {
    width: 100%;
    opacity: 0.7;
  }
}

/* A link that is navigating.

   Same ring the buttons get, and the same reason it is not a
   width change: the row must not move while the next page is
   still loading. */
a.btn[aria-busy="true"] {
  color: transparent;
  pointer-events: none;
  cursor: progress;
}

/* components/badge.css */
/* =========================================================
   Component — Badge
   Status, plan tier, and delta. Colour alone never carries the
   meaning: every badge also carries a word.
   ========================================================= */
.badge {
  --badge-bg: var(--color-primary-soft);
  --badge-fg: var(--color-primary-text);

  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.25rem 0.65rem;
  border-radius: var(--radius-pill);
  background: var(--badge-bg);
  color: var(--badge-fg);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  line-height: 1.5;
  white-space: nowrap;
}

.badge--success { --badge-bg: var(--color-success-soft); --badge-fg: var(--color-success); }
.badge--warning { --badge-bg: var(--color-warning-soft); --badge-fg: var(--color-warning); }
.badge--danger  { --badge-bg: var(--color-danger-soft);  --badge-fg: var(--color-danger); }
.badge--growth  { --badge-bg: var(--color-growth-soft);  --badge-fg: var(--color-growth-text); }
.badge--neutral { --badge-bg: var(--color-surface-sunken); --badge-fg: var(--color-text-secondary); }

/* Entry tier — the first paid plan, and nothing else */
.badge--entry {
  --badge-bg: var(--color-entry-soft);
  --badge-fg: var(--color-entry-text);
  box-shadow: inset 0 0 0 1px var(--color-line-entry);
}

/* Gold tier — the highest plan, and nothing else */
.badge--gold {
  --badge-bg: var(--color-gold-soft);
  --badge-fg: var(--color-gold-text);
  box-shadow: inset 0 0 0 1px var(--color-line-gold);
}

.badge__dot {
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: var(--radius-pill);
  background: currentColor;
}

.badge__figure {
  font-family: var(--font-figures);
  font-variant-numeric: tabular-nums;
}

/* Pill used as a hero eyebrow — larger, quieter */
.badge--hero {
  --badge-bg: var(--color-surface-raised);
  --badge-fg: var(--color-text-secondary);
  padding: 0.45rem 1rem;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  box-shadow: inset 0 0 0 1px var(--color-line);
}

/* Small — a row of them under a card's note, where the standard
   size would outweigh the name above it. */
.badge--sm {
  padding: 0.1rem var(--space-2);
  font-size: var(--fs-xs);
  font-weight: var(--fw-regular);
}

/* components/field.css */
/* =========================================================
   Component — Field (label + control + help/error)
   ========================================================= */
.field {
  --field-bg: var(--color-surface-sunken);
  --field-border: var(--color-line);
  --field-fg: var(--color-text);

  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field__label {
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  color: var(--color-text);
}
.field__required {
  display: inline-block;
  margin-inline-start: var(--space-2);
  padding: 0.05em 0.5em;
  border: 1px solid var(--color-gold);
  border-radius: var(--radius-xs);
  color: var(--color-gold);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  line-height: 1.5;
  vertical-align: 0.1em;
}

/*
   The three custom properties are scoped to .field, so a control written on
   its own — a select inside a table row, a filter with no label — resolved
   them to nothing and rendered as bare text on a transparent ground: no
   border, no background, invisible as a control. The admin stores screen
   worked around it by redeclaring all three on .store-status; the next
   screen to do the same would be the second copy of a fix.

   The fallbacks below are read only when nothing above has set them, so a
   control inside .field still takes .field's values — including the ones
   .field.is-invalid overrides.
*/
.field__control {
  width: 100%;
  padding: 0.8rem 1rem;
  border: 1px solid var(--field-border, var(--color-line));
  border-radius: var(--radius-sm);
  background: var(--field-bg, var(--color-surface-sunken));
  color: var(--field-fg, var(--color-text));
  font-size: var(--fs-sm);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}
.field__control::placeholder { color: var(--color-text-muted); }
.field__control:hover { --field-border: var(--color-line-strong); }
.field__control:focus {
  outline: none;
  --field-border: var(--color-focus);
  box-shadow: var(--ring-focus);
}
.field__control:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Below 16px, iOS Safari reads a tap into the field as a request to zoom
   the whole page in — a finger never taps precisely enough to explain the
   jump away, so it reads as the page malfunctioning. Desktop keeps the
   smaller size; a mouse click never triggers it. */
@media (pointer: coarse) {
  .field__control { font-size: 16px; }
}

textarea.field__control {
  min-height: 7rem;
  resize: vertical;
  line-height: var(--lh-normal);
}

/*
   The chevron is two solid half-squares that meet at a point, drawn in
   currentColor so no icon file and no hardcoded hex is involved.

   Order matters and was reversed until 2026-08-27: the first gradient is
   the triangle whose mass sits top-RIGHT, so it has to be the LEFT half of
   the pair, and the second (mass top-LEFT) the right half. Placed the other
   way round the two wedges lean apart and the arrow reads as a notched bar
   rather than a v. The offsets are in px because the two boxes must sit
   exactly one width apart: 0.25rem between them left a pixel of overlap.
*/
select.field__control {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: left 13px top 55%, left 18px top 55%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-inline-start: 2.4rem;
}
:root[dir="ltr"] select.field__control {
  background-position: right 18px top 55%, right 13px top 55%;
  padding-inline-start: 1rem;
  padding-inline-end: 2.4rem;
}

.field__help {
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

/* Errors state what happened and what to do. They do not apologise. */
.field__error {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  color: var(--color-danger);
}

.field.is-invalid { --field-border: var(--color-danger); }
.field.is-invalid .field__control:focus {
  box-shadow: 0 0 0 3px var(--color-danger-soft);
}

/* Input with a trailing affordance (password reveal, unit, search) */
.field__wrap { position: relative; display: flex; align-items: center; }
.field__wrap .field__control { padding-inline-end: 2.9rem; }
.field__affix {
  position: absolute;
  inset-inline-end: 0.6rem;
  display: inline-flex;
  padding: var(--space-2);
  border: 0;
  border-radius: var(--radius-xs);
  background: none;
  color: var(--color-text-muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}
.field__affix:hover { color: var(--color-text); }
.field__affix:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 1px;
  color: var(--color-text);
}

/* One eye at a time, chosen by the button's own pressed state — the same
   pattern as the sun and moon on the theme toggle. Pressed means the secret
   is on screen, so the crossed-out eye is what you press to hide it again. */
.field__eye { inline-size: 1.15rem; block-size: 1.15rem; }
.field__affix .field__eye--shut,
.field__affix[aria-pressed="true"] .field__eye--open { display: none; }
.field__affix[aria-pressed="true"] .field__eye--shut { display: inline; }
.field__affix[aria-pressed="true"] { color: var(--color-text); }
/* =========================================================
   Component — Field · additions
   ---------------------------------------------------------
   م0 defined the label, the control, the help line and the error.
   Two things the forms in م6–م15 needed and it did not have: a
   checkbox row, and a way to lay fields out side by side.
   ========================================================= */

/* A checkbox and its label on one line. The box keeps the brand
   colour rather than the browser's, and the whole row is the hit
   target — a 13px square is not one. */
.field__check {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  font-size: var(--fs-sm);
  color: var(--color-text);
  cursor: pointer;
  line-height: var(--lh-snug);
}
.field__check input[type="checkbox"] {
  flex: none;
  inline-size: 1.1rem;
  block-size: 1.1rem;
  margin-block-start: 0.15rem;
  accent-color: var(--color-primary);
  cursor: pointer;
}
.field__check:hover { color: var(--color-text); }
.field__check input:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Fields in a row. Collapses to one column early: two number inputs
   side by side at 380px are two inputs nobody can read the labels of. */
.field-row {
  display: grid;
  gap: var(--space-4);
}
@media (min-width: 620px) {
  .field-row--2 { grid-template-columns: 1fr 1fr; }
  .field-row--3 { grid-template-columns: repeat(3, 1fr); }
}

/* A group of fields under a heading, inside a card. */
.field-set {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* ---------------------------------------------------------
   The file picker
   ---------------------------------------------------------
   ::file-selector-button could be painted, but the words on
   it belong to the browser: «Choose file / No file chosen»,
   in the browser's language, unmoved by our own switch. So
   the input is moved out of sight and a face is drawn for
   it — and the input stays focusable, because a control you
   cannot reach with Tab is worse than an ugly one.

   The input is 1px at the start corner rather than pushed
   off with a negative offset: in RTL the overflow lands on
   the left, and a -1px margin is a scrollbar across the
   whole page.
   --------------------------------------------------------- */
.field__file { position: relative; }

.field__file-input {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: 1px;
  block-size: 1px;
  opacity: 0;
  pointer-events: none;
}

.field__file-face {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
  border: 1px solid var(--field-border, var(--color-line));
  border-radius: var(--radius-sm);
  background: var(--field-bg, var(--color-surface-sunken));
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}
.field__file-face:hover { --field-border: var(--color-line-strong); }

/* The ring goes on the face, because the face is what is on screen. */
.field__file-input:focus-visible + .field__file-face {
  --field-border: var(--color-focus);
  box-shadow: var(--ring-focus);
}
.field.is-invalid .field__file-input:focus-visible + .field__file-face {
  box-shadow: 0 0 0 3px var(--color-danger-soft);
}
.field__file-input:disabled + .field__file-face {
  opacity: 0.5;
  cursor: not-allowed;
}

.field__file-btn {
  flex: none;
  padding: 0.45rem 0.9rem;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-xs);
  background: var(--color-surface-raised);
  color: var(--color-text);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
}

/* One line however many names: a chosen file must not push the
   button out of the card. */
.field__file-name {
  min-inline-size: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}
.field__file.has-file .field__file-name {
  color: var(--color-text);
  font-weight: var(--fw-medium);
}

/* A value that cannot be edited, shown where its input would have been. It
   keeps the field's rhythm so a locked row does not fall out of the column. */
.field__static {
  padding: var(--space-2) 0;
  color: var(--color-text);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
}

/* ---------------------------------------------------------
   A line under a field that offers a way out
   ---------------------------------------------------------
   The same size and colour as .field__help, because it is
   the same kind of line: quiet, and read only by somebody
   who has stopped and looked.
   --------------------------------------------------------- */
.field__aside {
  margin: calc(var(--space-2) * -1) 0 0;
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
  line-height: var(--lh-snug);
}

/* A button that has to read as a link: it submits, so it cannot be an <a>,
   and it sits mid-sentence, so it cannot look like a button either. */
.link-button {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--color-primary-text);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.link-button:hover { color: var(--color-text); }
.link-button:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

/* ---------------------------------------------------------
   The shape of a date
   ---------------------------------------------------------
   An empty date input carries the browser's own «يوم/شهر/سنة»,
   and Chrome lays the three words out left to right — day
   first on the left, the order the figures go in. Read right
   to left, as Arabic is, they arrive as سنة/شهر/يوم and the
   field looks like it is asking for the year first. dir on
   the input does not move them; neither does direction on
   ::-webkit-datetime-edit. The order is fixed in the browser.

   So while the box is empty and unfocused the browser's line
   is painted transparent — the calendar button is an image
   and stays where it was — and ours is printed over it as
   ordinary Arabic. A filled field is left alone: ٢٥/٠٣/١٩٩٠
   is a run of numerals and reads correctly on its own.

   .is-empty comes from js/date-field.js, so a page with no
   JavaScript keeps the browser's field untouched rather than
   an empty box with nothing written in it.
   --------------------------------------------------------- */
.field__date { position: relative; display: flex; align-items: center; }

/* The side it sits on is the side the browser puts its own figures
   on, which is the browser locale's and not the page's: an Arabic
   Chrome keeps the date box RTL — calendar button on the left, text
   on the right — even after the page switches to English. So the
   dir here is written by js/date-field.js from what the input
   actually computed to. Anchored to the page instead, the hint slid
   under the calendar button the moment somebody pressed EN. */
.field__date-hint {
  position: absolute;
  inset-inline-start: 1rem;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
  pointer-events: none;
  opacity: 0;
}
.field__date-hint[dir="rtl"] { inset: auto 1rem auto auto; }
.field__date-hint[dir="ltr"] { inset: auto auto auto 1rem; }

.field__date.is-empty:not(:focus-within) .field__control { color: transparent; }
.field__date.is-empty:not(:focus-within) .field__date-hint { opacity: 1; }

/* components/alert.css */
/* =========================================================
   Component — Alert
   ---------------------------------------------------------
   The line that says what just happened. Tinted rather than
   filled: a solid green bar across the top of a page reads as
   an interruption, and most of what it announces is routine.
   ========================================================= */
.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--alert-line, var(--color-line));
  border-radius: var(--radius-md);
  background: var(--alert-bg, var(--color-surface));
  color: var(--color-text);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
}

.alert + .alert { margin-block-start: var(--space-3); }

.alert svg {
  flex: none;
  inline-size: 1.15rem;
  block-size: 1.15rem;
  margin-block-start: 0.1rem;
  color: var(--alert-icon, var(--color-text-muted));
}

.alert p { margin: 0; }
.alert p + p { margin-block-start: var(--space-1); }

.alert--success {
  --alert-bg: var(--color-success-soft);
  --alert-line: color-mix(in srgb, var(--color-success) 34%, transparent);
  --alert-icon: var(--color-success);
}

.alert--danger {
  --alert-bg: var(--color-danger-soft);
  --alert-line: color-mix(in srgb, var(--color-danger) 34%, transparent);
  --alert-icon: var(--color-danger);
}

.alert--warning {
  --alert-bg: var(--color-warning-soft);
  --alert-line: color-mix(in srgb, var(--color-warning) 34%, transparent);
  --alert-icon: var(--color-warning);
}

.alert--info {
  --alert-bg: var(--color-primary-soft);
  --alert-line: color-mix(in srgb, var(--color-primary) 30%, transparent);
  --alert-icon: var(--color-primary-text);
}

/* A hairline rule of the same tint, for notes that sit inside a card
   and should not repeat the card's own border. */
.alert--flat {
  border: 0;
  border-inline-start: 3px solid var(--alert-icon, var(--color-line));
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
:root[dir="ltr"] .alert--flat {
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}

/* components/platform-toggle.css */
/* =========================================================
   Component — Platform toggles (language + theme)

   Two small square controls that travel together wherever the
   shell goes: header, drawer, footer, auth card. The theme button
   shows the mode it will switch you to, not the one you are in —
   a control should be labelled by its action.
   ========================================================= */
.platform-toggles {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.platform-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-secondary);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.platform-toggle:hover {
  border-color: var(--color-line-strong);
  background: var(--color-surface-raised);
  color: var(--color-text);
}

.platform-toggle--lang {
  width: auto;
  padding-inline: var(--space-3);
  font-family: var(--font-latin);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide);
}

.platform-toggle__icon { width: 17px; height: 17px; }

/* Only one of the two icons is ever drawn: sun while dark is on
   (press it and you get light), moon while light is on. */
:root .platform-toggle__icon--moon,
:root[data-theme="light"] .platform-toggle__icon--sun { display: none; }
:root .platform-toggle__icon--sun,
:root[data-theme="light"] .platform-toggle__icon--moon { display: block; }

/* Theme and language are the two controls a visitor reaches for on
   a phone with one hand. 34px was a miss waiting to happen. */
@media (pointer: coarse) {
  .platform-toggle {
    min-inline-size: 44px;
    min-block-size: 44px;
  }
}

/* components/chip.css */
/* =========================================================
   Component — Chip

   A small standalone label used inside panels and mocks.
   ========================================================= */
.chip {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
}

/* Small — for chips inside a mock, where the standard size would
   outweigh the text it sits under. */
.chip--sm {
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-xs);
  font-weight: var(--fw-regular);
  background: var(--color-surface-sunken);
}

/* Live — a chip that says the figures beside it are read from the database
   rather than drawn. The dot is the whole signal; it does not pulse, because
   a blinking light on a marketing page is a notification nobody asked for. */
.chip--live {
  gap: var(--space-2);
  border-color: var(--color-growth);
  color: var(--color-growth-text);
}
.chip--live::before {
  content: "";
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--color-growth);
}

/* Not shipped yet. Muted rather than tinted: a chip that says «soon»
   should read as a note, not as a feature already earned. */
.chip--soon {
  border-style: dashed;
  color: var(--color-text-muted);
}

/* components/accordion.css */
/* =========================================================
   Component — Accordion

   Built on native <details>/<summary>: it opens without
   JavaScript, it is announced as a disclosure by a screen reader,
   and the browser's own find-in-page can open it. shell.js only
   adds "one open at a time" on top of that.
   ========================================================= */
.accordion {
  display: flex;
  flex-direction: column;
}

.accordion__item {
  border-bottom: 1px solid var(--color-line);
}
.accordion__item:first-child { border-top: 1px solid var(--color-line); }

.accordion__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding-block: var(--space-5);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--color-text);
  cursor: pointer;
  list-style: none;
  transition: color var(--dur-fast) var(--ease-out);
}
/* Kill both the WebKit triangle and the Firefox one */
.accordion__head::-webkit-details-marker { display: none; }
.accordion__head::marker { content: ""; }

.accordion__head:hover { color: var(--color-gold-text); }
.accordion__head:focus-visible { outline-offset: -2px; }

/* A plus drawn from two rules, one of which retracts on open, so
   the sign becomes a minus without swapping a glyph or a file. */
.accordion__sign {
  position: relative;
  flex: none;
  width: 16px;
  height: 16px;
  margin-inline-start: auto;
  color: var(--color-gold-text);
}
.accordion__sign::before,
.accordion__sign::after {
  content: "";
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 0;
  width: 100%;
  height: 1.6px;
  border-radius: var(--radius-pill);
  background: currentColor;
  translate: 0 -50%;
  transition: rotate var(--dur-base) var(--ease-out);
}
.accordion__sign::after { rotate: 90deg; }
.accordion__item[open] .accordion__sign::after { rotate: 0deg; }

.accordion__body {
  max-width: 62ch;
  margin: 0;
  padding-bottom: var(--space-5);
  color: var(--color-text-secondary);
  font-size: var(--fs-base);
  line-height: var(--lh-relaxed);
}

@media (prefers-reduced-motion: reduce) {
  .accordion__sign::before,
  .accordion__sign::after { transition: none; }
}

/* components/pager.css */
/* =========================================================
   Component — Pagination

   Lived in pages/dashboard.css until the storefront's category
   and search pages needed the same widget. Two screens using one
   set of class names is the point at which it stops belonging to
   a page (the constitution, §4.2).
   ========================================================= */
.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-block-start: 1px solid var(--color-line);
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}
.pager__range { display: inline-flex; align-items: baseline; gap: var(--space-2); }
.pager__links { display: flex; gap: var(--space-2); }
.pager__link {
  min-inline-size: 2rem;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-xs);
  color: var(--color-text-secondary);
  font-family: var(--font-latin);
  text-align: center;
  text-decoration: none;
}
.pager__link:hover { border-color: var(--color-line-strong); color: var(--color-text); }
.pager__link.is-active {
  border-color: var(--color-primary);
  background: var(--color-primary-soft);
  color: var(--color-primary-text);
}
.pager__link.is-disabled { opacity: 0.4; pointer-events: none; }


/* components/shop-header.css */
/* =========================================================
   Component — Storefront header

   Quieter than the marketing header on purpose. A shop's header
   is furniture: it has to stay out of the way of the goods and
   still put the basket one glance away. No gradient, one accent
   (the basket count), and one small menu panel — «المزيد», only
   when a shop has more than six shelves.

   Three widths:
     ≥ 1000px  mark · sections · search · basket, one row
     ≥ 640px   sections move into the drawer, search stays
     < 640px   search drops to a row of its own under the bar
   ========================================================= */

/* ---------------------------------------------------------
   Utility strip — the two facts a shopper looks for before
   they look at anything else: where this shop is, and how to
   reach a human. It scrolls away; the header does not.
   --------------------------------------------------------- */
.shop-strip {
  border-block-end: 1px solid var(--color-line);
  background: var(--color-canvas-deep);
  font-size: var(--fs-xs);
}

.shop-strip__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-5);
  min-height: 38px;
  padding-block: var(--space-2);
}

.shop-strip__note {
  margin: 0;
  color: var(--color-text-muted);
}

.shop-strip__contact {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-5);
}

.shop-strip__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--color-text-secondary);
}
.shop-strip__link:hover { color: var(--color-growth-text); }
.shop-strip__link svg {
  width: 15px;
  height: 15px;
  flex: none;
}

@media (max-width: 560px) {
  /* The tagline is on the hero as well; on a phone the contact
     line is what earns the space. */
  .shop-strip__note { display: none; }
  .shop-strip__inner { justify-content: center; }
}

/* ---------------------------------------------------------
   The bar
   --------------------------------------------------------- */
/*
   At rest the bar draws nothing under itself.

   The line was there for the state the header is in for about one
   second of a visit — the top of the page, where the strip above it
   has already drawn a rule and the hero below it draws its own. Three
   horizontal lines stacked in the first 120 pixels is what makes a
   shop front look like an admin screen.

   Once the page has moved, the header is floating over the goods and
   the line is doing real work, so it arrives with the shadow. The
   border is transparent rather than absent so the bar keeps its
   height and nothing under it shifts by a pixel when the state flips.
*/
.shop-header {
  position: sticky;
  top: 0;
  z-index: 40;
  border-block-end: 1px solid transparent;
  background: color-mix(in srgb, var(--color-canvas) 88%, transparent);
  backdrop-filter: blur(14px);
  transition:
    box-shadow var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out);
}
.shop-header.is-stuck {
  border-block-end-color: var(--color-line);
  box-shadow: var(--shadow-md);
}

.shop-header__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  min-height: var(--header-height);
  padding-block: var(--space-3);
}

/* ---- The shop's own mark ---- */
.shop-mark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  color: var(--color-text);
}
.shop-mark:hover { color: var(--color-text); }

.shop-mark__img {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--radius-md);
  object-fit: cover;
}

/* No uploaded logo yet: the shop's first letter, set as a monogram.
   It is a real mark for a real shop, not a placeholder box. */
.shop-mark__initial {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--radius-md);
  background: var(--color-primary-soft);
  box-shadow: inset 0 0 0 1px var(--color-line);
  color: var(--color-primary-text);
  font-family: var(--font-display-ar);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  line-height: 1;
}

.shop-mark__name {
  font-family: var(--font-display-ar);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
:root[data-lang="en"] .shop-mark__name { font-family: var(--font-latin); }

/* ---- Sections ----
   Between 1000 and ~1100px the search and the basket used to drop to a
   second line: six shelves asked for more than the row had, and a row
   that may wrap wraps before anything in it shrinks. From 1001px the
   row does not wrap (below), and shop.js moves shelves into «المزيد»
   until the ones left fit at their full width.

   Without the script the first six stay: the row shrinks instead, a
   long name shortens with an ellipsis (its full name is the title), and
   what still does not fit is clipped at the bar's far end rather than
   drawn over the search field. */
@media (min-width: 1001px) {
  .shop-header__inner { flex-wrap: nowrap; }
}

.shop-nav {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  margin-inline-start: var(--space-4);
  min-width: 0;
  overflow-x: clip;
  overflow-clip-margin: var(--space-1);
}
/* Clipping one axis would still cut the open panel at the bar's edge. */
.shop-nav:has(.shop-nav__more[open]) { overflow: visible; }
.shop-nav > * { flex: none; }

/* Closer shelves on a tighter row, so one more fits before «المزيد». */
@media (max-width: 1200px) {
  .shop-nav { gap: var(--space-4); }
}
.shop-nav > .shop-nav__link--shelf {
  flex: 0 1 auto;
  min-width: 3em;
  overflow: hidden;
  text-overflow: ellipsis;
}

.shop-nav__link {
  position: relative;
  padding-block: var(--space-2);
  color: var(--color-text-secondary);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  white-space: nowrap;
}
.shop-nav__link:hover { color: var(--color-text); }
.shop-nav__link::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 2px;
  border-radius: var(--radius-pill);
  background: var(--color-gold);
  scale: 0 1;
  transition: scale var(--dur-fast) var(--ease-out);
}
.shop-nav__link:hover::after { scale: 1 1; }

/* «المزيد» — the one panel this header has, and only for a shop that has
   outgrown the bar. */
.shop-nav__more { position: relative; }

.shop-nav__more > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  cursor: pointer;
  list-style: none;
}
.shop-nav__more > summary::-webkit-details-marker { display: none; }
.shop-nav__more > summary svg {
  width: 14px;
  height: 14px;
  transition: rotate var(--dur-fast) var(--ease-out);
}
.shop-nav__more[open] > summary { color: var(--color-text); }
.shop-nav__more[open] > summary svg { rotate: 180deg; }

.shop-nav__menu {
  position: absolute;
  inset-block-start: calc(100% + var(--space-2));
  inset-inline-start: calc(var(--space-3) * -1);
  z-index: 45;
  display: grid;
  min-width: 200px;
  max-height: min(60vh, 420px);
  overflow-y: auto;
  padding: var(--space-2);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
}

.shop-nav__menu-link {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  white-space: nowrap;
}
.shop-nav__menu-link:hover {
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

/* ---- Search ---- */
.shop-search {
  position: relative;
  flex: 1 1 180px;
  max-width: 300px;
  margin-inline-start: auto;
}

.shop-search__icon {
  position: absolute;
  inset-inline-start: var(--space-3);
  top: 50%;
  translate: 0 -50%;
  width: 17px;
  height: 17px;
  color: var(--color-text-muted);
  pointer-events: none;
}

.shop-search__input {
  width: 100%;
  padding: 0.55rem var(--space-3);
  padding-inline-start: calc(var(--space-3) * 2 + 17px);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  color: var(--color-text);
  font-size: var(--fs-sm);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.shop-search__input::placeholder { color: var(--color-text-muted); }
.shop-search__input:hover { border-color: var(--color-line-strong); }
.shop-search__input:focus {
  outline: none;
  border-color: var(--color-focus);
}
.shop-search__input::-webkit-search-cancel-button { filter: invert(0.5); }

/* The 16px floor from field.css: this field skips .field__control, so it
   needs its own guard against iOS zooming the page on focus. */
@media (pointer: coarse) {
  .shop-search__input { font-size: 16px; }
}

/* ---- Basket and toggles ---- */
.shop-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.shop-cart {
  position: relative;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  color: var(--color-text);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.shop-cart:hover {
  border-color: var(--color-line-strong);
  background: var(--color-surface);
  color: var(--color-text);
}
.shop-cart svg { width: 20px; height: 20px; }

.shop-cart__count {
  position: absolute;
  inset-inline-end: -6px;
  top: -6px;
  min-width: 19px;
  height: 19px;
  padding-inline: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-size: 0.6875rem;
  font-weight: var(--fw-bold);
  line-height: 19px;
  text-align: center;
}

/* ---- Burger ---- */
.shop-header__burger {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  width: 40px;
  height: 40px;
  padding: 0 10px;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: none;
  cursor: pointer;
}
.shop-header__burger span {
  display: block;
  height: 1.6px;
  border-radius: var(--radius-pill);
  background: var(--color-text);
}
.shop-header__burger:hover { border-color: var(--color-line-strong); }

/* ---------------------------------------------------------
   Narrower
   --------------------------------------------------------- */
@media (max-width: 1000px) {
  .shop-nav { display: none; }
  .shop-header__burger { display: flex; }
}

/* The search opener exists only where the field has left the bar. */
.shop-find {
  display: none;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  color: var(--color-text);
}
.shop-find svg { width: 20px; height: 20px; }

/*
   A phone gets one row: menu, name, search, basket.

   It used to stack the contact strip, the bar, the language and theme
   buttons on a row of their own, then the search field — close to 300px
   before the shop began, and 190px of it stayed stuck to the top of the
   screen for the whole visit. A quarter of a phone spent on furniture is
   a quarter the goods do not get.

   So the strip goes (its contact moves into the drawer), the two toggles
   move into the drawer, and the field folds behind an icon that opens it
   under the bar. The buttons lose their frames: four outlined squares in
   a row read as a toolbar, not as a shop.
*/
@media (max-width: 640px) {
  .shop-strip { display: none; }

  .shop-header__inner {
    flex-wrap: nowrap;
    gap: var(--space-2);
    min-height: 56px;
    padding-block: var(--space-2);
  }

  .shop-header__actions {
    gap: var(--space-1);
    margin-inline-start: auto;
  }
  .shop-header__actions > .platform-toggles { display: none; }

  .shop-header__burger,
  .shop-cart {
    border-color: transparent;
  }

  .shop-find { display: grid; }

  .shop-mark { gap: var(--space-2); }
  .shop-mark__img,
  .shop-mark__initial {
    width: 34px;
    height: 34px;
  }
  .shop-mark__name { font-size: var(--fs-base); }

  .shop-search {
    display: none;
    position: absolute;
    inset-inline: var(--space-4);
    top: calc(100% + var(--space-2));
    max-width: none;
    margin: 0;
  }
  .shop-header.is-searching .shop-search { display: block; }
  .shop-search__input {
    background: var(--color-surface-raised);
    box-shadow: var(--shadow-md);
  }
}

.shop-drawer__toggles {
  margin-block-start: var(--space-4);
}

/* ---------------------------------------------------------
   Drawer

   Slides in from the reading edge — the same edge the burger
   sits on, which is the right in Arabic and the left in
   English. `translate` is physical, so the resting position
   is written once per direction rather than assumed.
   --------------------------------------------------------- */
.shop-drawer {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  width: min(320px, 86vw);
  border-inline-end: 1px solid var(--color-line);
  background: var(--color-surface);
  /* Longer than --dur-base on purpose. A 400px panel crossing the screen in
     240ms is gone before the eye has followed it, and the slide is the
     confirmation that something was added. shop.js waits the same 300ms
     before hiding it. */
  transition: translate 300ms var(--ease-out);
}
/* display: flex above outranks the UA stylesheet's [hidden] rule, so a
   closed drawer stayed in the tab order and screen-reader tree — reachable
   by feel even though translate had pushed it off the visible screen.
   This is the one thing allowed to say [hidden] and mean it. */
.shop-drawer[hidden] { display: none; }
/*
   Off-screen toward its own edge, in whichever direction the shop reads.
   A translate is geometry and does not flip with the writing mode, so the
   sign has to be written out twice.

   ---- Why the open rule names the direction as well ----

   It used to be a bare `.shop-drawer.is-open`, and the drawer never
   opened: two classes lose to a pseudo-class, an attribute and a class,
   so `translate: 0 0` was outranked by the very rule it exists to undo
   and the panel stayed parked off the edge of the window with `is-open`
   on it. Nothing in the markup or the script was wrong, which is why it
   survived — the class went on, the transition ran, and it ran between
   100% and 100%.

   Matching the two selectors rather than adding `:root` to the front of
   one makes the pairing explicit: the rule that parks it and the rule
   that brings it back are now the same shape, and neither can quietly
   outweigh the other again.
*/
:root[dir="rtl"] .shop-drawer { translate: 100% 0; }
:root[dir="ltr"] .shop-drawer { translate: -100% 0; }

:root[dir="rtl"] .shop-drawer.is-open,
:root[dir="ltr"] .shop-drawer.is-open { translate: 0 0; }

.shop-drawer__scrim {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--color-scrim);
}

.shop-drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-block-end: 1px solid var(--color-line);
}

.shop-drawer__title {
  font-family: var(--font-display-ar);
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
  color: var(--color-text);
}
:root[data-lang="en"] .shop-drawer__title { font-family: var(--font-latin); }

.shop-drawer__close {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-secondary);
  cursor: pointer;
}
.shop-drawer__close:hover { color: var(--color-text); border-color: var(--color-line-strong); }
.shop-drawer__close svg { width: 17px; height: 17px; }

.shop-drawer__body {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-4) var(--space-5);
}

.shop-drawer__body .hairline { margin-block: var(--space-4); }

.shop-drawer__link {
  display: block;
  padding-block: var(--space-3);
  color: var(--color-text-secondary);
  font-size: var(--fs-base);
}
.shop-drawer__link:hover { color: var(--color-text); }

.shop-drawer__foot {
  padding: var(--space-5);
  border-block-start: 1px solid var(--color-line);
}

/* =========================================================
   Search suggestions

   A panel under the field, filled by shop.js from the shop's
   own catalogue. It is an addition and never a replacement:
   the form still submits to the search page, so a visitor
   without JavaScript — or one who types faster than the
   network — loses nothing by ignoring it.
   ========================================================= */
.suggest {
  position: absolute;
  inset-inline: 0;
  top: calc(100% + var(--space-2));
  z-index: 50;
  max-block-size: min(70vh, 26rem);
  overflow-y: auto;
  margin: 0;
  padding: var(--space-1);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
  box-shadow: var(--shadow-deep);
  list-style: none;
}

.suggest[hidden] { display: none; }

.suggest__item { margin: 0; }

.suggest__link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  text-decoration: none;
}

/* Hover and keyboard selection paint the same, because they
   are the same state — the difference is only which device
   the shopper is holding. */
.suggest__link:hover,
.suggest__link.is-active {
  background: var(--color-surface-highest);
  color: var(--color-text);
}

.suggest__shot {
  flex: none;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  overflow: hidden;
  border-radius: var(--radius-xs);
  background: var(--color-surface-sunken);
}
.suggest__shot img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

.suggest__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  font-size: var(--fs-sm);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.suggest__price {
  flex: none;
  color: var(--color-text-secondary);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
}

/* The last row: what the form would have done anyway, said
   in words. A shopper whose product is not in the five above
   needs the full results, not a dead end. */
.suggest__all {
  display: block;
  padding: var(--space-2);
  border-block-start: 1px solid var(--color-line);
  margin-block-start: var(--space-1);
  color: var(--color-growth-text);
  font-size: var(--fs-xs);
  text-align: center;
  text-decoration: none;
}
.suggest__all:hover { text-decoration: underline; color: var(--color-growth-text); }

.suggest__none {
  padding: var(--space-4) var(--space-2);
  color: var(--color-text-muted);
  font-size: var(--fs-sm);
  text-align: center;
}

/* components/cart-drawer.css */
/* =========================================================
   Component — Basket drawer

   The panel that slides in when something is added to the
   basket. It borrows the navigation drawer's scrim, close
   button and timing on purpose: two panels that arrive from
   opposite edges of the same shop should arrive the same way.

   What it does not borrow is the edge. The menu comes from
   the side the reading starts on; the basket comes from the
   side it ends on — the side the basket icon is already on,
   so the panel appears to come out of the thing that was
   pressed.
   ========================================================= */
.cart-drawer {
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  width: min(400px, 92vw);
  border-inline-start: 1px solid var(--color-line);
  background: var(--color-surface);
  /* 300ms, the menu drawer's number too — see shop-header.css. */
  transition: translate 300ms var(--ease-out);
}
/* Same gap as .shop-drawer's, same fix: display: flex above otherwise
   outranks [hidden], and a closed panel stayed reachable by Tab. */
.cart-drawer[hidden] { display: none; }

/* Off-screen toward its own edge, in both directions. `inset-inline-end`
   already picked the side; this has to be told, because a translate is
   geometry and does not flip with the writing mode. */
:root[dir="rtl"] .cart-drawer { translate: -100% 0; }
:root[dir="ltr"] .cart-drawer { translate: 100% 0; }

/* The open rule names the direction too, and must. Two classes lose to a
   pseudo-class plus an attribute plus a class, so a bare
   `.cart-drawer.is-open` is outranked by the very rule it exists to undo —
   the class goes on, the transition runs, and it runs from 100% to 100%.
   The navigation drawer had exactly this bug; see the longer note beside
   `.shop-drawer` in shop-header.css. */
:root[dir="rtl"] .cart-drawer.is-open,
:root[dir="ltr"] .cart-drawer.is-open { translate: 0 0; }

@media (prefers-reduced-motion: reduce) {
  .cart-drawer { transition: none; }
}

.cart-drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-block-end: 1px solid var(--color-line);
}

.cart-drawer__title {
  margin: 0;
  font-family: var(--font-display-ar);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-tight);
  color: var(--color-text);
}
:root[data-lang="en"] .cart-drawer__title { font-family: var(--font-latin); }

.cart-drawer__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding: var(--space-5);
  gap: var(--space-5);
}

/*
   While a request is in flight.

   Not a spinner: the panel already holds the basket as it was a moment
   ago, and replacing a correct list with a spinning circle to show that
   a quantity went from two to three is a worse answer than leaving the
   list alone. It dims and stops taking clicks, which says «this is not
   current» without taking the information away.
*/
.cart-drawer.is-busy .cart-drawer__body {
  opacity: 0.55;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out);
}

/* ---------------------------------------------------------
   The lines
   --------------------------------------------------------- */
.cart-panel__lines {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}

.cart-panel__line {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: var(--space-4);
  align-items: start;
  padding-block: var(--space-4);
  border-block-end: 1px solid var(--color-line);
}
.cart-panel__line:first-child { padding-block-start: 0; }

.cart-panel__shot {
  display: block;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
}
.cart-panel__shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Same answer as the grid tile and the basket line: an unphotographed
   product is named, not left as a grey rectangle. */
.cart-panel__plate {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: var(--space-2);
  background:
    repeating-linear-gradient(
      135deg,
      transparent 0 9px,
      color-mix(in srgb, var(--color-line) 60%, transparent) 9px 10px
    );
  text-align: center;
}

.cart-panel__plate-text {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--fs-eyebrow);
  line-height: var(--lh-snug);
  color: var(--color-text-secondary);
}

.cart-panel__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.cart-panel__name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  line-height: var(--lh-snug);
  color: var(--color-text);
}
.cart-panel__name:hover { color: var(--color-growth-text); }

.cart-panel__variant {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

.cart-panel__controls {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-block-start: var(--space-1);
}

/* The stepper, at the size a 400px panel has room for. */
.qty--sm .qty__step { width: 30px; }
.qty--sm .qty__input { width: 40px; font-size: var(--fs-sm); }

/* The 16px floor from shop.css, restated: this rule's extra class would
   otherwise outrank it and bring the zoom-on-focus back on a phone. */
@media (pointer: coarse) {
  .qty--sm .qty__input { font-size: 16px; }
}

.cart-panel__total {
  margin: 0 auto 0 0;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--color-text);
}
:root[dir="ltr"] .cart-panel__total { margin: 0 0 0 auto; }

.cart-panel__drop {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}
.cart-panel__drop:hover { color: var(--color-danger); }
.cart-panel__drop svg { width: 15px; height: 15px; }

/* ---------------------------------------------------------
   The floor
   --------------------------------------------------------- */
/*
   Stuck to the bottom of the panel rather than sitting under the last
   line. A shopper with six things in the basket should not have to
   scroll a drawer to find the button the drawer was opened to offer.
*/
.cart-panel__foot {
  position: sticky;
  bottom: calc(var(--space-5) * -1);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-block-start: auto;
  padding-block: var(--space-4) var(--space-5);
  border-block-start: 1px solid var(--color-line);
  background: var(--color-surface);
}

.cart-panel__sum {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
}
.cart-panel__sum strong {
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
  color: var(--color-text);
}

.cart-panel__note {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

.cart-panel__all {
  align-self: center;
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
  border-block-end: 1px solid var(--color-line-gold);
  padding-block-end: 2px;
}
.cart-panel__all:hover { color: var(--color-gold-text); }

/* ---------------------------------------------------------
   Nothing in it
   --------------------------------------------------------- */
.cart-panel__empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  margin-block: auto;
  padding-block: var(--space-10);
  text-align: center;
}

.cart-panel__empty-title {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--color-text);
}

.cart-panel__empty-body {
  margin: 0 0 var(--space-4);
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

/* components/countdown.css */
/* =========================================================
   Component — Offer countdown

   A live clock under an offer with an end date. Warning tokens, not
   danger: nothing has gone wrong, the offer is only ending.
   ========================================================= */
.countdown {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-block-start: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--color-warning-soft);
  color: var(--color-warning);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
}

.countdown__clock {
  font-family: var(--font-figures);
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-bold);
}

/* components/product-card.css */
/* =========================================================
   Component — Product card

   The whole tile is one link, so the hit area is the tile and
   not a 14px line of text. Everything else here is restraint:
   one frame, one name, one price, and a flag only when there
   is something true to flag.
   ========================================================= */
/*
   One frame around the whole tile, on a plain raised surface
   (owner's call, 2026-09-09, confirmed 2026-09-17).

   A name and a price loose on the band do not read as the
   caption of the picture above them; one border around both
   makes that answer free. On 2026-09-17 the frame was taken
   off for an afternoon, and the owner compared «خطوة» (framed)
   with «زهرة النارنج» (loose) and chose the frame.

   What stayed gone is the cyan glass that used to fill the
   caption: it put the same pale blue on every shop whatever
   its own colours were. The surface is the shop's own raised
   ground now. A merchant who wants the photograph alone picks
   the «بلا إطار» card (data-card="frameless" in
   themes/layouts.css), which is where that look belongs.
*/
.pcard {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
  overflow: hidden;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface-raised);
  color: var(--color-text);
  transition: border-color var(--dur-base) var(--ease-out);
}
.pcard:hover {
  color: var(--color-text);
  border-color: var(--color-line-strong);
}

/* ---- Frame ---- */
/*
   The ratio is a variable because themes/layouts.css sets a
   different one per arrangement — 4/5 standard, 2/3 fashion,
   1/1 yookeh — and it used to set them by writing aspect-ratio
   straight onto this element at :root[data-layout] specificity.
   Nothing could override that from here without an escalation
   the layouts file explicitly asks callers not to make, which
   is how the hero tile below spent its first draft rendering
   at the ordinary portrait ratio and twice the height it owed.

   So the property lives here, once, and the layouts hand it a
   value. The fallbacks are the standard arrangement, for any
   page that renders a tile outside the shop shell.
*/
.pcard__frame {
  position: relative;
  display: block;
  aspect-ratio: var(--pcard-ratio, 4 / 5);
  overflow: hidden;
  background: var(--color-surface-sunken);
}

/* The zoom is 500ms, longer than any token. Quick enough to see it start
   and it reads as a twitch; at half a second the photograph leans in. */
.pcard__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition:
    scale 500ms var(--ease-out),
    opacity var(--dur-base) var(--ease-out);
}

/* Hover only where there is one. On a touch screen :hover sticks to the
   tile that was tapped, and the shopper would watch the picture change
   under their finger on the way to the product page. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .pcard:hover .pcard__img { scale: 1.04; }
}

/*
   The second photograph.

   The one gesture every expensive grid has in common, and the only
   one of them that adds information rather than decoration: the
   shopper sees the back of the jacket, the bottle from the other
   side, the shoe on a foot — without leaving the row they are
   scanning, and without the tile having to say anything more.

   It costs nothing to serve. A listing already loads the images
   relation to find the first one, so the second is a row that is
   already in memory. It stays lazy even on the eagerly-loaded first
   four: it is not part of the first paint and should not compete
   with what is.

   Where there is no second photograph, and on a touch screen where
   there is no hover to have, nothing happens at all. Both are
   silence rather than an effect that half fires.
*/
.pcard__img--alt {
  position: absolute;
  inset: 0;
  opacity: 0;
}
@media (hover: hover) {
  .pcard:hover .pcard__img--alt { opacity: 1; }
}

/*
   No photograph yet.

   A shop that has not uploaded pictures should not show a grid of
   grey rectangles — that reads as a page that failed to load. The
   tile falls back to the product's own name set as a printed label
   over a hatched ground, which is a thing rather than an absence.
*/
.pcard__plate {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  padding: var(--space-5);
  background:
    repeating-linear-gradient(
      135deg,
      transparent 0 11px,
      color-mix(in srgb, var(--color-line) 60%, transparent) 11px 12px
    );
  text-align: center;
}

.pcard__plate-text {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  padding: var(--space-2) var(--space-3);
  border-block: 1px solid var(--color-line-gold);
  background: var(--color-surface-sunken);
  color: var(--color-text-secondary);
  font-family: var(--font-display-ar);
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
}
:root[data-lang="en"] .pcard__plate-text { font-family: var(--font-latin); }

/* ---- Flags ---- */
/* Square, not a pill, and one size smaller. A pill is a badge and a
   badge is a thing the shop is saying; this is a printed mark on a
   picture, which is what a discount actually is. */
.pcard__flag {
  position: absolute;
  inset-inline-start: var(--space-3);
  top: var(--space-3);
  padding: 0.25rem 0.5rem;
  border-radius: var(--radius-xs);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-eyebrow);
  line-height: 1.6;
}

/* A real discount is the one thing on a shopping grid worth an accent,
   so it gets the brass and nothing else on the page does. */
.pcard__flag--sale {
  background: var(--color-gold);
  color: var(--color-canvas-deep);
}

/* The figure reads -23%, which reverses to 23%- when it inherits an RTL
   paragraph. The isolation is on the number and not on the box: a
   direction set on the box also decides what inset-inline-start means,
   and this flag was landing in the corner opposite the one the rule
   above puts it in — under the heart, once there was a heart. */
.pcard__flag--sale bdi {
  direction: ltr;
  unicode-bidi: isolate;
}

.pcard__flag--out {
  inset-inline-start: auto;
  inset-inline-end: var(--space-3);
  background: var(--color-surface-raised);
  box-shadow: inset 0 0 0 1px var(--color-line-strong);
  color: var(--color-text-secondary);
}

/* On video: a small play mark in the lower corner, over the
   photograph. Fixed ink and shade — it sits on a picture, not on
   the page. */
.pcard__clip {
  position: absolute;
  inset-inline-start: var(--space-3);
  bottom: var(--space-3);
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--athar-neutral-900) 55%, transparent);
  color: var(--athar-paper-50);
  backdrop-filter: blur(6px);
}
.pcard__clip svg { width: 14px; height: 14px; }
/* Not the second photograph: it matched this rule too and sat at 0.4 over
   the first one on every sold-out tile, hovered or not. */
.pcard.is-sold-out .pcard__img:not(.pcard__img--alt) { opacity: 0.4; }
.pcard.is-sold-out .pcard__plate { opacity: 0.55; }
@media (hover: hover) {
  .pcard.is-sold-out:hover .pcard__img--alt { opacity: 0.4; }
}

/*
   While the photograph loads.

   The frame paints the picture's own average colour (ProductImage::color),
   pulled a fifth of the way toward the ground so a saturated product does
   not flash on a dark page, and the photograph fades in over it.

   Behind .has-img-fade, which shop.js sets in the same task that marks the
   images already loaded. A page whose script never arrived shows its
   pictures the plain way instead of not at all.

   The colour goes once the first photograph is in — behind a transparent
   cut-out it would otherwise stay for the life of the page — and it goes
   after the fade, not at its start, or the frame blinks grey first.

   Last in the image rules on purpose: the hidden state has to win the
   0,4,0 ties with the sold-out rules above.
*/
:root.has-img-fade .pcard__frame {
  transition: background-color 0s linear var(--dur-base);
}
:root.has-img-fade .pcard__frame:has(> .pcard__img:not(.pcard__img--alt):not(.is-loaded)) {
  background-color: color-mix(in srgb, var(--pcard-tint, var(--color-surface-sunken)) 80%, var(--color-surface-sunken));
}
:root.has-img-fade .pcard__img:not(.is-loaded) { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .pcard__img { transition: none; }
}

/* ---- Body ---- */
/*
   The price sits on the floor of the tile, not under the name.

   Tiles in a row are already the same height — the grid stretches them —
   but the body used to be content-height, so the price landed wherever the
   name and the note left it. A row where one tile carries a line of fact
   and its neighbour does not then prints its price twelve pixels lower,
   and a grid whose prices are not on one line is read as a grid that is
   slightly broken rather than as one with something extra to say.
*/
.pcard__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
  padding: var(--space-3);
}

.pcard__name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  line-height: var(--lh-snug);
  color: var(--color-text);
  transition: color var(--dur-fast) var(--ease-out);
}
.pcard:hover .pcard__name { color: var(--color-growth-text); }

/*
   The one line of fact.

   Deliberately not an accent colour. The comment on the sale flag above
   claims the brass for a real discount and nothing else on the grid, and
   a second warm colour arguing with it three lines lower would make both
   of them mean less. «آخر قطعتين» carries its own urgency in the words,
   so the difference between an urgent line and an ordinary one is a step
   up the neutral ramp and half a weight — visible when the eye is on the
   tile, invisible when it is scanning the row.
*/
/*
   Both tiers moved up one notch when the plate arrived, and the
   numbers are the reason. Measured on the composited plate: the
   muted ink came out at 3.02:1 in light and 4.00:1 in dark,
   against 3.53 and 5.21 on the bare band it used to sit on. A
   tinted surface is darker than the page it tints, and --fs-xs
   text at 3:1 is not small print, it is decoration.

   The step between an ordinary line and an urgent one survives
   intact — it is still one rung and half a weight, just both
   rungs higher: secondary against text, 6.07:1 and 13.16:1 in
   light, 7.44:1 and 7.86:1 in dark.
*/
.pcard__note {
  font-size: var(--fs-xs);
  line-height: var(--lh-snug);
  color: var(--color-text-secondary);
}

.pcard__note--low {
  color: var(--color-text);
  font-weight: var(--fw-medium);
}

.pcard__price {
  margin-block-start: auto;
  padding-block-start: var(--space-1);
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/*
   Medium, not bold.

   A bold price on every tile is a grid shouting a number twelve
   times, and a shop where the number is the loudest thing on the
   picture is a discount shop — which is the opposite of the read
   this pass is after. The price still has full contrast and the
   figures still have their own font feature; it just stops being
   the first thing the eye lands on in a row of photographs.
*/
.pcard__now {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--color-text);
}

/* Same move, same reason. The rule through it is what says this
   price is the old one; the colour was never carrying that, and
   at 3:1 it was only making the figure hard to read. */
.pcard__was {
  font-size: var(--fs-xs);
  color: var(--color-text-secondary);
  text-decoration-thickness: 1px;
}

/* =========================================================
   The hero tile

   One tile in the first grid, two columns wide. Everything
   below is arranged around a single number: the enlarged
   frame has to come out roughly as tall as the ordinary
   frames beside it, or the first row of the shop steps.

   At the 1180 container that is 4 columns of 280 with 20px
   gaps. An ordinary frame is 4/5, so 350 tall. The hero is
   2·280 + 20 = 580 wide, and 16/10 puts it at 362 — twelve
   pixels of difference across a 350px row, which the grid's
   own stretch absorbs. Landscape is not a style choice here;
   it is the only ratio that makes the row lie flat.

   Two of four, and — on a phone, where .pgrid is two across —
   the full width, which is the best thing that can happen to
   the first product a shopper sees on a 375px screen. Not at
   three across: the counts the home page enlarges on leave a
   hole there, so between .pgrid's 640 and 1000 breakpoints
   the slot does not span (the span lives on .pcard-slot--hero
   in components/social-proof.css) and everything below stands
   down with it, sentence included.
   ========================================================= */
@media (max-width: 639.98px), (min-width: 1000px) {
  .pcard--hero .pcard__frame {
    aspect-ratio: var(--pcard-hero-ratio, 16 / 10);
  }

  /* The name and the price step up one size, and no further.
     This tile is an accent inside a grid, not a banner: at
     --fs-h3 it stops belonging to the row it is standing in. */
  .pcard--hero .pcard__name {
    -webkit-line-clamp: 2;
    font-size: var(--fs-lg);
    font-weight: var(--fw-bold);
  }

  .pcard--hero .pcard__now {
    font-size: var(--fs-base);
  }
}

/* The fourth thing — the sentence no other tile has room
   for. Two lines and a reading measure: the width that makes
   the tile a hero would otherwise make this one long line
   running the whole way across, which is harder to read than
   the small tiles it is meant to outshine. */
.pcard__short {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-inline-size: 46ch;
  color: var(--color-text-secondary);
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
}

@media (min-width: 640px) and (max-width: 999.98px) {
  .pcard__short { display: none; }
}

/* ---- The star line on a tile ----
   Between the name and the price, and never instead of the one
   line of fact: a tile can carry both and they answer different
   questions. */
.pcard .stars { margin-block-start: 0.125rem; }


/* components/stars.css */
/* =========================================================
   Component — Stars

   Five glyphs and a clipped copy of the same five on top of
   them. No half-star image, no sprite, no icon font: the
   merchant chooses the shop's typeface and a rating drawn out
   of that typeface's own ★ is the one drawing that cannot fall
   back to a missing file.

   The fill is a percentage rather than a rounded whole star,
   because 4.2 and 4.8 are different sentences and a shopper
   reading a shelf is comparing them.
   ========================================================= */
.stars {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--color-text-secondary);
  font-size: var(--fs-xs);
  line-height: 1;
  text-decoration: none;
}

/* The row is one line of text with a second line laid exactly
   over it, so both copies must have the same metrics and no
   letter-spacing of their own. Direction is forced to ltr:
   a partially filled row is clipped from its start edge, and
   in an RTL paragraph the start edge is the other side — the
   fill would grow from the wrong end and 4.2 would read 0.8. */
.stars__row {
  position: relative;
  display: inline-block;
  direction: ltr;
  unicode-bidi: isolate;
  white-space: nowrap;
  font-family: var(--font-figures);
  letter-spacing: 0.06em;
}

.stars__back { color: var(--color-line-strong); }

.stars__fill {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  overflow: hidden;
  white-space: nowrap;
  color: var(--color-gold);
}

.stars__value {
  color: var(--color-text);
  font-weight: var(--fw-semibold);
}

.stars__count { color: var(--color-text-muted); }

/* ---- Sizes ----
   Two, and they are the two places a rating appears: on a tile
   in a grid, and beside a product's name. A third size would be
   a third opinion about how loud a rating is. */
.stars--sm { font-size: var(--fs-xs); }
.stars--sm .stars__row { font-size: 0.8125rem; }

.stars--md { font-size: var(--fs-sm); }
.stars--md .stars__row { font-size: 1.0625rem; }

/* As a link it is the way into the reviews, and it says so on
   hover — but only there. A permanently underlined rating in a
   grid of twenty tiles is twenty underlines. */
a.stars:hover .stars__value { text-decoration: underline; }
a.stars:hover { color: var(--color-text); }

a.stars:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 3px;
  border-radius: var(--radius-xs);
}

/* components/social-proof.css */
/* =========================================================
   Social proof, and the small print around a purchase
   =========================================================

   Eight pieces that arrived together because they answer one
   question in different places: is this shop worth trusting
   with money.

     .rate       the five radios a shopper fills one in with
     .review     one verdict
     .assure     the promises under the buy button
     .freeship   how far the basket is from free delivery
     .announce   the one line the merchant wants read first
     .pcard-slot the heart that sits beside a tile
     .buybar     the phone's persistent buy button

   Nothing here invents a colour. The stars are the palette's
   gold, because gold is what a star is; everything else runs
   on the shop's own line, text and surface tokens, so a shop
   themed rose does not grow a blue promise row.
   ========================================================= */

/* The star row itself lives in components/stars.css, which is the
   one authority on it: this file had a second copy of those rules
   and the two disagreed about direction — theirs forces the row to
   ltr so a partial fill grows from the right end of an Arabic line,
   which is correct, and this file's copy was quietly undoing it. */

/* ---------------------------------------------------------
   The rating a shopper gives
   --------------------------------------------------------- */
.rate {
  border: 0;
  padding: 0;
  margin: 0 0 var(--space-4);
}

.rate__legend {
  padding: 0;
  margin-block-end: var(--space-2);
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

/*
  Reversed, so that hovering one star can light the ones before it
  with a sibling selector — CSS can only look forward. The visual
  order is restored by the row itself, so five is still the far
  star in both languages.
*/
.rate__stars {
  display: inline-flex;
  flex-direction: row-reverse;
  justify-content: flex-end;
  gap: var(--space-1);
  font-size: var(--fs-h3);
  line-height: 1;
}

.rate__star {
  color: var(--color-line-strong);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}

.rate__star:hover,
.rate__star:hover ~ .rate__star,
.rate__input:checked ~ .rate__star,
.rate__input:checked + .rate__star { color: var(--color-gold); }

/* The radios are visually hidden, so the focus ring has to be drawn
   on the label, which is the part a shopper can actually see. */
.rate__input:focus-visible + .rate__star {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

/* ---------------------------------------------------------
   The reviews themselves
   --------------------------------------------------------- */
.reviews {
  display: grid;
  gap: var(--space-8);
  align-items: start;
}

@media (min-width: 62rem) {
  /* The list gets the width; the form is a column beside it rather
     than a wall of fields under a wall of text. */
  .reviews {
    grid-template-columns: minmax(0, 1fr) 22rem;
    gap: var(--space-12);
  }
}

.reviews__list {
  display: grid;
  gap: var(--space-5);
}

.review {
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

.review__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-block-end: var(--space-2);
}

.review__who { font-weight: 600; }

.review__verified {
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-success-soft);
  color: var(--color-success);
  font-size: var(--fs-xs);
}

.review__when {
  margin-inline-start: auto;
  color: var(--color-text-muted);
  font-size: var(--fs-xs);
}

.review__body {
  margin: var(--space-2) 0 0;
  color: var(--color-text);
  line-height: var(--lh-relaxed);
}

.reviews__none,
.reviews__more {
  color: var(--color-text-muted);
  font-size: var(--fs-sm);
}

.reviews__thanks {
  padding: var(--space-3);
  margin-block-end: var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--color-success-soft);
  color: var(--color-success);
  font-size: var(--fs-sm);
}

.reviews__form {
  padding: var(--space-5);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
}

/* The form behind «اكتب رأيك». The summary is drawn as the shop's
   quiet button; the marker triangle goes. */
.reviews__compose > summary {
  list-style: none;
  cursor: pointer;
}
.reviews__compose > summary::-webkit-details-marker { display: none; }
.reviews__compose[open] > summary { margin-block-end: var(--space-4); }

/* No reviews yet: an invitation, not a two-column empty table. */
.reviews--empty {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
}
.reviews--empty .reviews__compose { inline-size: min(100%, 32rem); }

/* ---------------------------------------------------------
   The promises under the button
   --------------------------------------------------------- */
.assure {
  list-style: none;
  margin: var(--space-5) 0 0;
  padding: var(--space-4);
  display: grid;
  gap: var(--space-3);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  font-size: var(--fs-sm);
}

.assure__row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--color-text);
}

.assure__row svg {
  flex: none;
  width: 20px;
  height: 20px;
  color: var(--color-primary);
}

/* ---------------------------------------------------------
   The one line Athar writes on a merchant's page

   Written after .assure__row deliberately: inside the assurance
   box the mark rides on that row, and it has to win the colour —
   this line is the agency's, not the merchant's palette.
   --------------------------------------------------------- */
.verified {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: var(--space-2);
  row-gap: var(--space-1);
  margin: 0;
  color: var(--color-success);
  font-weight: var(--fw-medium);
}

/* In em, so one mark serves three type sizes: the footer's caption,
   the assurance row, and the line under «أرسل الطلب». */
.verified svg {
  flex: none;
  inline-size: 1.15em;
  block-size: 1.15em;
  color: currentColor;
}

/* The answer to «what was actually checked», folded. Open it and the
   page does not move anywhere: a link out of the buy button is a way
   out of the purchase. */
.verified__why {
  flex-basis: 100%;
  color: var(--color-text-muted);
  font-size: var(--fs-xs);
  font-weight: var(--fw-regular);
}

.verified__why > summary {
  cursor: pointer;
}

.verified__why > p {
  margin: var(--space-1) 0 0;
  line-height: var(--lh-relaxed);
}

/* ---------------------------------------------------------
   Free delivery, and how far away it is
   --------------------------------------------------------- */
.freeship {
  margin-block-end: var(--space-4);
  font-size: var(--fs-sm);
}

.freeship__line {
  margin: 0 0 var(--space-2);
  color: var(--color-text);
}

.freeship.is-met .freeship__line { color: var(--color-success); }

.freeship__track {
  display: block;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-line);
  overflow: hidden;
}

.freeship__fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--color-primary);
  transition: width var(--dur-slow) var(--ease-out);
}

.freeship.is-met .freeship__fill { background: var(--color-success); }

/* ---------------------------------------------------------
   The announcement
   --------------------------------------------------------- */
.announce {
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-size: var(--fs-sm);
}

.announce__inner {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 2.4rem;
  padding-block: var(--space-2);
}

.announce__text {
  flex: 1;
  margin: 0;
  text-align: center;
  color: inherit;
}

a.announce__text {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.announce__close {
  flex: none;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: inherit;
  cursor: pointer;
  opacity: 0.75;
}

.announce__close:hover { opacity: 1; }
.announce__close svg { width: 14px; height: 14px; }

/* ---------------------------------------------------------
   The heart on a tile
   --------------------------------------------------------- */
.pcard-slot {
  position: relative;
  display: flex;
  /* A column so the quick-add can be the last child and land under
     the tile rather than beside it. The tile still takes the whole
     slot: it is the only flexible one. */
  flex-direction: column;
  min-width: 0;
}

/* The wide tile's span moved here with the wrapper: the slot is the
   grid item now, and the tile inside it fills whatever it got.

   Two across and four across only — the breakpoints are .pgrid's in
   pages/shop.css. home.blade.php enlarges a tile only on counts that
   fill whole rows at those two widths (3, 7, 11), and at three across
   those counts end one or two cells short: seven picks and a wide
   tile are eight cells, one shy of nine. There the tile is ordinary,
   and components/product-card.css drops its look at the same width. */
@media (max-width: 639.98px), (min-width: 1000px) {
  .pcard-slot--hero { grid-column: span 2; }
}

.pcard-slot > .pcard {
  flex: 1;
  min-width: 0;
}

.pcard__save {
  position: absolute;
  inset-block-start: var(--space-3);
  inset-inline-end: var(--space-3);
  display: none;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--color-surface) 88%, transparent);
  color: var(--color-text-muted);
  cursor: pointer;
  backdrop-filter: blur(6px);
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}

/* Only for a browser that ran the script: the list lives in that
   browser's own storage, and a heart without it forgets. */
:root.has-wishlist .pcard__save { display: grid; }

/* «نفد» stands in this corner too. The heart steps below it rather
   than sitting on top of it — the tile is a sibling of the button, so
   the tile's own state can say so. */
.pcard.is-sold-out + .pcard__save {
  inset-block-start: calc(var(--space-3) + 2.1rem);
}

.pcard__save svg { width: 18px; height: 18px; }

.pcard__save:hover {
  color: var(--color-danger);
  border-color: var(--color-danger);
}

.pcard__save.is-saved {
  color: var(--color-danger);
  border-color: var(--color-danger);
}

.pcard__save.is-saved svg { fill: currentColor; }

.shop-save {
  position: relative;
  display: none;
  align-items: center;
  padding: var(--space-2);
  color: var(--color-text-muted);
}

/* :not([hidden]) rather than a later [hidden] rule: the reveal selector
   carries more weight than a plain attribute one, and a link that stays
   on screen saying «0» is exactly what the hidden attribute was for. */
:root.has-wishlist .shop-save:not([hidden]) { display: inline-flex; }
.shop-save:hover { color: var(--color-text); }
.shop-save svg { width: 22px; height: 22px; }

.shop-save__count {
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: 0;
  min-width: 16px;
  padding: 0 4px;
  border-radius: var(--radius-pill);
  background: var(--color-danger);
  color: #fff;
  font-size: 10px;
  line-height: 16px;
  text-align: center;
}

/* ---------------------------------------------------------
   One press, on a tile

   Bottom of the frame rather than beside the price: the
   photograph is the part of a tile a thumb is already over,
   and a control down in the body would sit under the name it
   is meant to be about.

   It is drawn for touch first — always on screen below 62rem,
   because there is no hover on a phone and a button that only
   appears on one is a button a phone will never see. On a
   pointer it fades in with the tile, which keeps a grid of
   twelve from reading as a grid of twelve buttons.
   --------------------------------------------------------- */
.pcard__add {
  margin-block-start: var(--space-2);
}

.pcard__add-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--color-surface) 92%, transparent);
  color: var(--color-text);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  cursor: pointer;
  backdrop-filter: blur(6px);
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}

.pcard__add-btn svg { width: 15px; height: 15px; }

.pcard__add-btn:hover {
  border-color: var(--color-primary);
  background: var(--color-primary);
  color: var(--color-on-primary);
}

/* On a pointer it fades rather than appears, and the row it sits in
   is reserved whether it is showing or not — a button that pushes the
   tile taller on hover makes the grid jump under the cursor that is
   reaching for it.

   opacity alone, never display or visibility: the button stays in the
   tab order, and focus-within brings it up for the keyboard. */
@media (min-width: 62rem) and (hover: hover) {
  .pcard__add { opacity: 0; transition: opacity var(--dur-fast) var(--ease-out); }

  .pcard-slot:hover .pcard__add,
  .pcard-slot:focus-within .pcard__add { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .pcard__add { transition: none; }
}

.wish__empty {
  display: grid;
  justify-items: start;
  gap: var(--space-3);
  padding: var(--space-8) 0;
  color: var(--color-text-muted);
}

.wish__hint {
  margin: 0;
  font-size: var(--fs-sm);
}

/* ---------------------------------------------------------
   The buy bar a phone keeps
   --------------------------------------------------------- */
.buybar {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 40;
  padding: var(--space-3) 0;
  border-block-start: 1px solid var(--color-line);
  background: color-mix(in srgb, var(--color-surface) 94%, transparent);
  backdrop-filter: blur(10px);
  transform: translateY(100%);
  transition: transform var(--dur-base) var(--ease-out);
}

.buybar.is-up { transform: translateY(0); }

/* A shopper on a laptop keeps the real button in view for most of
   the page, and a docked bar there is a banner, not a shortcut. */
@media (min-width: 62rem) {
  .buybar { display: none; }
}

.buybar__inner {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: min(100% - 2rem, 60rem);
  margin-inline: auto;
}

.buybar__price {
  font-size: var(--fs-lg);
  font-weight: 600;
}

.buybar__btn { flex: 1; }

/* components/shop-footer.css */
/* =========================================================
   Component — Storefront footer

   Four columns that collapse to one, then a bar carrying the
   three things a shopper checks last: whose shop this is, what
   it takes as payment, and who runs the platform underneath.
   ========================================================= */
.shop-footer {
  margin-block-start: var(--space-20);
  border-block-start: 1px solid var(--color-line);
  background: var(--color-canvas-deep);
}

.shop-footer__grid {
  display: grid;
  gap: var(--space-8);
  grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
  padding-block: var(--space-12);
}

.shop-footer__col {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.shop-footer__col--brand { gap: var(--space-3); }

.shop-footer__name {
  font-family: var(--font-display-ar);
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  color: var(--color-text);
}
:root[data-lang="en"] .shop-footer__name { font-family: var(--font-latin); }

.shop-footer__note {
  max-width: 34ch;
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
}
.shop-footer__note--place { color: var(--color-text-muted); }

.shop-footer__head {
  margin: 0 0 var(--space-2);
  font-family: var(--font-body-ar);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-bold);
  color: var(--color-gold-text);
}
:root[data-lang="en"] .shop-footer__head {
  font-family: var(--font-latin);
  text-transform: uppercase;
  letter-spacing: var(--tracking-eyebrow);
}

.shop-footer__link {
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
}
.shop-footer__link:hover { color: var(--color-growth-text); }

/* The shop's accounts. Line glyphs in the footer's own grey rather
   than each network's colours — five brand colours in a row would be
   the loudest thing on the merchant's page. 44px so a thumb lands. */
.shop-footer__socials {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

.shop-footer__social {
  display: inline-grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-pill);
  color: var(--color-text-secondary);
  transition: color 150ms ease, border-color 150ms ease;
}
.shop-footer__social:hover {
  color: var(--color-growth-text);
  border-color: currentColor;
}
.shop-footer__social:focus-visible {
  outline: 2px solid var(--color-growth-text);
  outline-offset: 2px;
}

.shop-footer__social svg {
  width: 1.25rem;
  height: 1.25rem;
}

/* ---- The bar ---- */
.shop-footer__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4) var(--space-6);
  padding-block: var(--space-5);
  border-block-start: 1px solid var(--color-line);
  font-size: var(--fs-xs);
}

.shop-footer__rights {
  margin: 0;
  color: var(--color-text-muted);
}

.shop-footer__pay {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.shop-footer__method {
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-pill);
  color: var(--color-text-secondary);
}

/* Verified by Athar.

   Quiet on purpose. It is a fact about the shop, not a sales badge,
   and a loud green pill under the shop's own name would outrank the
   name. The word travels with the tick — colour alone never carries
   a meaning on this platform. */
/* The mark itself is .verified in social-proof.css — shape, colour and
   icon. What belongs to the footer is only where it sits in the column
   and how small it is down here. */
.shop-footer__verified {
  margin: var(--space-1) 0 0;
  font-size: var(--fs-xs);
}

/* The platform's one appearance on a merchant's shop. Small, and
   at the very bottom — this page belongs to the merchant. */
.shop-footer__athar {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--color-text-muted);
}
.shop-footer__athar:hover { color: var(--color-text-secondary); }

.shop-footer__athar-mark {
  width: auto;
  height: 20px;
  opacity: 0.85;
}

@media (max-width: 560px) {
  .shop-footer__bar { justify-content: center; text-align: center; }
}

/* components/look-notice.css */
/* =========================================================
   «You are trying a look on»
   ---------------------------------------------------------
   The one strip on a storefront that is the platform talking to the
   merchant rather than the shop talking to a customer. It still borrows the
   shop's own tokens — surface, line, text, primary — because it has to sit
   legibly on six palettes in two grounds, and a fixed colour that looked
   right on «ورقي» would be invisible on «نبيذي».

   Fixed, and out of the flow: the preview exists to show the first screen
   as a customer gets it, and a strip that pushes the page down by its own
   height is showing a different first screen.
   ========================================================= */
.look-notice {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 60;

  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* A fixed height, the same as the showcase strip, so the room the page
     makes below and the place the header sticks are the strip itself
     rather than a guess at what its padding adds up to. */
  box-sizing: border-box;
  block-size: 2.5rem;
  padding: 0 var(--space-4);

  border-block-end: 1px solid var(--color-line);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: var(--fs-xs);
  line-height: 1.5;

  /* Above the sticky header, and translucent, so what is behind it is
     still readable as the page it is describing. */
  backdrop-filter: blur(8px);
}

.look-notice__dot {
  inline-size: 0.5rem;
  block-size: 0.5rem;
  flex: none;
  border-radius: 999px;
  background: var(--color-primary);
}

.look-notice__text {
  margin: 0;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.look-notice__text strong { color: var(--color-text); }

.look-notice__out {
  margin-inline-start: auto;
  flex: none;
  padding: 0.15rem var(--space-3);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-pill);
  color: var(--color-text);
  font-weight: var(--fw-bold);
  text-decoration: none;
}

.look-notice__out:hover { border-color: var(--color-primary); }

/* The strip covers the top of the page, and on a shop whose header is
   sticky it would cover that too. Pushing the body down by its height is
   the one place the flow does move — but only by 2rem, and only while a
   preview is open. */
.shop-page:has(.look-notice) { padding-block-start: 2.5rem; }

/* Same as the showcase strip: the sticky header stops under it. */
.shop-page:has(.look-notice) .shop-header { top: 2.5rem; }

/* The note is the half a narrow screen can lose: «معاينة رملي» is the
   sentence, the rest is the reassurance. */
@media (max-width: 30rem) {
  .look-notice__note { display: none; }
}

/* components/showcase-notice.css */
/* =========================================================
   «This shop is an example»
   ---------------------------------------------------------
   The strip on a showcase store, and the note that stands where its
   checkout button would be. Built like the look-notice beside it: fixed
   and out of the flow, on the shop's own tokens so it stays legible on
   every palette in both grounds.
   ========================================================= */
.showcase-notice {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 60;

  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* A fixed height, and the page below pushed down by exactly it. The strip
     used to size itself from its padding and line-height (40px) while the
     body made room for 2rem (32px), so it sat over the top 8px of every
     showcase shop's header. */
  box-sizing: border-box;
  block-size: 2.5rem;
  padding: 0 var(--space-4);

  border-block-end: 1px solid var(--color-line);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: var(--fs-xs);
  line-height: 1.5;
  backdrop-filter: blur(8px);
}

.showcase-notice__dot {
  inline-size: 0.5rem;
  block-size: 0.5rem;
  flex: none;
  border-radius: 999px;
  background: var(--color-primary);
}

.showcase-notice__text {
  margin: 0;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.showcase-notice__text > span:first-child { color: var(--color-text); }

.showcase-notice__out {
  margin-inline-start: auto;
  flex: none;
  padding: 0.15rem var(--space-3);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-pill);
  color: var(--color-text);
  font-weight: var(--fw-bold);
  text-decoration: none;
}

.showcase-notice__out:hover { border-color: var(--color-primary); }

.shop-page:has(.showcase-notice) { padding-block-start: 2.5rem; }

/* The header is sticky at the top of the window, and the strip is fixed
   over that same top: once the page moved, the header slid under it and
   lost its upper 40px — the shop name and the search. It sticks below the
   strip instead. */
.shop-page:has(.showcase-notice) .shop-header { top: 2.5rem; }

@media (max-width: 30rem) {
  .showcase-notice__note { display: none; }
}

/* Where «إتمام الطلب» would be, on the basket page and in the drawer. */
.showcase-stop {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border: 1px dashed var(--color-line);
  border-radius: var(--radius-md);
  color: var(--color-text-secondary);
  font-size: var(--fs-sm);
  line-height: 1.6;
  text-align: center;
}

/* pages/shop.css */
/* =========================================================
   Page — Storefront

   Bands shared by every shopper-facing page (the flash slot,
   the band heading, the product grid) plus what only the home
   page uses: the hero, the section rail, the video shelf and
   the journal cards.
   ========================================================= */

/* A short page — an empty search, a one-line policy page — should not
   leave the footer floating halfway up the window. */
.shop-page {
  display: flex;
  flex-direction: column;
  min-block-size: 100vh;

  /*
     The primary button's fill, in the merchant's colour and flat.

     tokens.css declares --gradient-primary on :root out of var() calls,
     and a custom property resolves its var()s where it is declared —
     so the value every shop inherited was the platform's teal, long
     before x-shop.theme put the merchant's --color-primary on this
     element. Declared again here, it resolves against the shop's own
     palette.

     Flat since 2026-09-17: the three-stop gradient, the sheen and the
     hover glow were the look of a generated interface, and a shop that
     reads as designed uses its colour solid. The dashboard's buttons
     are untouched.

     The ink follows for the same reason: --color-on-primary is the
     platform's dark teal on a dark ground, which StoreTheme never sets.
     --color-text-on-brand is the ink StoreTheme measured against this
     very fill (Palette::textOn), so the button reads it.
  */
  --gradient-primary: var(--color-primary);
  --gradient-sheen: none;
  --color-on-primary: var(--color-text-on-brand);

  /*
     The storefront rounds its corners harder than the platform does.

     A dashboard is made of panels and a 16px corner is what tells one
     panel from the next. A shop is made of photographs, and the same
     corner on a picture reads as a sticker — the tile, the gallery,
     the summary and the checkout panes were each rounded twice as
     hard as the thing inside them deserved.

     Declared here rather than edited into fourteen rules because it
     is one decision, and because the shop is where it applies: the
     dashboard reads tokens.css and is untouched. Nothing else loaded
     by this shell uses these two, so the blast radius is the shop's
     own panels.
  */
  --radius-lg: 0.625rem;
  --radius-xl: 0.75rem;

  /*
     Prices in the shop's own face.

     --font-figures is the system monospace so the dashboard's revenue
     columns line up. A shop has no such columns — one price under one
     name — and a price set in Consolas reads as a spreadsheet cell under
     a photograph. The store's body face keeps tabular-nums from
     .u-figure where the face has it.
  */
  --font-figures: var(--font-body-ar);
}
.shop-page .btn--primary:hover {
  --btn-bg: var(--color-primary-hover);
  --btn-shadow: none;
}
.shop-page > main { flex: 1 0 auto; }
.shop-page > .shop-footer { flex: none; }

.shop-flash { padding-block-start: var(--space-6); }

/* ---------------------------------------------------------
   Film grain

   The one thing on the storefront that carries no information.

   A perfectly flat fill is the tell of a page nobody touched after
   the palette was picked: a photograph has grain in it and printed
   paper has tooth, so a screen with neither reads as a render of a
   shop rather than as a shop. This is four hundredths of an opacity
   of noise and nothing else — at any value where a visitor could
   name it, it has stopped being texture and become a filter.

   Fixed to the viewport rather than tiled down the body: a repeating
   background on a page six thousand pixels tall repaints on every
   scroll frame, and the grain would travel with the photographs
   instead of sitting in front of them.
   --------------------------------------------------------- */
.shop-page::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.042;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Paper needs less of it. The same value that reads as tooth on a
   dark ground reads as dirt on a white one. */
[data-theme="light"] .shop-page::before { opacity: 0.026; }

/* ---------------------------------------------------------
   Band
   --------------------------------------------------------- */
.shop-band { padding-block: var(--space-16); }
.shop-band--tight { padding-block: var(--space-12); }

/*
   More air once there is room for it.

   The spacing was set at the width the bands were written at, and on
   a desktop the page reads as a stack of rows rather than a sequence
   of sections. Every shop that reads as expensive is buying most of
   that with vertical space and nothing else — it is the cheapest
   thing on this list and the one that changes the most.
*/
@media (min-width: 900px) {
  .shop-band { padding-block: var(--space-20); }
  .shop-band--tight { padding-block: var(--space-16); }
}

.shop-band--sunken {
  border-block: 1px solid var(--color-line);
  background: var(--color-canvas-deep);
}

.shop-band__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3) var(--space-6);
  /*
     A rule under the heading, the way a magazine opens a section.

     It is also the reason the bands do not need alternating grounds
     to be told apart: one hairline says «a new thing starts here»
     without the page changing colour six times on the way down. The
     tonal break is spent once, on «من خلف المتجر», and a page that
     spends it six times has not emphasised anything.
  */
  padding-block-end: var(--space-4);
  border-block-end: 1px solid var(--color-line);
  margin-block-end: var(--space-8);
}

/* One step up the scale, with the tracking a heading needs at that
   size — Arabic display type set at body tracking reads loose the
   moment it stops being body copy. */
.shop-band__title {
  margin: 0;
  font-size: var(--fs-h2);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
}

.shop-band__more {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--color-text-secondary);
  border-block-end: 1px solid var(--color-line-gold);
  padding-block-end: 2px;
}
.shop-band__more:hover { color: var(--color-gold-text); }

/* ---------------------------------------------------------
   Who is behind the shop

   The one band on the page with no tiles in it, and every
   decision here is set against that: a reading measure rather
   than a grid, a size the eye slows down for, and the sunken
   ground so the band reads as a held breath between two grids.

   Two columns — what was said, then who said it — and the
   same two whether or not a photograph exists. That is the
   whole trick of this band: a shop with no portrait is not a
   shop with a hole in its layout, it is a shop whose second
   column is a name. Which is where all four seeded shops
   stand, because nobody photographed these shopkeepers, and
   a silhouette standing in for a face is a picture of a
   person who does not exist.
   --------------------------------------------------------- */
.shop-voice__inner {
  display: grid;
  gap: var(--space-8);
  align-items: start;
}

@media (min-width: 860px) {
  .shop-voice__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
    gap: var(--space-12);
  }
}

.shop-voice__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-width: 0;
}

.shop-voice__title {
  margin: 0;
  font-size: var(--fs-h3);
}

/*
   The measure decides the line length, not the column. This is
   the only place on the page a visitor is asked to read more
   than a label, and a paragraph running the full width of a
   1180px band is a paragraph nobody finishes.
*/
.shop-voice__words {
  max-inline-size: 56ch;
  color: var(--color-text-secondary);
  font-size: var(--fs-lg);
  line-height: var(--lh-relaxed);
}
.shop-voice__words p { margin: 0; }
.shop-voice__words p + p { margin-block-start: var(--space-4); }

/* ---- Who ---- */
.shop-voice__who {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

.shop-voice__figure {
  margin: 0;
  overflow: hidden;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface-sunken);
}

.shop-voice__portrait {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}

/* A rule above the name, not a box around it: the signature is
   the end of a sentence the band started, and a card would make
   it a second thing on the page. */
.shop-voice__sign {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
  padding-block-start: var(--space-4);
  border-block-start: 1px solid var(--color-line-gold);
  font-size: var(--fs-sm);
}

.shop-voice__name {
  color: var(--color-text);
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
}

/* Secondary, not muted. Muted on the deep ground measures 3.56:1 in the
   light theme, and this line is set at --fs-sm — under the bar, and the
   session that raised a 4.39 to a 5.06 elsewhere on this page set that bar.
   Secondary reads 6.1:1 and the hierarchy against the name above survives on
   size and weight, which is where it should have been resting anyway. */
.shop-voice__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  color: var(--color-text-secondary);
}

/* The separator is drawn, not typed: a «·» written between two
   spans becomes a stray character the moment one of them is
   empty, and both the role and the city are optional. The rule
   is «any span that follows one», which cannot leave a dot at
   the front of the line however few of them there are. */
.shop-voice__meta > * + *::before {
  content: "·";
  margin-inline-end: var(--space-3);
  color: var(--color-line-strong);
}

/* ---------------------------------------------------------
   Hero

   Two shapes. With a cover photograph it is the photograph,
   dimmed enough for type to sit on it. Without one it is a
   plain title block on the sunken ground — no stock image, no
   gradient standing in for a picture nobody took.
   --------------------------------------------------------- */
.shop-hero {
  position: relative;
  overflow: hidden;
  border-block-end: 1px solid var(--color-line);
  background: var(--color-canvas-deep);
}

.shop-hero__inner {
  position: relative;
  z-index: 1;
  padding-block: var(--space-20);
}

/*
   With a photograph the hero stops being a band and becomes a window.

   It takes most of the first screen and the type sits on the floor of
   it rather than in the middle — which is the arrangement every shop
   that reads as expensive has arrived at, because it lets the picture
   be the argument and the words be the caption.

   78svh and not 100: a first screen with no product on it at all is a
   poster, and a shopper who has to scroll to learn that anything is
   for sale leaves. The clamp holds it usable on a laptop in landscape
   and stops it becoming a mural on a 4K monitor.

   svh rather than vh, because on a phone the browser chrome retracts
   as you scroll and vh measures the window it is not going to be.
*/
.shop-hero--cover {
  display: grid;
  align-items: end;
  min-block-size: clamp(440px, 78svh, 720px);

  /* Which corner the type stands in, and therefore which corner the
     wash below has to cover. Gradients do not mirror with direction
     the way padding does, so the one number that differs is named
     here and the gradient is written once. */
  --hero-wash-x: 100%;
}
:root[dir="ltr"] .shop-hero--cover { --hero-wash-x: 0%; }
.shop-hero--cover .shop-hero__inner {
  align-self: end;
  padding-block: var(--space-16);
}

/*
   Shorter on a phone, and not for the usual reason.

   The storefront header stacks on a narrow screen — announcement bar,
   then the shop's name and the menu, then the basket and the search
   field — and comes out around 230px before the hero has started. At
   78svh on top of that, the first screen is a header and a shop name
   and no goods at all, and the shopper has to scroll a whole window
   to find out the shop sells anything. 60svh puts the top of the next
   band on screen, which is the entire job of a shop front.
*/
@media (max-width: 700px) {
  .shop-hero--cover { min-block-size: clamp(380px, 60svh, 520px); }
  .shop-hero--cover .shop-hero__inner { padding-block: var(--space-10); }
}

.shop-hero__cover {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
   Three washes, and none of them covers the picture.

   The band the type used to sit under was a full-width gradient
   holding most of the frame at 70-odd percent of the ground colour.
   At the old height that was a stripe; at 78svh it is the photograph,
   and the first shop tried against it — a wall of fashion drawings —
   came out as a flat grey rectangle with a name on it. A hero that
   erases its own photograph is worse than one with no photograph.

   So the wash is a corner rather than a band. It is anchored where
   the type stands, at full strength under the words and gone by
   three-quarters of the way across, which leaves the other half of
   the frame at the exposure the photographer chose.

   Then two thin ones that are structure, not styling: a floor, so the
   picture meets the band below it instead of being cut off by it, and
   a shallow ceiling, because the header is sticky and mostly
   transparent and would otherwise put the shop's own name on whatever
   the photograph happens to be doing in that corner.
*/
/*
   The corner and the floor are dark in both themes (2026-09-17).

   They were the page's own ground, which on paper is near-white: the
   light shops laid a white fog over their photograph and set grey
   type on it, and the line under the name could not be read. A
   photograph takes light type over a dark shade whatever the page
   around it is, so these two read fixed primitives, and the type
   below reads a fixed light ink. The ceiling stays on the page's
   ground, because it is there for the header's own ink.
*/
.shop-hero--cover::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(
      118% 88% at var(--hero-wash-x) 100%,
      color-mix(in srgb, var(--athar-neutral-900) 82%, transparent) 0%,
      color-mix(in srgb, var(--athar-neutral-900) 56%, transparent) 30%,
      color-mix(in srgb, var(--athar-neutral-900) 18%, transparent) 56%,
      transparent 76%
    ),
    linear-gradient(
      to top,
      color-mix(in srgb, var(--athar-neutral-900) 60%, transparent) 0%,
      transparent 30%
    ),
    linear-gradient(
      to bottom,
      color-mix(in srgb, var(--color-canvas-deep) 46%, transparent) 0%,
      transparent 20%
    );
}

/*
   On a phone the words run the full width of the photograph, so a shade
   in one corner leaves half of every line on the bright part of the
   picture («خيط ولون», a white wall of sketches, was unreadable). The
   shade becomes a floor under the whole block of type instead.
*/
@media (max-width: 700px) {
  .shop-hero--cover::after {
    background:
      linear-gradient(
        to top,
        color-mix(in srgb, var(--athar-neutral-900) 90%, transparent) 0%,
        color-mix(in srgb, var(--athar-neutral-900) 72%, transparent) 42%,
        color-mix(in srgb, var(--athar-neutral-900) 20%, transparent) 72%,
        transparent 88%
      ),
      linear-gradient(
        to bottom,
        color-mix(in srgb, var(--color-canvas-deep) 46%, transparent) 0%,
        transparent 20%
      );
  }
}

/*
   One settle, once, and never on a loop — motion that repeats is a
   banner ad. It doubles as cover for the moment the photograph
   decodes: the frame arrives eight percent large and comes to rest,
   which reads as the page arriving rather than as an image popping in.
*/
@media (prefers-reduced-motion: no-preference) {
  .shop-hero__cover { animation: shop-hero-settle 1400ms var(--ease-out) both; }
}

@keyframes shop-hero-settle {
  from { scale: 1.08; opacity: 0.35; }
  to { scale: 1; opacity: 1; }
}

.shop-hero__name {
  max-width: 18ch;
  margin: 0 0 var(--space-4);
  font-size: var(--fs-h1);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
}

/* The photograph is what earns the larger size. A name set this big
   on a plain ground is a title page, not a shop front. */
.shop-hero--cover .shop-hero__name {
  color: var(--athar-paper-50);
  max-width: 15ch;
  font-size: clamp(2.5rem, 1.5rem + 4.2vw, 4.25rem);
  line-height: 1.08;
}

.shop-hero__tagline {
  max-width: 46ch;
  margin: 0 0 var(--space-8);
  font-size: var(--fs-lg);
  line-height: var(--lh-relaxed);
  color: var(--color-text-secondary);
}

.shop-hero--cover .shop-hero__tagline,
.shop-hero--cover .shop-hero__eyebrow {
  color: color-mix(in srgb, var(--athar-paper-50) 88%, transparent);
}

/* The quiet button, glass over the photograph rather than the
   page's own outline, which on paper was dark ink on a dark shade. */
.shop-hero--cover .btn--secondary {
  --btn-bg: color-mix(in srgb, var(--athar-neutral-900) 28%, transparent);
  --btn-fg: var(--athar-paper-50);
  --btn-border: color-mix(in srgb, var(--athar-paper-50) 55%, transparent);
  backdrop-filter: blur(8px);
}
.shop-hero--cover .btn--secondary:hover {
  --btn-bg: color-mix(in srgb, var(--athar-paper-50) 16%, transparent);
  --btn-fg: var(--athar-paper-50);
  --btn-border: var(--athar-paper-50);
}

/* Nothing follows the line under the name now — the way in moved out of
   the hero and onto its own row below the picture (.shop-rail). */
.shop-hero__copy > :last-child { margin-block-end: 0; }

/* =========================================================
   The rail under the photograph

   One row of pills on the ground below the hero, in the shape the
   theme store uses for its trades: the everything door filled in
   the accent, the sections outlined beside it, each carrying what
   it holds.

   It stands below the picture and not on it. Type over a
   photograph is readable only where the wash reaches, and the wash
   is a corner — a rail that wrapped to a second line on a phone
   walked out of the shade and onto whatever the picture was doing
   there. On the ground there is nothing to fight, so the row keeps
   one line and scrolls sideways rather than wrapping.

   The ground is the hero's own, so the strip reads as the plinth
   the photograph stands on rather than as the first band of the
   page.
   ========================================================= */
.shop-rail {
  position: relative;
  z-index: 1;
  border-block-end: 1px solid var(--color-line);
  background: var(--color-canvas-deep);
}

/*
   The fade is the only thing that says «there is more sideways» on a
   machine that draws no scrollbar. 1.25rem of it against the
   container's own --space-6 of padding, so the first and last pill
   come to rest clear of the gradient instead of half inside it.
*/
.shop-rail__inner {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-block: var(--space-4);
  overflow-x: auto;
  scroll-padding-inline: var(--space-6);
  scrollbar-width: none;
  scroll-snap-type: x proximity;
  -webkit-mask-image: linear-gradient(to left, transparent, #000 1.25rem, #000 calc(100% - 1.25rem), transparent);
  mask-image: linear-gradient(to left, transparent, #000 1.25rem, #000 calc(100% - 1.25rem), transparent);
}
.shop-rail__inner::-webkit-scrollbar { display: none; }

.shop-chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  /* The count sits inside the pill, so the space between word and
     figure is the pill's own rhythm and not a space in the markup. */
  gap: var(--space-2);
  scroll-snap-align: center;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  line-height: var(--lh-tight);
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.shop-chip:hover {
  border-color: var(--color-line-strong);
  color: var(--color-text);
}

/* The everything door: the one action the first screen is asking for,
   and so the one chip in the shop's accent. */
.shop-chip--all {
  border-color: transparent;
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-weight: var(--fw-bold);
  box-shadow: var(--shadow-soft);
}
.shop-chip--all:hover {
  border-color: transparent;
  background: var(--color-primary-hover, var(--color-primary));
  color: var(--color-on-primary);
}

/*
   The count.

   Its own chip rather than a figure after the word: at a dozen pills
   the numbers are what the eye scans, and a bare digit behind a name
   reads as part of the name. tabular-nums so «12» and «3» take the
   same width and the row does not jitter as it is read.
*/
.shop-chip__count {
  min-inline-size: 1.4rem;
  padding-inline: 0.3rem;
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
  color: var(--color-text-muted);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* White on the accent at low alpha — a solid white chip on a solid
   coloured pill is two flat shapes fighting; a wash of white is the
   figure reading as part of the pill it is in. */
.shop-chip--all .shop-chip__count {
  background: rgb(255 255 255 / 0.22);
  color: var(--color-on-primary);
}

/* The last chip is a jump down this same page, not another door into
   the catalogue, so it is set quieter than the names beside it. */
.shop-chip--more {
  border-style: dashed;
  color: var(--color-text-muted);
}

@media (prefers-reduced-motion: reduce) {
  .shop-chip { transition: none; }
}

/*
   Where it ships from, above the name.

   It was the count and the city, set small and spaced over the
   headline the way a magazine sets a standfirst. The count moved
   into the rail below the picture on 2026-09-22 — «كل المنتجات ١٣»
   — and the place stayed, because it is the one fact here a shopper
   cannot guess from the logo.

   uppercase does nothing to Arabic and everything to the English
   half, which is the half that would otherwise sit at body case in a
   slot that is not body copy.
*/
.shop-hero__eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-4);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-text-secondary);
}

/* ---------------------------------------------------------
   Section rail

   Every tile has a shape a photograph can live in.

   The row before this one let the grid decide: auto-fit stretched
   a lone section across the whole page at 118px tall, and the
   fourth of four sections across a full row at 168px. Both came
   out ten and seven times wider than tall, and the picture on a
   tile is most often a product's own portrait photograph — a
   dress in one showed its waist and nothing else. No size the
   merchant could have been asked for fits a strip, so the fix is
   the shape, not an instruction.

   The count picks it (categories.blade.php):
     --solo   one section — a card, the picture beside its words
     --duo    two — a pair of equal doors
     --bento  three and up — the first twice as wide and as tall

   The crop sits a little above centre everywhere: product photos
   are shot with the goods in the middle and the head of a model,
   the neck of a bottle, the top of a stack at the upper third.
   --------------------------------------------------------- */
.cat-rail {
  display: grid;
  gap: var(--space-3);
}

/* The tile fills whatever cell the grid hands it, which is what makes
   the double-height one below possible. */
.cat-rail__item { display: grid; }

/* Two: side by side on a phone too, as portraits, which is the shape
   the photographs were taken in. */
.cat-rail--duo { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cat-rail--duo > .cat-rail__item { aspect-ratio: 4 / 5; }

/* Three and up on a phone scroll sideways rather than stacking: three
   sections stacked push the goods below the fold on every visit. The
   rail runs to the screen's edge so the next tile shows at the side —
   that sliver is the only sign the row goes on. */
@media (max-width: 759px) {
  .cat-rail--bento {
    grid-auto-flow: column;
    grid-auto-columns: 44%;
    margin-inline: calc(-1 * var(--space-6));
    padding-inline: var(--space-6);
    scroll-padding-inline: var(--space-6);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
  }
  .cat-rail--bento::-webkit-scrollbar { display: none; }
  .cat-rail--bento > .cat-rail__item {
    aspect-ratio: 4 / 5;
    scroll-snap-align: start;
  }
}

/*
   Above 760 the sections stop being a row of equal doors.

   Three across with the first one twice as wide and twice as tall:
   a row of identical tiles is read once and scrolled past, while one
   tile carrying real weight makes the eye enter the row instead of
   skimming it. Which section grows is not a judgement the page makes;
   it is the first one, and the merchant ordered them.

   ---- Why a tile sometimes grows taller ----

   The big tile takes four cells, so the grid comes out level only
   when the count is a multiple of three. The first answer to that
   stretched the last tile across the leftover row, and at four
   sections that made a 1120×168 strip — the same fault this block
   was rewritten for. So the spare cells go upward instead of across,
   into shapes a photograph still fits:

     3n+1 (4, 7…)  the big tile stands three rows tall, and the next
                   three stack beside it;
     3n+2 (5, 8…)  the second tile stands two rows tall, a portrait.

   `:first-child` that is also the nth-from-last is how CSS asks how
   many children there are. A multiple of three matches neither.
*/
@media (min-width: 760px) {
  .cat-rail { gap: var(--space-4); }

  .cat-rail--duo > .cat-rail__item { aspect-ratio: 4 / 3; }

  .cat-rail--bento {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-auto-rows: clamp(168px, 15vw, 212px);
  }
  .cat-rail--bento > .cat-rail__item:first-child {
    grid-column: span 2;
    grid-row: span 2;
  }
  .cat-rail--bento > .cat-rail__item:first-child:nth-last-child(3n + 1) { grid-row: span 3; }
  .cat-rail--bento > :first-child:nth-last-child(3n + 2) + .cat-rail__item { grid-row: span 2; }
}

.cat-tile {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 118px;
  height: 100%;
  overflow: hidden;
  padding: var(--space-4);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  transition:
    border-color var(--dur-base) var(--ease-out),
    background var(--dur-base) var(--ease-out);
}
.cat-tile:hover {
  border-color: var(--color-growth);
  background: var(--color-surface-raised);
  color: var(--color-text);
}
@media (min-width: 760px) {
  .cat-tile { padding: var(--space-5); }
}

/*
   The photograph, no longer held at 45% so the name can sit on it.

   Dimming the whole picture to make one line of type readable costs
   the picture and buys very little: the type only needs the ground
   where the type actually is. So the image comes up to something you
   can see the goods in, and a scrim below it does the work over the
   bottom third — the same trade the hero makes, at tile scale.
*/
.cat-tile__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 22%;
  transition: scale var(--dur-slow) var(--ease-out);
}
.cat-tile:hover .cat-tile__img { scale: 1.05; }

/* Dark in both themes, for the reason the hero's shade is: on paper
   the page's ground was a white fog over every section photograph. */
.cat-tile__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    color-mix(in srgb, var(--athar-neutral-900) 78%, transparent) 0%,
    color-mix(in srgb, var(--athar-neutral-900) 22%, transparent) 50%,
    transparent 72%
  );
}
.cat-tile:has(.cat-tile__img) .cat-tile__name { color: var(--athar-paper-50); }
.cat-tile:has(.cat-tile__img) .cat-tile__count { color: color-mix(in srgb, var(--athar-paper-50) 80%, transparent); }

.cat-tile__text {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.cat-tile__name {
  font-family: var(--font-display-ar);
  font-size: var(--fs-h4);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
  letter-spacing: var(--tracking-tight);
}

/* The tile that got the extra space says its name at the size the
   space is for. Anything smaller and the emphasis was only ever a
   bigger picture. */
@media (min-width: 760px) {
  .cat-rail--duo .cat-tile__name { font-size: var(--fs-h3); }
  .cat-rail--bento .cat-rail__item:first-child .cat-tile__name { font-size: var(--fs-h2); }
}
:root[data-lang="en"] .cat-tile__name { font-family: var(--font-latin); }

.cat-tile__count {
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

/* A hairline that grows across the tile on hover — the tile is a door,
   and this is the only thing on it that says so. */
.cat-tile::after {
  content: "";
  position: absolute;
  inset-inline: var(--space-5);
  bottom: var(--space-3);
  height: 1px;
  background: var(--color-gold);
  scale: 0 1;
  transform-origin: right;
  transition: scale var(--dur-base) var(--ease-out);
}
:root[dir="ltr"] .cat-tile::after { transform-origin: left; }
.cat-tile:hover::after { scale: 1 1; }

/*
   One section: a card, the picture beside its words.

   A lone tile has the whole row, and no photograph fills a whole row
   at any height a band can spend. So the picture takes a column in
   the shape it was shot in, and the other side is the section said
   at the size of a heading, its own description if the merchant
   wrote one, and a plain way in. Without a picture the words simply
   take the card.
*/
.cat-rail--solo .cat-tile {
  --solo-media: 40%;
  justify-content: center;
  min-height: 11.5rem;
}
.cat-rail--solo .cat-tile:has(.cat-tile__img) {
  padding-inline-start: calc(var(--solo-media) + var(--space-5));
}
.cat-rail--solo .cat-tile__img {
  inset-inline-end: auto;
  width: var(--solo-media);
}
.cat-rail--solo .cat-tile__scrim,
.cat-rail--solo .cat-tile::after { display: none; }

.cat-rail--solo .cat-tile .cat-tile__name {
  font-size: var(--fs-h3);
  color: var(--color-text);
}
.cat-rail--solo .cat-tile .cat-tile__count { color: var(--color-text-muted); }

.cat-tile__desc {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  max-width: 46ch;
  margin-block: var(--space-1) var(--space-2);
  font-size: var(--fs-sm);
  line-height: var(--lh-relaxed);
  color: var(--color-text-secondary);
}

.cat-tile__go {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-block-start: var(--space-4);
  padding-block-end: 2px;
  border-block-end: 1px solid var(--color-line-gold);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--color-text);
  transition: color var(--dur-base) var(--ease-out);
}
.cat-tile__go::after {
  content: "←";
  transition: translate var(--dur-base) var(--ease-out);
}
:root[dir="ltr"] .cat-tile__go::after { content: "→"; }
.cat-tile:hover .cat-tile__go { color: var(--color-gold-text); }
.cat-tile:hover .cat-tile__go::after { translate: -4px 0; }
:root[dir="ltr"] .cat-tile:hover .cat-tile__go::after { translate: 4px 0; }

@media (min-width: 760px) {
  .cat-rail--solo .cat-tile {
    --solo-media: 38%;
    min-height: clamp(20rem, 32vw, 28rem);
    padding: var(--space-12);
  }
  .cat-rail--solo .cat-tile:has(.cat-tile__img) {
    padding-inline-start: calc(var(--solo-media) + var(--space-12));
  }
  .cat-rail--solo .cat-tile .cat-tile__name { font-size: var(--fs-h1); }
  .cat-rail--solo .cat-tile__count { font-size: var(--fs-sm); }
  .cat-tile__go { margin-block-start: var(--space-8); }
}

/* ---------------------------------------------------------
   Product grid
   --------------------------------------------------------- */
/*
   A counted grid rather than auto-fill.

   auto-fill decides the column count from the container, so a band
   holding three picks on a wide screen opened five tracks and left two
   of them empty — which reads as a page that failed to finish loading
   rather than as a shop with three favourites.
*/
.pgrid {
  display: grid;
  gap: var(--space-6) var(--space-5);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 640px) {
  .pgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1000px) {
  .pgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  /* Two to a row, not one: a phone showing one product per screen
     makes a ten-product shop feel like a hundred-screen scroll. */
  .pgrid { gap: var(--space-5) var(--space-4); }
}

/* ---------------------------------------------------------
   A grid arrives in order

   The band around it already fades in as a whole, and that was the
   problem: every tile inside it appeared on the same frame, which is
   a flash rather than a movement. Sixty milliseconds apart is enough
   to read as a sequence and short enough that the last tile is in
   place before the eye gets there.

   Capped at eight, which is what fits on a screen — a ninth tile
   arriving 540ms late is not a sequence, it is a tile that looks
   broken. Everything past the eighth keeps the eighth's delay.

   Nothing here runs without script (`:root.js`) or against a stated
   preference for less motion, and in both cases the tiles are simply
   visible, which is what they were before this block existed.
   --------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  :root.js [data-reveal] :is(.pgrid, .cat-rail, .agrid, .vshelf) > * {
    opacity: 0;
    translate: 0 12px;
    transition:
      opacity var(--dur-slow) var(--ease-out),
      translate var(--dur-slow) var(--ease-out);
  }

  :root.js [data-reveal].is-revealed :is(.pgrid, .cat-rail, .agrid, .vshelf) > * {
    opacity: 1;
    translate: 0 0;
  }

  :root.js [data-reveal].is-revealed :is(.pgrid, .cat-rail, .agrid, .vshelf) > *:nth-child(2) { transition-delay: 60ms; }
  :root.js [data-reveal].is-revealed :is(.pgrid, .cat-rail, .agrid, .vshelf) > *:nth-child(3) { transition-delay: 120ms; }
  :root.js [data-reveal].is-revealed :is(.pgrid, .cat-rail, .agrid, .vshelf) > *:nth-child(4) { transition-delay: 180ms; }
  :root.js [data-reveal].is-revealed :is(.pgrid, .cat-rail, .agrid, .vshelf) > *:nth-child(5) { transition-delay: 240ms; }
  :root.js [data-reveal].is-revealed :is(.pgrid, .cat-rail, .agrid, .vshelf) > *:nth-child(6) { transition-delay: 300ms; }
  :root.js [data-reveal].is-revealed :is(.pgrid, .cat-rail, .agrid, .vshelf) > *:nth-child(7) { transition-delay: 360ms; }
  :root.js [data-reveal].is-revealed :is(.pgrid, .cat-rail, .agrid, .vshelf) > *:nth-child(n + 8) { transition-delay: 420ms; }
}

/* ---------------------------------------------------------
   Video shelf
   --------------------------------------------------------- */
/*
   auto-fill, not auto-fit: a shop with one clip should show one
   portrait card, not one card stretched across the whole band and
   turned into a poster a metre tall.
*/
.vshelf {
  display: grid;
  gap: var(--space-8) var(--space-6);
  grid-template-columns: repeat(auto-fill, minmax(min(250px, 100%), 1fr));
}

.vcard {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

/*
   One clip, and it is the first thing on the page.

   auto-fill above is right for a shelf — but a shelf with a single card on
   it leaves three empty columns, and at the top of a shop that reads as a
   page that failed to load rather than as a shop with one clip. So the lone
   card lies down: the poster keeps its 9/13 frame at a readable width and
   the title and pins move beside it, which is also the only layout with
   room to list what is actually in the clip rather than three of it.

   No second markup. The card is the same element the shelf renders, and
   this is the grid it is placed in.
*/
.vshelf--solo { grid-template-columns: minmax(0, 1fr); }

@media (min-width: 720px) {
  .vshelf--solo .vcard {
    display: grid;
    grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
    grid-template-rows: auto auto 1fr;
    align-items: start;
    gap: var(--space-3) var(--space-8);
  }

  .vshelf--solo .vcard__frame {
    grid-column: 1;
    grid-row: 1 / span 3;
  }

  .vshelf--solo .vcard__pins-head { grid-column: 2; }

  .vshelf--solo .vcard__title {
    grid-column: 2;
    font-size: var(--fs-lg);
  }

  /* Six pins in one column would run past the poster. Two columns keep the
     list inside the frame's height, and collapse back to one when the band
     is too narrow to hold them. */
  .vshelf--solo .vcard__pins {
    grid-column: 2;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
    gap: var(--space-1) var(--space-4);
    align-content: start;
  }
}

.vcard__frame {
  position: relative;
  display: block;
  aspect-ratio: 9 / 13;
  overflow: hidden;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface-sunken);
}
.vcard__frame:hover { border-color: var(--color-line-strong); }

.vcard__poster {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A clip with no poster frame yet. Hatched like the product plate,
   so the two absences look like one decision. */
.vcard__plate {
  display: block;
  width: 100%;
  height: 100%;
  background:
    repeating-linear-gradient(
      135deg,
      transparent 0 11px,
      color-mix(in srgb, var(--color-line) 60%, transparent) 11px 12px
    );
}

.vcard__play {
  position: absolute;
  inset: 0;
  margin: auto;
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--color-canvas-deep) 70%, transparent);
  box-shadow: inset 0 0 0 1px var(--color-line-strong);
  color: var(--color-text);
  transition: scale var(--dur-base) var(--ease-out);
}
.vcard__frame:hover .vcard__play { scale: 1.08; }
.vcard__play svg { width: 26px; height: 26px; }

.vcard__tag {
  position: absolute;
  inset-inline-start: var(--space-3);
  top: var(--space-3);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--color-canvas-deep) 78%, transparent);
  color: var(--color-text-secondary);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  line-height: 1.6;
}

.vcard__tag--live {
  background: var(--color-danger-soft);
  color: var(--color-danger);
}
.vcard__tag--live::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: currentColor;
}

.vcard__title {
  margin: 0;
  font-size: var(--fs-base);
  line-height: var(--lh-snug);
}
.vcard__title a { color: var(--color-text); }
.vcard__title a:hover { color: var(--color-growth-text); }

.vcard__pins-head {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--fs-xs);
}

.vcard__pins {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.vcard__pin-link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary);
  transition: background var(--dur-fast) var(--ease-out);
}
.vcard__pin-link:hover {
  background: var(--color-surface);
  color: var(--color-text);
}

/* The smallest step rather than --radius-sm: on a shop set to
   «دائرية» that step turned these into circles, and a product in a
   circle reads as an avatar. */
.vcard__pin-img {
  width: 44px;
  height: 44px;
  flex: none;
  border-radius: var(--radius-xs);
  object-fit: cover;
}

.vcard__pin-img--empty {
  display: grid;
  place-items: center;
  background: var(--color-surface-raised);
  box-shadow: inset 0 0 0 1px var(--color-line);
  color: var(--color-text-muted);
  font-family: var(--font-display-ar);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
}

.vcard__pin-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  font-size: var(--fs-sm);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vcard__pin-price {
  flex: none;
  font-size: var(--fs-xs);
  color: var(--color-text);
}

/* ---------------------------------------------------------
   Journal
   --------------------------------------------------------- */
/* One column, always. See the note on .acard below for why the journal
   stopped being a grid: the number of posts a shop has is not the number
   a grid needs to look deliberate. */
.agrid {
  display: flex;
  flex-direction: column;
}

/*
   The journal is an index, not a shelf of cards.

   It was a grid of bordered boxes, and on a shop with two posts that is
   two small rectangles and an empty third column — which reads as a page
   that failed to load rather than as a journal with two entries in it. A
   card grid is the right shape for goods, where the picture is the
   argument and twelve of them are a catalogue. It is the wrong shape for
   writing, where the title is the argument and there are rarely twelve.

   So: full-width rows, one under the other, separated by the same
   hairline the rest of the shop separates things with. It reads at two
   posts and at forty, the title gets the size a title deserves, and a
   post nobody photographed is no longer a box with a hole at the top.
   --------------------------------------------------------- */
.acard { border-block-end: 1px solid var(--color-line); }
.acard:last-child { border-block-end: 0; }

.acard__link {
  display: grid;
  gap: var(--space-3);
  padding-block: var(--space-8);
  color: var(--color-text);
}

/*
   With a photograph, the row splits and the picture takes the leading
   side — where a magazine puts it, and where the eye already is.

   `:has()` rather than a class from the Blade: whether there is a cover
   is a fact about the markup, and asking the markup is more honest than
   asking the controller to pass the same fact down as a modifier that
   can fall out of step with it.
*/
@media (min-width: 720px) {
  .acard__link:has(.acard__cover) {
    grid-template-columns: 240px minmax(0, 1fr);
    column-gap: var(--space-6);
    align-items: start;
  }
  .acard__link:has(.acard__cover) .acard__cover { grid-row: 1 / span 3; }
}

.acard__cover {
  width: 100%;
  margin: 0;
  aspect-ratio: 3 / 2;
  border-radius: var(--radius-md);
  object-fit: cover;
}

.acard__title {
  margin: 0;
  font-size: var(--fs-h3);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
  transition: color var(--dur-fast) var(--ease-out);
}
.acard__link:hover .acard__title { color: var(--color-growth-text); }

/* A measure, because the row is now the width of the page and a line of
   prose that long is read by nobody. */
.acard__excerpt {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-inline-size: 68ch;
  margin: 0;
  font-size: var(--fs-base);
  line-height: var(--lh-normal);
  color: var(--color-text-secondary);
}

.acard__meta {
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

/* ---------------------------------------------------------
   Breadcrumb
   --------------------------------------------------------- */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding-block: var(--space-6) 0;
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}
.crumbs a { color: var(--color-text-muted); }
.crumbs a:hover { color: var(--color-growth-text); }
.crumbs__sep { color: var(--color-line-strong); }

/* ---------------------------------------------------------
   Page heading — sections, search, journal
   --------------------------------------------------------- */
/* The same air the home page's bands got. A listing that opens
   tight against the header reads as a search result; one that opens
   with room reads as a section of a shop. */
.page-top {
  padding-block: var(--space-8) var(--space-10);
  border-block-end: 1px solid var(--color-line);
  margin-block-end: var(--space-10);
}

.page-top__title {
  margin: 0;
  font-size: var(--fs-h2);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
}

/* The searched words, marked the way a copy editor marks a quoted
   phrase — brass on a brass rule, not a coloured box. */
.page-top__term {
  color: var(--color-gold-text);
  text-decoration: underline;
  text-decoration-color: var(--color-line-gold);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.2em;
  text-decoration-skip-ink: none;
}

.page-top__note {
  max-width: 62ch;
  margin: var(--space-4) 0 0;
  color: var(--color-text-secondary);
}

.page-top__children {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-block-start: var(--space-5);
}
.page-top__children .chip { color: var(--color-text-secondary); }
.page-top__children .chip:hover {
  border-color: var(--color-growth);
  color: var(--color-growth-text);
}

/* ---------------------------------------------------------
   Catalogue: rail beside results
   --------------------------------------------------------- */
.catalog {
  display: grid;
  gap: var(--space-8);
  grid-template-columns: 232px minmax(0, 1fr);
  align-items: start;
  padding-block-end: var(--space-16);
}

/* The merchant switched the filter rail off: the results take the
   whole row rather than leaving an empty column beside them. */
.catalog--no-filters { grid-template-columns: minmax(0, 1fr); }

@media (max-width: 900px) {
  /* The rail becomes a band above the results rather than a drawer
     that has to be opened: with three controls in it, hiding them
     behind a button costs more taps than it saves space. */
  .catalog { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }
}

/* ---- The rail ---- */
.filters-rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  position: sticky;
  top: calc(var(--header-height) + var(--space-4));
  padding: var(--space-5);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

@media (max-width: 900px) {
  .filters-rail {
    position: static;
    display: grid;
    gap: var(--space-5);
    grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
    align-items: start;
  }
}

.filters-rail__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.filters-rail__title {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
}

.filters-rail__clear {
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
  border-block-end: 1px solid var(--color-line);
}
.filters-rail__clear:hover { color: var(--color-danger); border-color: var(--color-danger); }

.filters-rail__group {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  border: 0;
}

.filters-rail__legend {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 0;
  margin-block-end: var(--space-1);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  color: var(--color-text-muted);
}
.filters-rail__unit { color: var(--color-line-strong); }

.filters-rail__choice {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
  cursor: pointer;
}
.filters-rail__choice:hover { color: var(--color-text); }
.filters-rail__choice input { accent-color: var(--color-primary); }
.filters-rail__choice input:checked + span {
  color: var(--color-text);
  font-weight: var(--fw-medium);
}

.filters-rail__range {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.filters-rail__range .field__label { font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--color-text-muted); }
.filters-rail__range .field__control { padding: 0.5rem 0.7rem; }

.filters-rail__actions { margin-block-start: var(--space-1); }

@media (max-width: 900px) {
  .filters-rail__head { grid-column: 1 / -1; }
  .filters-rail__actions { align-self: end; }
}

/* Beside a filter rail the results column is 230px narrower, so four
   tiles across leaves each of them too small to read a price on. */
@media (min-width: 1000px) {
  .catalog .pgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1280px) {
  .catalog .pgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ---- Results ---- */
.results {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
}

/* The count and the sorter, ruled off from the goods — the same
   hairline the home page puts under a band heading, so a listing and
   the home page are recognisably one shop. */
.results__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-block-end: var(--space-4);
  border-block-end: 1px solid var(--color-line);
}

.results__count {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

.results .pager { border-block-start: 1px solid var(--color-line); padding-inline: 0; }

/*
   A hairline, not a dashed rectangle.

   A dashed border is the convention for a slot that is waiting to be
   filled — an upload target, a drop zone — and a shopper whose search
   found nothing is not looking at an unfinished part of the shop.
   Rules above and below, air, and the sentence in the middle: it
   reads as a passage of the page rather than as a hole in it.
*/
.shop-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-16) var(--space-6);
  border-block: 1px solid var(--color-line);
  text-align: center;
}

.shop-empty__title {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--color-text);
}

.shop-empty__body {
  max-width: 44ch;
  margin: 0 0 var(--space-2);
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

/* ---- Sorter ---- */
.sorter {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.sorter__label {
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
  white-space: nowrap;
}

.sorter__select {
  padding: 0.45rem var(--space-3);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-sm);
  background: var(--color-surface-sunken);
  color: var(--color-text);
  font-size: var(--fs-sm);
  cursor: pointer;
}
.sorter__select:hover { border-color: var(--color-line-strong); }

/* With script, the select submits on change and the button is noise.
   Without it, the button is the only way to sort. */
:root.js .sorter__go { display: none; }

/* =========================================================
   Product page
   ========================================================= */
.product {
  display: grid;
  gap: var(--space-10);
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  align-items: start;
  padding-block: var(--space-8) var(--space-12);
}

@media (max-width: 900px) {
  .product { grid-template-columns: minmax(0, 1fr); gap: var(--space-8); }
}

/* ---------------------------------------------------------
   Gallery

   The stage stacks every photograph and shows the one whose
   radio is checked. No script: a shopper on a slow line can
   go through the pictures before any JavaScript lands.
   --------------------------------------------------------- */
/*
   The thumbnails move up the side of the picture.

   Under the stage they were a strip of small squares reading as a
   caption, and they pushed the sticky column taller than it needed to
   be. Beside it they are what a gallery wall looks like — and the eye
   finds them without leaving the photograph, which is the whole
   argument for a picker at all.

   The radios have to stay where they are in the source (the checked
   selector reaches its shot with `~`, which only walks forward), so
   the order is set with `order` rather than by moving markup: the
   rail is drawn first in RTL and in LTR alike, because it is a margin
   of the picture and margins sit on the outside.

   Below 900 the column stacks and the strip goes back under the
   stage, where a phone has the width for it and the side does not.
*/
.gallery {
  position: sticky;
  top: calc(var(--header-height) + var(--space-4));
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

@media (min-width: 901px) {
  .gallery--many {
    display: grid;
    grid-template-columns: 76px minmax(0, 1fr);
    align-items: start;
    gap: var(--space-4);
  }
  .gallery--many .gallery__stage { order: 2; }
  .gallery--many .gallery__thumbs {
    order: 1;
    flex-direction: column;
    flex-wrap: nowrap;
    gap: var(--space-3);
  }
  /* The rail's own width decides the thumbnail, so it stops being a
     72px square that happens to sit in a 76px column. */
  .gallery--many .gallery__thumb { width: 100%; }
  .gallery--many .gallery__thumb img { height: auto; aspect-ratio: 4 / 5; }
}

@media (max-width: 900px) {
  .gallery { position: static; }
}

.gallery__stage {
  position: relative;
  display: grid;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border: 1px solid var(--color-line);
  /* The same corner the grid tiles now take. A product page whose
     photograph is rounded twice as hard as the tile the shopper
     clicked to get here reads as a different shop. */
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
}

.gallery__shot {
  grid-area: 1 / 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}

/* Each radio precedes the stage, so the nth checked one lights the
   nth shot. Written out for the first eight photographs, which is
   more than any product in this catalogue carries. */
.gallery__radio:nth-of-type(1):checked ~ .gallery__stage .gallery__shot:nth-child(1),
.gallery__radio:nth-of-type(2):checked ~ .gallery__stage .gallery__shot:nth-child(2),
.gallery__radio:nth-of-type(3):checked ~ .gallery__stage .gallery__shot:nth-child(3),
.gallery__radio:nth-of-type(4):checked ~ .gallery__stage .gallery__shot:nth-child(4),
.gallery__radio:nth-of-type(5):checked ~ .gallery__stage .gallery__shot:nth-child(5),
.gallery__radio:nth-of-type(6):checked ~ .gallery__stage .gallery__shot:nth-child(6),
.gallery__radio:nth-of-type(7):checked ~ .gallery__stage .gallery__shot:nth-child(7),
.gallery__radio:nth-of-type(8):checked ~ .gallery__stage .gallery__shot:nth-child(8) {
  opacity: 1;
}

/* A single photograph has no radio to check it. */
.gallery:not(.gallery--many) .gallery__shot { opacity: 1; }

.gallery__plate {
  display: grid;
  place-items: center;
  padding: var(--space-10);
  background:
    repeating-linear-gradient(
      135deg,
      transparent 0 13px,
      color-mix(in srgb, var(--color-line) 60%, transparent) 13px 14px
    );
  text-align: center;
}

.gallery__plate-text {
  padding: var(--space-3) var(--space-5);
  border-block: 1px solid var(--color-line-gold);
  background: var(--color-surface-sunken);
  color: var(--color-text-secondary);
  font-family: var(--font-display-ar);
  font-size: var(--fs-h4);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
}
:root[data-lang="en"] .gallery__plate-text { font-family: var(--font-latin); }

.gallery__thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.gallery__thumb {
  width: 72px;
  overflow: hidden;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.gallery__thumb:hover { border-color: var(--color-line-strong); }
.gallery__thumb img { width: 100%; height: 90px; object-fit: cover; }

.gallery__radio:nth-of-type(1):checked ~ .gallery__thumbs .gallery__thumb:nth-child(1),
.gallery__radio:nth-of-type(2):checked ~ .gallery__thumbs .gallery__thumb:nth-child(2),
.gallery__radio:nth-of-type(3):checked ~ .gallery__thumbs .gallery__thumb:nth-child(3),
.gallery__radio:nth-of-type(4):checked ~ .gallery__thumbs .gallery__thumb:nth-child(4),
.gallery__radio:nth-of-type(5):checked ~ .gallery__thumbs .gallery__thumb:nth-child(5),
.gallery__radio:nth-of-type(6):checked ~ .gallery__thumbs .gallery__thumb:nth-child(6),
.gallery__radio:nth-of-type(7):checked ~ .gallery__thumbs .gallery__thumb:nth-child(7),
.gallery__radio:nth-of-type(8):checked ~ .gallery__thumbs .gallery__thumb:nth-child(8) {
  border-color: var(--color-growth);
}

/* Keyboard focus lands on the hidden radio, so the ring is drawn on
   the label the shopper can actually see. */
.gallery__radio:focus-visible ~ .gallery__thumbs .gallery__thumb {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Square and small, like the mark on the grid tile the shopper
   clicked to get here. */
.gallery__flag {
  position: absolute;
  inset-inline-start: var(--space-4);
  top: var(--space-4);
  z-index: 1;
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius-xs);
  background: var(--color-gold);
  color: var(--color-canvas-deep);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-eyebrow);
  direction: ltr;
  unicode-bidi: isolate;
}

/*
   With the thumbnails beside the picture, the flag has to be told
   which half it belongs to: an absolutely-positioned child of a grid
   container is measured against its grid area when it has one, and
   without this it would be measured against the whole gallery and
   land on the thumbnail rail.
*/
@media (min-width: 901px) {
  .gallery--many .gallery__flag {
    grid-column: 2;
    grid-row: 1;
  }
}

/* ---------------------------------------------------------
   Buy panel
   --------------------------------------------------------- */
.buy {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-width: 0;
}

/*
   The maker's name and the code, set as an eyebrow.

   Same move as the hero's line above the shop name and for the same
   reason: a fact that identifies the thing belongs over the title, not
   beside it in bold brass. Dropping the brass also settles an argument
   the page was having with itself — the discount flag on the gallery
   is warm, and a brand name in the same colour four inches away made
   neither of them mean anything.
*/
.buy__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-text-secondary);
}
.buy__brand { color: var(--color-text); }
.buy__sku { color: var(--color-text-muted); }

.buy__name {
  margin: 0;
  font-size: clamp(1.75rem, 1.35rem + 1.6vw, 2.5rem);
  line-height: 1.14;
  letter-spacing: var(--tracking-tight);
}

.buy__price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0;
}

/* Semibold, not bold — the same argument the grid tile's price
   settled. On a page with exactly one product on it the price does
   not need weight to be found. */
.buy__now {
  font-size: var(--fs-h3);
  font-weight: var(--fw-semibold);
  color: var(--color-text);
}

.buy__was {
  font-size: var(--fs-base);
  color: var(--color-text-muted);
}

.buy__short {
  max-width: 52ch;
  margin: 0;
  font-size: var(--fs-base);
  line-height: var(--lh-relaxed);
  color: var(--color-text-secondary);
}

/*
   The box around the buying is gone.

   It was a raised panel with a border and a radius, and it made the
   one thing this page exists for look like a form on a dashboard —
   a shopper reads a bordered card as «fill this in», not as «buy
   this». Every shop that reads as expensive puts the size, the
   quantity and the button straight onto the page and separates them
   from the description with a line.

   So: a rule above, air below it, and nothing drawn around the
   controls themselves. The quantity stepper and the button keep
   their own edges, which is now the only chrome in the column and
   therefore reads as chrome that means something.
*/
.buy__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding-block-start: var(--space-6);
  border-block-start: 1px solid var(--color-line);
}

/* ---- Variants ---- */
.variants {
  margin: 0;
  padding: 0;
  border: 0;
}

.variants__legend {
  padding: 0;
  margin-block-end: var(--space-3);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  color: var(--color-text-muted);
}

.variants__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.variants__choice {
  position: relative;
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.variants__choice:hover { border-color: var(--color-line-strong); }
.variants__choice input {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
}
/*
   The chosen option is outlined, not filled.

   A filled block of the shop's accent colour is the loudest thing on
   the page, and what it is shouting is «medium» — a size, which the
   shopper picked and already knows. Two pixels of the shop's own line
   colour say the same thing and leave the loudest thing on the page
   to be the photograph.
*/
.variants__choice:has(input:checked) {
  border-color: var(--color-text);
  box-shadow: inset 0 0 0 1px var(--color-text);
  background: var(--color-surface-raised);
}
.variants__choice:has(input:focus-visible) {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.variants__name {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--color-text);
}
.variants__price {
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

.variants__choice.is-out {
  opacity: 0.45;
  cursor: not-allowed;
}
.variants__out {
  font-size: var(--fs-xs);
  color: var(--color-danger);
}

/* ---- Stock line ---- */
.buy__stock {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-growth-text);
}
.buy__stock::before {
  content: "";
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: var(--radius-pill);
  background: currentColor;
}
.buy__stock.is-out { color: var(--color-danger); }

/* ---- Quantity and submit ---- */
.buy__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.qty {
  display: inline-flex;
  align-items: stretch;
  overflow: hidden;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
}

.qty__step {
  width: 40px;
  border: 0;
  background: none;
  color: var(--color-text-secondary);
  font-size: var(--fs-lg);
  line-height: 1;
  cursor: pointer;
}
.qty__step:hover { background: var(--color-surface-raised); color: var(--color-text); }
:root:not(.js) .qty__step { display: none; }

.qty__input {
  width: 56px;
  border: 0;
  border-inline: 1px solid var(--color-line);
  background: none;
  color: var(--color-text);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  text-align: center;
  appearance: textfield;
}
.qty__input::-webkit-outer-spin-button,
.qty__input::-webkit-inner-spin-button { appearance: none; margin: 0; }
.qty__input:focus { outline: none; }

/* Same 16px floor as .field__control, for the same reason — a coarse
   pointer must never trigger iOS's zoom-on-focus. */
@media (pointer: coarse) {
  .qty__input { font-size: 16px; }
}
.qty:focus-within { border-color: var(--color-growth); }

.buy__submit { flex: 1 1 200px; }

.buy__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* ---------------------------------------------------------
   Delivery — the merchant's real zones, prices and windows
   --------------------------------------------------------- */
.delivery {
  padding: var(--space-5);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface-sunken);
}

.delivery__head {
  margin: 0 0 var(--space-4);
  font-family: var(--font-body-ar);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-bold);
  color: var(--color-gold-text);
}
:root[data-lang="en"] .delivery__head {
  font-family: var(--font-latin);
  text-transform: uppercase;
  letter-spacing: var(--tracking-eyebrow);
}

.delivery__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.delivery__zone {
  display: grid;
  gap: var(--space-1) var(--space-4);
  grid-template-columns: minmax(0, 1fr) auto;
  padding-block-end: var(--space-3);
  border-block-end: 1px solid var(--color-line);
  font-size: var(--fs-sm);
}
.delivery__zone:last-child { padding-block-end: 0; border-block-end: 0; }

.delivery__name { color: var(--color-text); }

.delivery__terms {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-3);
  white-space: nowrap;
}
.delivery__price { font-weight: var(--fw-bold); color: var(--color-text); }
.delivery__eta { font-size: var(--fs-xs); color: var(--color-text-muted); }

.delivery__free {
  grid-column: 1 / -1;
  font-size: var(--fs-xs);
  color: var(--color-growth-text);
}

/* Folded, on the product page: one row that opens into the zones. */
.delivery--folded { padding: 0; }

.delivery__summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  cursor: pointer;
  list-style: none;
  font-size: var(--fs-sm);
  color: var(--color-text);
}
.delivery__summary::-webkit-details-marker { display: none; }

.delivery__from {
  margin-inline-start: auto;
  color: var(--color-text-secondary);
  font-size: var(--fs-xs);
}

.delivery__summary::after {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  margin-inline-start: var(--space-1);
  /* Physical sides: the arrow points down in both directions. */
  border-bottom: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  rotate: 45deg;
  translate: 0 -2px;
  transition: rotate var(--dur-fast) var(--ease-out);
}
.delivery__summary:not(:has(.delivery__from))::after { margin-inline-start: auto; }
.delivery--folded[open] > .delivery__summary::after { rotate: 225deg; translate: 0 2px; }

.delivery--folded > .delivery__list {
  padding: 0 var(--space-5) var(--space-5);
}

/* ---------------------------------------------------------
   The prose the merchant writes

   Product descriptions, page bodies and articles all land here,
   after Html::safe has cut them down to a dozen structural tags.
   --------------------------------------------------------- */
/* Inside the buy column since 2026-09-18, so the column sets the
   measure and a rule is enough to part it from the button above. */
.product-prose {
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--color-line);
}

.product-prose__head {
  margin: 0 0 var(--space-4);
  font-size: var(--fs-lg);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
}

.prose { color: var(--color-text-secondary); }
.prose > :last-child { margin-bottom: 0; }
.prose h2, .prose h3, .prose h4 { margin-block: var(--space-8) var(--space-3); color: var(--color-text); }
.prose h2 { font-size: var(--fs-h4); }
.prose h3, .prose h4 { font-size: var(--fs-lg); }
.prose p { line-height: var(--lh-relaxed); }
.prose ul, .prose ol {
  margin: 0 0 var(--space-4);
  padding-inline-start: var(--space-6);
  list-style: disc;
}
.prose ol { list-style: decimal; }
.prose li { margin-block-end: var(--space-2); }
.prose blockquote {
  margin: var(--space-6) 0;
  padding-inline-start: var(--space-5);
  border-inline-start: 2px solid var(--color-line-gold);
  color: var(--color-text);
}
.prose table {
  width: 100%;
  margin-block-end: var(--space-4);
  border-collapse: collapse;
  font-size: var(--fs-sm);
}
.prose th, .prose td {
  padding: var(--space-3);
  border-block-end: 1px solid var(--color-line);
  text-align: start;
}
.prose th { color: var(--color-text); font-weight: var(--fw-bold); }
.prose hr { height: 1px; border: 0; margin: var(--space-8) 0; background: var(--color-line); }

/* =========================================================
   Basket
   ---------------------------------------------------------
   Lines on the left, the summary in a column that stays in
   view beside them. The summary is what a shopper checks
   after every change, and on a long basket a total that has
   scrolled off the top is a total nobody trusts.
   ========================================================= */
.cart {
  display: grid;
  gap: var(--space-8);
  grid-template-columns: minmax(0, 1fr) 336px;
  align-items: start;
  padding-block-end: var(--space-16);
}

.cart-empty { margin-block-end: var(--space-16); }

@media (max-width: 900px) {
  /* The summary drops under the lines rather than beside them.
     It keeps its order: a shopper on a phone reads down to the
     total and finds the checkout button waiting there. */
  .cart { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }
}

.cart .alert { margin-block-end: var(--space-6); }

/* ---- One line ---- */
.cart-lines {
  display: flex;
  flex-direction: column;
}

.cart-line {
  display: grid;
  grid-template-columns: 88px minmax(0, 1fr) auto auto auto;
  gap: var(--space-4) var(--space-5);
  align-items: center;
  padding-block: var(--space-5);
  border-block-end: 1px solid var(--color-line);
}
.cart-line:first-child { padding-block-start: 0; }

.cart-line__shot {
  display: block;
  overflow: hidden;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  aspect-ratio: 4 / 5;
}

.cart-line__shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Same answer as the grid tile: no photograph yet, so the name
   is set as a label rather than left as a grey rectangle. */
.cart-line__plate {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: var(--space-2);
  background:
    repeating-linear-gradient(
      135deg,
      var(--color-surface) 0 6px,
      var(--color-surface-sunken) 6px 12px
    );
}

.cart-line__plate-text {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
  color: var(--color-text-muted);
  text-align: center;
}

.cart-line__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.cart-line__name {
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
  color: var(--color-text);
}
.cart-line__name:hover { color: var(--color-growth-text); }

.cart-line__variant {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
}

.cart-line__unit {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

.cart-line__left {
  margin: var(--space-1) 0 0;
  font-size: var(--fs-xs);
  color: var(--color-gold-text);
}

.cart-line__qty {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* With script the field submits on change; without it this button
   is the only way to commit a new quantity. */
:root.js .cart-line__go { display: none; }

.cart-line__total {
  margin: 0;
  min-inline-size: 7ch;
  font-weight: var(--fw-bold);
  color: var(--color-text);
  text-align: end;
}

.cart-line__drop-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border: 0;
  background: none;
  color: var(--color-text-muted);
  font-size: var(--fs-xs);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}
.cart-line__drop-btn:hover { color: var(--color-danger); }

.cart-line__drop-btn svg {
  inline-size: 0.9rem;
  block-size: 0.9rem;
}

.cart-back {
  align-self: start;
  margin-block-start: var(--space-6);
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
  border-block-end: 1px solid var(--color-line-gold);
  padding-block-end: 2px;
}
.cart-back:hover { color: var(--color-text); }

@media (max-width: 640px) {
  /* Five columns will not fit a phone. The picture keeps its own
     column and everything else stacks in the second — but the
     quantity and the line total share one row, at opposite ends,
     because they are the two halves of the same sum and reading
     them apart means scrolling to multiply. */
  .cart-line {
    grid-template-columns: 72px minmax(0, 1fr);
    gap: var(--space-3) var(--space-4);
    align-items: start;
  }
  .cart-line__shot { grid-area: 1 / 1; }
  .cart-line__body { grid-area: 1 / 2; }
  .cart-line__qty { grid-area: 2 / 2; justify-self: start; align-self: center; }
  .cart-line__total { grid-area: 2 / 2; justify-self: end; align-self: center; }
  .cart-line__drop { grid-area: 3 / 2; justify-self: start; }
  .cart-line__drop-btn { padding-inline: 0; }
}

/* ---- Summary ---- */
.cart-side {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  position: sticky;
  top: calc(var(--header-height) + var(--space-4));
}

@media (max-width: 900px) {
  .cart-side { position: static; }
}

.cart-sum {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-5);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

.cart-sum__head {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
}

.cart-sum__rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--color-line);
}

.cart-sum__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  font-size: var(--fs-sm);
}

.cart-sum__row dt { color: var(--color-text-secondary); }
.cart-sum__row dd { margin: 0; color: var(--color-text); }
.cart-sum__row--off dd { color: var(--color-growth-text); }

.cart-sum__total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--color-line);
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
}

.cart-sum__total strong {
  font-size: var(--fs-h4);
  color: var(--color-text);
}

/* The delivery table sits under the summary rather than inside it:
   in the card it competed with the total for the same corner. */
.cart-side .delivery {
  padding: var(--space-5);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

/* ---------------------------------------------------------
   Discount code — basket and checkout
   --------------------------------------------------------- */
.coupon {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.coupon__label {
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

.coupon__row {
  display: flex;
  gap: var(--space-2);
  margin-block-start: var(--space-2);
}

.coupon__input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.55rem var(--space-3);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-sm);
  background: var(--color-surface-sunken);
  color: var(--color-text);
  font-size: var(--fs-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}
.coupon__input:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 1px;
}
.coupon__form.is-invalid .coupon__input { border-color: var(--color-danger); }

.coupon__apply { flex: none; }

.coupon__stale {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--color-warning);
}

/* A code that is holding. Brass, because it is the one thing on
   this card the shopper gained rather than owes. */
.coupon__held {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px dashed var(--color-line-gold);
  border-radius: var(--radius-md);
  background: var(--color-gold-soft);
}

.coupon__held-text {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  min-width: 0;
}

.coupon__code {
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide);
  color: var(--color-gold-text);
}

.coupon__what {
  font-size: var(--fs-xs);
  color: var(--color-text-secondary);
}

.coupon__drop {
  flex: none;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text-muted);
  font-size: var(--fs-xs);
  cursor: pointer;
}
.coupon__drop:hover { color: var(--color-danger); }

/* =========================================================
   Checkout
   ---------------------------------------------------------
   Same two-column frame as the basket — the summary belongs
   in the same place on both pages — with the form broken
   into numbered panes. Numbering them is not decoration: it
   tells a shopper how much is left before they are asked to
   pay, which is the question that closes tabs.
   ========================================================= */
.checkout {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.pane {
  padding: var(--space-6);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

.pane__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0 0 var(--space-5);
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
}

.pane__step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  inline-size: 1.55rem;
  block-size: 1.55rem;
  border: 1px solid var(--color-line-gold);
  border-radius: var(--radius-pill);
  color: var(--color-gold-text);
  font-size: var(--fs-xs);
}

.pane__grid {
  display: grid;
  gap: var(--space-4) var(--space-5);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.pane__wide { grid-column: 1 / -1; }

@media (max-width: 560px) {
  .pane { padding: var(--space-5); }
  .pane__grid { grid-template-columns: minmax(0, 1fr); }
}

/* ---- 1 · Where it is going ----
   The form carries .field too, so its stacking and its control
   colours come from components/field.css rather than being said
   a second time here. */
.zoner__row {
  display: flex;
  gap: var(--space-3);
  margin-block-start: var(--space-2);
}

.zoner__row .field__control { flex: 1 1 auto; min-width: 0; }
.zoner__go { flex: none; }

/* With script the select reloads the page on change; without it
   this button is the only way to price the delivery. */
:root.js .zoner__go { display: none; }

.zoner__hint {
  margin: var(--space-2) 0 0;
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

/* The answer to the one question everybody asks first. Brass rule
   under it, the same mark the rest of the shop uses for a figure
   worth stopping on. */
.zoner__answer {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
  margin: var(--space-3) 0 0;
  padding-block-start: var(--space-3);
  border-block-start: 1px solid var(--color-line-gold);
}

.zoner__name { font-weight: var(--fw-bold); color: var(--color-text); }
.zoner__cost { font-weight: var(--fw-bold); color: var(--color-gold-text); }
.zoner__eta { font-size: var(--fs-sm); color: var(--color-text-muted); }

.zoner .alert { margin-block-start: var(--space-3); }

/* Delivery or pickup: two cards side by side, drawn like the
   payment choices below so one page does not speak two dialects. */
.ways {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  margin-block-end: var(--space-2);
}

.way {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-1) var(--space-3);
  align-items: start;
  padding: var(--space-4);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  cursor: pointer;
}
.way:hover { border-color: var(--color-line-strong); }
.way:has(input:checked) {
  border-color: var(--color-primary);
  background: var(--color-primary-soft);
}
.way:has(input:focus-visible) {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.way input {
  grid-row: 1 / span 2;
  margin-block-start: 0.15rem;
  accent-color: var(--color-primary);
}

.way__name { font-weight: var(--fw-bold); color: var(--color-text); }
.way__blurb { grid-column: 2; font-size: var(--fs-sm); color: var(--color-text-secondary); }

.zoner__where { color: var(--color-text); line-height: var(--lh-relaxed); white-space: pre-line; }
.zoner__go--alone { margin-block-start: var(--space-3); }

@media (max-width: 420px) {
  .ways { grid-template-columns: minmax(0, 1fr); }
}

/* ---- 3 · Payment ---- */
.pays {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.pay {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-1) var(--space-3);
  align-items: start;
  padding: var(--space-4);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}

.pay:hover { border-color: var(--color-line-strong); }
.pay:has(input:checked) {
  border-color: var(--color-primary);
  background: var(--color-primary-soft);
}
.pay:has(input:focus-visible) {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.pay input {
  grid-row: 1 / span 2;
  margin-block-start: 0.15rem;
  accent-color: var(--color-primary);
}

.pay__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.pay__name { font-weight: var(--fw-bold); color: var(--color-text); }

.pay__blurb {
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
}

/* The wallet number, the account, or "pay the courier". Shown by
   CSS alone when its own radio is checked — a shopper with no
   JavaScript still gets the number they have to transfer to. */
.pay__note {
  display: none;
  grid-column: 2;
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  font-size: var(--fs-sm);
  line-height: var(--lh-relaxed);
  color: var(--color-text);
}
.pay input:checked ~ .pay__note { display: block; }

/* shop.js hides this block by setting the hidden attribute, and an
   author rule of any kind beats the browser's own [hidden]{display:none}
   — so «الدفع عند التسليم» was leaving the transfer-number fields on the
   page, asking for a reference that does not exist. Said here rather
   than by swapping the script to a class, so the block stays hidden from
   assistive technology as well as from the eye. */
.proof[hidden] { display: none; }

.proof {
  display: grid;
  gap: var(--space-4) var(--space-5);
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-block-start: var(--space-5);
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--color-line);
}

@media (max-width: 560px) {
  .proof { grid-template-columns: minmax(0, 1fr); }
}

/* ---- Submit ---- */
.checkout__submit {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  align-items: center;
}

.checkout__vow {
  max-width: 52ch;
  margin: 0;
  font-size: var(--fs-xs);
  line-height: var(--lh-relaxed);
  color: var(--color-text-muted);
  text-align: center;
}

/* Under the vow, centred on the button like it. Small: this is the last
   reassurance before the press, not a second headline. */
.checkout__verified {
  justify-content: center;
  margin-block-start: var(--space-2);
  font-size: var(--fs-xs);
  text-align: center;
}

.checkout__verified .verified__why {
  text-align: center;
}

/* ---- The basket, read-only, in the summary card ---- */
.cart-sum__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.cart-sum__edit {
  font-size: var(--fs-xs);
  font-weight: var(--fw-regular);
  color: var(--color-text-secondary);
  border-block-end: 1px solid var(--color-line-gold);
  padding-block-end: 1px;
}
.cart-sum__edit:hover { color: var(--color-text); }

.mini {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.mini__line {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--space-3);
  align-items: baseline;
  font-size: var(--fs-sm);
}

.mini__qty { color: var(--color-text-muted); }

.mini__what {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.mini__name { color: var(--color-text); line-height: var(--lh-snug); }
.mini__variant { font-size: var(--fs-xs); color: var(--color-text-muted); }
.mini__sum { font-weight: var(--fw-bold); color: var(--color-text); white-space: nowrap; }

.cart-sum__free { color: var(--color-growth-text); }

/* =========================================================
   Order placed
   ---------------------------------------------------------
   The same two-column frame as the basket and the checkout,
   under a heading that does one job: hand over the order
   number. That number and the phone number are what the
   tracking page asks for, so it is set as a figure to be
   copied, not as a line of body text to be skimmed past.

   No confetti and no celebration graphic. The shopper has
   just given a stranger their address and, on a transfer
   rail, their money; the tone that reassures is the tone of
   a receipt.
   ========================================================= */
.done {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--space-5);
  align-items: start;
  padding-block: var(--space-8) var(--space-10);
  border-block-end: 1px solid var(--color-line);
  margin-block-end: var(--space-8);
}

.done__mark {
  grid-row: 1 / 3;
  display: inline-flex;
  color: var(--color-success);
}

.done__mark svg {
  inline-size: 2.75rem;
  block-size: 2.75rem;
}

.done__title {
  margin: 0;
  font-size: var(--fs-h2);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
}

/* A flex line, not a sentence: the shopper's name sits between two
   translated halves, and a space that falls where a right-to-left run
   meets a left-to-right one is a space that sometimes is not drawn.
   A gap is geometry and cannot go missing. */
.done__sub {
  display: flex;
  flex-wrap: wrap;
  gap: 0 0.25em;
  max-width: 62ch;
  margin: var(--space-3) 0 0;
  color: var(--color-text-secondary);
}

/* The number, given the weight of the thing the page exists to
   hand over. Brass rule under it rather than a filled box: it is
   a fact to keep, not a button to press. */
.done__number {
  grid-column: 2;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  /* The rule is as wide as the number, not as wide as the column: a
     hairline running the width of the page reads as a divider, and the
     block already ends in one. */
  inline-size: fit-content;
  margin: var(--space-6) 0 0;
  padding-block-end: var(--space-3);
  padding-inline-end: var(--space-8);
  border-block-end: 1px solid var(--color-line-gold);
}

.done__number-label {
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

.done__number-value {
  font-size: var(--fs-h3);
  font-weight: var(--fw-bold);
  letter-spacing: 0.02em;
  color: var(--color-gold-text);
  user-select: all;
}

.done__keep {
  grid-column: 2;
  max-width: 62ch;
  margin: var(--space-3) 0 0;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

/* The receipt line sits under the one above it, and the address inside it
   is a figure the shopper may want to read back — so it keeps the muted
   size but not the muted colour, and the address is set apart from the
   sentence rather than lost in it. */
.done__receipt {
  margin-block-start: var(--space-2);
  color: var(--color-text-secondary);
}

.done__receipt bdi {
  color: var(--color-text);
  font-weight: var(--fw-medium);
  overflow-wrap: anywhere;
}

@media (max-width: 560px) {
  /* The mark stops holding a column of its own and sits above the
     heading: at this width a 44px gutter costs the title a word per
     line for the whole block. */
  .done {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 0;
    padding-block: var(--space-6) var(--space-8);
  }
  .done__mark { grid-row: auto; margin-block-end: var(--space-3); }
  .done__number,
  .done__keep { grid-column: 1; }
}

/* ---- What is still to pay ---- */
.pay-say {
  margin: 0;
  line-height: var(--lh-normal);
  color: var(--color-text-secondary);
}

.due {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin: var(--space-5) 0 0;
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--color-line-gold);
  border-radius: var(--radius-md);
  background: var(--color-gold-soft);
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
}

.due strong {
  font-size: var(--fs-h4);
  color: var(--color-text);
}

.pay-ref {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  margin: var(--space-4) 0 0;
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
}
.pay-ref .u-figure { color: var(--color-text); }

.pay-state {
  margin: var(--space-4) 0 0;
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

/* ---- What happens next ----
   A rail with a dot per step. The step running now is the only
   one that carries colour, because the answer to "where is my
   order" is a position on this list. */
.steps {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.steps__item {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-4);
  align-items: start;
}

.steps__dot {
  inline-size: 0.7rem;
  block-size: 0.7rem;
  margin-block-start: 0.42em;
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-pill);
  background: var(--color-canvas);
}

.steps__item.is-now .steps__dot {
  border-color: var(--color-growth);
  background: var(--color-growth);
  box-shadow: 0 0 0 4px var(--color-growth-soft);
}

/* Behind us: the ring without the fill. Solid for both would make the
   stage the order is standing on indistinguishable from the four it
   has already left, which is the one thing this rail has to say. */
.steps__item.is-done .steps__dot {
  border-color: var(--color-growth);
  background: var(--color-growth-soft);
}

/* Where the order stopped. */
.steps__item.is-dead .steps__dot {
  border-color: var(--color-danger);
  background: var(--color-danger);
  box-shadow: 0 0 0 4px var(--color-danger-soft);
}

/* The rail between the dots. Drawn from the dot's own column so it
   stays under it in both writing directions. */
.steps__item:not(:last-child)::before {
  content: "";
  position: absolute;
  inset-block: 1.3em -2rem;
  inset-inline-start: 0.32rem;
  inline-size: 1px;
  background: var(--color-line);
}

.steps__what {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.steps__title { color: var(--color-text); }
.steps__item.is-now .steps__title { font-weight: var(--fw-semibold); }

.steps__note {
  display: flex;
  flex-wrap: wrap;
  gap: 0 0.25em;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

/* ---- Address, as written down ----
   Not «.facts»: pages/dashboard.css already has a .facts, and it is a
   different widget — a key on one edge and a figure on the other. An
   address is a long line that has to sit next to a short label, so this
   one is a two-column list. Same word, two meanings, is how a system
   starts contradicting itself. */
.order-facts {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
}

.order-facts__row {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: var(--space-4);
  font-size: var(--fs-sm);
}

.order-facts__row dt { color: var(--color-text-muted); }
.order-facts__row dd {
  margin: 0;
  color: var(--color-text);
  line-height: var(--lh-snug);
}

.order-facts__fix {
  margin: var(--space-5) 0 0;
  padding-block-start: var(--space-4);
  border-block-start: 1px solid var(--color-line);
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

/* A courier the shop booked by hand links out to its own tracking page. */
.order-facts__link { margin-block-start: var(--space-5); }

/* The courier's journey under its facts (x-shop.courier): the same rule
   line that sets off a note, so the rail reads as its own part. */
.order-facts + .steps,
.order-facts__fix + .steps {
  margin-block-start: var(--space-5);
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--color-line);
}

@media (max-width: 560px) {
  .order-facts__row { grid-template-columns: minmax(0, 1fr); gap: var(--space-1); }
}

/* ---- Offers under the buy button: a name, its tiers, what counts with it ---- */
.buy__offer { margin-block-start: var(--space-4); }

.buy__offer-name {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--fs-sm);
}

.buy__offer-end,
.buy__offer-with {
  color: var(--color-text-muted);
  font-size: var(--fs-xs);
}

.buy__offer-with { margin: var(--space-2) 0 0; }

.buy__tiers {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.buy__tiers li {
  font-size: var(--fs-sm);
  border: 1px dashed var(--color-line-gold);
  border-radius: var(--radius-sm, 6px);
  padding: 2px var(--space-2);
}

/* ---- «أعلمني حين يتوفّر» on a sold-out product ---- */
.notify {
  display: grid;
  gap: var(--space-3);
  margin-block-start: var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--color-line-gold);
  border-radius: var(--radius-md, 10px);
}

.notify__head { margin: 0; font-weight: var(--fw-bold); }
.notify__done { margin: 0; color: var(--color-text-secondary); }

/* ---- The customer's yes or no, on their own order link ---- */
.order-answer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-block-start: var(--space-4);
}

/* ---- Where to go from here ----
   Under the summary card on both the confirmation and the tracking
   page, so it is not named after either of them. */
.side-acts {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.side-acts__back {
  align-self: center;
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
  border-block-end: 1px solid var(--color-line-gold);
  padding-block-end: 1px;
}
.side-acts__back:hover { color: var(--color-text); }

/* The date the order was placed, in the summary card's heading —
   the same slot the checkout gives its «تعديل» link. */
.cart-sum__when {
  font-size: var(--fs-xs);
  font-weight: var(--fw-regular);
  color: var(--color-text-muted);
}

/* =========================================================
   Tracking
   ---------------------------------------------------------
   Two pages: two fields to find an order, then the order.
   The second one borrows the confirmation's frame whole —
   same rail, same summary card, same address block — because
   a buyer who saw one and comes back to the other should not
   have to learn the page twice.
   ========================================================= */
/* The heading is inside this column, not above it: a full-width title
   and rule over a narrow card leaves the page looking like it lost its
   right half on the way down. */
/*
   Find my order — centred, and no longer in a box.

   Two fields in a bordered panel pushed against the leading edge of a
   1180px page is a form on an admin screen. It is also the only thing on
   this page, and the one place a shopper arrives already slightly anxious:
   they have paid and they want to know where it is.

   So it sits in the middle of the page at a reading width, the heading
   gets the size the other pages' headings got, and the panel around the
   fields goes — the same trade the product page's buy panel made. A page
   with one thing on it does not need that thing drawn around.
*/
.finder {
  max-inline-size: 34rem;
  margin-inline: auto;
  margin-block-end: var(--space-24);
  text-align: center;
}

.finder .page-top {
  border-block-end: 0;
  margin-block-end: var(--space-8);
}

.finder .page-top__note { margin-inline: auto; }

/* The fields read left-to-right-ish and must not inherit the centring:
   a label centred over its own input is a form nobody can scan down. */
.finder .pane {
  padding: 0;
  border: 0;
  background: none;
  text-align: start;
}

.finder__grid {
  display: grid;
  gap: var(--space-4) var(--space-5);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (max-width: 560px) {
  .finder__grid { grid-template-columns: minmax(0, 1fr); }
}

.finder__go { margin-block-start: var(--space-6); }

.finder__aside {
  display: flex;
  flex-wrap: wrap;
  gap: 0 0.25em;
  margin: var(--space-5) 0 0;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

.finder__aside a {
  color: var(--color-text-secondary);
  border-block-end: 1px solid var(--color-line-gold);
  padding-block-end: 1px;
}
.finder__aside a:hover { color: var(--color-text); }

/* ---- The order's own page ---- */
.track-top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-5);
}

/* The number is a <bdi> beside the word, not inside it: «طلب رقم 1000»
   written as one string reorders around the digits with the language
   toggle, and a gap cannot be reordered. */
.track-top__title {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-3);
  font-size: var(--fs-h3);
}

.track-grant {
  margin: 0;
  font-size: var(--fs-xs);
  line-height: var(--lh-normal);
  color: var(--color-text-muted);
}

/* =========================================================
   Video
   ---------------------------------------------------------
   The list is the shelf the home page already uses, at full
   width. The clip's own page is the player and, beside it,
   the products pinned to the timeline — near enough that a
   viewer can look from the bottle on screen to its price
   without losing the frame.
   ========================================================= */
.vlist {
  display: grid;
  gap: var(--space-10) var(--space-6);
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
  margin-block-end: var(--space-8);
}

.vlist__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

/*
   One clip on the index, lying down.

   The home page's band worked this case out and this page had never been
   told: a shop with a single clip opened «شاهد واشترِ» on one narrow card
   with three empty columns beside it, which reads as a page that failed to
   load rather than as a shop with one clip.

   The rule is deliberately the same shape as .vshelf--solo rather than a
   second idea about the same problem — the poster keeps its 9/13 frame at a
   readable width and everything the clip contains moves beside it, which is
   the only layout with room to say what is in it.
*/
.vlist--solo { grid-template-columns: minmax(0, 1fr); }

@media (min-width: 720px) {
  .vlist--solo .vlist__item {
    display: grid;
    grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
    align-items: start;
    gap: var(--space-2) var(--space-8);
  }

  .vlist--solo .vcard {
    display: contents;
  }

  .vlist--solo .vcard__frame {
    grid-column: 1;
    grid-row: 1 / span 3;
  }

  .vlist--solo .vcard__title {
    grid-column: 2;
    font-size: var(--fs-h3);
    line-height: var(--lh-tight);
    letter-spacing: var(--tracking-tight);
  }

  .vlist--solo .vcard__pins {
    grid-column: 2;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
    gap: var(--space-1) var(--space-4);
    align-content: start;
  }

  /* The count is the item's own child, not the card's, so it needs
     placing by hand once the card has dissolved into the grid. */
  .vlist--solo .vlist__count { grid-column: 2; }
}

.vlist__count {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}

.vlist__none { margin-block-end: var(--space-16); }

/* ---- One clip ---- */
/* The player column is as wide as the clip and no wider, and the rail
   sits against its edge, the way YouTube seats a Short. fit-content lets
   a landscape clip take every pixel the rail leaves it; a 9:16 clip
   settles at the width its height allows, and the pair is centred
   instead of a black band filling the difference. */
.watch {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: fit-content(100%) minmax(17.5rem, 21.25rem);
  justify-content: center;
  align-items: start;
  padding-block-end: var(--space-12);
}

@media (max-width: 1000px) {
  /* The rail goes under the player rather than squeezing it: a
     16:9 frame narrower than about 460px stops being watchable. */
  .watch { grid-template-columns: minmax(0, 1fr); }
}

.watch__main {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

/* The words under the clip wrap at its width rather than setting it. A
   long description would otherwise stretch the column past a vertical
   clip and put the black band straight back. */
.watch__main > :not(.stage) { contain: inline-size; }

/* The frame is the clip's own box. The video sizes itself from its
   natural shape — as wide as the column allows or as tall as the screen
   under the header allows, whichever binds first — and the frame wraps
   it, so a 9:16 clip gets a 9:16 frame. object-fit: contain covers the
   moment a poster and its clip disagree on shape: fitted, never cut. */
.stage {
  --stage-cap: max(18rem, calc(100svh - var(--header-height) - var(--space-12)));

  /* The dark theme's deepest ground in both themes, so the frame reads
     as the player's and not as the page. */
  --stage-ground: var(--athar-surface-container-lowest);

  position: relative;
  display: grid;
  place-items: center;
  align-self: center;
  max-inline-size: 100%;
  overflow: clip;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--stage-ground);
}

/* No clip to take a shape from: a 16:9 box for the message to sit in. */
.stage--gone {
  inline-size: 48rem;
  aspect-ratio: 16 / 9;
}

.stage__video {
  display: block;
  inline-size: auto;
  block-size: auto;
  max-inline-size: 100%;
  max-block-size: var(--stage-cap);
  object-fit: contain;
  background: var(--stage-ground);
}

.stage__gone {
  max-width: 34ch;
  margin: 0;
  padding: var(--space-6);
  text-align: center;
  line-height: var(--lh-normal);
  color: var(--athar-on-surface-variant); /* on the dark ground in either theme */
}

.watch__title {
  margin: 0;
  font-size: var(--fs-h4);
  line-height: var(--lh-snug);
}

.watch__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

.watch__say {
  max-width: 68ch;
  margin: 0;
  line-height: var(--lh-normal);
  color: var(--color-text-secondary);
}

/* ---- The pinned products ---- */
.watch__side {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  position: sticky;
  top: calc(var(--header-height) + var(--space-4));
}

@media (max-width: 1000px) {
  .watch__side { position: static; }
}

.watch__head {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
}

.watch__none,
.watch__hint {
  margin: 0;
  font-size: var(--fs-xs);
  line-height: var(--lh-normal);
  color: var(--color-text-muted);
}

.pins {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.pin {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
}

/* The product on screen right now. A row that lights up as the
   bottle appears is the whole of video commerce; without it the
   list is a second catalogue printed beside a film. */
.pin.is-now {
  border-color: var(--color-growth);
  background: var(--color-growth-soft);
}

.pin__at {
  flex: none;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-sm);
  background: var(--color-surface-sunken);
  font-size: var(--fs-xs);
  color: var(--color-text-secondary);
}

.pin__at--go {
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.pin__at--go:hover {
  border-color: var(--color-growth);
  color: var(--color-growth-text);
}

.pin.is-now .pin__at {
  border-color: var(--color-growth);
  color: var(--color-growth-text);
}

.pin__link {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-3);
  align-items: center;
  min-width: 0;
}

.pin__img {
  inline-size: 48px;
  block-size: 48px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--color-surface-sunken);
}

.pin__img--empty {
  display: grid;
  place-items: center;
  border: 1px solid var(--color-line);
  color: var(--color-text-muted);
  font-size: var(--fs-sm);
}

.pin__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.pin__name {
  color: var(--color-text);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
}

.pin__price { color: var(--color-text-secondary); font-size: var(--fs-xs); }

.pin__link:hover .pin__name { color: var(--color-growth-text); }

/* The «مقاطع أخرى» shelf sits inside the container here, so it
   does not carry the band's own horizontal padding. */
.watch + .shop-band { padding-block: 0 var(--space-16); }

/* =========================================================
   Merchant text: the journal, a post, a page
   ---------------------------------------------------------
   .prose above already dresses everything Html::safe lets
   through, so what is written here is the frame around it:
   the title, the line of facts under it, and the way out.
   One column at 68ch. Running text wider than that is read
   twice.
   ========================================================= */
.alist { margin-block-end: var(--space-10); }
.alist__none { margin-block-end: var(--space-16); }

.post {
  max-width: 68ch;
  margin-block: var(--space-8) var(--space-10);
}

.post__top {
  margin-block-end: var(--space-8);
  padding-block-end: var(--space-6);
  border-block-end: 1px solid var(--color-line);
}

.post__title {
  margin: 0 0 var(--space-4);
  font-size: var(--fs-h3);
  line-height: var(--lh-tight);
}

.post__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}
.post__dot { color: var(--color-line-strong); }

.post__cover {
  width: 100%;
  aspect-ratio: 16 / 9;
  margin-block-end: var(--space-8);
  border-radius: var(--radius-lg);
  object-fit: cover;
}

/* The excerpt again, as the opening line: it is the sentence the card
   promised, and a post that opens on something else reads as a different
   post than the one that was clicked. */
.post__lead {
  margin: 0 0 var(--space-6);
  font-size: var(--fs-lg);
  line-height: var(--lh-relaxed);
  color: var(--color-text);
}

.post__back {
  display: inline-block;
  margin-block-end: var(--space-8);
  padding-block-end: 2px;
  font-size: var(--fs-sm);
  color: var(--color-text-secondary);
  border-block-end: 1px solid var(--color-line-gold);
}
.post__back:hover { color: var(--color-text); }

/* A policy page answers the common case. The line under it is for the
   shopper whose case is not the common one, and who would otherwise
   close the tab. */
.docpage__ask {
  max-width: 68ch;
  margin-block-end: var(--space-16);
  padding: var(--space-5) var(--space-6);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

.docpage__ask-line {
  margin: 0 0 var(--space-2);
  color: var(--color-text);
}

.docpage__ask-ways {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  margin: 0;
  font-size: var(--fs-sm);
}
.docpage__ask-ways a {
  color: var(--color-growth-text);
  padding-block-end: 2px;
  border-block-end: 1px solid transparent;
}
.docpage__ask-ways a:hover { border-block-end-color: var(--color-line-gold); }

/* =========================================================
   Premium fills — §7's two unspent gradients

   --gradient-primary and --gradient-gold were written into
   tokens.css in §7 and never used: they are for a large
   surface carrying no small text, and until the storefront
   grew a hero panel and a sale flag there was no such surface.

   Both are laid over a solid fill, never instead of one, so
   the colour a label's contrast was measured against is still
   underneath. A gradient that changes the ground under text is
   a contrast bug with a nice name.
   ========================================================= */

/* The hero's «كل المنتجات» and the sale flag used to carry the
   sheen, a soft shadow and a gradient. Both are flat since
   2026-09-17 (see .shop-page above): a shop's colour used solid. */
.pcard__flag--sale {
  background: var(--color-primary);
  border-color: transparent;
  color: var(--color-on-primary);
}

/* Brass is reserved. It marks the shop's own picks and nothing
   else, so it stays a signal rather than decoration. */

/* ---------------------------------------------------------
   The row that raises a basket

   Small tiles laid across rather than down: this is an
   afterthought offered to somebody who has finished shopping,
   and a full grid of it reads as the shop refusing to let
   them leave.
   --------------------------------------------------------- */
.xsell {
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--color-line);
}

.xsell__head {
  margin: 0 0 var(--space-3);
  color: var(--color-text-secondary);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-eyebrow);
}

.xsell__row {
  display: flex;
  gap: var(--space-3);
  margin: 0;
  padding: 0 0 var(--space-2);
  overflow-x: auto;
  list-style: none;
  scroll-snap-type: x proximity;
  /* The scrollbar is the only thing here allowed to say
     «there is more this way», so it stays. */
  scrollbar-width: thin;
}

.xsell__item {
  flex: none;
  inline-size: 9.5rem;
  scroll-snap-align: start;
}

.xtile {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  block-size: 100%;
  padding: var(--space-2);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  text-decoration: none;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.xtile:hover { border-color: var(--color-line-strong); color: var(--color-text); }

.xtile__shot {
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: var(--radius-xs);
  background: var(--color-surface-sunken);
}
.xtile__shot img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

.xtile__name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--fs-xs);
  line-height: var(--lh-snug);
}

.xtile__price {
  margin-block-start: auto;
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
}

/* The add button, for a product with nothing to choose. A
   product with variants gets no button at all — sending a
   shopper to a control that answers «اختر المقاس أولًا» is
   worse than sending them to the product. */
.xsell__form { margin-block-start: var(--space-2); }

.xsell__add {
  inline-size: 100%;
  padding: 0.375rem;
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--color-text);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--fs-eyebrow);
  transition: background var(--dur-fast) var(--ease-out);
}
.xsell__add:hover { background: var(--color-surface-raised); }

/* ---------------------------------------------------------
   Recently seen

   A band like any other, filled from ids the browser kept. It
   ships hidden so a first visit never renders a heading over
   nothing.
   --------------------------------------------------------- */
.recent[hidden] { display: none; }

/* ---------------------------------------------------------
   The shorter road

   Under the row that holds the quantity and the basket, at
   full width, and in the secondary weight. A shop whose
   loudest button skips the basket is a shop that has decided
   nobody will ever buy two things.
   --------------------------------------------------------- */
.buy__now { margin-block-start: var(--space-3); }

/* ---------------------------------------------------------
   Questions before buying

   The accordion itself is components/accordion.css, shared
   with the marketing pages. This is only the measure it sits
   in and the heading over it: on a product page the block is
   a footnote to the description, not a section of its own, so
   it is set narrower and quieter than «التفاصيل» above it.
   --------------------------------------------------------- */
.qna {
  max-inline-size: 52rem;
  margin-block-start: var(--space-10);
}

.qna__head {
  margin: 0 0 var(--space-2);
  font-size: var(--fs-h4);
  font-weight: var(--fw-bold);
}

/* One step down from the marketing accordion, which is set
   for a page whose whole job is answering questions. Here the
   questions are beside a price. */
.qna .accordion__head {
  padding-block: var(--space-4);
  font-size: var(--fs-base);
}

.qna__ways {
  margin: 0;
  padding-inline-start: var(--space-5);
  color: inherit;
}
.qna__ways li { margin-block: var(--space-1); }

/* themes/layouts.css */
/* =========================================================
   Storefront layouts — three arrangements of one shop

   The sixth theme dimension. Palette, type, corner, density
   and band order were already the merchant's; how the shop is
   *arranged* was not, so two stores on different palettes were
   still the same store in two colours.

   Everything here answers one attribute on <html>:

       data-layout="standard" | "fashion" | "yookeh"

   written by layouts/shop.blade.php out of StoreTheme::resolve().
   No template branches on it, no component knows it exists, and
   a fourth arrangement is a block of CSS rather than a fourth
   copy of fourteen pages (StoreTheme, scope decision 2026-09-01).

   Loaded last in the shop shell, after components/product-card.css,
   so these rules win on order rather than on specificity — the
   selectors below are deliberately no heavier than the ones they
   are overriding.

   Layer 3 only — every value below reads a token.
   ========================================================= */

/* ---------------------------------------------------------
   Standard

   What every shop rendered as before this file existed, named
   so a merchant can come back to it. Declared rather than left
   implicit: somebody who tries «أناقة» and changes their mind
   needs somewhere to return to, and "no layout" is not a thing
   anyone can pick off a menu.
   --------------------------------------------------------- */
/*
   Two ratios each, from here on: the ordinary tile and the one
   hero tile the home page's first grid opens on.

   The hero is two columns wide and has to come out the same
   height as the ordinary tiles beside it, or the first row of
   the shop steps. So its ratio is not a landscape picked by
   eye — it is this layout's own ratio times a little over two,
   because what the hero spans is two columns *plus the gap
   between them*, and under «يوكه» minus the padding that
   layout puts inside every tile.

   Measured at 1280 rather than derived, since that padding
   makes the excess differ per layout: ×2.08 standard, ×2.1
   fashion, ×2.14 yookeh. All three land within two pixels of
   the ordinary frame beside them, at three columns and at
   four. Change the ordinary ratio and this one moves with it.
*/
:root[data-layout="standard"] .pcard__frame {
  --pcard-ratio: 4 / 5;
  --pcard-hero-ratio: 5 / 3;
}
/* Nothing to add: the standard tile is the framed one in
   components/product-card.css. */

/* ---------------------------------------------------------
   Fashion Luxury

   Editorial, and the changes are almost all subtraction: taller
   images, a hairline under the tile instead of a frame around
   it, wide letter-spacing on the band headings, and a price
   that stops competing with the name.

   A luxury shelf is mostly empty space. The one thing this
   layout must not do is add ornament in order to say so.
   --------------------------------------------------------- */
/* Square corners: a right angle is the editorial version of
   the same frame. */
:root[data-layout="fashion"] .pcard {
  border-radius: 0;
}
:root[data-layout="fashion"] .pcard:hover {
  border-color: var(--color-text);
}

:root[data-layout="fashion"] .pcard__frame {
  --pcard-ratio: 2 / 3;
  --pcard-hero-ratio: 7 / 5;
  background: transparent;
}

/* More air under the price than the other two layouts give it.
   A luxury shelf is mostly empty space, and the plate is where
   this layout still has room to say so. */
:root[data-layout="fashion"] .pcard__body {
  padding: var(--space-4) var(--space-3);
}

/* The name leads and the price follows in a quieter weight: on
   a fashion shelf the object is chosen before the price is read. */
:root[data-layout="fashion"] .pcard__name {
  font-weight: var(--fw-regular);
  letter-spacing: var(--tracking-tight);
}
:root[data-layout="fashion"] .pcard__now {
  font-weight: var(--fw-regular);
  color: var(--color-text-secondary);
}

:root[data-layout="fashion"] .section-head__title {
  font-weight: var(--fw-regular);
  letter-spacing: var(--tracking-wide);
}

/* ---------------------------------------------------------
   Yookeh Modern

   The opposite move: soft, round, generous, every tile on its
   own raised surface. For shops whose photography is bright and
   whose catalogue is short enough that each item can afford the
   room.
   --------------------------------------------------------- */
/* The padding is gone: it used to hold the picture off the
   tile's edge, and a plate that stops short of the edge is a
   label lying on a card rather than the bottom of it. The
   raised ground and the soft shadow are this layout's own. */
:root[data-layout="yookeh"] .pcard {
  border-radius: var(--radius-premium);
  box-shadow: var(--shadow-soft);
  transition:
    border-color var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}
:root[data-layout="yookeh"] .pcard:hover {
  box-shadow: var(--shadow-deep);
  transform: translateY(-3px);
}

:root[data-layout="yookeh"] .pcard__frame {
  --pcard-ratio: 1 / 1;
  --pcard-hero-ratio: 15 / 7;
}

:root[data-layout="yookeh"] .pcard__name {
  font-weight: var(--fw-semibold);
}

/* Pill actions — the one place this layout is louder than the
   others. A round button under a round card is consistent;
   under a square one it is a costume. */
:root[data-layout="yookeh"] .shop-page .btn {
  border-radius: var(--radius-pill);
}

/* =========================================================
   Shapes — the header, the tile and the hero

   Three more dimensions beside the layout (StoreTheme::HEADERS,
   CARDS, HEROES). Header and card answer one attribute each on
   <html>, data-header and data-card; the hero writes its shape
   as a class on the band, because two of its shapes need markup
   the other two do not.

   Every default is the absence of a rule here: `side`, `layout`
   and `cover` are what the shop drew before these existed, so a
   store that never chose loads this block and matches nothing.

   After the layouts on purpose. A card shape is a choice made
   on top of a layout — «أناقة» with square tiles — so on equal
   specificity it has to win on order.
   ========================================================= */

/* ---------------------------------------------------------
   Header: centred

   The mark in the middle of its own row, the sections in a row
   under it. Only where the sections are in the bar at all:
   under 1000px they live in the drawer, and a centred mark
   between a burger and a basket is the side header already.
   --------------------------------------------------------- */
@media (min-width: 1001px) {
  :root[data-header="center"] .shop-header__inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
      "search mark actions"
      "nav    nav  nav";
    column-gap: var(--space-6);
    row-gap: var(--space-2);
    padding-block-end: 0;
  }

  :root[data-header="center"] .shop-mark {
    grid-area: mark;
    justify-self: center;
  }

  :root[data-header="center"] .shop-search {
    grid-area: search;
    justify-self: start;
    inline-size: 100%;
    max-width: 260px;
    margin-inline-start: 0;
  }

  :root[data-header="center"] .shop-header__actions {
    grid-area: actions;
    justify-self: end;
  }

  :root[data-header="center"] .shop-nav {
    grid-area: nav;
    justify-content: center;
    margin-inline-start: 0;
    border-block-start: 1px solid var(--color-line);
  }
}

/* ---------------------------------------------------------
   Header: transparent

   No ground while the page is at rest; the ordinary one comes
   back with .is-stuck, which shop.js sets the moment the page
   moves — a bar floating over goods with nothing behind it is
   a bar nobody can read.

   Over a cover photograph, on a wide screen, it also gives up
   its own height so the picture runs up under it. The cover
   band already washes its top edge for a sticky header (see
   .shop-hero--cover::after), which is what keeps the mark
   legible there. Not on a phone: the bar wraps to two rows
   and would sit on the shop's name.
   --------------------------------------------------------- */
:root[data-header="transparent"] .shop-header:not(.is-stuck) {
  background: transparent;
  backdrop-filter: none;
}

@media (min-width: 1001px) {
  :root[data-header="transparent"] body:has(.shop-hero--cover) .shop-header {
    margin-block-end: calc(-1 * var(--header-height) - 1px);
  }

  :root[data-header="transparent"] body:has(.shop-hero--cover) .shop-header__inner {
    flex-wrap: nowrap;
    block-size: var(--header-height);
    min-height: 0;
    padding-block: 0;
  }
}

/* ---------------------------------------------------------
   Card shapes

   Ratios as variables, the way the layouts hand them over. The
   hero tile's ratio follows the same measured rule as above —
   the ordinary ratio times a little over two — so the first
   row still comes out level: 2/3 → 7/5, 1/1 → 25/12.
   --------------------------------------------------------- */
:root[data-card="tall"] .pcard__frame {
  --pcard-ratio: 2 / 3;
  --pcard-hero-ratio: 7 / 5;
}

:root[data-card="square"] .pcard__frame {
  --pcard-ratio: 1 / 1;
  --pcard-hero-ratio: 25 / 12;
}

/* Subtraction only: the frame, the plate and the lift go, the
   photograph keeps a corner so it still reads as a picture and
   not as a hole in the page. */
:root[data-card="frameless"] .pcard,
:root[data-card="frameless"] .pcard:hover {
  border-color: transparent;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  transform: none;
}

:root[data-card="frameless"] .pcard__frame {
  border-radius: var(--radius-md);
}

:root[data-card="frameless"] .pcard__body {
  padding-inline: 0;
  border-block-start: 0;
  background: none;
}

/* ---------------------------------------------------------
   Hero: one product, and the grid

   Two columns — the words, then the goods — collapsing to one
   under 800px with the words first, since the name is still
   what tells a visitor where they are.
   --------------------------------------------------------- */
.shop-hero--product .shop-hero__inner,
.shop-hero--grid .shop-hero__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: var(--space-10);
  padding-block: var(--space-12);
}

@media (max-width: 800px) {
  .shop-hero--product .shop-hero__inner,
  .shop-hero--grid .shop-hero__inner {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-8);
    padding-block: var(--space-10);
  }
}

.shop-hero__product {
  display: grid;
  gap: var(--space-3);
  justify-self: end;
  inline-size: min(100%, 26rem);
  color: var(--color-text);
}
.shop-hero__product:hover { color: var(--color-text); }

@media (max-width: 800px) {
  .shop-hero__product {
    justify-self: stretch;
    inline-size: 100%;
  }
}

.shop-hero__product-frame {
  display: block;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--color-surface-sunken);
}

.shop-hero__product-frame img,
.shop-hero__tile img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  transition: scale var(--dur-base) var(--ease-out);
}

.shop-hero__product:hover img,
.shop-hero__tile:hover img {
  scale: 1.03;
}

.shop-hero__product-body {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
}

.shop-hero__product-name {
  font-weight: var(--fw-semibold);
}

.shop-hero__product-price {
  color: var(--color-text-secondary);
}

.shop-hero__mosaic {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.shop-hero__mosaic > li {
  min-inline-size: 0;
}

.shop-hero__tile {
  display: block;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
}

/* Three photographs: the first stands the height of the other two,
   so the block is still a square rather than a square with a bite
   out of its corner. */
.shop-hero__mosaic > li:first-child:nth-last-child(3) {
  grid-row: span 2;
}

.shop-hero__mosaic > li:first-child:nth-last-child(3) .shop-hero__tile {
  aspect-ratio: auto;
  block-size: 100%;
}

@media (prefers-reduced-motion: reduce) {
  .shop-hero__product img,
  .shop-hero__tile img {
    transition: none;
  }
}
