/* A Recomend — the three loyalty surfaces: the owner's panel (/loyalty), the
   till (/till) and the customer's cards (/cards).

   THE LOOK IS NOT DEFINED HERE. It comes from auth.css, the same sheet the
   owner and admin consoles are built on — the tokens, the .topbar/.wrap shell,
   .section, .field, .stats/.stat, table.data, .chip/.badge, .empty/.loading and
   the button classes. This file only adds shapes that sheet has no name for: a
   repeating editable row, a till-sized tap target, the stamp dots, the QR
   block, the balance headline.

   So there is no :root here, no colour literal, and no radius or spacing that
   is not a token — a change to auth.css has to move these pages too, which was
   the whole point of dropping the standalone stylesheet this replaces. Any rule
   below that can be answered by something auth.css already carries should be
   deleted rather than kept.

   owner.css carries near-identical row shapes (.menu-row, .mw-row) and is
   deliberately NOT loaded instead: it ends in a print block that hides every
   body child except #printRoot, so a page that never builds one prints blank —
   and a member list is a thing a shop prints. */

/* The topbar mark is the REAL Aloke logo, not the old «أ» stand-in. It has to
   win over console-theme.css's `.logo` gradient, which loads after this file —
   `img.logo` is one specificity step up, so order stops mattering. */
img.logo { object-fit: cover; background: none; padding: 0; }

/* Cleared messages ---------------------------------------------------------- */
/* Every message on these three pages is written with textContent and cleared
   with "" — so an emptied box has to leave the flow, or a dismissed error
   leaves a coloured band behind and a saved form keeps a stale "تم الحفظ" gap. */
.error-box:empty, .ok-box:empty, .section-sub:empty { display: none; }
/* The success twin of auth.css's .error-box, which has no counterpart there. */
.ok-box {
  color: var(--accent-press); font-size: 13px; background: var(--accent-weak);
  border-radius: var(--r-btn); padding: var(--s3); margin: var(--s3) 0;
  word-break: break-word;
}

/* Repeating editable rows ---------------------------------------------------- */
/* auth.css has .field and .grid-2 for a form whose fields are known in advance.
   It has no shape for a list the owner ADDS rows to — the rewards, the tier
   ladder, the double-point days. Same construction the console uses for its own
   row lists: a hairline card, fields that wrap before they squeeze, the delete
   at the end of the line. .lp-row on its own is just the wrapping line, so the
   same class also lays out the fixed rows inside a section. */
.lp-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s2) var(--s3); }
.lp-rows { display: flex; flex-direction: column; gap: var(--s2); margin-bottom: var(--s3); }
.lp-rows .lp-row { border: 1px solid var(--line); border-radius: var(--r-card); padding: var(--s3); }
.lp-row .field { flex: 1 1 150px; min-width: 0; margin-bottom: 0; }
/* Buttons and messages sitting on a field row align with the inputs, never
   stretch, and carry no margin of their own — the row owns the spacing. */
.lp-row > button, .lp-row > .ok-box { flex: 0 0 auto; margin: 0; }
/* The vertical rhythm inside a .section is carried by .field's bottom margin,
   which the row above deliberately strips — so the row itself has to replace
   it, or two stacked rows of fields touch. */
.section > .lp-row { margin-bottom: var(--s4); }
.section > .lp-row:last-child { margin-bottom: 0; }

/* In the console every <h2> is followed by a .section-sub, and that line is
   what holds the content off the heading — .section > h2 leaves only 4px. The
   sections here that have nothing to explain need the same air anyway. */
.section > h2 + *:not(.section-sub) { margin-top: var(--s3); }

/* A consent checkbox is one line WITH its sentence; .field would stack a label
   above a full-width control, which is wrong for a box you tick. */
.lp-check {
  display: flex; align-items: center; gap: var(--s2);
  font-size: 14px; margin-bottom: var(--s3); cursor: pointer;
}
.lp-check input {
  width: 18px; height: 18px; flex: none; accent-color: var(--accent); margin: 0; padding: 0;
}

/* The one line of page copy on /cards, whose body is entirely JS-rendered
   sections — there is no .section for it to be a .section-sub of. */
.lp-lead { margin: 0 0 var(--s4); font-size: 13px; color: var(--muted); }

/* The row of actions under a customer's card: the QR, the menu link. */
.lp-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); margin-top: var(--s3); }

/* The customer's card ---------------------------------------------------- */
/* EVERY CARD WEARS ITS OWN SHOP'S CLOTHES. cards.js writes that shop's menu
   theme onto this element's own style attribute — --surface, --text, --brand,
   --line, --font, --radius, plus the four auth.css tokens the theme has no
   name for — so the whole card and everything auth.css draws inside it (the
   .section shell, .chip, .badge, .ghost, .field) resolves in the shop's
   palette. Nothing here names a colour: this file only says WHICH properties
   follow the theme, never what they are.

   Scoped by construction, not by selector — a page shows cards from several
   businesses at once and one shop's palette must never reach the next. */
/* `color` is set here and not left to the --ink remap alone: <body> resolved
   var(--ink) once, and everything inside inherits that COMPUTED colour, not
   the variable — so a shop on a dark theme got its own near-black surface
   under the console's near-black ink and its name disappeared. Setting color
   on the card re-roots the inheritance at the shop's own text colour, and the
   remap then covers the few rules that name --ink explicitly. */
.lp-card { font-family: var(--font, inherit); color: var(--text, inherit); }
.lp-card > h2 { font-family: var(--heading, inherit); }
/* A shop's logo is any shape and any aspect; `contain` keeps it whole and the
   fixed box keeps twenty cards on one page in a straight line. */
.lp-logo {
  width: 34px; height: 34px; flex: 0 0 auto;
  border-radius: var(--r-btn); object-fit: contain;
}

/* THE CARD HEAD, DRESSED IN THE PROGRAMME'S OWN DESIGN ------------------------ */
/* The founder's «a banner image across the card head, custom logo, font, font
   colour». cards.js writes the shop-chosen banner/colours/font onto the <h2>'s
   OWN style attribute — the head is the h2, and the balance and the stamp row
   are its siblings below, so a banner is physically incapable of covering the
   numbers the customer opened the card to read.
   Applied to the h2 DIRECTLY, never a wrapper: an undesigned card keeps the exact
   .section > h2 it always had, and only a card with a design grows a box. */
.lp-logo.lp-logo-bg { padding: 3px; }

/* THE HEAD LOGO'S SLOT (cards.js, 2026-08-13) — the box the customer wallet
   draws a shop's mark in, and the thing that makes the mark REPAINTABLE.
   An <img> cannot carry a stencil (`img::after` never renders), so the mark
   sits in a span and `.silo` masks that span exactly as `.stamp-logo.silo`
   masks a stamp. Same mechanism, one page, nothing new invented.
   Scoped `>` on purpose: the owner console's preview draws a bare `.lp-logo`
   and must keep the geometry it has always had. */
.lp-logo-slot {
  width: 34px; height: 34px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  position: relative; overflow: hidden;
  border-radius: var(--r-btn);
}
.lp-logo-slot > .lp-logo { width: 100%; height: 100%; }
.lp-logo-slot.lp-logo-bg { padding: 3px; }
/* Repainted: the art becomes a stencil filled with the colour the contrast
   rule chose, which is `color` on the slot. Full strength — this is the mark
   itself, not the ghosted empty state a stamp slot draws. */
.lp-logo-slot.silo > .lp-logo { visibility: hidden; }
.lp-logo-slot.silo::after {
  content: ""; position: absolute; inset: 3px;
  background: currentColor;
  -webkit-mask: var(--silo-url) center / contain no-repeat;
  mask: var(--silo-url) center / contain no-repeat;
}
/* A colour or a banner needs a box to sit in and clip to. Two classes on the
   element clear .section > h2 without reaching for !important. */
.lp-card > h2.lp-boxed {
  padding: var(--s3);
  border-radius: var(--r-card);
  background-size: cover; background-position: center; background-repeat: no-repeat;
  overflow: hidden;
}
/* A photo head is taller and sets its text at the foot — where a banner's
   subject is least likely to be — with a whisper of shadow so light text lifts
   off a busy image, faint enough to leave dark text untouched. */
.lp-card > h2.lp-head-photo {
  min-height: 84px; align-items: flex-end;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .28);
}

/* THE CARD-DESIGN PANEL (the owner's console only) --------------------------- */
/* The live preview is the customer card in miniature, built from the SAME
   .lp-card / .lp-boxed / .lp-logo classes above — so the panel can never promise
   a look /cards will not deliver, the same contract the stamp preview keeps. */
/* THE CARD PANEL, IN TWO COLUMNS — «خلي المعاينة ثابتة في جهة».
   The split now lives at PANE level (loyalty.html): it wraps تصميم البطاقة
   AND شكل الختم AND المستويات, so the one sticky card follows the owner
   through every decision that changes it — not only the first section
   (founder, 2026-08-09). On a screen wide enough the preview STAYS while
   the sections scroll under it; below 1100px there is no room for a column,
   so it goes back to being the first thing on the pane and scrolls with it —
   a sticky panel on a phone would eat the screen it is previewing for. */
.lp-cd-split { display: block; }
@media (min-width: 1100px) {
  .lp-cd-split {
    display: grid; grid-template-columns: minmax(0, 1fr) 340px;
    gap: var(--s4); align-items: start;
  }
  .lp-cd-main { min-width: 0; order: 1; }
  .lp-cd-side {
    order: 2; position: sticky; top: var(--s3);
    /* Its own scroll, so a tall structure gallery inside the preview can never
       push the card itself out of reach. */
    max-height: calc(100vh - var(--s5)); overflow: auto;
  }
}
/* Outside any .section now, so it wears the section's own card chrome — the
   preview must not float naked on the page ground. */
.lp-cd-side {
  margin-bottom: var(--s4);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-card); padding: var(--s4);
}
.lp-prev-cap {
  margin: 0 0 var(--s2); font-size: 12px; font-weight: 700; color: var(--muted);
}
.lp-prevwrap { margin-bottom: var(--s2); }
/* THE DETAILS WASH under the preview face — the expanded body's silhouette
   (stats row + check-in plate), painted by the same resolution the apps
   apply. Its default is the wallet's own royal ground, said in loyalty.js
   (CD_BODY_SURFACE) — this block only shapes the silhouette. */
.lp-body-prev {
  padding: var(--s3); border-radius: 14px; font-size: 12px;
  margin-bottom: var(--s2);
}
.lp-body-prev-t {
  font-size: 11px; font-weight: 700; opacity: .75; margin-bottom: var(--s2);
}
.lp-body-prev-stats {
  display: flex; flex-wrap: wrap; justify-content: space-between;
  gap: var(--s1) var(--s2);
}
.lp-body-prev-stats b { font-size: 15px; margin-inline-end: 4px; }
.lp-body-prev-plate {
  display: block; height: 34px; margin-top: var(--s2); border-radius: 10px;
  background: color-mix(in srgb, currentColor 14%, transparent);
}
.lp-prevcard {
  border: 1px solid var(--line); border-radius: var(--r-card);
  padding: var(--s3); max-width: 360px;
}
/* The preview card sits outside any .section, so it inherits no .section > h2 —
   restate the head shape so the miniature matches the real card exactly. */
.lp-prevcard h2 {
  margin: 0; font-size: 20px; font-weight: 600;
  display: flex; align-items: center; gap: var(--s2);
}

/* A banner/logo upload: a thumbnail of what is set, the picker, the remove. */
.lp-cd-thumbs { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2); }
.lp-cd-thumb {
  width: 56px; height: 40px; flex: 0 0 auto;
  border: 1px solid var(--line); border-radius: var(--r-btn);
  background: var(--bg) center / cover no-repeat;
}
/* A colour swatch, and the checkbox that decides whether it is sent at all — a
   native colour input can never be empty, and empty is how a field says
   "inherit the shop's theme". */
.lp-cd-color { max-width: 190px; }

/* THE PALETTE PANEL (v1.7) ------------------------------------------------ */
/* Ten colours is a lot of controls, so they wrap as equal cards rather than a
   ragged row — an owner scanning for «لون الشارة» reads a grid faster than a
   line. */
.lp-cd-colors {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: var(--s2);
}
/* Each colour is a small CARD of its own: label, swatch, and — for the four
   that take one — the gradient under it. Left as flex children they wrapped
   into each other and the panel read as a pile of checkboxes. */
.lp-cd-colors .lp-cd-color {
  max-width: none; display: flex; flex-direction: column; gap: 4px;
  padding: var(--s2); background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r-card);
}
.lp-cd-colors .lp-cd-color > .lp-check { font-size: 12px; font-weight: 600; }
.lp-cd-colors .cd-col { width: 100%; height: 28px; padding: 0; }
/* A role wearing a gradient: the flat swatch stays visible (it is what the
   card falls back to) but says plainly that it is not what paints the card. */
.lp-cd-gradded > .cd-col { opacity: .45; }
.lp-cd-grad {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1);
  margin-top: 2px;
}
.lp-cd-grad .lp-check { font-size: 11px; color: var(--muted); }
.lp-cd-stops { display: inline-flex; align-items: center; gap: 3px; }
.lp-cd-stops input[type="color"] { width: 30px; height: 24px; padding: 0; }
.lp-cd-3rd { font-size: 11px; }
.lp-cd-graddir { max-width: 190px; }

/* THE THEME PICKER — real swatches, drawn from the served palettes. */
.lp-th-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  gap: var(--s2); margin-bottom: var(--s4);
}
.lp-th-grid--sm { grid-template-columns: repeat(auto-fill, minmax(78px, 1fr)); }
.lp-th {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: var(--s2) 4px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-card); color: var(--muted);
  font-size: 11px; font-weight: 600; line-height: 1.3; text-align: center;
  overflow: hidden;
}
.lp-th span {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lp-th-sw {
  width: 100%; height: 34px; border-radius: 8px;
  display: grid; place-items: center; font-size: 15px; font-weight: 700;
  border: 1px solid rgba(0, 0, 0, .08);
}
.lp-th-menu { background: var(--accent-weak); color: var(--accent-press); }
.lp-th:hover:not(.on) { border-color: var(--accent); }
.lp-th.on {
  border-color: var(--accent); background: var(--accent-weak);
  color: var(--accent-press);
}
.lp-cd-color .lp-check { margin-bottom: var(--s1); }
.lp-cd-color .cd-col {
  width: 100%; height: 38px; padding: 2px; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r-btn); background: var(--surface);
}
.lp-cd-color .cd-col:disabled { opacity: .45; cursor: not-allowed; }
/* Each tier's own card look, FOLDED under its row on a hairline — the full
   palette (theme grid + ten colour cards + gradients + uploads) sat OPEN in
   every row and read as a broken page. Closed it is one quiet line with a
   state pill (the same voice as the pre-arrival fold below); open it is an
   organised panel: theme → uploads → colours. Full row width at every size. */
.lp-cd-tier {
  flex: 1 1 100%; margin-top: var(--s2); padding-top: var(--s2);
  border-top: 1px dashed var(--line);
}
.lp-cd-tier > summary {
  display: flex; align-items: center; gap: var(--s2);
  min-height: 40px; cursor: pointer; list-style: none;
  font-size: 13px; font-weight: 700; color: var(--muted);
}
.lp-cd-tier > summary::-webkit-details-marker { display: none; }
.lp-cd-tier > summary:hover, .lp-cd-tier[open] > summary { color: var(--ink); }
.lp-cd-tier > summary .ic {
  width: 16px; height: 16px; flex: none; transition: transform .15s ease;
}
.lp-cd-tier[open] > summary .ic { transform: rotate(180deg); }
.lp-cd-tier-b {
  display: grid; gap: var(--s2); padding: var(--s1) 0 var(--s2);
}
.lp-cd-tier-b .lp-th-grid { margin-bottom: var(--s2); }
.lp-cd-tier-h {
  margin: var(--s2) 0 0; font-size: 12px; font-weight: 700; color: var(--muted);
}
/* The uploads keep the row voice but never squeeze: each control takes a
   comfortable column and wraps at 390px instead of overlapping. */
.lp-cd-tier-ups { align-items: flex-end; }
.lp-cd-tier-ups .field { flex: 1 1 240px; min-width: 0; }

/* PRE-ARRIVAL (wave 2) --------------------------------------------------- */
/* The per-tier disclosure, folded under its row on the same hairline voice as
   the tier's card block: closed it is one quiet line with a state pill, open
   it is the switch, the range and the plain-words promise. Full row width at
   every size — a fold squeezed beside the factor field is unreadable at
   375px. */
.lp-pa {
  flex: 1 1 100%; margin-top: var(--s2); padding-top: var(--s2);
  border-top: 1px dashed var(--line);
}
.lp-pa > summary {
  display: flex; align-items: center; gap: var(--s2);
  min-height: 40px; cursor: pointer; list-style: none;
  font-size: 13px; font-weight: 700; color: var(--muted);
}
.lp-pa > summary::-webkit-details-marker { display: none; }
.lp-pa > summary:hover, .lp-pa[open] > summary { color: var(--ink); }
.lp-pa > summary .ic {
  width: 16px; height: 16px; flex: none; transition: transform .15s ease;
}
.lp-pa[open] > summary .ic { transform: rotate(180deg); }
/* The state pill says the verdict without opening the fold. */
.lp-pa-state {
  font-size: 11px; font-weight: 700; padding: 2px 10px;
  border-radius: var(--r-pill); border: 1px solid var(--line);
  background: var(--bg); color: var(--muted);
}
.lp-pa-state.on {
  background: var(--accent-weak); border-color: var(--accent);
  color: var(--accent-press);
}
.lp-pa-b { display: grid; gap: var(--s2); padding: var(--s1) 0 var(--s2); }
.lp-pa-b .hint { line-height: 1.8; }
.lp-pa-b .field { max-width: 340px; }

/* The blacklist rows: one person per card — name, number, when, which order —
   and the forgive button pushed to the row's far end. Everything wraps at
   375px instead of squeezing. */
.lp-pb-list { display: flex; flex-direction: column; gap: var(--s2); }
.lp-pb-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2) var(--s3);
  border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--surface); padding: var(--s3) var(--s4);
}
.lp-pb-who { font-weight: 700; }
.lp-pb-meta { font-size: 13px; color: var(--muted); }
.lp-pb-meta [dir="ltr"], .lp-pb-meta[dir="ltr"] { unicode-bidi: isolate; }
.lp-pb-row .btn-danger { margin-inline-start: auto; }

/* THE WEEKLY MESSAGE (the owner's console only) ------------------------------ */
/* A counter that only matters when it is wrong: muted while there is room, the
   danger colour the moment the draft is longer than the server will accept —
   the same instant the send button goes flat. */
.lp-bc-count { margin: var(--s1) 0 0; font-size: 12px; color: var(--muted); }
.lp-bc-count.over { color: var(--danger); font-weight: 600; }
/* auth.css gives .chip a role tint and a muted one and stops there. A refusal
   needs the third: «ما تمت الموافقة» in the accent colour reads like approval. */
.chip.bad { background: var(--danger-weak); color: var(--danger); }
.lp-bc-list { display: flex; flex-direction: column; gap: var(--s2); }
.lp-bc-item {
  border: 1px solid var(--line); border-radius: var(--r-card); padding: var(--s3);
}
.lp-bc-head { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2); }
/* The owner's text as they typed it, line breaks and all — a message shown back
   reflowed into one paragraph is not the message they approved. */
.lp-bc-body {
  margin: var(--s2) 0 0; font-size: 14px;
  white-space: pre-wrap; word-break: break-word;
}
.lp-bc-meta { margin: var(--s1) 0 0; font-size: 12px; color: var(--muted); }

/* A GIFT FROM THE MENU (the owner's console only) ---------------------------- */
/* The count and the select-all sit on ONE line directly above the list, because
   the list scrolls and the send button is below it — an owner must never have
   to scroll to find out how many people they are about to pay for. */
.lp-gf-bar {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--s2); margin: var(--s3) 0 var(--s2);
}
.lp-gf-all {
  display: flex; align-items: center; gap: var(--s2);
  font-size: 13px; cursor: pointer;
}
.lp-gf-count { font-size: 12px; color: var(--muted); }
/* Over the server's cap — the same danger tint the message counter uses, so
   "you have gone too far" looks the same everywhere on this page. */
.lp-gf-count.over { color: var(--danger); font-weight: 600; }
/* A BOUNDED, SCROLLING LIST. A shop with hundreds of members would otherwise
   push the send button off the bottom of the page; capping the height keeps the
   picker and its action visible together. */
.lp-gf-list {
  max-height: 320px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--r-card);
}
.lp-gf-row {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s3); cursor: pointer;
  border-bottom: 1px solid var(--line);
}
.lp-gf-row:last-child { border-bottom: 0; }
.lp-gf-row:hover { background: var(--surface-2, rgba(0, 0, 0, 0.03)); }
/* The name takes the room; the mobile and the balance are the disambiguators
   and sit at the end, quiet. */
.lp-gf-who { flex: 1 1 auto; font-size: 14px; word-break: break-word; }
.lp-gf-meta { font-size: 12px; color: var(--muted); white-space: nowrap; }

/* The till --------------------------------------------------------------- */
/* USED ONE-HANDED, AT SPEED, ON A PHONE BEHIND A COUNTER. The console's 40px
   controls are a mis-tap here, so the till's own actions are full-width and
   56px tall: the affordance has to be unmissable rather than tasteful. */
.tap { width: 100%; min-height: 56px; font-size: 17px; }
/* 16px is the iOS threshold — below it Safari zooms the page on focus, and a
   till that jumps mid-sale costs more than a large font does. 48px is the
   smallest target a thumb hits reliably while holding the phone. */
#till input, #till select { min-height: 48px; font-size: 16px; }
/* till.js flips style.display on the video, so the closed state lives here
   rather than in an inline style it would have to fight. */
.till-cam { display: none; width: 100%; border-radius: var(--r-card); margin-top: var(--s3); }

/* The balance ------------------------------------------------------------ */
/* Read across a counter on the till, and the entire point of the page on
   /cards. .stat-num (28px) is a dashboard figure; this is a headline. Tabular
   numerals so a balance that ticks up does not shuffle its own digits. */
.balance { display: flex; align-items: baseline; gap: var(--s2); margin: var(--s3) 0; }
.balance-num {
  font-size: 44px; font-weight: 700; line-height: 1.1; margin: 0;
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.balance-unit { font-size: 15px; color: var(--muted); }

/* Stamps ----------------------------------------------------------------- */
/* A stamp card is READ AT A GLANCE — that is its whole appeal — so the balance
   is drawn and not only counted. Nothing in the system draws a row of tokens.

   FILLED VERSUS FADED IS THE WHOLE MESSAGE, so it is carried by three things
   at once and not by colour alone: opacity, ring weight, and a tinted disc.
   A card read in daylight, or by somebody who cannot separate the two hues,
   still counts the same number of stamps. */
.stamps { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s3) 0; }
.stamp {
  width: 34px; height: 34px; border-radius: var(--r-pill);
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); color: var(--muted); opacity: .4;
}
.stamp .ic { width: 20px; height: 20px; }
/* --brand is the SHOP's, set per card by cards.js; the console's accent is
   the fallback for the till and the owner's preview, which wear ours. */
.stamp.on {
  opacity: 1; color: var(--brand, var(--accent));
  border: 2px solid currentColor;
  /* Two declarations: a browser without color-mix() keeps the plain ring
     rather than losing the token's fill to an invalid value. */
  background: transparent;
  background: color-mix(in srgb, currentColor 14%, transparent);
}

/* Group headings inside the advanced fold — «الهدايا التلقائية» over its three
   gifts, «الحدود» over its three limits. One quiet line, the same voice as the
   dish editor's group headers on /owner: the grouping is the information. */
.lp-grp-h {
  margin: 0 0 var(--s2); font-size: 12px; font-weight: 700;
  color: var(--accent-press); letter-spacing: .02em;
}
.lp-adv .lp-row + .lp-grp-h { margin-top: var(--s4); }

/* The stamp picker ------------------------------------------------------- */
/* The owner is choosing a DRAWING, so the grid shows drawings — a <select> of
   twenty Arabic nouns would make them save and reload to see what they picked.
   auto-fill rather than a fixed count: this is opened on a phone behind a
   counter as often as on a laptop. */
.lp-stamp { margin-top: var(--s4); }
/* شكل الختم is its own .section on the card tab now, and applyKind() hides it
   whole on a points programme. The pane guard's own lesson, applied before it
   bites: a section could one day carry a `display` of its own, which would
   beat the UA's `[hidden]` rule — and a stamp picker showing on a points
   programme is exactly the disabled-furniture the hiding exists to end. */
.section[hidden] { display: none !important; }
.icon-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
  gap: var(--s2); margin-bottom: var(--s4);
}
.icon-pick {
  display: flex; flex-direction: column; align-items: center; gap: var(--s1);
  min-height: 74px; padding: var(--s3) var(--s1);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-card); color: var(--muted);
  font-size: 11px; font-weight: 600; line-height: 1.3; text-align: center;
}
.icon-pick .ic { width: 26px; height: 26px; color: var(--ink); }
/* THE SHOP'S OWN LOGO AS A PICKER TILE. This img had NO rule: the tile only
   ever showed the camera placeholder until the stamp logo learned to fall
   back to the shop's menu logo — the first real image then rendered at its
   NATURAL size, a 989px wordmark sprawled across the whole grid. An image in
   a 26px tile is 26px, whatever file it came from. */
.icon-pick { overflow: hidden; }
.icon-pick-logo {
  width: 26px; height: 26px;
  object-fit: contain;
  display: block;
  flex: 0 0 auto;
}
/* The label is one line always: twenty tiles of different heights read as a
   broken grid rather than a set to choose from. */
.icon-pick span {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.icon-pick:hover:not(.on) { border-color: var(--accent); }
.icon-pick.on {
  border-color: var(--accent); background: var(--accent-weak); color: var(--accent-press);
}
.icon-pick.on .ic { color: var(--accent-press); }

/* THE PAIR IN THE PICKER TILE (v1.7). When the chosen mode draws the earned
   drawing, a tile shows the mark EMPTY and EARNED side by side with a small
   arrow between them, because the owner is choosing a pair — a cup and the
   full cup — not a picture. `stampMarkHtml` stacks the two layers, and the
   tile has to give the stack a size of its own or it collapses to nothing. */
.icon-pick .mk { width: 26px; height: 26px; flex: 0 0 auto; }
.icon-pick .mk .ic { width: 26px; height: 26px; }
.pick-pair { display: flex; align-items: center; gap: 3px; }
.pick-pair .pick-arrow {
  width: 12px; height: 12px; color: var(--muted); flex: 0 0 auto;
  /* The sprite's chevron points right; the page reads right-to-left, so the
     arrow between «فاضي» and «مختوم» has to point the way the eye travels. */
  transform: scaleX(-1);
}
/* Inside a tile the two states sit SIDE BY SIDE, so the stack has to come
   apart: the empty mark is drawn on its own to the right of the arrow, and
   the earned one is the `.mk-f` layer alone — showing the stack's own empty
   layer as well would put the same drawing in the tile twice, in a 26px box
   built for one. */
.pick-pair .mk > .mk-e { display: none; }
.pick-pair .mk > .mk-f { position: static; opacity: 1; }
.pick-pair .mk { overflow: visible; }

/* HOW IT LANDS — the three modes, as a row of choices in the same visual
   language as the marks above them. Not a <select>: three named behaviours a
   person is choosing between read better as three things to press. */
.sm-modes {
  display: flex; flex-wrap: wrap; gap: var(--s2); margin-bottom: var(--s4);
}
.sm-pick {
  flex: 1 1 130px; min-width: 120px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: var(--s2) var(--s3);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-card); color: var(--muted);
  font-size: 12px; line-height: 1.4; text-align: start;
}
.sm-pick b { font-size: 13px; color: var(--ink); font-weight: 700; }
.sm-pick:hover:not(.on) { border-color: var(--accent); }
.sm-pick.on {
  border-color: var(--accent); background: var(--accent-weak);
  color: var(--accent-press);
}
.sm-pick.on b { color: var(--accent-press); }

/* The check-in QR -------------------------------------------------------- */
/* Minted on demand and only then, so the block is empty until it is asked for
   and must take up no room in the meantime. The SVG comes from our own server
   (segno) with no intrinsic size; it is held up to a scanner on whatever phone
   the customer happens to have, so it is capped in vw as well as px. */
.qr { display: none; margin-top: var(--s3); text-align: center; }
.qr:not(:empty) { display: block; }
.qr svg { width: min(240px, 60vw); height: auto; }

/* THE SHOP'S LOGO AS THE STAMP ------------------------------------------- */
/* An EMPTY slot in the logo's shape until it is earned, then the logo itself.
   The unearned state is the same drawing at a whisper — never a grey box and
   never a different shape — so the card reads as "three of these six" the way
   a paper punch card lets it. The plate keeps its ring at both states, which is
   what makes an unstamped slot look like somewhere a stamp GOES. */
.stamp-logo { overflow: hidden; padding: 3px; }
.stamp-logo img {
  width: 100%; height: 100%; object-fit: contain; display: block;
  /* Unearned: the logo held back to a ghost of itself, desaturated so a
     colourful mark does not read as already collected. */
  opacity: .22; filter: grayscale(1);
  transition: opacity .18s ease, filter .18s ease;
}
.stamp-logo.on img { opacity: 1; filter: none; }

/* ============================== THE REAL CARD, LIVE ====================== */
/* The card-design preview — the apps' card anatomy, drawn at phone-card width
   so what the owner approves is what a phone will show. Colours arrive as
   custom properties set inline by previewCardHtml; everything here reads them
   so the CSS never guesses. */
.cdp {
  max-width: 380px;
  border-radius: 18px;
  overflow: hidden;
  background: var(--cdp-bg, #5B2C91);
  color: var(--cdp-ink, #fff);
  box-shadow: 0 10px 28px rgb(0 0 0 / 0.22);
  margin-top: var(--s3);
}
.cdp-banner {
  position: relative;
  /* The iOS card's band (132pt on a ~360pt card). The preview wore 2.1 and
     the founder saw two different cards for one design. */
  aspect-ratio: 2.6;
  background-size: cover;
  background-position: center;
}
/* The identity reads against a colour WE chose, not against whatever
   photograph the owner uploaded — nothing at the top, the card's own colour
   at the foot. Same scrim the apps draw. */
.cdp-scrim {
  position: absolute; inset: 0;
  background: linear-gradient(to bottom,
    transparent 0%,
    color-mix(in srgb, var(--cdp-bg) 72%, transparent) 55%,
    color-mix(in srgb, var(--cdp-bg) 94%, transparent) 100%);
}
.cdp-banner .cdp-id { position: absolute; inset: auto 0 0 0; }
.cdp-id {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px;
}
.cdp-logo {
  width: 44px; height: 44px; flex: 0 0 auto;
  border-radius: 12px; display: grid; place-items: center;
  overflow: hidden;
}
.cdp-logo img { max-width: 82%; max-height: 82%; object-fit: contain; }
/* AN UNFRAMED LOGO THAT WOULD VANISH INTO THE CARD IS STENCILLED, not pasted:
   the mark's own alpha painted in the ink that survives this card. With
   `logo_shape: none` there is no plate left to guarantee contrast — see
   `lpBareLogoTint` — and this is the same mechanism the logo STAMP already
   uses (`.cdp-silo`) and the same answer `.template` gives on iOS. */
/* Its BOX is written inline, because a stencil has to keep the same inset as
   the picture it stands in for: full bleed with no frame, letterboxed inside
   a plate. The 100% here is only the fallback for a caller that says nothing. */
.cdp-logo-silo, .cdf-logo-silo {
  display: block; width: 100%; height: 100%;
  -webkit-mask: var(--silo-url) center / contain no-repeat;
  mask: var(--silo-url) center / contain no-repeat;
}
.cdp-name { flex: 1 1 auto; font-weight: 700; font-size: 15px; min-width: 0; }
.cdp-count { text-align: center; flex: 0 0 auto; }
.cdp-count-l {
  display: block; font-size: 9px; font-weight: 700; letter-spacing: 1px;
  opacity: 0.72;
}
.cdp-count-n { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
.cdp-body { padding: 12px 14px 14px; }
.cdp-marks {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
  margin-bottom: 10px;
}
.cdp-mark {
  width: 34px; height: 34px; display: grid; place-items: center;
  color: var(--cdp-ink);
  opacity: 0.26;
}
.cdp-mark .ic { width: 26px; height: 26px; }
.cdp-mark.on { opacity: 1; }
/* The logo slot: FULL-STRENGTH plate in both states (a dimmed plate on a dark
   card is the invisible-slot bug back again) — only the ART ghosts. */
.cdp-mark-logo { border-radius: 8px; overflow: hidden; opacity: 1; }
.cdp-mark-logo img {
  max-width: 76%; max-height: 76%; object-fit: contain;
  opacity: 0.32; filter: grayscale(1);
}
.cdp-mark-logo.on img { opacity: 1; filter: none; }
.cdp-stats {
  display: flex; justify-content: space-between; gap: var(--s3);
  font-size: 11px; opacity: 0.9;
}
.cdp-stats b { font-size: 18px; display: block; }

/* THE HOUSE CARD'S WORKING HALF (/super's Aloke preview). The shop card's
   body counts stamps; Aloke's counts thank-yous and walks toward a gift, so
   its body is a label and a bar. Same `.cdp-body` ground underneath, so a
   `body_color` / «تتبع البطاقة» wash paints both consoles' previews
   identically. */
.cdp-prog-l {
  display: flex; justify-content: space-between; gap: var(--s2);
  font-size: 12px; font-weight: 700; margin-bottom: 6px;
}
.cdp-prog {
  height: 8px; border-radius: 50px; overflow: hidden;
  background: rgba(0, 0, 0, 0.08); margin-bottom: 8px;
}
.cdp-prog > span { display: block; height: 100%; border-radius: 50px; }


/* THE LOGO'S OWN SHAPE AS THE STAMP — no boxes. The mark is the logo's alpha
   painted in the card's ink: hollow-faint when empty, filled when earned.
   Only a ONE-COLOUR mark is stencilled; a colourful crest keeps its plate
   (flattening somebody's colours is brand damage, not a style). */
.cdp-silo {
  position: relative;
  opacity: 1;
}
.cdp-silo::after {
  content: ""; position: absolute; inset: 1px;
  background: var(--cdp-ink);
  -webkit-mask: var(--silo-url) center / contain no-repeat;
  mask: var(--silo-url) center / contain no-repeat;
  opacity: 0.3;
}
.cdp-silo.on::after { opacity: 1; }

/* The same rule on the customer web wallet (see silhouetteStamps in cards.js). */
.stamp { position: relative; }
.stamp-logo.silo { background: none; border-color: transparent; }
.stamp-logo.silo img { visibility: hidden; }
.stamp-logo.silo::after {
  content: ""; position: absolute; inset: 3px;
  background: currentColor;
  -webkit-mask: var(--silo-url) center / contain no-repeat;
  mask: var(--silo-url) center / contain no-repeat;
  opacity: 0.3;
}
.stamp-logo.silo.on::after { opacity: 1; }

/* ============================ v1.5 additions ============================ */

/* MENU-LINKED PRIZE CHOICES — the per-rung picker in the owner's rewards
   editor. One panel open at a time, drawn directly under its rung's row so
   the owner never loses which prize they are filling. */
.rw-choice-panel {
  border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--surface); padding: var(--s3); margin-top: calc(-1 * var(--s1));
}
.rw-choice-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s2); margin-bottom: var(--s2);
  font-size: 13px; color: var(--muted);
}
.rw-choice-list { display: flex; flex-direction: column; gap: 2px; max-height: 300px; overflow-y: auto; }
.rw-choice-item {
  display: flex; align-items: center; gap: var(--s2);
  padding: 6px 8px; border-radius: var(--r-btn); cursor: pointer;
  font-size: 14px;
}
.rw-choice-item:hover { background: var(--accent-weak); }
.rw-choice-item input {
  width: 18px; height: 18px; flex: none; accent-color: var(--accent); margin: 0;
}
/* The tick and the dish name are ONE label; the option select sits outside it,
   or clicking the select would toggle the tick under the owner's hand. */
.rw-choice-pick {
  display: flex; align-items: center; gap: var(--s2);
  cursor: pointer; min-width: 0;
}
.rw-choice-pick span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* v1.5.2 — WHICH OPTION of that dish this rung promises. */
.rw-opt {
  margin-inline-start: auto; max-width: 45%;
  font-size: 12px; padding: 3px 6px;
  border: 1px solid var(--line); border-radius: var(--r-btn);
  background: var(--surface); color: inherit; font-family: inherit;
}
.rw-choice-item .rw-price + .rw-opt { margin-inline-start: var(--s2); }
.rw-choice-item .rw-price { margin-inline-start: auto; color: var(--muted); font-size: 12px; direction: ltr; }
.rw-choice-item .rw-gone { color: var(--danger); font-size: 12px; }
.rw-choice-empty { color: var(--muted); font-size: 13px; padding: var(--s2) 0; }

/* SUBSCRIPTIONS + SYSTEM FLAGS — the /super rows. The same quiet card row the
   clinic grant uses, plus a red state for a branch that has lapsed. */
.sub-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2) var(--s3);
  padding: 10px 12px; border: 1.5px solid var(--line); border-radius: 12px;
  margin-top: 8px;
}
.sub-row.sub-expired { border-color: var(--danger); background: var(--danger-weak); }
.sub-main { flex: 1 1 180px; min-width: 0; }
.sub-name { font-weight: 700; font-size: 15px; overflow-wrap: anywhere; }
.sub-meta { font-size: 12px; color: var(--muted); direction: ltr; text-align: end; overflow-wrap: anywhere; }
.sub-row.sub-expired .sub-meta { color: var(--danger); }
.sub-date { min-width: 150px; }
.sub-row .chip { flex: 0 0 auto; }

.flag-row {
  display: flex; align-items: flex-start; gap: var(--s3);
  padding: 12px; border: 1.5px solid var(--line); border-radius: 12px;
  margin-top: 8px;
}
.flag-row.flag-off { border-color: var(--danger); }
.flag-main { flex: 1 1 auto; min-width: 0; }
.flag-name { font-weight: 700; font-size: 15px; }
.flag-desc { font-size: 13px; color: var(--muted); line-height: 1.8; margin-top: 2px; }
.flag-state { font-size: 12px; font-weight: 700; margin-top: 4px; }
.flag-row.flag-off .flag-state { color: var(--danger); }

/* The switch itself — a real checkbox dressed as a pill, so keyboard and
   screen-reader behaviour come free. */
.flag-toggle { position: relative; flex: 0 0 auto; width: 46px; height: 26px; margin-top: 4px; }
.flag-toggle input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; margin: 0; cursor: pointer;
}
.flag-toggle .knob {
  position: absolute; inset: 0; border-radius: 999px;
  background: var(--line); transition: background .15s ease; pointer-events: none;
}
.flag-toggle .knob::after {
  content: ""; position: absolute; top: 3px; inset-inline-start: 3px;
  width: 20px; height: 20px; border-radius: 50%; background: #fff;
  box-shadow: 0 1px 3px rgba(0,0,0,.25); transition: transform .15s ease;
}
.flag-toggle input:checked + .knob { background: var(--accent); }
.flag-toggle input:checked + .knob::after { transform: translateX(-20px); }
[dir="ltr"] .flag-toggle input:checked + .knob::after { transform: translateX(20px); }
.flag-toggle input:focus-visible + .knob { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The broadcast queue's own lesson, applied here: .chip carries a `display`
   of its own, which beats the UA's `[hidden]` rule — and a "منتهي" chip
   showing on a LIVE branch is a false alarm about somebody's livelihood. */
.sub-row [hidden], .flag-row [hidden] { display: none !important; }

/* SELL A PREPAID SET TO ONE CUSTOMER (the owner's console) ------------------- */
/* Founder: «in /loyalty i want to add to customer a package». It sits INSIDE
   the bundles section, under the catalogue, because the two are one thought:
   these are the sets, and this is how one of them reaches a person. A rule
   above it says so without a second heading competing with the section's own. */
.lp-sell {
  margin-top: var(--s5); padding-top: var(--s4);
  border-top: 1px dashed var(--line);
}
.lp-sell > h3 { margin: 0; font-size: 15px; font-weight: 700; }
/* WIDER BASIS THAN THE PAGE'S 150px, so on a phone the two controls STACK
   rather than share the line. A <select> holding «باقة قهوة الشهر — ٣٠ ×
   قهوة اليوم — ٢٠٠ ر.س» in half of 375px shows about four words of it, and
   the price is the half that gets cut — which is the half that matters. */
.lp-sell .lp-row .field { flex: 1 1 240px; }
.lp-sell .hint { margin: var(--s1) 0 var(--s3); font-size: 13px; color: var(--muted); line-height: 1.8; }
/* THE SENTENCE THE OWNER READS BACK BEFORE THEY PRESS. Tinted like the page's
   own success box rather than shouted, because it is not an alarm — it is the
   receipt they are about to write, and it has to be legible at a glance on a
   phone behind a counter. */
.lp-sell-confirm {
  margin: var(--s3) 0 0; padding: var(--s3);
  border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--accent-weak); color: var(--accent-press);
  font-size: 14px; line-height: 1.8; font-weight: 600;
}
/* What the customer holds AFTERWARDS — the server's own numbers, one line per
   set, so an owner who sold a second set can see both and their expiries. */
.lp-sell-held { margin-top: var(--s3); }
.lp-sell-total { font-size: 14px; font-weight: 700; margin: 0 0 var(--s2); }
.lp-sell-grant {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--s2);
  padding: var(--s2) var(--s3); font-size: 13px;
  border: 1px solid var(--line); border-radius: var(--r-card);
  margin-top: var(--s2);
}
.lp-sell-grant .lp-sell-left { font-weight: 700; white-space: nowrap; }
.lp-sell-grant .lp-sell-when { color: var(--muted); font-size: 12px; }
/* A NUMBER IS READ LEFT TO RIGHT even on a right-to-left page. `unicode-bidi:
   isolate` so a mobile printed inside an Arabic sentence cannot drag the words
   around it out of order — and it is the SPAN that is pinned, never the
   paragraph, which is the mistake that renders a phone number backwards.
   The search box itself carries dir="auto" instead: it holds a name as often
   as a number, and forcing either direction is wrong half the time. */
.lp-sell .num { direction: ltr; unicode-bidi: isolate; }

/* ================= THE TWO-LEVEL NAV (the /loyalty tabs) ================= */
/* The owner console's pattern, the super console's shapes: three thumb-sized
   group segments, and under the active one at most five tabs — the strip
   scrolls sideways, so nothing is ever unreachable at 375px. Tokens only,
   like everything in this file. These class names exist ONLY on /loyalty
   (till and cards never render a nav), so the sheet stays safe to share. */
#lyNav { margin-bottom: var(--s4); }
.pgroup-bar {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s2); margin-bottom: var(--s3);
}
.pgroup {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 48px; padding: var(--s2) var(--s3);
  font: inherit; font-size: 14px; font-weight: 700; cursor: pointer;
  color: var(--muted); background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r-card);
}
.pgroup .ic { width: 18px; height: 18px; flex: 0 0 auto; }
.pgroup:hover { color: var(--ink); border-color: var(--accent); }
.pgroup.on {
  color: var(--accent-press); background: var(--accent-weak);
  border-color: var(--accent);
}
@media (max-width: 560px) {
  .pgroup { flex-direction: column; gap: 4px; min-height: 56px; font-size: 12.5px; padding: var(--s2); }
}
.panel-tabs {
  display: flex; gap: var(--s1); border-bottom: 1px solid var(--line);
  overflow-x: auto; scrollbar-width: none;
}
.panel-tabs::-webkit-scrollbar { display: none; }
.ptab {
  display: flex; flex-direction: column; gap: 1px; text-align: start;
  flex: 0 0 auto; min-height: 44px;
  background: none; border: none; border-bottom: 2px solid transparent;
  color: var(--muted); white-space: nowrap; cursor: pointer;
  padding: var(--s2) var(--s3); border-radius: 0; height: auto; font: inherit;
}
.ptab:hover:not(:disabled) { color: var(--ink); }
.ptab.on { color: var(--accent-press); border-bottom-color: var(--accent); }
.ptab-t { font-size: 14px; font-weight: 600; }
.ptab-s { font-size: 11px; font-weight: 400; color: var(--muted); }
.ptab.on .ptab-s { color: var(--accent-press); }
@media (max-width: 560px) {
  /* On a phone the second line is dropped rather than dragging four two-line
     tabs across the width; the title attribute keeps it. */
  .ptab-s { display: none; }
  .ptab { padding: var(--s2); }
}

/* THE PANES. The broadcast queue's own lesson, applied before it bites here:
   a section inside could one day carry a `display` of its own, which beats
   the UA's `[hidden]` rule — and a hidden tab showing through another is the
   exact confusion the tabs exist to end. */
.ly-pane[hidden], .ly-save[hidden] { display: none !important; }

/* THE SAVE BAR — the page's one big Save, visible only on the tabs whose
   fields it stores, and sticky so a long ladder never puts the button below
   the fold. It is the same .section shell, just pinned; the messages it
   carries grow the bar rather than covering content. */
.ly-save { position: sticky; bottom: 0; z-index: 5; }
.ly-save .section {
  margin-bottom: 0; padding: var(--s3) var(--s4);
  box-shadow: 0 -6px 18px rgb(0 0 0 / 0.06);
}

/* ================= THE LOYALTY DASHBOARD (the packages wave) ============ */
/* THE ONE DELIBERATE DEVIATION FROM THIS FILE'S HEADER, and it is scoped so it
   cannot leak: a categorical chart palette is a thing auth.css genuinely has
   no name for. It declares one accent, one danger and one muted — three lines
   drawn in an accent and a darker accent are two lines an owner cannot tell
   apart. So two hues are literals here (a teal and an amber, both ≥4.5:1 on
   the surface), the other two ARE tokens, and the whole set lives on #dash
   rather than :root so nothing outside this tab can pick it up. The marks
   carry classes rather than inline strokes: an SVG attribute cannot be themed,
   and a hard-coded stroke is a chart that goes black in a future palette. */
#dash {
  --ly-c1: var(--accent);
  --ly-c2: #2E7D8F;
  --ly-c3: #B07C2B;
  --ly-c4: var(--danger);
}

/* A NUMBER READS LEFT TO RIGHT inside an Arabic line, and it is the span that
   is isolated — never the paragraph. */
.ly-num { direction: ltr; unicode-bidi: isolate; font-variant-numeric: tabular-nums; }

.ly-sub-h { margin: var(--s4) 0 var(--s2); font-size: 14px; font-weight: 700; }
.ly-soon { border-color: var(--danger); background: var(--danger-weak); }

/* THE FILTER STRIP — above the numbers, never folded. It is a band rather than
   a card so it reads as a control surface and not as one more panel of data. */
.dash-filters {
  border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--bg); padding: var(--s3); margin-bottom: var(--s4);
}
.dash-filters .lp-row { margin-bottom: 0; align-items: flex-end; }
.dash-filters .field { flex: 1 1 160px; }
/* THE BROADCAST QUEUE'S LESSON, THIRD TIME PAID — and this one was caught in
   the browser, not in review: `.field` carries a `display` of its own, which
   beats the UA's `[hidden]` rule, so «من» and «إلى» sat open under «هذا
   الشهر» offering a window nothing was going to send. Same shape as the
   `.sub-row [hidden]` rule above, same reason. */
.dash-filters [hidden] { display: none !important; }
.dash-filters #dqApply { margin-inline-start: auto; }
.dash-filters .hint { margin: var(--s2) 0 0; }

/* ONE CARD PER QUESTION. Hairline, generous, and stacked — at 375px they are
   one column; the grid only kicks in where two fit side by side. */
.ly-card {
  border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--surface); padding: var(--s4); margin-bottom: var(--s4);
}
.ly-card > h3.ly-card-h { margin: 0 0 var(--s1); font-size: 16px; font-weight: 700; }
.ly-card-note { margin: 0 0 var(--s3); font-size: 13px; color: var(--muted); line-height: 1.8; }
.ly-card .stats { margin-top: var(--s3); }
.ly-nodata {
  margin: var(--s3) 0 0; padding: var(--s4);
  border: 1px dashed var(--line); border-radius: var(--r-card);
  color: var(--muted); font-size: 13px; text-align: center;
}

/* THE PLOT. A fixed viewBox stretched to the column: the geometry is computed
   once in loyalty.js (and pinned there), and the browser only scales it. */
.ly-plot { width: 100%; height: 200px; display: block; margin-top: var(--s2); }
.ly-axis { stroke: var(--line); stroke-width: 1.5; }
.ly-grid { stroke: var(--line); stroke-width: 1; stroke-dasharray: 3 4; }
.ly-tick { font-size: 11px; fill: var(--muted); direction: ltr; }
.ly-line { stroke-width: 2; fill: none; stroke-linejoin: round; stroke-linecap: round; }
.ly-area { opacity: .16; stroke: none; }
.ly-dot { stroke: var(--surface); stroke-width: 1; }
.ly-s1, .ly-f1 { stroke: var(--ly-c1); fill: var(--ly-c1); }
.ly-s2, .ly-f2 { stroke: var(--ly-c2); fill: var(--ly-c2); }
.ly-s3, .ly-f3 { stroke: var(--ly-c3); fill: var(--ly-c3); }
.ly-s4, .ly-f4 { stroke: var(--ly-c4); fill: var(--ly-c4); }
.ly-line.ly-s1, .ly-line.ly-s2, .ly-line.ly-s3, .ly-line.ly-s4 { fill: none; }

.ly-legend { display: flex; flex-wrap: wrap; gap: var(--s3); font-size: 12px; color: var(--muted); }
.ly-legend-i { display: inline-flex; align-items: center; gap: 6px; }
.ly-key { width: 12px; height: 3px; border-radius: 2px; display: inline-block; }
.ly-key.ly-f1 { background: var(--ly-c1); }
.ly-key.ly-f2 { background: var(--ly-c2); }
.ly-key.ly-f3 { background: var(--ly-c3); }
.ly-key.ly-f4 { background: var(--ly-c4); }

/* THE TEXT ALTERNATIVE. Every chart carries one; it is a real <details> so it
   is reachable by keyboard and read by a screen reader without a mouse. */
.ly-alt { margin-top: var(--s3); }
.ly-alt > summary {
  cursor: pointer; font-size: 12px; color: var(--muted);
  min-height: 32px; display: flex; align-items: center;
}
.ly-alt > summary:hover { color: var(--ink); }
.ly-alt table.data { font-size: 12px; }

/* HORIZONTAL BARS: the label is HTML (Arabic wraps, ellipsises and selects),
   the mark is a two-rect SVG stretched to the row. It grows from the RIGHT,
   because so does the sentence above it. */
.ly-bars { display: flex; flex-direction: column; gap: var(--s3); margin-top: var(--s3); }
.ly-bar-row { min-width: 0; }
.ly-bar-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s2); font-size: 13px; margin-bottom: 4px;
}
.ly-bar-lbl { min-width: 0; overflow-wrap: anywhere; font-weight: 600; }
.ly-bar-val { flex: 0 0 auto; color: var(--muted); font-size: 12px; }
.ly-bar { width: 100%; height: 10px; display: block; }
.ly-bar-bg { fill: var(--line); }
.ly-bar-fill { fill: var(--ly-c1); stroke: none; }
.ly-bar-note { margin: 4px 0 0; font-size: 12px; color: var(--muted); }

/* Two answers side by side where there is room, stacked on a phone — the two
   item channels are a COMPARISON and a comparison that scrolls is not one. */
.ly-two { display: grid; grid-template-columns: 1fr; gap: var(--s4); }
@media (min-width: 760px) { .ly-two { grid-template-columns: 1fr 1fr; } }
.ly-col { min-width: 0; }
.ly-inline-filter { margin-bottom: var(--s3); }
.ly-inline-filter .field { flex: 0 1 220px; }

/* THE HEAT MAP — one strip of 24 squares per day, hour 0 on the right. The
   ruler underneath is 24 equal HTML cells in the page's own RTL flow, so it
   lines up with the squares without a second mirroring rule. */
.ly-heat-wrap { display: flex; flex-direction: column; gap: 3px; margin-top: var(--s3); }
.ly-heat-row { display: flex; align-items: center; gap: var(--s2); }
.ly-heat-day { flex: 0 0 56px; font-size: 12px; color: var(--muted); }
.ly-heat { flex: 1 1 auto; height: 14px; min-width: 0; display: block; }
.ly-heat-tot { flex: 0 0 40px; font-size: 12px; color: var(--muted); text-align: start; }
/* FIVE STEPS, and step 0 is EMPTY, not pale — a shop that was shut at 04:00
   must not look like one that served a single cup. Opacity rather than a mixed
   colour so the ramp works in every browser this console has ever run in. */
.ly-heat-c { fill: var(--ly-c1); }
.ly-heat-0 { fill: var(--bg); stroke: var(--line); stroke-width: .5; }
.ly-heat-1 { opacity: .22; }
.ly-heat-2 { opacity: .45; }
.ly-heat-3 { opacity: .70; }
.ly-heat-4 { opacity: 1; }
.ly-heat-ticks { flex: 1 1 auto; display: flex; min-width: 0; }
.ly-heat-ticks > span {
  flex: 1 1 0; min-width: 0; text-align: center;
  font-size: 10px; color: var(--muted); direction: ltr;
}
.ly-peak { margin: 0; font-size: 14px; }

/* The honesty fold: same quiet dashed voice as إعدادات متقدمة. */
.ly-limits ul { margin: 0; padding-inline-start: var(--s4); }
.ly-limits li { font-size: 13px; color: var(--muted); line-height: 1.9; margin-bottom: var(--s2); }

/* ============================================================== */
/* LAPSED = READ-ONLY, NEVER DARK (brand-first-billing §3, §4).    */
/*                                                                */
/* The founder's locked decision: on expiry the OWNER is locked    */
/* and the CUSTOMER is not. So nothing here hides, collapses or    */
/* greys a READ — every number, member, prize and setting stays    */
/* on screen. Only the write controls are visibly unavailable,     */
/* and they say why rather than simply failing.                    */
/* ============================================================== */
.lapsed-banner {
  margin: var(--s3) 0 0; padding: var(--s3);
  border: 1px solid #d9a441; border-radius: var(--r-card);
  background: #fdf3dd; color: #5b3d00;
}
.lapsed-h { margin: 0 0 6px; font-size: 14px; font-weight: 700; }
.lapsed-why { margin: 0 0 var(--s2); font-size: 13.5px; line-height: 1.7; }
/* What it costs, where to write, and what still works — a lapsed screen
   that offers no way forward is a dead end, and this is the way forward.
   There is deliberately NO pay button: Aloke is billed by hand. */
.lapsed-how {
  margin: 0; padding-inline-start: 1.2em;
  display: grid; gap: 6px; font-size: 13px; line-height: 1.7;
}
/* A write control this branch cannot use. Dimmed and un-pressable, never
   hidden: an owner has to see what renewing gives them back. */
.is-locked, button.is-locked, .is-locked:hover {
  opacity: .55; cursor: not-allowed; filter: grayscale(35%);
}

/* ============================================================== */
/* THE EMPLOYEE GRANT (employee-roles-contract §2, §3).            */
/*                                                                */
/* The permission editor is DRAWN FROM THE SERVED CATALOG — these  */
/* rules only dress whatever keys arrived: preset pills over       */
/* grouped checkboxes, one muted help line per group, and the      */
/* manager sentence while «إدارة الموظفين» is in the pick.         */
/* ============================================================== */
.pm-presets { display: flex; flex-wrap: wrap; gap: var(--s2); margin: 0 0 var(--s3); }
/* A preset is a ONE-TAP FILL, and the lit one is a MIRROR of the boxes —
   it lights when the boxes match its set exactly, not because it was the
   last thing pressed. */
.pm-preset {
  min-height: 36px; padding: 6px var(--s4); cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--surface); color: var(--ink); font-size: 13px; font-weight: 600;
}
.pm-preset:hover:not(:disabled) { border-color: var(--accent); }
.pm-preset.on {
  border-color: var(--accent); color: var(--accent);
  background: var(--accent-weak, transparent);
}
.pm-group + .pm-group { margin-top: var(--s4); }
.pm-help { margin: calc(-1 * var(--s1)) 0 var(--s2); font-size: 12px; color: var(--muted); }
/* Two columns where they fit, one at 375 — a column of eighteen boxes is a
   scroll, a grid is a glance. */
.pm-list {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
  gap: 0 var(--s4);
}
.pm-item { margin-bottom: var(--s2); font-size: 13.5px; }
.pm-mgr-note {
  margin: 0 0 var(--s3); padding: var(--s2) var(--s3);
  border-inline-start: 3px solid var(--accent); border-radius: var(--r-btn);
  background: var(--accent-weak, var(--bg)); font-size: 12.5px; line-height: 1.8;
}
/* The roster's edit door, under the table — one editor at a time. */
.pm-edit {
  margin-top: var(--s4); padding: var(--s4);
  border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--surface);
}
.pm-actions { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s3); }
/* THE INVITE ROW CARRIES FOUR FIELDS since the role picker joined it, and at
   the narrowest width two-up squeezed «حسب حساب الموظف» under its own
   chevron by a couple of pixels (measured, not guessed: the language select
   went 157px → 151px against 153px of text). One field per line below 420
   gives every label its full width and keeps giving it as labels grow. */
@media (max-width: 420px) {
  .st-invite .field { flex: 1 1 100%; }
}
/* WHAT THE CHOSEN ROLE ACTUALLY GRANTS, spelled out under the invite row —
   «مدير فرع» on its own tells an owner nothing about what that person can
   do, so the count and the catalog's own permission names sit right there,
   readable without opening the fold. Muted and small: it explains the pick,
   it is not a second control. */
.pm-grants {
  margin: calc(-1 * var(--s2)) 0 var(--s3);
  font-size: 12.5px; line-height: 1.9; color: var(--muted);
}
.pm-grants:empty { display: none; }
/* The compact grant cell keeps its count LTR inside the RTL sentence. */
#staff td [dir="ltr"], .pm-grants [dir="ltr"] { unicode-bidi: isolate; }

/* ===================== THE CRM (the CRM wave) =========================== */
/* The customer profile drawer, the tag chips and the segments builder.
   Tokens only, like the rest of this sheet; every string inside these
   shapes is written with textContent by loyalty.js. */

/* --- the profile drawer: a side sheet at desk width, a bottom sheet on a
   phone. [hidden] beats everything, the pane rule's own lesson. */
.pf-wrap[hidden] { display: none !important; }
.pf-wrap { position: fixed; inset: 0; z-index: 40; }
.pf-scrim { position: absolute; inset: 0; background: rgb(0 0 0 / 0.45); }
.pf-drawer {
  position: absolute; inset-block: 0; inset-inline-start: auto;
  inset-inline-end: 0; width: min(560px, 100vw);
  background: var(--bg); overflow-y: auto; padding: var(--s4);
  box-shadow: 0 0 40px rgb(0 0 0 / 0.25);
}
@media (max-width: 560px) {
  .pf-drawer {
    inset: auto 0 0 0; width: auto; max-height: 90vh;
    border-radius: 16px 16px 0 0; padding: var(--s3);
  }
}
body.pf-lock { overflow: hidden; }
.pf-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s2);
}
.pf-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2); }
.pf-name { font-size: 18px; }
.pf-phone { font-size: 13px; color: var(--muted); unicode-bidi: isolate; }
.pf-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: var(--s2) 0; }
.pf-actions { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s2) 0 var(--s3); }
.pf-stats .stat-num { font-size: 20px; }
.pf-sec { margin-top: var(--s4); }
.pf-sec-h {
  margin: 0 0 var(--s2); font-size: 15px; font-weight: 700;
  border-bottom: 1px solid var(--line); padding-bottom: 6px;
}

/* Rows that open the drawer say so with the cursor and a hover tint. */
.pf-open { cursor: pointer; }
.pf-open:hover { background: var(--surface-2, rgba(0, 0, 0, 0.03)); }
tr.pf-open:hover td { background: var(--surface-2, rgba(0, 0, 0, 0.03)); }

/* --- notes */
.pf-note {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
  border: 1px solid var(--line); border-radius: var(--r-card);
  padding: var(--s2) var(--s3); margin-bottom: var(--s2);
}
.pf-note-body { flex: 1 1 100%; margin: 0; font-size: 14px; line-height: 1.7; }
.pf-note .btn-danger { margin-inline-start: auto; }
.pf-note-add { display: flex; align-items: flex-end; gap: var(--s2); margin-top: var(--s2); }
.pf-note-add textarea { flex: 1 1 auto; }

/* --- tag chips: the DB colour rides only the dot and the border, so an
   unreadable colour can never paint the words. */
.tag-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px; border-radius: 999px; font-size: 12px;
  background: var(--surface, var(--bg)); border: 1.5px solid var(--line);
  color: var(--ink);
}
.tag-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--line); flex: 0 0 auto; }
.tag-x {
  border: 0; background: none; cursor: pointer; font-size: 14px;
  line-height: 1; padding: 0 2px; color: var(--muted);
}
.tag-x:hover { color: var(--danger); }
.pf-tag-pick { margin-top: var(--s2); }
.pf-tag-new { display: flex; align-items: center; gap: var(--s2); margin-top: var(--s2); }
.pf-tag-new input { flex: 1 1 auto; }

/* --- the timeline: one story column, newest first. */
.pf-tl { margin-top: var(--s1); }
.pf-tl-row { padding: 8px 0; border-bottom: 1px solid var(--line); }
.pf-tl-row:last-child { border-bottom: 0; }
.pf-tl-label { font-size: 14px; line-height: 1.7; }
.pf-tl-body { font-size: 13px; color: var(--muted); line-height: 1.7; }
.pf-tl-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
  font-size: 12px; color: var(--muted); margin-top: 2px;
}

/* --- THE RFM MAP — the CRM's own reading, drawn as the grid it actually is.
   `loyalty_crm.rfm_label(r, f)` is pure and total over r,f ∈ 1..5, and it
   partitions the 5×5 into exactly these ten regions. THE `grid-area` RULES
   BELOW *ARE* THAT PARTITION — not ten chips arranged to look like one — and
   tests/frontend/test_loyalty_crm_console.py expands every rectangle here and
   compares all twenty-five cells against the Python function. If this block
   and the engine ever disagree the test goes red, because a map that disagrees
   with the label an owner then filters by is a map that lies.

   Row 1 is the TOP and holds r=5 (here this week); column 1 is the INLINE
   START (the right, in this RTL console) and holds f=1. There is NO monetary
   axis: `m` scores only where a till recorded an amount (contract §5), so it
   is absent in any shop whose staff type none, and an axis that vanishes in
   half the shops is not a thing to draw.

   The ten shades are the console's own two tokens, mixed — warm (the Aloke
   purple) for the recent rows, grey (the muted ink) for the two that went
   quiet. No colour literal enters this sheet, exactly as its header says. */
.rfm {
  --rfm-1: var(--accent);
  --rfm-2: color-mix(in srgb, var(--accent) 82%, #fff);
  --rfm-3: color-mix(in srgb, var(--accent) 52%, #fff);
  --rfm-4: color-mix(in srgb, var(--accent) 69%, #fff);
  --rfm-5: color-mix(in srgb, var(--accent) 38%, #fff);
  --rfm-6: color-mix(in srgb, var(--accent) 25%, #fff);
  --rfm-7: color-mix(in srgb, var(--muted) 44%, #fff);
  --rfm-8: color-mix(in srgb, var(--muted) 33%, #fff);
  --rfm-9: color-mix(in srgb, var(--muted) 25%, #fff);
  --rfm-10: color-mix(in srgb, var(--muted) 18%, #fff);
  display: grid; grid-template-columns: auto 1fr; gap: var(--s2) var(--s3);
  /* A map is a thing you take in at a glance. Stretched across a 1280 console
     the grid would be 1100px wide and taller than the fold, which pushes the
     saved segments — the thing a press lands in — off the screen. */
  max-width: 720px;
}
.rfm-y, .rfm-x { font-size: 12px; font-weight: 600; color: var(--muted); }
.rfm-y {
  grid-area: 1 / 1; display: flex; flex-direction: column;
  justify-content: space-between; align-items: flex-end; padding: 2px 0;
}
.rfm-g {
  grid-area: 1 / 2; display: grid; gap: 4px;
  grid-template-columns: repeat(5, 1fr); grid-template-rows: repeat(5, 1fr);
  aspect-ratio: 5 / 2.6;
}
.rfm-x { grid-area: 2 / 2; display: flex; justify-content: space-between; }
/* A bucket is a BUTTON: it carries the owner into that segment. */
.rfm-c {
  border: 0; border-radius: var(--r-btn); padding: 4px 8px; min-width: 0;
  overflow: hidden; font: inherit; text-align: start; cursor: pointer;
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  background: var(--accent-weak); color: var(--ink);
  transition: transform .12s ease, box-shadow .12s ease;
}
.rfm-c:hover { transform: translateY(-1px); box-shadow: var(--card-shadow); }
.rfm-c:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.rfm-c i { font-style: normal; font-size: 12px; font-weight: 700; line-height: 1.25; }
.rfm-c b { font-size: 13px; font-weight: 700; opacity: .78; }
/* THE EMPTY ROOM IS STILL A ROOM (§ "a bucket with no members must read as
   empty, not as missing"). It keeps its colour, its name and a «٠», loses a
   little weight, and is still pressable — the preview then says so in words. */
.rfm-c.rfm-0 { opacity: .55; }
.rfm-c.on { box-shadow: 0 0 0 2px var(--ink); }
/* THE PARTITION. r is the row (5 = most recent, at the top), f the column.
   Geometry only — an owner on a browser too old for the shades below still
   gets the ten rooms in the right places. */
.rfm-champion { grid-area: 1 / 4 / 3 / 6; }
.rfm-promising { grid-area: 1 / 2 / 3 / 4; }
.rfm-new { grid-area: 1 / 1 / 3 / 2; }
.rfm-loyal { grid-area: 3 / 4 / 4 / 6; }
.rfm-needs_attention { grid-area: 3 / 2 / 4 / 4; }
.rfm-casual { grid-area: 3 / 1 / 4 / 2; }
.rfm-at_risk { grid-area: 4 / 3 / 5 / 6; }
.rfm-slipping { grid-area: 4 / 1 / 5 / 3; }
.rfm-lost_loyal { grid-area: 5 / 3 / 6 / 6; }
.rfm-hibernating { grid-area: 5 / 1 / 6 / 3; }
/* THE HEAT, GUARDED. `color-mix` is how the shades exist without a single
   colour literal — but a browser that cannot compute one makes the whole
   `background` unset, and white text on an unset background is an invisible
   map. So the mixes AND the white they were chosen for live behind the same
   test; without it every square keeps the flat `--accent-weak` and dark ink
   it is declared with above, which is plain but readable. */
@supports (background: color-mix(in srgb, red 50%, #fff)) {
  .rfm-champion { background: var(--rfm-1); color: #fff; }
  .rfm-promising { background: var(--rfm-2); color: #fff; }
  .rfm-new { background: var(--rfm-3); color: #fff; }
  .rfm-loyal { background: var(--rfm-4); color: #fff; }
  .rfm-needs_attention { background: var(--rfm-5); }
  .rfm-casual { background: var(--rfm-6); }
  .rfm-at_risk { background: var(--rfm-7); }
  .rfm-slipping { background: var(--rfm-8); }
  .rfm-lost_loyal { background: var(--rfm-9); }
  .rfm-hibernating { background: var(--rfm-10); }
}
/* The never-visited line: they have no square, so they get a sentence and
   their own door (the cold report's «never»). */
.rfm-foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
  margin: var(--s3) 0 0; font-size: 13px; color: var(--muted);
}
/* AT PHONE WIDTH THE MAP IS STILL THE MAP. Five rows of Arabic names cannot
   shrink past legibility, so the grid grows taller instead of thinner and the
   counts step down a size; nothing is hidden, because a hidden bucket is the
   one an owner needed. */
@media (max-width: 560px) {
  .rfm { gap: var(--s1) var(--s2); }
  .rfm-g { aspect-ratio: 5 / 4.6; gap: 3px; }
  .rfm-c { padding: 3px 6px; }
  .rfm-c i { font-size: 10.5px; }
  .rfm-c b { font-size: 11px; }
  .rfm-y, .rfm-x { font-size: 10.5px; }
}

/* --- segments: seeds, the builder rows, the saved list. */
.sg-seeds { display: flex; flex-wrap: wrap; gap: var(--s2); margin-bottom: var(--s3); }
.sg-seed {
  border: 1.5px dashed var(--line); border-radius: 999px;
  background: var(--bg); padding: 6px 14px; font: inherit; font-size: 13px;
  color: var(--ink); cursor: pointer;
}
.sg-seed:hover { border-color: var(--accent); color: var(--accent-press); }
.sg-row { align-items: flex-start; }
.sg-row-h { flex: 0 0 100%; font-size: 13px; font-weight: 700; color: var(--muted); }
.sg-widget { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s2); flex: 1 1 auto; min-width: 0; }
.sg-check { font-size: 13px; }
.sg-num { flex: 0 1 150px; }
.sg-row .btn-danger { margin-inline-start: auto; }
.sg-item {
  border: 1px solid var(--line); border-radius: var(--r-card);
  padding: var(--s3); margin-bottom: var(--s2);
}
.sg-item-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2); }
.sg-item-words { margin: var(--s1) 0 var(--s2); font-size: 13px; color: var(--muted); line-height: 1.8; }

/* ==================== THE SPEC-DRIVEN CARD FACE (card looks) ============ */
/* docs/loyalty-card-looks-contract.md. Everything positional here is set
   INLINE by the interpreter in loyalty.js from the served `metrics` — this
   stylesheet only carries the things a number cannot say: what a scrim is,
   what a dashed rule looks like, how a mark ghosts when it is not earned.
   If you find a width, a height, a gap or a font-size in this block that the
   server does not publish, that is a drift and it is a bug. */

/* ONE GEOMETRY, MANY BOXES. The face is laid out at 360 — the narrow phone the
   whole spec was drawn at — and scaled into whatever it lands in, so the 375
   console and the 1280 console show the same card at two zooms rather than two
   layouts. The wrapper's height is set in JS because a transform does not
   change layout height. */
.lp-face { position: relative; width: 100%; overflow: hidden; }
.cdf {
  position: absolute; top: 0; inset-inline-start: 0;
  display: flex; flex-direction: column;
  overflow: hidden;
  box-shadow: 0 10px 28px rgb(0 0 0 / 0.22);
  transform: scale(var(--fit, 1));
  transform-origin: top right;
  /* The face draws its own colours; it must never inherit the console's. */
  text-align: start;
}
/* An LTR console (or an English structure) scales from the other corner. */
[dir="ltr"] .cdf { transform-origin: top left; }
.cdf-b { position: relative; flex: 0 0 auto; }

/* --- banner: the cover under a scrim, or a wash of the card's own colour. */
.cdf-banner { overflow: hidden; }
.cdf-banner-img, .cdf-banner-wash { position: absolute; inset: 0; display: block; }
.cdf-banner-img { background-size: cover; background-position: center; }
.cdf-banner-wash {
  /* «a plain wash of the card's own colour when there is none» — a lift at the
     top so the band reads as a head rather than as a mistake. */
  background: linear-gradient(to bottom, rgb(255 255 255 / 0.14), transparent 70%);
}
.cdf-scrim { position: absolute; inset: 0; display: block; }
.cdf-ov { position: absolute; display: flex; flex-direction: column; gap: 6px; }

/* --- identity */
.cdf-id { display: flex; align-items: center; gap: 10px; }
.cdf-logo { flex: 0 0 auto; display: grid; place-items: center; overflow: hidden; }
.cdf-logo img { object-fit: contain; }
.cdf-idt { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.cdf-name {
  font-weight: 700; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cdf-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.cdf-chip {
  border-radius: 999px; padding: 2px 8px; font-weight: 700;
  white-space: nowrap;
}

/* --- balance: the count plate */
.cdf-bal { display: flex; align-items: center; }
.cdf-plate { display: inline-flex; flex-direction: column; align-items: inherit; }
.cdf-bal .cdf-plate { align-items: center; }
.cdf-bal-c { font-weight: 700; letter-spacing: 1px; opacity: 0.74; }
.cdf-bal-n { font-weight: 700; line-height: 1.05; font-variant-numeric: tabular-nums; }

/* --- the marks grid. Row lengths come from mark_rows(); the container is
   forced LTR and each row emitted in reverse, exactly as both apps do, or an
   Arabic card mirrors twice and fills backwards. */
.cdf-marks { display: flex; flex-direction: column; align-items: stretch; }
.cdf-mrow { display: flex; direction: ltr; }
.cdf-mk {
  display: grid; place-items: center; flex: 0 0 auto;
  color: currentColor; opacity: 0.26;
  transition: opacity .18s ease;
}
.cdf-mk.on { opacity: 1; }
.cdf-mk .ic { width: 72%; height: 72%; }
/* mark_shape — how an EMPTY slot is drawn. The earned mark is always the
   drawing itself; the shape is the socket it sits in. */
.cdf-mk--disc, .cdf-mk--square, .cdf-mk--ticket {
  border: 1.5px solid currentColor; opacity: 1;
}
.cdf-mk--disc { border-radius: 50%; }
.cdf-mk--square { border-radius: 22%; }
.cdf-mk--ticket { border-radius: 22%; border-style: dashed; }
.cdf-mk--disc .ic, .cdf-mk--square .ic, .cdf-mk--ticket .ic { opacity: 0.26; }
.cdf-mk--disc.on, .cdf-mk--square.on, .cdf-mk--ticket.on { border-style: solid; }
.cdf-mk--disc.on .ic, .cdf-mk--square.on .ic, .cdf-mk--ticket.on .ic { opacity: 1; }
.cdf-mk-logo { overflow: hidden; opacity: 1; }
.cdf-mk-logo img {
  max-width: 76%; max-height: 76%; object-fit: contain;
  opacity: 0.32; filter: grayscale(1);
}
.cdf-mk-logo.on img { opacity: 1; filter: none; }

/* --- progress */
.cdf-prog { display: flex; align-items: center; }
.cdf-bar { display: block; width: 100%; overflow: hidden; }
.cdf-bar > span { display: block; height: 100%; border-radius: inherit; }

/* --- the two counters */
.cdf-stats { display: flex; align-items: flex-start; gap: 10px; }
.cdf-stat { display: flex; flex-direction: column; gap: 1px; opacity: 0.92; }
.cdf-stat b { font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }

/* --- the footer line */
.cdf-prize { display: flex; align-items: center; }
/* INLINE-BLOCK, NOT FLEX, and the reason is worth the line: «باقي 3 على قهوة»
   is ONE sentence with an LTR-isolated number inside it. In a flex container
   each text run becomes an anonymous flex item and its leading/trailing
   whitespace is TRIMMED — the browser drew «باقي3على قهوة» with the spaces
   gone. A sentence is inline flow, never a layout. */
.cdf-prize .cdf-plate { display: inline-block; line-height: 1.6; opacity: 0.95; }

/* --- rules. `hair` is a line, `dash` a dashed rule, `notch` a perforation:
   the two circles are pushed out by the card's own padding so the tear reads
   at the edge, which is the only place a ticket tears. */
.cdf-rule { background: currentColor; opacity: 0.28; }
.cdf-rule--dash {
  background: repeating-linear-gradient(to right,
    currentColor 0 var(--dash), transparent var(--dash)
    calc(var(--dash) + var(--dashgap)));
}
/* The perforation's own line is the ::before, so the two punched circles can
   stay OPAQUE — a translucent notch reads as a smudge, not as a tear. */
.cdf-rule--notch { background: none; opacity: 1; overflow: visible; }
.cdf-rule--notch::before {
  content: ""; position: absolute; inset: 0;
  background: currentColor; opacity: 0.28;
}
.cdf-rule--notch > i {
  position: absolute; top: 50%;
  width: calc(var(--notch) * 2); height: calc(var(--notch) * 2);
  border-radius: 50%; background: var(--surface, #fff);
  transform: translateY(-50%);
}
.cdf-rule--notch > i:first-child { inset-inline-start: calc((var(--notch) + var(--notch-out)) * -1); }
.cdf-rule--notch > i:last-child { inset-inline-end: calc((var(--notch) + var(--notch-out)) * -1); }

/* --- rows: the ONLY side-by-side the spec allows. */
.cdf-row { display: flex; align-items: center; }

/* ===================== THE STRUCTURE GALLERY (the picker) =============== */
/* Twenty real miniatures, each rendered from the served spec by the same
   interpreter the preview uses — never a thumbnail image, so a refinement on
   the server updates this gallery with no console release and no artwork. */
.lp-looks { margin-top: var(--s4); }
.lp-looks-h {
  font-size: 15px; font-weight: 700; margin: 0 0 var(--s1);
}
.lp-cs-grid {
  display: grid; gap: var(--s3);
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  margin: var(--s3) 0;
}
.lp-cs-grid--sm { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
.lp-cs {
  display: flex; flex-direction: column; gap: 2px; text-align: center;
  border: 2px solid var(--line); border-radius: var(--r-card);
  background: var(--surface); padding: var(--s2); font: inherit;
  color: var(--ink); cursor: pointer;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.lp-cs:hover { border-color: var(--accent); }
.lp-cs.on { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }
/* The stage crops nothing: fitFaces sets the wrapper's height to the scaled
   height of the card inside it, so a tall structure is a tall tile. */
.lp-cs-stage { display: block; width: 100%; margin-bottom: var(--s2); }
.lp-cs-ar { font-size: 13px; font-weight: 700; }
.lp-cs-en { font-size: 11px; color: var(--muted); }

/* --- the three tokens a structure leaves to the owner. */
.lp-tok { display: flex; flex-direction: column; gap: var(--s2); }
.lp-tokrow { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); }
.lp-tok-l { font-size: 13px; font-weight: 700; min-width: 110px; }
.lp-tok-en { display: block; font-size: 11px; font-weight: 400; color: var(--muted); }
.lp-tok-b { display: flex; flex-wrap: wrap; gap: 6px; }
.lp-tokv {
  border: 1.5px solid var(--line); border-radius: 999px; background: var(--bg);
  padding: 5px 14px; font: inherit; font-size: 13px; color: var(--ink);
  cursor: pointer;
}
.lp-tokv:hover { border-color: var(--accent); }
.lp-tokv.on { border-color: var(--accent); background: var(--accent-weak); font-weight: 700; }
.lp-cs-clear { margin-top: var(--s3); }

/* --- the per-tier fold: the same gallery, under the level it dresses. */
.lp-cs-tier { flex: 0 0 100%; margin-top: var(--s2); }
.lp-cs-tier > summary {
  display: flex; align-items: center; gap: var(--s2);
  cursor: pointer; font-size: 13px; font-weight: 700; color: var(--muted);
  padding: 6px 0; list-style: none;
}
.lp-cs-tier > summary::-webkit-details-marker { display: none; }
.lp-cs-tier[open] > summary .ic { transform: rotate(180deg); }
.lp-cs-tier-state {
  border: 1px solid var(--line); border-radius: 999px;
  padding: 1px 9px; font-size: 11px; font-weight: 700; color: var(--muted);
}
.lp-cs-tier-state.on { border-color: var(--accent); color: var(--accent-press); }
.lp-cs-tier-b {
  border: 1px solid var(--line); border-radius: var(--r-card);
  padding: var(--s3); margin-bottom: var(--s2);
}
.lp-cs-tier-prev { max-width: 300px; margin: var(--s3) 0; }

/* Reduce motion: the face and its picker carry no animation an owner asked
   for — the two eases above are polish, and polish is the first thing to go. */
@media (prefers-reduced-motion: reduce) {
  .cdf, .cdf-mk, .lp-cs { transition: none; }
}

@media (max-width: 480px) {
  /* 375-first: two tiles a row still reads, and the label under each stays on
     one line. */
  .lp-cs-grid, .lp-cs-grid--sm { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2); }
  .lp-tok-l { min-width: 100%; }
}

/* ===================== v1.7 — the engraving («النقش») ==================== */

/* THE PATTERN PICKER. Real swatches drawn from the served art, so the owner
   chooses a drawing rather than a word. The tile paints the card's own ground
   with the pattern over it — at a boosted alpha (see CD_PAT_SWATCH_BOOST):
   the card wears it at ~9%, which is invisible in a 64px square, while the
   preview card above always uses the served number and stays honest. */
.lp-pt-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: var(--s2); margin-bottom: var(--s4);
}
.lp-pt-grid--sm { grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); }
.lp-pt {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: var(--s2) 4px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-card); color: var(--muted);
  font-size: 11px; font-weight: 600; line-height: 1.3; text-align: center;
  overflow: hidden;
}
.lp-pt:hover:not(.on) { border-color: var(--accent); }
.lp-pt.on {
  border-color: var(--accent); background: var(--accent-weak);
  color: var(--accent-press);
}
.lp-pt-sw {
  width: 100%; height: 42px; border-radius: 8px;
  border: 1px solid rgba(0, 0, 0, .08);
}
.lp-pt-l {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* A NO-BACKGROUND LOGO IS A STENCIL ON THE STRUCTURED FACE TOO. The legacy
   preview (`.cdp-silo`) and both apps have drawn it this way since the
   logo-stamp wave; this is the same rule for the twenty structured
   arrangements, in the face's own ink rather than the preview panel's
   variable — a structured card can be any colour. */
.cdf-mk-silo { position: relative; opacity: 1; background: none; }
.cdf-mk-silo::after {
  content: ""; position: absolute; inset: 1px;
  background: currentColor;
  -webkit-mask: var(--silo-url) center / contain no-repeat;
  mask: var(--silo-url) center / contain no-repeat;
  opacity: 0.3;
}
.cdf-mk-silo.on::after { opacity: 1; }

/* ====================== THE OWNER'S OWN STAMP ARTWORK ==================== */
/* «صورتك» — two uploaded PNGs, PAINTED AS-IS.
   No opacity, no grayscale, NO BORDER and no disc, and every one of those is
   a deliberate absence: `.stamp-logo` above holds an unearned logo back to a
   grayscale ghost because that mark is ONE picture doing two jobs. A custom
   pair is TWO pictures, and the owner already drew the difference — dimming
   their «empty» artwork would be inventing a state on top of the one they
   made. The rule the whole feature turns on. */
.stamp-art {
  border: none; opacity: 1; overflow: hidden; padding: 2px;
  background: transparent;
}
.stamp-art.on { border: none; background: transparent; }
.stamp-art img {
  width: 100%; height: 100%; object-fit: contain; display: block;
}

.sc-wrap { margin: var(--s3) 0; }
.sc-slots { display: flex; gap: var(--s3); flex-wrap: wrap; }
.sc-slot {
  display: flex; flex-direction: column; align-items: center; gap: var(--s2);
  padding: var(--s3); border: 1px solid var(--line); border-radius: var(--r-md);
  min-width: 128px;
}
.sc-slot b { font-size: 12px; color: var(--muted); }
/* A CHEQUERBOARD, so a transparent PNG reads as transparent. On a flat panel
   an owner cannot tell a cut-out mark from one with a white background — and
   a white background is exactly the mistake this upload refuses. */
.sc-drop {
  width: 84px; height: 84px; border-radius: var(--r-sm);
  display: flex; align-items: center; justify-content: center;
  background-color: #fff;
  background-image:
    linear-gradient(45deg, #e6e6ea 25%, transparent 25%, transparent 75%, #e6e6ea 75%),
    linear-gradient(45deg, #e6e6ea 25%, transparent 25%, transparent 75%, #e6e6ea 75%);
  background-size: 16px 16px;
  background-position: 0 0, 8px 8px;
}
.sc-drop img { max-width: 100%; max-height: 100%; object-fit: contain; }
.sc-drop .ic { width: 26px; height: 26px; color: var(--muted); }
.sc-pick { cursor: pointer; }
.sc-note { margin: var(--s3) 0 0; font-size: 12px; }
.sc-note.ok { color: var(--ok, var(--accent-press)); }
.sc-note.warn { color: var(--warn, #B45309); }

/* ======================== THE APPLE WALLET CARD ========================= */
/* APPLE'S OWN STACKING, because the pass is stacked that way: one FLAT
   `backgroundColor` (Wallet has no gradient and no pattern property), the
   strip composited over it edge to edge, the count in a HEADER field beside
   the logo, and the field rows below.
   `primaryFields` is deliberately absent — Wallet paints it ON TOP of the
   strip, which is what once put «10 / 12» across the first three marks. */
.wp-frame { max-width: 380px; }
.wp-card {
  border-radius: 12px; overflow: hidden;
  box-shadow: 0 1px 3px rgb(0 0 0 / .18), 0 8px 24px rgb(0 0 0 / .12);
}
.wp-head {
  display: flex; align-items: center; gap: var(--s2);
  padding: 10px 12px 8px;
}
.wp-logo { height: 26px; width: auto; max-width: 120px; object-fit: contain; }
.wp-org { font-size: 12px; font-weight: 700; opacity: .92; }
/* The header count sits at the FAR END of the row — Wallet's own placement,
   and in an RTL pass that is the left. `margin-inline-start:auto` says that
   once for both directions. */
.wp-count {
  margin-inline-start: auto; text-align: center; line-height: 1.1;
}
.wp-count b { font-size: 15px; display: block; }
/* The real `strip@3x.png`, 1125x369, shown at the pass's own 375x123 aspect.
   `display:block` or the inline-image baseline opens a gap under it that the
   pass does not have. */
.wp-strip { display: block; width: 100%; height: auto; aspect-ratio: 375 / 123; }
.wp-strip-none { background: rgb(255 255 255 / .06); }
.wp-fields {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  padding: 10px 12px 12px;
}
.wp-field { min-width: 90px; }
.wp-flabel {
  display: block; font-size: 10px; letter-spacing: .04em;
  text-transform: uppercase; opacity: .72;
}
.wp-fvalue { font-size: 14px; }
.wp-empty {
  padding: var(--s4); border: 1px dashed var(--line); border-radius: var(--r-md);
  color: var(--muted); font-size: 12px; text-align: center;
}

/* THE WALLET'S OWN COLOURWAY — the same colour cards the card-design panel
   uses (`.lp-cd-colors` grid), plus the one thing this panel has that that one
   does not: a visible INHERITING state. A colour that is off is not "empty",
   it is following the card design, and the row says so with the actual colour
   it is following beside it. */
.wp-colors { margin-top: var(--s4); }
.wp-ctitle { margin: 0 0 var(--s1); font-size: 14px; font-weight: 700; }
/* The same colour CARD the design panel above uses — written out rather than
   borrowed, because `.lp-cd-color` is a SELECTOR `wireCardDesign` walks to
   decide what to write onto the card design. A Wallet row answering to it
   would be a Wallet control editing the phone's card. */
.wp-color {
  max-width: none; display: flex; flex-direction: column; gap: 4px;
  padding: var(--s2); background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r-card);
}
.wp-color > .lp-check { font-size: 12px; font-weight: 600; margin-bottom: 2px; }
.wp-color .wc-col {
  width: 100%; height: 30px; padding: 2px; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r-btn);
  background: var(--surface);
}
.wp-color .wc-col:disabled { opacity: .45; cursor: not-allowed; }
.wp-inherit {
  display: flex; align-items: center; gap: 6px;
  font-size: 11px; color: var(--muted);
}
/* The swatch of the colour being inherited RIGHT NOW, painted from the render
   the preview above came from.

   THE RING IS DARK, NOT `--line`. Half the palettes in this product inherit
   `#FFFFFF` for the ink and the filled mark, and against a pale hairline a
   white swatch read as an empty box. */
.wp-isw {
  width: 14px; height: 14px; border-radius: 4px; flex: none;
  position: relative; background: transparent;
  border: 1px solid rgb(0 0 0 / .32);
}
/* NOTHING TO INHERIT — an undesigned card resolves no colour for this role at
   all, and the pass falls back to the renderer's own charcoal. Found by
   LOOKING: a chequerboard at 14px was invisible, so "inheriting white" and
   "inheriting nothing" were the same square on screen. A struck-through box is
   the one no-colour affordance that reads at this size. */
.wp-isw-none { background: var(--surface); }
.wp-isw-none::after {
  content: ""; position: absolute; inset: -1px;
  background: linear-gradient(to top left, transparent calc(50% - 1px),
              rgb(0 0 0 / .45) calc(50% - 1px), rgb(0 0 0 / .45) calc(50% + 1px),
              transparent calc(50% + 1px));
}
/* Only ONE of the two can be true at a time: a role is either painted by the
   swatch above it or inherited from the card. Showing both at once was read,
   in the first pass, as "the colour is the inherited one" — which it is not. */
.wp-color .wp-inherit { display: none; }
.wp-color.wp-inheriting .wp-inherit { display: flex; }
.wp-color.wp-inheriting > .wc-col { display: none; }
.wp-paints { font-size: 10px; line-height: 1.35; }
/* THE ONE WASH A PASS CAN CARRY, on the stamp band. Same controls as the card
   panel's gradients, and the same signal that a wash owns the surface: the
   flat swatch stays visible (it is what the band falls back to) but says
   plainly that it is not what paints it. */
.wp-grad {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1);
  margin-top: 2px;
}
.wp-gradded > .wc-col { opacity: .45; }
/* THE PICTURE BEHIND THE STAMPS. The drop is the strip's own 3.05:1 band, not
   a square: an owner has to SEE how wide and shallow the canvas is before they
   pick a photo, or they choose a portrait and meet the crop afterwards. */
.wp-photo { margin-top: var(--s4); }
/* `height: auto` is LOAD-BEARING: `.sc-drop` ships a square 84x84, and a box
   with both a width and a height ignores `aspect-ratio` entirely — the drop
   rendered as a tall rectangle and told the owner nothing about the shallow
   3.05:1 band their photograph is actually going into. */
.wp-photo .sc-drop {
  width: 100%; max-width: 320px; height: auto; aspect-ratio: 1125 / 369;
}
.wp-slot { align-items: flex-start; }
/* No picture of its own, nothing to wash: the switch is still there (so it is
   not a control that appears and disappears) but says plainly it has no
   effect. On a level that is inheriting, the scrim is inherited too. */
.wp-nophoto .wp-scrim { opacity: .45; pointer-events: none; }
/* A picture this level is BORROWING from the shop's card — shown, because
   «use the main card's image» must let the owner SEE what they are getting,
   and marked, because it is not theirs to delete from here. */
.sc-drop.wp-borrowed { opacity: .8; border-style: dashed; }

/* WHERE THIS LEVEL'S BACKDROP COMES FROM — two options, one visibly on.
   An empty upload box and "inheriting the shop's image" looked identical,
   and the owner could not tell which state they were in. */
.wp-src { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s2) 0; }
/* `display: block` overrides the `inline-flex` every button in this console
   gets: as flex items the title and its one-line explanation sat SIDE BY SIDE
   and the two options read as four interleaved fragments. */
.wp-srcopt {
  display: block; flex: 1 1 190px; text-align: start; cursor: pointer;
  padding: var(--s2) var(--s3); background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r-card);
}
.wp-srcopt b { display: block; font-size: 13px; }
.wp-srcopt .hint { display: block; margin: 2px 0 0; }
.wp-srcopt.on {
  border-color: var(--accent); background: var(--accent-weak);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* THE LEVEL SWITCHER. The Wallet card is per level now, so the preview and
   the panel under it are always showing ONE level — and which one has to be
   unmissable, or an owner edits platinum thinking they are editing the shop. */
.wp-tiers { display: flex; flex-wrap: wrap; gap: var(--s1); margin-bottom: var(--s2); }
.wp-tier {
  cursor: pointer; font-size: 12px; font-weight: 600;
  padding: 4px var(--s3); border-radius: var(--r-pill);
  background: var(--bg); border: 1px solid var(--line); color: var(--muted);
}
.wp-tier.on {
  background: var(--accent-weak); border-color: var(--accent);
  color: var(--accent-press);
}
/* One warning, one line. Three of them run together as a paragraph read as a
   single long complaint, and the owner could not tell how many things were
   actually wrong. */
.wp-warnline { display: block; }
.wp-warnline + .wp-warnline { margin-top: 4px; }

/* The owner's own artwork on the two CARD FACES — the legacy preview and the
   structured one. Same absences as `.stamp-art` above and for the same
   reason: no plate, no ghosting, no grayscale. The card face's own `.on`
   opacity rules must not reach these either, or an unearned slot would fade
   artwork the owner drew as the unearned state. */
.cdp-mark-art, .cdf-mk-art {
  background: none; border: none; opacity: 1; overflow: hidden; padding: 2px;
}
.cdp-mark-art img, .cdf-mk-art img {
  width: 100%; height: 100%; object-fit: contain; display: block;
}

/* MANY OPTIONS ON ONE RUNG. A dish can now be offered in several of its own
   variants at the same prize tier, so the single <select> became a row of
   checkboxes. Laid out as a wrapping strip under the dish it belongs to —
   these are a property OF that dish, never a second list beside it. */
.rw-opts {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px;
  width: 100%; margin-top: 6px; padding-inline-start: 26px;
}
.rw-opts-l { font-size: 12px; font-weight: 700; color: var(--muted); }
.rw-optck-l {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 13px; padding: 2px 8px; border-radius: 999px;
  background: var(--bg); border: 1px solid var(--hair); cursor: pointer;
}
.rw-optck-l.gone { opacity: .7; border-style: dashed; }
.rw-opts-hint { font-size: 11.5px; color: var(--muted); }
/* The full price beside an option — the figure a diner would pay for that
   exact way of having the dish, so an owner pricing a prize is reading the
   same number their menu shows. */
.rw-optck-p {
  font-size: 12px; font-weight: 700; color: var(--muted);
  margin-inline-start: 2px; white-space: nowrap;
}

/* ══ الألعاب والشارات — THE GAME PANEL ═════════════════════════════════════
   docs/games-contract.md §2. Everything here is built out of the console's
   own tokens and its existing shapes (.lp-row, .chip, .empty, table.data,
   .rw-choice-panel for the menu picker) — a game panel that invented its own
   look would read as a bolted-on product rather than a tab of this one.

   The whole file is written for a phone behind a counter first: every grid
   below is `auto-fill`, every card wraps, and nothing has a fixed width. */

/* A GROUP HEADING NEEDS AIR ABOVE IT. `.lp-grp-h` carries a top margin only
   inside the advanced fold, where it was born; on this pane every heading
   follows a row of controls, and without one «اللعبة نفسها» reads as the last
   field's hint rather than as the title of what comes next. */
[data-pane="games"] .lp-row + .lp-grp-h,
[data-pane="games"] .gm-note + .lp-grp-h,
[data-pane="games"] .gm-games + .lp-grp-h,
[data-pane="games"] .gm-slots + .lp-grp-h,
[data-pane="games"] .gm-desc + .lp-grp-h,
[data-pane="games"] .gm-opts + .lp-grp-h,
[data-pane="games"] .gm-sum + .lp-grp-h,
[data-pane="games"] .hint + .lp-grp-h,
[data-pane="games"] .lp-actions + .lp-grp-h,
[data-pane="games"] .lp-rows + .lp-grp-h { margin-top: var(--s5); }
/* The line under the settings row that explains the friend stake in full. It
   is a sentence about the whole row, not a hint on one field — a four-line
   hint inside a `.field` lifts its box and breaks the row's bottom edge. */
.gm-note { margin: 0 0 var(--s2); max-width: 70ch; }

/* THE ROWS ON THIS PANE ALIGN AT THE TOP, and that is a fix, not a taste.
   `.lp-row` bottom-aligns its fields, which is right when every field carries
   the same amount of hint under it — and wrong here, because the hints are
   the SERVER'S (`desc_ar` on the chosen play rate) and change length with the
   choice. Bottom-aligned, one long hint drops its own box and the row's boxes
   stop lining up; the founder would read that as a broken form. Top-aligned,
   the labels and the boxes line up always and the hints simply hang at
   whatever length they are. The one standalone button in a row — a slot's
   «حذف» — keeps the bottom edge, where it sits beside the boxes it deletes. */
[data-pane="games"] .lp-row { align-items: flex-start; }
[data-pane="games"] .lp-row > button { align-self: flex-end; }

/* The arcade picker. Tiles rather than a <select>, for the stamp picker's own
   reason: the owner is choosing a THING their customer will see, and four
   Arabic nouns in a dropdown make them save and reload to find out what they
   picked. */
.gm-games {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--s2); margin-bottom: var(--s4);
}
.gm-pick {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  min-height: 56px; padding: var(--s2) var(--s3); text-align: start;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-card); color: var(--muted); cursor: pointer;
}
.gm-pick b { font-size: 14px; color: var(--ink); font-weight: 700; }
.gm-pick span { font-size: 11.5px; }
.gm-pick:hover:not(.on) { border-color: var(--accent); }
.gm-pick.on {
  border-color: var(--accent); background: var(--accent-weak); color: var(--accent-press);
}
.gm-pick.on b { color: var(--accent-press); }

/* THE WIN CHANCE READS AS A SENTENCE, not as a number to do arithmetic on.
   The slider is full width because it is the one control on this pane an
   owner drags rather than types. */
.gm-chance { margin-bottom: var(--s4); }
.gm-chance input[type="range"] {
  display: block; width: 100%; max-width: 420px; height: 28px;
  accent-color: var(--accent);
}
.gm-chance-read { margin: var(--s1) 0 0; font-size: 13px; color: var(--muted); }
.gm-chance-read b { color: var(--accent-press); }

/* ══ THE OWNER'S ECONOMY (docs/games-contract.md §2, the four served
   pickers) ═══════════════════════════════════════════════════════════════
   Nothing new is invented below: the tile is `.gm-pick`, the sentence under
   it is `.hint`, the state word is `.chip`. A settings block that arrived
   with its own look would read as somebody else's feature bolted onto this
   pane, which is exactly what it is not — it is the same one PUT. */

/* THE TWO PACES, SIDE BY SIDE, AND THE ADJACENCY IS THE HONESTY. A shop runs
   a luck game and a rack of skill games at once; «حظ الفوز» moves only the
   first and «قوة الجهاز» only the second. On separate screens an owner would
   reasonably read one as the other's fine print — so they share a row, each
   with the chip that says which game it steers. `auto-fit` stacks them on a
   phone, where «فوق/تحت» still reads correctly. */
.gm-pace {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
  gap: var(--s4); margin-bottom: var(--s4);
}
.gm-pace .gm-chance { margin-bottom: 0; }
/* A label for a GROUP of tiles, not for one input — so it is a <p> with the
   field label's weight, and it carries its chip on the same line. */
.gm-lbl {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  margin: 0 0 var(--s1);
  font-size: 13px; font-weight: 600; color: var(--ink);
}
.gm-lbl label { font: inherit; color: inherit; margin: 0; }
[data-pane="games"] .gm-desc + .gm-lbl,
[data-pane="games"] .gm-opts + .gm-lbl { margin-top: var(--s4); }

/* The four pickers. `auto-FIT`, not `auto-fill`, and capped — and both halves
   are a fix made by looking. Auto-fill keeps the empty tracks, so a two-option
   setting drew two small tiles marooned against 680px of nothing and read as a
   row that had failed to finish loading. Auto-fit collapses them, and the cap
   stops the other extreme: two tiles stretched across the whole console for
   four words each. The 112px minimum is measured too — it is the largest floor
   that still fits all four difficulties across the half-width column they
   share with the win-chance slider. */
.gm-opts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
  gap: var(--s2); max-width: 520px;
}
/* «WHAT HAPPENS IF I NEVER TOUCH THIS» is the first question anybody asks of
   a setting, and the server answers it — `economy.defaults`. */
.gm-dflt {
  align-self: flex-start; margin-top: 2px; padding: 0 6px;
  font-size: 10.5px; font-weight: 700; line-height: 1.7;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--bg); color: var(--muted);
}
.gm-pick.on .gm-dflt { background: var(--surface); color: var(--accent-press); }
/* The SERVER'S own sentence for the chosen option, printed under the row. */
.gm-desc { margin: var(--s2) 0 0; max-width: 70ch; }

/* THE SKILL GAMES. Wider tiles than the settings, because each carries the
   line that says what the game IS — an owner picking «مانكالا» for their
   counter has to know what they just offered. */
.gm-arena {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--s2); margin-bottom: var(--s2);
}
.gm-arena .gm-pick { min-height: 84px; }
/* NOTHING PICKED IS «ALL», NOT «NONE» — the server's rule, drawn. The tiles
   are outlined rather than filled: they are included because the owner has
   not narrowed the list, which is a different fact from «I chose this one»
   and must not wear the same paint. */
.gm-arena.all .gm-pick {
  border-style: dashed; border-color: var(--accent); color: var(--accent-press);
}
.gm-arena.all .gm-pick b { color: var(--accent-press); }
/* Switched off, but still configurable — the platform-pause philosophy: an
   owner may get ready for something that is not running yet. */
.gm-arena.is-off { opacity: .6; }
.gm-arena-blurb { font-size: 11.5px; line-height: 1.45; }
.gm-arena-min { font-size: 10.5px; opacity: .8; }
.gm-sum { margin: 0 0 var(--s2); font-size: 13px; color: var(--muted); max-width: 70ch; }
.gm-sum b { color: var(--accent-press); }

/* The refill mode, printed beside the pools' own «جولة جديدة» buttons. A
   strip rather than a card: it is an answer and a way out, not a thing with
   parts. */
.gm-refill-note {
  display: flex; align-items: center; gap: var(--s2) var(--s3);
  flex-wrap: wrap; margin-bottom: var(--s3); padding: var(--s2) var(--s3);
  border: 1px dashed var(--line); border-radius: var(--r-card);
  background: var(--surface);
}
.gm-refill-note:empty { display: none; }
.gm-refill-t {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  margin: 0; font-size: 13px; color: var(--muted);
}
.gm-refill-t b { color: var(--ink); }
.gm-refill-note .hint { flex: 1 1 240px; margin: 0; }
.gm-refill-note button { margin-inline-start: auto; }

/* A POOL IS A CARD, because it is a thing with parts — a title, a trigger, a
   round, and the slots it still holds. A row would flatten all four into one
   line the owner has to decode. */
.gm-pool, .gm-edit {
  border: 1px solid var(--line); border-radius: var(--r-card);
  padding: var(--s3); margin-bottom: var(--s3); background: var(--surface);
}
/* A retired pool is DIMMED, never hidden: its prizes are still on somebody's
   card, and an owner has to be able to find the thing they switched off. */
.gm-pool.off { opacity: .68; }
.gm-edit { border-color: var(--accent); }
.gm-pool-h {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
  margin-bottom: var(--s2);
}
.gm-pool-h b { font-size: 15px; }
/* The streak, as the sentence the owner checks: «٣ زيارات هذا الأسبوع ←
   الجائزة». Boxed so it reads as a rule rather than as prose. */
.gm-streak {
  margin: 0 0 var(--s2); padding: var(--s2) var(--s3);
  background: var(--accent-weak); color: var(--accent-press);
  border-radius: var(--r-btn); font-size: 14px; font-weight: 600;
}
.gm-slots { list-style: none; margin: 0 0 var(--s2); padding: 0; }
.gm-slots li {
  display: flex; align-items: center; gap: var(--s2);
  padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 13px;
}
.gm-slots li:last-child { border-bottom: 0; }
.gm-slot-t { flex: 1 1 auto; min-width: 0; }
.gm-slot-n {
  flex: 0 0 auto; font-size: 12px; font-weight: 700; color: var(--muted);
  white-space: nowrap;
}
/* A slot with a win on it is not the owner's to edit — dashed, so the lock is
   visible before they reach for it rather than after a refusal. */
.gm-slots li.held { opacity: .78; border-bottom-style: dashed; }
/* The menu picker is a FIELD control standing beside three boxes, so it has
   to be as tall as they are — `.lp-row` bottom-aligns, and a short button in
   that line reads as a misprint. */
.gm-itembtn {
  width: 100%; min-height: 42px; justify-content: flex-start;
  font-size: 14px; font-weight: 500; text-align: start;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- the badges -------------------------------------------------------- */
/* THE DISC IS THE BADGE'S FRAME EVERYWHERE — the share spec's `disc`, the
   card row's circle, this picker. Drawing it here in the tier's own colour is
   what makes the console preview and the earned emblem the same object. */
.gm-disc {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; flex: 0 0 auto; border-radius: 50%;
  color: var(--tier, var(--accent));
  border: 2px solid currentColor; background: var(--bg);
}
.gm-art { width: 22px; height: 22px; display: block; object-fit: contain; }
/* ارعَ جائزة في ألوكي — a shop's own offers.
   The rows read like the badge list beside them on purpose: this is a small
   standing list an owner scans, not a form they work in. The accent bar marks
   the one thing on this page that leaves the shop's own card and lands in
   Aloke's national pool. */
.gm-spons { display: flex; flex-direction: column; gap: var(--s2); }
.gm-spon {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s2) var(--s3);
  border: 1px solid var(--line); border-inline-start: 3px solid var(--accent);
  border-radius: var(--r-card); padding: var(--s3);
}
.gm-spon-t { flex: 1 1 220px; min-width: 0; display: flex;
             flex-direction: column; gap: 2px; }
.gm-spon-t b { font-size: 14px; }
.gm-spon-t .hint { margin: 0; }
.gm-spon .btn-sm { margin-inline-start: auto; }
/* «رعاية جديدة» must not sit flush against the last offer — the list above is
   a record and the row below is a form, and an owner has to see where one
   stops. */
#gmSponsorSection .lp-grp-h { margin-top: var(--s4); }
/* «راحت لـ ٢» — the half of the rule an owner must see BEFORE they press
   withdraw, so it is coloured like a fact and not like a warning. */
/* …and it never breaks across two lines. At 375px «راحت لـ ٢» wrapped with
   the count alone on the next row, which reads as a stray number under a
   sentence rather than as the fact it is. */
.gm-spon-won { color: var(--accent-press); font-weight: 700;
               white-space: nowrap; }
.gm-spon-until { color: var(--muted); }
.gm-spon-none { max-width: 60ch; }
.gm-sponsor-hint {
  margin: var(--s2) 0 0; max-width: 70ch;
  font-size: 12.5px; line-height: 1.6; color: var(--muted);
}

.gm-badges { display: flex; flex-direction: column; gap: var(--s2); }
.gm-badge {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s3);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--line); border-radius: var(--r-card);
}
.gm-badge-t { flex: 1 1 200px; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.gm-badge-t b { font-size: 14px; }
.gm-badge-t .hint { margin: 0; }
.gm-badge-chips { display: flex; flex-wrap: wrap; gap: var(--s1); }
.gm-badge .btn-danger { margin-inline-start: auto; }

/* The catalogue. Small tiles, because there are dozens of them and the owner
   is scanning drawings — the name under each is a caption, not the label. */
.gm-cat {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: var(--s2); margin-bottom: var(--s4);
}
.gm-cat-tile {
  display: flex; flex-direction: column; align-items: center; gap: var(--s1);
  min-height: 76px; padding: var(--s2) var(--s1);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-card); color: var(--muted);
  font-size: 11px; font-weight: 600; line-height: 1.3; text-align: center;
  overflow: hidden; cursor: pointer;
}
.gm-cat-tile .gm-art { width: 26px; height: 26px; color: var(--ink); }
.gm-cat-tile span {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gm-cat-tile:hover:not(.on) { border-color: var(--accent); }
.gm-cat-tile.on {
  border-color: var(--accent); background: var(--accent-weak); color: var(--accent-press);
}
.gm-cat-tile.on .gm-art { color: var(--accent-press); }

.gm-up {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s3);
  margin-bottom: var(--s4);
}
.gm-up-prev { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.gm-up-prev .hint { margin: 0; }

.gm-tiers { display: flex; flex-wrap: wrap; gap: var(--s2); margin-bottom: var(--s4); }
.gm-tier {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: 6px var(--s3); font-size: 13px; font-weight: 600;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-pill); color: var(--muted); cursor: pointer;
}
.gm-tier-sw {
  width: 14px; height: 14px; border-radius: 50%; flex: 0 0 auto;
  border: 1px solid rgba(0, 0, 0, .12);
}
.gm-tier:hover:not(.on) { border-color: var(--accent); }
.gm-tier.on {
  border-color: var(--accent); background: var(--accent-weak); color: var(--accent-press);
}

/* THE MEMBER ROSTER'S TOOLBAR ------------------------------------------------ */
/* Search, order, page size — the three controls a list of four thousand people
   needs before it is usable at all. They sit ABOVE the count deliberately: the
   owner reads «يعرض ١–٥٠ من ١٬٢٨٤» as the answer to what they just typed, and a
   count printed above its own controls reads as a fact about the shop instead.

   Everything wraps rather than squeezes, the blacklist rows' own discipline —
   a shop is run from a phone at a till, not a desk. */
.rs-bar {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--s2) var(--s3); margin-bottom: var(--s3);
}
/* The search box takes the room the two selects do not, down to a floor that
   still fits a name and a number. Below that everything stacks. */
.rs-bar .rs-search { flex: 1 1 220px; min-width: 180px; }
.rs-bar .rs-sort { flex: 0 1 240px; }
.rs-bar .rs-size { flex: 0 0 96px; }
.rs-bar .field { margin: 0; }

.rs-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2) var(--s3);
  margin-bottom: var(--s2); font-size: 13px; color: var(--muted);
}
/* The range is a NUMBER SENTENCE and its digits are Arabic-Indic like every
   other count on this page; `lyNum` isolates each run so the ١–٥٠ stays in the
   order it was written in an RTL paragraph. */
.rs-range { font-weight: 600; color: var(--ink); }

/* The active narrowing, in the owner's own words. It is a row rather than a
   line so «امسح الكل» sits at the far end where a clear control belongs, and
   it only exists when something IS narrowed — an empty filter bar is a control
   that teaches nothing. */
.rs-active {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s2); margin-bottom: var(--s2);
}
.rs-active .rs-words { font-size: 13px; color: var(--muted); }
.rs-active .btn-link { margin-inline-start: auto; }

/* THE FOOT — where the next twenty arrive. A fixed minimum height so the page
   does not jolt when «حمّل المزيد» is swapped for «…يحمّل» and then for
   nothing: a list that shifts under the thumb mid-scroll makes the reader lose
   their place in exactly the moment they were trying to keep it. */
.rs-foot {
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--s2);
  min-height: 44px; margin-top: var(--s3);
}
.rs-foot .rs-more { min-width: 160px; }
/* The button is disabled WHILE loading rather than removed, for the same
   reason: a control that vanishes moves everything under it. */
.rs-foot .btn-ghost[disabled] { opacity: .55; cursor: default; }
.rs-end { font-size: 13px; color: var(--muted); margin: 0; }
