/* ============================================================
   Titanium POS — owner panel (read only)
   ------------------------------------------------------------
   Dark, card-led, pill-shaped. Sidebar on a desktop, floating tab
   bar on a phone. The accent is the Titanium orange the till and
   the logo already use, so the panel reads as part of the product
   rather than a different app.

   Dark-only by design: this is one committed look, so every colour
   is painted explicitly and nothing is left to inherit from the
   viewer's theme.
   ============================================================ */
:root{
  --bg:#131316;
  --card:#222228;
  --card-2:#2a2a31;
  --inset:#1b1b20;
  --line:#33333b;

  /* Titanium orange — the till's accent, not the mockup's yellow. */
  --accent:#FF7A18;
  --accent-deep:#E96A0C;
  --on-accent:#170A02;
  --accent-soft:rgba(255,122,24,.13);

  --text:#ffffff;
  --muted:#8c8c96;
  --faint:#63636c;

  --red:#ff5a5f;      --red-soft:rgba(255,90,95,.13);
  --green:#4cd98a;    --green-soft:rgba(76,217,138,.13);
  --purple:#8b5cf6;   --purple-soft:rgba(139,92,246,.15);
  --blue:#38bdf8;     --blue-soft:rgba(56,189,248,.13);
  --amber:#f6d51f;    --amber-soft:rgba(246,213,31,.13);

  --r-lg:24px; --r-md:18px; --r-sm:12px;
  --ease-out:cubic-bezier(.23,1,.32,1);
  --ease-io:cubic-bezier(.77,0,.175,1);
  --safe:env(safe-area-inset-bottom,0px);
  /* The STATUS BAR's height when installed to an iOS home screen.
     apple-mobile-web-app-status-bar-style is black-translucent (see
     partials/head.php), which makes the page full-bleed UNDER the clock and
     battery — so every top edge has to pay this back or it sits behind them.
     0px in a browser tab and on Android, where the bar is not overlaid. */
  --safe-top:env(safe-area-inset-top,0px);
  /* Landscape on a notched phone indents one side. Cheap to honour, and
     without it a rotated dashboard loses its first column to the notch. */
  --safe-l:env(safe-area-inset-left,0px);
  --safe-r:env(safe-area-inset-right,0px);
  --ui:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
  --mono:ui-monospace,'SF Mono',Menlo,Consolas,monospace;
}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html{color-scheme:dark;-webkit-text-size-adjust:100%}

/* ---------------- native feel on a phone ----------------
   Four browser behaviours that are right on a WEB PAGE and wrong in an
   installed app. The owner's complaint was that the panel felt like a
   web page on iOS; these are the reasons it did.

   1. touch-action:manipulation — kills DOUBLE-TAP ZOOM. It is the only
      reliable way: `user-scalable=no` in the viewport meta is IGNORED by
      iOS Safari (deliberately, since iOS 10, for accessibility), which is
      why the meta alone never fixed this. manipulation keeps single tap
      and scrolling and drops only the double-tap-to-zoom gesture, so a
      fast second tap on a nav tab registers as a tap instead of zooming
      the layout in and leaving it there.

      It does NOT block pinch-to-zoom. That is on purpose — pinch is a
      deliberate two-finger gesture, an accessibility lifeline for an
      owner reading small figures, and no native app disables it either.

   2. -webkit-touch-callout:none — stops the grey "Copy / Look Up / Share"
      sheet that iOS pops on a LONG PRESS. In a browser that is a feature;
      in an app it fires whenever a finger rests on a number for half a
      second, and it is the single most web-page-like thing iOS does.

   3. user-select:none — stops a long press or drag from painting the UI
      blue with a text selection. Chrome/Android shows selection handles
      for the same gesture. Selection is re-enabled below for the places
      that genuinely need it.

   4. overscroll-behavior:none — no rubber-band bounce past the top and
      bottom of a page, and no pull-to-refresh. On iOS the bounce reveals
      the page background above the content, which in a full-bleed
      standalone app looks like the UI has come unstuck. */
body{
  font-family:var(--ui);
  background:var(--bg);
  color:var(--text);
  min-height:100vh;
  -webkit-font-smoothing:antialiased;
  overscroll-behavior:none;
  touch-action:manipulation;
  -webkit-touch-callout:none;
  -webkit-user-select:none;
  user-select:none;
}

/* Selection back where it has a purpose. An owner reading the panel on a
   phone still needs to copy an invoice number into WhatsApp, or a till
   name into a message to their supplier — turning selection off
   EVERYWHERE trades one annoyance for a worse one.

   Inputs are the non-negotiable ones: on iOS a text field inside a
   user-select:none subtree cannot be tapped to place a caret or
   double-tapped to select a word, so the PIN box and every search field
   become unusable. `.selectable` is the opt-in hook for anything else
   that should stay copyable. */
input,textarea,select,[contenteditable],.selectable{
  -webkit-user-select:text;
  user-select:text;
  -webkit-touch-callout:default;
}
[hidden]{display:none !important}
button{font-family:inherit;border:0;cursor:pointer;background:none;color:inherit}
a{color:inherit;text-decoration:none}
button:focus-visible,a:focus-visible,select:focus-visible,input:focus-visible{
  outline:2px solid var(--accent);outline-offset:2px;border-radius:8px}
.num,.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}

/* ---------------- shell ---------------- */
.shell{display:grid;grid-template-columns:248px minmax(0,1fr);min-height:100vh}

/* ---------------- sidebar ---------------- */
.sidebar{
  background:var(--inset);border-right:1px solid #232329;
  padding:22px 14px;display:flex;flex-direction:column;gap:6px;
  position:sticky;top:0;height:100vh;overflow-y:auto;
}
.brand{display:flex;align-items:center;gap:12px;padding:6px 10px 22px}
.brand-badge{
  width:42px;height:42px;border-radius:50%;flex-shrink:0;
  background:radial-gradient(circle at 30% 30%,#3a3a44,#26262c);
  border:2px solid #3d3d47;display:grid;place-items:center;
}
.brand-badge svg{width:20px;height:20px}
.brand h1{font-size:15px;font-weight:700;letter-spacing:-.01em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:150px}
.brand span{display:block;font-size:11px;color:var(--muted);font-weight:500;margin-top:2px}
.nav-label{font-size:10px;font-weight:700;letter-spacing:.12em;color:var(--faint);
  text-transform:uppercase;padding:14px 12px 8px}
.nav-btn{
  display:flex;align-items:center;gap:12px;width:100%;
  padding:12px 14px;border-radius:999px;
  font-size:13.5px;font-weight:600;color:var(--muted);text-align:left;
  transition:background 140ms var(--ease-out),color 140ms var(--ease-out),transform 120ms var(--ease-out);
}
.nav-btn svg{width:18px;height:18px;flex-shrink:0}
.nav-btn:active{transform:scale(.97)}
@media (hover:hover) and (pointer:fine){.nav-btn:hover{background:var(--card);color:var(--text)}}
.nav-btn.active{background:var(--accent);color:var(--on-accent)}
.nav-btn.active svg{stroke:var(--on-accent)}
.nav-count{margin-left:auto;font-size:11px;font-weight:700;background:var(--card-2);
  color:var(--muted);border-radius:999px;padding:2px 8px}
.nav-btn.active .nav-count{background:rgba(0,0,0,.16);color:var(--on-accent)}
.nav-count.bad{background:var(--red-soft);color:var(--red)}
/* ---- the phone menu's pieces (only shown under 760px, see below) ---- */
.burger{display:none;position:fixed;z-index:60;top:calc(14px + var(--safe-top));
  inset-inline-end:calc(14px + var(--safe-r));width:44px;height:44px;border-radius:50%;
  flex-direction:column;align-items:center;justify-content:center;gap:4px;
  background:var(--inset);border:1px solid var(--line);box-shadow:0 8px 24px rgba(0,0,0,.45);cursor:pointer}
.burger span{display:block;width:18px;height:2px;border-radius:2px;background:var(--text);
  transition:transform .28s var(--ease-out),opacity .2s var(--ease-out)}
/* the three bars fold into a cross while the drawer is open */
.nav-open .burger span:nth-child(1){transform:translateY(6px) rotate(45deg)}
.nav-open .burger span:nth-child(2){opacity:0;transform:scaleX(.3)}
.nav-open .burger span:nth-child(3){transform:translateY(-6px) rotate(-45deg)}
.burger:active{transform:scale(.94)}
.nav-scrim{position:fixed;inset:0;z-index:90;background:rgba(0,0,0,.6);opacity:0;pointer-events:none;
  transition:opacity .28s var(--ease-out)}
.nav-x{display:none;position:absolute;top:calc(14px + var(--safe-top));inset-inline-end:14px;
  width:38px;height:38px;border-radius:50%;place-items:center;background:var(--card);
  border:1px solid var(--line);color:var(--text);cursor:pointer}
.nav-x svg{width:18px;height:18px}
/* Focus moves to the ✕ when the drawer opens (so a keyboard or screen-reader
   user lands in the menu). The ring is for KEYBOARD focus only — after a tap
   it read as a stray orange box around the close button. */
.nav-x:focus,.burger:focus{outline:none}
.nav-x:focus-visible,.burger:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.sidebar-foot{margin-top:auto;display:flex;align-items:center;gap:11px;
  padding:12px 10px;border-top:1px solid #232329}
.avatar{width:38px;height:38px;border-radius:50%;flex-shrink:0;
  background:linear-gradient(135deg,var(--accent),#c95c00);
  display:grid;place-items:center;font-weight:800;font-size:13px;color:var(--on-accent)}
.sidebar-foot b{font-size:13px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px}
.sidebar-foot span{font-size:11px;color:var(--muted)}
.sidebar-foot .out{margin-left:auto;color:var(--faint);flex-shrink:0}
.sidebar-foot .out svg{width:16px;height:16px;display:block}
@media (hover:hover) and (pointer:fine){.sidebar-foot .out:hover{color:var(--red)}}

/* ---------------- main ---------------- */
.main{padding:26px 30px 90px;max-width:1160px;width:100%;margin:0 auto;
  /* min-width:0 is what stops a wide flex child (the hour bars) from forcing
     the whole column wider than the viewport: a grid item's default
     min-width:auto refuses to shrink below its content. Deliberately NOT
     overflow:hidden — that would conceal an overflow rather than fix it, and
     the wide things here (bars, tables) each scroll inside their own box. */
  min-width:0}
.topbar{display:flex;align-items:flex-start;gap:16px;margin-bottom:22px;flex-wrap:wrap}
.topbar .titles{flex:1;min-width:180px}
.page-title{font-size:22px;font-weight:800;letter-spacing:-.02em}
.page-sub{font-size:12.5px;color:var(--muted);font-weight:500;margin-top:4px}

.chip{
  display:inline-flex;align-items:center;gap:7px;
  background:var(--card);border:1px solid var(--line);border-radius:999px;
  padding:9px 15px;font-size:12.5px;font-weight:600;color:var(--text);
  transition:background 140ms ease,transform 120ms var(--ease-out);
}
.chip:active{transform:scale(.97)}
@media (hover:hover) and (pointer:fine){.chip:hover{background:var(--card-2)}}
.chip svg{width:14px;height:14px;flex-shrink:0}

.icon-btn{
  width:40px;height:40px;border-radius:50%;flex-shrink:0;
  background:var(--accent);color:var(--on-accent);display:grid;place-items:center;
  transition:transform 120ms var(--ease-out),background 140ms ease;
}
.icon-btn:active{transform:scale(.94)}
@media (hover:hover) and (pointer:fine){.icon-btn:hover{background:var(--accent-deep)}}
.icon-btn svg{width:18px;height:18px}
.icon-btn.ghost{background:var(--card);color:var(--text);border:1px solid var(--line)}
@media (hover:hover) and (pointer:fine){.icon-btn.ghost:hover{background:var(--card-2)}}

/* ---------------- period picker ----------------
   A real <form> + <select>: it reloads with the server-side query, so a
   refresh or a shared link keeps the period. Styled to read as the
   mockup's chip. */
.range-form{position:relative;display:inline-flex}
.range-form select{
  appearance:none;-webkit-appearance:none;
  background:var(--card);border:1px solid var(--line);border-radius:999px;
  padding:9px 34px 9px 15px;font:inherit;font-size:12.5px;font-weight:600;
  color:var(--text);cursor:pointer;outline:none;
}
/* inset-inline-end, not right: the panel is bilingual and a physical `right`
   left the chevron sitting on top of the Arabic text while the padding that
   made room for it had correctly flipped to the other side. Latent since the
   period picker shipped; it only became visible when products.php put a long
   sort label in this control. */
.range-form::after{
  content:"";position:absolute;inset-inline-end:14px;top:50%;
  width:7px;height:7px;pointer-events:none;
  border-right:2px solid var(--muted);border-bottom:2px solid var(--muted);
  transform:translateY(-70%) rotate(45deg);
}
/* The select's own padding has to flip with it. */
.range-form select{padding-inline:15px 34px}
/* A long sort label ("Potential profit") must not push the title off a phone.
   The control keeps its own width down and ellipsises instead. */
@media (max-width:560px){
  .range-form{max-width:52vw}
  .range-form select{max-width:100%;text-overflow:ellipsis}
}
@media (hover:hover) and (pointer:fine){.range-form select:hover{background:var(--card-2)}}

/* ---------------- hero ---------------- */
.hero{display:flex;flex-direction:column;align-items:center;text-align:center;padding:6px 0 22px}
.hero h2{font-size:26px;font-weight:800;letter-spacing:-.02em}
.hero .hero-sub{font-size:12.5px;color:var(--muted);font-weight:600;margin-top:5px}
.hero .hero-sub b{color:var(--text)}
.hero .hero-sub .ahead{color:var(--green)}

/* ---- ringless hero ----
   The figure IS the hero now. Its size is clamped rather than fixed: a
   three-decimal figure in a big currency runs to eleven characters, and at a
   flat 44px that wrapped mid-number on a 360px phone. */
.hero.no-ring{padding:2px 0 20px}
.hero.no-ring h2{font-size:15px;font-weight:700;color:var(--muted);
  letter-spacing:.01em}
/* NO `order` on these. Source order (title, amount, sub) is already the
   reading order; setting order on two of the three children left the third
   at the default 0 and floated the invoice count above the title. */
.hero-amt{
  display:flex;align-items:baseline;justify-content:center;gap:8px;
  margin-top:6px;max-width:100%;
  /* A money figure reads left-to-right in every language this panel speaks.
     plaintext keeps the currency mark and the digits in that order inside an
     RTL page without forcing the whole row to LTR alignment. */
  direction:ltr;unicode-bidi:plaintext;
}
.hero-amt .cur{font-size:15px;font-weight:700;color:var(--muted);flex-shrink:0}
.hero-amt b{
  /* The UI face, not the monospace one. The element still carries .num — the
     count-up in shell.js and the RTL rules elsewhere both key off it — and
     .num sets font-family:var(--mono); this rule is (0,1,1) against its
     (0,1,0), so it wins on specificity without needing !important.

     Inter here because the amount is the headline of the screen and reads as
     a title, not as data in a column: the mono face is there to keep FIGURES
     IN A COLUMN aligned with each other, and this number has no column to
     line up with. tabular-nums below does the one job mono was really doing
     for it. */
  font-family:var(--ui);
  font-size:clamp(34px,11vw,52px);font-weight:800;line-height:1.05;
  letter-spacing:-.03em;
  /* tabular figures: without them each frame of the count-up is a different
     width and the number visibly jitters as it climbs */
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
  min-width:0;overflow-wrap:anywhere;
}
.hero-amt.is-ahead b{color:var(--green)}

/* ---- the headline card ----
   The figure and the health verdict in one box. The card supplies the
   padding, so the hero inside it drops its own bottom padding rather than
   stacking two lots of space above the divider. */
.card.hero-card{padding:22px 18px 0;margin-bottom:24px;
  /* clips the full-bleed strip below to the card's rounded corners */
  overflow:hidden}
.hero-card .hero.no-ring{padding:0 0 18px}
/* A stale panel is showing an old number as if it were current, which is a
   stronger claim than any wording can walk back — so the card itself carries
   the warning, not just the strip inside it. */
.hero-card.is-stale{border:1px solid var(--red-soft)}

.hero-state{
  display:flex;align-items:center;gap:11px;
  /* Full-bleed against the card's own padding, so the strip reads as the
     card's foot rather than a floating row inside it. The card has no bottom
     padding (see .hero-card) so this sits flush on the edge and its square
     corners are clipped by the card's overflow:hidden. */
  margin:0 -18px;padding:13px 18px;
  border-top:1px solid var(--line);background:var(--card-2);
}
.hero-state-ic{
  width:30px;height:30px;border-radius:50%;flex-shrink:0;display:grid;place-items:center;
  background:var(--inset);color:var(--muted);
}
.hero-state-ic svg{width:15px;height:15px}
.hero-state-txt{min-width:0}
.hero-state-txt b{display:block;font-size:13px;font-weight:700}
.hero-state-txt span{display:block;font-size:11.5px;color:var(--muted);font-weight:500;margin-top:1px}
/* The warning tone. Red on the disc and the heading only: colouring the whole
   strip would out-shout the takings directly above it, which is the one thing
   this card exists to show. */
.hero-state.warn{background:var(--red-soft)}
.hero-state.warn .hero-state-ic{background:var(--red);color:#fff}
.hero-state.warn .hero-state-txt b{color:var(--red)}

/* ---------------- banner ---------------- */
.banner{
  background:var(--accent);color:var(--on-accent);border-radius:var(--r-md);
  padding:17px 19px;display:flex;align-items:center;gap:14px;margin-bottom:24px;
}
.banner .txt{flex:1;min-width:0}
.banner b{font-size:15px;font-weight:700;display:block}
/* Scoped to .txt on purpose. A bare `.banner span` also matched the icon
   disc — which IS a <span> — and its display:block beat the .banner-ic grid
   on source order, knocking the glyph 11px off centre in every banner. */
.banner .txt span{font-size:12px;font-weight:500;opacity:.78;margin-top:3px;display:block}
.banner-ic{width:40px;height:40px;border-radius:50%;background:#141414;color:var(--accent);
  display:grid;place-items:center;flex-shrink:0}
.banner-ic svg{width:17px;height:17px}
/* a warning banner is red, not accent — it must not read as a promo */
.banner.warn{background:var(--red);color:#fff}
/* the disc is near-black by default, which disappears on the red banner —
   give the warning variant a light disc so the glyph actually reads */
.banner.warn .banner-ic{background:#fff;color:var(--red)}
.banner.flat{background:var(--card);color:var(--text);border:1px solid var(--line)}
.banner.flat .banner-ic{background:var(--card-2);color:var(--muted)}
.banner.flat .txt span{opacity:1;color:var(--muted)}

/* ---------------- sync strip ---------------- */
.sync{
  display:flex;align-items:center;gap:11px;padding:11px 16px;margin-bottom:20px;
  background:var(--card);border:1px solid var(--line);border-radius:999px;
}
.sync .dot{width:9px;height:9px;border-radius:50%;flex-shrink:0;background:var(--faint)}
.sync.live .dot{background:var(--green);box-shadow:0 0 0 4px var(--green-soft);
  animation:pulse 2.4s ease-in-out infinite}
.sync.recent .dot{background:var(--amber);box-shadow:0 0 0 4px var(--amber-soft)}
.sync.stale .dot,.sync.never .dot{background:var(--red);box-shadow:0 0 0 4px var(--red-soft)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
.sync .txt{min-width:0;font-size:12.5px;font-weight:600}
.sync .txt span{color:var(--muted);font-weight:500}
/* margin-inline-start, not margin-left: in RTL the timestamp belongs at the
   other end, and margin-left pins it to the wrong side of the pill. */
.sync .when{margin-inline-start:auto;font-size:11.5px;color:var(--faint);font-weight:600;
  white-space:nowrap;flex-shrink:0}

/* The refresh button, now living inside the pill. It is sized as a proper
   touch target (36px, with the pill's own padding taking it past 44px of
   reachable area) and pulled tight to the pill's edge so the capsule keeps
   its shape rather than growing a bulge around the button. */
.sync-refresh{
  width:36px;height:36px;border-radius:50%;flex-shrink:0;display:grid;place-items:center;
  background:var(--card-2);color:var(--muted);border:1px solid var(--line);
  margin-inline-start:12px;margin-inline-end:-8px;
  transition:transform 120ms var(--ease-out),background 140ms ease,color 140ms ease;
}
.sync-refresh svg{width:16px;height:16px}
.sync-refresh:active{transform:scale(.92)}
@media (hover:hover) and (pointer:fine){
  .sync-refresh:hover{background:var(--accent);color:var(--on-accent);border-color:transparent}
}
/* The pill has to hold a dot, two lines of wrapping text, a timestamp and a
   button without bursting at phone width. Text wraps; nothing else does. */
.sync{flex-wrap:nowrap}

/* ---------------- section heads ---------------- */
.sect-head{display:flex;align-items:center;gap:8px;margin:26px 0 14px;font-size:14px;font-weight:700}
.sect-head .em{font-size:15px;flex-shrink:0}
.sect-head .spacer{flex:1}
.sect-head .mini{font-size:11.5px;color:var(--muted);font-weight:600}
.sect-head .mini.neg{color:var(--red)}

/* ---------------- cards ---------------- */
.grid{display:grid;gap:14px;min-width:0}
/* align-items:start stops a short card being stretched to match a tall
   neighbour, which leaves a dead grey slab under its last row */
.grid{align-items:start}
.grid > *{min-width:0}
.grid.c4{grid-template-columns:repeat(4,1fr)}
.grid.c2{grid-template-columns:repeat(2,1fr)}
.card{background:var(--card);border-radius:var(--r-lg);padding:18px;
  animation:rise 320ms var(--ease-out) both;min-width:0}
.card.pad-s{padding:14px 16px}

.kpi{display:flex;flex-direction:column;gap:12px}
.kpi-top{display:flex;align-items:center;gap:11px;min-width:0}
.kpi-ring{position:relative;width:40px;height:40px;flex-shrink:0}
.kpi-ring svg{transform:rotate(-90deg)}
/* Each KPI ring sweeps to its value rather than appearing at it. Staggered
   per card in the responsive block below so the row fills left to right
   instead of all four snapping together. */
.kpi-ring circle+circle{transition:stroke-dashoffset 1100ms var(--ease-io)}
.kpi-ring .ic{position:absolute;inset:0;display:grid;place-items:center}
.kpi-ring .ic svg{transform:none;width:15px;height:15px}
.kpi-name{font-size:12.5px;font-weight:600;color:var(--muted);min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.kpi-val{font-size:19px;font-weight:800;letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;word-break:break-word}
.kpi-val small{font-size:11px;font-weight:600;color:var(--faint);margin-left:3px}
.kpi-foot{font-size:11px;color:var(--faint);font-weight:600;display:flex;align-items:center;gap:5px}
.kpi-foot.alert{color:var(--red)}
.kpi-foot.good{color:var(--green)}
.kpi-foot svg{width:11px;height:11px;flex-shrink:0}

/* ---------------- list rows ---------------- */
.row-card{background:var(--card);border-radius:var(--r-md);padding:15px 16px 0;
  margin-bottom:12px;animation:rise 320ms var(--ease-out) both;display:block;color:inherit}
.row-top{display:flex;gap:13px;align-items:flex-start;padding-bottom:13px;min-width:0}
/* The row's leading glyph. `color` is set inline per row and the SVG strokes
   with currentColor, so one rule tints every variant. A soft wash of the same
   colour sits behind it, which reads on the dark card without needing a
   second hard-coded shade per state. */
.pill-ic{width:40px;height:40px;border-radius:12px;background:var(--card-2);
  display:grid;place-items:center;flex-shrink:0;overflow:hidden;
  /* a hairline of the row's own colour: enough to tie the glyph to its state
     without competing with the tag pill next to it */
  box-shadow:inset 0 0 0 1px color-mix(in srgb, currentColor 34%, transparent)}
@supports not (color: color-mix(in srgb, red 50%, blue)) {
  .pill-ic{box-shadow:inset 0 0 0 1px var(--line)}
}
.pill-ic svg{width:19px;height:19px;display:block}
.row-body{min-width:0;flex:1}
.row-name{font-size:14.5px;font-weight:700;display:flex;align-items:center;gap:7px;
  flex-wrap:wrap;line-height:1.35}
/* The product row's detail block: barcode, SKU, cost, margin, sell price and
   category, each as a labelled cell.

   A GRID, not a sentence. The old run-on line ("SKU: P89 · Household · Cost:
   6.000 …") rewrapped at every width, so the eye had to re-find each label on
   every row; with six fields it stopped being readable at all.

   auto-fit/minmax reflows without a media query — three or four across on a
   desktop card, two on a phone — and 84px is set from the widest realistic
   VALUE (a 13-digit barcode at 11px monospace), not from taste. */
.row-meta{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(84px,1fr));
  gap:7px 14px;margin-top:7px;
  font-size:11.5px;color:var(--muted);font-weight:500;
  overflow-wrap:anywhere;
}
.rm-i{display:flex;flex-direction:column;gap:1px;min-width:0}
.rm-k{
  font-size:9.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* The VALUE carries the emphasis, not the label — the label is scaffolding
   the owner stops reading after the first row. */
.rm-v{
  color:var(--text);font-size:12px;font-weight:600;
  font-variant-numeric:tabular-nums;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.rm-v.is-strong{font-weight:800}
/* Only the margin is coloured, and only when negative: it is the one figure
   here that is a judgement rather than a fact. */
.rm-v.is-neg{color:var(--red)}
.row-meta b{color:var(--text)}
/* The barcode. Monospace and letter-spaced because this number gets compared
   against a printed label digit by digit — a proportional font makes 1/7 and
   0/8 harder to tell apart at 11px, and that is exactly the check being made.
   unicode-bidi:isolate alongside the dir="ltr" in the markup so the run cannot
   reorder when it sits inside an Arabic line. */
.row-bc{
  font-family:var(--mono);font-size:11px;letter-spacing:.02em;
  unicode-bidi:isolate;
}
/* Inside the grid the barcode is a VALUE like the others, so it keeps
   --text rather than the dimmed --faint it used as a trailing aside. */
.rm-v.row-bc{color:var(--text)}
.row-amt{margin-left:auto;text-align:right;flex-shrink:0}
.row-amt b{font-size:15px;font-weight:800;letter-spacing:-.01em;display:block;
  font-variant-numeric:tabular-nums}
.row-amt span{font-size:11px;color:var(--muted);font-weight:600}
.row-amt b.neg{color:var(--red)}
.row-amt b.pos{color:var(--green)}
.row-foot{border-top:1px solid var(--line);display:flex;align-items:center;gap:8px;
  padding:11px 0;font-size:11.5px;font-weight:600;color:var(--muted)}
.row-foot svg{width:13px;height:13px;flex-shrink:0}
.row-foot.alert{color:var(--red)}
.row-foot .go{margin-left:auto;color:var(--faint);flex-shrink:0;display:flex}
.row-foot .go svg{width:15px;height:15px}
@media (hover:hover) and (pointer:fine){
  a.row-card:hover{background:var(--card-2)}
  a.row-card:hover .row-foot .go{color:var(--accent)}
}

/* stock bar inside a row */
.stock-line{display:flex;align-items:center;gap:12px;padding-bottom:14px}
.bar{height:6px;border-radius:999px;background:#33333b;overflow:hidden;flex:1}
.bar i{display:block;height:100%;border-radius:999px;background:var(--accent)}
.bar i.low{background:var(--red)}
.bar i.good{background:var(--green)}
.stock-line .num{font-size:11.5px;font-weight:700;color:var(--muted);width:84px;
  text-align:right;flex-shrink:0}
.stock-line .num.low{color:var(--red)}

/* ---------------- tags ---------------- */
.tag{font-size:10.5px;font-weight:700;border-radius:999px;padding:3px 9px;
  letter-spacing:.02em;white-space:nowrap}
.tag.paid{background:var(--green-soft);color:var(--green)}
.tag.unpaid{background:var(--red-soft);color:var(--red)}
.tag.refund{background:var(--purple-soft);color:#b699ff}
.tag.disc{background:var(--amber-soft);color:var(--amber)}
.tag.void{background:var(--red-soft);color:var(--red)}
.tag.card-m{background:var(--blue-soft);color:var(--blue)}
.tag.cash-m{background:var(--amber-soft);color:var(--amber)}
.tag.bank-m{background:var(--purple-soft);color:#b699ff}
.tag.n{background:var(--card-2);color:var(--muted)}
.tag.ok{background:var(--green-soft);color:var(--green)}

/* ---------------- filter chips ---------------- */
/* ---- catalogue totals strip (products.php) ------------------------------
   A single card rather than five tiles: these are five readings of ONE
   selection, and boxing each separately made them look like unrelated KPIs.

   auto-fit/minmax lets it reflow without a media query — five across on a
   desktop, two on a phone, and it never squeezes a money figure into a
   column too narrow to hold it. 132px is set from the longest realistic
   value (a 9-figure retail total at 3 decimals), not from taste. */
.p-tot{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));
  /* The ROW gap has to clear the label above it. At 4px the wrapped rows
     interleaved — "Retail value" sat nearer the figure ABOVE it than its
     own, so the eye paired every label with the wrong number. Measured on a
     390px phone, where five items wrap to three rows. */
  gap:14px 18px;
  background:var(--card);border:1px solid var(--line);border-radius:var(--r-md);
  padding:14px 18px;margin-bottom:12px;
}
/* A tile is a LINK now (it sorts by its own column), so it needs the padding
   and the hit area of a control. Negative margin keeps the text aligned with
   where it sat as a plain <div>, so adding the affordance did not reflow the
   card. 44px min-height is the touch target a finger needs. */
.p-tot-i{
  display:flex;flex-direction:column;gap:3px;min-width:0;
  padding:6px 8px;margin:-6px -8px;border-radius:10px;
  min-height:44px;justify-content:center;
  transition:background 140ms ease;
}
/* The active sort. A left rule rather than a filled background: five tiles in
   one card, and filling one made it read as a separate widget. Logical border
   so it moves to the right edge in Arabic. */
.p-tot-i.is-on{
  background:var(--accent-soft);
  box-shadow:inset 2px 0 0 0 var(--accent);
}
[dir="rtl"] .p-tot-i.is-on{box-shadow:inset -2px 0 0 0 var(--accent)}
.p-tot-i.is-on .p-tot-k{color:var(--accent)}
@media (hover:hover) and (pointer:fine){
  .p-tot-i:hover{background:var(--card-2)}
  .p-tot-i.is-on:hover{background:var(--accent-soft)}
}
.p-tot-i:active{transform:scale(.98)}
.p-tot-k{font-size:10.5px;font-weight:700;letter-spacing:.07em;
  text-transform:uppercase;color:var(--faint);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
/* tabular-nums via .num keeps the five figures on a shared grid, so the eye
   can compare cost against value without re-reading each digit. */
.p-tot-v{font-size:16px;font-weight:800;letter-spacing:-.02em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.p-tot-v.is-neg{color:var(--red)}
/* On a phone the five wrap to two columns, and a lone fifth item stranded on
   its own row reads as an afterthought rather than part of the set. Pinning
   to exactly 2 columns and letting "Potential profit" span BOTH makes the
   bottom line the widest thing in the card, which is also the right emphasis:
   it is the figure the owner came for. */
@media (max-width:560px){
  .p-tot{grid-template-columns:1fr 1fr;padding:13px 15px;gap:12px 14px}
  .p-tot-i:last-child{grid-column:1 / -1;border-top:1px solid var(--line);
    padding-top:11px;margin-top:0;border-radius:0 0 10px 10px}
  .p-tot-v{font-size:15px}
}

.filters{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
.f-chip{padding:8px 15px;border-radius:999px;background:var(--card);
  font-size:12px;font-weight:600;color:var(--muted);
  transition:background 140ms ease,color 140ms ease,transform 120ms var(--ease-out)}
.f-chip:active{transform:scale(.96)}
.f-chip.active{background:var(--accent);color:var(--on-accent)}
/* The count inside a pill. Dimmed and slightly smaller so the NAME stays the
   thing being read — the number is a size cue, not the label.
   currentColor at reduced opacity rather than a fixed grey, so it stays
   legible when the pill inverts to orange on selection. */
/* The chip becomes a flex row so the gap is honoured in BOTH directions.
   margin-inline-start alone left "Bakery6" glued together in Arabic: the
   count is a separate inline box with no whitespace text node beside it, and
   the PHP deliberately emits none (a newline there would render as a space
   in LTR and shift the number). A flex gap is direction-agnostic and needs
   no second RTL rule. */
.f-chip{display:inline-flex;align-items:center;gap:6px}
.f-chip .f-n{
  font-size:11px;font-weight:700;
  opacity:.55;font-variant-numeric:tabular-nums;
}
@media (hover:hover) and (pointer:fine){
  .f-chip:not(.active):hover{background:var(--card-2);color:var(--text)}}

/* ---------------- search ---------------- */
.searchbar{display:flex;gap:9px;margin-bottom:16px;flex-wrap:wrap}
.searchbar input,.searchbar select{
  height:44px;padding:0 16px;font:inherit;font-size:13.5px;color:var(--text);
  background:var(--card);border:1px solid var(--line);border-radius:999px;outline:none;min-width:0;
}
.searchbar input{flex:1 1 200px}
.searchbar input::placeholder{color:var(--faint)}
.searchbar select{appearance:none;-webkit-appearance:none;padding-right:34px;cursor:pointer;font-weight:600}
.searchbar input:focus,.searchbar select:focus{border-color:var(--accent)}
.searchbar .go{height:44px;padding:0 20px;border-radius:999px;background:var(--accent);
  color:var(--on-accent);font-weight:700;font-size:13.5px}
@media (hover:hover) and (pointer:fine){.searchbar .go:hover{background:var(--accent-deep)}}

/* ---- search button INSIDE the field (.searchbar.one-line) ----------------
   A modifier, not a change to .searchbar itself: sales.php and refunds.php
   still use the base bar with a visible "Search" label, and this must not
   reach them.

   The button is absolutely positioned over the input rather than laid out
   beside it, because the round pill has to keep its shape while the field
   flexes. The input pays for it with padding-inline-end so a long product
   name scrolls UNDER the button instead of behind it.

   Everything here is LOGICAL (inline-start/end, padding-inline) so Arabic
   gets the button on the left with no second rule — the panel is bilingual
   and a physical `right:6px` would have parked it on top of the caret in RTL. */
/* The search row: the field (which grows) and the scan button (which does
   not). A flex row rather than putting the button inside .searchbar, because
   the button is NOT part of the form — it must not submit it. */
.searchrow{display:flex;gap:9px;align-items:flex-start;margin-bottom:16px}
.searchrow .searchbar{flex:1 1 auto;margin-bottom:0;min-width:0}
/* Same 44px circle as the field's height, so the two line up exactly.
   flex:0 0 auto or it would be squeezed by a long placeholder. */
.scanbtn{
  flex:0 0 auto;width:44px;height:44px;border-radius:50%;
  background:var(--card);border:1px solid var(--line);color:var(--text);
  display:grid;place-items:center;
  transition:background 140ms ease,transform 120ms var(--ease-out);
}
.scanbtn svg{width:19px;height:19px}
.scanbtn:active{transform:scale(.94)}
@media (hover:hover) and (pointer:fine){.scanbtn:hover{background:var(--card-2)}}
/* [hidden] is already display:none !important globally, so a device with no
   camera costs nothing — but say it here too, since this button's default
   state IS hidden and that is easy to mistake for a bug while reading. */
.scanbtn[hidden]{display:none !important}

.searchbar.one-line{flex-wrap:nowrap;position:relative}
.searchbar.one-line input{flex:1 1 auto;padding-inline-end:54px}
/* With a clear button present the text must stop before BOTH circles, or a
   long product name slides underneath the X. :has() is the only way to react
   to a sibling that PHP renders conditionally; browsers without it simply
   keep the single-button padding, which overlaps only on a very long query —
   a graceful loss, not a broken field. */
.searchbar.one-line:has(.clr) input{padding-inline-end:92px}
.searchbar.one-line .go,
.searchbar.one-line .clr{
  position:absolute;inset-inline-end:6px;top:6px;
  width:32px;height:32px;padding:0;border-radius:50%;
  display:grid;place-items:center;
}
/* The clear sits INSIDE the field, to the left of Search in LTR and to its
   right in RTL. inset-inline-end handles both from one rule: 6px + 32px
   button + 4px gap. */
.searchbar.one-line .clr{
  inset-inline-end:42px;
  background:var(--card-2);color:var(--muted);
  border:1px solid var(--line);
}
.searchbar.one-line .clr svg{width:13px;height:13px}
.searchbar.one-line .clr:active{transform:scale(.92)}
@media (hover:hover) and (pointer:fine){
  .searchbar.one-line .clr:hover{background:var(--line);color:var(--text)}
}
.searchbar.one-line .clr:focus-visible{outline-offset:3px;border-radius:50%}
.searchbar.one-line .go svg{width:15px;height:15px}
/* The icon is the only label, so the focus ring has to be visibly round —
   the shared rule's 8px radius would draw a square box around a circle. */
.searchbar.one-line .go:focus-visible{outline-offset:3px;border-radius:50%}

/* ---------------- CTA ---------------- */
.cta{width:100%;background:var(--accent);color:var(--on-accent);border-radius:999px;
  padding:16px 22px;display:flex;align-items:center;justify-content:space-between;
  font-size:15px;font-weight:700;margin:18px 0 8px;
  transition:transform 130ms var(--ease-out),background 140ms ease}
.cta:active{transform:scale(.985)}
@media (hover:hover) and (pointer:fine){.cta:hover{background:var(--accent-deep)}}
.cta .plus{width:26px;height:26px;border-radius:50%;background:#141414;color:var(--accent);
  display:grid;place-items:center;flex-shrink:0}
.cta .plus svg{width:13px;height:13px}
.cta.ghost{background:var(--card);color:var(--text);border:1px solid var(--line)}
.cta.ghost .plus{background:var(--card-2);color:var(--text)}
@media (hover:hover) and (pointer:fine){.cta.ghost:hover{background:var(--card-2)}}

/* ---------------- chart ---------------- */
.chart-card{background:var(--card);border-radius:var(--r-lg);padding:20px 18px 12px;
  margin-bottom:14px;animation:rise 320ms var(--ease-out) both;min-width:0}
.chart-head{display:flex;align-items:center;gap:10px;margin-bottom:6px;flex-wrap:wrap}
.chart-head b{font-size:14px;font-weight:700}
.chart-head .spacer{flex:1}
.bars{display:flex;align-items:flex-end;gap:0;height:170px;padding:10px 4px 0;
  /* the bars scroll INSIDE this box; max-width + min-width:0 keep the box
     itself inside the card however many columns there are */
  overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%;min-width:0}
.bcol{flex:1 0 auto;min-width:30px;display:flex;flex-direction:column;align-items:center;
  gap:9px;justify-content:flex-end;height:100%}
.bcol .b{width:26px;border-radius:8px 8px 4px 4px;background:#3a3a43;min-height:6px;
  transition:background 200ms ease}
.bcol.hot .b{background:var(--accent)}
.bcol .v{font-size:10px;font-weight:700;color:var(--faint);opacity:0;transition:opacity 200ms ease;
  white-space:nowrap}
.bcol.hot .v{opacity:1;color:var(--accent)}
.bcol .d{font-size:10.5px;font-weight:600;color:var(--muted);white-space:nowrap}
.bcol.hot .d{color:var(--text)}
@media (hover:hover) and (pointer:fine){
  .bcol:hover .b{background:#4a4a55}
  .bcol.hot:hover .b{background:var(--accent-deep)}
  .bcol:hover .v{opacity:1}
}
.legend{display:flex;gap:16px;flex-wrap:wrap;padding:8px 4px 6px;
  border-top:1px solid var(--line);margin-top:12px}
.legend span{font-size:11px;font-weight:600;color:var(--muted);display:flex;align-items:center;gap:6px}
.dot-l{width:8px;height:8px;border-radius:50%;flex-shrink:0}

/* ---------------- summary rows ---------------- */
.sum-row{display:flex;align-items:center;gap:12px;padding:13px 2px;
  border-bottom:1px solid var(--line);font-size:13px;font-weight:600}
.sum-row:last-child{border-bottom:0}
.sum-row .lbl{color:var(--muted);min-width:0}
.sum-row b{margin-left:auto;font-weight:800;font-size:13.5px;
  font-variant-numeric:tabular-nums;white-space:nowrap}
.sum-row b.neg{color:var(--red)}
.sum-row b.pos{color:var(--green)}
.sum-row.total{border-top:1px solid var(--line);margin-top:2px}

/* ---------------- tables ---------------- */
.tw{overflow-x:auto;-webkit-overflow-scrolling:touch;margin:0 -2px}
table.data{width:100%;border-collapse:collapse;font-size:13px;min-width:520px}
table.data th{text-align:left;font-size:10.5px;font-weight:700;text-transform:uppercase;
  letter-spacing:.08em;color:var(--faint);padding:10px 12px;
  border-bottom:1px solid var(--line);white-space:nowrap}
table.data td{padding:12px;border-bottom:1px solid var(--line);vertical-align:middle}
table.data tbody tr:last-child td{border-bottom:0}
table.data td.num,table.data th.num{text-align:right;white-space:nowrap;
  font-variant-numeric:tabular-nums}
table.data td .sm{display:block;font-size:11px;color:var(--faint);margin-top:2px}
table.data td b{font-weight:700}
@media (hover:hover) and (pointer:fine){table.data tbody tr:hover{background:var(--card-2)}}

/* ---------------- pager ---------------- */
.pager{display:flex;align-items:center;gap:10px;margin-top:16px}
.pager .mid{margin:0 auto;font-size:12px;color:var(--muted);font-weight:600}

/* ---------------- empty ---------------- */
.empty{text-align:center;color:var(--muted);padding:36px 16px;font-size:13px;font-weight:600}
.empty b{display:block;color:var(--text);font-size:14.5px;margin-bottom:5px;font-weight:700}
.note{font-size:11.5px;color:var(--faint);font-weight:500;line-height:1.7;
  margin-top:22px;padding:0 2px}
.note b{color:var(--muted);font-weight:700}

/* ---------------- PIN ---------------- */
/* Padded for the notch on all four edges. The card is centred, so on a tall
   phone it clears the status bar anyway — but in LANDSCAPE on a notched
   iPhone, and on a small screen where the card fills the height, the top of
   it slid under the clock once the status bar went translucent. Cheaper to
   pay the inset back than to special-case the orientation. */
.pin-page{
  min-height:100dvh;display:grid;place-items:center;
  padding:calc(24px + var(--safe-top)) calc(24px + var(--safe-r))
          calc(24px + var(--safe)) calc(24px + var(--safe-l));
}
.pin-card{width:100%;max-width:370px;background:var(--card);border-radius:var(--r-lg);
  padding:32px 26px 26px;text-align:center;box-shadow:0 24px 60px rgba(0,0,0,.5)}
.pin-card .brand-badge{width:56px;height:56px;margin:0 auto 16px}
.pin-card .brand-badge svg{width:26px;height:26px}
.pin-card h1{font-size:20px;font-weight:800;letter-spacing:-.02em}
.pin-card .sub{font-size:12.5px;color:var(--muted);margin-top:5px;margin-bottom:22px;font-weight:500}
.pin-dots{display:flex;justify-content:center;gap:12px;margin-bottom:20px}
.pin-dots i{width:13px;height:13px;border-radius:50%;background:var(--card-2);
  border:1.5px solid var(--line);transition:background 160ms var(--ease-out),
  border-color 160ms var(--ease-out),transform 160ms var(--ease-out)}
.pin-dots i.on{background:var(--accent);border-color:var(--accent);transform:scale(1.1)}
.pin-in{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}
.keypad{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.keypad button{height:58px;font:inherit;font-size:21px;font-weight:700;
  background:var(--card-2);color:var(--text);border-radius:16px;
  transition:background 140ms ease,transform 110ms var(--ease-out)}
.keypad button:active{background:var(--accent);color:var(--on-accent);transform:scale(.95)}
@media (hover:hover) and (pointer:fine){.keypad button:hover{background:#34343d}}
.keypad button.wide{font-size:13px;font-weight:700;color:var(--muted)}
.pin-err{background:var(--red-soft);color:var(--red);border-radius:14px;
  padding:11px 14px;font-size:12.5px;font-weight:600;margin-bottom:16px}
.pin-foot{font-size:11.5px;color:var(--faint);margin-top:20px;line-height:1.7;font-weight:500}
.pin-foot a{color:var(--muted);text-decoration:underline}
.pin-by{display:inline-block;margin-top:2px;color:var(--faint);opacity:.75}
/* The build number. Dimmer than the byline beside it and on its own line:
   it is a thing to look up when something is wrong, never a thing to read.
   Monospace so two versions can be compared digit by digit. */
.pin-ver{display:block;margin-top:6px;font-family:var(--mono);font-size:10.5px;
  color:var(--faint);opacity:.6;unicode-bidi:isolate}

/* ---- signing in -----------------------------------------------------
   The form posts and the page reloads, which on a phone over mobile data
   is a second or more of NOTHING: the sixth dot fills and the screen sits
   there. The natural reading is that the last tap missed, so the owner
   taps again — and the double-submit guard in index.php silently swallows
   it, which makes the panel feel broken rather than slow.

   The card says so instead. The keypad stops accepting taps, the dots hand
   over to a spinner, and the whole card dims a little so it reads as "busy"
   rather than "finished". */
.pin-card.is-signing .keypad{opacity:.45;pointer-events:none}
.pin-card.is-signing .pin-dots{display:none}
.pin-busy{display:none;align-items:center;justify-content:center;gap:10px;
  margin-bottom:20px;min-height:13px;
  font-size:12.5px;font-weight:600;color:var(--muted)}
.pin-card.is-signing .pin-busy{display:flex}
.pin-busy i{width:15px;height:15px;flex:none;border-radius:50%;
  border:2px solid var(--line);border-top-color:var(--accent);
  animation:pin-spin 620ms linear infinite}
@keyframes pin-spin{to{transform:rotate(360deg)}}
/* A viewer who has asked for less motion gets a pulse instead of a spin —
   the state still reads, without the rotation. */
@media (prefers-reduced-motion:reduce){
  .pin-busy i{animation:pulse 1200ms ease-in-out infinite}
}

/* ---------------- bottom nav (phone) ---------------- */

/* ---------------- animation ---------------- */
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.row-card:nth-child(1){animation-delay:0ms}
.row-card:nth-child(2){animation-delay:40ms}
.row-card:nth-child(3){animation-delay:80ms}
.row-card:nth-child(4){animation-delay:120ms}
.row-card:nth-child(5){animation-delay:160ms}
.row-card:nth-child(6){animation-delay:200ms}
.grid .card:nth-child(1){animation-delay:0ms}
.grid .card:nth-child(2){animation-delay:40ms}
.grid .card:nth-child(3){animation-delay:80ms}
.grid .card:nth-child(4){animation-delay:120ms}

/* ---------------- dashboard foot: sign out + build ----------------
   The sidebar is display:none on a phone, and it held the only sign-out
   link — so on the device most owners use there was no way to leave the
   session at all. This is that way.

   It sits inside .main, which already reserves 112px at the bottom for the
   floating tab bar, so nothing here has to know about that. */
.dash-foot{
  margin-top:26px;padding-top:20px;border-top:1px solid var(--line);
  display:flex;flex-direction:column;align-items:center;gap:10px;
}
/* Outlined, not filled. Signing out is destructive-ish and rare: giving it
   the accent would make it compete with the figures above, and a solid
   button at the end of a page reads as the thing you are meant to press. */
.signout-btn{
  display:inline-flex;align-items:center;gap:9px;
  height:44px;padding:0 22px;border-radius:999px;
  background:var(--card);border:1px solid var(--line);color:var(--muted);
  font-size:13px;font-weight:700;
  transition:background 140ms ease,color 140ms ease,border-color 140ms ease,
             transform 120ms var(--ease-out);
}
.signout-btn svg{width:16px;height:16px}
.signout-btn:active{transform:scale(.97)}
@media (hover:hover) and (pointer:fine){
  .signout-btn:hover{background:var(--red-soft);border-color:var(--red);color:var(--red)}
}
/* The build. Monospace so two versions compare digit by digit, and quiet
   enough to be furniture until something is wrong. */
.dash-ver{font-family:var(--mono);font-size:10.5px;color:var(--faint);
  opacity:.6;unicode-bidi:isolate}

/* ---------------- responsive ---------------- */
@media (max-width:1024px){
  .grid.c4{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:760px){
  .shell{grid-template-columns:minmax(0,1fr)}
  /* ---- phone drawer ----
     The sidebar leaves the grid and becomes an off-canvas panel on the
     inline-END side (right in English, left in Arabic/Persian — the side the
     burger sits on), slid by transform only, so the animation runs on the
     compositor. visibility is held until the slide-out finishes, which keeps
     the links out of the tab order while closed without killing the exit
     animation the way display:none / [hidden] would. */
  .sidebar{position:fixed;top:0;bottom:0;inset-inline-end:0;z-index:95;
    width:min(86vw,320px);height:100dvh;
    padding:calc(18px + var(--safe-top)) 14px calc(14px + var(--safe));
    border-right:0;border-inline-start:1px solid var(--line);
    box-shadow:-24px 0 60px rgba(0,0,0,.55);
    transform:translateX(100%);visibility:hidden;
    transition:transform .32s var(--ease-out),visibility 0s linear .32s;
    overscroll-behavior:contain}
  html[dir="rtl"] .sidebar{transform:translateX(-100%);box-shadow:24px 0 60px rgba(0,0,0,.55)}
  .nav-open .sidebar,html[dir="rtl"].nav-open .sidebar{transform:none;visibility:visible;
    transition:transform .32s var(--ease-out),visibility 0s linear 0s}
  .nav-x{display:grid}
  .burger{display:flex}
  .nav-open .nav-scrim{opacity:1;pointer-events:auto}
  .nav-open body{overflow:hidden}
  /* the title must not run under the floating burger */
  .topbar{padding-inline-end:54px}
  /* THE STATUS-BAR FIX. In a browser tab --safe-top is 0 and this reads as
     the original flat 20px; installed on iOS it becomes 20px + the notch,
     which is what keeps the page title clear of the clock. The side insets
     matter only in landscape on a notched phone. */
  .main{padding:calc(20px + var(--safe-top)) calc(16px + var(--safe-r))
        calc(28px + var(--safe)) calc(16px + var(--safe-l))}
  .grid.c2{grid-template-columns:1fr}
  .topbar{margin-bottom:18px}
  .page-title{font-size:20px}
}
/* The delayed fill. Each card's ring and each coverage bar starts a beat
   after the one before it, so the dashboard resolves in a sweep rather than
   flashing every figure at once. Delay only — the durations are on the
   properties themselves. */
.grid.c4 .card:nth-child(1) .kpi-ring circle+circle{transition-delay:60ms}
.grid.c4 .card:nth-child(2) .kpi-ring circle+circle{transition-delay:160ms}
.grid.c4 .card:nth-child(3) .kpi-ring circle+circle{transition-delay:260ms}
.grid.c4 .card:nth-child(4) .kpi-ring circle+circle{transition-delay:360ms}
.cover-bar:nth-of-type(1) .cover-track i{transition-delay:80ms}
.cover-bar:nth-of-type(2) .cover-track i{transition-delay:240ms}

@media (max-width:430px){
  .grid.c4{grid-template-columns:repeat(2,1fr)}
  .row-amt b{font-size:14px}
  .kpi-val{font-size:17px}
}

/* ---- phone ----
   At this width the sync pill is the busiest thing on the screen: a dot, a
   relative age, a verdict, an absolute timestamp and now a button. The
   absolute timestamp is the one part the strip states twice — "4 hours ago"
   already answers it — so it is the part that goes, which leaves the text
   room to breathe and the button room to be tapped. */
@media (max-width:560px){
  .sync{padding:9px 10px 9px 14px;gap:9px}
  .sync .when{display:none}
  .sync .txt{font-size:12px;line-height:1.35}
  /* The pill's own end padding is small here, so the button no longer needs
     to be pulled outward to sit flush. */
  .sync-refresh{margin-inline-start:auto;margin-inline-end:0}
}
@media (prefers-reduced-motion:reduce){
  *{animation-duration:.01ms !important;transition-duration:.01ms !important}
}
@media print{
  .sidebar,.burger,.nav-scrim,.range-form,.searchbar,.filters,.cta,.icon-btn{display:none !important}
  body{background:#fff;color:#000}
  .shell{grid-template-columns:1fr}
  .card,.row-card,.chart-card{background:#fff;border:1px solid #ccc;break-inside:avoid}
}

/* ---------------- single-page shell ----------------
   The panel swaps its <main> instead of reloading (see assets/shell.js).
   These are the only styles that behaviour needs: a top progress bar so a
   slow query is visibly in flight, and a short fade-up on the new content
   so the change registers as a transition rather than a flicker. */
/* Below the status bar, not under it. With black-translucent the page is
   full-bleed, so a strip at top:0 would render behind the clock and the
   "query in flight" signal would simply never be seen on an installed
   phone. --safe-top is 0 everywhere else, so a browser tab is unchanged. */
.nav-bar{position:fixed;top:var(--safe-top);left:0;right:0;height:2px;z-index:60;
  background:transparent;opacity:0;transition:opacity 160ms ease;pointer-events:none}
.nav-bar.on{opacity:1}
.nav-bar i{display:block;height:100%;width:0;
  background:linear-gradient(90deg,var(--accent),#ffb25a);
  box-shadow:0 0 8px rgba(255,122,24,.6)}

/* The outgoing view dims slightly rather than disappearing, so the page does
   not flash empty on a slow connection. */
.main.is-loading{opacity:.55;transition:opacity 140ms ease 90ms}

.main.swap-in{animation:swapIn 240ms var(--ease-out) both}
@keyframes swapIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

/* Skip-link style focus target: after a swap the new heading takes focus for
   screen readers without stealing it visually from a mouse user. */
.main:focus{outline:none}

@media (prefers-reduced-motion:reduce){
  .main.swap-in{animation:none}
  .nav-bar i{transition:none}
}

/* ---------------- live-data prompt ----------------
   Appears when pulse.php reports new data on a page the shell will not
   refresh under the reader (see assets/shell.js). Centred and floating so it
   never displaces content — the whole point is that nothing moves until the
   owner asks it to. It sits above the phone tab bar rather than behind it. */
.live-pill{
  position:fixed;left:50%;bottom:calc(24px + var(--safe));z-index:55;
  transform:translate(-50%,14px);opacity:0;pointer-events:none;
  display:inline-flex;align-items:center;gap:9px;
  padding:11px 18px;border-radius:999px;border:0;
  background:var(--accent);color:var(--on-accent);
  font:inherit;font-size:13.5px;font-weight:700;cursor:pointer;
  box-shadow:0 10px 30px rgba(0,0,0,.45);
  transition:opacity 220ms var(--ease-out),transform 220ms var(--ease-out);
}
.live-pill.on{opacity:1;transform:translate(-50%,0);pointer-events:auto}
.live-pill:active{transform:translate(-50%,0) scale(.97)}
.live-pill .dot{width:8px;height:8px;border-radius:50%;background:var(--on-accent);
  flex-shrink:0;animation:pulse 1.8s ease-in-out infinite}
.live-pill svg{width:15px;height:15px;flex-shrink:0}
@media (max-width:760px){
  /* clear the floating tab bar */
  .live-pill{bottom:calc(88px + var(--safe))}
}
@media (prefers-reduced-motion:reduce){
  .live-pill{transition:none}
  .live-pill .dot{animation:none}
}

/* ---------------- manual refresh ---------------- */
#refreshBtn{transition:transform 120ms var(--ease-out),background 140ms ease}
#refreshBtn.spin svg{animation:spin 700ms linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){
  /* still show it is working, just without the rotation */
  #refreshBtn.spin svg{animation:none;opacity:.45}
}

/* ============================================================
   RIGHT-TO-LEFT (Arabic, Farsi)
   ------------------------------------------------------------
   Driven entirely by dir="rtl" on <html>, so a language change is one
   attribute and no page needs its own RTL variant.

   Only the handful of PHYSICAL properties in this stylesheet are flipped —
   the layout is flexbox and grid, which mirror by themselves. Rewriting the
   whole sheet in logical properties (margin-inline-start and friends) would
   be a bigger change with more ways to regress the LTR layout that is
   already correct and shipping.

   NUMBERS AND MONEY STAY LTR. A total, an invoice number and a timestamp are
   not sentences: reversing them turns 194.321 into something the owner has
   to decode. Latin-script runs inside Arabic text are handled by the
   browser's own bidi algorithm; the explicit rules below are for the cells
   that are pure figures, where there is no surrounding text to anchor them.
   ============================================================ */
[dir="rtl"] .sidebar{border-right:0;border-left:1px solid #232329}
[dir="rtl"] .nav-label{text-align:right}
[dir="rtl"] .nav-count,
[dir="rtl"] .sidebar-foot .out,
[dir="rtl"] .sync .when,
[dir="rtl"] .row-amt,
[dir="rtl"] .row-foot .go,
[dir="rtl"] .sum-row b{margin-left:0;margin-right:auto}
[dir="rtl"] .row-amt{text-align:left}
[dir="rtl"] .searchbar select{padding-right:12px;padding-left:34px}
[dir="rtl"] table.data th{text-align:right}

/* Figures read left-to-right in every language. `unicode-bidi:plaintext`
   lets each cell pick its own direction from its first strong character, so
   a money column stays 194.321 while a name column beside it still flows
   right-to-left. */
[dir="rtl"] table.data td.num,
[dir="rtl"] table.data th.num{text-align:left}
[dir="rtl"] .kpi-val,
[dir="rtl"] .row-amt b,
[dir="rtl"] table.data td.num{direction:ltr;unicode-bidi:plaintext}

/* The progress bar fills from the leading edge whichever way that is. */
[dir="rtl"] .nav-bar i{transform-origin:right center}

/* ---------- Settings: the language chooser ---------- */
.lang-grid{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));
  padding:4px 0 14px}
.lang-opt{position:relative;display:flex;align-items:center;gap:10px;cursor:pointer;
  padding:14px 16px;border:1px solid var(--line);border-radius:12px;
  background:var(--card-2);transition:border-color .15s,background .15s}
.lang-opt:hover{border-color:var(--muted)}
.lang-opt.on{border-color:var(--accent);background:rgba(255,122,24,.08)}
.lang-opt input{position:absolute;opacity:0;pointer-events:none}
.lang-name{font-size:15px;font-weight:700}
/* The code is a tag, so it keeps its own direction inside an RTL row. */
.lang-code{margin-inline-start:auto;font-size:11px;font-weight:700;color:var(--faint);
  letter-spacing:.06em;direction:ltr}
.lang-opt.on .lang-code{color:var(--accent)}
.lang-foot{display:flex;align-items:center;gap:14px;flex-wrap:wrap;
  padding-top:12px;border-top:1px solid var(--line)}
.lang-foot .mini{flex:1;min-width:180px}
.btn-primary{border:0;border-radius:10px;padding:10px 22px;cursor:pointer;
  background:var(--accent);color:#12120f;font-size:13.5px;font-weight:800;
  font-family:inherit}
.btn-primary:hover{filter:brightness(1.08)}

/* A relative time ("11 minutes ago") is an English phrase embedded in Arabic
   chrome. Without isolation the bidi algorithm reorders its leading number
   to the wrong end and it reads "minutes ago 11". Isolation makes the run a
   single opaque unit, which is what it is. */
[dir="rtl"] [data-ago]{unicode-bidi:isolate}
/* Same for anything that is a bare timestamp or figure. */
[dir="rtl"] .when,
[dir="rtl"] .kpi-foot,
[dir="rtl"] .row-foot{unicode-bidi:isolate}

/* ============================================================
   Dashboard: a shift, with what it took and in what
   ------------------------------------------------------------
   Replaces the old "Latest sales" list. Individual tickets are a wall of
   near-identical rows on a busy day; a shift is the unit an owner actually
   thinks in ("how did the morning go?").
   ============================================================ */
.shift-card{background:var(--card);border:1px solid var(--line);border-radius:14px;
  padding:14px 16px;margin-bottom:10px}
.shift-top{display:flex;align-items:flex-start;gap:12px}
.shift-who{min-width:0}
.shift-who b{font-size:14.5px;font-weight:700;display:block}
.shift-meta{font-size:11.5px;color:var(--faint);font-weight:600;
  display:block;margin-top:3px;unicode-bidi:isolate}

/* Which till this shift ran on. A chip rather than another grey line: on a
   multi-till shop it is the field that tells two shift cards apart, and it
   lost that job entirely when it was the first word of the meta line. */
.shift-till{
  display:inline-flex;align-items:center;gap:5px;margin-top:5px;max-width:100%;
  background:var(--card-2);border-radius:999px;padding:3px 9px 3px 7px;
  font-size:11px;font-weight:700;color:var(--muted);
  /* a till may be named in the shop's own script; isolate keeps a Latin
     station id from reordering around Arabic text beside it */
  unicode-bidi:isolate;
}
.shift-till svg{width:12px;height:12px;flex-shrink:0;color:var(--accent)}
.shift-till span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.shift-amt{margin-inline-start:auto;text-align:end;flex-shrink:0}
.shift-amt b{font-size:16px;font-weight:800;display:block;
  direction:ltr;unicode-bidi:plaintext}
.shift-amt span{font-size:11px;color:var(--faint);font-weight:700}

/* The tender split. A grid rather than a table: it has to stay readable at
   phone width, where a four-column table would either scroll or crush. */
.shift-split{display:grid;gap:8px;margin-top:12px;
  grid-template-columns:repeat(auto-fit,minmax(120px,1fr))}
.tender{background:var(--card-2);border-radius:10px;padding:8px 10px;min-width:0}
.tender .tl{display:block;font-size:10.5px;font-weight:700;color:var(--muted);
  text-transform:uppercase;letter-spacing:.04em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tender .tv{display:block;font-size:14px;font-weight:800;margin-top:2px;
  direction:ltr;unicode-bidi:plaintext}
/* A refund is money that went back out, so it is never silently netted away. */
.tender .tr{display:block;font-size:10.5px;font-weight:700;color:var(--red);
  margin-top:1px;direction:ltr;unicode-bidi:plaintext}

.shift-none{margin-top:10px;font-size:12px;color:var(--faint);font-weight:600}
.shift-foot{display:flex;flex-wrap:wrap;gap:14px;margin-top:12px;padding-top:10px;
  border-top:1px solid var(--line);font-size:11.5px;font-weight:600;color:var(--muted)}
.shift-foot b{color:var(--text);font-weight:800;margin-inline-start:4px;
  direction:ltr;unicode-bidi:plaintext}
.shift-foot .good b{color:var(--green)}
.shift-foot .bad b{color:var(--red)}

/* ============================================================
   THE MONTH'S COVERAGE
   ------------------------------------------------------------
   Two bars answering "has this month paid for itself" — one by sales,
   one by profit. See vd_month_coverage() for why both exist.
   ============================================================ */
.cover{margin-bottom:18px}
/* Covered is the good news of the month, so it gets a green edge rather than
   a colour change: recolouring the whole card would fight the accent the rest
   of the dashboard uses for "now". */
.cover.is-done{border-color:rgba(76,217,138,.45)}
.cover-head{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-start;
  justify-content:space-between;margin-bottom:14px}
.cover-t{font-size:15px;font-weight:800}
.cover-s{font-size:12px;font-weight:600;color:var(--muted);margin-top:3px}
.cover.is-done .cover-s{color:var(--green)}

/* The target. Sits to the END of the title on a wide card and drops onto its
   own full-width row on a phone.

   THE BUG THIS FIXES: the block was only `text-align:end`, which aligns
   against whatever width the flex item happens to take. Once the title
   wrapped it onto a second line the item shrank to its content and sat
   wherever the wrap left it — the label above the number, both adrift of
   both edges. `flex:1 0 100%` once wrapped gives it the full row to align
   in, and the label/number then share one baseline instead of stacking. */
.cover-costs{text-align:end;min-width:0;margin-inline-start:auto}
.cover-costs b{display:block;font-size:20px;font-weight:800;line-height:1.2;
  direction:ltr;unicode-bidi:plaintext}
.cover-costs-l{display:block;font-size:10.5px;font-weight:700;color:var(--faint);
  text-transform:uppercase;letter-spacing:.04em;
  direction:ltr;unicode-bidi:plaintext}

/* Narrow: the costs take their own row, and the label sits BESIDE the figure
   rather than above it — a stacked label reads as a heading for the bars
   that follow, which is what made the number look orphaned. */
@media (max-width:560px){
  /* flex-wrap:wrap is LOAD-BEARING, not tidying. The "due so far" footnote
     below asks for `flex:1 0 100%` to get a row of its own, and a 100% basis
     in a NOWRAP row cannot start a new line — it just becomes a third column
     322px wide. That pushed the card to 603px inside a 390px phone, and
     because the overflow was on the page rather than in a scroller, the
     WHOLE dashboard scrolled sideways: the nav, the till card and every bar
     dragged left, with the footnote stranded out in the black. */
  .cover-costs{flex:1 0 100%;display:flex;flex-wrap:wrap;align-items:baseline;
    justify-content:space-between;gap:10px;text-align:start;
    padding-top:10px;border-top:1px solid var(--line)}
  /* The label must not wrap. Left to itself "THIS MONTH'S COSTS" broke
     onto three lines at 390px and shouldered the figure out of its row —
     the crowding this whole media query exists to prevent. It keeps its
     one line and the number takes the space that is left. */
  .cover-costs-l{margin:0;white-space:nowrap}
  .cover-costs b{font-size:22px;margin-inline-start:auto}
  /* A "due so far" line is a footnote to the figure, not a third column —
     push it under the number on its own row.

     AND LET IT WRAP AGAIN. The nowrap above exists for the SHORT heading
     label; inherited by this one it was a full sentence that could not
     break, so "583.500 due so far" held the row open at 322px inside a
     390px phone and the WHOLE PAGE scrolled sideways ~213px — the card,
     the nav and every other row dragged with it. It is a footnote on its
     own row with nothing beside it to crowd, so it has no reason to
     nowrap. min-width:0 lets it actually shrink inside the flex row. */
  .cover-costs-l + b + .cover-costs-l{flex:1 0 100%;text-align:end;
    white-space:normal;min-width:0}
}

/* ---- what the target is made of -------------------------------------
   Collapsed by default; see the comment in dashboard.php for why this is a
   native <details> rather than a scripted panel. */
.cover-break{margin:12px 0 16px}
.cover-break summary{list-style:none;cursor:pointer;display:flex;
  align-items:center;gap:8px;font-size:11.5px;font-weight:700;
  color:var(--muted);padding:7px 10px;border-radius:9px;
  background:var(--inset);border:1px solid transparent}
.cover-break summary::-webkit-details-marker{display:none}
.cover-break summary:hover{color:var(--text);border-color:var(--line)}
.cover-break summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* The chevron is drawn, not a glyph, so it mirrors with the writing
   direction instead of pointing the wrong way in Arabic. */
.cover-break summary::before{content:"";width:0;height:0;flex:none;
  border-inline-start:5px solid currentColor;
  border-top:4px solid transparent;border-bottom:4px solid transparent;
  transition:transform 160ms var(--ease-io);transform-origin:30% 50%}
/* Open points DOWN in both scripts — but which way that is depends on which
   way the closed triangle points, and border-inline-start makes that mirror
   with the writing direction. Closed points right in LTR, so +90deg brings
   it down; closed points LEFT in RTL, so the same +90deg would swing it UP.
   Hence the sign flips with direction rather than the glyph. */
.cover-break[open] summary::before{transform:rotate(90deg)}
[dir="rtl"] .cover-break[open] summary::before{transform:rotate(-90deg)}
.cover-break-t{flex:1}
.cover-break-n{font-variant-numeric:tabular-nums;color:var(--faint)}

.cover-items{list-style:none;margin:10px 0 0;padding:0}
.cover-items li{display:flex;align-items:center;gap:11px;
  padding:9px 11px;border-radius:11px;
  background:var(--card-2);border:1px solid transparent}
.cover-items li + li{margin-top:6px}
/* Not zebra striping. Every row gets the same panel so the eye reads a list
   of equals; the SIZE of a cost is carried by its bar and its figure, which
   is information, where an alternating background is only decoration. */
.cover-items li.is-spend{border-color:var(--line);background:transparent}

/* The leading glyph, sized and shaped like .pill-ic on the sales rows so the
   two lists look like they belong to one panel. `color` is set inline per row
   and the SVG strokes with currentColor, so one rule tints every variant. */
.cover-ic{width:34px;height:34px;border-radius:10px;flex:none;
  display:grid;place-items:center;background:var(--card);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent)}
@supports not (color: color-mix(in srgb, red 50%, blue)){
  .cover-ic{box-shadow:inset 0 0 0 1px var(--line)}
}
.cover-ic svg{width:17px;height:17px;display:block}

.cover-item-b{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
.cover-item-n{font-size:12.5px;font-weight:700;line-height:1.25;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cover-item-m{font-size:10.5px;font-weight:600;color:var(--faint);
  display:flex;align-items:center;gap:7px;line-height:1.2;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* capitalize the CATEGORY only. Applied to the whole meta row it also hit
   the spending line's "2 entries", which is a count, not a name. */
.cover-item-m em{font-style:normal;text-transform:capitalize}
.cover-items li.is-spend .cover-item-m em{text-transform:none}
/* "due 28" earns its own colour: it is the one thing in the row that says
   this money has NOT left the shop yet. */
.cover-when{color:var(--muted);
  padding:1px 6px;border-radius:999px;background:var(--inset);
  direction:ltr;unicode-bidi:plaintext}

/* Proportion. Deliberately thin and low-contrast — it ranks the costs at a
   glance without competing with the two coverage bars below, which are the
   card's actual verdict. */
/* Full row width, under the name and its meta — a bar that stops where the
   text happens to end reads as an underline on the words rather than as a
   measure of the row, which is how the first cut of this looked. */
.cover-item-bar{display:block;width:100%;height:3px;border-radius:999px;
  background:var(--inset);overflow:hidden;margin-top:3px}
.cover-item-bar i{display:block;height:100%;border-radius:999px;opacity:.55}

/* The UI face, not the mono one — the same call the hero amount makes.
   The mono stack resolves to a slashed-zero face (Consolas on Windows), and
   "400.000" is four zeros in a row: at this size their slashes line up into
   what reads as a line struck THROUGH the amount, i.e. as cancelled, which
   is the opposite of "not charged yet". font-feature-settings:"zero" 0 does
   not fix it — the face ignores it, as a 6x capture of the rendered row
   confirmed. Changing the font is what actually works, and tabular-nums
   still keeps the column aligned. */
.cover-item-a{flex:none;font-size:13px;font-weight:800;
  font-family:var(--ui);font-variant-numeric:tabular-nums;
  letter-spacing:-.01em;
  direction:ltr;unicode-bidi:plaintext}

/* A cost that has not fallen due yet is dimmed to say "not yet", never
   hidden, or the rows would not sum to the total. The amount keeps more
   contrast than the rest: it is still real money the month owes. */
.cover-items li.is-later{opacity:.72}
.cover-items li.is-later .cover-item-a{opacity:.85}

.cover-items-sum{display:flex;flex-wrap:wrap;gap:14px;justify-content:flex-end;
  margin-top:10px;padding-top:9px;border-top:1px solid var(--line);
  font-size:11px;font-weight:700;color:var(--muted)}
.cover-items-sum b{margin-inline-start:5px;color:var(--text);
  font-family:var(--ui);font-variant-numeric:tabular-nums;
  direction:ltr;unicode-bidi:plaintext}

.cover-bar + .cover-bar{margin-top:12px}
.cover-bar-l{display:flex;justify-content:space-between;align-items:baseline;
  gap:10px;font-size:11.5px;font-weight:700;color:var(--muted)}
.cover-bar-l b{font-size:14px;font-weight:800;color:var(--text);
  direction:ltr;unicode-bidi:plaintext}
.cover-track{height:10px;border-radius:999px;background:var(--inset);
  overflow:hidden;margin:6px 0 5px}
/* The fill grows from the reading edge, so an RTL panel fills right-to-left
   without a second rule. */
.cover-track i{display:block;height:100%;border-radius:999px;
  transition:width .5s var(--ease-out)}
/* Sales is the softer, larger figure; profit is the one that counts, so it
   takes the accent. Distinct hues rather than two shades of one colour —
   at 10px a lightness difference alone is not reliably visible. */
/* The fill grows from zero once, on arrival. assets/shell.js parks the width
   at 0 and hands back the real one a frame later; the duration and easing
   live here so a reduced-motion reader gets the finished bar instantly via
   the global override at the foot of this file. */
.cover-track i{transition:width 1100ms var(--ease-io)}
.cover-track i.s{background:linear-gradient(90deg,var(--blue),#6cc9f5)}
.cover-track i.p{background:linear-gradient(90deg,var(--accent),#ffa25c)}
.cover.is-done .cover-track i.p{background:linear-gradient(90deg,var(--green),#7fe6ab)}
.cover-bar-f{display:flex;justify-content:space-between;gap:10px;
  font-size:11px;font-weight:600;color:var(--faint)}
.cover-bar-f .num{color:var(--muted);font-weight:700;
  direction:ltr;unicode-bidi:plaintext}

.cover-note{margin-top:12px;padding-top:10px;border-top:1px solid var(--line);
  font-size:11.5px;font-weight:600;color:var(--muted)}
.cover.is-empty .cover-s{margin-top:6px;max-width:60ch}

/* ============================================================
   PRODUCT PICTURES  (panel/products.php)
   ------------------------------------------------------------
   The thumbnail sits exactly where the row's glyph does, so a
   catalogue with only some pictures still lines up: same 40px box,
   same radius, same flex-shrink. It is a <button> and not a <div>
   because tapping it opens the full-size view — a keyboard and a
   screen reader both need that to be a real control.
   ============================================================ */
.p-thumb{width:40px;height:40px;border-radius:12px;background:var(--card-2);
  flex-shrink:0;overflow:hidden;padding:0;border:1px solid var(--line);
  cursor:zoom-in;display:block;line-height:0}
.p-thumb img{width:100%;height:100%;object-fit:cover;display:block}
.p-thumb:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* PRESS FEEDBACK. On a phone the picture takes a moment to decode before it
   opens, and with no response to the press at all that gap reads as a missed
   tap — so the owner taps again. The dip is the acknowledgement: it happens
   on touchdown, not on the click, so it is immediate however long the decode
   takes.

   Two different durations on purpose. Pressing IN is near-instant (60ms), so
   the finger feels it land; releasing eases back over 200ms, which reads as
   the button relaxing rather than snapping. Equal timings feel mechanical.

   `transform` only — a compositor property, so this stays smooth on the
   cheap tablets these shops use, and it cannot disturb the row's layout. */
.p-thumb{transition:transform 200ms var(--ease-out)}
.p-thumb:active{transform:scale(.9);transition-duration:60ms}
/* A reader who asked for less motion still gets an acknowledgement — just a
   tint instead of movement. */
@media (prefers-reduced-motion:reduce){
  .p-thumb{transition:none}
  .p-thumb:active{transform:none;border-color:var(--accent)}
}

/* How old the stock figure is. Muted and small on purpose: it qualifies
   the number above it and must never compete with it. */
.p-fresh{display:block;font-size:10.5px;font-weight:600;color:var(--muted);
  margin-top:2px;white-space:nowrap}

/* The full-screen view. `dialog` rather than a hand-rolled overlay, so
   Escape, focus trapping and the top layer are the browser's job and
   cannot be got wrong here. */
/* ---------------- barcode scanner sheet ---------------- */
#p-scan{
  border:0;padding:0;background:#000;color:#fff;
  width:100vw;max-width:100vw;height:100dvh;max-height:100dvh;
}
#p-scan::backdrop{background:#000}
.p-scan-wrap{position:relative;width:100%;height:100%;overflow:hidden}
/* object-fit:cover so the preview fills the phone instead of letterboxing —
   the decoder reads the full frame regardless of how it is displayed. */
#p-scan-v{width:100%;height:100%;object-fit:cover;display:block;background:#000}
/* The aiming frame. Decoration only: cropping the DECODE to this box made a
   slightly-off aim return nothing with no explanation, so the box guides the
   eye while the decoder still reads the whole frame. */
.p-scan-box{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:min(78vw,340px);height:min(42vw,190px);
  border:2px solid rgba(255,255,255,.9);border-radius:14px;
  box-shadow:0 0 0 100vmax rgba(0,0,0,.45);
}
.p-scan-msg{
  position:absolute;left:0;right:0;
  /* Clear of the home indicator on a notched phone. */
  bottom:calc(28px + var(--safe));
  text-align:center;font-size:14px;font-weight:600;
  text-shadow:0 1px 3px rgba(0,0,0,.9);padding-inline:20px;
}
.p-scan-x{
  position:absolute;top:calc(12px + var(--safe-top));inset-inline-end:12px;
  width:42px;height:42px;border-radius:50%;
  background:rgba(0,0,0,.55);color:#fff;font-size:26px;line-height:1;
  display:grid;place-items:center;
}

#p-lightbox{border:0;padding:0;background:transparent;max-width:100vw;
  max-height:100vh;width:100%;height:100%;overflow:hidden;
  /* --lb-dim is written by the drag in shell.js: 1 while the picture is
     at rest, falling toward 0 as it is pulled away, so the backdrop
     thins with the gesture and the drag previews its own outcome. */
  --lb-dim:1}
/* The backdrop FADES rather than appearing. A hard cut under a picture
   that is growing out of the row makes the two look unrelated — the point
   of the shared-element transition is that they are the same object.

   ::backdrop cannot be transitioned from `display:none`, so the opacity is
   animated instead and `.is-open` (added by shell.js one frame after
   showModal) is what starts it. */
#p-lightbox::backdrop{
  background:rgba(0,0,0,.88);
  opacity:0;transition:opacity 260ms cubic-bezier(.2,.8,.25,1);
}
#p-lightbox.is-open::backdrop{opacity:var(--lb-dim,1)}
.p-lb-wrap{width:100%;height:100%;display:grid;place-items:center;
  padding:24px;box-sizing:border-box;cursor:zoom-out}
#p-lightbox img{max-width:100%;max-height:calc(100vh - 96px);
  object-fit:contain;border-radius:10px;display:block;
  /* touch-action:none because the SCRIPT owns every gesture here — drag to
     dismiss, one-finger pan, two-finger pinch. Anything less and the browser
     would claim the pinch for its own page zoom, which shell.js deliberately
     blocks (installed on iOS there is no chrome to reset it, so a stuck 2x
     has no way out). Ours always has a way out: closing resets it, and
     releasing below fit snaps back.

     user-drag and user-select off so a long press does not offer to save the
     image mid-gesture. */
  touch-action:none;-webkit-user-drag:none;user-select:none;
  will-change:transform,opacity;
}
/* The cursor is set inline by shell.js: 'grab' only once the picture is
   actually zoomed, because at fit a drag dismisses rather than pans and a
   grab cursor would promise the wrong thing. */
#p-lightbox img:active{cursor:grabbing}
/* The X and the caption fade with the picture; shell.js sets their opacity
   inline during the flight, and this carries them back. */
.p-lb-x,.p-lb-name{transition:opacity 160ms ease}
/* A reader who has asked for less motion gets the picture with no flight
   and no zoom — a full-screen zoom is exactly the motion that triggers
   vestibular symptoms. shell.js skips the FLIP for the same setting. */
@media (prefers-reduced-motion:reduce){
  #p-lightbox::backdrop{transition:none}
  #p-lightbox img{will-change:auto}
}
.p-lb-name{position:fixed;left:0;right:0;bottom:0;padding:14px 18px;
  text-align:center;font-size:13px;font-weight:700;color:#fff;
  background:linear-gradient(to top,rgba(0,0,0,.75),transparent)}
.p-lb-x{position:fixed;top:12px;inset-inline-end:12px;width:40px;height:40px;
  border-radius:50%;border:0;background:rgba(255,255,255,.14);color:#fff;
  font-size:22px;line-height:1;cursor:pointer}

/* ============================================================
   Tills & sync: what each till actually sent
   ------------------------------------------------------------
   A LIST, not a table. The rows are ragged by nature — one trip carries
   three products, the next twelve sales and a picture — so a table would
   spend most of its width on empty columns for the kinds that did not
   move that trip.
   ============================================================ */
.act-sum{padding:10px 14px;border-bottom:1px solid var(--line);
  font-size:12.5px;color:var(--muted)}
.act-sum b{color:var(--text);font-weight:600}
.act-row{display:flex;flex-wrap:wrap;gap:12px;align-items:baseline;padding:9px 14px;
  border-bottom:1px solid var(--line);font-size:13px;line-height:1.5}
.act-row:last-child{border-bottom:0}
/* Fixed-width and monospaced so the descriptions line up down the page:
   a ragged left edge makes a list this dense unreadable. */
.act-when{flex:0 0 116px;font-size:11.5px;color:var(--muted);
  font-variant-numeric:tabular-nums}
.act-what{flex:1 1 auto;min-width:0;color:var(--text);
  overflow-wrap:anywhere;white-space:normal}
.act-till{flex:0 1 auto;min-width:0;font-size:11.5px;color:var(--muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.act-row.bad .act-what{color:var(--red)}
/* A timestamp is a number: it must read left-to-right in Arabic and
   Persian too, or the minutes land before the hour. The descriptions
   beside it are ordinary prose and are deliberately NOT forced — they
   translate and should mirror with the rest of the page. */
[dir="rtl"] .act-when{direction:ltr;unicode-bidi:plaintext;text-align:start}
/* "3 new products" is a number leading a translated phrase. Isolated for
   the same reason as [data-ago] above: without it the count is dragged to
   the wrong end of its own sentence. */
[dir="rtl"] .act-what,
[dir="rtl"] .act-sum{unicode-bidi:isolate}
@media (max-width:640px){
  /* 116px of clock plus a description plus a till name does not fit at
     360px, and the description is what is being read. */
  .act-row{flex-wrap:wrap;gap:2px 10px}
  .act-what{flex:1 0 100%;order:2}
  .act-when,.act-till{order:1}
  .act-till{margin-inline-start:auto}
}

/* ============================================================
   Product master: sticky category pills + lazy loading
   ============================================================ */
/* The chips follow the scroll so a long catalogue can be re-filtered
   without going back to the top. The page scrolls on <body>, so sticky
   resolves against the viewport and needs no scroll container of its own.

   The negative inline margin plus matching padding lets the strip's
   background reach the full width of .main while the chips stay aligned
   with everything else — without it a sticky bar shows a sliver of the
   rows sliding past on either side. */
.filters.is-sticky{
  position:sticky; top:0; z-index:30;
  margin-inline:-30px; padding:10px 30px;
  background:var(--bg); backdrop-filter:saturate(140%) blur(6px);
  /* Only once it has actually stuck — a permanent line under a strip
     sitting in the flow looks like a stray border. Driven by the script,
     which is also what sets .is-stuck. */
  border-bottom:1px solid transparent;
  transition:border-color .15s ease, background .15s ease;
}
.filters.is-sticky.is-stuck{ border-bottom-color:var(--line); }
/* Many categories must not become a wall of chips: the strip scrolls
   sideways on its own rather than pushing the list down the page. */
.filters.is-sticky{ flex-wrap:nowrap; overflow-x:auto; overflow-y:hidden;
  scrollbar-width:none; -ms-overflow-style:none; }
.filters.is-sticky::-webkit-scrollbar{ display:none; }
.filters.is-sticky .f-chip{ flex:0 0 auto; }
@media (max-width:640px){
  .filters.is-sticky{ margin-inline:-16px; padding-inline:16px; }
}

/* ---- the shimmer shown while the next page is in flight ----
   Three placeholder cards, shaped like the real ones, so the list does
   not jump when they are replaced. A spinner would say "something is
   happening"; this says "more rows, about this big, right here". */
#p-more{ display:grid; gap:12px; }
#p-more[hidden]{ display:none; }
.p-skel{ display:flex; gap:14px; align-items:center;
  background:var(--card); border:1px solid var(--line);
  border-radius:var(--r-card,14px); padding:16px; }
.p-skel-ic{ width:44px; height:44px; border-radius:12px; flex:0 0 auto; }
.p-skel-tx{ flex:1; min-width:0; display:grid; gap:8px; }
.p-skel-l{ height:10px; border-radius:6px; }
.p-skel-l.w60{ width:60%; } .p-skel-l.w40{ width:40%; } .p-skel-l.w30{ width:30%; }
/* One gradient swept across the placeholders. Painted on a background
   rather than with a moving overlay element, so nothing is added to the
   layout and nothing can be left behind if the animation is interrupted. */
.p-skel-ic,.p-skel-l{
  background:linear-gradient(90deg,
    var(--line) 25%, var(--card-2,#26262c) 37%, var(--line) 63%);
  background-size:400% 100%;
  animation:pSkel 1.4s ease-in-out infinite;
}
@keyframes pSkel{ 0%{background-position:100% 50%} 100%{background-position:0 50%} }
/* A shimmer is decoration: someone who asked for less motion gets a
   plain block, which still reserves the space and still reads as
   "loading". */
@media (prefers-reduced-motion:reduce){
  .p-skel-ic,.p-skel-l{ animation:none; background:var(--line); }
}
/* Appended rows fade in so the page does not jolt as they land. */
.p-fade-in{ animation:pFade .28s var(--ease-out,ease) both; }
@keyframes pFade{ from{opacity:0; transform:translateY(6px)} to{opacity:1; transform:none} }
@media (prefers-reduced-motion:reduce){ .p-fade-in{ animation:none; } }
/* The end of a long list deserves a full stop. */
.p-end{ text-align:center; color:var(--muted); font-size:12.5px; padding:18px 0 4px; }

/* ===== COMPANY FILES (files.php) ===== */
.cf-chips{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}
.cf-chip{display:inline-flex;align-items:center;gap:8px;padding:9px 14px;border-radius:999px;font-size:13px;font-weight:700;border:1px solid transparent}
.cf-chip svg{width:16px;height:16px}
.cf-chip.bad{background:var(--red-soft);color:var(--red)}
.cf-chip.warn{background:var(--amber-soft);color:var(--amber)}
.cf-chip.on{border-color:currentColor}
.cf-filters{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px}
/* Its own field, NOT .searchbar: that class expects Product master's markup,
   and borrowing it left this icon unsized — a magnifier the size of the page. */
.cf-search{position:relative;flex:1 1 240px;min-width:0}
.cf-search svg{position:absolute;inset-inline-start:14px;top:50%;transform:translateY(-50%);width:17px;height:17px;color:var(--muted);pointer-events:none}
.cf-search input{width:100%;height:42px;border-radius:999px;background:var(--card);color:var(--text);border:1px solid var(--line);padding-inline:40px 14px;font:inherit;font-size:13.5px}
.cf-search input:focus{outline:none;border-color:var(--accent)}
.cf-search input::placeholder{color:var(--faint)}
.cf-filters select{height:42px;padding:0 12px;border-radius:999px;background:var(--card);color:var(--text);border:1px solid var(--line);font:inherit;font-size:13px;max-width:100%}
.cf-clear{font-size:13px;font-weight:700;color:var(--accent);padding:8px 4px}
.cf-pills{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
.cf-pills .chip b{margin-inline-start:4px;opacity:.75;font-variant-numeric:tabular-nums}
.cf-pills .chip.on{background:var(--accent);color:var(--on-accent);border-color:var(--accent)}
.cf-count{font-size:12.5px;color:var(--muted);margin:4px 2px 14px;font-variant-numeric:tabular-nums}
.cf-fold{background:var(--card);border-radius:var(--r-md);margin-bottom:12px;overflow:hidden}
.cf-fold>summary{list-style:none;display:flex;align-items:center;gap:8px;padding:14px 16px;cursor:pointer;flex-wrap:wrap}
.cf-fold>summary::-webkit-details-marker{display:none}
.cf-chev{width:16px;height:16px;color:var(--muted);transition:transform .18s var(--ease-out);flex-shrink:0}
.cf-fold[open] .cf-chev{transform:rotate(90deg)}
html[dir="rtl"] .cf-fold:not([open]) .cf-chev{transform:rotate(180deg)}
.cf-fold-ico{width:19px;height:19px;color:var(--accent);flex-shrink:0}
.cf-fold-name{font-weight:800;font-size:14.5px;overflow-wrap:anywhere}
.tag.cf-cat{background:var(--accent-soft);color:var(--accent)}
.cf-fold-desc{margin:-6px 16px 10px;color:var(--muted);font-size:12.5px}
.cf-grid{display:grid;gap:10px;padding:0 12px 14px;grid-template-columns:repeat(auto-fill,minmax(270px,1fr))}
.cf-none{grid-column:1/-1;color:var(--muted);font-size:13px;padding:6px 4px}
.cf-card{background:var(--inset);border:1px solid var(--line);border-radius:var(--r-sm);padding:12px;display:flex;flex-direction:column;gap:8px;min-width:0}
.cf-card-top{display:flex;gap:10px;align-items:flex-start;min-width:0}
.cf-card-tx{min-width:0}
.cf-type{flex-shrink:0;width:40px;height:46px;border-radius:9px;display:grid;place-items:center;font-size:10.5px;font-weight:900;color:#fff}
.cf-type.pdf{background:#E5484D}.cf-type.doc{background:#3E7BFA}.cf-type.xls{background:#1FA463}
.cf-type.img{background:#8E5CF7}.cf-type.txt{background:#63636c}
.cf-name{font-weight:700;font-size:13.5px;overflow-wrap:anywhere;line-height:1.35}
.cf-meta{display:block;font-size:11.5px;color:var(--muted);margin-top:3px;font-variant-numeric:tabular-nums}
.cf-exp{align-self:flex-start;padding:3px 9px;border-radius:999px;font-size:11.5px;font-weight:800}
.cf-exp.expired{background:var(--red-soft);color:var(--red)}
.cf-exp.due{background:var(--amber-soft);color:var(--amber)}
.cf-exp.ok{background:var(--green-soft);color:var(--green)}
.cf-exp.none{background:var(--card-2,var(--card));color:var(--muted)}
.cf-facts{display:flex;gap:4px 12px;flex-wrap:wrap;font-size:11.5px;color:var(--muted)}
.cf-facts b{color:var(--text);font-weight:600;unicode-bidi:isolate}
.cf-acts{display:flex;gap:6px;flex-wrap:wrap;margin-top:auto;padding-top:8px;border-top:1px solid var(--line)}
.cf-btn{display:inline-flex;align-items:center;justify-content:center;padding:8px 13px;border-radius:999px;background:var(--card);color:var(--text);border:1px solid var(--line);font:inherit;font-size:12.5px;font-weight:700;cursor:pointer;text-decoration:none}
.cf-btn.accent{background:var(--accent);color:var(--on-accent);border-color:var(--accent)}
.cf-btn.danger{color:var(--red);border-color:var(--red);background:transparent}
.cf-btn:disabled{opacity:.5}
.cf-wait{font-size:12px;color:var(--muted);padding:6px 2px}
/* dialogs: native <dialog>, so Escape, focus and stacking are the browser's */
/* margin:auto is what centres a modal <dialog>; the panel's global reset
   (*{margin:0}) had pinned both dialogs to the top-left corner. */
.cf-dlg{margin:auto;border:0;padding:0;background:var(--card);color:var(--text);border-radius:var(--r-md);box-shadow:0 30px 80px rgba(0,0,0,.6);max-width:min(1000px,94vw);width:94vw;max-height:92vh}
.cf-dlg::backdrop{background:rgba(0,0,0,.7)}
.cf-dlg-head{display:flex;align-items:center;gap:8px;padding:12px 12px 12px 16px;border-bottom:1px solid var(--line)}
.cf-dlg-head b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14.5px}
.cf-x{width:36px;height:36px;border-radius:999px;background:var(--inset);color:var(--text);border:1px solid var(--line);font-size:20px;line-height:1;cursor:pointer}
.cf-pv-body{height:min(80vh,900px);display:flex;align-items:center;justify-content:center;background:var(--inset)}
.cf-pv-body img{width:100%;height:100%;object-fit:contain}
.cf-pv-body iframe{width:100%;height:100%;border:0;background:#fff}
.cf-share{max-width:520px}
.cf-sh-body{padding:14px 16px 18px;display:flex;flex-direction:column;gap:12px}
.cf-big{display:flex;align-items:center;gap:12px;padding:14px;border-radius:var(--r-sm);background:var(--accent);color:var(--on-accent);border:0;text-align:start;cursor:pointer;font:inherit}
.cf-big[hidden]{display:none}
.cf-big svg{width:22px;height:22px;flex-shrink:0}
.cf-big b{display:block;font-size:14.5px}
.cf-big small{display:block;font-size:12px;opacity:.8}
.cf-sh-sep{font-size:12px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.04em}
.cf-sh-make,.cf-sh-out{display:flex;gap:8px;flex-wrap:wrap}
.cf-sh-out[hidden]{display:none}
.cf-sh-make select{flex:1 1 auto;height:40px;border-radius:999px;background:var(--inset);color:var(--text);border:1px solid var(--line);padding:0 12px;font:inherit;font-size:13px}
.cf-sh-out input{flex:1 1 220px;min-width:0;height:40px;border-radius:999px;background:var(--inset);color:var(--text);border:1px solid var(--line);padding:0 14px;font:inherit;font-size:12.5px}
.cf-sh-note{font-size:12px;color:var(--muted);line-height:1.5}
.cf-sh-note.flash{color:var(--green);font-weight:700}
.cf-sh-list{display:flex;flex-direction:column;gap:6px;font-size:12.5px;color:var(--muted)}
.cf-sh-row{display:flex;align-items:center;gap:8px;justify-content:space-between;padding:8px 10px;border-radius:var(--r-sm);background:var(--inset)}
.cf-sh-row span{min-width:0}
@media (max-width:760px){
  .cf-grid{grid-template-columns:1fr;padding:0 10px 12px}
  .cf-filters select{flex:1 1 140px}
  .cf-dlg{width:100vw;max-width:100vw;max-height:100dvh;height:100dvh;border-radius:0;margin:0}
  .cf-pv-body{height:calc(100dvh - 62px)}
  .cf-share{height:auto;max-height:90dvh;margin:auto 0 0;border-radius:var(--r-md) var(--r-md) 0 0}
}
